Skip to content

Commit

Permalink
New link previews (initial implementation) (#15891)
Browse files Browse the repository at this point in the history
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 status-im/status-go#3471.

Fixes #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
  status-im/status-go#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.
  • Loading branch information
ilmotta authored May 18, 2023
1 parent a17efa7 commit 1952650
Show file tree
Hide file tree
Showing 34 changed files with 664 additions and 129 deletions.
6 changes: 6 additions & 0 deletions src/quo2/components/links/link_preview/component_spec.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/quo2/components/links/link_preview/view.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
[logo]
[rn/image
{:accessibility-label :logo
:source logo
:source (if (string? logo)
{:uri logo}
logo)
:style style/logo}])

(defn view
Expand All @@ -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])]
Expand Down
6 changes: 5 additions & 1 deletion src/quo2/components/links/url_preview/component_spec.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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}])
Expand Down
9 changes: 6 additions & 3 deletions src/quo2/components/links/url_preview/view.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}]]))
27 changes: 10 additions & 17 deletions src/quo2/components/links/url_preview_list/view.cljs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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}}]])))
Expand Down
26 changes: 16 additions & 10 deletions src/status_im/chat/models/input.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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-im.utils.utils :as utils]
[status-im2.constants :as constants]
[utils.re-frame :as rf]
[status-im2.contexts.chat.composer.link-preview.events :as link-preview]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[status-im.utils.utils :as utils]
[taoensso.timbre :as log]))
[utils.re-frame :as rf]))

(defn text->emoji
"Replaces emojis in a specified `text`"
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/status_im/data_store/activities_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/status_im/data_store/messages.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/status_im/data_store/messages_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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"
Expand All @@ -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))))))
58 changes: 29 additions & 29 deletions src/status_im/transport/message/protocol.cljs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
1 change: 1 addition & 0 deletions src/status_im2/contexts/activity_center/events_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
:text nil}
:outgoing false
:outgoing-status nil
:link-previews []
:quoted-message nil}
:name "0x04d03f"
:read true
Expand Down
6 changes: 6 additions & 0 deletions src/status_im2/contexts/chat/composer/constants.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Loading

0 comments on commit 1952650

Please sign in to comment.