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.
This commit is contained in:
parent
a17efa7299
commit
19526508f2
|
@ -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
|
||||
|
|
|
@ -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])]
|
||||
|
|
|
@ -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}])
|
||||
|
|
|
@ -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}]]))
|
||||
|
|
|
@ -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}}]])))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -202,6 +202,7 @@
|
|||
:text nil}
|
||||
:outgoing false
|
||||
:outgoing-status nil
|
||||
:link-previews []
|
||||
:quoted-message nil}
|
||||
:name "0x04d03f"
|
||||
:read true
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))}))
|
|
@ -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"}}}))))
|
|
@ -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})
|
|
@ -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)}]))
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]]]]))
|
||||
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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}}])])))
|
|
@ -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))
|
|
@ -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]])
|
||||
|
|
|
@ -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}}]]]))))
|
||||
|
||||
|
|
|
@ -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")}]]]])))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"_comment": "Instead use: scripts/update-status-go.sh <rev>",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue