From 19526508f2b7ea026b64df4017dca294861d19fa Mon Sep 17 00:00:00 2001 From: Icaro Motta Date: Thu, 18 May 2023 16:19:41 -0300 Subject: [PATCH] New link previews (initial implementation) (#15891) This is the introductory work to support the new requirements for unfurling URLs (while the message is a draft) and displaying link previews (after the message is sent). Refer to the related status-go PR for a lot more interesting details https://github.com/status-im/status-go/pull/3471. Fixes https://github.com/status-im/status-mobile/issues/15469 ### Notes - The old link preview code will be removed separately, both in status-go and status-mobile. - I did the bulk of the work in status-go https://github.com/status-im/status-go/pull/3471. If you want to understand how this is all implemented, do check out the status-go PR because I heavily documented the solution, rationale, next steps, etc. ### Performance Does the feature perform well? Yes, there's very little overhead because unfurling URLs happen in status-go and the event is debounced. I also payed special attention to use a simple caching mechanism to avoid doing unnecessary RPC requests to status-go if the URLs are cached in the client. I have some ideas on how to improve performance further, but not in this PR which is already screaming for reviews. --- .../links/link_preview/component_spec.cljs | 6 + .../components/links/link_preview/view.cljs | 10 +- .../links/url_preview/component_spec.cljs | 6 +- .../components/links/url_preview/view.cljs | 9 +- .../links/url_preview_list/view.cljs | 27 +-- src/status_im/chat/models/input.cljs | 28 ++- src/status_im/data_store/activities_test.cljs | 2 + src/status_im/data_store/messages.cljs | 12 +- src/status_im/data_store/messages_test.cljs | 10 +- src/status_im/transport/message/protocol.cljs | 58 ++--- .../contexts/activity_center/events_test.cljs | 1 + .../contexts/chat/composer/constants.cljs | 6 + .../contexts/chat/composer/effects.cljs | 31 ++- .../contexts/chat/composer/handlers.cljs | 15 +- .../chat/composer/link_preview/events.cljs | 132 ++++++++++++ .../composer/link_preview/events_test.cljs | 204 ++++++++++++++++++ .../chat/composer/link_preview/style.cljs | 14 ++ .../chat/composer/link_preview/view.cljs | 28 +++ .../contexts/chat/composer/mentions/view.cljs | 8 +- .../contexts/chat/composer/sub_view.cljs | 15 +- .../contexts/chat/composer/utils.cljs | 27 +-- .../contexts/chat/composer/view.cljs | 7 +- src/status_im2/contexts/chat/events.cljs | 2 + .../messages/content/link_preview/view.cljs | 27 +++ .../content/link_preview/view_test.cljs | 16 ++ .../chat/messages/content/text/view.cljs | 11 +- .../quo_preview/links/link_preview.cljs | 37 +++- .../quo_preview/links/url_preview.cljs | 7 +- .../quo_preview/links/url_preview_list.cljs | 7 +- src/status_im2/subs/chat/chats.cljs | 12 ++ src/status_im2/subs/chat/messages.cljs | 7 +- src/status_im2/subs/root.cljs | 6 +- status-go-version.json | 6 +- translations/en.json | 1 + 34 files changed, 665 insertions(+), 130 deletions(-) create mode 100644 src/status_im2/contexts/chat/composer/link_preview/events.cljs create mode 100644 src/status_im2/contexts/chat/composer/link_preview/events_test.cljs create mode 100644 src/status_im2/contexts/chat/composer/link_preview/style.cljs create mode 100644 src/status_im2/contexts/chat/composer/link_preview/view.cljs create mode 100644 src/status_im2/contexts/chat/messages/content/link_preview/view.cljs create mode 100644 src/status_im2/contexts/chat/messages/content/link_preview/view_test.cljs diff --git a/src/quo2/components/links/link_preview/component_spec.cljs b/src/quo2/components/links/link_preview/component_spec.cljs index 24c468bd3c..cad4b757ee 100644 --- a/src/quo2/components/links/link_preview/component_spec.cljs +++ b/src/quo2/components/links/link_preview/component_spec.cljs @@ -6,6 +6,7 @@ {:title "Some title" :description "Some description" :link "status.im" + :logo "data:image/png,logo-x" :thumbnail "data:image/png,whatever"}) (h/describe "Links - Link Preview" @@ -19,12 +20,17 @@ (h/is-truthy (h/query-by-text (:title props))) (h/is-truthy (h/query-by-text (:description props))) (h/is-truthy (h/query-by-text (:link props))) + (h/is-truthy (h/query-by-label-text :logo)) (h/is-truthy (h/query-by-label-text :thumbnail))) (h/test "does not render thumbnail if prop is not present" (h/render [view/view (dissoc props :thumbnail)]) (h/is-null (h/query-by-label-text :thumbnail))) + (h/test "does not render logo if prop is not present" + (h/render [view/view (dissoc props :logo)]) + (h/is-null (h/query-by-label-text :logo))) + (h/test "shows button to enable preview when preview is disabled" (h/render [view/view (assoc props diff --git a/src/quo2/components/links/link_preview/view.cljs b/src/quo2/components/links/link_preview/view.cljs index e5fa244b46..5f53f92b9c 100644 --- a/src/quo2/components/links/link_preview/view.cljs +++ b/src/quo2/components/links/link_preview/view.cljs @@ -54,7 +54,9 @@ [logo] [rn/image {:accessibility-label :logo - :source logo + :source (if (string? logo) + {:uri logo} + logo) :style style/logo}]) (defn view @@ -68,9 +70,11 @@ (if enabled? [:<> [rn/view {:style style/header-container} - [logo-comp logo] + (when logo + [logo-comp logo]) [title-comp title]] - [description-comp description] + (when description + [description-comp description]) [link-comp link] (when thumbnail [thumbnail-comp thumbnail thumbnail-size])] diff --git a/src/quo2/components/links/url_preview/component_spec.cljs b/src/quo2/components/links/url_preview/component_spec.cljs index 961a65b8c8..3936e46ed8 100644 --- a/src/quo2/components/links/url_preview/component_spec.cljs +++ b/src/quo2/components/links/url_preview/component_spec.cljs @@ -7,10 +7,14 @@ (h/test "default render" (h/render [view/view]) (h/is-truthy (h/query-by-label-text :title)) - (h/is-truthy (h/query-by-label-text :logo)) (h/is-truthy (h/query-by-label-text :button-clear-preview)) + (h/is-null (h/query-by-label-text :logo)) (h/is-null (h/query-by-label-text :url-preview-loading))) + (h/test "renders logo when prop is present" + (h/render [view/view {:logo "data:image/png,logo"}]) + (h/is-truthy (h/query-by-label-text :logo))) + (h/test "on-clear event" (let [on-clear (h/mock-fn)] (h/render [view/view {:on-clear on-clear}]) diff --git a/src/quo2/components/links/url_preview/view.cljs b/src/quo2/components/links/url_preview/view.cljs index 7de0828213..3341d110b5 100644 --- a/src/quo2/components/links/url_preview/view.cljs +++ b/src/quo2/components/links/url_preview/view.cljs @@ -6,11 +6,13 @@ [quo2.foundations.colors :as colors] [react-native.core :as rn])) -(defn- logo-component +(defn- logo-comp [{:keys [logo]}] [rn/image {:accessibility-label :logo - :source logo + :source (if (string? logo) + {:uri logo} + logo) :style style/logo}]) (defn- content @@ -57,6 +59,7 @@ [rn/view {:accessibility-label :url-preview :style (merge (style/container) container-style)} - [logo-component {:logo logo}] + (when logo + [logo-comp {:logo logo}]) [content {:title title :body body}] [clear-button {:on-press on-clear}]])) diff --git a/src/quo2/components/links/url_preview_list/view.cljs b/src/quo2/components/links/url_preview_list/view.cljs index f91611ddbd..615488b287 100644 --- a/src/quo2/components/links/url_preview_list/view.cljs +++ b/src/quo2/components/links/url_preview_list/view.cljs @@ -1,10 +1,9 @@ (ns quo2.components.links.url-preview-list.view (:require - [oops.core :as oops] [quo2.components.links.url-preview-list.style :as style] [quo2.components.links.url-preview.view :as url-preview] [react-native.core :as rn] - [reagent.core :as reagent])) + [react-native.gesture :as gesture])) (defn- use-scroll-to-last-item [flat-list-ref item-count item-width] @@ -44,39 +43,33 @@ :on-clear on-clear :container-style (merge container-style {:width width})}]) -(defn- calculate-width - [preview-width horizontal-spacing ^js e] - (reset! preview-width - (- (oops/oget e "nativeEvent.layout.width") - (* 2 horizontal-spacing)))) - (defn- f-view [] - (let [preview-width (reagent/atom 0) - flat-list-ref (atom nil)] + (let [flat-list-ref (atom nil)] (fn [{:keys [data key-fn horizontal-spacing on-clear loading-message - container-style container-style-item]}] - (use-scroll-to-last-item flat-list-ref (count data) @preview-width) + container-style container-style-item + preview-width]}] + (use-scroll-to-last-item flat-list-ref (count data) preview-width) ;; We need to use a wrapping view expanded to 100% instead of "flex 1", ;; otherwise `on-layout` will be triggered multiple times as the flat list ;; renders its children. [rn/view - {:style (merge container-style {:width "100%"}) + {:style container-style :accessibility-label :url-preview-list} - [rn/flat-list + [gesture/flat-list {:ref #(reset! flat-list-ref %) + :keyboard-should-persist-taps :always :key-fn key-fn - :on-layout #(calculate-width preview-width horizontal-spacing %) :horizontal true :deceleration-rate :fast :on-scroll-to-index-failed identity :content-container-style {:padding-horizontal horizontal-spacing} :separator [separator] - :snap-to-interval (+ @preview-width style/url-preview-gap) + :snap-to-interval (+ preview-width style/url-preview-gap) :shows-horizontal-scroll-indicator false :data data :render-fn item-component - :render-data {:width @preview-width + :render-data {:width preview-width :on-clear on-clear :loading-message loading-message :container-style container-style-item}}]]))) diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index ae9d8c3620..3ab9e42df8 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -6,11 +6,12 @@ [status-im.chat.models.mentions :as mentions] [status-im.chat.models.message :as chat.message] [status-im.chat.models.message-content :as message-content] - [status-im2.constants :as constants] - [utils.re-frame :as rf] - [utils.i18n :as i18n] [status-im.utils.utils :as utils] - [taoensso.timbre :as log])) + [status-im2.constants :as constants] + [status-im2.contexts.chat.composer.link-preview.events :as link-preview] + [taoensso.timbre :as log] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) (defn text->emoji "Replaces emojis in a specified `text`" @@ -144,13 +145,15 @@ preferred-name (get-in db [:multiaccount :preferred-name]) emoji? (message-content/emoji-only-content? {:text input-text :response-to message-id})] - {:chat-id current-chat-id - :content-type (if emoji? - constants/content-type-emoji - constants/content-type-text) - :text input-text - :response-to message-id - :ens-name preferred-name}))) + {:chat-id current-chat-id + :content-type (if emoji? + constants/content-type-emoji + constants/content-type-text) + :text input-text + :response-to message-id + :ens-name preferred-name + :link-previews (map #(select-keys % [:url :title :description :thumbnail]) + (get-in db [:chat/link-previews :unfurled]))}))) (defn build-image-messages [{db :db} chat-id input-text] @@ -185,6 +188,7 @@ (let [current-chat-id (:current-chat-id db)] (rf/merge cofx (clean-input current-chat-id) + (link-preview/reset-unfurled) (mentions/clear-mentions)))) (rf/defn send-messages @@ -196,6 +200,7 @@ (when (seq messages) (rf/merge cofx (clean-input (:current-chat-id db)) + (link-preview/reset-unfurled) (chat.message/send-messages messages))))) (rf/defn send-audio-message @@ -241,6 +246,7 @@ :on-success (fn [result] (re-frame/dispatch [:sanitize-messages-and-process-response result]))}]} + (link-preview/reset-unfurled) (cancel-message-edit))) (rf/defn send-current-message diff --git a/src/status_im/data_store/activities_test.cljs b/src/status_im/data_store/activities_test.cljs index 02eb87b456..4b79b42da4 100644 --- a/src/status_im/data_store/activities_test.cljs +++ b/src/status_im/data_store/activities_test.cljs @@ -31,6 +31,7 @@ (= {:last-message {:quoted-message nil :outgoing-status nil :command-parameters nil + :link-previews [] :content {:sticker nil :rtl? nil :ens-name nil @@ -46,6 +47,7 @@ :reply-message {:quoted-message nil :outgoing-status nil :command-parameters nil + :link-previews [] :content {:sticker nil :rtl? nil :ens-name nil diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index d273a4c062..044703ed6a 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -15,6 +15,13 @@ :community-id :communityId :clock-value :clock}))) +(defn- <-link-preview-rpc + [preview] + (update preview + :thumbnail + (fn [thumbnail] + (set/rename-keys thumbnail {:dataUri :data-uri})))) + (defn <-rpc [message] (-> message @@ -43,8 +50,9 @@ :imageHeight :image-height :new :new? :albumImagesCount :album-images-count - :displayName :display-name}) - + :displayName :display-name + :linkPreviews :link-previews}) + (update :link-previews #(map <-link-preview-rpc %)) (update :quoted-message set/rename-keys {:parsedText :parsed-text diff --git a/src/status_im/data_store/messages_test.cljs b/src/status_im/data_store/messages_test.cljs index d6fdd761e2..8a3ca272be 100644 --- a/src/status_im/data_store/messages_test.cljs +++ b/src/status_im/data_store/messages_test.cljs @@ -8,7 +8,7 @@ "0x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1") (deftest message<-rpc - (testing "message to rpc" + (testing "message from RPC" (let [expected {:message-id message-id :content {:chat-id chat-id :sticker {:hash "hash" :pack 1} @@ -34,7 +34,9 @@ :text "reply"} :content-type 1 :compressed-key "c" - :timestamp 3} + :timestamp 3 + :link-previews [{:thumbnail {:url "http://localhost" + :data-uri "data:image/png"}}]} message {:id message-id :whisperTimestamp 1 :parsedText "parsed-text" @@ -56,5 +58,7 @@ :quotedMessage {:from "from" :text "reply"} :timestamp 3 - :outgoingStatus "sending"}] + :outgoingStatus "sending" + :linkPreviews [{:thumbnail {:url "http://localhost" + :dataUri "data:image/png"}}]}] (is (= expected (m/<-rpc message)))))) diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs index 9496a6c530..6f41f6b3ca 100644 --- a/src/status_im/transport/message/protocol.cljs +++ b/src/status_im/transport/message/protocol.cljs @@ -1,35 +1,35 @@ (ns ^{:doc "Protocol API and protocol utils"} status-im.transport.message.protocol - (:require [re-frame.core :as re-frame] - [utils.re-frame :as rf] - [taoensso.timbre :as log])) + (:require [clojure.set :as set] + [re-frame.core :as re-frame] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(defn- link-preview->rpc + [preview] + (update preview + :thumbnail + (fn [thumbnail] + (set/rename-keys thumbnail {:data-uri :dataUri})))) (defn build-message - [{:keys [chat-id - album-id - image-width - image-height - text - response-to - ens-name - community-id - image-path - audio-path - audio-duration-ms - sticker - content-type]}] - {:chatId chat-id - :albumId album-id - :imageWidth image-width - :imageHeight image-height - :text text - :responseTo response-to - :ensName ens-name - :imagePath image-path - :audioPath audio-path - :audioDurationMs audio-duration-ms - :communityId community-id - :sticker sticker - :contentType content-type}) + [msg] + (-> msg + (update :link-previews #(map link-preview->rpc %)) + (set/rename-keys + {:album-id :albumId + :audio-duration-ms :audioDurationMs + :audio-path :audioPath + :chat-id :chatId + :community-id :communityId + :content-type :contentType + :ens-name :ensName + :image-height :imageHeight + :image-path :imagePath + :image-width :imageWidth + :link-previews :linkPreviews + :response-to :responseTo + :sticker :sticker + :text :text}))) (rf/defn send-chat-messages [_ messages] diff --git a/src/status_im2/contexts/activity_center/events_test.cljs b/src/status_im2/contexts/activity_center/events_test.cljs index 33059c9006..5e8b37b61c 100644 --- a/src/status_im2/contexts/activity_center/events_test.cljs +++ b/src/status_im2/contexts/activity_center/events_test.cljs @@ -202,6 +202,7 @@ :text nil} :outgoing false :outgoing-status nil + :link-previews [] :quoted-message nil} :name "0x04d03f" :read true diff --git a/src/status_im2/contexts/chat/composer/constants.cljs b/src/status_im2/contexts/chat/composer/constants.cljs index c9e482a7c2..aa6d5c2e7b 100644 --- a/src/status_im2/contexts/chat/composer/constants.cljs +++ b/src/status_im2/contexts/chat/composer/constants.cljs @@ -19,6 +19,8 @@ (def ^:const images-container-height 76) +(def ^:const links-container-height 76) + (def ^:const reply-container-height 32) (def ^:const edit-container-height 32) @@ -36,3 +38,7 @@ (def ^:const background-threshold 0.75) (def ^:const max-text-size 4096) + +(def ^:const unfurl-debounce-ms + "Use a high threshold to prevent unnecessary rendering overhead." + 400) diff --git a/src/status_im2/contexts/chat/composer/effects.cljs b/src/status_im2/contexts/chat/composer/effects.cljs index 5fab5b8cfe..eb02bf253d 100644 --- a/src/status_im2/contexts/chat/composer/effects.cljs +++ b/src/status_im2/contexts/chat/composer/effects.cljs @@ -1,14 +1,16 @@ (ns status-im2.contexts.chat.composer.effects (:require - [react-native.platform :as platform] - [status-im.async-storage.core :as async-storage] + [clojure.string :as string] + [oops.core :as oops] [react-native.core :as rn] + [react-native.platform :as platform] [react-native.reanimated :as reanimated] + [status-im.async-storage.core :as async-storage] [status-im2.contexts.chat.composer.constants :as constants] [status-im2.contexts.chat.composer.keyboard :as kb] + [utils.debounce :as debounce] [utils.number :as utils.number] - [oops.core :as oops] - [utils.debounce :as debounce])) + [utils.re-frame :as rf])) (defn reenter-screen-effect [{:keys [text-value saved-cursor-position maximized?]} @@ -53,6 +55,12 @@ (reanimated/set-shared-value background-y 0) (reanimated/animate opacity 1))) +(defn link-preview-effect + [{:keys [text-value]}] + (let [text @text-value] + (when-not (string/blank? text) + (rf/dispatch [:link-preview/unfurl-urls text])))) + (defn images-effect [{:keys [sending-images? input-ref]} {:keys [container-opacity]} @@ -78,10 +86,17 @@ [{:keys [text-value maximized? focused?]} {:keys [container-opacity]} images? + link-previews? reply? audio] (when - (and (empty? @text-value) (not images?) (not reply?) (not @maximized?) (not @focused?) (not audio)) + (and (empty? @text-value) + (not images?) + (not link-previews?) + (not reply?) + (not @maximized?) + (not @focused?) + (not audio)) (reanimated/animate-delay container-opacity constants/empty-opacity 200))) (defn component-will-unmount @@ -91,7 +106,8 @@ (.remove ^js @keyboard-frame-listener)) (defn initialize - [props state animations {:keys [max-height] :as dimensions} {:keys [chat-input images reply audio]}] + [props state animations {:keys [max-height] :as dimensions} + {:keys [chat-input images link-previews? reply audio]}] (rn/use-effect (fn [] (maximized-effect state animations dimensions chat-input) @@ -100,8 +116,9 @@ (kb-default-height-effect state) (background-effect state animations dimensions chat-input) (images-effect props animations images) + (link-preview-effect state) (audio-effect state animations audio) - (empty-effect state animations images reply audio) + (empty-effect state animations images link-previews? reply audio) (kb/add-kb-listeners props state animations dimensions) #(component-will-unmount props)) [max-height])) diff --git a/src/status_im2/contexts/chat/composer/handlers.cljs b/src/status_im2/contexts/chat/composer/handlers.cljs index 09710ab23b..23c759f35a 100644 --- a/src/status_im2/contexts/chat/composer/handlers.cljs +++ b/src/status_im2/contexts/chat/composer/handlers.cljs @@ -1,15 +1,15 @@ (ns status-im2.contexts.chat.composer.handlers (:require + [oops.core :as oops] [react-native.core :as rn] [react-native.reanimated :as reanimated] [reagent.core :as reagent] - [oops.core :as oops] [status-im2.contexts.chat.composer.constants :as constants] [status-im2.contexts.chat.composer.keyboard :as kb] - [status-im2.contexts.chat.composer.utils :as utils] [status-im2.contexts.chat.composer.selection :as selection] - [utils.re-frame :as rf] - [utils.debounce :as debounce])) + [status-im2.contexts.chat.composer.utils :as utils] + [utils.debounce :as debounce] + [utils.re-frame :as rf])) (defn focus [{:keys [input-ref] :as props} @@ -39,7 +39,7 @@ maximized? recording?]} {:keys [height saved-height last-height gradient-opacity container-opacity opacity background-y]} {:keys [content-height max-height window-height]} - {:keys [images reply]}] + {:keys [images link-previews? reply]}] (when-not @recording? (let [lines (utils/calc-lines (- @content-height constants/extra-content-offset)) min-height (utils/get-min-height lines) @@ -51,7 +51,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 (utils/empty-input? @text-value images reply nil) + (when (utils/empty-input? @text-value images link-previews? reply nil) (reanimated/animate container-opacity constants/empty-opacity)) (reanimated/animate gradient-opacity 0) (reset! lock-selection? true) @@ -111,6 +111,9 @@ [text {:keys [input-ref record-reset-fn]} {:keys [text-value cursor-position recording?]}] + (debounce/debounce-and-dispatch [:link-preview/unfurl-urls text] + constants/unfurl-debounce-ms) + (reset! text-value text) (reagent/next-tick #(when @input-ref (.setNativeProps ^js @input-ref diff --git a/src/status_im2/contexts/chat/composer/link_preview/events.cljs b/src/status_im2/contexts/chat/composer/link_preview/events.cljs new file mode 100644 index 0000000000..5e1ce19458 --- /dev/null +++ b/src/status_im2/contexts/chat/composer/link_preview/events.cljs @@ -0,0 +1,132 @@ +(ns status-im2.contexts.chat.composer.link-preview.events + (:require + [clojure.set :as set] + [clojure.string :as string] + [status-im.data-store.messages :as data-store.messages] + [taoensso.timbre :as log] + [utils.collection] + [utils.re-frame :as rf])) + +(rf/defn unfurl-urls + {:events [:link-preview/unfurl-urls]} + [{:keys [db]} text] + (if (string/blank? text) + {:db (update db :chat/link-previews dissoc :unfurled :request-id)} + {:json-rpc/call + [{:method "wakuext_getTextURLs" + :params [text] + :on-success #(rf/dispatch [:link-preview/unfurl-parsed-urls %]) + :on-error (fn [error] + (log/error "Failed to parse text and extract URLs" + {:error error + :event :link-preview/unfurl-urls}))}]})) + +(defn- urls->previews + [preview-cache urls] + (->> urls + (map #(get preview-cache % {:url % :loading? true})) + (remove :failed?))) + +(def new-request-id (comp str random-uuid)) + +(rf/defn unfurl-parsed-urls + {:events [:link-preview/unfurl-parsed-urls]} + [{:keys [db]} urls] + (let [cleared (set (get-in db [:chat/link-previews :cleared]))] + (when (or (empty? urls) + (not= (set urls) cleared)) + (let [cache (get-in db [:chat/link-previews :cache]) + previews (urls->previews cache urls) + new-urls (->> previews + (filter :loading?) + (map :url)) + ;; `request-id` is a must because we need to process only the last + ;; unfurling event, as well as avoid needlessly updating the app db + ;; if the user changes the URLs in the input text when there are + ;; in-flight RPC requests. + request-id (new-request-id)] + (merge {:db (-> db + (assoc-in [:chat/link-previews :unfurled] previews) + (assoc-in [:chat/link-previews :request-id] request-id) + (update :chat/link-previews dissoc :cleared))} + (when (seq new-urls) + (log/debug "Unfurling URLs" {:urls new-urls :request-id request-id}) + {:json-rpc/call + [{:method "wakuext_unfurlURLs" + :params [new-urls] + :on-success #(rf/dispatch [:link-preview/unfurl-parsed-urls-success request-id %]) + :on-error #(rf/dispatch [:link-preview/unfurl-parsed-urls-error request-id + %])}]})))))) + +(defn- failed-previews + [curr-previews new-previews] + (let [curr-urls (set (->> curr-previews + (filter :loading?) + (map :url))) + new-urls (set (map :url new-previews))] + (map (fn [url] + {:url url :failed? true}) + (set/difference curr-urls new-urls)))) + +(defn- reconcile-unfurled + [curr-previews indexed-new-previews] + (reduce (fn [acc preview] + (if (:loading? preview) + (if-let [loaded-preview (get indexed-new-previews + (:url preview))] + (conj acc loaded-preview) + acc) + (conj acc preview))) + [] + curr-previews)) + +(rf/defn unfurl-parsed-urls-success + {:events [:link-preview/unfurl-parsed-urls-success]} + [{:keys [db]} request-id new-previews] + (when (= request-id (get-in db [:chat/link-previews :request-id])) + (let [new-previews (map data-store.messages/<-link-preview-rpc new-previews) + curr-previews (get-in db [:chat/link-previews :unfurled]) + indexed-new-previews (utils.collection/index-by :url new-previews)] + (log/debug "URLs unfurled" + {:event :link-preview/unfurl-parsed-urls-success + :previews (map #(update % :thumbnail dissoc :data-uri) new-previews) + :request-id request-id}) + {:db (-> db + (update-in [:chat/link-previews :unfurled] reconcile-unfurled indexed-new-previews) + (update-in [:chat/link-previews :cache] + merge + indexed-new-previews + (utils.collection/index-by :url + (failed-previews curr-previews new-previews))))}))) + +(rf/defn unfurl-parsed-urls-error + {:events [:link-preview/unfurl-parsed-urls-error]} + [{:keys [db]} request-id error] + (log/error "Failed to unfurl URLs" + {:request-id request-id + :error error + :event :link-preview/unfurl-parsed-urls})) + +(rf/defn reset-unfurled + "Reset preview state, but keep the cache. Use this event after a message is + sent." + {:events [:link-preview/reset-unfurled]} + [{:keys [db]}] + {:db (update db :chat/link-previews dissoc :unfurled :request-id :cleared)}) + +(rf/defn reset-all + "Reset all preview state. It is especially important to delete any cached + URLs, as failing to do so results in its unbounded growth." + {:events [:link-preview/reset-all]} + [{:keys [db]}] + {:db (dissoc db :chat/link-previews)}) + +(rf/defn clear-link-previews + "Mark current unfurled URLs as `cleared`, meaning the user won't see previews + until they insert/remove non-cleared URL(s)." + {:events [:link-preview/clear]} + [{:keys [db]}] + (let [unfurled-urls (set (map :url (get-in db [:chat/link-previews :unfurled])))] + {:db (-> db + (update :chat/link-previews dissoc :unfurled :request-id) + (assoc-in [:chat/link-previews :cleared] unfurled-urls))})) diff --git a/src/status_im2/contexts/chat/composer/link_preview/events_test.cljs b/src/status_im2/contexts/chat/composer/link_preview/events_test.cljs new file mode 100644 index 0000000000..ddff60844e --- /dev/null +++ b/src/status_im2/contexts/chat/composer/link_preview/events_test.cljs @@ -0,0 +1,204 @@ +(ns status-im2.contexts.chat.composer.link-preview.events-test + (:require [status-im2.contexts.chat.composer.link-preview.events :as events] + [cljs.test :refer [is deftest testing]])) + +(def url-github "https://github.com") +(def url-gitlab "https://gitlab.com") +(def preview-github {:url url-github :thumbnail nil}) +(def preview-gitlab {:url url-gitlab :thumbnail nil}) +(def request-id "abc123") + +(defn remove-rpc-callbacks + [effects] + (if (get-in effects [:json-rpc/call 0]) + (update-in effects [:json-rpc/call 0] dissoc :on-success :on-error) + effects)) + +(deftest unfurl-urls + (testing "clear up state when text is empty" + (let [cofx {:db {:chat/link-previews {:unfurled {} + :cache {} + :request-id "123"}}}] + (is (= {:db {:chat/link-previews {:cache {}}}} + (events/unfurl-urls cofx " "))))) + + (testing "fetches parsed URLs" + (let [cofx {:db {:chat/link-previews {:unfurled {} + :cache {} + :request-id "123"}}}] + (is (= {:json-rpc/call [{:method "wakuext_getTextURLs" + :params [url-github]}]} + (remove-rpc-callbacks + (events/unfurl-urls cofx url-github))))))) + +(deftest unfurl-parsed-urls-test + (with-redefs [events/new-request-id (constantly request-id)] + (testing "empty state with 2 stale URLs" + (let [cofx {:db {:chat/link-previews + {:cache {} + :unfurled []}}}] + (is (= {:db {:chat/link-previews + {:request-id request-id + :cache {} + :unfurled [{:url url-github :loading? true} + {:url url-gitlab :loading? true}]}} + :json-rpc/call [{:method "wakuext_unfurlURLs" + :params [[url-github url-gitlab]]}]} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "nothing unfurled, 1 cached URL and the other stale" + (let [cache {url-github preview-github} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled []}}}] + (is (= {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [preview-github + {:url url-gitlab :loading? true}]}} + :json-rpc/call [{:method "wakuext_unfurlURLs" + :params [[url-gitlab]]}]} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "does nothing when the URLs are identical to the last cleared ones" + (let [cofx {:db {:chat/link-previews + {:cache {url-github preview-github} + :cleared #{url-github url-gitlab} + :unfurled [preview-github preview-gitlab]}}}] + (is (nil? (events/unfurl-parsed-urls cofx [url-github url-gitlab]))))) + + (testing "app db has previews, but the new parsed text has no valid URLs" + (let [cache {url-github preview-github} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github]}}}] + (is (= {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled []}}} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [])))))) + + (testing "2 unfurled, 2 cached URLs" + (let [cache {url-github preview-github url-gitlab preview-gitlab} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github preview-gitlab]}}}] + (is (= {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [preview-github preview-gitlab]}}} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "1 unfurled, 1 successful in the cache, 1 failed in the cache" + (let [cache {url-github preview-github + url-gitlab (assoc preview-gitlab :failed? true)} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github]}}}] + (is (= {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [preview-github]}}} + (events/unfurl-parsed-urls cofx [url-github url-gitlab]))))) + + (testing "nothing unfurled, 1 failed in the cache" + (let [cache {url-gitlab (assoc preview-gitlab :failed? true)} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled []}}}] + (is (= {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled [{:url url-github :loading? true}]}} + :json-rpc/call [{:method "wakuext_unfurlURLs" + :params [[url-github]]}]} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [url-github url-gitlab])))))) + + (testing "empties unfurled collection when there are no valid previews" + (let [unknown-url "https://github.abcdef" + cache {unknown-url {:url unknown-url :failed? true} + url-github preview-github} + cofx {:db {:chat/link-previews + {:cache cache + :unfurled [preview-github]}}}] + (is (= {:db {:chat/link-previews + {:request-id request-id + :cache cache + :unfurled []}}} + (remove-rpc-callbacks + (events/unfurl-parsed-urls cofx [unknown-url])))))))) + +(deftest unfurl-parsed-urls-success-test + (testing "does nothing if the success request-id is different than the current one" + (let [cofx {:db {:chat/link-previews + {:request-id request-id + :unfurled [] + :cache {}}}}] + (is (nil? (events/unfurl-parsed-urls-success cofx "banana" [preview-github]))))) + + (testing "reconciles new previews with existing ones" + (let [cofx {:db {:chat/link-previews + {:request-id request-id + :unfurled [preview-github + {:url url-gitlab :loading? true}] + :cache {url-github preview-github}}}} + {db :db} (events/unfurl-parsed-urls-success cofx + request-id + [preview-gitlab])] + (is (= {:chat/link-previews + {:request-id request-id + :unfurled [preview-github preview-gitlab] + :cache {url-github preview-github + url-gitlab preview-gitlab}}} + db)))) + + (testing "identify and write failed preview in the cache" + (let [preview-youtube {:url "https://youtube.com" :thumbnail nil} + cofx {:db {:chat/link-previews + {:request-id request-id + :unfurled [{:url url-github :loading? true} + preview-youtube + {:url url-gitlab :loading? true}] + :cache {(:url preview-youtube) preview-youtube}}}} + {db :db} (events/unfurl-parsed-urls-success cofx + request-id + [preview-github + preview-youtube])] + (is (= {:chat/link-previews + {:request-id request-id + :unfurled [preview-github preview-youtube] + :cache {(:url preview-youtube) preview-youtube + url-github preview-github + url-gitlab {:url "https://gitlab.com" :failed? true}}}} + db))))) + +(deftest clear-link-previews-test + (let [cache {url-github preview-github} + effects (events/clear-link-previews + {:db {:chat/link-previews {:unfurled [preview-github] + :request-id request-id + :cache cache}}})] + (is (= {:db {:chat/link-previews {:cache cache + :cleared #{url-github}}}} + effects)))) + +(deftest reset-unfurled-test + (let [cache {url-github preview-github}] + (is (= {:db {:chat/link-previews {:cache cache}}} + (events/reset-unfurled + {:db {:chat/link-previews {:unfurled [preview-github] + :request-id request-id + :cleared #{url-github} + :cache cache}}}))))) + +(deftest reset-all-test + (is (= {:db {:non-related-key :some-value}} + (events/reset-all + {:db {:non-related-key :some-value + :chat/link-previews {:request-id request-id + :any-other-key "farewell"}}})))) diff --git a/src/status_im2/contexts/chat/composer/link_preview/style.cljs b/src/status_im2/contexts/chat/composer/link_preview/style.cljs new file mode 100644 index 0000000000..fb9a4ddbff --- /dev/null +++ b/src/status_im2/contexts/chat/composer/link_preview/style.cljs @@ -0,0 +1,14 @@ +(ns status-im2.contexts.chat.composer.link-preview.style) + +(def padding-horizontal 20) +(def preview-list-padding-top 12) +(def preview-list-padding-bottom 8) +(def preview-height 56) + +(def preview-list + {:padding-top preview-list-padding-top + :padding-bottom preview-list-padding-bottom + :margin-horizontal (- padding-horizontal) + ;; Keep a high index, otherwise the parent gesture detector used by the + ;; composer grabs the initiating gesture event. + :z-index 9999}) diff --git a/src/status_im2/contexts/chat/composer/link_preview/view.cljs b/src/status_im2/contexts/chat/composer/link_preview/view.cljs new file mode 100644 index 0000000000..e4a2e0b472 --- /dev/null +++ b/src/status_im2/contexts/chat/composer/link_preview/view.cljs @@ -0,0 +1,28 @@ +(ns status-im2.contexts.chat.composer.link-preview.view + (:require + [quo2.core :as quo] + [react-native.core :as rn] + [status-im2.contexts.chat.composer.link-preview.events] + [status-im2.contexts.chat.composer.link-preview.style :as style] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn message-draft-link-previews + [] + (let [previews (rf/sub [:chats/link-previews-unfurled])] + [quo/url-preview-list + {:key-fn :url + :preview-width (- (:width (rn/get-window)) + (* 2 style/padding-horizontal)) + :container-style (when (seq previews) style/preview-list) + :container-style-item {:height style/preview-height} + :horizontal-spacing style/padding-horizontal + :loading-message (i18n/label :t/link-preview-loading-message) + :on-clear #(rf/dispatch [:link-preview/clear]) + :data (map (fn [{:keys [title thumbnail hostname loading? url]}] + {:title title + :body hostname + :loading? loading? + :thumbnail (:data-uri thumbnail) + :url url}) + previews)}])) diff --git a/src/status_im2/contexts/chat/composer/mentions/view.cljs b/src/status_im2/contexts/chat/composer/mentions/view.cljs index 07708a0735..431cf03805 100644 --- a/src/status_im2/contexts/chat/composer/mentions/view.cljs +++ b/src/status_im2/contexts/chat/composer/mentions/view.cljs @@ -30,7 +30,7 @@ user]) (defn- f-view - [suggestions-atom props state animations max-height cursor-pos images reply edit] + [suggestions-atom props state animations max-height cursor-pos images link-previews? reply edit] (let [suggestions (rf/sub [:chat/mention-suggestions]) opacity (reanimated/use-shared-value (if (seq suggestions) 1 0)) size (count suggestions) @@ -39,6 +39,7 @@ :curr-height (reanimated/get-shared-value (:height animations)) :window-height (:height (rn/get-window)) :images images + :link-previews? link-previews? :reply reply :edit edit} mentions-pos (utils/calc-suggestions-position cursor-pos max-height size state data)] @@ -61,6 +62,7 @@ :accessibility-label :mentions-list}]])) (defn view - [props state animations max-height cursor-pos images reply edit] + [props state animations max-height cursor-pos images link-previews? reply edit] (let [suggestions-atom (reagent/atom {})] - [:f> f-view suggestions-atom props state animations max-height cursor-pos images reply edit])) + [:f> f-view suggestions-atom props state animations max-height cursor-pos images link-previews? reply + edit])) diff --git a/src/status_im2/contexts/chat/composer/sub_view.cljs b/src/status_im2/contexts/chat/composer/sub_view.cljs index 731178c5db..f574c0eef5 100644 --- a/src/status_im2/contexts/chat/composer/sub_view.cljs +++ b/src/status_im2/contexts/chat/composer/sub_view.cljs @@ -26,16 +26,17 @@ [:f> f-blur-view layout-height focused?]) (defn- f-shell-button - [{:keys [maximized?]} {:keys [height]} {:keys [images reply edit]}] + [{:keys [maximized?]} {:keys [height]} {:keys [images link-previews? reply edit]}] (let [insets (safe-area/get-insets) - extra-height (utils/calc-extra-content-height images reply edit) + extra-height (utils/calc-extra-content-height images link-previews? reply edit) translate-y (reanimated/use-shared-value (utils/calc-shell-neg-y insets maximized? extra-height))] - (rn/use-effect (fn [] - (let [extra-height (utils/calc-extra-content-height images reply edit)] - (reanimated/animate translate-y - (utils/calc-shell-neg-y insets maximized? extra-height)))) - [@maximized? images reply edit]) + (rn/use-effect + (fn [] + (let [extra-height (utils/calc-extra-content-height images link-previews? reply edit)] + (reanimated/animate translate-y + (utils/calc-shell-neg-y insets maximized? extra-height)))) + [@maximized? images link-previews? reply edit]) [reanimated/view {:style (reanimated/apply-animations-to-style {:bottom height ; we use height of the input directly as bottom position diff --git a/src/status_im2/contexts/chat/composer/utils.cljs b/src/status_im2/contexts/chat/composer/utils.cljs index 4183fdae13..8300cbd5f9 100644 --- a/src/status_im2/contexts/chat/composer/utils.cljs +++ b/src/status_im2/contexts/chat/composer/utils.cljs @@ -66,30 +66,31 @@ (if platform/ios? lines (dec lines)))) (defn calc-extra-content-height - [images? reply? edit?] + [images? link-previews? reply? edit?] (let [height (if images? constants/images-container-height 0) + height (if link-previews? (+ height constants/links-container-height) height) height (if reply? (+ height constants/reply-container-height) height) height (if edit? (+ height constants/edit-container-height) height)] height)) (defn calc-max-height - [{:keys [images reply edit]} window-height kb-height insets] + [{:keys [images link-previews? reply edit]} window-height kb-height insets] (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) - max-height (- max-height (calc-extra-content-height images reply edit))] + max-height (- max-height (calc-extra-content-height images link-previews? reply edit))] max-height)) (defn empty-input? - [text images reply? audio?] - (and (empty? text) (empty? images) (not reply?) (not audio?))) - -(defn android-elevation? - [lines images reply? edit?] - (or (> lines 1) (seq images) reply? edit?)) + [text images link-previews? reply? audio?] + (and (empty? text) + (empty? images) + (not link-previews?) + (not reply?) + (not audio?))) (defn cancel-edit-message [{:keys [text-value]}] @@ -126,10 +127,10 @@ (defn calc-suggestions-position [cursor-pos max-height size {:keys [maximized?]} - {:keys [insets curr-height window-height keyboard-height images reply edit]}] + {:keys [insets curr-height window-height keyboard-height images link-previews? reply edit]}] (let [base (+ constants/composer-default-height (:bottom insets) 8) base (+ base (- curr-height constants/input-height)) - base (+ base (calc-extra-content-height images reply edit)) + base (+ base (calc-extra-content-height images link-previews? reply edit)) view-height (- window-height keyboard-height (:top insets)) container-height (bounded-val (* (/ constants/mentions-max-height 4) size) @@ -181,6 +182,7 @@ [] (let [chat-input (rf/sub [:chats/current-chat-input])] {:images (seq (rf/sub [:chats/sending-image])) + :link-previews? (rf/sub [:chats/link-previews?]) :audio (rf/sub [:chats/sending-audio]) :reply (rf/sub [:chats/reply-message]) :edit (rf/sub [:chats/edit-message]) @@ -189,7 +191,7 @@ :input-content-height (:input-content-height chat-input)})) (defn init-animations - [{:keys [input-text images reply audio]} + [{:keys [input-text images link-previews? reply audio]} lines content-height max-height opacity background-y] (let [initial-height (if (> lines 1) constants/multiline-minimized-height @@ -199,6 +201,7 @@ (if (empty-input? input-text images + link-previews? reply audio) 0.7 diff --git a/src/status_im2/contexts/chat/composer/view.cljs b/src/status_im2/contexts/chat/composer/view.cljs index 3bd5c0ae72..fb3f4450e1 100644 --- a/src/status_im2/contexts/chat/composer/view.cljs +++ b/src/status_im2/contexts/chat/composer/view.cljs @@ -8,6 +8,7 @@ [reagent.core :as reagent] [utils.i18n :as i18n] [status-im2.contexts.chat.composer.style :as style] + [status-im2.contexts.chat.composer.link-preview.view :as link-preview] [status-im2.contexts.chat.composer.images.view :as images] [status-im2.contexts.chat.composer.reply.view :as reply] [status-im2.contexts.chat.composer.edit.view :as edit] @@ -62,7 +63,10 @@ (effects/edit-mentions props state subs) [:<> [sub-view/shell-button state animations subs] - [mentions/view props state animations max-height cursor-pos (:images subs) (:reply subs) + [mentions/view props state animations max-height cursor-pos + (:images subs) + (:link-previews? subs) + (:reply subs) (:edit subs)] [gesture/gesture-detector {:gesture (drag-gesture/drag-gesture props state animations dimensions keyboard-shown)} @@ -104,6 +108,7 @@ :max-length constants/max-text-size :accessibility-label :chat-message-input}]] [gradients/view props state animations show-bottom-gradient?]] + [link-preview/message-draft-link-previews] [images/images-list] [actions/view props state animations window-height insets subs]]]])) diff --git a/src/status_im2/contexts/chat/events.cljs b/src/status_im2/contexts/chat/events.cljs index 43b5a1d2d7..38104d1eaf 100644 --- a/src/status_im2/contexts/chat/events.cljs +++ b/src/status_im2/contexts/chat/events.cljs @@ -6,6 +6,7 @@ [status-im2.contexts.chat.messages.list.state :as chat.state] [status-im2.contexts.chat.messages.delete-message-for-me.events :as delete-for-me] [status-im2.contexts.chat.messages.delete-message.events :as delete-message] + [status-im2.contexts.chat.composer.link-preview.events :as link-preview] [status-im2.navigation.events :as navigation] [status-im2.constants :as constants] [status-im.chat.models.loading :as loading] @@ -177,6 +178,7 @@ (when (and community-id (not navigate-to-shell?)) {:dispatch-n [[:shell/add-switcher-card :community-overview community-id]]}))) + (link-preview/reset-all) (delete-for-me/sync-all) (delete-message/send-all) (offload-messages chat-id)))) diff --git a/src/status_im2/contexts/chat/messages/content/link_preview/view.cljs b/src/status_im2/contexts/chat/messages/content/link_preview/view.cljs new file mode 100644 index 0000000000..b9e15b2f02 --- /dev/null +++ b/src/status_im2/contexts/chat/messages/content/link_preview/view.cljs @@ -0,0 +1,27 @@ +(ns status-im2.contexts.chat.messages.content.link-preview.view + (:require + [quo2.core :as quo] + [utils.re-frame :as rf])) + +(defn nearly-square? + [{:keys [width height]}] + (if (or (zero? width) (zero? height)) + false + (let [ratio (/ (max width height) + (min width height))] + (< (Math/abs (dec ratio)) 0.1)))) + +(defn view + [{:keys [chat-id message-id]}] + (let [previews (rf/sub [:chats/message-link-previews chat-id message-id])] + (when (seq previews) + [:<> + (for [{:keys [url title description thumbnail hostname]} previews] + ^{:key url} + [quo/link-preview + {:title title + :description description + :link hostname + :thumbnail (:url thumbnail) + :thumbnail-size (when (nearly-square? thumbnail) :large) + :container-style {:margin-top 8}}])]))) diff --git a/src/status_im2/contexts/chat/messages/content/link_preview/view_test.cljs b/src/status_im2/contexts/chat/messages/content/link_preview/view_test.cljs new file mode 100644 index 0000000000..41f75557a2 --- /dev/null +++ b/src/status_im2/contexts/chat/messages/content/link_preview/view_test.cljs @@ -0,0 +1,16 @@ +(ns status-im2.contexts.chat.messages.content.link-preview.view-test + (:require [status-im2.contexts.chat.messages.content.link-preview.view :as view] + [cljs.test :refer [is deftest are]])) + +(deftest nearly-square?-test + (are [pred width height] (is (pred (view/nearly-square? {:width width :height height}))) + false? 0 0 + true? 1 1 + false? 100 89 + false? 100 90 + true? 100 91 + true? 100 92 + true? 100 101 + true? 100 109 + false? 100 110 + false? 100 111)) diff --git a/src/status_im2/contexts/chat/messages/content/text/view.cljs b/src/status_im2/contexts/chat/messages/content/text/view.cljs index 7259de077e..ab4dae5ee1 100644 --- a/src/status_im2/contexts/chat/messages/content/text/view.cljs +++ b/src/status_im2/contexts/chat/messages/content/text/view.cljs @@ -3,11 +3,10 @@ [quo2.core :as quo] [quo2.foundations.colors :as colors] [react-native.core :as rn] + [status-im2.contexts.chat.messages.content.link-preview.view :as link-preview] [status-im2.contexts.chat.messages.content.text.style :as style] - [status-im2.contexts.chat.messages.link-preview.view :as link-preview] - [utils.re-frame :as rf] - [utils.i18n :as i18n])) - + [utils.i18n :as i18n] + [utils.re-frame :as rf])) (defn render-inline [units {:keys [type literal destination]} chat-id] @@ -134,7 +133,7 @@ add-edited-tag))]) (defn text-content - [message-data context] + [message-data _] [rn/view [render-parsed-text message-data] - [link-preview/link-preview message-data context]]) + [link-preview/view message-data]]) diff --git a/src/status_im2/contexts/quo_preview/links/link_preview.cljs b/src/status_im2/contexts/quo_preview/links/link_preview.cljs index 38d9bbaa3d..79bca73eab 100644 --- a/src/status_im2/contexts/quo_preview/links/link_preview.cljs +++ b/src/status_im2/contexts/quo_preview/links/link_preview.cljs @@ -21,6 +21,15 @@ {:label "Container width" :key :width :type :text} + {:label "With logo?" + :key :with-logo? + :type :boolean} + {:label "With description?" + :key :with-description? + :type :boolean} + {:label "With thumbnail?" + :key :with-thumbnail? + :type :boolean} {:label "Disabled text" :key :disabled-text :type :text} @@ -45,14 +54,17 @@ (defn cool-preview [] (let [state (reagent/atom - {:title "Rarible - NFT Marketplace" - :description "Turn your products or services into publicly tradeable items" - :link "rarible.com" - :thumbnail :collectible - :width "295" - :enabled? true - :thumbnail-size :normal - :disabled-text "Enable Preview"})] + {:title "Rarible - NFT Marketplace" + :description "Turn your products or services into publicly tradeable items" + :link "rarible.com" + :thumbnail :collectible + :width "295" + :with-logo? true + :with-thumbnail? true + :with-description? true + :enabled? true + :thumbnail-size :normal + :disabled-text "Enable Preview"})] (fn [] (let [width (utils.number/parse-int (:width @state) 295) thumbnail (get resources/mock-images (:thumbnail @state))] @@ -62,14 +74,17 @@ {:style {:align-items :center :margin-top 20}} [quo/link-preview - {:logo (resources/get-mock-image :status-logo) + {:logo (when (:with-logo? @state) + (resources/get-mock-image :status-logo)) :title (:title @state) - :description (:description @state) + :description (when (:with-description? @state) + (:description @state)) :enabled? (:enabled? @state) :on-enable #(js/alert "Button pressed") :disabled-text (:disabled-text @state) :link (:link @state) - :thumbnail thumbnail + :thumbnail (when (:with-thumbnail? @state) + thumbnail) :thumbnail-size (:thumbnail-size @state) :container-style {:width width}}]]])))) diff --git a/src/status_im2/contexts/quo_preview/links/url_preview.cljs b/src/status_im2/contexts/quo_preview/links/url_preview.cljs index b5c66da0b0..876b34fa82 100644 --- a/src/status_im2/contexts/quo_preview/links/url_preview.cljs +++ b/src/status_im2/contexts/quo_preview/links/url_preview.cljs @@ -14,6 +14,9 @@ {:label "Body" :key :body :type :text} + {:label "With logo?" + :key :with-logo? + :type :boolean} {:label "Loading?" :key :loading? :type :boolean} @@ -26,6 +29,7 @@ (let [state (reagent/atom {:title "Status - Private, Secure Communication" :body "Status.im" + :with-logo? true :loading? false :loading-message "Generating preview"})] (fn [] @@ -39,7 +43,8 @@ [quo/url-preview {:title (:title @state) :body (:body @state) - :logo (resources/get-mock-image :status-logo) + :logo (when (:with-logo? @state) + (resources/get-mock-image :status-logo)) :loading? (:loading? @state) :loading-message (:loading-message @state) :on-clear #(js/alert "Clear button pressed")}]]]]))) diff --git a/src/status_im2/contexts/quo_preview/links/url_preview_list.cljs b/src/status_im2/contexts/quo_preview/links/url_preview_list.cljs index 840307c34d..e191e6a4f7 100644 --- a/src/status_im2/contexts/quo_preview/links/url_preview_list.cljs +++ b/src/status_im2/contexts/quo_preview/links/url_preview_list.cljs @@ -17,14 +17,17 @@ [] (let [state (reagent/atom {:previews-length "3"})] (fn [] - (let [previews-length (min 6 (utils.number/parse-int (:previews-length @state)))] + (let [previews-length (min 6 (utils.number/parse-int (:previews-length @state))) + padding 20] [rn/view {:style {:padding-bottom 150}} [preview/customizer state descriptor] [rn/view {:style {:align-items :center :margin-top 50}} [quo/url-preview-list - {:horizontal-spacing 20 + {:horizontal-spacing padding + :preview-width (- (:width (rn/get-window)) + (* 2 padding)) :on-clear #(js/alert "Clear button pressed") :key-fn :url :data (for [index (range previews-length) diff --git a/src/status_im2/subs/chat/chats.cljs b/src/status_im2/subs/chat/chats.cljs index f6853fcd6a..70e5f09e0f 100644 --- a/src/status_im2/subs/chat/chats.cljs +++ b/src/status_im2/subs/chat/chats.cljs @@ -401,3 +401,15 @@ :<- [:chat/inputs-with-mentions] (fn [[chat-id cursor]] (get cursor chat-id))) + +(re-frame/reg-sub + :chats/link-previews-unfurled + :<- [:chat/link-previews] + (fn [previews] + (get previews :unfurled))) + +(re-frame/reg-sub + :chats/link-previews? + :<- [:chats/link-previews-unfurled] + (fn [previews] + (boolean (seq previews)))) diff --git a/src/status_im2/subs/chat/messages.cljs b/src/status_im2/subs/chat/messages.cljs index a15a4aab70..187ec4e797 100644 --- a/src/status_im2/subs/chat/messages.cljs +++ b/src/status_im2/subs/chat/messages.cljs @@ -144,6 +144,12 @@ (fn [messages [_ chat-id]] (get messages chat-id {}))) +(re-frame/reg-sub + :chats/message-link-previews + :<- [:messages/messages] + (fn [messages [_ chat-id message-id]] + (get-in messages [chat-id message-id :link-previews]))) + (re-frame/reg-sub :chats/pinned :<- [:messages/pin-messages] @@ -251,4 +257,3 @@ (if (= mention constants/everyone-mention-id) (i18n/label :t/everyone-mention) contact-name))) - diff --git a/src/status_im2/subs/root.cljs b/src/status_im2/subs/root.cljs index 920a88c708..9a883396c4 100644 --- a/src/status_im2/subs/root.cljs +++ b/src/status_im2/subs/root.cljs @@ -68,7 +68,6 @@ (reg-root-key-sub :supported-biometric-auth :supported-biometric-auth) (reg-root-key-sub :connectivity/ui-status-properties :connectivity/ui-status-properties) (reg-root-key-sub :logged-in-since :logged-in-since) -(reg-root-key-sub :link-previews-whitelist :link-previews-whitelist) (reg-root-key-sub :app-state :app-state) (reg-root-key-sub :home-items-show-number :home-items-show-number) (reg-root-key-sub :waku/v2-peer-stats :peer-stats) @@ -194,6 +193,11 @@ (reg-root-key-sub :wallet/swap-to-token-amount :wallet/swap-to-token-amount) (reg-root-key-sub :wallet/swap-advanced-mode? :wallet/swap-advanced-mode?) +;;; Link previews + +(reg-root-key-sub :link-previews-whitelist :link-previews-whitelist) +(reg-root-key-sub :chat/link-previews :chat/link-previews) + ;;commands (reg-root-key-sub :commands/select-account :commands/select-account) diff --git a/status-go-version.json b/status-go-version.json index 57641ab88c..304a366944 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -3,7 +3,7 @@ "_comment": "Instead use: scripts/update-status-go.sh ", "owner": "status-im", "repo": "status-go", - "version": "v0.151.11", - "commit-sha1": "c7aebfeed36754e03d002cda03ff3339d3d1d2f7", - "src-sha256": "162v2r5af8m9ia1pd4wxip9a472cg65z8k25rrip3dc31pb5gw8d" + "version": "v0.151.12", + "commit-sha1": "6fa8c113821509d744b81d90dc21c1e79c575577", + "src-sha256": "02mmhy4z6gd9xlqjhw4i4j48gd85pijyyga48if9dd3zasqkp3y1" } diff --git a/translations/en.json b/translations/en.json index 016b2f80ad..ff73ceac6f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2083,6 +2083,7 @@ "scan-sync-qr-code": "Scan QR code", "enter-sync-code": "Enter sync code", "enable-access-to-camera": "Enable access to camera", + "link-preview-loading-message": "Generating preview", "to-scan-a-qr-enable-your-camera": "To scan a QR, enable your camera", "enable-camera": "Enable camera", "ensure-qr-code-in-focus-to-scan": "Ensure that the QR code is in focus to scan",