diff --git a/ci/Jenkinsfile.android b/ci/Jenkinsfile.android index 9ac2919e66..0596094708 100644 --- a/ci/Jenkinsfile.android +++ b/ci/Jenkinsfile.android @@ -6,7 +6,7 @@ pipeline { options { timestamps() /* Prevent Jenkins jobs from running forever */ - timeout(time: 20, unit: 'MINUTES') + timeout(time: 30, unit: 'MINUTES') /* Limit builds retained */ buildDiscarder(logRotator( numToKeepStr: '10', diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index b6e1115a18..1f5f4ad2d4 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -1,7 +1,10 @@ (ns status-im.chat.models.input (:require [clojure.string :as string] [goog.object :as object] + [re-frame.core :as re-frame] + [taoensso.timbre :as log] [status-im.chat.constants :as chat.constants] + [status-im.ethereum.json-rpc :as json-rpc] [status-im.chat.models :as chat] [status-im.chat.models.message :as chat.message] [status-im.chat.models.message-content :as message-content] @@ -112,9 +115,25 @@ {:db (-> db (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] message) + (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] nil) (update-in [:chat/inputs current-chat-id :metadata] dissoc :sending-image))}))) +(fx/defn edit-message + "Sets reference to previous chat message and focuses on input" + {:events [:chat.ui/edit-message]} + [{:keys [db] :as cofx} message] + (let [current-chat-id (:current-chat-id db) + + text (get-in message [:content :text])] + {:dispatch [:chat.ui.input/set-chat-input-text text current-chat-id] + :db (-> db + (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] + message) + (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-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]} @@ -152,16 +171,28 @@ (fx/merge cofx {:db (-> db (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))} (set-chat-input-text nil current-chat-id))) +(fx/defn cancel-message-edit + "Cancels stage message edit" + {:events [:chat.ui/cancel-message-edit]} + [{:keys [db] :as cofx}] + (let [current-chat-id (:current-chat-id db)] + (fx/merge cofx + {:set-input-text [current-chat-id ""]} + (clean-input current-chat-id) + (mentions/clear-mentions) + (mentions/clear-cursor)))) + (fx/defn send-messages [{:keys [db] :as cofx} input-text current-chat-id] (let [image-messages (build-image-messages cofx current-chat-id) text-message (build-text-message cofx input-text current-chat-id) messages (keep identity (conj image-messages text-message))] (when (seq messages) (fx/merge cofx - (clean-input cofx (:current-chat-id db)) + (clean-input (:current-chat-id db)) (process-cooldown) (chat.message/send-messages messages))))) @@ -197,14 +228,28 @@ :pack pack} :text (i18n/label :t/update-to-see-sticker)}))) +(fx/defn send-edited-message [{:keys [db] :as cofx} text {:keys [message-id]}] + (fx/merge + cofx + {::json-rpc/call [{:method "wakuext_editMessage" + :params [{:id message-id :text text}] + :js-response true + :on-error #(log/error "failed to edit message " %) + :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]} + (clean-input (:current-chat-id db)) + (process-cooldown))) + (fx/defn send-current-message "Sends message from current chat input" {:events [:chat.ui/send-current-message]} [{{:keys [current-chat-id] :as db} :db :as cofx}] - (let [{:keys [input-text]} (get-in db [:chat/inputs current-chat-id]) + (let [{:keys [input-text metadata]} (get-in db [:chat/inputs current-chat-id]) + editing-message (:editing-message metadata) input-text-with-mentions (mentions/check-mentions cofx input-text)] (fx/merge cofx - (send-messages input-text-with-mentions current-chat-id) + (if editing-message + (send-edited-message input-text-with-mentions editing-message) + (send-messages input-text-with-mentions current-chat-id)) (mentions/clear-mentions) (mentions/clear-cursor)))) diff --git a/src/status_im/chat/models/mentions.cljs b/src/status_im/chat/models/mentions.cljs index c406c2566f..5b56a78ffb 100644 --- a/src/status_im/chat/models/mentions.cljs +++ b/src/status_im/chat/models/mentions.cljs @@ -374,7 +374,7 @@ (< to+1 start))) entry - ;; starts before change intersects with it + ;; starts before change intersects with it (and (< from start) (>= to+1 start)) {:from from @@ -516,8 +516,8 @@ {:keys [new-text at-idxs start end] :as state} (get-in db [:chats/mentions chat-id :mentions]) new-text (or new-text text)] - (log/debug "[mentions] calculate suggestions" - "state" state) + (log/info "[mentions] calculate suggestions" + "state" state) (if-not (seq at-idxs) {:db (-> db (assoc-in [:chats/mention-suggestions chat-id] nil) @@ -646,3 +646,144 @@ (update user :searchable-phrases (fnil concat []) new-words)))) user [alias name nickname])) + +(defn is-valid-terminating-character? [c] + (case c + "\t" true ; tab + "\n" true ; newline + "\f" true ; new page + "\r" true ; carriage return + " " true ; whitespace + "," true + "." true + ":" true + ";" true + false)) + +(def hex-reg #"[0-9a-f]") + +(defn is-public-key-character? [c] + (.test hex-reg c)) + +(def mention-length 133) + +(defn ->input-field + "->input-field takes a string with mentions in the @0xpk format + and retuns a list in the format + [{:type :text :text text} {:type :mention :text 0xpk}...]" + [text] + (let [{:keys [text + current-mention-length + current-text + current-mention]} + (reduce (fn [{:keys [text + current-text + current-mention + current-mention-length]} character] + (let [is-pk-character (is-public-key-character? character) + is-termination-character (is-valid-terminating-character? character)] + (cond + ;; It's a valid mention. + ;; Add any text that is before if present + ;; and add the mention. + ;; Set the text to the new termination character + (and (= current-mention-length mention-length) + is-termination-character) + {:current-mention-length 0 + :current-mention "" + :current-text character + :text (cond-> text + (seq current-text) + (conj [:text current-text]) + :always + (conj [:mention current-mention]))} + + + ;; It's either a pk character, or the `x` in the pk + ;; in this case add the text to the mention and continue + + + (or + (and is-pk-character + (pos? current-mention-length)) + (and (= 2 current-mention-length) + (= "x" character))) + {:current-mention-length (inc current-mention-length) + :current-text current-text + :current-mention (str current-mention character) + :text text} + + + ;; The beginning of a mention, discard the @ sign + ;; and start following a mention + + + (= "@" character) + {:current-mention-length 1 + :current-mention "" + :current-text current-text + :text text} + + ;; Not a mention character, but we were following a mention + ;; discard everything up to know an count as text + (and (not is-pk-character) + (pos? current-mention-length)) + {:current-mention-length 0 + :current-text (str current-text "@" current-mention character) + :current-mention "" + :text text} + + ;; Just a normal text character + :else + {:current-mention-length 0 + :current-mention "" + :current-text (str current-text character) + :text text}))) + {:current-mention-length 0 + :current-text "" + :current-mention "" + :text []} + text)] + ;; Process any remaining mention/text + (cond-> text + (seq current-text) + (conj [:text current-text]) + (= current-mention-length mention-length) + (conj [:mention current-mention])))) + +(defn ->info + "->info convert a input-field representation of mentions to + a db based representation used to indicate where mentions are placed in the + input string" + [m] + (reduce (fn [{:keys [start end at-idxs at-sign-idx mention-end]} [t text]] + (if (= :mention t) + (let [new-mention {:checked? true + :mention? true + :from mention-end + :to (+ start (count text))} + has-previous? (seq at-idxs)] + {:new-text (last text) + :previous-text "" + :start (+ start (count text)) + :end (+ end (count text)) + :at-idxs (cond-> at-idxs + has-previous? + (-> pop + (conj (assoc (peek at-idxs) :next-at-idx mention-end))) + :always + (conj new-mention)) + :at-sign-idx mention-end + :mention-end (+ mention-end (count text))}) + {:new-text (last text) + :previous-text "" + :start (+ start (count text)) + :end (+ end (count text)) + :at-idxs at-idxs + :at-sign-idx at-sign-idx + :mention-end (+ mention-end (count text))})) + {:start -1 + :end -1 + :at-idxs [] + :mention-end 0} + m)) diff --git a/src/status_im/chat/models/mentions_test.cljs b/src/status_im/chat/models/mentions_test.cljs index a0cccadc60..c1b05c681a 100644 --- a/src/status_im/chat/models/mentions_test.cljs +++ b/src/status_im/chat/models/mentions_test.cljs @@ -3,6 +3,60 @@ [clojure.string :as string] [cljs.test :as test :include-macros true])) +(def ->info-input + [[:text "H."] + [:mention + "@helpinghand.eth"] + [:text + " "]]) +(def ->info-expected + {:at-sign-idx 2 + :mention-end 19 + :new-text " " + :previous-text "" + :start 18 + :end 18 + :at-idxs [{:mention? true + :from 2 + :to 17 + :checked? true}]}) + +(test/deftest test->info + (test/testing "->info base case" + (test/is (= ->info-expected (mentions/->info ->info-input))))) + +;; No mention +(def mention-text-1 "parse-text") +(def mention-text-result-1 [[:text "parse-text"]]) + +;; Mention in the middle +(def mention-text-2 "hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he") +(def mention-text-result-2 [[:text "hey "] [:mention "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"] [:text " he"]]) + +;; Mention at the beginning +(def mention-text-3 "@0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he") +(def mention-text-result-3 [[:mention "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"] [:text " he"]]) + +;; Mention at the end +(def mention-text-4 "hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073") +(def mention-text-result-4 [[:text "hey "] [:mention "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"]]) + +;; Invalid mention +(def mention-text-5 "invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073") +(def mention-text-result-5 [[:text "invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"]]) + +(test/deftest test-to-input + (test/testing "only text" + (test/is (= mention-text-result-1 (mentions/->input-field mention-text-1)))) + (test/testing "in the middle" + (test/is (= mention-text-result-2 (mentions/->input-field mention-text-2)))) + (test/testing "at the beginning" + (test/is (= mention-text-result-3 (mentions/->input-field mention-text-3)))) + (test/testing "at the end" + (test/is (= mention-text-result-4 (mentions/->input-field mention-text-4)))) + (test/testing "invalid" + (test/is (= mention-text-result-5 (mentions/->input-field mention-text-5))))) + (test/deftest test-replace-mentions (let [users {"User Number One" {:name "User Number One" diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index d880774fe7..e31cffbcb9 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -18,6 +18,7 @@ (-> message (clojure.set/rename-keys {:id :message-id :whisperTimestamp :whisper-timestamp + :editedAt :edited-at :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 fc874a4323..d16ccd7dce 100644 --- a/src/status_im/ethereum/json_rpc.cljs +++ b/src/status_im/ethereum/json_rpc.cljs @@ -37,6 +37,7 @@ "waku_markTrustedPeer" {} "wakuext_post" {} "wakuext_requestAllHistoricMessages" {} + "wakuext_editMessage" {} "wakuext_fillGaps" {} "wakuext_syncChatFromSyncedFrom" {} "wakuext_createPublicChat" {} diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 888ab69ae9..24dc636a04 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -1074,6 +1074,12 @@ (fn [{:keys [metadata]}] (:responding-to-message metadata))) +(re-frame/reg-sub + :chats/edit-message + :<- [:chats/current-chat-inputs] + (fn [{:keys [metadata]}] + (:editing-message metadata))) + (re-frame/reg-sub :chats/sending-image :<- [:chats/current-chat-inputs] @@ -1095,19 +1101,23 @@ :<- [:current-chat/one-to-one-chat?] :<- [:current-chat/metadata] :<- [:chats/reply-message] - (fn [[disconnected? {:keys [processing]} sending-image mainnet? one-to-one-chat? {:keys [public?]} reply]] + :<- [:chats/edit-message] + (fn [[disconnected? {:keys [processing]} sending-image mainnet? one-to-one-chat? {:keys [public?]} reply edit]] (let [sending-image (seq sending-image)] {:send (and (not (or processing disconnected?))) :stickers (and mainnet? (not sending-image) (not reply)) :image (and (not reply) + (not edit) (not public?)) :extensions (and one-to-one-chat? (or config/commands-enabled? mainnet?) + (not edit) (not reply)) :audio (and (not sending-image) (not reply) + (not edit) (not public?)) :sending-image sending-image}))) diff --git a/src/status_im/ui/screens/chat/components/edit.cljs b/src/status_im/ui/screens/chat/components/edit.cljs new file mode 100644 index 0000000000..3f1bb4fe95 --- /dev/null +++ b/src/status_im/ui/screens/chat/components/edit.cljs @@ -0,0 +1,49 @@ +(ns status-im.ui.screens.chat.components.edit + (: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] + [quo.components.animated.pressable :as pressable] + [status-im.ui.components.icons.icons :as icons] + [status-im.ui.screens.chat.components.style :as styles] + [re-frame.core :as re-frame])) + +(defn input-focus [text-input-ref] + (some-> ^js (quo.react/current-ref text-input-ref) .focus)) + +(defn edit-message [] + [rn/view {:style {:flex-direction :row}} + [rn/view {} + [icons/icon :tiny-icons/tiny-edit {:container-style {:margin-top 5}}]] + [rn/view {:style (styles/reply-content)} + [quo/text {:weight :medium + :number-of-lines 1} + (i18n/label :t/editing-message)]] + [rn/view + [pressable/pressable {:on-press #(re-frame/dispatch [:chat.ui/cancel-message-edit]) + :accessibility-label :cancel-message-reply} + [icons/icon :main-icons/close-circle {:container-style (styles/close-button) + :color (:icon-02 @quo.colors/theme)}]]]]) + +(defn focus-input-on-edit [edit had-edit text-input-ref] + ;;when we show edit we focus input + (when-not (= edit @had-edit) + (reset! had-edit edit) + (when edit + (js/setTimeout #(input-focus text-input-ref) 250)))) + +(defn edit-message-wrapper [edit] + [rn/view {:style {:padding-horizontal 15 + :border-top-width 1 + :border-top-color (:ui-01 @quo.colors/theme) + :padding-vertical 8}} + [edit-message edit]]) + +(defn edit-message-auto-focus-wrapper [text-input-ref] + (let [had-edit (atom nil)] + (fn [] + (let [edit @(re-frame/subscribe [:chats/edit-message])] + (focus-input-on-edit edit had-edit text-input-ref) + (when edit + [edit-message-wrapper]))))) diff --git a/src/status_im/ui/screens/chat/components/input.cljs b/src/status_im/ui/screens/chat/components/input.cljs index 3791431c64..7644a58021 100644 --- a/src/status_im/ui/screens/chat/components/input.cljs +++ b/src/status_im/ui/screens/chat/components/input.cljs @@ -6,7 +6,9 @@ [quo.components.text :as text] [quo.design-system.colors :as colors] [status-im.ui.screens.chat.components.style :as styles] + [status-im.utils.fx :as fx] [status-im.ui.screens.chat.components.reply :as reply] + [status-im.multiaccounts.core :as multiaccounts] [status-im.chat.constants :as chat.constants] [status-im.utils.utils :as utils.utils] [quo.components.animated.pressable :as pressable] @@ -109,6 +111,13 @@ (defonce input-texts (atom {})) (defonce mentions-enabled (reagent/atom {})) +(defonce chat-input-key (reagent/atom 1)) + +(defn force-text-input-update! + "force-text-input-update! forces the + input to re-render, necessary when we are setting value" + [] + (swap! chat-input-key inc)) (defn show-send [{:keys [actions-ref send-ref sticker-ref]}] (quo.react/set-native-props actions-ref #js {:width 0 :left -88}) @@ -165,6 +174,37 @@ (when platform/ios? (re-frame/dispatch [::mentions/calculate-suggestions mentionable-users])))) +(re-frame/reg-fx + :set-input-text + (fn [[chat-id text]] + ;; We enable mentions + (swap! mentions-enabled assoc chat-id true) + (on-text-change text chat-id) + ;; We update the key so that we force a refresh of the text input, as those + ;; are not ratoms + (force-text-input-update!))) + +(fx/defn set-input-text + "Set input text for current-chat. Takes db and input text and cofx + as arguments and returns new fx. Always clear all validation messages." + {:events [:chat.ui.input/set-chat-input-text]} + [{:keys [db] :as cofx} text chat-id] + (let [text-with-mentions (mentions/->input-field text) + contacts (:contacts db) + hydrated-mentions (map (fn [[t mention :as e]] + (if (= t :mention) + [:mention (str "@" (multiaccounts/displayed-name + (or (get contacts mention) + {:public-key mention})))] + e)) text-with-mentions) + info (mentions/->info hydrated-mentions)] + {:set-input-text [chat-id text] + :db + (-> db + (assoc-in [:chats/cursor chat-id] (:mention-end info)) + (assoc-in [:chat/inputs-with-mentions chat-id] hydrated-mentions) + (assoc-in [:chats/mentions chat-id :mentions] info))})) + (defn on-text-input [mentionable-users chat-id args] (let [native-event (.-nativeEvent ^js args) text (.-text ^js native-event) @@ -193,6 +233,7 @@ timeout-id (atom nil) last-text-change (atom nil) mentions-enabled (get @mentions-enabled chat-id)] + [rn/text-input {:style (styles/text-input) :ref (:text-input-ref refs) @@ -281,21 +322,6 @@ (defn on-chat-toolbar-layout [^js ev] (reset! chat-toolbar-height (-> ev .-nativeEvent .-layout .-height))) -(defn focus-input-on-reply [reply had-reply text-input-ref] - ;;when we show reply we focus input - (when-not (= reply @had-reply) - (reset! had-reply reply) - (when reply - (js/setTimeout #(input-focus text-input-ref) 250)))) - -(defn reply-message [text-input-ref] - (let [had-reply (atom nil)] - (fn [] - (let [reply @(re-frame/subscribe [:chats/reply-message])] - (focus-input-on-reply reply had-reply text-input-ref) - (when reply - [reply/reply-message reply]))))) - (defn send-image [] (let [sending-image @(re-frame/subscribe [:chats/sending-image])] (when (seq sending-image) @@ -331,10 +357,9 @@ show-send (or sending-image (seq (get @input-texts chat-id)))] [rn/view {:style (styles/toolbar) :on-layout on-chat-toolbar-layout} - ;;EXTENSIONS and IMAGE buttons + ;;EXTENSIONS and IMAGE buttons [actions extensions image show-send actions-ref active-panel set-active-panel] [rn/view {:style (styles/input-container)} - [reply-message text-input-ref] [send-image] [rn/view {:style styles/input-row} [text-input {:chat-id chat-id @@ -348,17 +373,18 @@ (re-frame/dispatch [:chat.ui/send-current-message]))])] ;;STICKERS and AUDIO buttons - [rn/view {:style (merge {:flex-direction :row} (when show-send {:width 0 :right -100})) - :ref sticker-ref} - (when stickers - [touchable-stickers-icon {:panel :stickers - :accessibility-label :show-stickers-icon - :active active-panel - :input-focus #(input-focus text-input-ref) - :set-active set-active-panel}]) - (when audio - [touchable-audio-icon {:panel :audio - :accessibility-label :show-audio-message-icon - :active active-panel - :input-focus #(input-focus text-input-ref) - :set-active set-active-panel}])]]]])))) + (when-not @(re-frame/subscribe [:chats/edit-message]) + [rn/view {:style (merge {:flex-direction :row} (when show-send {:width 0 :right -100})) + :ref sticker-ref} + (when stickers + [touchable-stickers-icon {:panel :stickers + :accessibility-label :show-stickers-icon + :active active-panel + :input-focus #(input-focus text-input-ref) + :set-active set-active-panel}]) + (when audio + [touchable-audio-icon {:panel :audio + :accessibility-label :show-audio-message-icon + :active active-panel + :input-focus #(input-focus text-input-ref) + :set-active set-active-panel}])])]]])))) diff --git a/src/status_im/ui/screens/chat/components/reply.cljs b/src/status_im/ui/screens/chat/components/reply.cljs index 493dcf4cf0..a61d4ef9c7 100644 --- a/src/status_im/ui/screens/chat/components/reply.cljs +++ b/src/status_im/ui/screens/chat/components/reply.cljs @@ -1,30 +1,34 @@ (ns status-im.ui.screens.chat.components.reply (: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] - [quo.design-system.colors :as colors] [quo.components.animated.pressable :as pressable] [status-im.ui.components.icons.icons :as icons] [status-im.ethereum.stateofus :as stateofus] [status-im.ui.screens.chat.components.style :as styles] [re-frame.core :as re-frame] - [status-im.ui.components.react :as react] [clojure.string :as string])) (def ^:private reply-symbol "↪ ") +(defn input-focus [text-input-ref] + (some-> ^js (quo.react/current-ref text-input-ref) .focus)) + (defn format-author [contact-name] - (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)) + (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 :replying-to {:author author}))) (defn format-reply-author [from username current-public-key] (or (and (= from current-public-key) (str reply-symbol (i18n/label :t/You))) - (format-author (str reply-symbol username)))) + (str reply-symbol (format-author username)))) (defn get-quoted-text-with-mentions [parsed-text] (string/join @@ -43,36 +47,23 @@ literal)) parsed-text))) -(defn reply-message [{:keys [from content]}] +(defn reply-message [{:keys [from]}] (let [contact-name @(re-frame/subscribe [:contacts/contact-name-by-identity from]) - current-public-key @(re-frame/subscribe [:multiaccount/public-key]) - {:keys [image parsed-text]} content] - [rn/view {:style (styles/reply-container false)} + current-public-key @(re-frame/subscribe [:multiaccount/public-key])] + [rn/view {:style {:flex-direction :row}} [rn/view {:style (styles/reply-content)} [quo/text {:weight :medium :number-of-lines 1 - :style {:line-height 18} - :size :small} - (format-reply-author from contact-name current-public-key)] - (if image - [react/image {:style {:width 56 - :height 56 - :background-color :black - :margin-top 2 - :border-radius 4} - :source {:uri image}}] - [quo/text {:size :small - :number-of-lines 1 - :style {:line-height 18}} - (get-quoted-text-with-mentions parsed-text)])] + :style {:line-height 18}} + (format-reply-author from contact-name current-public-key)]] [rn/view [pressable/pressable {:on-press #(re-frame/dispatch [:chat.ui/cancel-message-reply]) :accessibility-label :cancel-message-reply} [icons/icon :main-icons/close-circle {:container-style (styles/close-button) - :color (:icon-01 @colors/theme)}]]]])) + :color (:icon-02 @quo.colors/theme)}]]]])) (defn send-image [images] - [rn/view {:style (styles/reply-container true)} + [rn/view {:style (styles/reply-container-image)} [rn/scroll-view {:horizontal true :style (styles/reply-content)} (for [{:keys [uri]} (vals images)] @@ -86,4 +77,26 @@ [pressable/pressable {:on-press #(re-frame/dispatch [:chat.ui/cancel-sending-image]) :accessibility-label :cancel-send-image} [icons/icon :main-icons/close-circle {:container-style (styles/close-button) - :color colors/white}]]]]) + :color quo.colors/white}]]]]) + +(defn focus-input-on-reply [reply had-reply text-input-ref] + ;;when we show reply we focus input + (when-not (= reply @had-reply) + (reset! had-reply reply) + (when reply + (js/setTimeout #(input-focus text-input-ref) 250)))) + +(defn reply-message-wrapper [reply] + [rn/view {:style {:padding-horizontal 15 + :border-top-width 1 + :border-top-color (:ui-01 @quo.colors/theme) + :padding-vertical 8}} + [reply-message reply]]) + +(defn reply-message-auto-focus-wrapper [text-input-ref] + (let [had-reply (atom nil)] + (fn [] + (let [reply @(re-frame/subscribe [:chats/reply-message])] + (focus-input-on-reply reply had-reply text-input-ref) + (when reply + [reply-message-wrapper reply]))))) diff --git a/src/status_im/ui/screens/chat/components/style.cljs b/src/status_im/ui/screens/chat/components/style.cljs index 5ee6365864..148f871294 100644 --- a/src/status_im/ui/screens/chat/components/style.cljs +++ b/src/status_im/ui/screens/chat/components/style.cljs @@ -74,16 +74,17 @@ (:icon-04 @colors/theme) (:icon-02 @colors/theme))}) -(defn reply-container [image] +(defn reply-container-image [] {:border-top-left-radius 14 :border-top-right-radius 14 :border-bottom-right-radius 4 :border-bottom-left-radius 14 :margin 2 :flex-direction :row - :background-color (if image - (:ui-03 @colors/theme) - (:ui-02 @colors/theme))}) + :background-color (:ui-03 @colors/theme)}) + +(defn reply-container [] + {:flex-direction :row}) (defn reply-content [] {:padding-vertical 6 @@ -91,7 +92,7 @@ :flex 1}) (defn close-button [] - {:padding 4}) + {:margin-top 3}) (defn send-message-button [] {:margin-vertical 4 @@ -115,4 +116,4 @@ :bottom bottom :background-color (colors/get-color :ui-background) :border-top-width 1 - :border-top-color (colors/get-color :ui-01)}) \ No newline at end of file + :border-top-color (colors/get-color :ui-01)}) diff --git a/src/status_im/ui/screens/chat/message/message.cljs b/src/status_im/ui/screens/chat/message/message.cljs index 94c74da7a8..9643b7b516 100644 --- a/src/status_im/ui/screens/chat/message/message.cljs +++ b/src/status_im/ui/screens/chat/message/message.cljs @@ -29,10 +29,12 @@ (letsubs [contact-name [:contacts/contact-name-by-identity from]] contact-name)) +(def edited-at-text (str " ⌫ " (i18n/label :t/edited))) + (defn message-timestamp ([message] [message-timestamp message false]) - ([{:keys [timestamp-str outgoing content outgoing-status]} justify-timestamp?] + ([{:keys [timestamp-str outgoing content outgoing-status edited-at]} justify-timestamp?] [react/view (when justify-timestamp? {:align-self :flex-end :position :absolute @@ -52,7 +54,9 @@ :color colors/white :accessibility-label (name outgoing-status)}]) [react/text {:style (style/message-timestamp-text outgoing)} - timestamp-str]])) + (str + timestamp-str + (when edited-at edited-at-text))]])) (defview quoted-message [_ {:keys [from parsed-text image]} outgoing current-public-key public?] @@ -161,10 +165,10 @@ (defn render-parsed-text [message tree] (reduce (fn [acc e] (render-block message acc e)) [:<>] tree)) -(defn render-parsed-text-with-timestamp [{:keys [timestamp-str outgoing] :as message} tree] +(defn render-parsed-text-with-timestamp [{:keys [timestamp-str outgoing edited-at] :as message} tree] (let [elements (render-parsed-text message tree) timestamp [react/text {:style (style/message-timestamp-placeholder)} - (str (if outgoing " " " ") timestamp-str)] + (str (if outgoing " " " ") timestamp-str (when edited-at edited-at-text))] last-element (peek elements)] ;; Using `nth` here as slightly faster than `first`, roughly 30% ;; It's worth considering pure js structures for this code path as @@ -334,6 +338,9 @@ (defn on-long-press-fn [on-long-press message content] (on-long-press (concat + (when (:outgoing message) + [{:on-press #(re-frame/dispatch [:chat.ui/edit-message message]) + :label (i18n/label :t/edit)}]) (when (:show-input? message) [{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message]) :label (i18n/label :t/message-reply)}]) diff --git a/src/status_im/ui/screens/chat/views.cljs b/src/status_im/ui/screens/chat/views.cljs index d31f448028..36b2f7b703 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -7,6 +7,8 @@ [status-im.ui.components.connectivity.view :as connectivity] [status-im.ui.components.icons.icons :as icons] [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.components.react :as react] [quo.animated :as animated] [quo.react-native :as rn] @@ -348,14 +350,21 @@ [invitation-bar chat-id]]) [components/autocomplete-mentions text-input-ref max-bottom-space] (when show-input? + ;; NOTE: this only accepts two children [accessory/view {:y position-y :pan-state pan-state :has-panel (boolean @active-panel) :on-close on-close :on-update-inset on-update} - [components/chat-toolbar - {:chat-id chat-id - :active-panel @active-panel - :set-active-panel set-active-panel - :text-input-ref text-input-ref}] + [react/view + [edit/edit-message-auto-focus-wrapper text-input-ref] + [reply/reply-message-auto-focus-wrapper text-input-ref] + ;; We set the key so we can force a re-render as + ;; it does not rely on ratom but just atoms + ^{:key (str @components/chat-input-key "chat-input")} + [components/chat-toolbar + {:chat-id chat-id + :active-panel @active-panel + :set-active-panel set-active-panel + :text-input-ref text-input-ref}]] [bottom-sheet @active-panel]])]))}))) diff --git a/status-go-version.json b/status-go-version.json index 6244952587..3aba83e73f 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -2,7 +2,7 @@ "_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh ' instead", "owner": "status-im", "repo": "status-go", - "version": "v0.80.2", - "commit-sha1": "96d5683b3b1fa2b283c829e0bb351a2e2c0e34c5", - "src-sha256": "1hg1jkqbkmp0js5kdpxiz6b34gpmy84wpq0bybf8hbbqh84drrx7" + "version": "v0.80.3", + "commit-sha1": "45a8de8e2bd3d3cdb1350ebfd71e6bcaadff630e", + "src-sha256": "054r35mgckv2zzy21sxidqfh7f2jk1dkl3vzp4n1rlc8807fqybm" } diff --git a/translations/en.json b/translations/en.json index ca74cce140..d6bd2fc49c 100644 --- a/translations/en.json +++ b/translations/en.json @@ -174,7 +174,9 @@ "request-access": "Request access", "membership-request-pending": "Membership request pending", "create-community": "Create a community", + "edited": "Edited", "edit-community": "Edit community", + "editing-message": "Editing message", "community-edit-title": "Edit community", "community-invite-title": "Invite", "community-share-title": "Share", @@ -825,6 +827,7 @@ "message-not-sent": "Message not sent", "message-options-cancel": "Cancel", "message-reply": "Reply", + "replying-to": "Replying to {{author}}", "data-syncing": "Data syncing", "messages": "Messages", "chat-is-a-contact": "Contact",