Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
183b98d09c | ||
|
|
19526508f2 | ||
|
|
a17efa7299 | ||
|
|
c7bc16608a | ||
|
|
3c9ad509cd | ||
|
|
eea55b22d7 | ||
|
|
7efd1e5d6b | ||
|
|
b264d25b69 | ||
|
|
03f501ca7b | ||
|
|
6598873343 | ||
|
|
a60235abf3 | ||
|
|
b5a8f0a127 | ||
|
|
7d0dfec22c | ||
|
|
51fa99affa | ||
|
|
369aed389c | ||
|
|
10d9c34559 | ||
|
|
8ccf2d1596 |
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
/* Options section can't access functions in objects. */
|
||||
def isPRBuild = utils.isPRBuild()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
agent { label 'linux' }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
/* Options section can't access functions in objects. */
|
||||
def isPRBuild = utils.isPRBuild()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
agent { label params.AGENT_LABEL }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
/* Options section can't access functions in objects. */
|
||||
def isPRBuild = utils.isPRBuild()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
agent { label 'macos' }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
agent { label 'linux' }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library 'status-jenkins-lib@v1.7.3'
|
||||
library 'status-jenkins-lib@v1.7.5'
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
|
||||
@@ -365,6 +365,43 @@ dispatch.
|
||||
(str "Hello " username)]])
|
||||
```
|
||||
|
||||
### Registering effects
|
||||
|
||||
When registering re-frame effects (`reg-fx`), prefer to expose a data-only
|
||||
interface because that will allow event handlers to stay pure.
|
||||
|
||||
For instance, if an effect needs a `on-success` callback, allow it to receive a
|
||||
*re-frame event vector*. This approach is used by us in the [json-rpc/call
|
||||
effect](src/status_im2/common/json_rpc/events.cljs), but also by third-party
|
||||
effects, such as https://github.com/Day8/re-frame-http-fx. For the complete
|
||||
rationale, see [PR #15936](https://github.com/status-im/status-mobile/pull/15936).
|
||||
|
||||
### Using the effect `:json-rpc/call`
|
||||
|
||||
Prefer the pure version of `:json-rpc/call` (no callbacks).
|
||||
|
||||
```clojure
|
||||
;; not as good
|
||||
(rf/defn accept-contact-request
|
||||
{:events [:activity-center.contact-requests/accept]}
|
||||
[_ contact-id]
|
||||
{:json-rpc/call
|
||||
[{:method "wakuext_acceptContactRequest"
|
||||
:params [{:id contact-id}]
|
||||
:on-success #(rf/dispatch [:sanitize-messages-and-process-response %])
|
||||
:on-error #(rf/dispatch [:activity-center.contact-requests/accept-error contact-id %])}]})
|
||||
|
||||
;; better
|
||||
(rf/defn accept-contact-request
|
||||
{:events [:activity-center.contact-requests/accept]}
|
||||
[_ contact-id]
|
||||
{:json-rpc/call
|
||||
[{:method "wakuext_acceptContactRequest"
|
||||
:params [{:id contact-id}]
|
||||
:on-success [:sanitize-messages-and-process-response]
|
||||
:on-error [:activity-center.contact-requests/accept-error contact-id]}]})
|
||||
```
|
||||
|
||||
### Registering event handlers
|
||||
|
||||
Events must always be declared with the `utils.fx/defn` macro. Also, don't use
|
||||
|
||||
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 838 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -8,7 +8,7 @@
|
||||
{:label (colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-40 override-theme)
|
||||
:icon (colors/theme-colors colors/neutral-80-opa-70 colors/white-opa-70 override-theme)
|
||||
:button-border (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 override-theme)
|
||||
:password-icon (colors/theme-colors colors/neutral-100 colors/white override-theme)
|
||||
:password-icon (colors/theme-colors colors/neutral-100 colors/white-opa-70 override-theme)
|
||||
:clear-icon (colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 override-theme)
|
||||
:cursor (colors/theme-colors (colors/custom-color :blue 50)
|
||||
colors/white
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
(ns quo2.components.keycard.component-spec
|
||||
(:require [quo2.components.keycard.view :as keycard]
|
||||
[test-helpers.component :as h]
|
||||
[utils.i18n :as i18n]))
|
||||
|
||||
(h/describe "keycard component"
|
||||
(h/test "Render of keycard component when: Status = Empty, Locked = False"
|
||||
(h/render [keycard/keycard])
|
||||
(-> (h/expect (h/query-by-label-text :holder-name))
|
||||
(h/is-equal (i18n/label :t/empty-keycard))))
|
||||
|
||||
(h/test "Render of keycard component when: Status = Filled, Locked = False"
|
||||
(h/render [keycard/keycard {:holder-name? "Alisha"}])
|
||||
(-> (h/expect (h/query-by-label-text :holder-name))
|
||||
(h/is-equal (i18n/label :t/user-keycard {:name :holder-name}))))
|
||||
|
||||
(h/test "Render of keycard component when: Status = Filled, Locked = True"
|
||||
(h/render [keycard/keycard {:holder-name? "Alisha"}])
|
||||
(-> (h/expect (h/query-by-label-text :holder-name))
|
||||
(h/is-equal (i18n/label :t/user-keycard {:name :holder-name})))))
|
||||
@@ -0,0 +1,42 @@
|
||||
(ns quo2.components.keycard.style
|
||||
(:require [quo2.foundations.colors :as colors]))
|
||||
|
||||
(def keycard-height 210)
|
||||
(def keycard-chip-height 28)
|
||||
|
||||
(defn card-container
|
||||
[locked?]
|
||||
{:overflow :hidden
|
||||
:height keycard-height
|
||||
:align-items :flex-start
|
||||
:justify-content :space-between
|
||||
:border-width 1
|
||||
:border-color (if locked?
|
||||
colors/white-opa-5
|
||||
(colors/theme-colors colors/neutral-20 colors/neutral-80))
|
||||
:border-style :solid
|
||||
:border-radius 16
|
||||
:padding 16
|
||||
:background-color (if locked?
|
||||
(colors/theme-colors colors/danger colors/danger-opa-40)
|
||||
(colors/theme-colors colors/neutral-5 colors/neutral-90))})
|
||||
|
||||
(defn keycard-logo
|
||||
[locked?]
|
||||
{:tint-color (if locked? colors/white (colors/theme-colors colors/neutral-100 colors/white))})
|
||||
|
||||
(defn keycard-watermark
|
||||
[locked?]
|
||||
{:position :absolute
|
||||
:tint-color (if locked? colors/white-opa-5 (colors/theme-colors colors/neutral-100 colors/white))
|
||||
:align-self :center
|
||||
:height 280.48
|
||||
:transform [{:rotate "-30deg"} {:translateY -30}]
|
||||
:opacity (when-not locked? 0.02)
|
||||
:z-index 1})
|
||||
|
||||
(def keycard-chip
|
||||
{:height keycard-chip-height
|
||||
:position :absolute
|
||||
:right 16
|
||||
:top (/ (- keycard-height 28) 2)})
|
||||
@@ -0,0 +1,38 @@
|
||||
(ns quo2.components.keycard.view
|
||||
(:require [react-native.core :as rn]
|
||||
[quo2.components.keycard.style :as style]
|
||||
[quo2.components.tags.tag :as tag]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[quo2.foundations.resources :as resources]
|
||||
[utils.i18n :as i18n]))
|
||||
|
||||
(defn keycard
|
||||
"This component based on the following properties:
|
||||
- :holder-name - Can be owner's name. Default is Empty
|
||||
- :locked? - Boolean to specify whether the keycard is locked or not
|
||||
"
|
||||
[{:keys [holder-name locked?]}]
|
||||
(let [label (if (boolean holder-name)
|
||||
(i18n/label :t/user-keycard {:name holder-name})
|
||||
(i18n/label :t/empty-keycard))]
|
||||
[rn/view {:style (style/card-container locked?)}
|
||||
[rn/image
|
||||
{:source (resources/get-image :keycard-logo)
|
||||
:style (style/keycard-logo locked?)}]
|
||||
[rn/image
|
||||
{:source (resources/get-image
|
||||
(if (or locked? (colors/dark?)) :keycard-chip-dark :keycard-chip-light))
|
||||
:style style/keycard-chip}]
|
||||
[rn/image
|
||||
{:source (resources/get-image :keycard-watermark)
|
||||
:style (style/keycard-watermark locked?)}]
|
||||
[tag/tag
|
||||
{:size 32
|
||||
:type (when locked? :icon)
|
||||
:label label
|
||||
:labelled? true
|
||||
:blurred? true
|
||||
:resource (when locked? :i/locked)
|
||||
:accessibility-label :holder-name
|
||||
:icon-color colors/white-70-blur
|
||||
:override-theme (when locked? :dark)}]]))
|
||||
@@ -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}}]])))
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
(ns quo2.components.share.qr-code.view
|
||||
(:require [quo2.components.share.qr-code.style :as style]
|
||||
[react-native.core :as rn]))
|
||||
[react-native.core :as rn]
|
||||
[react-native.fast-image :as fast-image]))
|
||||
|
||||
(defn qr-code
|
||||
[{:keys [source width height]}]
|
||||
[rn/view
|
||||
{:style style/container}
|
||||
[rn/image
|
||||
[fast-image/fast-image
|
||||
{:source source
|
||||
:style (style/image width height)}]])
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[quo2.components.icon :as icons]
|
||||
[quo2.components.markdown.text :as text]
|
||||
[quo2.components.tags.context-tag.style :as style]
|
||||
[quo2.components.avatars.user-avatar.style :as user-avatar-style]
|
||||
[react-native.core :as rn]))
|
||||
|
||||
(defn trim-public-key
|
||||
@@ -39,16 +40,21 @@
|
||||
(trim-public-key public-key)]])
|
||||
|
||||
(defn context-tag
|
||||
[{:keys [text-style blur?] :as params} photo name channel-name]
|
||||
(let [text-params {:weight :medium
|
||||
:size :paragraph-2
|
||||
:style (assoc text-style :justify-content :center)}]
|
||||
[{:keys [text-style blur? no-avatar-placeholder?] :as params} photo name channel-name]
|
||||
(let [text-params {:weight :medium
|
||||
:size :paragraph-2
|
||||
:style (assoc text-style :justify-content :center)}
|
||||
empty-photo? (empty? photo)
|
||||
avatar-size :xxs
|
||||
avatar-outer-size (get-in user-avatar-style/sizes [avatar-size :outer])]
|
||||
[base-tag (assoc-in params [:style :padding-left] 3)
|
||||
[user-avatar/user-avatar
|
||||
{:full-name name
|
||||
:profile-picture photo
|
||||
:size :xxs
|
||||
:status-indicator? false}]
|
||||
(if (and empty-photo? no-avatar-placeholder?)
|
||||
[rn/view {:style {:width avatar-outer-size}}]
|
||||
[user-avatar/user-avatar
|
||||
{:full-name name
|
||||
:profile-picture photo
|
||||
:size avatar-size
|
||||
:status-indicator? false}])
|
||||
[rn/view {:style style/context-tag-text-container}
|
||||
[text/text text-params (str " " name)]
|
||||
(when channel-name
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
quo2.components.inputs.recovery-phrase.view
|
||||
quo2.components.inputs.search-input.view
|
||||
quo2.components.inputs.title-input.view
|
||||
quo2.components.keycard.view
|
||||
quo2.components.links.url-preview-list.view
|
||||
quo2.components.links.url-preview.view
|
||||
quo2.components.links.link-preview.view
|
||||
@@ -126,6 +127,7 @@
|
||||
|
||||
;;;; CARDS
|
||||
(def small-option-card quo2.components.onboarding.small-option-card.view/small-option-card)
|
||||
(def keycard quo2.components.keycard.view/keycard)
|
||||
|
||||
;;;; COLORS
|
||||
(def color-picker quo2.components.colors.color-picker.view/view)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
(ns quo2.foundations.resources)
|
||||
|
||||
(def ui
|
||||
{:keycard-logo (js/require "../resources/images/ui2/keycard-logo.png")
|
||||
:keycard-chip-light (js/require "../resources/images/ui2/keycard-chip-light.png")
|
||||
:keycard-chip-dark (js/require "../resources/images/ui2/keycard-chip-dark.png")
|
||||
:keycard-watermark (js/require "../resources/images/ui2/keycard-watermark.png")})
|
||||
|
||||
(defn get-image
|
||||
[k]
|
||||
(get ui k))
|
||||
@@ -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]
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
:style {:width 40
|
||||
:height 40}}]
|
||||
(seq thumbnail-image)
|
||||
[photos/photo thumbnail-image {:size 40}]
|
||||
[photos/photo {:uri thumbnail-image} {:size 40}]
|
||||
|
||||
:else
|
||||
[chat-icon.screen/chat-icon-view-chat-list
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
[status-im.wallet.choose-recipient.core :as choose-recipient]
|
||||
[status-im2.navigation.events :as navigation]
|
||||
[taoensso.timbre :as log]
|
||||
[native-module.core :as native-module]))
|
||||
[native-module.core :as native-module]
|
||||
[react-native.platform :as platform]))
|
||||
|
||||
;; TODO(yenda) investigate why `handle-universal-link` event is
|
||||
;; dispatched 7 times for the same link
|
||||
@@ -175,10 +176,15 @@
|
||||
|
||||
(rf/defn handle-url
|
||||
"Store url in the database if the user is not logged in, to be processed
|
||||
on login, otherwise just handle it"
|
||||
on login, otherwise just handle it. On Android platform the app-state value
|
||||
in db is `background` immediately after account creation, hence we avoid the
|
||||
app-state check for android platforms to fix this e2e blocker :
|
||||
https://github.com/status-im/status-mobile/issues/15859
|
||||
"
|
||||
{:events [:universal-links/handle-url]}
|
||||
[{:keys [db] :as cofx} url]
|
||||
(if (and (multiaccounts.model/logged-in? db) (= (:app-state db) "active"))
|
||||
(if (and (multiaccounts.model/logged-in? db)
|
||||
(or platform/android? (= (:app-state db) "active")))
|
||||
(route-url cofx url)
|
||||
(store-url-for-later cofx url)))
|
||||
|
||||
|
||||
@@ -95,7 +95,9 @@
|
||||
(assoc button-common-props :accessibility-label :open-scanner-button)
|
||||
:i/scan]
|
||||
[quo/button
|
||||
(assoc button-common-props :accessibility-label :show-qr-button)
|
||||
(merge button-common-props
|
||||
{:accessibility-label :show-qr-button
|
||||
:on-press #(rf/dispatch [:open-modal :share-shell])})
|
||||
:i/qr-code]
|
||||
[rn/view
|
||||
[unread-indicator]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
(ns status-im2.common.json-rpc.events
|
||||
(:require [clojure.string :as string]
|
||||
[native-module.core :as native-module]
|
||||
[re-frame.core :as re-frame]
|
||||
[react-native.background-timer :as background-timer]
|
||||
[native-module.core :as native-module]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.transforms :as transforms]))
|
||||
|
||||
(defn on-error-retry
|
||||
(defn- on-error-retry
|
||||
[call-method {:keys [method number-of-retries delay on-error] :as arg}]
|
||||
(if (pos? number-of-retries)
|
||||
(fn [error]
|
||||
@@ -24,6 +25,31 @@
|
||||
on-error))
|
||||
|
||||
(defn call
|
||||
"Call private RPC endpoint.
|
||||
|
||||
method: string - The name of an endpoint function in status-go, with the first
|
||||
character lowercased and prefixed by wakuext_. For example, the BackupData
|
||||
function should be represented as the string wakuext_backupData.
|
||||
|
||||
params: sequence - A positional sequence of zero or more arguments.
|
||||
|
||||
on-success/on-error: function/vector (optional) - When a function, it will be
|
||||
called with the transformed response as the sole argument. When a vector, it
|
||||
is expected to be a valid re-frame event vector, and the event will be
|
||||
dispatched with the transformed response conj'ed at the end.
|
||||
|
||||
js-response: boolean - When non-nil, the successful response will not be
|
||||
recursively converted to Clojure data structures. Default: nil.
|
||||
|
||||
number-of-retries: integer - The maximum number of retries in case of failure.
|
||||
Default: nil.
|
||||
|
||||
delay: integer - The number of milliseconds to wait between retries. Default:
|
||||
nil.
|
||||
|
||||
Note that on-error is optional, but if not provided, a default implementation
|
||||
will be used.
|
||||
"
|
||||
[{:keys [method params on-success on-error js-response] :as arg}]
|
||||
(let [params (or params [])
|
||||
on-error (or on-error
|
||||
@@ -34,15 +60,25 @@
|
||||
:id 1
|
||||
:method method
|
||||
:params params})
|
||||
(fn [response]
|
||||
(if (string/blank? response)
|
||||
(on-error {:message "Blank response"})
|
||||
(let [response-js (transforms/json->js response)]
|
||||
(if (.-error response-js)
|
||||
(on-error (transforms/js->clj (.-error response-js)))
|
||||
(on-success (if js-response
|
||||
(.-result response-js)
|
||||
(transforms/js->clj (.-result response-js)))))))))))
|
||||
(fn [raw-response]
|
||||
(if (string/blank? raw-response)
|
||||
(let [error {:message "Blank response"}]
|
||||
(if (vector? on-error)
|
||||
(rf/dispatch (conj on-error error))
|
||||
(on-error error)))
|
||||
(let [^js response-js (transforms/json->js raw-response)]
|
||||
(if-let [error (.-error response-js)]
|
||||
(let [error (transforms/js->clj error)]
|
||||
(if (vector? on-error)
|
||||
(rf/dispatch (conj on-error error))
|
||||
(on-error error)))
|
||||
(when on-success
|
||||
(let [result (if js-response
|
||||
(.-result response-js)
|
||||
(transforms/js->clj (.-result response-js)))]
|
||||
(if (vector? on-success)
|
||||
(rf/dispatch (conj on-success result))
|
||||
(on-success result)))))))))))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:json-rpc/call
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
:use-keycard (js/require "../resources/images/ui2/keycard.png")
|
||||
:onboarding-illustration (js/require "../resources/images/ui2/onboarding_illustration.png")
|
||||
:qr-code (js/require "../resources/images/ui2/qr-code.png")
|
||||
})
|
||||
:keycard-logo (js/require "../resources/images/ui2/keycard-logo.png")
|
||||
:keycard-chip-light (js/require "../resources/images/ui2/keycard-chip-light.png")
|
||||
:keycard-chip-dark (js/require "../resources/images/ui2/keycard-chip-dark.png")
|
||||
:keycard-watermark (js/require "../resources/images/ui2/keycard-watermark.png")})
|
||||
|
||||
(def mock-images
|
||||
{:coinbase (js/require "../resources/images/mock2/coinbase.png")
|
||||
|
||||
@@ -112,12 +112,9 @@
|
||||
(when-let [notification (get-notification db notification-id)]
|
||||
{:json-rpc/call [{:method "wakuext_markActivityCenterNotificationsRead"
|
||||
:params [[notification-id]]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/mark-as-read-success
|
||||
notification])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:notification/mark-as-read
|
||||
%])}]}))
|
||||
:on-success [:activity-center.notifications/mark-as-read-success notification]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:notification/mark-as-read]}]}))
|
||||
|
||||
(rf/defn mark-as-read-success
|
||||
{:events [:activity-center.notifications/mark-as-read-success]}
|
||||
@@ -130,12 +127,9 @@
|
||||
(when-let [notification (get-notification db notification-id)]
|
||||
{:json-rpc/call [{:method "wakuext_markActivityCenterNotificationsUnread"
|
||||
:params [[notification-id]]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/mark-as-unread-success
|
||||
notification])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:notification/mark-as-unread
|
||||
%])}]}))
|
||||
:on-success [:activity-center.notifications/mark-as-unread-success notification]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:notification/mark-as-unread]}]}))
|
||||
|
||||
(rf/defn mark-as-unread-success
|
||||
{:events [:activity-center.notifications/mark-as-unread-success]}
|
||||
@@ -149,12 +143,9 @@
|
||||
(when (>= now undoable-till)
|
||||
{:json-rpc/call [{:method "wakuext_markAllActivityCenterNotificationsRead"
|
||||
:params []
|
||||
:on-success #(rf/dispatch
|
||||
[:activity-center.notifications/mark-all-as-read-success])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
nil
|
||||
:notification/mark-all-as-read
|
||||
%])}]})))
|
||||
:on-success [:activity-center.notifications/mark-all-as-read-success]
|
||||
:on-error [:activity-center/process-notification-failure nil
|
||||
:notification/mark-all-as-read]}]})))
|
||||
|
||||
(rf/defn mark-all-as-read-success
|
||||
{:events [:activity-center.notifications/mark-all-as-read-success]}
|
||||
@@ -210,17 +201,14 @@
|
||||
[{:keys [db]} notification-id]
|
||||
{:json-rpc/call [{:method "wakuext_acceptActivityCenterNotifications"
|
||||
:params [[notification-id]]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/accept-success
|
||||
notification-id %])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:notification/accept
|
||||
%])}]})
|
||||
:on-success [:activity-center.notifications/accept-success notification-id]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:notification/accept]}]})
|
||||
|
||||
(rf/defn accept-notification-success
|
||||
{:events [:activity-center.notifications/accept-success]}
|
||||
[{:keys [db] :as cofx} notification-id {:keys [chats]}]
|
||||
(let [notification (get-notification db notification-id)]
|
||||
(when-let [notification (get-notification db notification-id)]
|
||||
(rf/merge cofx
|
||||
(chat.events/ensure-chats (map data-store.chats/<-rpc chats))
|
||||
(notifications-reconcile [(assoc notification :read true :accepted true)]))))
|
||||
@@ -230,17 +218,14 @@
|
||||
[{:keys [db]} notification-id]
|
||||
{:json-rpc/call [{:method "wakuext_dismissActivityCenterNotifications"
|
||||
:params [[notification-id]]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/dismiss-success
|
||||
notification-id])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:notification/dismiss
|
||||
%])}]})
|
||||
:on-success [:activity-center.notifications/dismiss-success notification-id]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:notification/dismiss]}]})
|
||||
|
||||
(rf/defn dismiss-notification-success
|
||||
{:events [:activity-center.notifications/dismiss-success]}
|
||||
[{:keys [db] :as cofx} notification-id]
|
||||
(let [notification (get-notification db notification-id)]
|
||||
(when-let [notification (get-notification db notification-id)]
|
||||
(notifications-reconcile cofx [(assoc notification :read true :dismissed true)])))
|
||||
|
||||
(rf/defn delete-notification
|
||||
@@ -248,12 +233,9 @@
|
||||
[{:keys [db]} notification-id]
|
||||
{:json-rpc/call [{:method "wakuext_deleteActivityCenterNotifications"
|
||||
:params [[notification-id]]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/delete-success
|
||||
notification-id])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:notification/delete
|
||||
%])}]})
|
||||
:on-success [:activity-center.notifications/delete-success notification-id]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:notification/delete]}]})
|
||||
|
||||
(rf/defn delete-notification-success
|
||||
{:events [:activity-center.notifications/delete-success]}
|
||||
@@ -268,48 +250,36 @@
|
||||
[_ notification-id]
|
||||
{:json-rpc/call [{:method "wakuext_declineContactVerificationRequest"
|
||||
:params [notification-id]
|
||||
:on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response
|
||||
%])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:contact-verification/decline
|
||||
%])}]})
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/decline]}]})
|
||||
|
||||
(rf/defn contact-verification-reply
|
||||
{:events [:activity-center.contact-verification/reply]}
|
||||
[_ notification-id reply]
|
||||
{:json-rpc/call [{:method "wakuext_acceptContactVerificationRequest"
|
||||
:params [notification-id reply]
|
||||
:on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response
|
||||
%])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:contact-verification/reply
|
||||
%])}]})
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/reply]}]})
|
||||
|
||||
(rf/defn contact-verification-mark-as-trusted
|
||||
{:events [:activity-center.contact-verification/mark-as-trusted]}
|
||||
[_ notification-id]
|
||||
{:json-rpc/call [{:method "wakuext_verifiedTrusted"
|
||||
:params [{:id notification-id}]
|
||||
:on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response
|
||||
%])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:contact-verification/mark-as-trusted
|
||||
%])}]})
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/mark-as-trusted]}]})
|
||||
|
||||
(rf/defn contact-verification-mark-as-untrustworthy
|
||||
{:events [:activity-center.contact-verification/mark-as-untrustworthy]}
|
||||
[_ notification-id]
|
||||
{:json-rpc/call [{:method "wakuext_verifiedUntrustworthy"
|
||||
:params [{:id notification-id}]
|
||||
:on-success #(rf/dispatch [:activity-center/reconcile-notifications-from-response
|
||||
%])
|
||||
:on-error #(rf/dispatch [:activity-center/process-notification-failure
|
||||
notification-id
|
||||
:contact-verification/mark-as-untrustworthy
|
||||
%])}]})
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/mark-as-untrustworthy]}]})
|
||||
|
||||
;;;; Notifications fetching and pagination
|
||||
|
||||
@@ -368,10 +338,9 @@
|
||||
:limit per-page
|
||||
:activityTypes (filter-type->rpc-param filter-type)
|
||||
:readType (->rpc-read-type filter-status)}]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/fetch-success
|
||||
reset-data? %])
|
||||
:on-error #(rf/dispatch [:activity-center.notifications/fetch-error
|
||||
filter-type filter-status %])}]})))
|
||||
:on-success [:activity-center.notifications/fetch-success reset-data?]
|
||||
:on-error [:activity-center.notifications/fetch-error filter-type
|
||||
filter-status]}]})))
|
||||
|
||||
(rf/defn notifications-fetch-first-page
|
||||
{:events [:activity-center.notifications/fetch-first-page]}
|
||||
@@ -434,9 +403,8 @@
|
||||
:limit 20
|
||||
:activityTypes [types/contact-request]
|
||||
:readType (->rpc-read-type :unread)}]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/fetch-pending-contact-requests-success %])
|
||||
:on-error #(rf/dispatch [:activity-center.notifications/fetch-error types/contact-request :unread
|
||||
%])}]})
|
||||
:on-success [:activity-center.notifications/fetch-pending-contact-requests-success]
|
||||
:on-error [:activity-center.notifications/fetch-error types/contact-request :unread]}]})
|
||||
|
||||
(rf/defn notifications-fetch-pending-contact-requests-success
|
||||
{:events [:activity-center.notifications/fetch-pending-contact-requests-success]}
|
||||
@@ -464,8 +432,8 @@
|
||||
{:json-rpc/call
|
||||
[{:method "wakuext_hasUnseenActivityCenterNotifications"
|
||||
:params []
|
||||
:on-success #(rf/dispatch [:activity-center/update-seen-state-success %])
|
||||
:on-error #(rf/dispatch [:activity-center/update-seen-state-error %])}]})
|
||||
:on-success [:activity-center/update-seen-state-success]
|
||||
:on-error [:activity-center/update-seen-state-error]}]})
|
||||
|
||||
(rf/defn update-seen-state-success
|
||||
{:events [:activity-center/update-seen-state-success]}
|
||||
@@ -485,8 +453,8 @@
|
||||
{:json-rpc/call
|
||||
[{:method "wakuext_markAsSeenActivityCenterNotifications"
|
||||
:params []
|
||||
:on-success #(rf/dispatch [:activity-center/mark-as-seen-success %])
|
||||
:on-error #(rf/dispatch [:activity-center/mark-as-seen-error %])}]})
|
||||
:on-success [:activity-center/mark-as-seen-success]
|
||||
:on-error [:activity-center/mark-as-seen-error]}]})
|
||||
|
||||
(rf/defn mark-as-seen-success
|
||||
{:events [:activity-center/mark-as-seen-success]}
|
||||
@@ -509,8 +477,8 @@
|
||||
[{:method "wakuext_activityCenterNotificationsCount"
|
||||
:params [{:activityTypes types/all-supported
|
||||
:readType (->rpc-read-type :unread)}]
|
||||
:on-success #(rf/dispatch [:activity-center.notifications/fetch-unread-count-success %])
|
||||
:on-error #(rf/dispatch [:activity-center.notifications/fetch-unread-count-error %])}]})
|
||||
:on-success [:activity-center.notifications/fetch-unread-count-success]
|
||||
:on-error [:activity-center.notifications/fetch-unread-count-error]}]})
|
||||
|
||||
(rf/defn notifications-fetch-unread-count-success
|
||||
{:events [:activity-center.notifications/fetch-unread-count-success]}
|
||||
|
||||
@@ -1,163 +1,169 @@
|
||||
(ns status-im2.contexts.activity-center.events-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[status-im2.constants :as constants]
|
||||
status-im.events
|
||||
[test-helpers.unit :as h]
|
||||
[status-im2.contexts.activity-center.events :as events]
|
||||
[status-im2.contexts.activity-center.notification-types :as types]
|
||||
[utils.re-frame :as rf]))
|
||||
[test-helpers.unit :as h]))
|
||||
|
||||
(h/use-log-fixture)
|
||||
|
||||
(def notification-id "0x1")
|
||||
|
||||
(defn setup
|
||||
[]
|
||||
(h/register-helper-events)
|
||||
(rf/dispatch [:setup/app-started]))
|
||||
|
||||
(defn test-log-on-failure
|
||||
[{:keys [before-test notification-id event action]}]
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(when before-test
|
||||
(before-test))
|
||||
(h/stub-fx-with-callbacks :json-rpc/call :on-error (constantly :fake-error))
|
||||
|
||||
(rf/dispatch event)
|
||||
|
||||
(is (= {:args [(str "Failed to " action)
|
||||
{:notification-id notification-id
|
||||
:error :fake-error}]
|
||||
:level :warn}
|
||||
(last @h/logs)))))
|
||||
|
||||
;;;; Misc
|
||||
|
||||
(deftest open-activity-center-test
|
||||
(testing "opens the activity center with filters enabled"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(rf/dispatch [:activity-center/open
|
||||
{:filter-type types/contact-request
|
||||
:filter-status :unread}])
|
||||
|
||||
(is (= {:status :unread
|
||||
:type types/contact-request}
|
||||
(get-in (h/db) [:activity-center :filter])))))
|
||||
|
||||
(testing "opens the activity center with default filters"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(is (= {:db {}
|
||||
:dispatch [:open-modal :activity-center {}]
|
||||
:dispatch-later [{:ms 1000 :dispatch [:activity-center/mark-as-seen]}]}
|
||||
(events/open-activity-center {:db {}} nil))))
|
||||
|
||||
(rf/dispatch [:activity-center/open])
|
||||
(testing "opens the activity center with filters enabled"
|
||||
(is (= {:db {:activity-center {:filter {:status :unread :type types/contact-request}}}
|
||||
:dispatch [:open-modal :activity-center {}]
|
||||
:dispatch-later [{:ms 1000 :dispatch [:activity-center/mark-as-seen]}]}
|
||||
(events/open-activity-center {:db {}}
|
||||
{:filter-type types/contact-request
|
||||
:filter-status :unread})))))
|
||||
|
||||
(is (= {:status :unread :type types/no-type}
|
||||
(get-in (h/db) [:activity-center :filter]))))))
|
||||
(deftest process-notification-failure-test
|
||||
(testing "logs and returns nil"
|
||||
(is (nil? (events/process-notification-failure
|
||||
{:db {}}
|
||||
notification-id
|
||||
:some-action-name
|
||||
:some-error)))
|
||||
(is (= {:args ["Failed to :some-action-name"
|
||||
{:notification-id notification-id
|
||||
:error :some-error}]
|
||||
:level :warn}
|
||||
(last @h/logs)))))
|
||||
|
||||
;;;; Mark as read/unread
|
||||
|
||||
(deftest mark-as-read-test
|
||||
(testing "does nothing if the notification ID cannot be found in the app db"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [spy-queue (atom [])]
|
||||
(h/spy-fx spy-queue :json-rpc/call)
|
||||
(let [notifications [{:id notification-id
|
||||
:read false
|
||||
:type types/one-to-one-chat}]]
|
||||
(rf/dispatch [:test/assoc-in [:activity-center :notifications] notifications])
|
||||
(let [cofx {:db {:activity-center
|
||||
{:notifications [{:id "0x1"
|
||||
:read false
|
||||
:type types/one-to-one-chat}]}}}]
|
||||
(is (nil? (events/mark-as-read cofx "0x99")))))
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/mark-as-read "0x666"])
|
||||
(testing "dispatches RPC call"
|
||||
(let [notif {:id "0x1" :read false :type types/one-to-one-chat}
|
||||
cofx {:db {:activity-center {:notifications [notif]}}}]
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_markActivityCenterNotificationsRead"
|
||||
:params [[(:id notif)]]
|
||||
:on-success [:activity-center.notifications/mark-as-read-success notif]
|
||||
:on-error [:activity-center/process-notification-failure (:id notif)
|
||||
:notification/mark-as-read]}]}
|
||||
(events/mark-as-read cofx (:id notif)))))))
|
||||
|
||||
(is (= [] @spy-queue))
|
||||
(is (= notifications (get-in (h/db) [:activity-center :notifications])))))))
|
||||
(deftest mark-as-read-success-test
|
||||
(let [f-args (atom [])
|
||||
cofx {:db {}}
|
||||
notif {:id "0x1" :read false :type types/one-to-one-chat}]
|
||||
(with-redefs [events/notifications-reconcile
|
||||
(fn [& args]
|
||||
(reset! f-args args)
|
||||
:result)]
|
||||
(is (= :result (events/mark-as-read-success cofx notif)))
|
||||
(is (= [cofx [(assoc notif :read true)]]
|
||||
@f-args)))))
|
||||
|
||||
(testing "marks notifications as read and updates app db"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat}
|
||||
notif-2 {:id "0x2" :read false :type types/one-to-one-chat}
|
||||
notif-3 {:id "0x3" :read false :type types/one-to-one-chat}
|
||||
new-notif-3 (assoc notif-3 :read true)
|
||||
new-notif-2 (assoc notif-2 :read true)]
|
||||
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly nil))
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:status :all :type types/no-type}
|
||||
:notifications [notif-3 notif-2 notif-1]}])
|
||||
(deftest mark-as-unread-test
|
||||
(testing "does nothing if the notification ID cannot be found in the app db"
|
||||
(let [cofx {:db {:activity-center
|
||||
{:notifications [{:id "0x1"
|
||||
:read true
|
||||
:type types/one-to-one-chat}]}}}]
|
||||
(is (nil? (events/mark-as-unread cofx "0x99")))))
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/mark-as-read (:id notif-2)])
|
||||
(is (= [notif-3 new-notif-2 notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications])))
|
||||
(testing "dispatches RPC call"
|
||||
(let [notif {:id "0x1" :read true :type types/one-to-one-chat}
|
||||
cofx {:db {:activity-center {:notifications [notif]}}}]
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_markActivityCenterNotificationsUnread"
|
||||
:params [[(:id notif)]]
|
||||
:on-success [:activity-center.notifications/mark-as-unread-success notif]
|
||||
:on-error [:activity-center/process-notification-failure (:id notif)
|
||||
:notification/mark-as-unread]}]}
|
||||
(events/mark-as-unread cofx (:id notif)))))))
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/mark-as-read (:id notif-3)])
|
||||
(is (= [new-notif-3 new-notif-2 notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
|
||||
(testing "logs on failure"
|
||||
(test-log-on-failure
|
||||
{:notification-id notification-id
|
||||
:event [:activity-center.notifications/mark-as-read notification-id]
|
||||
:action :notification/mark-as-read
|
||||
:before-test (fn []
|
||||
(rf/dispatch
|
||||
[:test/assoc-in [:activity-center :notifications]
|
||||
[{:id notification-id
|
||||
:read false
|
||||
:type types/one-to-one-chat}]]))})))
|
||||
(deftest mark-as-unread-success-test
|
||||
(let [f-args (atom [])
|
||||
cofx {:db {}}
|
||||
notif {:id "0x1" :read true :type types/one-to-one-chat}]
|
||||
(with-redefs [events/notifications-reconcile
|
||||
(fn [& args]
|
||||
(reset! f-args args)
|
||||
:reconciliation-result)]
|
||||
(is (= :reconciliation-result (events/mark-as-unread-success cofx notif)))
|
||||
(is (= [cofx [(assoc notif :read false)]]
|
||||
@f-args)))))
|
||||
|
||||
;;;; Acceptance/dismissal
|
||||
|
||||
(deftest notification-acceptance-test
|
||||
(deftest accept-notification-test
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_acceptActivityCenterNotifications"
|
||||
:params [[notification-id]]
|
||||
:on-success [:activity-center.notifications/accept-success notification-id]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:notification/accept]}]}
|
||||
(events/accept-notification {:db {}} notification-id))))
|
||||
|
||||
(deftest accept-notification-success-test
|
||||
(testing "does nothing if the notification ID cannot be found in the app db"
|
||||
(let [cofx {:db {:activity-center
|
||||
{:notifications [{:id "0x1"
|
||||
:read false
|
||||
:type types/one-to-one-chat}]}}}]
|
||||
(is (nil? (events/accept-notification-success cofx "0x99" nil)))))
|
||||
|
||||
(testing "marks notification as accepted and read, then reconciles"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :type types/private-group-chat}
|
||||
notif-2 {:id "0x2" :type types/private-group-chat}
|
||||
notif-2-accepted (assoc notif-2 :accepted true :read true)]
|
||||
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2))
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/no-type :status :all}
|
||||
:notifications [notif-2 notif-1]}])
|
||||
(let [notif-1 {:id "0x1" :type types/private-group-chat}
|
||||
notif-2 {:id "0x2" :type types/private-group-chat}
|
||||
notif-2-accepted (assoc notif-2 :accepted true :read true)
|
||||
cofx {:db {:activity-center {:filter {:type types/no-type :status :all}
|
||||
:notifications [notif-2 notif-1]}}}]
|
||||
(is (= {:db {:activity-center {:filter {:type 0 :status :all}
|
||||
:notifications [notif-2-accepted notif-1]}
|
||||
:chats {}
|
||||
:chats-home-list nil}
|
||||
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
|
||||
[:activity-center.notifications/fetch-pending-contact-requests]]}
|
||||
(events/accept-notification-success cofx (:id notif-2) nil))))))
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/accept (:id notif-2)])
|
||||
(deftest dismiss-notification-test
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_dismissActivityCenterNotifications"
|
||||
:params [[notification-id]]
|
||||
:on-success [:activity-center.notifications/dismiss-success notification-id]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:notification/dismiss]}]}
|
||||
(events/dismiss-notification {:db {}} notification-id))))
|
||||
|
||||
(is (= [notif-2-accepted notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications])))
|
||||
(deftest dismiss-notification-success-test
|
||||
(testing "does nothing if the notification ID cannot be found in the app db"
|
||||
(let [cofx {:db {:activity-center
|
||||
{:notifications [{:id "0x1"
|
||||
:read false
|
||||
:type types/one-to-one-chat}]}}}]
|
||||
(is (nil? (events/dismiss-notification-success cofx "0x99")))))
|
||||
|
||||
;; Ignores accepted notification if the Unread filter is enabled because
|
||||
;; accepted notifications are also marked as read in status-go.
|
||||
(rf/dispatch [:test/assoc-in [:activity-center :filter]
|
||||
{:filter {:type types/no-type :status :unread}}])
|
||||
(rf/dispatch [:activity-center.notifications/accept (:id notif-2)])
|
||||
(is (= [notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
|
||||
(testing "logs on failure"
|
||||
(test-log-on-failure
|
||||
{:notification-id notification-id
|
||||
:event [:activity-center.notifications/accept notification-id]
|
||||
:action :notification/accept})))
|
||||
|
||||
(deftest notification-dismissal-test
|
||||
(testing "dismisses & mark notification as read, and keep it in the app db"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :type types/private-group-chat}
|
||||
notif-2 {:id "0x2" :type types/admin}
|
||||
dismissed-notif-1 (assoc notif-1 :dismissed true :read true)]
|
||||
(h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2))
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/no-type :status :all}
|
||||
:notifications [notif-2 notif-1]}])
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/dismiss (:id notif-1)])
|
||||
|
||||
(is (= [notif-2 dismissed-notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
|
||||
(testing "logs on failure"
|
||||
(test-log-on-failure
|
||||
{:notification-id notification-id
|
||||
:event [:activity-center.notifications/dismiss notification-id]
|
||||
:action :notification/dismiss})))
|
||||
(testing "marks notification as dismissed and read, then reconciles"
|
||||
(let [notif-1 {:id "0x1" :type types/private-group-chat}
|
||||
notif-2 {:id "0x2" :type types/private-group-chat}
|
||||
notif-2-dismissed (assoc notif-2 :dismissed true :read true)
|
||||
cofx {:db {:activity-center {:filter {:type types/no-type :status :all}
|
||||
:notifications [notif-2 notif-1]}}}]
|
||||
(is (= {:db {:activity-center {:filter {:type 0 :status :all}
|
||||
:notifications [notif-2-dismissed notif-1]}}
|
||||
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
|
||||
[:activity-center.notifications/fetch-pending-contact-requests]]}
|
||||
(events/dismiss-notification-success cofx (:id notif-2)))))))
|
||||
|
||||
;;;; Contact verification
|
||||
|
||||
@@ -196,6 +202,7 @@
|
||||
:text nil}
|
||||
:outgoing false
|
||||
:outgoing-status nil
|
||||
:link-previews []
|
||||
:quoted-message nil}
|
||||
:name "0x04d03f"
|
||||
:read true
|
||||
@@ -204,339 +211,228 @@
|
||||
:type types/contact-verification})
|
||||
|
||||
(deftest contact-verification-decline-test
|
||||
(testing "declines notification and reconciles"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [spy-queue (atom [])]
|
||||
(h/stub-fx-with-callbacks :json-rpc/call
|
||||
:on-success
|
||||
(constantly contact-verification-rpc-response))
|
||||
(h/spy-fx spy-queue :json-rpc/call)
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/contact-verification :status :all}}])
|
||||
|
||||
(rf/dispatch [:activity-center.contact-verification/decline notification-id])
|
||||
|
||||
(is (= [contact-verification-expected-notification]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
|
||||
(testing "logs on failure"
|
||||
(test-log-on-failure
|
||||
{:notification-id notification-id
|
||||
:event [:activity-center.contact-verification/decline notification-id]
|
||||
:action :contact-verification/decline})))
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_declineContactVerificationRequest"
|
||||
:params [notification-id]
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/decline]}]}
|
||||
(events/contact-verification-decline {:db {}} notification-id))))
|
||||
|
||||
(deftest contact-verification-reply-test
|
||||
(testing "sends reply and reconciles"
|
||||
(let [reply "any answer"]
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [spy-queue (atom [])]
|
||||
(h/stub-fx-with-callbacks :json-rpc/call
|
||||
:on-success
|
||||
(constantly contact-verification-rpc-response))
|
||||
(h/spy-fx spy-queue :json-rpc/call)
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/contact-verification :status :all}}])
|
||||
|
||||
(rf/dispatch [:activity-center.contact-verification/reply notification-id reply])
|
||||
|
||||
(is (= [contact-verification-expected-notification]
|
||||
(get-in (h/db) [:activity-center :notifications])))))))
|
||||
|
||||
(testing "logs on failure"
|
||||
(test-log-on-failure
|
||||
{:notification-id notification-id
|
||||
:event [:activity-center.contact-verification/reply notification-id "any answer"]
|
||||
:action :contact-verification/reply})))
|
||||
(let [reply "The answer is 42"]
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_acceptContactVerificationRequest"
|
||||
:params [notification-id reply]
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/reply]}]}
|
||||
(events/contact-verification-reply {:db {}} notification-id reply)))))
|
||||
|
||||
(deftest contact-verification-mark-as-trusted-test
|
||||
(testing "app db reconciliation"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(h/stub-fx-with-callbacks :json-rpc/call
|
||||
:on-success
|
||||
(constantly contact-verification-rpc-response))
|
||||
|
||||
;; With "Unread" filter disabled
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/no-type :status :all}}])
|
||||
(rf/dispatch [:activity-center.contact-verification/mark-as-trusted notification-id])
|
||||
(is (= [contact-verification-expected-notification]
|
||||
(get-in (h/db) [:activity-center :notifications])))
|
||||
|
||||
;; With "Unread" filter enabled
|
||||
(rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread])
|
||||
(rf/dispatch [:activity-center.contact-verification/mark-as-trusted notification-id])
|
||||
(is (= [] (get-in (h/db) [:activity-center :notifications])))))
|
||||
|
||||
(testing "logs on failure"
|
||||
(test-log-on-failure
|
||||
{:notification-id notification-id
|
||||
:event [:activity-center.contact-verification/mark-as-trusted notification-id]
|
||||
:action :contact-verification/mark-as-trusted})))
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_verifiedTrusted"
|
||||
:params [{:id notification-id}]
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/mark-as-trusted]}]}
|
||||
(events/contact-verification-mark-as-trusted {:db {}} notification-id))))
|
||||
|
||||
(deftest contact-verification-mark-as-untrustworthy-test
|
||||
(testing "app db reconciliation"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(h/stub-fx-with-callbacks
|
||||
:json-rpc/call
|
||||
:on-success
|
||||
(constantly contact-verification-rpc-response))
|
||||
|
||||
;; With "Unread" filter disabled
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/no-type :status :all}}])
|
||||
(rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy notification-id])
|
||||
(is (= [contact-verification-expected-notification]
|
||||
(get-in (h/db) [:activity-center :notifications])))
|
||||
|
||||
;; With "Unread" filter enabled
|
||||
(rf/dispatch [:test/assoc-in [:activity-center :filter :status] :unread])
|
||||
(rf/dispatch [:activity-center.contact-verification/mark-as-untrustworthy notification-id])
|
||||
(is (= [] (get-in (h/db) [:activity-center :notifications])))))
|
||||
|
||||
(testing "logs on failure"
|
||||
(test-log-on-failure
|
||||
{:notification-id notification-id
|
||||
:event [:activity-center.contact-verification/mark-as-untrustworthy notification-id]
|
||||
:action :contact-verification/mark-as-untrustworthy})))
|
||||
(is (= {:json-rpc/call
|
||||
[{:method "wakuext_verifiedUntrustworthy"
|
||||
:params [{:id notification-id}]
|
||||
:on-success [:activity-center/reconcile-notifications-from-response]
|
||||
:on-error [:activity-center/process-notification-failure notification-id
|
||||
:contact-verification/mark-as-untrustworthy]}]}
|
||||
(events/contact-verification-mark-as-untrustworthy {:db {}} notification-id))))
|
||||
|
||||
;;;; Notification reconciliation
|
||||
|
||||
(deftest notifications-reconcile-test
|
||||
(testing "All tab + All filter"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat}
|
||||
notif-2 {:id "0x2" :read false :type types/system}
|
||||
new-notif-3 {:id "0x3" :read false :type types/system}
|
||||
new-notif-4 {:id "0x4" :read true :type types/system}
|
||||
new-notif-2 (assoc notif-2 :read true)]
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/no-type :status :all}
|
||||
:notifications [notif-2 notif-1]}])
|
||||
|
||||
(rf/dispatch
|
||||
[:activity-center.notifications/reconcile
|
||||
[(assoc notif-1 :deleted true) ; will be removed
|
||||
new-notif-2
|
||||
new-notif-3
|
||||
new-notif-4]])
|
||||
|
||||
(is (= [new-notif-4 new-notif-3 new-notif-2]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
(let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat}
|
||||
notif-2 {:id "0x2" :read false :type types/system}
|
||||
new-notif-3 {:id "0x3" :read false :type types/system}
|
||||
new-notif-4 {:id "0x4" :read true :type types/system}
|
||||
new-notif-2 (assoc notif-2 :read true)
|
||||
cofx {:db {:activity-center
|
||||
{:filter {:type types/no-type :status :all}
|
||||
:notifications [notif-2 notif-1]}}}]
|
||||
(is (= {:db {:activity-center
|
||||
{:filter {:type types/no-type :status :all}
|
||||
:notifications [new-notif-4 new-notif-3 new-notif-2]}}
|
||||
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
|
||||
[:activity-center.notifications/fetch-pending-contact-requests]]}
|
||||
(events/notifications-reconcile
|
||||
cofx
|
||||
[(assoc notif-1 :deleted true) ; will be removed
|
||||
new-notif-2
|
||||
new-notif-3
|
||||
new-notif-4])))))
|
||||
|
||||
(testing "All tab + Unread filter"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :read false :type types/one-to-one-chat}
|
||||
notif-2 {:id "0x2" :read false :type types/system}
|
||||
new-notif-2 (assoc notif-2 :read true)
|
||||
new-notif-3 {:id "0x3" :read false :type types/system}
|
||||
new-notif-4 {:id "0x4" :read true :type types/system}
|
||||
notif-5 {:id "0x5" :type types/system}]
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/no-type :status :unread}
|
||||
:notifications [notif-5 notif-2 notif-1]}])
|
||||
|
||||
(rf/dispatch
|
||||
[:activity-center.notifications/reconcile
|
||||
[new-notif-2 ; will be removed because it's read
|
||||
new-notif-3 ; will be inserted
|
||||
new-notif-4 ; will be ignored because it's read
|
||||
(assoc notif-5 :deleted true) ; will be removed
|
||||
]])
|
||||
|
||||
(is (= [new-notif-3 notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
(let [notif-1 {:id "0x1" :read false :type types/one-to-one-chat}
|
||||
notif-2 {:id "0x2" :read false :type types/system}
|
||||
new-notif-2 (assoc notif-2 :read true)
|
||||
new-notif-3 {:id "0x3" :read false :type types/system}
|
||||
new-notif-4 {:id "0x4" :read true :type types/system}
|
||||
notif-5 {:id "0x5" :type types/system}
|
||||
cofx {:db {:activity-center
|
||||
{:filter {:type types/no-type :status :unread}
|
||||
:notifications [notif-5 notif-2 notif-1]}}}]
|
||||
(is (= {:db {:activity-center
|
||||
{:filter {:type types/no-type :status :unread}
|
||||
:notifications [new-notif-3 notif-1]}}
|
||||
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
|
||||
[:activity-center.notifications/fetch-pending-contact-requests]]}
|
||||
(events/notifications-reconcile
|
||||
cofx
|
||||
[new-notif-2 ; will be removed because it's read
|
||||
new-notif-3 ; will be inserted
|
||||
new-notif-4 ; will be ignored because it's read
|
||||
(assoc notif-5 :deleted true) ; will be removed
|
||||
])))))
|
||||
|
||||
(testing "Contact request tab + All filter"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :read true :type types/contact-request}
|
||||
notif-2 {:id "0x2" :read false :type types/contact-request}
|
||||
new-notif-2 (assoc notif-2 :read true)
|
||||
new-notif-3 {:id "0x3" :read false :type types/contact-request}
|
||||
new-notif-4 {:id "0x4" :read true :type types/system}
|
||||
notif-5 {:id "0x5" :read false :type types/contact-request}]
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/contact-request :status :all}
|
||||
:notifications [notif-5 notif-2 notif-1]}])
|
||||
|
||||
(rf/dispatch
|
||||
[:activity-center.notifications/reconcile
|
||||
[new-notif-2 ; will be updated
|
||||
new-notif-3 ; will be inserted
|
||||
new-notif-4 ; will be ignored because it's not a contact request
|
||||
(assoc notif-5 :deleted true) ; will be removed
|
||||
]])
|
||||
|
||||
(is (= [new-notif-3 new-notif-2 notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
(let [notif-1 {:id "0x1" :read true :type types/contact-request}
|
||||
notif-2 {:id "0x2" :read false :type types/contact-request}
|
||||
new-notif-2 (assoc notif-2 :read true)
|
||||
new-notif-3 {:id "0x3" :read false :type types/contact-request}
|
||||
new-notif-4 {:id "0x4" :read true :type types/system}
|
||||
notif-5 {:id "0x5" :read false :type types/contact-request}
|
||||
cofx {:db {:activity-center
|
||||
{:filter {:type types/contact-request :status :all}
|
||||
:notifications [notif-5 notif-2 notif-1]}}}]
|
||||
(is (= {:db {:activity-center
|
||||
{:filter {:type types/contact-request :status :all}
|
||||
:notifications [new-notif-3 new-notif-2 notif-1]}}
|
||||
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
|
||||
[:activity-center.notifications/fetch-pending-contact-requests]]}
|
||||
(events/notifications-reconcile
|
||||
cofx
|
||||
[new-notif-2 ; will be updated
|
||||
new-notif-3 ; will be inserted
|
||||
new-notif-4 ; will be ignored because it's not a contact request
|
||||
(assoc notif-5 :deleted true) ; will be removed
|
||||
])))))
|
||||
|
||||
(testing "Contact request tab + Unread filter"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :read false :type types/contact-request}
|
||||
notif-2 {:id "0x2" :read false :type types/contact-request}
|
||||
new-notif-2 (assoc notif-2 :read true)
|
||||
new-notif-3 {:id "0x3" :read false :type types/contact-request}
|
||||
new-notif-4 {:id "0x4" :read true :type types/contact-request}
|
||||
new-notif-5 {:id "0x5" :read true :type types/system}
|
||||
notif-6 {:id "0x6" :read false :type types/contact-request}]
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:type types/contact-request :status :unread}
|
||||
:notifications [notif-6 notif-2 notif-1]}])
|
||||
|
||||
(rf/dispatch
|
||||
[:activity-center.notifications/reconcile
|
||||
[new-notif-2 ; will be removed because it's read
|
||||
new-notif-3 ; will be inserted
|
||||
new-notif-4 ; will be ignored because it's read
|
||||
new-notif-5 ; will be ignored because it's not a contact request
|
||||
(assoc notif-6 :deleted true) ; will be removed
|
||||
]])
|
||||
|
||||
(is (= [new-notif-3 notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
(let [notif-1 {:id "0x1" :read false :type types/contact-request}
|
||||
notif-2 {:id "0x2" :read false :type types/contact-request}
|
||||
new-notif-2 (assoc notif-2 :read true)
|
||||
new-notif-3 {:id "0x3" :read false :type types/contact-request}
|
||||
new-notif-4 {:id "0x4" :read true :type types/contact-request}
|
||||
new-notif-5 {:id "0x5" :read true :type types/system}
|
||||
notif-6 {:id "0x6" :read false :type types/contact-request}
|
||||
cofx {:db {:activity-center
|
||||
{:filter {:type types/contact-request
|
||||
:status :unread}
|
||||
:notifications [notif-6 notif-2 notif-1]}}}]
|
||||
(is (= {:db {:activity-center
|
||||
{:filter {:type types/contact-request
|
||||
:status :unread}
|
||||
:notifications [new-notif-3 notif-1]}}
|
||||
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
|
||||
[:activity-center.notifications/fetch-pending-contact-requests]]}
|
||||
(events/notifications-reconcile
|
||||
cofx
|
||||
[new-notif-2 ; will be removed because it's read
|
||||
new-notif-3 ; will be inserted
|
||||
new-notif-4 ; will be ignored because it's read
|
||||
new-notif-5 ; will be ignored because it's not a contact request
|
||||
(assoc notif-6 :deleted true) ; will be removed
|
||||
])))))
|
||||
|
||||
;; Sorting by timestamp and ID is compatible with what the backend does when
|
||||
;; returning paginated results.
|
||||
(testing "sorts notifications by timestamp and id in descending order"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [notif-1 {:id "0x1" :timestamp 1}
|
||||
notif-2 {:id "0x2" :timestamp 1}
|
||||
notif-3 {:id "0x3" :timestamp 50}
|
||||
notif-4 {:id "0x4" :timestamp 100}
|
||||
notif-5 {:id "0x5" :timestamp 100}
|
||||
new-notif-1 (assoc notif-1 :last-message {})
|
||||
new-notif-4 (assoc notif-4 :last-message {})]
|
||||
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
|
||||
[notif-1 notif-3 notif-4 notif-2 notif-5]])
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/reconcile [new-notif-1 new-notif-4]])
|
||||
|
||||
(is (= [notif-5 new-notif-4 notif-3 notif-2 new-notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications])))))))
|
||||
(let [notif-1 {:id "0x1" :timestamp 1}
|
||||
notif-2 {:id "0x2" :timestamp 1}
|
||||
notif-3 {:id "0x3" :timestamp 50}
|
||||
notif-4 {:id "0x4" :timestamp 100}
|
||||
notif-5 {:id "0x5" :timestamp 100}
|
||||
new-notif-1 (assoc notif-1 :last-message {})
|
||||
new-notif-4 (assoc notif-4 :last-message {})
|
||||
cofx {:db {:activity-center
|
||||
{:notifications [notif-1 notif-3 notif-4 notif-2
|
||||
notif-5]}}}]
|
||||
(is (= {:db {:activity-center
|
||||
{:notifications [notif-5
|
||||
new-notif-4
|
||||
notif-3
|
||||
notif-2
|
||||
new-notif-1]}}
|
||||
:dispatch-n [[:activity-center.notifications/fetch-unread-count]
|
||||
[:activity-center.notifications/fetch-pending-contact-requests]]}
|
||||
(events/notifications-reconcile cofx [new-notif-1 new-notif-4]))))))
|
||||
|
||||
(deftest remove-pending-contact-request-test
|
||||
(testing "removes notification from all related filter types and status"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [author "0x99"
|
||||
notif-1 {:id "0x1" :read true :type types/contact-request}
|
||||
notif-2 {:id "0x2" :read false :type types/contact-request :author author}
|
||||
notif-3 {:id "0x3" :read false :type types/private-group-chat :author author}]
|
||||
(rf/dispatch [:test/assoc-in [:activity-center :notifications]
|
||||
[notif-3 ; will be ignored because it's not a contact request
|
||||
notif-2 ; will be removed
|
||||
notif-1 ; will be ignored because it's not from the same author
|
||||
]])
|
||||
|
||||
(rf/dispatch [:activity-center/remove-pending-contact-request author])
|
||||
|
||||
(is (= [notif-3 notif-1]
|
||||
(get-in (h/db) [:activity-center :notifications])))))))
|
||||
(let [author "0x99"
|
||||
notif-1 {:id "0x1" :read true :type types/contact-request}
|
||||
notif-2 {:id "0x2" :read false :type types/contact-request :author author}
|
||||
notif-3 {:id "0x3" :read false :type types/private-group-chat :author author}
|
||||
cofx {:db {:activity-center
|
||||
{:notifications
|
||||
[notif-3 ; will be ignored because it's not a contact
|
||||
; request
|
||||
notif-2 ; will be removed
|
||||
notif-1 ; will be ignored because it's not from the
|
||||
; same author
|
||||
]}}}]
|
||||
(is (= {:db {:activity-center {:notifications [notif-3 notif-1]}}}
|
||||
(events/notifications-remove-pending-contact-request cofx author))))))
|
||||
|
||||
;;;; Notifications fetching and pagination
|
||||
|
||||
(deftest notifications-fetch-test
|
||||
(deftest notifications-fetch-first-page-test
|
||||
(testing "fetches first page"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [spy-queue (atom [])]
|
||||
(h/stub-fx-with-callbacks
|
||||
:json-rpc/call
|
||||
:on-success
|
||||
(constantly {:cursor "10"
|
||||
:notifications [{:id "0x1"
|
||||
:type types/one-to-one-chat
|
||||
:read false
|
||||
:chatId "0x9"}]}))
|
||||
(h/spy-fx spy-queue :json-rpc/call)
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/fetch-first-page
|
||||
{:filter-type types/one-to-one-chat}])
|
||||
|
||||
(is (= :unread (get-in (h/db) [:activity-center :filter :status])))
|
||||
(is (= "" (get-in @spy-queue [0 :args 0 :params 0 :cursor]))
|
||||
"Should be called with empty cursor when fetching first page")
|
||||
(is (= "10" (get-in (h/db) [:activity-center :cursor])))
|
||||
(is (= [{:chat-id "0x9"
|
||||
:chat-name nil
|
||||
:chat-type types/one-to-one-chat
|
||||
:group-chat false
|
||||
:id "0x1"
|
||||
:public? false
|
||||
:last-message nil
|
||||
:message nil
|
||||
:read false
|
||||
:reply-message nil
|
||||
:type types/one-to-one-chat}]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
(let [cofx {:db {}}]
|
||||
(is (= {:db {:activity-center {:filter {:type types/one-to-one-chat
|
||||
:status :unread}
|
||||
:loading? true}}
|
||||
:json-rpc/call [{:method "wakuext_activityCenterNotifications"
|
||||
:params [{:cursor ""
|
||||
:limit (:notifications-per-page events/defaults)
|
||||
:activityTypes [types/one-to-one-chat]
|
||||
:readType events/read-type-unread}]
|
||||
:on-success [:activity-center.notifications/fetch-success true]
|
||||
:on-error [:activity-center.notifications/fetch-error
|
||||
types/one-to-one-chat :unread]}]}
|
||||
(events/notifications-fetch-first-page cofx {:filter-type types/one-to-one-chat}))))))
|
||||
|
||||
(deftest notifications-fetch-next-next-page-test
|
||||
(testing "does not fetch next page when pagination cursor reached the end"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [spy-queue (atom [])]
|
||||
(h/spy-fx spy-queue :json-rpc/call)
|
||||
(is (nil? (events/notifications-fetch-next-page
|
||||
{:db {:activity-center {:cursor events/start-or-end-cursor}}}))))
|
||||
|
||||
(rf/dispatch [:test/assoc-in [:activity-center :cursor] ""])
|
||||
(rf/dispatch [:activity-center.notifications/fetch-next-page])
|
||||
(is (= [] @spy-queue)))))
|
||||
(testing "fetches the next page"
|
||||
(let [f-args (atom [])
|
||||
cursor "abc"
|
||||
cofx {:db {:activity-center {:cursor cursor
|
||||
:filter {:type types/one-to-one-chat
|
||||
:status :unread}}}}]
|
||||
(with-redefs [events/notifications-fetch
|
||||
(fn [& args]
|
||||
(reset! f-args args)
|
||||
:result)]
|
||||
(is (= :result (events/notifications-fetch-next-page cofx)))
|
||||
(is (= [cofx
|
||||
{:cursor cursor
|
||||
:filter-type types/one-to-one-chat
|
||||
:filter-status :unread
|
||||
:reset-data? false}]
|
||||
@f-args))))))
|
||||
|
||||
(testing "fetches next page when pagination cursor is not empty"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [spy-queue (atom [])]
|
||||
(h/stub-fx-with-callbacks
|
||||
:json-rpc/call
|
||||
:on-success
|
||||
(constantly {:cursor ""
|
||||
:notifications [{:id "0x1"
|
||||
:type types/mention
|
||||
:read false
|
||||
:chatId "0x9"}]}))
|
||||
(h/spy-fx spy-queue :json-rpc/call)
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:status :unread :type types/mention}
|
||||
:cursor "10"}])
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/fetch-next-page])
|
||||
|
||||
(is (= "10" (get-in @spy-queue [0 :args 0 :params 0 :cursor]))
|
||||
"Should be called with current cursor")
|
||||
(is (= "" (get-in (h/db) [:activity-center :cursor])))
|
||||
(is (= [{:chat-id "0x9"
|
||||
:chat-name nil
|
||||
:chat-type 3
|
||||
:id "0x1"
|
||||
:last-message nil
|
||||
:message nil
|
||||
:read false
|
||||
:reply-message nil
|
||||
:type types/mention}]
|
||||
(get-in (h/db) [:activity-center :notifications]))))))
|
||||
|
||||
(testing "resets loading state after error"
|
||||
(h/run-test-sync
|
||||
(setup)
|
||||
(let [spy-queue (atom [])]
|
||||
(h/stub-fx-with-callbacks :json-rpc/call :on-error (constantly :fake-error))
|
||||
(h/spy-event-fx spy-queue :activity-center.notifications/fetch-error)
|
||||
(h/spy-fx spy-queue :json-rpc/call)
|
||||
(rf/dispatch [:test/assoc-in [:activity-center]
|
||||
{:filter {:status :unread :type types/one-to-one-chat}
|
||||
:cursor ""}])
|
||||
|
||||
(rf/dispatch [:activity-center.notifications/fetch-first-page])
|
||||
|
||||
(is (nil? (get-in (h/db) [:activity-center :loading?])))
|
||||
(is (= [:activity-center.notifications/fetch-error
|
||||
types/one-to-one-chat
|
||||
:unread
|
||||
:fake-error]
|
||||
(:args (last @spy-queue))))))))
|
||||
(deftest notifications-fetch-error-test
|
||||
(testing "resets loading state"
|
||||
(let [cofx {:db {:activity-center
|
||||
{:loading? true
|
||||
:filter {:status :unread
|
||||
:type types/one-to-one-chat}
|
||||
:cursor ""}}}]
|
||||
(is (= {:db {:activity-center {:filter {:status :unread
|
||||
:type types/one-to-one-chat}
|
||||
:cursor ""}}}
|
||||
(events/notifications-fetch-error cofx :dummy-error))))))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
(ns status-im2.contexts.activity-center.notification.admin.view
|
||||
(:require [quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.contexts.activity-center.notification.common.style :as common-style]
|
||||
[status-im2.contexts.activity-center.notification.common.view :as common]
|
||||
@@ -69,12 +68,8 @@
|
||||
:context [[common/user-avatar-tag author]
|
||||
(i18n/label :t/wants-to-join)
|
||||
[quo/context-tag
|
||||
{:size :small
|
||||
:override-theme :dark
|
||||
:color colors/primary-50
|
||||
:style common-style/user-avatar-tag
|
||||
:text-style common-style/user-avatar-tag-text}
|
||||
{:uri community-image} community-name]]
|
||||
common/tag-params
|
||||
community-image community-name]]
|
||||
:items (case membership-status
|
||||
constants/activity-center-membership-status-accepted
|
||||
[{:type :status
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(def tag-params
|
||||
{:size :small
|
||||
:override-theme :dark
|
||||
:color colors/primary-50
|
||||
:style style/user-avatar-tag
|
||||
:text-style style/user-avatar-tag-text})
|
||||
{:size :small
|
||||
:override-theme :dark
|
||||
:color colors/primary-50
|
||||
:style style/user-avatar-tag
|
||||
:text-style style/user-avatar-tag-text
|
||||
:no-avatar-placeholder? true})
|
||||
|
||||
(defn user-avatar-tag
|
||||
[user-id]
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
:unread? (not read)
|
||||
:context [[quo/text {:style common-style/user-avatar-tag-text}
|
||||
(i18n/label :t/community-kicked-body)]
|
||||
[quo/context-tag common/tag-params {:uri community-image}
|
||||
[quo/context-tag common/tag-params community-image
|
||||
community-name]]}]]))
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
[community membership-status]
|
||||
(let [community-name (:name community)
|
||||
community-image (get-in community [:images :thumbnail :uri])
|
||||
community-context-tag [quo/context-tag common/tag-params {:uri community-image}
|
||||
community-context-tag [quo/context-tag common/tag-params community-image
|
||||
community-name]]
|
||||
(cond
|
||||
(= membership-status constants/activity-center-membership-status-idle)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
(ns status-im2.contexts.activity-center.notification.mentions.view
|
||||
(:require [clojure.string :as string]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.gesture :as gesture]
|
||||
[status-im2.contexts.activity-center.notification.common.view :as common]
|
||||
[status-im2.contexts.activity-center.notification.mentions.style :as style]
|
||||
@@ -9,13 +8,6 @@
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(def ^:private tag-params
|
||||
{:size :small
|
||||
:override-theme :dark
|
||||
:color colors/primary-50
|
||||
:style style/tag
|
||||
:text-style style/tag-text})
|
||||
|
||||
(defn- message-body
|
||||
[message]
|
||||
(let [parsed-text (get-in message [:content :parsed-text])
|
||||
@@ -68,6 +60,6 @@
|
||||
:context [[common/user-avatar-tag author]
|
||||
[quo/text {:style style/tag-text} (string/lower-case (i18n/label :t/on))]
|
||||
(if community-chat?
|
||||
[quo/context-tag tag-params {:uri community-image} community-name chat-name]
|
||||
[quo/group-avatar-tag chat-name tag-params])]
|
||||
[quo/context-tag common/tag-params community-image community-name chat-name]
|
||||
[quo/group-avatar-tag chat-name common/tag-params])]
|
||||
:message {:body (message-body message)}}]]]))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
(ns status-im2.contexts.activity-center.notification.reply.view
|
||||
(:require [clojure.string :as string]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.gesture :as gesture]
|
||||
[status-im.ui2.screens.chat.messages.message :as old-message]
|
||||
[status-im2.common.not-implemented :as not-implemented]
|
||||
@@ -12,13 +11,6 @@
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(def ^:private tag-params
|
||||
{:size :small
|
||||
:override-theme :dark
|
||||
:color colors/primary-50
|
||||
:style style/tag
|
||||
:text-style style/tag-text})
|
||||
|
||||
;; NOTE: Replies support text, image and stickers only.
|
||||
(defn- get-message-content
|
||||
[{:keys [content-type] :as message}]
|
||||
@@ -76,7 +68,7 @@
|
||||
:context [[common/user-avatar-tag author]
|
||||
[quo/text {:style style/lowercase-text} (i18n/label :t/on)]
|
||||
(if community-chat?
|
||||
[quo/context-tag tag-params {:uri community-image} community-name chat-name]
|
||||
[quo/group-avatar-tag chat-name tag-params])]
|
||||
[quo/context-tag common/tag-params community-image community-name chat-name]
|
||||
[quo/group-avatar-tag chat-name common/tag-params])]
|
||||
:message {:body-number-of-lines 1
|
||||
:body (get-message-content message)}}]]]))
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
[:f> f-send-button state animations window-height images? btn-opacity z-index]))
|
||||
|
||||
(defn audio-button
|
||||
[{:keys [record-reset-fn]}
|
||||
[{:keys [record-reset-fn input-ref]}
|
||||
{:keys [record-permission? recording? gesture-enabled? focused?]}
|
||||
{:keys [container-opacity]}]
|
||||
(let [audio (rf/sub [:chats/sending-audio])]
|
||||
@@ -86,17 +86,22 @@
|
||||
(reset! recording? false)
|
||||
(reset! gesture-enabled? true)
|
||||
(rf/dispatch [:chat/send-audio file-path duration])
|
||||
(when-not @focused?
|
||||
(if-not @focused?
|
||||
(reanimated/animate container-opacity
|
||||
constants/empty-opacity))
|
||||
constants/empty-opacity)
|
||||
(js/setTimeout #(when @input-ref (.focus ^js @input-ref))
|
||||
300))
|
||||
(rf/dispatch [:chat.ui/set-input-audio nil]))
|
||||
:on-cancel (fn []
|
||||
(when @recording?
|
||||
(reset! recording? false)
|
||||
(reset! gesture-enabled? true)
|
||||
(when-not @focused?
|
||||
(if-not @focused?
|
||||
(reanimated/animate container-opacity
|
||||
constants/empty-opacity))
|
||||
constants/empty-opacity)
|
||||
(js/setTimeout #(when @input-ref
|
||||
(.focus ^js @input-ref))
|
||||
300))
|
||||
(rf/dispatch [:chat.ui/set-input-audio nil])))
|
||||
:on-check-audio-permissions (fn []
|
||||
(permissions/permission-granted?
|
||||
|
||||
@@ -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}
|
||||
@@ -36,29 +36,30 @@
|
||||
|
||||
(defn blur
|
||||
[{:keys [text-value focused? lock-selection? cursor-position saved-cursor-position gradient-z-index
|
||||
maximized?]}
|
||||
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]}]
|
||||
(let [lines (utils/calc-lines (- @content-height constants/extra-content-offset))
|
||||
min-height (utils/get-min-height lines)
|
||||
reopen-height (utils/calc-reopen-height text-value min-height content-height saved-height)]
|
||||
(reset! focused? false)
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(reanimated/set-shared-value last-height reopen-height)
|
||||
(reanimated/animate height min-height)
|
||||
(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)
|
||||
(reanimated/animate container-opacity constants/empty-opacity))
|
||||
(reanimated/animate gradient-opacity 0)
|
||||
(reset! lock-selection? true)
|
||||
(reset! saved-cursor-position @cursor-position)
|
||||
(reset! gradient-z-index (if (= (reanimated/get-shared-value gradient-opacity) 1) -1 0))
|
||||
(when (not= reopen-height max-height)
|
||||
(reset! maximized? false)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false]))))
|
||||
{: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)
|
||||
reopen-height (utils/calc-reopen-height text-value min-height content-height saved-height)]
|
||||
(reset! focused? false)
|
||||
(rf/dispatch [:chat.ui/set-input-focused false])
|
||||
(reanimated/set-shared-value last-height reopen-height)
|
||||
(reanimated/animate height min-height)
|
||||
(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 link-previews? reply nil)
|
||||
(reanimated/animate container-opacity constants/empty-opacity))
|
||||
(reanimated/animate gradient-opacity 0)
|
||||
(reset! lock-selection? true)
|
||||
(reset! saved-cursor-position @cursor-position)
|
||||
(reset! gradient-z-index (if (= (reanimated/get-shared-value gradient-opacity) 1) -1 0))
|
||||
(when (not= reopen-height max-height)
|
||||
(reset! maximized? false)
|
||||
(rf/dispatch [:chat.ui/set-input-maximized false])))))
|
||||
|
||||
(defn content-size-change
|
||||
[event
|
||||
@@ -110,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))))
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
[:<>
|
||||
[rn/view {:style {:margin-bottom 10}} [text/text-content first-image context]]
|
||||
[rn/view
|
||||
{:style (style/album-container portrait?)}
|
||||
{:style (style/album-container portrait?)
|
||||
:accessible true
|
||||
:accessibility-label :image-container}
|
||||
(map-indexed
|
||||
(fn [index item]
|
||||
(let [images-size-key (if (< images-count constants/max-album-photos)
|
||||
|
||||
@@ -18,18 +18,19 @@
|
||||
(let [insets (safe-area/get-insets)
|
||||
dimensions (calculate-dimensions (or image-width 1000) (or image-height 1000))
|
||||
shared-element-id (rf/sub [:shared-element-id])]
|
||||
[rn/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:style {:margin-top (when (pos? index) 10)}
|
||||
:on-long-press on-long-press
|
||||
:on-press #(rf/dispatch [:chat.ui/navigate-to-lightbox
|
||||
message-id
|
||||
{:messages [message]
|
||||
:index 0
|
||||
:insets insets}])}
|
||||
[:<>
|
||||
(when (= index 0)
|
||||
[rn/view {:style {:margin-bottom 10}} [text/text-content message context]])
|
||||
[fast-image/fast-image
|
||||
{:source {:uri (:image content)}
|
||||
:style (merge dimensions {:border-radius 12})
|
||||
:native-ID (when (= shared-element-id message-id) :shared-element)}]]))
|
||||
[rn/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:style {:margin-top (when (pos? index) 10)}
|
||||
:on-long-press on-long-press
|
||||
:on-press #(rf/dispatch [:chat.ui/navigate-to-lightbox
|
||||
message-id
|
||||
{:messages [message]
|
||||
:index 0
|
||||
:insets insets}])}
|
||||
[fast-image/fast-image
|
||||
{:source {:uri (:image content)}
|
||||
:style (merge dimensions {:border-radius 12})
|
||||
:native-ID (when (= shared-element-id message-id) :shared-element)}]]]))
|
||||
|
||||
@@ -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]])
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
(ns status-im2.contexts.onboarding.create-password.style
|
||||
(:require [quo2.foundations.colors :as colors]))
|
||||
|
||||
(def image-background
|
||||
{:height "100%"
|
||||
:width "100%"})
|
||||
|
||||
(def overlay
|
||||
{:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0
|
||||
:background-color colors/neutral-80-opa-80-blur})
|
||||
|
||||
(def content-style {:flex-grow 1})
|
||||
(def flex-fill {:flex 1})
|
||||
|
||||
(def heading {:margin-bottom 20})
|
||||
(def heading-subtitle {:color colors/white})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns status-im2.contexts.onboarding.create-password.view
|
||||
(:require
|
||||
[oops.core :refer [ocall]]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
@@ -66,7 +67,8 @@
|
||||
:shown true}
|
||||
:placeholder (i18n/label :t/password-creation-placeholder-1)
|
||||
:on-change-text on-change-password
|
||||
:on-focus on-input-focus}]
|
||||
:on-focus on-input-focus
|
||||
:auto-focus true}]
|
||||
[rn/view {:style style/space-between-inputs}]
|
||||
[password-with-hint
|
||||
{:hint {:text hint-2-text
|
||||
@@ -120,7 +122,7 @@
|
||||
(count)))
|
||||
|
||||
(defn password-form
|
||||
[{:keys [scroll-to-end-fn]}]
|
||||
[]
|
||||
(let [password (reagent/atom "")
|
||||
repeat-password (reagent/atom "")
|
||||
accepts-disclaimer? (reagent/atom false)
|
||||
@@ -134,7 +136,7 @@
|
||||
:as validations} (password-validations @password)
|
||||
password-strength (calc-password-strength validations)
|
||||
empty-password? (empty? @password)
|
||||
same-passwords? (= @password @repeat-password)
|
||||
same-passwords? (and (not empty-password?) (= @password @repeat-password))
|
||||
meet-requirements? (and (not empty-password?)
|
||||
(utils.string/at-least-n-chars? @password 10)
|
||||
same-passwords?
|
||||
@@ -147,9 +149,7 @@
|
||||
:passwords-match? same-passwords?
|
||||
:empty-password? empty-password?
|
||||
:show-password-validation? @show-password-validation?
|
||||
:on-input-focus (fn []
|
||||
(scroll-to-end-fn)
|
||||
(reset! focused-input :password))
|
||||
:on-input-focus #(reset! focused-input :password)
|
||||
:on-change-password (fn [new-value]
|
||||
(reset! password new-value)
|
||||
(when (same-password-length?)
|
||||
@@ -163,14 +163,14 @@
|
||||
(reset! show-password-validation? true))}]]
|
||||
|
||||
[rn/view {:style style/bottom-part}
|
||||
[rn/view {:style style/disclaimer-container}
|
||||
[quo/disclaimer
|
||||
{:blur? true
|
||||
:on-change #(swap! accepts-disclaimer? not)
|
||||
:checked? @accepts-disclaimer?}
|
||||
(i18n/label :t/password-creation-disclaimer)]]
|
||||
|
||||
(when (= @focused-input :password)
|
||||
(when same-passwords?
|
||||
[rn/view {:style style/disclaimer-container}
|
||||
[quo/disclaimer
|
||||
{:blur? true
|
||||
:on-change #(swap! accepts-disclaimer? not)
|
||||
:checked? @accepts-disclaimer?}
|
||||
(i18n/label :t/password-creation-disclaimer)]])
|
||||
(when (and (= @focused-input :password) (not same-passwords?))
|
||||
[help
|
||||
{:validations validations
|
||||
:password-strength password-strength}])
|
||||
@@ -196,25 +196,45 @@
|
||||
(i18n/label
|
||||
:t/create-profile-password-info-box-description)]]])
|
||||
|
||||
(defn- f-create-password
|
||||
[]
|
||||
(let [keyboard-shown? (reagent/atom false)
|
||||
{:keys [top bottom]} (safe-area/get-insets)]
|
||||
(fn []
|
||||
(rn/use-effect
|
||||
(let [will-show-listener (ocall rn/keyboard
|
||||
"addListener"
|
||||
"keyboardWillShow"
|
||||
#(reset! keyboard-shown? true))
|
||||
will-hide-listener (ocall rn/keyboard
|
||||
"addListener"
|
||||
"keyboardWillHide"
|
||||
#(reset! keyboard-shown? false))]
|
||||
(fn []
|
||||
(fn []
|
||||
(ocall will-show-listener "remove")
|
||||
(ocall will-hide-listener "remove"))))
|
||||
[])
|
||||
[rn/touchable-without-feedback
|
||||
{:on-press rn/dismiss-keyboard!
|
||||
:accessible false}
|
||||
[rn/view {:style style/flex-fill}
|
||||
[rn/keyboard-avoiding-view {:style style/flex-fill}
|
||||
[navigation-bar/navigation-bar
|
||||
{:top top
|
||||
:right-section-buttons [{:type :blur-bg
|
||||
:icon :i/info
|
||||
:icon-override-theme :dark
|
||||
:on-press (fn []
|
||||
(rn/dismiss-keyboard!)
|
||||
(rf/dispatch [:show-bottom-sheet
|
||||
{:content create-password-doc
|
||||
:shell? true}]))}]}]
|
||||
[password-form]
|
||||
[rn/view {:style {:height (if-not @keyboard-shown? bottom 0)}}]]]])))
|
||||
|
||||
(defn create-password
|
||||
[]
|
||||
(let [scroll-view-ref (atom nil)
|
||||
scroll-to-end-fn #(js/setTimeout ^js/Function (.-scrollToEnd @scroll-view-ref) 250)]
|
||||
(fn []
|
||||
(let [{:keys [top]} (safe-area/get-insets)]
|
||||
[:<>
|
||||
[background/view true]
|
||||
[rn/scroll-view
|
||||
{:ref #(reset! scroll-view-ref %)
|
||||
:style style/overlay
|
||||
:content-container-style style/content-style}
|
||||
[navigation-bar/navigation-bar
|
||||
{:top top
|
||||
:right-section-buttons [{:type :blur-bg
|
||||
:icon :i/info
|
||||
:icon-override-theme :dark
|
||||
:on-press #(rf/dispatch
|
||||
[:show-bottom-sheet
|
||||
{:content create-password-doc
|
||||
:shell? true}])}]}]
|
||||
[password-form {:scroll-to-end-fn scroll-to-end-fn}]]]))))
|
||||
[:<>
|
||||
[background/view true]
|
||||
[:f> f-create-password]])
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
(ns status-im2.contexts.onboarding.events
|
||||
(:require
|
||||
[utils.re-frame :as rf]
|
||||
[taoensso.timbre :as log]
|
||||
[clojure.string :as string]
|
||||
[native-module.core :as native-module]
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.ethereum.core :as ethereum]
|
||||
[status-im.utils.types :as types]
|
||||
[status-im2.config :as config]
|
||||
[clojure.string :as string]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.security.core :as security]
|
||||
[native-module.core :as native-module]
|
||||
[status-im.ethereum.core :as ethereum]
|
||||
[status-im2.constants :as constants]
|
||||
[status-im2.contexts.onboarding.profiles.view :as profiles.view]))
|
||||
[taoensso.timbre :as log]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:multiaccount/create-account-and-login
|
||||
@@ -140,12 +139,10 @@
|
||||
{:title (i18n/label :t/multiaccount-exists-title)
|
||||
:content (i18n/label :t/multiaccount-exists-content)
|
||||
:confirm-button-text (i18n/label :t/unlock)
|
||||
:on-accept #(do
|
||||
(re-frame/dispatch [:pop-to-root :profiles])
|
||||
;; FIXME(rasom): obviously not cool
|
||||
(reset! profiles.view/show-profiles? false)
|
||||
(re-frame/dispatch
|
||||
[:multiaccounts.login.ui/multiaccount-selected key-uid]))
|
||||
:on-accept (fn []
|
||||
(re-frame/dispatch [:pop-to-root :profiles])
|
||||
(re-frame/dispatch
|
||||
[:multiaccounts.login.ui/multiaccount-selected key-uid]))
|
||||
:on-cancel #(re-frame/dispatch [:pop-to-root :multiaccounts])}}
|
||||
{:db (assoc-in db [:onboarding-2/profile :seed-phrase] seed-phrase)
|
||||
:dispatch [:navigate-to :create-profile]}))
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
[utils.security.core :as security]
|
||||
[utils.transforms :as types]))
|
||||
|
||||
(def show-profiles? (reagent/atom false))
|
||||
|
||||
(defn login-multiaccount
|
||||
[]
|
||||
(rf/dispatch [:multiaccounts.login.ui/password-input-submitted]))
|
||||
@@ -43,14 +41,14 @@
|
||||
:context context
|
||||
:button-text (i18n/label :t/remove)
|
||||
:close-button-text (i18n/label :t/cancel)
|
||||
:on-press #(do
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(native-module/delete-multiaccount
|
||||
key-uid
|
||||
(fn [result]
|
||||
(let [{:keys [error]} (types/json->clj result)]
|
||||
(rf/dispatch [:onboarding-2/on-delete-profile-success key-uid])
|
||||
(log/info "profile deleted: error" error)))))}])
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(native-module/delete-multiaccount
|
||||
key-uid
|
||||
(fn [result]
|
||||
(let [{:keys [error]} (types/json->clj result)]
|
||||
(rf/dispatch [:onboarding-2/on-delete-profile-success key-uid])
|
||||
(log/info "profile deleted: error" error)))))}])
|
||||
|
||||
(defn show-confirmation
|
||||
[key-uid context]
|
||||
@@ -72,7 +70,9 @@
|
||||
{:content (fn [] [profile-options key-uid context])}]))
|
||||
|
||||
(defn profile-card
|
||||
[{:keys [name key-uid customization-color keycard-pairing last-index] :as multiaccount} index]
|
||||
[{:keys [name key-uid customization-color keycard-pairing last-index set-hide-profiles]
|
||||
:as multiaccount}
|
||||
index]
|
||||
(let [last-item? (= last-index index)
|
||||
profile-picture (:uri (first (:images multiaccount)))]
|
||||
[quo/profile-card
|
||||
@@ -89,15 +89,18 @@
|
||||
{:name name
|
||||
:color customization-color
|
||||
:profile-picture profile-picture})
|
||||
:on-card-press #(do
|
||||
(rf/dispatch
|
||||
[:multiaccounts.login.ui/multiaccount-selected key-uid])
|
||||
(when-not keycard-pairing (reset! show-profiles? false)))}]))
|
||||
:on-card-press (fn []
|
||||
(rf/dispatch
|
||||
[:multiaccounts.login.ui/multiaccount-selected key-uid])
|
||||
(when-not keycard-pairing (set-hide-profiles)))}]))
|
||||
|
||||
(defn profiles-section
|
||||
[]
|
||||
[{:keys [set-hide-profiles]}]
|
||||
(let [multiaccounts (vals (rf/sub [:multiaccounts/multiaccounts]))
|
||||
multiaccounts (map #(assoc % :last-index (- (count multiaccounts) 1)) multiaccounts)]
|
||||
profiles-data (map #(assoc %
|
||||
:last-index (- (count multiaccounts) 1)
|
||||
:set-hide-profiles set-hide-profiles)
|
||||
multiaccounts)]
|
||||
[rn/view
|
||||
{:style style/profiles-container}
|
||||
[rn/view
|
||||
@@ -116,7 +119,7 @@
|
||||
:override-theme :dark}
|
||||
:main-icons/add]]
|
||||
[rn/flat-list
|
||||
{:data (sort-by :timestamp > multiaccounts)
|
||||
{:data (sort-by :timestamp > profiles-data)
|
||||
:key-fn :key-uid
|
||||
:content-container-style {:padding-bottom 20}
|
||||
:render-fn profile-card}]]))
|
||||
@@ -169,8 +172,8 @@
|
||||
(i18n/label :t/forgot-your-password-info-create-new-password-description)]]]]])
|
||||
|
||||
(defn login-section
|
||||
[]
|
||||
(let [{:keys [name customization-color error processing]
|
||||
[{:keys [set-show-profiles]}]
|
||||
(let [{:keys [name customization-color error processing password]
|
||||
:as multiaccount} (rf/sub [:multiaccounts/login])
|
||||
sign-in-enabled? (rf/sub [:sign-in-enabled?])
|
||||
profile-picture (:uri (first (:images multiaccount)))]
|
||||
@@ -180,7 +183,8 @@
|
||||
{:size 32
|
||||
:type :blur-bg
|
||||
:icon true
|
||||
:on-press #(reset! show-profiles? true)
|
||||
:on-press set-show-profiles
|
||||
:disabled processing
|
||||
:override-theme :dark
|
||||
:width 32
|
||||
:accessibility-label :show-profiles
|
||||
@@ -197,18 +201,18 @@
|
||||
[quo/input
|
||||
{:type :password
|
||||
:blur? true
|
||||
:override-theme :dark
|
||||
:disabled? processing
|
||||
:placeholder (i18n/label :t/type-your-password)
|
||||
:auto-focus true
|
||||
:error? (when (not-empty error) error)
|
||||
:error? (seq error)
|
||||
:label (i18n/label :t/profile-password)
|
||||
:secure-text-entry true
|
||||
:on-change-text (fn [password]
|
||||
(rf/dispatch [:set-in [:multiaccounts/login :password]
|
||||
(security/mask-data password)])
|
||||
(rf/dispatch [:set-in [:multiaccounts/login :error] ""]))
|
||||
:default-value (security/safe-unmask-data password)
|
||||
:on-submit-editing (when sign-in-enabled? login-multiaccount)}]
|
||||
(when (not-empty error)
|
||||
(when (seq error)
|
||||
[quo/info-message
|
||||
{:type :error
|
||||
:size :default
|
||||
@@ -219,6 +223,7 @@
|
||||
{:size 40
|
||||
:type :ghost
|
||||
:before :i/info
|
||||
:disabled processing
|
||||
:accessibility-label :forget-password-button
|
||||
:override-theme :dark
|
||||
:style style/forget-password-button
|
||||
@@ -239,10 +244,12 @@
|
||||
|
||||
(defn views
|
||||
[]
|
||||
(reset! show-profiles? false)
|
||||
(fn []
|
||||
[:<>
|
||||
[background/view true]
|
||||
(if @show-profiles?
|
||||
[profiles-section]
|
||||
[login-section])]))
|
||||
(let [show-profiles? (reagent/atom false)
|
||||
set-show-profiles #(reset! show-profiles? true)
|
||||
set-hide-profiles #(reset! show-profiles? false)]
|
||||
(fn []
|
||||
[:<>
|
||||
[background/view true]
|
||||
(if @show-profiles?
|
||||
[profiles-section {:set-hide-profiles set-hide-profiles}]
|
||||
[login-section {:set-show-profiles set-show-profiles}])])))
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
(ns status-im2.contexts.quo-preview.keycard.keycard
|
||||
(:require [status-im2.contexts.quo-preview.preview :as preview]
|
||||
[react-native.core :as rn]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[reagent.core :as reagent]
|
||||
[quo2.core :as quo]))
|
||||
|
||||
(def descriptor
|
||||
[{:label "Holder"
|
||||
:key :holder-name
|
||||
:type :text}
|
||||
{:label "Locked?"
|
||||
:key :locked?
|
||||
:type :boolean}])
|
||||
|
||||
(defn cool-preview
|
||||
[]
|
||||
(let [state (reagent/atom {:holder-name nil
|
||||
:locked? true})]
|
||||
(fn
|
||||
[]
|
||||
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
|
||||
[rn/view [preview/customizer state descriptor]
|
||||
[quo/keycard
|
||||
{:holder-name? (:holder-name @state)
|
||||
:locked? (:locked? @state)}]]])))
|
||||
|
||||
(defn preview-keycard
|
||||
[]
|
||||
[rn/view
|
||||
{:style {:background-color (colors/theme-colors colors/white colors/neutral-95)
|
||||
:flex 1}}
|
||||
[rn/flat-list
|
||||
{:style {:flex 1}
|
||||
:keyboard-should-persist-taps :always
|
||||
:header [cool-preview]
|
||||
:key-fn str}]])
|
||||
@@ -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)
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
[status-im2.contexts.quo-preview.wallet.lowest-price :as lowest-price]
|
||||
[status-im2.contexts.quo-preview.wallet.network-amount :as network-amount]
|
||||
[status-im2.contexts.quo-preview.wallet.network-breakdown :as network-breakdown]
|
||||
[status-im2.contexts.quo-preview.wallet.token-overview :as token-overview]))
|
||||
[status-im2.contexts.quo-preview.wallet.token-overview :as token-overview]
|
||||
[status-im2.contexts.quo-preview.keycard.keycard :as keycard]))
|
||||
|
||||
(def screens-categories
|
||||
{:foundations [{:name :shadows
|
||||
@@ -344,7 +345,10 @@
|
||||
:component network-breakdown/preview-network-breakdown}
|
||||
{:name :network-amount
|
||||
:options {:topBar {:visible true}}
|
||||
:component network-amount/preview}]})
|
||||
:component network-amount/preview}]
|
||||
:keycard [{:name :keycard-component
|
||||
:options {:topBar {:visible true}}
|
||||
:component keycard/preview-keycard}]})
|
||||
|
||||
(def screens (flatten (map val screens-categories)))
|
||||
|
||||
|
||||
@@ -43,19 +43,27 @@
|
||||
:type :boolean}])
|
||||
|
||||
(def context-tag-descriptor
|
||||
[{:label "Label"
|
||||
[{:label "Show avatar"
|
||||
:key :show-avatar?
|
||||
:type :boolean}
|
||||
{:label "Label"
|
||||
:key :label
|
||||
:type :text}
|
||||
{:label "Channel Name"
|
||||
{:label "Channel name"
|
||||
:key :channel-name
|
||||
:type :text}])
|
||||
:type :text}
|
||||
{:label "Avatar placeholder"
|
||||
:key :no-avatar-placeholder?
|
||||
:type :boolean}])
|
||||
|
||||
(defn cool-preview
|
||||
[]
|
||||
(let [state (reagent/atom {:label "Name"
|
||||
:channel-name "Channel"
|
||||
:type :group-avatar
|
||||
:duration "00:32"})]
|
||||
(let [state (reagent/atom {:label "Name"
|
||||
:channel-name "Channel"
|
||||
:type :group-avatar
|
||||
:duration "00:32"
|
||||
:show-avatar? true
|
||||
:no-avatar-placeholder? false})]
|
||||
(fn []
|
||||
(let [contacts {example-pk {:public-key example-pk
|
||||
:primary-name "Automatic incompatible Coati"
|
||||
@@ -122,10 +130,12 @@
|
||||
:show-blur-background? (:blur? @state)}
|
||||
(case (:type @state)
|
||||
:context-tag [quo2/context-tag
|
||||
{:blur? (:blur? @state)
|
||||
:size :small
|
||||
:color :purple}
|
||||
{:uri example-photo2}
|
||||
{:blur? (:blur? @state)
|
||||
:size :small
|
||||
:color :purple
|
||||
:no-avatar-placeholder? (:no-avatar-placeholder? @state)}
|
||||
(when (:show-avatar? @state)
|
||||
example-photo2)
|
||||
(:label @state)
|
||||
(:channel-name @state)]
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
(ns status-im2.contexts.share.events
|
||||
(:require [utils.re-frame :as rf]
|
||||
[status-im2.common.toasts.events :as toasts]
|
||||
[quo2.foundations.colors :as colors]))
|
||||
|
||||
(rf/defn copy-text-and-show-toast
|
||||
{:events [:share/copy-text-and-show-toast]}
|
||||
[cofx text-to-copy post-copy-message]
|
||||
(rf/merge cofx
|
||||
{:copy-to-clipboard text-to-copy}
|
||||
(toasts/upsert
|
||||
{:icon :correct
|
||||
:icon-color colors/success-50
|
||||
:override-theme :dark
|
||||
:text post-copy-message})))
|
||||
@@ -0,0 +1,102 @@
|
||||
(ns status-im2.contexts.share.style
|
||||
(:require [quo2.foundations.colors :as colors]
|
||||
[react-native.platform :as platform]))
|
||||
|
||||
(def screen-padding 20)
|
||||
|
||||
(def blur
|
||||
{:style {:position :absolute
|
||||
:top 0
|
||||
:left 0
|
||||
:right 0
|
||||
:bottom 0}
|
||||
:overlay-color colors/neutral-80-opa-80
|
||||
:blur-amount 20})
|
||||
|
||||
(def header-button
|
||||
{:margin-bottom 12
|
||||
:margin-left screen-padding})
|
||||
|
||||
(def header-heading
|
||||
{:padding-horizontal screen-padding
|
||||
:padding-vertical 12
|
||||
:color colors/white})
|
||||
|
||||
(def qr-code-container
|
||||
{:padding 12
|
||||
:border-radius 16
|
||||
:margin-top 12
|
||||
:margin-bottom 4
|
||||
:margin-horizontal screen-padding
|
||||
:background-color colors/white-opa-5
|
||||
:flex-direction :column
|
||||
:justify-content :center})
|
||||
|
||||
(def emoji-hash-container
|
||||
{:border-radius 16
|
||||
:margin-top 12
|
||||
:margin-horizontal screen-padding
|
||||
:background-color colors/white-opa-5
|
||||
:flex-direction :row
|
||||
:justify-content :flex-start
|
||||
:align-items :flex-start})
|
||||
|
||||
(def profile-address-column
|
||||
{:flex-direction :column})
|
||||
|
||||
(def emoji-address-column
|
||||
{:flex-direction :column})
|
||||
|
||||
(def profile-address-label
|
||||
{:color colors/white-opa-40
|
||||
:padding-top 10})
|
||||
|
||||
(def profile-address-content
|
||||
{:color colors/white
|
||||
:align-self :flex-start
|
||||
:padding-top 2})
|
||||
|
||||
(def profile-address-container
|
||||
{:flex-direction :row
|
||||
:justify-content :flex-start
|
||||
:margin-top 6
|
||||
:margin-horizontal 4})
|
||||
|
||||
(def emoji-address-container
|
||||
{:flex-direction :row
|
||||
:justify-content :flex-start
|
||||
:margin-top 6
|
||||
:margin-horizontal 4})
|
||||
|
||||
(def emoji-hash-label
|
||||
{:color colors/white-opa-40
|
||||
:margin-top 8
|
||||
:padding-bottom (if platform/ios? 2 0)
|
||||
:padding-left 12})
|
||||
|
||||
|
||||
(def share-button-container
|
||||
{:position :absolute
|
||||
:right 0
|
||||
:top 16})
|
||||
|
||||
(def emoji-share-button-container
|
||||
{:position :absolute
|
||||
:right 4
|
||||
:top 16})
|
||||
|
||||
(def emoji-hash-content
|
||||
{:color colors/white
|
||||
:align-self :flex-start
|
||||
:padding-top 4
|
||||
:padding-bottom 12
|
||||
:padding-left 12
|
||||
:font-size 14})
|
||||
|
||||
(def tabs-container
|
||||
{:padding-horizontal screen-padding
|
||||
:margin-vertical 8})
|
||||
|
||||
(def wip-style
|
||||
{:color colors/white
|
||||
:text-align :center})
|
||||
@@ -0,0 +1,162 @@
|
||||
(ns status-im2.contexts.share.view
|
||||
(:require [utils.i18n :as i18n]
|
||||
[quo2.core :as quo]
|
||||
[react-native.core :as rn]
|
||||
[status-im2.contexts.share.style :as style]
|
||||
[utils.re-frame :as rf]
|
||||
[reagent.core :as reagent]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[clojure.string :as string]
|
||||
[react-native.blur :as blur]
|
||||
[status-im.ui.components.list-selection :as list-selection]
|
||||
[utils.image-server :as image-server]
|
||||
[react-native.navigation :as navigation]))
|
||||
|
||||
(defn header
|
||||
[]
|
||||
[:<>
|
||||
[quo/button
|
||||
{:icon true
|
||||
:type :blur-bg
|
||||
:size 32
|
||||
:accessibility-label :close-shell-share-tab
|
||||
:override-theme :dark
|
||||
:style style/header-button
|
||||
:on-press #(rf/dispatch [:navigate-back])}
|
||||
:i/close]
|
||||
[quo/text
|
||||
{:size :heading-1
|
||||
:weight :semi-bold
|
||||
:style style/header-heading}
|
||||
(i18n/label :t/share)]])
|
||||
|
||||
(defn abbreviated-url
|
||||
"The goal here is to generate a string that begins with
|
||||
join.status.im/u/ joined with the 1st 5 characters
|
||||
of the compressed public key followed by an ellipsis followed by
|
||||
the last 12 characters of the compressed public key"
|
||||
[base-url public-pk]
|
||||
(let [first-part-of-public-pk (subs public-pk 0 5)
|
||||
ellipsis "..."
|
||||
public-pk-size (count public-pk)
|
||||
last-part-of-public-pk (subs public-pk (- public-pk-size 12) (- public-pk-size 1))
|
||||
abbreviated-url (str base-url first-part-of-public-pk ellipsis last-part-of-public-pk)]
|
||||
abbreviated-url))
|
||||
|
||||
(defn profile-tab
|
||||
[window-width]
|
||||
(let [multiaccount (rf/sub [:multiaccount])
|
||||
emoji-hash (string/join (get multiaccount :emoji-hash))
|
||||
qr-size (int (- window-width 64))
|
||||
public-pk (get multiaccount :compressed-key)
|
||||
abbreviated-url (abbreviated-url image-server/status-profile-base-url-without-https public-pk)
|
||||
profile-url (str image-server/status-profile-base-url public-pk)
|
||||
port (rf/sub [:mediaserver/port])
|
||||
key-uid (get multiaccount :key-uid)
|
||||
source-uri (image-server/get-account-qr-image-uri {:key-uid key-uid
|
||||
:public-key public-pk
|
||||
:port port
|
||||
:qr-size qr-size})]
|
||||
[:<>
|
||||
[rn/view {:style style/qr-code-container}
|
||||
[quo/qr-code
|
||||
{:source {:uri source-uri}
|
||||
:width qr-size
|
||||
:height qr-size}]
|
||||
[rn/view {:style style/profile-address-container}
|
||||
[rn/view {:style style/profile-address-column}
|
||||
[quo/text
|
||||
{:size :paragraph-2
|
||||
:weight :medium
|
||||
:style style/profile-address-label}
|
||||
(i18n/label :t/link-to-profile)]
|
||||
[rn/touchable-highlight
|
||||
{:active-opacity 1
|
||||
:underlay-color colors/neutral-80-opa-1-blur
|
||||
:background-color :transparent
|
||||
:on-press #(rf/dispatch [:share/copy-text-and-show-toast profile-url
|
||||
(i18n/label :t/link-to-profile-copied)])
|
||||
:on-long-press #(rf/dispatch [:share/copy-text-and-show-toast profile-url
|
||||
(i18n/label :t/link-to-profile-copied)])}
|
||||
[quo/text
|
||||
{:style style/profile-address-content
|
||||
:size :paragraph-1
|
||||
:weight :medium
|
||||
:ellipsize-mode :middle
|
||||
:number-of-lines 1}
|
||||
abbreviated-url]]]
|
||||
[rn/view {:style style/share-button-container}
|
||||
[quo/button
|
||||
{:icon true
|
||||
:type :blur-bg
|
||||
:size 32
|
||||
:accessibility-label :link-to-profile
|
||||
:override-theme :dark
|
||||
:on-press #(list-selection/open-share {:message profile-url})}
|
||||
:i/share]]]]
|
||||
|
||||
[rn/view {:style style/emoji-hash-container}
|
||||
[rn/view {:style style/emoji-address-container}
|
||||
[rn/view {:style style/emoji-address-column}
|
||||
[quo/text
|
||||
{:size :paragraph-2
|
||||
:weight :medium
|
||||
:style style/emoji-hash-label}
|
||||
(i18n/label :t/emoji-hash)]
|
||||
[rn/touchable-highlight
|
||||
{:active-opacity 1
|
||||
:underlay-color colors/neutral-80-opa-1-blur
|
||||
:background-color :transparent
|
||||
:on-press #(rf/dispatch [:share/copy-text-and-show-toast emoji-hash
|
||||
(i18n/label :t/emoji-hash-copied)])
|
||||
:on-long-press #(rf/dispatch [:share/copy-text-and-show-toast emoji-hash
|
||||
(i18n/label :t/emoji-hash-copied)])}
|
||||
[rn/text {:style style/emoji-hash-content} emoji-hash]]]]
|
||||
[rn/view {:style style/emoji-share-button-container}
|
||||
[quo/button
|
||||
{:icon true
|
||||
:type :blur-bg
|
||||
:size 32
|
||||
:accessibility-label :link-to-profile
|
||||
:override-theme :dark
|
||||
:style {:margin-right 12}
|
||||
:on-press #(rf/dispatch [:share/copy-text-and-show-toast emoji-hash
|
||||
(i18n/label :t/emoji-hash-copied)])
|
||||
:on-long-press #(rf/dispatch [:share/copy-text-and-show-toast emoji-hash
|
||||
(i18n/label :t/emoji-hash-copied)])}
|
||||
:i/copy]]]]))
|
||||
|
||||
(defn wallet-tab
|
||||
[]
|
||||
[rn/text {:style style/wip-style} "not implemented"])
|
||||
|
||||
(defn tab-content
|
||||
[window-width]
|
||||
(let [selected-tab (reagent/atom :profile)]
|
||||
(fn []
|
||||
[:<>
|
||||
[header]
|
||||
[rn/view {:style style/tabs-container}
|
||||
[quo/segmented-control
|
||||
{:size 28
|
||||
:blur? true
|
||||
:override-theme :dark
|
||||
:on-change #(reset! selected-tab %)
|
||||
:default-active :profile
|
||||
:data [{:id :profile
|
||||
:label (i18n/label :t/profile)}
|
||||
{:id :wallet
|
||||
:label (i18n/label :t/wallet)}]}]]
|
||||
(if (= @selected-tab :profile)
|
||||
[profile-tab window-width]
|
||||
[wallet-tab])])))
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [window-width (rf/sub [:dimensions/window-width])]
|
||||
(fn []
|
||||
[rn/view
|
||||
{:flex 1
|
||||
:padding-top (navigation/status-bar-height)}
|
||||
[blur/view style/blur]
|
||||
[tab-content window-width]])))
|
||||
@@ -14,6 +14,7 @@
|
||||
[status-im2.db :as db]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.datetime :as datetime]
|
||||
status-im2.contexts.share.events
|
||||
status-im2.contexts.syncing.events
|
||||
status-im2.contexts.chat.events))
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
{:component {:id comp
|
||||
:name comp
|
||||
:options (merge (options/statusbar-and-navbar)
|
||||
{:layout {:orientation :portrait}}
|
||||
options
|
||||
(if (:topBar options)
|
||||
(options/merge-top-bar (options/topbar-options) options)
|
||||
@@ -167,7 +166,7 @@
|
||||
:id comp
|
||||
:options (merge (options/statusbar)
|
||||
{:layout {:componentBackgroundColor :transparent
|
||||
:orientation :portrait}
|
||||
:orientation ["portrait"]}
|
||||
:overlay {:interceptTouchOutside true}}
|
||||
opts)}})))
|
||||
|
||||
@@ -178,7 +177,8 @@
|
||||
(fn []
|
||||
(show-overlay "toasts"
|
||||
{:overlay {:interceptTouchOutside false}
|
||||
:layout {:componentBackgroundColor :transparent}})))
|
||||
:layout {:componentBackgroundColor :transparent
|
||||
:orientation ["portrait"]}})))
|
||||
(re-frame/reg-fx :hide-toasts (fn [] (dissmiss-overlay "toasts")))
|
||||
|
||||
;; bottom sheet
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
(defn default-options
|
||||
[]
|
||||
{:layout {:orientation :portrait}
|
||||
{:layout {:orientation ["portrait"]}
|
||||
:topBar {:visible false}})
|
||||
|
||||
;; Note: Currently, the status bar style provided while setting the root has a high preference,
|
||||
@@ -27,14 +27,14 @@
|
||||
{:topBar {:visible false}
|
||||
:layout {:componentBackgroundColor (or background-color
|
||||
(colors/theme-colors colors/white colors/neutral-100))
|
||||
:orientation :portrait
|
||||
:orientation ["portrait"]
|
||||
:backgroundColor (or background-color
|
||||
(colors/theme-colors colors/white
|
||||
colors/neutral-100))}}))
|
||||
|
||||
(def onboarding-layout
|
||||
{:componentBackgroundColor colors/neutral-80-opa-80-blur
|
||||
:orientation :portrait
|
||||
:orientation ["portrait"]
|
||||
:backgroundColor colors/neutral-80-opa-80-blur})
|
||||
|
||||
(defn navbar
|
||||
@@ -75,7 +75,7 @@
|
||||
(merge
|
||||
{:modalPresentationStyle :overCurrentContext
|
||||
:layout {:componentBackgroundColor :transparent
|
||||
:orientation :portrait
|
||||
:orientation ["portrait"]
|
||||
:backgroundColor :transparent}}
|
||||
(if platform/android?
|
||||
{:statusBar {:backgroundColor :transparent
|
||||
@@ -85,6 +85,7 @@
|
||||
|
||||
(def sheet-options
|
||||
{:layout {:componentBackgroundColor :transparent
|
||||
:orientation ["portrait"]
|
||||
:backgroundColor :transparent}
|
||||
:modalPresentationStyle :overCurrentContext})
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
[status-im2.contexts.chat.group-details.view :as group-details]
|
||||
[status-im.ui.screens.screens :as old-screens]
|
||||
[status-im2.contexts.communities.actions.request-to-join.view :as join-menu]
|
||||
[status-im2.contexts.syncing.setup-syncing.view :as settings-setup-syncing]))
|
||||
[status-im2.contexts.syncing.setup-syncing.view :as settings-setup-syncing]
|
||||
[status-im2.contexts.share.view :as share]))
|
||||
|
||||
(defn screens
|
||||
[]
|
||||
@@ -42,6 +43,10 @@
|
||||
:options options/transparent-screen-options
|
||||
:component activity-center/view}
|
||||
|
||||
{:name :share-shell
|
||||
:options options/transparent-screen-options
|
||||
:component share/view}
|
||||
|
||||
{:name :shell-stack
|
||||
:component shell/shell-stack}
|
||||
|
||||
|
||||
@@ -992,7 +992,9 @@
|
||||
:your-data-belongs-to-you
|
||||
:your-data-belongs-to-you-description
|
||||
:your-recovery-phrase
|
||||
:your-recovery-phrase-description})
|
||||
:your-recovery-phrase-description
|
||||
:empty-keycard
|
||||
:user-keycard})
|
||||
|
||||
;; NOTE: the rest checkpoints are based on the previous one, defined
|
||||
;; like this:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
(def ^:const contact-images-action "/contactImages")
|
||||
(def ^:const generate-qr-action "/GenerateQRCode")
|
||||
(def ^:const status-profile-base-url "https://join.status.im/u/")
|
||||
(def ^:const status-profile-base-url-without-https "join.status.im/u/")
|
||||
|
||||
(defn timestamp [] (datetime/timestamp))
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"_comment": "Instead use: scripts/update-status-go.sh <rev>",
|
||||
"owner": "status-im",
|
||||
"repo": "status-go",
|
||||
"version": "v0.151.5",
|
||||
"commit-sha1": "31d02f5197684317ad9f14874b0980d5ca716be6",
|
||||
"src-sha256": "0ih77pq3lbk6qadv018pp7gzd6vhgzcl34vm1vg2mdmv2f15zvlw"
|
||||
"version": "v0.151.12",
|
||||
"commit-sha1": "6fa8c113821509d744b81d90dc21c1e79c575577",
|
||||
"src-sha256": "02mmhy4z6gd9xlqjhw4i4j48gd85pijyyga48if9dd3zasqkp3y1"
|
||||
}
|
||||
|
||||
@@ -263,28 +263,28 @@ def pytest_runtest_makereport(item, call):
|
||||
is_group = "xdist_group" in item.keywords._markers or "xdist_group" in item.parent.keywords._markers
|
||||
error_intro, error = 'Test setup failed:', ''
|
||||
final_error = '%s %s' % (error_intro, error)
|
||||
if (hasattr(report, 'wasxfail') and not case_ids_set) or (hasattr(report, 'wasxfail') and (str([mark.args[0] for mark in item.iter_markers(name='testrail_id')][0]) in str(case_ids_set))):
|
||||
if '[NOTRUN]' in report.wasxfail:
|
||||
test_suite_data.set_current_test(item.name, testrail_case_id=get_testrail_case_id(item))
|
||||
test_suite_data.current_test.create_new_testrun()
|
||||
if is_group:
|
||||
test_suite_data.current_test.group_name = item.instance.__class__.__name__
|
||||
error_intro, error = 'Test is not run, e2e blocker ', report.wasxfail
|
||||
final_error = "%s [[%s]]" % (error_intro, error)
|
||||
else:
|
||||
if is_group:
|
||||
test_suite_data.current_test.group_name = item.instance.__class__.__name__
|
||||
error = catch_error()
|
||||
final_error = '%s %s [[%s]]' % (error_intro, error, report.wasxfail)
|
||||
else:
|
||||
if is_group and report.failed:
|
||||
test_suite_data.current_test.group_name = item.instance.__class__.__name__
|
||||
error = catch_error()
|
||||
final_error = '%s %s' % (error_intro, error)
|
||||
if is_sauce_env:
|
||||
update_sauce_jobs(test_suite_data.current_test.group_name,
|
||||
test_suite_data.current_test.testruns[-1].jobs,
|
||||
report.passed)
|
||||
# if (hasattr(report, 'wasxfail') and not case_ids_set) or (hasattr(report, 'wasxfail') and (str([mark.args[0] for mark in item.iter_markers(name='testrail_id')][0]) in str(case_ids_set))):
|
||||
# if '[NOTRUN]' in report.wasxfail:
|
||||
# test_suite_data.set_current_test(item.name, testrail_case_id=get_testrail_case_id(item))
|
||||
# test_suite_data.current_test.create_new_testrun()
|
||||
# if is_group:
|
||||
# test_suite_data.current_test.group_name = item.instance.__class__.__name__
|
||||
# error_intro, error = 'Test is not run, e2e blocker ', report.wasxfail
|
||||
# final_error = "%s [[%s]]" % (error_intro, error)
|
||||
# else:
|
||||
# if is_group:
|
||||
# test_suite_data.current_test.group_name = item.instance.__class__.__name__
|
||||
# error = catch_error()
|
||||
# final_error = '%s %s [[%s]]' % (error_intro, error, report.wasxfail)
|
||||
# else:
|
||||
if is_group and report.failed:
|
||||
test_suite_data.current_test.group_name = item.instance.__class__.__name__
|
||||
error = catch_error()
|
||||
final_error = '%s %s' % (error_intro, error)
|
||||
if is_sauce_env:
|
||||
update_sauce_jobs(test_suite_data.current_test.group_name,
|
||||
test_suite_data.current_test.testruns[-1].jobs,
|
||||
report.passed)
|
||||
if error:
|
||||
test_suite_data.current_test.testruns[-1].error = final_error
|
||||
from support.github_report import GithubHtmlReport
|
||||
@@ -295,10 +295,10 @@ def pytest_runtest_makereport(item, call):
|
||||
error = catch_error()
|
||||
if report.failed:
|
||||
current_test.testruns[-1].error = error
|
||||
if (hasattr(report, 'wasxfail') and not case_ids_set) or (hasattr(report, 'wasxfail') and (str([mark.args[0] for mark in item.iter_markers(name='testrail_id')][0]) in str(case_ids_set))):
|
||||
current_test.testruns[-1].xfail = report.wasxfail
|
||||
if error:
|
||||
current_test.testruns[-1].error = '%s [[%s]]' % (error, report.wasxfail)
|
||||
# if (hasattr(report, 'wasxfail') and not case_ids_set) or (hasattr(report, 'wasxfail') and (str([mark.args[0] for mark in item.iter_markers(name='testrail_id')][0]) in str(case_ids_set))):
|
||||
# current_test.testruns[-1].xfail = report.wasxfail
|
||||
# if error:
|
||||
# current_test.testruns[-1].error = '%s [[%s]]' % (error, report.wasxfail)
|
||||
if is_sauce_env:
|
||||
update_sauce_jobs(current_test.name, current_test.testruns[-1].jobs, report.passed)
|
||||
if item.config.getoption('docker'):
|
||||
|
||||
@@ -1065,7 +1065,6 @@ class TestOneToOneChatMultipleSharedDevicesNewUi(MultipleSharedDeviceTestCase):
|
||||
self.errors.verify_no_errors()
|
||||
|
||||
@marks.testrail_id(702813)
|
||||
@marks.xfail(reason="blocked by 15859")
|
||||
def test_1_1_chat_push_emoji(self):
|
||||
message_no_pn, message = 'No PN', 'Text push notification'
|
||||
|
||||
|
||||