Compare commits

..
Author SHA1 Message Date
Ibrahem 10de3c9759 [16153] Remove scrolling to first message(s) 2023-06-03 16:12:02 +03:00
Yevheniia Berdnyk eea737625d e2e: fixes for emoji and xfails added 2023-06-03 02:16:17 +03:00
Alexander 39ea12cf29 Fix for "Rendered more hooks than during the previous render" error on closing group chat/communty channel (#16145)
* "Rendered more hooks than during the previous render" error on closing group chat/communty channel

* Lint fix
2023-06-02 14:49:46 +01:00
Mohamed Javid c5166ac48e [Fix] Open app on push notification tap (#16136)
Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
2023-06-02 18:03:27 +05:30
Omar Basem c5e6bd790d Lightbox refactoring (#16096)
* refactor: lightbox
2023-06-02 15:56:39 +04:00
Ulises Manuel Cárdenas b29d248a9b [#16077] Empty state component 2023-06-02 04:16:55 -06:00
Alexander 396ee208bf Chat Screen Top Bar UI + new UI for user details (#15204)
* Fixes

* Reformatting + fixes

* Functions rewrite

* f-function

* One more f-function

* Minor constants fixes

* Jump to button removal

* Footer insets fix

* Better loading indicator

* Review fixes

* Fixes for Android

* More fixes

* More fixes

* Fix

* Fixes for scaling

* Overscroll fixes

* Better empty view on Android

* Android fixes, scrolling fixes

* Value fix

* Code style fixes

* Fix for scroll indicator insets

* Fixes

* Accessibility-ids

* Code style fixes

* Footer fix

* Style update
2023-06-01 16:08:47 +01:00
Rahul Pratap adb50fa0ee Feature/15776 slideshow styles (#15933)
* Fixed issues with styling in the slideshow.

* Fixed design feedbacks.
2023-06-01 19:56:36 +05:30
Icaro Motta 5017e13013 Fix reaction images and implement Selectors > Reactions component (#16114)
Fixes reaction images and implements the component Selectors > Reactions that,
for some reason, wasn't implemented as a separate quo2 component as per
Figma https://www.figma.com/file/qLLuMLfpGxK9OfpIavwsmK/Iconset?type=design&node-id=942-218&t=cqTr12Q3zVHaLoap-0

Fixes https://github.com/status-im/status-mobile/issues/16045

Note: Reaction images in the Design System are not icons, so that's why you are
seeing a bunch of icons removed from icons2. The directory
resources/images/reactions already existed, and so I used images from that
directory instead.
2023-06-01 11:14:20 -03:00
Icaro Motta a6fe626d78 Fix reaction images and implement Selectors > Reactions component (#16114)
Fixes reaction images and implements the component Selectors > Reactions that,
for some reason, wasn't implemented as a separate quo2 component as per
Figma https://www.figma.com/file/qLLuMLfpGxK9OfpIavwsmK/Iconset?type=design&node-id=942-218&t=cqTr12Q3zVHaLoap-0

Fixes https://github.com/status-im/status-mobile/issues/16045

Note: Reaction images in the Design System are not icons, so that's why you are
seeing a bunch of icons removed from icons2. The directory
resources/images/reactions already existed, and so I used images from that
directory instead.
2023-06-01 11:13:38 -03:00
Alexander 8f92fe344a Disable translations - only use English for the moment until designs are stable and translations are correct (#16103) 2023-06-01 14:56:25 +01:00
frank 19a76c22d1 fix #16043 (#16126)
* fix #16043

* update status-go-version.json
2023-06-01 21:17:08 +08:00
Ulises Manuel Cárdenas c62204121d [#15759] - Add onboarding recovery phrase screen 2023-06-01 05:23:07 -06:00
flexsurfer a4bc18ee3f improve photo-selector and adjust according to the latest designs (#16053) 2023-06-01 10:35:57 +02:00
Siddarth Kumar 68e53e05bd Adjust padding for QR Images (#15975) 2023-06-01 13:59:12 +05:30
Mohamed Javid d142d58677 Local Pairing Updates (#16065)
Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
2023-06-01 13:40:22 +05:30
Ibrahem Khalil 56dbb77ee5 [15660] Show who sent message reaction (#15677) 2023-06-01 09:21:33 +02:00
Icaro Motta 0dd3cb51fd Improve option to mark all notifications read (#16044)
- [x] Use correct icon to mark notifications as read
- [x] Add support for the disabled state in action drawer actions
- [x] Update quo2 preview `drawers` > `action-drawers`
- [x] Rewrite the action drawer component spec to use our test helpers

Fixes https://github.com/status-im/status-mobile/issues/14983
2023-05-31 21:25:19 -03:00
114 changed files with 2195 additions and 1394 deletions
@@ -67,8 +67,8 @@ public class PushNotificationHelper {
private static final long DEFAULT_VIBRATION = 300L;
private static final String CHANNEL_ID = "status-im-notifications";
public static final String ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION";
public static final String ACTION_TAP_NOTIFICATION = "im.status.ethereum.module.TAP_NOTIFICATION";
public static final String ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP";
final int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_CANCEL_CURRENT;
private NotificationManager notificationManager;
@@ -119,13 +119,8 @@ public class PushNotificationHelper {
private final BroadcastReceiver notificationActionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == ACTION_TAP_NOTIFICATION ||
intent.getAction() == ACTION_DELETE_NOTIFICATION) {
String deepLink = intent.getExtras().getString("im.status.ethereum.deepLink");
if (intent.getAction() == ACTION_DELETE_NOTIFICATION) {
String groupId = intent.getExtras().getString("im.status.ethereum.groupId");
if (intent.getAction() == ACTION_TAP_NOTIFICATION) {
context.startActivity(getOpenAppIntent(deepLink));
}
if (groupId != null) {
cleanGroup(groupId);
}
@@ -141,7 +136,6 @@ public class PushNotificationHelper {
private void registerBroadcastReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_DELETE_NOTIFICATION);
filter.addAction(ACTION_TAP_NOTIFICATION);
filter.addAction(ACTION_TAP_STOP);
context.registerReceiver(notificationActionReceiver, filter);
Log.e(LOG_TAG, "Broadcast Receiver registered");
@@ -701,37 +695,31 @@ public class PushNotificationHelper {
private StatusMessage createMessage(Bundle data) {
Person author = getPerson(data.getBundle("notificationAuthor"));
return new StatusMessage(data.getString("id"), author, data.getLong("timestamp"), data.getString("message"));
long timeStampLongValue = (long) data.getDouble("timestamp");
return new StatusMessage(data.getString("id"), author, timeStampLongValue, data.getString("message"));
}
private PendingIntent createGroupOnDismissedIntent(Context context, int notificationId, String groupId, String deepLink) {
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
intent.putExtra("im.status.ethereum.deepLink", deepLink);
intent.putExtra("im.status.ethereum.groupId", groupId);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
}
private PendingIntent createGroupOnTapIntent(Context context, int notificationId, String groupId, String deepLink) {
Intent intent = new Intent(ACTION_TAP_NOTIFICATION);
intent.putExtra("im.status.ethereum.deepLink", deepLink);
intent.putExtra("im.status.ethereum.groupId", groupId);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
Intent intent = getOpenAppIntent(deepLink);
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
}
private PendingIntent createOnTapIntent(Context context, int notificationId, String deepLink) {
Intent intent = new Intent(ACTION_TAP_NOTIFICATION);
intent.putExtra("im.status.ethereum.deepLink", deepLink);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
Intent intent = getOpenAppIntent(deepLink);
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
}
private PendingIntent createOnDismissedIntent(Context context, int notificationId, String deepLink) {
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
intent.putExtra("im.status.ethereum.deepLink", deepLink);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
}
public void removeStatusMessage(Bundle bundle) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1011 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+9 -8
View File
@@ -220,10 +220,10 @@
only icon
[button {:icon true} :i/close-circle]"
[_ _]
(let [pressed (reagent/atom false)]
(let [pressed-in (reagent/atom false)]
(fn
[{:keys [on-press disabled type size before after above
width customization-color override-theme override-background-color
[{:keys [on-press disabled type size before after above icon-secondary-no-color
width customization-color override-theme override-background-color pressed
on-long-press accessibility-label icon icon-no-color style inner-style test-ID]
:or {type :primary
size 40
@@ -234,17 +234,17 @@
[(or
override-theme
(theme/get-theme)) type])
state (cond disabled :disabled
@pressed :pressed
:else :default)
state (cond disabled :disabled
(or @pressed-in pressed) :pressed
:else :default)
icon-size (when (= 24 size) 12)
icon-secondary-color (or icon-secondary-color icon-color)]
[rn/touchable-without-feedback
(merge {:test-ID test-ID
:disabled disabled
:accessibility-label accessibility-label
:on-press-in #(reset! pressed true)
:on-press-out #(reset! pressed nil)}
:on-press-in #(reset! pressed-in true)
:on-press-out #(reset! pressed-in nil)}
(when on-press
{:on-press on-press})
(when on-long-press
@@ -306,5 +306,6 @@
[quo2.icons/icon after
{:container-style {:margin-left 4
:margin-right (if (= size 40) 12 8)}
:no-color icon-secondary-no-color
:color icon-secondary-color
:size icon-size}]])]]]))))
@@ -42,7 +42,8 @@
(case type
:words
[rn/flat-list
{:data words
{:keyboard-should-persist-taps :always
:data words
:content-container-style style/word-list
:render-fn word-component
:render-data {:blur? blur?
@@ -1,53 +1,62 @@
(ns quo2.components.drawers.action-drawers.component-spec
(:require ["@testing-library/react-native" :as rtl]
[quo2.components.drawers.action-drawers.view :as action-drawer]
[reagent.core :as reagent]))
(:require [quo2.components.drawers.action-drawers.view :as action-drawer]
[test-helpers.component :as h]))
(defn render-action-drawer
([options]
(rtl/render (reagent/as-element [action-drawer/action-drawer options]))))
(h/describe "action drawer"
(h/test "default render"
(h/render [action-drawer/action-drawer
[[{:icon :i/friend
:label "a sample label"}]]])
(h/is-truthy (h/get-by-text "a sample label")))
(js/global.test "action-drawer renders with elements label displaying"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"}]])
(-> (js/expect (rtl/screen.getByText "a sample label"))
(.toBeTruthy))))
(h/test "renders with elements sub-label displaying"
(h/render [action-drawer/action-drawer
[[{:icon :i/friend
:label "a sample label"
:sub-label "a sample sub label"}]]])
(h/is-truthy (h/get-by-text "a sample sub label")))
(js/global.test "action-drawer renders with elements sub-label displaying"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:sub-label "a sample sub label"}]])
(-> (js/expect (rtl/screen.getByText "a sample sub label"))
(.toBeTruthy))))
(h/test "on click action works on element"
(let [on-press (js/jest.fn)]
(h/render [action-drawer/action-drawer
[[{:icon :i/friend
:label "a sample label"
:on-press on-press}]]])
(h/fire-event :press (h/get-by-text "a sample label"))
(h/was-called on-press)))
(js/global.test "action-drawer on click action works on element"
(let [event (js/jest.fn)]
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:on-press event}]])
(rtl/fireEvent.press (rtl/screen.getByText "a sample label"))
(-> (js/expect event)
(.toHaveBeenCalled)))))
(h/test "disabled state"
(let [on-press-muted (js/jest.fn)
on-press-friend (js/jest.fn)
label-mute "Mute"
label-friend "View member details"]
(h/render [action-drawer/action-drawer
[[{:icon :i/muted
:label label-mute
:on-press on-press-muted}
{:icon :i/friend
:label label-friend
:disabled? true
:on-press on-press-friend}]]])
(h/fire-event :press (h/get-by-text label-mute))
(h/was-called on-press-muted)
(js/global.test "action-drawer renders two icons when set"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:right-icon :i/friend
:accessibility-label :first-element}]])
(-> (js/expect (rtl/screen.getByLabelText "right-icon-for-action"))
(.toBeTruthy))
(-> (js/expect (rtl/screen.queryByLabelText "left-icon-for-action"))
(.toBeTruthy))))
(h/fire-event :press (h/get-by-text label-friend))
(h/was-not-called on-press-friend)))
(js/global.test "action-drawer renders a divider when the add-divider? prop is true"
(fn []
(render-action-drawer [[{:icon :i/friend
:label "a sample label"
:add-divider? true
:accessibility-label :first-element}]])
(-> (js/expect (rtl/screen.getAllByLabelText "divider"))
(.toBeTruthy))))
(h/test "renders two icons when set"
(h/render [action-drawer/action-drawer
[[{:icon :i/friend
:label "a sample label"
:right-icon :i/friend
:accessibility-label :first-element}]]])
(h/is-truthy (h/get-by-label-text "right-icon-for-action"))
(h/is-truthy (h/query-by-label-text "left-icon-for-action")))
(h/test "renders a divider when the add-divider? prop is true"
(h/render [action-drawer/action-drawer
[[{:icon :i/friend
:label "a sample label"
:add-divider? true
:accessibility-label :first-element}]]])
(h/is-truthy (h/get-all-by-label-text "divider"))))
@@ -11,9 +11,10 @@
:flex-direction :row})
(defn container
[sub-label]
[sub-label disabled?]
{:border-radius 12
:height (if sub-label 58 50)
:opacity (when disabled? 0.3)
:margin-horizontal 8})
(defn row-container
@@ -18,12 +18,19 @@
:accessible true
:accessibility-label :divider}])
(defn action
(defn- maybe-pressable
[disabled? props child]
(if disabled?
[rn/view (dissoc props :on-press) child]
[rn/touchable-highlight props child]))
(defn- action
[{:keys [icon
label
sub-label
right-icon
danger?
disabled?
on-press
add-divider?
override-theme
@@ -33,9 +40,9 @@
[:<> {:key label}
(when add-divider?
[divider])
[rn/touchable-highlight
[maybe-pressable disabled?
{:accessibility-label accessibility-label
:style (style/container sub-label)
:style (style/container sub-label disabled?)
:underlay-color (colors/theme-colors colors/neutral-5 colors/neutral-90 override-theme)
:on-press on-press}
[rn/view
@@ -1,10 +1,11 @@
(ns quo2.components.drawers.drawer-buttons.style
(:require [quo2.foundations.colors :as colors]
[react-native.platform :as platform]))
(:require [quo2.foundations.colors :as colors]))
(def outer-container
{:height 216
:border-radius 20})
{:height 216
:border-top-left-radius 20
:border-top-right-radius 20
:overflow :hidden})
(def top-card
{:flex 1
@@ -12,9 +13,7 @@
:padding-horizontal 20
:border-top-left-radius 20
:border-top-right-radius 20
:background-color (if platform/ios?
colors/neutral-80-opa-80-blur
colors/neutral-80)})
:background-color colors/neutral-80-opa-80-blur})
(def bottom-card
{:position :absolute
@@ -33,11 +32,11 @@
:justify-content :space-between})
(def bottom-icon
{:border-radius 40
{:border-radius 32
:border-width 1
:margin-left 24
:height 28
:width 28
:height 32
:width 32
:justify-content :center
:align-items :center
:border-color colors/white-opa-5})
@@ -52,4 +51,4 @@
(defn heading-text
[gap]
{:color colors/white
:margin-bottom gap})
:margin-bottom gap})
@@ -71,14 +71,28 @@
child-2 string, keyword or hiccup
"
[{:keys [container-style top-card bottom-card]} child-1 child-2]
[blur/ios-view
[rn/view
{:style (merge container-style style/outer-container)}
[card
(merge {:gap 4
:top? true
:style style/top-card}
top-card) child-1]
[card
(merge {:style style/bottom-card
:gap 20}
bottom-card) child-2]])
[blur/view
{:blur-type :dark
:blur-amount 10
:style {:flex 1
:border-top-left-radius 20
:border-top-right-radius 20}}]
[rn/view
{:style {:flex 1
:background-color :transparent
:position :absolute
:top 0
:left 0
:right 0
:bottom 0}}
[card
(merge {:gap 4
:top? true
:style style/top-card}
top-card) child-1]
[card
(merge {:style style/bottom-card
:gap 20}
bottom-card) child-2]]])
+11 -162
View File
@@ -1,165 +1,14 @@
(ns quo2.components.dropdowns.dropdown
(:require [quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]))
(defn apply-anim
[dd-height val]
(reanimated/animate-shared-value-with-delay dd-height
val
300
:easing1
0))
(def sizes
{:big {:icon-size 20
:font {:font-size :paragraph-1}
:height 40
:padding {:padding-with-icon {:padding-vertical 9
:padding-horizontal 12}
:padding-with-no-icon {:padding-vertical 9
:padding-left 12
:padding-right 8}}}
:medium {:icon-size 20
:font {:font-size :paragraph-1}
:height 32
:padding {:padding-with-icon {:padding-vertical 5
:padding-horizontal 8}
:padding-with-no-icon {:padding-vertical 5
:padding-left 12
:padding-right 8}}}
:small {:icon-size 15
:font {:font-size :paragraph-2}
:height 24
:padding {:padding-with-icon {:padding-vertical 3
:padding-horizontal 6}
:padding-with-no-icon {:padding-vertical 3
:padding-horizontal 6}}}})
(defn color-by-10
[color]
(colors/alpha color 0.6))
(defn dropdown-comp
[{:keys [icon dd-height size disabled? dd-color use-border? border-color]}]
(let [dark? (colors/dark?)
{:keys [width height width-with-icon padding font icon-size]} (size sizes)
{:keys [padding-with-icon padding-with-no-icon]} padding
font-size (:font-size font)
spacing (case size
:big 4
:medium 2
:small 2)
open? (reagent/atom false)]
(fn []
[rn/touchable-opacity
(cond->
{:on-press (fn []
(if (swap! open? not)
(apply-anim dd-height 120)
(apply-anim dd-height 0)))
:style (cond->
(merge
(if icon
padding-with-icon
padding-with-no-icon)
{:width (if icon
width-with-icon
width)
:height height
:border-radius (case size
:big 12
:medium 10
:small 8)
:flex-direction :row
:align-items :center
:background-color (if @open?
dd-color
(color-by-10 dd-color))})
use-border? (assoc :border-width 1
:border-color (if @open?
border-color
(color-by-10 border-color))))}
disabled? (assoc-in [:style :opacity] 0.3)
disabled? (assoc :disabled true))
(when icon
[icons/icon icon
{:no-color true
:size 20
:container-style {:margin-right spacing
:margin-top 1
:width icon-size
:height icon-size}}])
[text/text
{:size font-size
:weight :medium
:font :font-medium
:color :main} "Dropdown"]
[icons/icon
(if @open?
(if dark?
:main-icons/pullup-dark
:main-icons/pullup)
(if dark?
:main-icons/dropdown-dark
:main-icons/dropdown))
{:size 20
:no-color true
:container-style {:width (+ icon-size 3)
:border-radius 20
:margin-left (if (= :small size)
2
4)
:margin-top 1
:height (+ icon-size 4)}}]])))
(defn items-comp
[{:keys [items on-select]}]
(let [items-count (count items)]
[rn/scroll-view
{:horizontal false
:nestedScrollEnabled true}
(doall
(map-indexed (fn [index item]
[rn/touchable-opacity
{:key (str item index)
:style {:padding 4
:border-bottom-width (if (= index (- items-count 1))
0
1)
:border-color (colors/theme-colors
colors/neutral-100
colors/white)
:text-align :center}
:on-press #(on-select item)}
[text/text {:style {:text-align :center}} item]])
items))]))
(defn- f-dropdown
[{:keys [items icon text default-item on-select size disabled? border-color use-border? dd-color]}]
(let [dd-height (reanimated/use-shared-value 0)]
[rn/view {:style {:flex-grow 1}}
[dropdown-comp
{:items items
:icon icon
:disabled? disabled?
:size size
:dd-color dd-color
:text text
:border-color (colors/custom-color-by-theme border-color 50 60)
:use-border? use-border?
:default-item default-item
:dd-height dd-height}]
[reanimated/view
{:style (reanimated/apply-animations-to-style
{:height dd-height}
{})}
[items-comp
{:items items
:on-select on-select}]]]))
(:require [quo2.components.buttons.button :as button]))
(defn dropdown
[params]
[:f> f-dropdown params])
[_ _]
(fn [{:keys [on-change selected] :as opts} children]
[button/button
(merge
opts
{:after (if selected :i/pullup :i/dropdown)
:icon-secondary-no-color true
:pressed selected
:on-press #(when on-change (on-change selected))})
children]))
@@ -0,0 +1,30 @@
(ns quo2.components.empty-state.empty-state.styles
(:require [quo2.foundations.colors :as colors]))
(def container
{:padding 12
:align-items :center})
(def image
{:width 80
:height 80})
(def text-container
{:margin-top 12
:align-items :center})
(defn title
[blur?]
(cond-> {:margin-bottom 2}
blur? (assoc :color colors/white)))
(defn description
[blur?]
(when blur?
{:color colors/white}))
(def button-container {:margin-top 20})
(defn upper-button-color
[customization-color]
(colors/custom-color-by-theme customization-color 50 60))
@@ -0,0 +1,50 @@
(ns quo2.components.empty-state.empty-state.view
(:require [quo2.components.buttons.button :as button]
[quo2.components.empty-state.empty-state.styles :as styles]
[quo2.components.markdown.text :as text]
[react-native.core :as rn]
[react-native.fast-image :as fast-image]))
(defn empty-state
[{:keys [customization-color image title description blur?]
upper-button :upper-button
lower-button :lower-button
:or {customization-color :blue}}]
[rn/view {:style styles/container}
[fast-image/fast-image
{:style styles/image
:source image}]
[rn/view {:style styles/text-container}
[text/text
{:style (styles/title blur?)
:number-of-lines 1
:weight :semi-bold
:size :paragraph-1}
title]
[text/text
{:style (styles/description blur?)
:number-of-lines 1
:text-align :center
:weight :regular
:size :paragraph-2}
description]]
(when-let [{upper-button-text :text
upper-button-on-press :on-press} upper-button]
[rn/view {:style styles/button-container}
[button/button
(cond-> {:type :primary
:size 32
:override-background-color (styles/upper-button-color customization-color)
:on-press upper-button-on-press}
blur? (assoc :override-theme :dark))
upper-button-text]
(when-let [{lower-button-text :text
lower-button-on-press :on-press} lower-button]
[button/button
(cond-> {:style {:margin-top 12}
:size 32
:type :blur-bg
:on-press lower-button-on-press}
blur? (assoc :override-theme :dark))
lower-button-text])])])
@@ -28,8 +28,9 @@
(h/describe "Error text"
(h/test "Marked when words doesn't satisfy a predicate"
(h/render [recovery-phrase/recovery-phrase-input
{:mark-errors? true
:error-pred #(>= (count %) 5)}
{:mark-errors? true
:error-pred-current-word #(>= (count %) 5)
:error-pred-written-words #(>= (count %) 5)}
"Text with some error words that don't satisfy the predicate"])
(let [children-text-nodes (-> (h/get-by-label-text :recovery-phrase-input)
(oops/oget "props" "children" "props" "children")
@@ -15,15 +15,27 @@
text])
(defn- mark-error-words
[pred text word-limit]
(let [word-limit (or word-limit ##Inf)]
(into [:<>]
(comp (map-indexed (fn [idx word]
(if (or (pred word) (>= idx word-limit))
[error-word word]
word)))
(interpose " "))
(string/split text #" "))))
[pred-last-word pred-previous-words text word-limit]
(let [last-index (dec (count (string/split text #"\s+")))
words (map #(apply str %)
(partition-by #(= " " %) text))]
(->> words
(reduce (fn [{:keys [idx] :as acc} word]
(let [error-pred (if (= last-index idx) pred-last-word pred-previous-words)
invalid-word? (and (or (error-pred word)
(>= idx word-limit))
(not (string/blank? word)))
not-blank-spaces? (not (string/blank? word))]
(cond-> acc
not-blank-spaces? (update :idx inc)
:always (update :result
conj
(if invalid-word?
[error-word word]
word)))))
{:result [:<>]
:idx 0})
:result)))
(defn recovery-phrase-input
[_ _]
@@ -31,9 +43,11 @@
set-focused #(reset! state :focused)
set-default #(reset! state :default)]
(fn [{:keys [customization-color override-theme blur? on-focus on-blur mark-errors?
error-pred word-limit]
:or {customization-color :blue
error-pred (constantly false)}
error-pred-current-word error-pred-written-words word-limit]
:or {customization-color :blue
word-limit ##Inf
error-pred-current-word (constantly false)
error-pred-written-words (constantly false)}
:as props}
text]
(let [extra-props (apply dissoc props custom-props)]
@@ -53,5 +67,5 @@
(when on-blur (on-blur)))}
extra-props)
(if mark-errors?
(mark-error-words error-pred text word-limit)
(mark-error-words error-pred-current-word error-pred-written-words text word-limit)
text)]]))))
+8 -5
View File
@@ -1,6 +1,7 @@
(ns quo2.components.reactions.reaction
(:require [quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[quo2.components.reactions.resource :as resource]
[quo2.components.reactions.style :as style]
[quo2.foundations.colors :as colors]
[quo2.theme :as theme]
@@ -13,7 +14,7 @@
{:on-press on-press
:accessibility-label :emoji-reaction-add
:style (style/add-reaction)}
[icons/icon :i/add
[icons/icon :i/add-reaction
{:size 20
:color (if dark?
colors/white
@@ -21,15 +22,17 @@
(defn reaction
"Add your emoji as a param here"
[{:keys [emoji clicks neutral? on-press accessibility-label]}]
[{:keys [emoji clicks neutral? on-press accessibility-label on-long-press]}]
(let [numeric-value (int clicks)]
[rn/touchable-opacity
{:on-press on-press
:on-long-press on-long-press
:accessibility-label accessibility-label
:style (style/reaction neutral?)}
[icons/icon emoji
{:no-color true
:size 16}]
[rn/image
{:style {:width 16 :height 16}
:accessibility-label :emoji
:source (resource/get-reaction emoji)}]
[text/text
{:size :paragraph-2
:weight :semi-bold
@@ -0,0 +1,30 @@
(ns quo2.components.reactions.resource
(:require [clojure.java.io :as io]
[clojure.string :as string]))
(def ^:private reactions-dir "./resources/images/reactions/")
(defn- resolve-reaction
[reaction]
(let [path (str reactions-dir (name reaction) ".png")
file (io/file path)]
(when (.exists file)
`(js/require ~(str "." path)))))
(defn- find-all-image-base-names
[]
(let [dir (io/file reactions-dir)]
(->> dir
file-seq
(filter #(string/ends-with? % "png"))
(map #(.getName %))
(map #(string/replace % #"\.png$" ""))
(map #(first (string/split % #"@")))
distinct)))
(defmacro resolve-all-reactions
[]
(reduce (fn [acc reaction]
(assoc acc reaction (resolve-reaction reaction)))
{}
(find-all-image-base-names)))
@@ -0,0 +1,12 @@
(ns quo2.components.reactions.resource
(:require-macros [quo2.components.reactions.resource :refer [resolve-all-reactions]]))
(def ^:private reactions
(resolve-all-reactions))
(defn get-reaction
[reaction]
(assert (keyword? reaction) "Reaction should be a keyword")
(assert (= "reaction" (namespace reaction))
"Reaction keyword should be namespaced with :reaction")
(get reactions (name reaction)))
@@ -0,0 +1,23 @@
(ns quo2.components.selectors.reactions.component-spec
(:require [quo2.components.selectors.reactions.view :as view]
[test-helpers.component :as h]))
(h/describe "Selectors > Reactions"
(h/test "renders component"
(h/render [view/view :reaction/sad])
(h/is-truthy (h/get-by-label-text :reaction)))
(h/describe "on-press event"
(h/test "starts with released state"
(let [on-press (h/mock-fn)]
(h/render [view/view :reaction/love {:on-press on-press}])
(h/fire-event :press (h/get-by-label-text :reaction))
(h/was-called on-press)))
(h/test "starts with pressed state"
(let [on-press (h/mock-fn)]
(h/render [view/view :reaction/love
{:on-press on-press
:start-pressed? true}])
(h/fire-event :press (h/get-by-label-text :reaction))
(h/was-called on-press)))))
@@ -0,0 +1,11 @@
(ns quo2.components.selectors.reactions.style
(:require [quo2.foundations.colors :as colors]))
(defn container
[pressed?]
{:padding 10
:border-radius 12
:border-width 1
:border-color (colors/theme-colors colors/neutral-20 colors/neutral-80)
:background-color (when pressed?
(colors/theme-colors colors/neutral-10 colors/neutral-80-opa-40))})
@@ -0,0 +1,25 @@
(ns quo2.components.selectors.reactions.view
(:require [quo2.components.reactions.resource :as reactions.resource]
[quo2.components.selectors.reactions.style :as style]
[react-native.core :as rn]
[reagent.core :as reagent]))
(defn view
[_ {:keys [start-pressed?]}]
(let [pressed? (reagent/atom start-pressed?)]
(fn [emoji
{:keys [container-style on-press
accessibility-label]
:or {accessibility-label :reaction}}]
[rn/touchable-without-feedback
{:accessibility-label accessibility-label
:on-press (fn [e]
(swap! pressed? not)
(when on-press
(on-press e)))}
[rn/view
{:style (merge (style/container @pressed?)
container-style)}
[rn/image
{:source (reactions.resource/get-reaction emoji)
:style {:width 20 :height 20}}]]])))
+7 -3
View File
@@ -6,7 +6,8 @@
[react-native.masked-view :as masked-view]
[reagent.core :as reagent]
[utils.collection :as utils.collection]
[utils.number :as utils.number]))
[utils.number :as utils.number]
[react-native.gesture :as gesture]))
(def default-tab-size 32)
(def unread-count-offset 3)
@@ -131,7 +132,8 @@
style
size
blur?
override-theme]
override-theme
in-scroll-view?]
:or {fade-end-percentage fade-end-percentage
fade-end? false
scrollable? false
@@ -142,7 +144,9 @@
[rn/view {:style {:margin-top (- (dec unread-count-offset))}}
[masked-view-wrapper
{:fade-end-percentage (get @fading :fade-end-percentage) :fade-end? fade-end?}
[rn/flat-list
[(if in-scroll-view?
gesture/flat-list
rn/flat-list)
(merge
(dissoc props
:default-active
+8
View File
@@ -30,6 +30,7 @@
quo2.components.drawers.permission-context.view
quo2.components.dropdowns.dropdown
quo2.components.header
quo2.components.empty-state.empty-state.view
quo2.components.icon
quo2.components.info.info-message
quo2.components.info.information-box
@@ -64,6 +65,7 @@
quo2.components.profile.profile-card.view
quo2.components.profile.select-profile.view
quo2.components.reactions.reaction
quo2.components.selectors.reactions.view
quo2.components.record-audio.record-audio.view
quo2.components.record-audio.soundtrack.view
quo2.components.selectors.disclaimer.view
@@ -110,6 +112,9 @@
(def skeleton quo2.components.loaders.skeleton/skeleton)
(def author quo2.components.messages.author.view/author)
;;;; SELECTORS
(def reactions quo2.components.selectors.reactions.view/view)
;;;; AVATAR
(def account-avatar quo2.components.avatars.account-avatar/account-avatar)
(def channel-avatar quo2.components.avatars.channel-avatar/channel-avatar)
@@ -164,6 +169,9 @@
(def drawer-buttons quo2.components.drawers.drawer-buttons.view/view)
(def permission-context quo2.components.drawers.permission-context.view/view)
;;;; EMPTY STATE
(def empty-state quo2.components.empty-state.empty-state.view/empty-state)
;;;; INPUTS
(def input quo2.components.inputs.input.view/input)
(def profile-input quo2.components.inputs.profile-input.view/profile-input)
+3 -2
View File
@@ -14,13 +14,13 @@
[quo2.components.drawers.drawer-buttons.component-spec]
[quo2.components.drawers.permission-context.component-spec]
[quo2.components.inputs.input.component-spec]
[quo2.components.keycard.component-spec]
[quo2.components.inputs.profile-input.component-spec]
[quo2.components.inputs.recovery-phrase.component-spec]
[quo2.components.inputs.title-input.component-spec]
[quo2.components.keycard.component-spec]
[quo2.components.links.link-preview.component-spec]
[quo2.components.links.url-preview-list.component-spec]
[quo2.components.links.url-preview.component-spec]
[quo2.components.links.link-preview.component-spec]
[quo2.components.markdown.--tests--.text-component-spec]
[quo2.components.notifications.notification.component-spec]
[quo2.components.onboarding.small-option-card.component-spec]
@@ -30,6 +30,7 @@
[quo2.components.record-audio.soundtrack.--tests--.soundtrack-component-spec]
[quo2.components.selectors.disclaimer.component-spec]
[quo2.components.selectors.filter.component-spec]
[quo2.components.selectors.reactions.component-spec]
[quo2.components.selectors.selectors.component-spec]
[quo2.components.share.share-qr-code.component-spec]
[quo2.components.tags.--tests--.status-tags-component-spec]))
+16
View File
@@ -0,0 +1,16 @@
(ns react-native.cameraroll
(:require ["@react-native-community/cameraroll" :as CameraRoll]
[utils.transforms :as transforms]
[taoensso.timbre :as log]))
(defn get-photos
[opts callback]
(-> (.getPhotos CameraRoll (clj->js opts))
(.then #(callback (transforms/js->clj %)))
(.catch #(log/warn "could not get camera roll photos" %))))
(defn get-albums
[opts callback]
(-> (.getAlbums CameraRoll (clj->js opts))
(.then #(callback (transforms/js->clj %)))
(.catch #(log/warn "could not get camera roll albums" %))))
+1
View File
@@ -17,6 +17,7 @@
(def view (reagent/adapt-react-class (.-View ^js react-native)))
(def scroll-view (reagent/adapt-react-class (.-ScrollView ^js react-native)))
(def image (reagent/adapt-react-class (.-Image ^js react-native)))
(defn image-get-size [uri callback] (.getSize ^js (.-Image ^js react-native) uri callback))
(def text (reagent/adapt-react-class (.-Text ^js react-native)))
(def text-input (reagent/adapt-react-class (.-TextInput ^js react-native)))
+4 -4
View File
@@ -91,7 +91,7 @@
[flat-list
(merge props
{:data data
:render-fn (fn [item]
(if (:header? item)
(render-section-header-fn item)
(render-fn item)))})]))
:render-fn (fn [p1 p2 p3 p4]
(if (:header? p1)
[render-section-header-fn p1 p2 p3 p4]
[render-fn p1 p2 p3 p4]))})]))
+1 -219
View File
@@ -1,47 +1,17 @@
(ns status-im.chat.models.images
(:require ["@react-native-community/cameraroll" :as CameraRoll]
["react-native-blob-util" :default ReactNativeBlobUtil]
[clojure.string :as string]
[re-frame.core :as re-frame]
[utils.i18n :as i18n]
[status-im.ui.components.permissions :as permissions]
[react-native.permissions :as permissions]
[status-im.ui.components.react :as react]
[status-im2.config :as config]
[status-im.utils.fs :as fs]
[utils.re-frame :as rf]
[status-im.utils.image-processing :as image-processing]
[status-im.utils.platform :as platform]
[status-im.utils.types :as types]
[status-im.utils.utils :as utils]
[taoensso.timbre :as log]))
(def maximum-image-size-px 2000)
(defn- resize-and-call
[uri cb]
(react/image-get-size
uri
(fn [width height]
(let [resize? (> (max width height) maximum-image-size-px)]
(image-processing/resize
uri
(if resize? maximum-image-size-px width)
(if resize? maximum-image-size-px height)
60
(fn [^js resized-image]
(let [path (.-path resized-image)
path (if (string/starts-with? path "file") path (str "file://" path))]
(cb {:resized-uri path
:width width
:height height})))
#(log/error "could not resize image" %))))))
(defn result->id
[^js result]
(if platform/ios?
(.-localIdentifier result)
(.-path result)))
(def temp-image-url (str (fs/cache-dir) "/StatusIm_Image.jpeg"))
(defn download-image-http
@@ -78,108 +48,6 @@
#(re-frame/dispatch [:chat.ui/image-captured current-chat-id (.-path %)])
{})))
(re-frame/reg-fx
::chat-open-image-picker
(fn [chat-id]
(react/show-image-picker
(fn [^js images]
;; NOTE(Ferossgp): Because we can't highlight the already selected images inside
;; gallery, we just clean previous state and set all newly picked images
(when (and platform/ios? (pos? (count images)))
(re-frame/dispatch [:chat.ui/clear-sending-images chat-id]))
(doseq [^js result (if platform/ios?
(take config/max-images-batch images)
[images])]
(resize-and-call (.-path result)
#(re-frame/dispatch [:chat.ui/image-selected chat-id (result->id result) %]))))
;; NOTE(Ferossgp): On android you cannot set max limit on images, when a user
;; selects too many images the app crashes.
{:media-type "photo"
:multiple platform/ios?})))
(re-frame/reg-fx
::image-selected
(fn [[image chat-id]]
(resize-and-call
(:uri image)
#(re-frame/dispatch [:chat.ui/image-selected chat-id image %]))))
(re-frame/reg-fx
::camera-roll-get-photos
(fn [[num end-cursor album]]
(permissions/request-permissions
{:permissions [:read-external-storage]
:on-allowed (fn []
(-> (if end-cursor
(.getPhotos
CameraRoll
#js
{:first num
:after end-cursor
:assetType "Photos"
:groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums")
:groupName (when (not= album (i18n/label :t/recent)) album)
:include (clj->js ["imageSize"])})
(.getPhotos
CameraRoll
#js
{:first num
:assetType "Photos"
:groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums")
:groupName (when (not= album (i18n/label :t/recent)) album)
:include (clj->js ["imageSize"])}))
(.then #(let [response (types/js->clj %)]
(re-frame/dispatch [:on-camera-roll-get-photos (:edges response)
(:page_info response) end-cursor])))
(.catch #(log/warn "could not get camera roll photos"))))})))
(re-frame/reg-fx
:chat.ui/camera-roll-get-albums
(fn []
(let [albums (atom [{:title :smart-albums :data []}
{:title (i18n/label :t/my-albums) :data []}])]
;; Get the "recent" album first
(->
(.getPhotos CameraRoll
#js
{:first 1
:groupTypes "All"})
(.then
(fn [res]
(let [recent-album {:title (i18n/label :t/recent)
:count "--"
:uri (get-in (first (:edges (types/js->clj res))) [:node :image :uri])}]
(swap! albums update-in [0 :data] conj recent-album)
;; Get albums, then loop over albums and get each one's cover (first photo)
(->
(.getAlbums CameraRoll #js {:assetType "All"})
(.then
(fn [response]
(let [response (types/js->clj response)]
(reduce
(fn [_ album]
(->
(.getPhotos
CameraRoll
#js
{:first 1
:groupTypes "Albums"
:groupName (:title album)})
(.then (fn [res]
(let [uri (get-in (first (:edges (types/js->clj res)))
[:node :image :uri])]
(swap! albums update-in [1 :data] conj (merge album {:uri uri}))
(when (= (count (get-in @albums [1 :data])) (count response))
(swap! albums update-in
[1 :data]
#(->> %
(sort-by :title)))
(re-frame/dispatch [:on-camera-roll-get-albums @albums])))))))
nil
response))))
(.catch #(log/warn "could not get camera roll albums"))))))))))
(rf/defn image-captured
{:events [:chat.ui/image-captured]}
[{:keys [db]} chat-id uri]
@@ -189,81 +57,17 @@
(not (get images uri)))
{::image-selected [uri current-chat-id]})))
(rf/defn on-end-reached
{:events [:camera-roll/on-end-reached]}
[_ end-cursor selected-album loading? has-next-page?]
(when (and (not loading?) has-next-page?)
(re-frame/dispatch [:chat.ui/camera-roll-loading-more true])
(re-frame/dispatch [:chat.ui/camera-roll-get-photos 20 end-cursor selected-album])))
(rf/defn camera-roll-get-photos
{:events [:chat.ui/camera-roll-get-photos]}
[_ num end-cursor selected-album]
{::camera-roll-get-photos [num end-cursor selected-album]})
(rf/defn camera-roll-get-albums
{:events [:chat.ui/camera-roll-get-albums]}
[_]
{:chat.ui/camera-roll-get-albums []})
(rf/defn camera-roll-loading-more
{:events [:chat.ui/camera-roll-loading-more]}
[{:keys [db]} is-loading]
{:db (assoc db :camera-roll/loading-more is-loading)})
(rf/defn on-camera-roll-get-photos
{:events [:on-camera-roll-get-photos]}
[{:keys [db] :as cofx} photos page-info end-cursor]
(let [photos_x (when end-cursor (:camera-roll/photos db))]
{:db (-> db
(assoc :camera-roll/photos (concat photos_x (map #(get-in % [:node :image]) photos)))
(assoc :camera-roll/end-cursor (:end_cursor page-info))
(assoc :camera-roll/has-next-page (:has_next_page page-info))
(assoc :camera-roll/loading-more false))}))
(rf/defn on-camera-roll-get-albums
{:events [:on-camera-roll-get-albums]}
[{:keys [db]} albums]
{:db (-> db
(assoc :camera-roll/albums albums)
(assoc :camera-roll/loading-albums false))})
(rf/defn clear-sending-images
{:events [:chat.ui/clear-sending-images]}
[{:keys [db]}]
{:db (update-in db [:chat/inputs (:current-chat-id db) :metadata] assoc :sending-image {})})
(rf/defn cancel-sending-image
{:events [:chat.ui/cancel-sending-image]}
[{:keys [db] :as cofx} chat-id]
(clear-sending-images cofx))
(rf/defn image-selected
{:events [:chat.ui/image-selected]}
[{:keys [db]} current-chat-id original {:keys [resized-uri width height]}]
{:db
(update-in db
[:chat/inputs current-chat-id :metadata :sending-image (:uri original)]
merge
original
{:resized-uri resized-uri
:width width
:height height})})
(rf/defn image-unselected
{:events [:chat.ui/image-unselected]}
[{:keys [db]} original]
(let [current-chat-id (:current-chat-id db)]
{:db (update-in db [:chat/inputs current-chat-id :metadata :sending-image] dissoc (:uri original))}))
(rf/defn chat-open-image-picker
{:events [:chat.ui/open-image-picker]}
[{:keys [db]} chat-id]
(let [current-chat-id (or chat-id (:current-chat-id db))
images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])]
(when (< (count images) config/max-images-batch)
{::chat-open-image-picker current-chat-id})))
(rf/defn chat-show-image-picker-camera
{:events [:chat.ui/show-image-picker-camera]}
[{:keys [db]} chat-id]
@@ -271,25 +75,3 @@
images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])]
(when (< (count images) config/max-images-batch)
{::chat-open-image-picker-camera current-chat-id})))
(rf/defn camera-roll-pick
{:events [:chat.ui/camera-roll-pick]}
[{:keys [db]} image chat-id]
(let [current-chat-id (or chat-id (:current-chat-id db))
images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])]
(if (get-in db [:chats current-chat-id :timeline?])
{:db (assoc-in db [:chat/inputs current-chat-id :metadata :sending-image] {})
::image-selected [image current-chat-id]}
(when (and (< (count images) config/max-images-batch)
(not (some #(= (:uri image) (:uri %)) images)))
{::image-selected [image current-chat-id]}))))
(rf/defn camera-roll-select-album
{:events [:chat.ui/camera-roll-select-album]}
[{:keys [db]} album]
{:db (assoc db :camera-roll/selected-album album)})
(rf/defn save-image-to-gallery
{:events [:chat.ui/save-image-to-gallery]}
[_ base64-uri]
{::save-image-to-gallery base64-uri})
+8 -6
View File
@@ -103,7 +103,7 @@
(rf/defn messages-loaded
"Loads more messages for current chat"
{:events [::messages-loaded]}
[{db :db} chat-id session-id {:keys [cursor messages]}]
[{db :db} chat-id session-id {:keys [cursor messages]} on-loaded]
(when-not (and (get-in db [:pagination-info chat-id :messages-initialized?])
(not= session-id
(get-in db [:pagination-info chat-id :messages-initialized?])))
@@ -141,6 +141,8 @@
[:pagination-info chat-id
:cursor-clock-value])
clock-value (when cursor (cursor->clock-value cursor))]
(when on-loaded
(on-loaded (count new-messages)))
{:db (-> db
(update-in [:pagination-info chat-id :cursor-clock-value]
#(if (and (seq cursor) (or (not %) (< clock-value %)))
@@ -161,7 +163,7 @@
(rf/defn load-more-messages
{:events [:chat.ui/load-more-messages]}
[{:keys [db]} chat-id first-request]
[{:keys [db]} chat-id first-request on-loaded]
(when-let [session-id (get-in db [:pagination-info chat-id :messages-initialized?])]
(when (and
(not (get-in db [:pagination-info chat-id :all-loaded?]))
@@ -175,13 +177,13 @@
chat-id
cursor
constants/default-number-of-messages
#(re-frame/dispatch [::messages-loaded chat-id session-id %])
#(re-frame/dispatch [::messages-loaded chat-id session-id % on-loaded])
#(re-frame/dispatch [::failed-loading-messages chat-id session-id %]))))))))
(rf/defn load-more-messages-for-current-chat
{:events [:chat.ui/load-more-messages-for-current-chat]}
[{:keys [db] :as cofx}]
(load-more-messages cofx (:current-chat-id db) false))
[{:keys [db] :as cofx} on-loaded]
(load-more-messages cofx (:current-chat-id db) false on-loaded))
(rf/defn load-messages
[{:keys [db now] :as cofx} chat-id]
@@ -191,4 +193,4 @@
:utils/dispatch-later [{:ms 50 :dispatch [:chat.ui/mark-all-read-pressed chat-id]}
(when-not (get-in cofx [:db :chats chat-id :public?])
{:ms 100 :dispatch [:pin-message/load-pin-messages chat-id]})]}
(load-more-messages chat-id true))))
(load-more-messages chat-id true nil))))
+41 -1
View File
@@ -4,7 +4,8 @@
[status-im.data-store.reactions :as data-store.reactions]
[status-im.transport.message.protocol :as message.protocol]
[utils.re-frame :as rf]
[taoensso.timbre :as log]))
[taoensso.timbre :as log]
[utils.transforms :as transforms]))
(defn update-reaction
[acc retracted chat-id message-id emoji-id emoji-reaction-id reaction]
@@ -94,3 +95,42 @@
acc))
[]
reactions))
(defn- <-rpc
[{compressed-key :compressedKey
emoji-id :emojiId
:keys [from]}]
{:compressed-key compressed-key
:emoji-id emoji-id
:from from})
(defn- format-response
[response]
(->> (transforms/js->clj response)
(map <-rpc)
(group-by :emoji-id)))
(rf/defn save-emoji-reaction-details
{:events [:chat/save-emoji-reaction-details]}
[{:keys [db]} message-reactions long-pressed-emoji]
{:db (assoc db
:chat/reactions-authors
{:reaction-authors-list message-reactions
:selected-reaction long-pressed-emoji})})
(rf/defn clear-emoji-reaction-details
{:events [:chat/clear-emoji-reaction-author-details]}
[{:keys [db]} message-reactions]
{:db (update db dissoc :chat/reactions-authors)})
(rf/defn emoji-reactions-by-message-id
{:events [:chat.ui/emoji-reactions-by-message-id]}
[{:keys [db]} {:keys [message-id long-pressed-emoji]}]
{:json-rpc/call [{:method "wakuext_emojiReactionsByChatIDMessageID"
:params [(:current-chat-id db) message-id]
:js-response true
:on-error #(log/error "failed to fetch emoji reaction by message-id: "
{:message-id message-id :error %})
:on-success #(rf/dispatch [:chat/save-emoji-reaction-details
(format-response %) long-pressed-emoji])}]})
+1 -1
View File
@@ -36,7 +36,7 @@
status-im.signals.core
status-im.stickers.core
status-im.transport.core
[status-im.ui.components.permissions :as permissions]
[react-native.permissions :as permissions]
[status-im.ui.components.react :as react]
status-im.ui.screens.privacy-and-security-settings.events
[status-im.utils.dimensions :as dimensions]
+29 -17
View File
@@ -165,6 +165,15 @@
(wallet/request-current-block-update))
(prices/update-prices)))
(rf/defn login-local-paired-user
{:events [:multiaccounts.login/local-paired-user]}
[{:keys [db]}]
(let [{:keys [key-uid name password]} (get-in db [:syncing :multiaccount])]
{::login [key-uid
(types/clj->json {:name name
:key-uid key-uid})
password]}))
(rf/defn login
{:events [:multiaccounts.login.ui/password-input-submitted]}
[{:keys [db]}]
@@ -384,22 +393,27 @@
(rf/defn redirect-to-root
"Decides which root should be initialised depending on user and app state"
[{:keys [db] :as cofx}]
(cond
(get db :onboarding-2/new-account?)
{:dispatch [:onboarding-2/finalize-setup]}
(let [pairing-completed? (= (get-in db [:syncing :pairing-status]) :completed)]
(cond
pairing-completed?
{:db (dissoc db :syncing)
:dispatch [:init-root :syncing-results]}
(get db :tos/accepted?)
(rf/merge
cofx
(multiaccounts/switch-theme nil :shell-stack)
(navigation/init-root :shell-stack))
(get db :onboarding-2/new-account?)
{:dispatch [:onboarding-2/finalize-setup]}
:else
{:dispatch [:init-root :tos]}))
(get db :tos/accepted?)
(rf/merge
cofx
(multiaccounts/switch-theme nil :shell-stack)
(navigation/init-root :shell-stack))
:else
{:dispatch [:init-root :tos]})))
(rf/defn get-settings-callback
{:events [::get-settings-callback]}
[{:keys [db] :as cofx} settings pairing-in-progress?]
[{:keys [db] :as cofx} settings]
(let [{:networks/keys [current-network networks]
:as settings}
(data-store.settings/rpc->settings settings)
@@ -431,8 +445,7 @@
(activity-center/notifications-fetch-pending-contact-requests)
(activity-center/update-seen-state)
(activity-center/notifications-fetch-unread-count)
(when-not pairing-in-progress?
(redirect-to-root)))))
(redirect-to-root))))
(re-frame/reg-fx
::open-last-chat
@@ -511,9 +524,8 @@
(rf/defn login-only-events
[{:keys [db] :as cofx} key-uid password save-password?]
(let [auth-method (:auth-method db)
new-auth-method (get-new-auth-method auth-method save-password?)
pairing-in-progress? (get-in db [:syncing :pairing-in-progress?])]
(let [auth-method (:auth-method db)
new-auth-method (get-new-auth-method auth-method save-password?)]
(log/debug "[login] login-only-events"
"auth-method" auth-method
"new-auth-method" new-auth-method)
@@ -521,7 +533,7 @@
{:db (assoc db :chats/loading? true)
:json-rpc/call
[{:method "settings_getSettings"
:on-success #(re-frame/dispatch [::get-settings-callback % pairing-in-progress?])}]}
:on-success #(re-frame/dispatch [::get-settings-callback %])}]}
(notifications/load-notification-preferences)
(when save-password?
(keychain/save-user-password key-uid password))
+3 -2
View File
@@ -89,8 +89,9 @@
{:events [:chat.ui/show-profile]}
[{:keys [db]} identity ens-name]
(let [my-public-key (get-in db [:multiaccount :public-key])]
(when (not= my-public-key identity)
(if (not= my-public-key identity)
{:db (-> db
(assoc :contacts/identity identity)
(assoc :contacts/ens-name ens-name))
:dispatch [:contacts/build-contact identity ens-name true]})))
:dispatch [:contacts/build-contact identity ens-name true]}
{:dispatch [:navigate-to :my-profile]})))
+29 -17
View File
@@ -10,8 +10,7 @@
[utils.re-frame :as rf]
[status-im2.contexts.chat.messages.link-preview.events :as link-preview]
[taoensso.timbre :as log]
[status-im2.constants :as constants]
[status-im.multiaccounts.model :as multiaccounts.model]))
[status-im2.constants :as constants]))
(rf/defn status-node-started
[{db :db :as cofx} {:keys [error]}]
@@ -56,36 +55,49 @@
:peers-count (count (:peers peer-stats)))}))
(rf/defn handle-local-pairing-signals
[{:keys [db] :as cofx} event]
[{:keys [db] :as cofx} {:keys [type action data] :as event}]
(log/info "local pairing signal received"
{:event event})
(let [connection-success? (and (= (:type event)
(let [{:keys [account password]} data
role (get-in db [:syncing :role])
receiver? (= role constants/local-pairing-role-receiver)
sender? (= role constants/local-pairing-role-sender)
connection-success? (and (= type
constants/local-pairing-event-connection-success)
(= (:action event)
(= action
constants/local-pairing-action-connect))
error-on-pairing? (contains? constants/local-pairing-event-errors (:type event))
completed-pairing? (and (= (:type event)
error-on-pairing? (contains? constants/local-pairing-event-errors type)
completed-pairing? (and (= type
constants/local-pairing-event-transfer-success)
(= (:action event)
(= action
constants/local-pairing-action-pairing-installation))
logged-in? (multiaccounts.model/logged-in? db)
;; since `connection-success` event is received on both sender and receiver devices
;; we check the `logged-in?` status to identify the receiver and take the user to next screen
navigate-to-syncing-devices? (and connection-success? (not logged-in?))
received-account? (and (= type
constants/local-pairing-event-received-account)
(= action
constants/local-pairing-action-pairing-account)
(and (some? account) (some? password)))
multiaccount-data (when received-account?
(merge account {:password password}))
navigate-to-syncing-devices? (and connection-success? receiver?)
user-in-syncing-devices-screen? (= (:view-id db) :syncing-progress)]
(merge {:db (cond-> db
connection-success?
(assoc-in [:syncing :pairing-in-progress?] :connected)
(assoc-in [:syncing :pairing-status] :connected)
received-account?
(assoc-in [:syncing :multiaccount] multiaccount-data)
error-on-pairing?
(assoc-in [:syncing :pairing-in-progress?] :error)
(assoc-in [:syncing :pairing-status] :error)
completed-pairing?
(assoc-in [:syncing :pairing-in-progress?] :completed))}
(assoc-in [:syncing :pairing-status] :completed))}
(when (and navigate-to-syncing-devices? (not user-in-syncing-devices-screen?))
{:dispatch [:navigate-to :syncing-progress]})
(when completed-pairing?
{:dispatch [:syncing/pairing-completed]}))))
(when (and completed-pairing? sender?)
{:dispatch [:syncing/clear-states]})
(when (and completed-pairing? receiver?)
{:dispatch [:multiaccounts.login/local-paired-user]}))))
(rf/defn process
{:events [:signals/signal-received]}
@@ -1,34 +0,0 @@
(ns status-im.ui.components.permissions
(:require ["react-native-permissions" :refer (requestMultiple PERMISSIONS RESULTS)]
[status-im.utils.platform :as platform]))
(def permissions-map
{:read-external-storage (cond
platform/android? (.-READ_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
:write-external-storage (cond
platform/low-device? (.-WRITE_EXTERNAL_STORAGE (.-ANDROID PERMISSIONS)))
:camera (cond
platform/android? (.-CAMERA (.-ANDROID PERMISSIONS))
platform/ios? (.-CAMERA (.-IOS PERMISSIONS)))
:record-audio (cond
platform/android? (.-RECORD_AUDIO (.-ANDROID PERMISSIONS))
platform/ios? (.-MICROPHONE (.-IOS PERMISSIONS)))})
(defn all-granted?
[permissions]
(let [permission-vals (distinct (vals permissions))]
(and (= (count permission-vals) 1)
(not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} (first permission-vals))))))
(defn request-permissions
[{:keys [permissions on-allowed on-denied]
:or {on-allowed #()
on-denied #()}}]
(let [permissions (remove nil? (mapv #(get permissions-map %) permissions))]
(if (empty? permissions)
(on-allowed)
(-> (requestMultiple (clj->js permissions))
(.then #(if (all-granted? (js->clj %))
(on-allowed)
(on-denied)))
(.catch on-denied)))))
+1 -1
View File
@@ -11,7 +11,7 @@
[status-im.ui.components.chat-icon.screen :as chat-icon]
[status-im.ui.components.connectivity.view :as connectivity]
[status-im.ui.components.icons.icons :as icons]
[status-im.ui.components.permissions :as components.permissions]
[react-native.permissions :as components.permissions]
[status-im.ui.components.react :as react]
[status-im.ui.components.tooltip.views :as tooltip]
[status-im.ui.components.webview :as components.webview]
@@ -13,7 +13,7 @@
:margin-vertical 8})
(defn sheet
[{:keys [top bottom]} window-height override-theme shell?]
[{:keys [top bottom]} window-height override-theme padding-bottom-override shell?]
{:position :absolute
:max-height (- window-height top 20)
:z-index 1
@@ -22,9 +22,9 @@
:right 0
:border-top-left-radius 20
:border-top-right-radius 20
:overflow :hidden
:overflow (when shell? :hidden)
:flex 1
:padding-bottom (max 20 bottom)
:padding-bottom (or padding-bottom-override (max 20 bottom))
:background-color (if shell?
:transparent
(colors/theme-colors colors/white colors/neutral-90 override-theme))})
+16 -9
View File
@@ -13,7 +13,9 @@
(def timing-options #js {:duration duration})
(defn hide
[translate-y bg-opacity window-height]
[translate-y bg-opacity window-height on-close]
(when (fn? on-close)
(on-close))
;; it will be better to use animation callback, but it doesn't work
;; so we have to use timeout, also we add 50ms for safety
(js/setTimeout #(rf/dispatch [:bottom-sheet-hidden]) (+ duration 50))
@@ -29,7 +31,7 @@
(def gesture-values (atom {}))
(defn get-sheet-gesture
[translate-y bg-opacity window-height]
[translate-y bg-opacity window-height on-close]
(-> (gesture/gesture-pan)
(gesture/on-start
(fn [_]
@@ -47,17 +49,22 @@
(fn [_]
(if (< (:dy @gesture-values) 0)
(show translate-y bg-opacity)
(hide translate-y bg-opacity window-height))))))
(hide translate-y bg-opacity window-height on-close))))))
(defn view
[{:keys [hide? insets]} {:keys [content override-theme selected-item shell?]}]
[{:keys [hide? insets]}
{:keys [content override-theme selected-item padding-bottom-override on-close shell?]}]
(let [{window-height :height} (rn/get-window)
bg-opacity (reanimated/use-shared-value 0)
translate-y (reanimated/use-shared-value window-height)
sheet-gesture (get-sheet-gesture translate-y bg-opacity window-height)]
(rn/use-effect #(if hide? (hide translate-y bg-opacity window-height) (show translate-y bg-opacity))
[hide?])
(hooks/use-back-handler #(do (rf/dispatch [:hide-bottom-sheet]) true))
sheet-gesture (get-sheet-gesture translate-y bg-opacity window-height on-close)]
(rn/use-effect
#(if hide? (hide translate-y bg-opacity window-height on-close) (show translate-y bg-opacity))
[hide?])
(hooks/use-back-handler #(do (when (fn? on-close)
(on-close))
(rf/dispatch [:hide-bottom-sheet])
true))
[rn/view {:flex 1}
;; backdrop
[rn/touchable-without-feedback {:on-press #(rf/dispatch [:hide-bottom-sheet])}
@@ -70,7 +77,7 @@
[reanimated/view
{:style (reanimated/apply-animations-to-style
{:transform [{:translateY translate-y}]}
(styles/sheet insets window-height override-theme shell?))}
(styles/sheet insets window-height override-theme padding-bottom-override shell?))}
(when shell?
[blur/ios-view
+3 -1
View File
@@ -23,7 +23,9 @@
:discover (js/require "../resources/images/ui2/discover.png")
:invite-friends (js/require "../resources/images/ui2/invite-friends.png")
:no-contacts-light (js/require "../resources/images/ui2/no-contacts-light.png")
:no-contacts-dark (js/require "../resources/images/ui2/no-contacts-dark.png")})
:no-contacts-dark (js/require "../resources/images/ui2/no-contacts-dark.png")
:no-messages-light (js/require "../resources/images/ui2/no-messages-light.png")
:no-messages-dark (js/require "../resources/images/ui2/no-messages-dark.png")})
(def mock-images
{:coinbase (js/require "../resources/images/mock2/coinbase.png")
+8 -7
View File
@@ -71,12 +71,12 @@
(def request-to-join-pending-state 1)
(def reactions
{emoji-reaction-love :i/love
emoji-reaction-thumbs-up :i/thumbs-up
emoji-reaction-thumbs-down :i/thumbs-down
emoji-reaction-laugh :i/laugh
emoji-reaction-sad :i/sad
emoji-reaction-angry :i/angry})
{emoji-reaction-love :reaction/love
emoji-reaction-thumbs-up :reaction/thumbs-up
emoji-reaction-thumbs-down :reaction/thumbs-down
emoji-reaction-laugh :reaction/laugh
emoji-reaction-sad :reaction/sad
emoji-reaction-angry :reaction/angry})
(def ^:const invitation-state-unknown 0)
(def ^:const invitation-state-requested 1)
@@ -277,9 +277,10 @@
(def ^:const local-pairing-event-transfer-error "transfer-error")
;; receiver events
(def ^:const local-pairing-event-received-amount "received-account")
(def ^:const local-pairing-event-received-account "received-account")
(def ^:const local-pairing-event-process-success "process-success")
(def ^:const local-pairing-event-process-error "process-error")
(def ^:const local-pairing-event-received-installation "received-installation")
(def ^:const local-pairing-event-errors
#{local-pairing-event-connection-error
@@ -38,22 +38,18 @@
[]
(let [unread-count (rf/sub [:activity-center/unread-count])]
[quo/action-drawer
[[{:icon :i/check
[[{:icon :i/mark-as-read
:override-theme :dark
:label (i18n/label :t/mark-all-notifications-as-read)
:disabled? (zero? unread-count)
:on-press (fn []
(if (pos? unread-count)
(rf/dispatch [:activity-center.notifications/mark-all-as-read-locally
(fn []
{:icon :up-to-date
:icon-color colors/success-50
:text (i18n/label :t/notifications-marked-as-read
{:count unread-count})
:override-theme :dark})])
;; Need design improvements if there is NO unread
;; notifications to mark as read
;; https://github.com/status-im/status-mobile/issues/14983
(js/alert "No unread notifications to mark as read"))
(rf/dispatch [:activity-center.notifications/mark-all-as-read-locally
(fn []
{:icon :up-to-date
:icon-color colors/success-50
:text (i18n/label :t/notifications-marked-as-read
{:count unread-count})
:override-theme :dark})])
(rf/dispatch [:hide-bottom-sheet]))}]]]))
(defn empty-tab
@@ -88,8 +88,7 @@
(rf/dispatch [:chat.ui/set-recording false]))
:on-reviewing-audio (fn [file]
(rf/dispatch [:chat.ui/set-recording false])
(rf/dispatch [:chat.ui/set-input-audio file])
(rf/dispatch [:dismiss-keyboard]))
(rf/dispatch [:chat.ui/set-input-audio file]))
:on-send (fn [{:keys [file-path duration]}]
(rf/dispatch [:chat.ui/set-recording false])
(reset! recording? false)
@@ -81,8 +81,7 @@
[edit/view state]
[reanimated/touchable-opacity
{:active-opacity 1
:on-press (when (and @(:input-ref props) (not (:audio subs)))
#(.focus ^js @(:input-ref props)))
:on-press (when @(:input-ref props) #(.focus ^js @(:input-ref props)))
:style (style/input-container (:height animations) max-height)
:accessibility-label :message-input-container}
[rn/selectable-text-input
@@ -1,7 +1,6 @@
(ns status-im2.contexts.chat.events-test
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im2.contexts.chat.events :as chat]
[status-im.chat.models.images :as images]
[status-im.utils.clocks :as utils.clocks]))
(deftest clear-history-test
@@ -90,8 +89,3 @@
(testing "Pagination info should be reset on navigation"
(let [res (chat/navigate-to-chat {:db db} chat-id)]
(is (nil? (get-in res [:db :pagination-info chat-id :all-loaded?])))))))
(deftest camera-roll-loading-more-test
(let [cofx {:db {:camera-roll/loading-more false}}]
(is (= {:db {:camera-roll/loading-more true}}
(images/camera-roll-loading-more cofx true)))))
@@ -15,24 +15,37 @@
[status-im2.contexts.chat.lightbox.constants :as constants]
[utils.worklets.lightbox :as worklet]))
(defn clear-timers
[timers]
(js/clearTimeout (:mount-animation @timers))
(js/clearTimeout (:mount-index-lock @timers))
(js/clearTimeout (:hide-0 @timers))
(js/clearTimeout (:hide-1 @timers))
(js/clearTimeout (:show-0 @timers))
(js/clearTimeout (:show-1 @timers))
(js/clearTimeout (:show-2 @timers)))
(defn effect
[{:keys [flat-list-ref scroll-index-lock?]} {:keys [opacity layout border]} index]
(rn/use-effect (fn []
(reagent/next-tick (fn []
(when @flat-list-ref
(.scrollToIndex ^js @flat-list-ref
#js {:animated false :index index}))))
(js/setTimeout (fn []
(anim/animate opacity 1)
(anim/animate layout 0)
(anim/animate border 12))
(if platform/ios? 250 100))
(js/setTimeout #(reset! scroll-index-lock? false) 300)
(fn []
(rf/dispatch [:chat.ui/zoom-out-signal nil])
(when platform/android?
(rf/dispatch [:chat.ui/lightbox-scale 1]))))))
[{:keys [flat-list-ref scroll-index-lock? timers]} {:keys [opacity layout border]} index]
(rn/use-effect
(fn []
(reagent/next-tick (fn []
(when @flat-list-ref
(.scrollToIndex ^js @flat-list-ref
#js {:animated false :index index}))))
(swap! timers assoc
:mount-animation
(js/setTimeout (fn []
(anim/animate opacity 1)
(anim/animate layout 0)
(anim/animate border 12))
(if platform/ios? 250 100)))
(swap! timers assoc :mount-index-lock (js/setTimeout #(reset! scroll-index-lock? false) 300))
(fn []
(rf/dispatch [:chat.ui/zoom-out-signal nil])
(when platform/android?
(rf/dispatch [:chat.ui/lightbox-scale 1]))
(clear-timers timers)))))
(defn handle-orientation
[result {:keys [flat-list-ref]} {:keys [scroll-index]} animations]
@@ -102,7 +115,7 @@
(anim/animate opacity 0)
(rf/dispatch [:navigate-back]))
(do
#(reset! set-full-height? true)
(reset! set-full-height? true)
(anim/animate (if x? pan-x pan-y) 0)
(anim/animate opacity 1)
(anim/animate layout 0)))))))
@@ -111,7 +124,8 @@
[]
{:flat-list-ref (atom nil)
:small-list-ref (atom nil)
:scroll-index-lock? (atom true)})
:scroll-index-lock? (atom true)
:timers (atom {})})
(defn init-state
[messages index]
@@ -88,7 +88,8 @@
:screen-width screen-width
:window-height window-height
:window-width window-width
:props props}
:props props
:curr-orientation curr-orientation}
:horizontal horizontal?
:inverted inverted?
:paging-enabled true
@@ -113,7 +114,8 @@
(on-viewable-items-changed e props state))]
(anim/animate (:background-color animations) "rgba(0,0,0,1)")
(reset! (:data state) messages)
(utils/orientation-change props state animations)
(when platform/ios? ; issue: https://github.com/wix/react-native-navigation/issues/7726
(utils/orientation-change props state animations))
(utils/effect props animations index)
[:f> lightbox-content props state animations derived messages index callback]))))
@@ -107,23 +107,34 @@
(defn toggle-opacity
[index {:keys [opacity-value border-value transparent? props]} portrait?]
(let [{:keys [small-list-ref]} props
opacity (reanimated/get-shared-value opacity-value)]
(let [{:keys [small-list-ref timers]} props
opacity (reanimated/get-shared-value opacity-value)]
(if (= opacity 1)
(do
(when platform/ios?
;; status-bar issue: https://github.com/status-im/status-mobile/issues/15343
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible false}}) 75))
(js/clearTimeout (:show-0 @timers))
(js/clearTimeout (:show-1 @timers))
(js/clearTimeout (:show-2 @timers))
(swap! timers assoc
:hide-0
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible false}})
(if platform/ios? 75 0)))
(anim/animate opacity-value 0)
(js/setTimeout #(reset! transparent? (not @transparent?)) 400))
(swap! timers assoc :hide-1 (js/setTimeout #(reset! transparent? (not @transparent?)) 400)))
(do
(js/clearTimeout (:hide-0 @timers))
(js/clearTimeout (:hide-1 @timers))
(reset! transparent? (not @transparent?))
(js/setTimeout #(anim/animate opacity-value 1) 50)
(js/setTimeout #(when @small-list-ref
(.scrollToIndex ^js @small-list-ref #js {:animated false :index index}))
100)
(when (and platform/ios? portrait?)
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible true}}) 150))))
(swap! timers assoc :show-0 (js/setTimeout #(anim/animate opacity-value 1) 50))
(swap! timers assoc
:show-1
(js/setTimeout #(when @small-list-ref
(.scrollToIndex ^js @small-list-ref #js {:animated false :index index}))
100))
(when portrait?
(swap! timers assoc
:show-2
(js/setTimeout #(navigation/merge-options "lightbox" {:statusBar {:visible true}})
(if platform/ios? 150 50))))))
(anim/animate border-value (if (= opacity 1) 0 12))))
;;; Dimensions
@@ -228,36 +228,35 @@
:style (style/image dimensions animations (:border-value render-data))}]]]))
(defn zoomable-image
[{:keys [image-width image-height content message-id]} index render-data]
(let [state (utils/init-state)
shared-element-id (rf/sub [:shared-element-id])
exit-lightbox-signal (rf/sub [:lightbox/exit-signal])
zoom-out-signal (rf/sub [:lightbox/zoom-out-signal])
curr-orientation (or (rf/sub [:lightbox/orientation])
orientation/portrait)
{:keys [set-full-height?]} render-data
focused? (= shared-element-id message-id)
dimensions (utils/get-dimensions
(or image-width c/default-dimension)
(or image-height c/default-duration)
curr-orientation
render-data)
animations (utils/init-animations)
rescale (fn [value exit?]
(utils/rescale-image value
exit?
dimensions
animations
state))]
(rn/use-effect (fn []
(js/setTimeout #(reset! set-full-height? true) 500)))
(when platform/ios?
(utils/handle-orientation-change curr-orientation focused? dimensions animations state)
(utils/handle-exit-lightbox-signal exit-lightbox-signal
index
(anim/get-val (:scale animations))
rescale
set-full-height?))
(utils/handle-zoom-out-signal zoom-out-signal index (anim/get-val (:scale animations)) rescale)
[:f> f-zoomable-image dimensions animations state rescale curr-orientation content focused?
index render-data]))
[]
(let [state (utils/init-state)]
(fn [{:keys [image-width image-height content message-id]} index render-data]
(let [shared-element-id (rf/sub [:shared-element-id])
exit-lightbox-signal (rf/sub [:lightbox/exit-signal])
zoom-out-signal (rf/sub [:lightbox/zoom-out-signal])
{:keys [set-full-height? curr-orientation]} render-data
focused? (= shared-element-id message-id)
dimensions (utils/get-dimensions
(or image-width c/default-dimension)
(or image-height c/default-duration)
curr-orientation
render-data)
animations (utils/init-animations)
rescale (fn [value exit?]
(utils/rescale-image value
exit?
dimensions
animations
state))]
(rn/use-effect (fn []
(js/setTimeout (fn [] (reset! set-full-height? true)) 500)))
(when platform/ios?
(utils/handle-orientation-change curr-orientation focused? dimensions animations state)
(utils/handle-exit-lightbox-signal exit-lightbox-signal
index
(anim/get-val (:scale animations))
rescale
set-full-height?))
(utils/handle-zoom-out-signal zoom-out-signal index (anim/get-val (:scale animations)) rescale)
[:f> f-zoomable-image dimensions animations state rescale curr-orientation content focused?
index render-data]))))
@@ -5,31 +5,69 @@
[utils.re-frame :as rf]
[status-im2.contexts.chat.messages.drawers.view :as drawers]))
(defn- on-press
[own message-id emoji-id emoji-reaction-id]
(if own
(rf/dispatch [:models.reactions/send-emoji-reaction-retraction
{:message-id message-id
:emoji-id emoji-id
:emoji-reaction-id emoji-reaction-id}])
(rf/dispatch [:models.reactions/send-emoji-reaction
{:message-id message-id
:emoji-id emoji-id}])))
(defn- on-long-press
[message-id emoji-id]
(rf/dispatch [:chat.ui/emoji-reactions-by-message-id
{:message-id message-id
:long-pressed-emoji emoji-id}]))
(defn show-authors-sheet
[user-message-content reactions]
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch
[:show-bottom-sheet
{:on-close (fn []
(rf/dispatch
[:chat/clear-emoji-reaction-author-details]))
:content (fn []
[drawers/reaction-authors
(map :emoji-id reactions)])
:selected-item (fn []
user-message-content)
:padding-bottom-override 0}]))
(defn message-reactions-row
[chat-id message-id]
[{:keys [message-id chat-id]} user-message-content]
(let [reactions (rf/sub [:chats/message-reactions message-id chat-id])]
(when (seq reactions)
[rn/view {:margin-left 52 :margin-bottom 12 :flex-direction :row}
(for [{:keys [own emoji-id quantity emoji-reaction-id] :as emoji-reaction} reactions]
^{:key (str emoji-reaction)}
[rn/view {:style {:margin-right 6}}
[quo/reaction
{:emoji (get constants/reactions emoji-id)
:neutral? own
:clicks quantity
:on-press (if own
#(rf/dispatch [:models.reactions/send-emoji-reaction-retraction
{:message-id message-id
:emoji-id emoji-id
:emoji-reaction-id emoji-reaction-id}])
#(rf/dispatch [:models.reactions/send-emoji-reaction
{:message-id message-id
:emoji-id emoji-id}]))
:accessibility-label (str "emoji-reaction-" emoji-id)}]])
[quo/add-reaction
{:on-press (fn []
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch
[:show-bottom-sheet
{:content (fn [] [drawers/reactions
{:chat-id chat-id :message-id message-id}])}]))}]])))
[:<>
(when (seq reactions)
[rn/view
{:style {:margin-left 52
:margin-bottom 12
:flex-direction :row}}
(for [{:keys [own emoji-id quantity emoji-reaction-id]
:as emoji-reaction} reactions]
^{:key emoji-reaction}
[rn/view {:style {:margin-right 6}}
[quo/reaction
{:emoji (get constants/reactions emoji-id)
:neutral? own
:clicks quantity
:on-press #(on-press own message-id emoji-id emoji-reaction-id)
:on-long-press (fn []
(on-long-press message-id
emoji-id)
(show-authors-sheet user-message-content
reactions))
:accessibility-label (str "emoji-reaction-" emoji-id)}]])
[quo/add-reaction
{:on-press (fn []
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch
[:show-bottom-sheet
{:content (fn [] [drawers/reactions
{:chat-id chat-id
:message-id message-id}])
:selected-item (fn []
user-message-content)}]))}]])]))
@@ -23,16 +23,18 @@
[status-im2.common.not-implemented :as not-implemented]
[utils.datetime :as datetime]
[reagent.core :as reagent]
[utils.address :as address]))
[utils.address :as address]
[react-native.gesture :as gesture]))
(def delivery-state-showing-time-ms 3000)
(defn avatar-container
[{:keys [content last-in-group? pinned-by quoted-message from]}]
[{:keys [content last-in-group? pinned-by quoted-message from]} message-reaction-view?]
(if (or (and (seq (:response-to content))
quoted-message)
last-in-group?
pinned-by)
pinned-by
message-reaction-view?)
[avatar/avatar from :small]
[rn/view {:padding-top 2 :width 32}]))
@@ -43,8 +45,9 @@
pinned-by
quoted-message
from
timestamp]}]
(when (or (and (seq response-to) quoted-message) last-in-group? pinned-by)
timestamp]}
message-reaction-view?]
(when (or (and (seq response-to) quoted-message) last-in-group? pinned-by message-reaction-view?)
(let [[primary-name secondary-name] (rf/sub [:contacts/contact-two-names-by-identity from])
{:keys [ens-verified added?]} (rf/sub [:contacts/contact-by-address from])]
[quo/author
@@ -82,7 +85,8 @@
(let [show-delivery-state? (reagent/atom false)]
(fn [{:keys [content-type quoted-message content outgoing outgoing-status] :as message-data}
context
keyboard-shown]
keyboard-shown?
message-reaction-view?]
(let [first-image (first (:album message-data))
outgoing-status (if (= content-type constants/content-type-album)
(:outgoing-status first-image)
@@ -91,7 +95,8 @@
(:outgoing first-image)
outgoing)
context (assoc context :on-long-press #(on-long-press message-data context))
response-to (:response-to content)]
response-to (:response-to content)
height (rf/sub [:dimensions/window-height])]
[rn/touchable-highlight
{:accessibility-label (if (and outgoing (= outgoing-status :sending))
:message-sending
@@ -100,7 +105,7 @@
:style {:border-radius 16
:opacity (if (and outgoing (= outgoing-status :sending)) 0.5 1)}
:on-press (fn []
(if (and platform/ios? @keyboard-shown)
(if (and platform/ios? keyboard-shown?)
(rn/dismiss-keyboard!)
(when (and outgoing
(not= outgoing-status :sending)
@@ -115,37 +120,41 @@
[rn/view
{:style {:padding-horizontal 12
:flex-direction :row}}
[avatar-container message-data]
[rn/view
{:style {:margin-left 8
:flex 1}}
[author message-data]
(case content-type
[avatar-container message-data message-reaction-view?]
(into
(if message-reaction-view?
[gesture/scroll-view]
[rn/view])
[{:style {:margin-left 8
:flex 1
:max-height (when message-reaction-view? (* 0.4 height))}}
[author message-data message-reaction-view?]
(case content-type
constants/content-type-text [content.text/text-content message-data]
constants/content-type-text [content.text/text-content message-data context]
constants/content-type-emoji
[not-implemented/not-implemented [old-message/emoji message-data]]
constants/content-type-emoji
[not-implemented/not-implemented [old-message/emoji message-data]]
constants/content-type-sticker
[not-implemented/not-implemented [old-message/sticker message-data]]
constants/content-type-sticker
[not-implemented/not-implemented [old-message/sticker message-data]]
constants/content-type-audio
[audio/audio-message message-data context]
constants/content-type-audio
[audio/audio-message message-data context]
constants/content-type-image
[image/image-message 0 message-data on-long-press]
constants/content-type-image
[image/image-message 0 message-data context on-long-press]
constants/content-type-album
[album/album-message message-data context on-long-press]
constants/content-type-album
[album/album-message message-data context on-long-press]
[not-implemented/not-implemented [content.unknown/unknown-content message-data]])
(when @show-delivery-state?
[status/status outgoing-status])]]]]))))
[not-implemented/not-implemented [content.unknown/unknown-content message-data]])
(when @show-delivery-state?
[status/status outgoing-status])])]]]))))
(defn message-with-reactions
[{:keys [pinned-by mentioned in-pinned-view? content-type last-in-group? message-id] :as message-data}
{:keys [chat-id] :as context}
[{:keys [pinned-by mentioned in-pinned-view? content-type last-in-group?] :as message-data}
context
keyboard-shown]
[rn/view
{:style (style/message-container in-pinned-view? pinned-by mentioned last-in-group?)
@@ -153,9 +162,10 @@
(when pinned-by
[pin/pinned-by-view pinned-by])
(if (#{constants/content-type-system-text constants/content-type-community
constants/content-type-system-pinned-message
constants/content-type-contact-request}
constants/content-type-contact-request
constants/content-type-system-pinned-message}
content-type)
[system-message-content message-data]
[user-message-content message-data context keyboard-shown])
[reactions/message-reactions-row chat-id message-id]])
[user-message-content message-data context keyboard-shown false])
[reactions/message-reactions-row message-data
[user-message-content message-data context keyboard-shown true]]])
@@ -0,0 +1,24 @@
(ns status-im2.contexts.chat.messages.drawers.style
(:require [quo2.foundations.colors :as colors]))
(def tab
{:flex-direction :row
:align-items :center
:justify-content :center})
(def tab-icon {:margin-right 4})
(defn tab-count
[active?]
{:color (if (or active? (colors/dark?)) colors/white colors/neutral-100)})
(def tabs-container
{:flex 1
:align-self :stretch
:padding-left 20
:padding-right 8
:margin-bottom 12})
(def authors-list
{:height 320
:flex 1})
@@ -3,10 +3,74 @@
[react-native.core :as rn]
[status-im.ui.components.react :as react]
[status-im2.contexts.chat.composer.reply.view :as reply]
[status-im2.common.not-implemented :as not-implemented]
[status-im2.constants :as constants]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
[utils.re-frame :as rf]
[reagent.core :as reagent]
[status-im2.common.contact-list-item.view :as contact-list-item]
[status-im2.contexts.chat.messages.drawers.style :as style]
[react-native.gesture :as gesture]))
(defn contact-list-item-fn
[{:keys [from compressed-key]}]
(let [[primary-name secondary-name] (rf/sub [:contacts/contact-two-names-by-identity
from])
{:keys [ens-verified added?]} (rf/sub [:contacts/contact-by-address from])]
^{:key compressed-key}
[contact-list-item/contact-list-item
{:on-press #(rf/dispatch [:chat.ui/show-profile from])}
{:primary-name primary-name
:secondary-name secondary-name
:public-key from
:compressed-key compressed-key
:ens-verified ens-verified
:added? added?}]))
(defn get-tabs-data
[reaction-authors selected-tab reactions-order]
(map (fn [reaction-type-int]
(let [author-details (get reaction-authors reaction-type-int)]
{:id reaction-type-int
:accessibility-label (keyword (str "authors-for-reaction-" reaction-type-int))
:label [rn/view {:style style/tab}
[quo/icon
(get constants/reactions reaction-type-int)
{:no-color true
:container-style style/tab-icon}]
[quo/text
{:weight :medium
:size :paragraph-1
:style (style/tab-count (= selected-tab
reaction-type-int))}
(count author-details)]]}))
reactions-order))
(defn reaction-authors-comp
[selected-tab reaction-authors reactions-order]
[:<>
[rn/view style/tabs-container
[quo/tabs
{:size 32
:scrollable? true
:in-scroll-view? true
:on-change #(reset! selected-tab %)
:default-active @selected-tab
:data (get-tabs-data reaction-authors @selected-tab reactions-order)}]]
[gesture/flat-list
{:data (for [contact (get reaction-authors @selected-tab)]
contact)
:render-fn contact-list-item-fn
:key-fn :from
:style style/authors-list}]])
(defn reaction-authors
[reactions-order]
(let [{:keys [reaction-authors-list
selected-reaction]} (rf/sub [:chat/reactions-authors])
selected-tab (reagent/atom (or selected-reaction
(first (keys reaction-authors-list))))]
(fn []
[reaction-authors-comp selected-tab reaction-authors-list reactions-order])))
(defn pin-message
[{:keys [chat-id pinned pinned-by] :as message-data}]
@@ -110,29 +174,23 @@
:padding-horizontal 30
:padding-top 5
:padding-bottom 15}}
(doall
(for [[id icon] constants/reactions]
(let [emoji-reaction-id (get own-reactions id)]
^{:key id}
[not-implemented/not-implemented
[quo/button
(merge
{:size 40
:type (if emoji-reaction-id :grey :ghost)
:icon true
:icon-no-color true
:accessibility-label (str "emoji-picker-" id)
:on-press (fn []
(if emoji-reaction-id
(rf/dispatch [:models.reactions/send-emoji-reaction-retraction
{:message-id message-id
:emoji-id id
:emoji-reaction-id emoji-reaction-id}])
(rf/dispatch [:models.reactions/send-emoji-reaction
{:message-id message-id
:emoji-id id}]))
(rf/dispatch [:hide-bottom-sheet]))})
icon]])))]))
(for [[id reaction-name] constants/reactions
:let [emoji-reaction-id (get own-reactions id)]]
^{:key id}
[quo/reactions reaction-name
{:start-pressed? (boolean emoji-reaction-id)
:accessibility-label (str "reaction-" (name reaction-name))
:on-press
(fn []
(if emoji-reaction-id
(rf/dispatch [:models.reactions/send-emoji-reaction-retraction
{:message-id message-id
:emoji-id id
:emoji-reaction-id emoji-reaction-id}])
(rf/dispatch [:models.reactions/send-emoji-reaction
{:message-id message-id
:emoji-id id}]))
(rf/dispatch [:hide-bottom-sheet]))}])]))
(defn reactions-and-actions
[message-data
@@ -0,0 +1,62 @@
(ns status-im2.contexts.chat.messages.list.style
(:require [quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]))
(defonce ^:const cover-height 168)
(defonce ^:const overscroll-cover-height 2000)
(defonce ^:const header-avatar-top-offset -36)
(defonce ^:const messages-list-bottom-offset 16)
(defn keyboard-avoiding-container
[{:keys [top]}]
{:position :relative
:flex 1
:top (- top)
:margin-bottom (- top)})
(def list-container
{:padding-vertical 16})
(defn header-container
[show?]
{:display (if show? :flex :none)
:background-color (colors/theme-colors colors/white colors/neutral-95)
:top (- overscroll-cover-height)
:margin-bottom (- overscroll-cover-height)})
(defn header-cover
[cover-bg-color]
{:flex 1
:height (+ overscroll-cover-height cover-height)
:background-color cover-bg-color})
(defn header-bottom-part
[animation]
(reanimated/apply-animations-to-style
{:border-top-right-radius animation
:border-top-left-radius animation}
{:top -16
:margin-bottom -16
:padding-bottom 24
:background-color (colors/theme-colors colors/white colors/neutral-100)}))
(def header-avatar
{:top header-avatar-top-offset
:margin-horizontal 20
:margin-bottom header-avatar-top-offset})
(defn header-image
[scale-animation top-margin-animation side-margin-animation]
(reanimated/apply-animations-to-style
{:transform [{:scale scale-animation}]
:margin-top top-margin-animation
:margin-left side-margin-animation
:margin-bottom side-margin-animation}
{:align-items :flex-start}))
(def name-container
{:flex-direction :row
:align-items :center})
(def bio
{:margin-top 8})
@@ -3,8 +3,12 @@
[quo2.core :as quo]
[react-native.background-timer :as background-timer]
[react-native.core :as rn]
[react-native.hooks :as hooks]
[react-native.platform :as platform]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]
[status-im.ui.screens.chat.group :as chat.group]
[status-im.ui.screens.chat.message.gap :as message.gap]
[status-im2.common.not-implemented :as not-implemented]
@@ -12,21 +16,35 @@
[status-im2.contexts.chat.messages.content.deleted.view :as content.deleted]
[status-im2.contexts.chat.messages.content.view :as message]
[status-im2.contexts.chat.messages.list.state :as state]
[utils.re-frame :as rf]
[status-im2.contexts.chat.composer.constants :as composer.constants]))
[status-im2.contexts.chat.messages.list.style :as style]
[status-im2.contexts.chat.messages.navigation.style :as navigation.style]
[status-im2.contexts.chat.composer.constants :as composer.constants]
[utils.re-frame :as rf]))
(defonce ^:const threshold-percentage-to-show-floating-scroll-down-button 75)
(defonce ^:const loading-indicator-extra-spacing 250)
(defonce ^:const loading-indicator-page-loading-height 100)
(defonce ^:const scroll-animation-input-range [50 125])
(defonce ^:const spacing-between-composer-and-content 64)
(defonce messages-list-ref (atom nil))
(defonce messages-view-height (reagent/atom 0))
(defonce messages-view-header-height (reagent/atom 0))
(defonce show-floating-scroll-down-button (reagent/atom false))
(defn list-key-fn [{:keys [message-id value]}] (or message-id value))
(defn list-ref [ref] (reset! messages-list-ref ref))
(defn scroll-to-offset
[position]
(some-> ^js @messages-list-ref
(.scrollToOffset #js
{:offset position
:animated true})))
(defn scroll-to-bottom
[]
(some-> ^js @messages-list-ref
(.scrollToOffset #js {:y 0 :animated true})))
(defonce ^:const threshold-percentage-to-show-floating-scroll-down-button 75)
(defonce show-floating-scroll-down-button (reagent/atom false))
(scroll-to-offset (- 0 style/messages-list-bottom-offset)))
(defn on-scroll
[evt]
@@ -40,11 +58,11 @@
(reset! show-floating-scroll-down-button reached-threshold?))))
(defn on-viewable-items-changed
[evt]
[e]
(when @messages-list-ref
(reset! state/first-not-visible-item
(when-let [last-visible-element (aget (oops/oget evt "viewableItems")
(dec (oops/oget evt "viewableItems.length")))]
(when-let [last-visible-element (aget (oops/oget e "viewableItems")
(dec (oops/oget e "viewableItems.length")))]
(let [index (oops/oget last-visible-element "index")
;; Get first not visible element, if it's a datemark/gap
;; we might unnecessarely add messages on receiving as
@@ -55,43 +73,164 @@
(= :message (:type first-not-visible)))
first-not-visible))))))
;;TODO this is not really working in pair with inserting new messages because we stop inserting new
;;messages
;;if they outside the viewarea, but we load more here because end is reached,so its slowdown UI because
;;we
;;load and render 20 messages more, but we can't prevent this , because otherwise :on-end-reached will
;;work wrong
(defn list-on-end-reached
[]
(if @state/scrolling
(rf/dispatch [:chat.ui/load-more-messages-for-current-chat])
(background-timer/set-timeout #(rf/dispatch [:chat.ui/load-more-messages-for-current-chat])
(if platform/low-device? 700 200))))
[scroll-y]
;; FIXME: that's a bit of a hack but we need to update `scroll-y` once the new messages
;; are fetched in order for the header to work properly
(let [on-loaded (fn [n]
(reanimated/set-shared-value scroll-y
(+ (reanimated/get-shared-value scroll-y)
(* n 200))))]
(if @state/scrolling
(rf/dispatch [:chat.ui/load-more-messages-for-current-chat on-loaded])
(background-timer/set-timeout #(rf/dispatch [:chat.ui/load-more-messages-for-current-chat
on-loaded])
(if platform/low-device? 700 100)))))
(defonce messages-view-height (reagent/atom 0))
(defn contact-icon
[{:keys [ens-verified added?]}]
(when (or ens-verified added?)
[rn/view
{:style {:padding-left 10
:margin-top 2}}
(if ens-verified
[quo/icon :i/verified
{:no-color true
:size 20
:color (colors/theme-colors colors/success-50 colors/success-60)}]
(when added?
[quo/icon :i/contact
{:no-color true
:size 20
:color (colors/theme-colors colors/primary-50 colors/primary-60)}]))]))
(defn on-messages-view-layout
[evt]
(reset! messages-view-height (oops/oget evt "nativeEvent.layout.height")))
(def header-extrapolation-option
{:extrapolateLeft "clamp"
:extrapolateRight "clamp"})
(defn list-footer
[{:keys [chat-id]}]
(let [loading-messages? (rf/sub [:chats/loading-messages? chat-id])
all-loaded? (rf/sub [:chats/all-loaded? chat-id])]
(when (or loading-messages? (not chat-id) (not all-loaded?))
[rn/view {:style (when platform/android? {:scaleY -1})}
[quo/skeleton @messages-view-height]])))
(defn loading-view
[chat-id]
(let [loading-messages? (rf/sub [:chats/loading-messages? chat-id])
all-loaded? (rf/sub [:chats/all-loaded? chat-id])
messages (rf/sub [:chats/raw-chat-messages-stream chat-id])
loading-first-page? (= (count messages) 0)
top-spacing (if loading-first-page? 0 navigation.style/navigation-bar-height)]
(when (or loading-messages? (not all-loaded?))
[rn/view {:padding-top top-spacing}
[quo/skeleton
(if loading-first-page?
(- @messages-view-height
@messages-view-header-height
composer.constants/composer-default-height
loading-indicator-extra-spacing)
loading-indicator-page-loading-height)]])))
(defn list-header
[{:keys [chat-id chat-type invitation-admin]}]
(when (= chat-type constants/private-group-chat-type)
[rn/view {:style (when platform/android? {:scaleY -1})}
[chat.group/group-chat-footer chat-id invitation-admin]]))
[insets]
[rn/view
{:background-color (colors/theme-colors colors/white colors/neutral-95)
:margin-bottom (- 0
(:top insets)
(when platform/ios? style/overscroll-cover-height))
:height (+ composer.constants/composer-default-height
(:bottom insets)
spacing-between-composer-and-content
(when platform/ios? style/overscroll-cover-height))}])
(defn f-list-footer-avatar
[{:keys [scroll-y display-name online? photo-path]}]
(let [image-scale-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[1 0.5]
header-extrapolation-option)
image-top-margin-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[0 40]
header-extrapolation-option)
image-side-margin-animation (reanimated/interpolate scroll-y
scroll-animation-input-range
[0 -20]
header-extrapolation-option)]
[reanimated/view
{:style (style/header-image image-scale-animation
image-top-margin-animation
image-side-margin-animation)}
[quo/user-avatar
{:full-name display-name
:online? online?
:profile-picture photo-path
:size :big}]]))
(defn list-footer-avatar
[props]
[:f> f-list-footer-avatar props])
(defn f-list-footer
[{:keys [chat scroll-y cover-bg-color on-layout]}]
(let [{:keys [chat-id chat-name emoji chat-type
group-chat]} chat
all-loaded? (rf/sub [:chats/all-loaded? chat-id])
display-name (if (= chat-type constants/one-to-one-chat-type)
(first (rf/sub [:contacts/contact-two-names-by-identity chat-id]))
(str emoji " " chat-name))
{:keys [bio]} (rf/sub [:contacts/contact-by-identity chat-id])
online? (rf/sub [:visibility-status-updates/online? chat-id])
contact (when-not group-chat
(rf/sub [:contacts/contact-by-address chat-id]))
photo-path (when-not (empty? (:images contact))
(rf/sub [:chats/photo-path chat-id]))
border-animation (reanimated/interpolate scroll-y
[30 125]
[14 0]
header-extrapolation-option)]
[rn/view {:flex 1}
[rn/view
{:style (style/header-container all-loaded?)
:on-layout on-layout}
(when cover-bg-color
[rn/view {:style (style/header-cover cover-bg-color)}])
[reanimated/view {:style (style/header-bottom-part border-animation)}
[rn/view {:style style/header-avatar}
[rn/view {:style {:align-items :flex-start}}
(when-not group-chat
[list-footer-avatar
{:scroll-y scroll-y
:display-name display-name
:online? online?
:profile-picture photo-path}])]
[rn/view {:style style/name-container}
[quo/text
{:weight :semi-bold
:size :heading-1
:style {:margin-top (if group-chat 54 12)}
:number-of-lines 1}
display-name
[contact-icon contact]]]
(when bio
[quo/text {:style style/bio}
bio])]]]
[loading-view chat-id]]))
(defn list-footer
[props]
[:f> f-list-footer props])
(defn list-group-chat-header
[{:keys [chat-id invitation-admin]}]
[rn/view
[chat.group/group-chat-footer chat-id invitation-admin]])
(defn footer-on-layout
[e]
(let [height (oops/oget e "nativeEvent.layout.height")
y (oops/oget e "nativeEvent.layout.y")]
(reset! messages-view-header-height (+ height y))))
(defn render-fn
[{:keys [type value deleted? deleted-for-me? content-type] :as message-data} _ _
{:keys [context keyboard-shown]}]
[rn/view {:style (when platform/android? {:scaleY -1})}
{:keys [context keyboard-shown?]}]
[rn/view {:background-color (colors/theme-colors colors/white colors/neutral-95)}
(if (= type :datemark)
[quo/divider-date value]
(if (= content-type constants/content-type-gap)
@@ -100,71 +239,91 @@
[rn/view {:padding-horizontal 8}
(if (or deleted? deleted-for-me?)
[content.deleted/deleted-message message-data context]
[message/message-with-reactions message-data context keyboard-shown])]))])
[message/message-with-reactions message-data context keyboard-shown?])]))])
(defn scroll-handler
[event scroll-y]
(let [content-size-y (- (oops/oget event "nativeEvent.contentSize.height")
(oops/oget event "nativeEvent.layoutMeasurement.height"))
current-y (oops/oget event "nativeEvent.contentOffset.y")]
(reanimated/set-shared-value scroll-y (- content-size-y current-y))))
(defn messages-list-content
[{:keys [chat-id] :as chat} insets keyboard-shown]
(fn []
(let [context (rf/sub [:chats/current-chat-message-list-view-context])
messages (rf/sub [:chats/raw-chat-messages-stream chat-id])
recording? (rf/sub [:chats/recording?])]
[rn/view
{:style {:flex 1}}
;; NOTE: DO NOT use anonymous functions for handlers
[rn/flat-list
{:key-fn list-key-fn
:ref list-ref
:header [list-header chat]
:footer [list-footer chat]
:data messages
:render-data {:context context
:keyboard-shown keyboard-shown}
:render-fn render-fn
:on-viewable-items-changed on-viewable-items-changed
:on-end-reached list-on-end-reached
:on-scroll-to-index-failed identity ; don't remove this
:content-container-style {:padding-top (+ composer.constants/composer-default-height
(:bottom insets)
32)
:padding-bottom 16}
:scroll-indicator-insets {:top (+ composer.constants/composer-default-height
(:bottom insets))}
:keyboard-dismiss-mode :interactive
:keyboard-should-persist-taps :handled
:on-momentum-scroll-begin state/start-scrolling
:on-momentum-scroll-end state/stop-scrolling
:scroll-event-throttle 16
:on-scroll on-scroll
;; TODO https://github.com/facebook/react-native/issues/30034
:inverted (when platform/ios? true)
:style (when platform/android? {:scaleY -1})
:on-layout on-messages-view-layout
:scroll-enabled (not recording?)}]])))
[{:keys [chat insets scroll-y cover-bg-color keyboard-shown?]}]
(let [context (rf/sub [:chats/current-chat-message-list-view-context])
messages (rf/sub [:chats/raw-chat-messages-stream (:chat-id chat)])
recording? (rf/sub [:chats/recording?])
all-loaded? (rf/sub [:chats/all-loaded? (:chat-id chat)])]
[rn/view {:style {:flex 1}}
[rn/flat-list
{:key-fn list-key-fn
:ref list-ref
:header [:<>
(when (= (:chat-type chat) constants/private-group-chat-type)
[list-group-chat-header chat])
[list-header insets]]
:footer [list-footer
{:chat chat
:scroll-y scroll-y
:cover-bg-color cover-bg-color
:on-layout footer-on-layout}]
:data messages
:render-data {:context context
:keyboard-shown? keyboard-shown?}
:render-fn render-fn
:on-viewable-items-changed on-viewable-items-changed
:on-end-reached #(list-on-end-reached scroll-y)
:on-scroll-to-index-failed identity
:content-container-style {:padding-bottom style/messages-list-bottom-offset}
:scroll-indicator-insets {:top (- composer.constants/composer-default-height 16)}
:keyboard-dismiss-mode :interactive
:keyboard-should-persist-taps :handled
:on-momentum-scroll-begin state/start-scrolling
:on-momentum-scroll-end state/stop-scrolling
:scroll-event-throttle 16
:on-scroll (fn [event]
(scroll-handler event scroll-y)
(when on-scroll
(on-scroll event)))
:style {:background-color (if all-loaded?
cover-bg-color
(colors/theme-colors colors/white
colors/neutral-95))}
:inverted true
:on-layout (fn [e]
(let [layout-height (oops/oget e "nativeEvent.layout.height")]
(reset! messages-view-height layout-height)))
:scroll-enabled (not recording?)}]]))
;; This should be replaced with keyboard hook. It has to do with flat-list probably. The keyboard-shown
;; value updates in the parent component, but does not get passed to the children.
;; When using listeners and resetting the value on an atom it works.
(defn use-keyboard-visibility
[]
(let [show-listener (atom nil)
hide-listener (atom nil)
shown? (atom nil)]
(defn f-messages-list
[{:keys [chat cover-bg-color header-comp footer-comp]}]
(let [insets (safe-area/get-insets)
scroll-y (reanimated/use-shared-value 0)
{:keys [keyboard-height keyboard-shown]} (hooks/use-keyboard)]
(rn/use-effect
(fn []
(reset! show-listener
(.addListener rn/keyboard "keyboardWillShow" #(reset! shown? true)))
(reset! hide-listener
(.addListener rn/keyboard "keyboardWillHide" #(reset! shown? false)))
(fn []
(.remove ^js @show-listener)
(.remove ^js @hide-listener))))
{:shown? shown?}))
(when keyboard-shown
(reanimated/set-shared-value scroll-y
(+ (reanimated/get-shared-value scroll-y)
keyboard-height))))
[keyboard-shown keyboard-height])
[rn/keyboard-avoiding-view
{:style (style/keyboard-avoiding-container insets)
:keyboard-vertical-offset (- (:bottom insets))}
(defn- f-messages-list
[chat insets]
(let [{keyboard-shown? :shown?} (use-keyboard-visibility)]
[messages-list-content chat insets keyboard-shown?]))
(when header-comp
[header-comp {:scroll-y scroll-y}])
[messages-list-content
{:chat chat
:insets insets
:scroll-y scroll-y
:cover-bg-color cover-bg-color
:keyboard-shown? keyboard-shown}]
(when footer-comp
(footer-comp {:insets insets}))]))
(defn messages-list
[chat insets]
[:f> f-messages-list chat insets])
[props]
[:f> f-messages-list props])
@@ -0,0 +1,101 @@
(ns status-im2.contexts.chat.messages.navigation.style
(:require [quo2.foundations.colors :as colors]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]))
(defonce ^:const navigation-bar-height 100)
(defonce ^:const header-offset 56)
(defn button-container
[position]
(merge
{:width 32
:height 32
:border-radius 10
:justify-content :center
:align-items :center
:background-color (colors/theme-colors colors/white-opa-40 colors/neutral-80-opa-40)}
position))
(defn blur-view
[status-bar-height]
{:position :absolute
:top 0
:left 0
:right 0
:height (- navigation-bar-height
(if platform/ios? 0 status-bar-height))
:display :flex
:flex-direction :row
:overflow :hidden})
(defn animated-blur-view
[enabled? animation status-bar-height]
(reanimated/apply-animations-to-style
(when enabled?
{:opacity animation})
(blur-view status-bar-height)))
(def navigation-view
{:z-index 4})
(defn header-container
[status-bar-height]
{:position :absolute
:top (- header-offset
(if platform/ios? 0 status-bar-height))
:left 0
:right 0
:padding-bottom 8
:display :flex
:flex-direction :row
:overflow :hidden})
(def header
{:flex 1})
(defn animated-header
[enabled? y-animation opacity-animation]
(reanimated/apply-animations-to-style
;; here using `top` won't work on Android, so we are using `translateY`
(when enabled?
{:transform [{:translateY y-animation}]
:opacity opacity-animation})
header))
(defn pinned-banner
[status-bar-height]
{:position :absolute
:left 0
:right 0
:top (- navigation-bar-height
(if platform/ios? 0 status-bar-height))})
(defn animated-pinned-banner
[enabled? animation status-bar-height]
(reanimated/apply-animations-to-style
(when enabled?
{:opacity animation})
(pinned-banner status-bar-height)))
(def header-content-container
{:flex-direction :row
:align-items :center
:margin-left 8
:margin-right 8
:margin-top -4
:height 40})
(def header-avatar-container
{:margin-right 8})
(def header-text-container
{:flex 1})
(defn header-display-name
[]
{:color (colors/theme-colors colors/black colors/white)})
(defn header-online
[]
{:color (colors/theme-colors colors/neutral-80-opa-50 colors/white-opa-50)})
@@ -0,0 +1,105 @@
(ns status-im2.contexts.chat.messages.navigation.view
(:require [quo2.core :as quo]
[quo2.foundations.colors :as colors]
[re-frame.db]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[react-native.safe-area :as safe-area]
[status-im2.contexts.chat.messages.navigation.style :as style]
[status-im2.contexts.chat.messages.pin.banner.view :as pin.banner]
[status-im2.constants :as constants]
[utils.re-frame :as rf]
[utils.i18n :as i18n]))
(defn f-navigation-view
[{:keys [scroll-y]}]
(let [insets (safe-area/get-insets)
status-bar-height (:top insets)
{:keys [group-chat chat-id chat-name emoji
chat-type]} (rf/sub [:chats/current-chat-chat-view])
all-loaded? (rf/sub [:chats/all-loaded? chat-id])
display-name (if (= chat-type constants/one-to-one-chat-type)
(first (rf/sub [:contacts/contact-two-names-by-identity chat-id]))
(str emoji " " chat-name))
online? (rf/sub [:visibility-status-updates/online? chat-id])
contact (when-not group-chat
(rf/sub [:contacts/contact-by-address chat-id]))
photo-path (when-not (empty? (:images contact))
(rf/sub [:chats/photo-path chat-id]))
opacity-animation (reanimated/interpolate scroll-y
[style/navigation-bar-height
(+ style/navigation-bar-height 30)]
[0 1]
{:extrapolateLeft "clamp"
:extrapolateRight "extend"})
banner-opacity-animation (reanimated/interpolate scroll-y
[(+ style/navigation-bar-height 50)
(+ style/navigation-bar-height 100)]
[0 1]
{:extrapolateLeft "clamp"
:extrapolateRight "clamp"})
translate-animation (reanimated/interpolate scroll-y
[(+ style/navigation-bar-height 25)
(+ style/navigation-bar-height 100)]
[50 0]
{:extrapolateLeft "clamp"
:extrapolateRight "clamp"})
title-opacity-animation (reanimated/interpolate scroll-y
[0 50]
[0 1]
{:extrapolateLeft "clamp"
:extrapolateRight "clamp"})]
[rn/view {:style style/navigation-view}
[reanimated/blur-view
{:blurAmount 32
:blurType (colors/theme-colors :xlight :dark)
:overlayColor :transparent
:style (style/animated-blur-view all-loaded? opacity-animation status-bar-height)}]
[rn/view
[rn/view {:style (style/header-container status-bar-height)}
[rn/touchable-opacity
{:active-opacity 1
:on-press #(rf/dispatch [:navigate-back])
:accessibility-label :back-button
:style (style/button-container {:margin-left 20})}
[quo/icon :i/arrow-left
{:size 20 :color (colors/theme-colors colors/black colors/white)}]]
[reanimated/view
{:style (style/animated-header all-loaded? translate-animation title-opacity-animation)}
[rn/view {:style style/header-content-container}
(when-not group-chat
[rn/view {:style style/header-avatar-container}
[quo/user-avatar
{:full-name display-name
:online? online?
:profile-picture photo-path
:size :small}]])
[rn/view {:style style/header-text-container}
[rn/view {:style {:flex-direction :row}}
[quo/text
{:weight :semi-bold
:size :paragraph-1
:number-of-lines 1
:style (style/header-display-name)}
display-name]]
(when online?
[quo/text
{:number-of-lines 1
:weight :regular
:size :paragraph-2
:style (style/header-online)}
(i18n/label :t/online)])]]]
[rn/touchable-opacity
{:active-opacity 1
:style (style/button-container {:margin-right 20})
:accessibility-label :options-button}
[quo/icon :i/options {:size 20 :color (colors/theme-colors colors/black colors/white)}]]]
[reanimated/view
{:style (style/animated-pinned-banner all-loaded? banner-opacity-animation status-bar-height)}
[pin.banner/banner chat-id]]]]))
(defn navigation-view
[props]
[:f> f-navigation-view props])
+17 -59
View File
@@ -1,17 +1,14 @@
(ns status-im2.contexts.chat.messages.view
(:require [quo2.core :as quo]
(:require [quo2.foundations.colors :as colors]
[re-frame.db]
[react-native.core :as rn]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im2.constants :as constants]
[status-im2.contexts.chat.composer.view :as composer]
[status-im2.contexts.chat.messages.contact-requests.bottom-drawer :as
contact-requests.bottom-drawer]
[status-im2.contexts.chat.messages.list.view :as messages.list]
[status-im2.contexts.chat.messages.pin.banner.view :as pin.banner]
[status-im2.contexts.chat.messages.navigation.view :as messages.navigation]
[status-im2.navigation.state :as navigation.state]
[utils.debounce :as debounce]
[utils.re-frame :as rf]))
(defn navigate-back-handler
@@ -24,62 +21,23 @@
;; and will call system back button action
true))
(defn page-nav
[]
(let [{:keys [group-chat chat-id chat-name emoji
chat-type]} (rf/sub [:chats/current-chat])
display-name (if (= chat-type constants/one-to-one-chat-type)
(first (rf/sub [:contacts/contact-two-names-by-identity chat-id]))
(str emoji " " chat-name))
online? (rf/sub [:visibility-status-updates/online? chat-id])
contact (when-not group-chat
(rf/sub [:contacts/contact-by-address chat-id]))
photo-path (rf/sub [:chats/photo-path chat-id])
avatar-image-key (if (seq (:images contact))
:profile-picture
:ring-background)]
[quo/page-nav
{:align-mid? true
:mid-section (if group-chat
{:type :text-only
:main-text display-name}
{:type :user-avatar
:avatar {:full-name display-name
:online? online?
:size :medium
avatar-image-key photo-path}
:main-text display-name
:on-press #(debounce/dispatch-and-chill [:chat.ui/show-profile chat-id]
1000)})
:left-section {:on-press #(do
(rf/dispatch [:chat/close])
(rf/dispatch [:navigate-back]))
:icon :i/arrow-left
:accessibility-label :back-button}
:right-section-buttons [{:on-press #()
:style {:border-width 1
:border-color :red}
:icon :i/options
:accessibility-label :options-button}]}]))
(defn chat-render
[]
(let [;;NOTE: we want to react only on these fields, do not use full chat map here
{:keys [chat-id contact-request-state group-chat able-to-send-message?] :as chat}
(rf/sub [:chats/current-chat-chat-view])
insets (safe-area/get-insets)]
[rn/keyboard-avoiding-view
{:style {:position :relative :flex 1}
:keyboardVerticalOffset (- (:bottom insets))}
[page-nav]
[pin.banner/banner chat-id]
[messages.list/messages-list chat insets]
(if-not able-to-send-message?
[contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]
[:f> composer/composer insets])]))
(let [{:keys [chat-id
contact-request-state
group-chat
able-to-send-message?]
:as chat} (rf/sub [:chats/current-chat-chat-view])]
[messages.list/messages-list
{:cover-bg-color (colors/custom-color :turquoise 50 20)
:chat chat
:header-comp (fn [{:keys [scroll-y]}]
[messages.navigation/navigation-view {:scroll-y scroll-y}])
:footer-comp (fn [{:keys [insets]}]
[rn/view
(if-not able-to-send-message?
[contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]
[:f> composer/composer insets])])}]))
(defn chat
[]
@@ -6,16 +6,16 @@
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[quo2.foundations.colors :as colors]
[status-im2.contexts.chat.photo-selector.view :refer [album-title]]
[status-im2.contexts.chat.photo-selector.album-selector.style :as style]))
(defn album
[{:keys [title count uri]} index _ selected-album]
(defn render-album
[{:keys [title count uri]} index _ {:keys [album? selected-album]}]
(let [selected? (= selected-album title)]
[rn/touchable-opacity
{:on-press (fn []
(rf/dispatch [:chat.ui/camera-roll-select-album title])
(rf/dispatch [:navigate-back]))
(rf/dispatch [:photo-selector/get-photos-for-selected-album])
(reset! album? false))
:style (style/album-container selected?)
:accessibility-label (str "album-" index)}
[rn/image
@@ -31,7 +31,7 @@
[quo/text
{:size :paragraph-2
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}}
(str count " " (i18n/label :t/images))]]
(when count (str count " " (i18n/label :t/images)))]]
(when selected?
[rn/view
{:style {:position :absolute
@@ -39,9 +39,11 @@
[quo/icon :i/check
{:size 20 :color (colors/theme-colors colors/primary-50 colors/primary-60)}]])]))
(def no-title "no-title")
(defn section-header
[{:keys [title]}]
(when (not= title "smart-albums")
(when-not (= title no-title)
[quo/divider-label
{:label title
:container-style style/divider}]))
@@ -51,22 +53,19 @@
(str (:title item) index))
(defn album-selector
[{:keys [on-scroll]}]
(rf/dispatch [:chat.ui/camera-roll-get-albums])
(fn [{:keys [scroll-enabled]}]
(let [albums (rf/sub [:camera-roll/albums])
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
[rn/view {:style {:padding-top 20}}
[album-title false]
[gesture/section-list
{:data albums
:render-fn album
:render-data selected-album
:sections albums
:sticky-section-headers-enabled false
:render-section-header-fn section-header
:style {:margin-top 12}
:content-container-style {:padding-bottom 40}
:key-fn key-fn
:scroll-enabled @scroll-enabled
:on-scroll on-scroll}]])))
[{:keys [scroll-enabled on-scroll]} album? selected-album]
(let [albums (rf/sub [:camera-roll/albums])
albums-sections [{:title no-title :data (:smart-albums albums)}
{:title (i18n/label :t/my-albums) :data (:my-albums albums)}]]
[gesture/section-list
{:data albums-sections
:sections albums-sections
:render-data {:album? album? :selected-album selected-album}
:render-fn render-album
:sticky-section-headers-enabled false
:render-section-header-fn section-header
:content-container-style {:padding-top 64
:padding-bottom 40}
:key-fn key-fn
:scroll-enabled @scroll-enabled
:on-scroll on-scroll}]))
@@ -0,0 +1,146 @@
(ns status-im2.contexts.chat.photo-selector.events
(:require [react-native.cameraroll :as cameraroll]
[clojure.string :as string]
[re-frame.core :as re-frame]
[utils.i18n :as i18n]
[react-native.permissions :as permissions]
[status-im2.config :as config]
[utils.re-frame :as rf]
[status-im.utils.image-processing :as image-processing]
[taoensso.timbre :as log]
[react-native.core :as rn]))
(def maximum-image-size-px 2000)
(defn- resize-photo
[uri callback]
(rn/image-get-size
uri
(fn [width height]
(let [resize? (> (max width height) maximum-image-size-px)]
(image-processing/resize
uri
(if resize? maximum-image-size-px width)
(if resize? maximum-image-size-px height)
60
(fn [^js resized-image]
(let [path (.-path resized-image)
path (if (string/starts-with? path "file") path (str "file://" path))]
(callback {:resized-uri path
:width width
:height height})))
#(log/error "could not resize image" %))))))
(re-frame/reg-fx
:camera-roll-request-permissions-and-get-photos
(fn [[num end-cursor album]]
(permissions/request-permissions
{:permissions [:read-external-storage]
:on-allowed
(fn []
(cameraroll/get-photos
(merge {:first num
:assetType "Photos"
:groupTypes (if (= album (i18n/label :t/recent)) "All" "Albums")
:groupName (when (not= album (i18n/label :t/recent)) album)
:include ["imageSize"]}
(when end-cursor
{:after end-cursor}))
#(re-frame/dispatch [:on-camera-roll-get-photos (:edges %) (:page_info %) end-cursor])))})))
(re-frame/reg-fx
:camera-roll-image-selected
(fn [[image chat-id]]
(resize-photo (:uri image) #(re-frame/dispatch [:photo-selector/image-selected chat-id image %]))))
(defn get-albums
[callback]
(let [albums (atom {:smart-albums []
:my-albums []})]
;; Get the "recent" album first
(cameraroll/get-photos
{:first 1 :groupTypes "All"}
(fn [res-recent]
(swap! albums assoc
:smart-albums
[{:title (i18n/label :t/recent)
:uri (get-in (first (:edges res-recent)) [:node :image :uri])}])
;; Get albums, then loop over albums and get each one's cover (first photo)
(cameraroll/get-albums
{:assetType "All"}
(fn [res-albums]
(let [response-count (count res-albums)]
(if (pos? response-count)
(doseq [album res-albums]
(cameraroll/get-photos
{:first 1 :groupTypes "Albums" :groupName (:title album)}
(fn [res]
(let [uri (get-in (first (:edges res)) [:node :image :uri])]
(swap! albums update :my-albums conj (merge album {:uri uri}))
(when (= (count (:my-albums @albums)) response-count)
(swap! albums update :my-albums #(sort-by :title %))
(callback @albums))))))
(callback @albums)))))))))
(re-frame/reg-fx
:camera-roll-get-albums
(fn []
(get-albums #(re-frame/dispatch [:on-camera-roll-get-albums %]))))
(rf/defn on-camera-roll-get-albums
{:events [:on-camera-roll-get-albums]}
[{:keys [db]} albums]
{:db (assoc db :camera-roll/albums albums)})
(rf/defn camera-roll-get-albums
{:events [:photo-selector/camera-roll-get-albums]}
[_]
{:camera-roll-get-albums nil})
(rf/defn camera-roll-select-album
{:events [:chat.ui/camera-roll-select-album]}
[{:keys [db]} album]
{:db (assoc db :camera-roll/selected-album album)})
(rf/defn image-selected
{:events [:photo-selector/image-selected]}
[{:keys [db]} current-chat-id original {:keys [resized-uri width height]}]
{:db
(update-in db
[:chat/inputs current-chat-id :metadata :sending-image (:uri original)]
merge
original
{:resized-uri resized-uri
:width width
:height height})})
(rf/defn on-camera-roll-get-photos
{:events [:on-camera-roll-get-photos]}
[{:keys [db]} photos page-info end-cursor]
(let [photos_x (when end-cursor (:camera-roll/photos db))]
{:db (-> db
(assoc :camera-roll/photos (concat photos_x (map #(get-in % [:node :image]) photos)))
(assoc :camera-roll/end-cursor (:end_cursor page-info))
(assoc :camera-roll/has-next-page (:has_next_page page-info))
(assoc :camera-roll/loading-more false))}))
(rf/defn get-photos-for-selected-album
{:events [:photo-selector/get-photos-for-selected-album]}
[{:keys [db]} end-cursor]
{:camera-roll-request-permissions-and-get-photos [21 end-cursor
(or (:camera-roll/selected-album db)
(i18n/label :t/recent))]})
(rf/defn camera-roll-loading-more
{:events [:photo-selector/camera-roll-loading-more]}
[{:keys [db]} is-loading]
{:db (assoc db :camera-roll/loading-more is-loading)})
(rf/defn camera-roll-pick
{:events [:photo-selector/camera-roll-pick]}
[{:keys [db]} image chat-id]
(let [current-chat-id (or chat-id (:current-chat-id db))
images (get-in db [:chat/inputs current-chat-id :metadata :sending-image])]
(when (and (< (count images) config/max-images-batch)
(not (some #(= (:uri image) (:uri %)) images)))
{:camera-roll-image-selected [image current-chat-id]})))
@@ -15,18 +15,13 @@
:flex-direction :row
:left 0
:right 0
:top 0
:top 20
:justify-content :center
:z-index 1})
(defn clear-container
[]
{:background-color (colors/theme-colors colors/neutral-10 colors/neutral-80)
:padding-horizontal 12
:padding-vertical 5
:border-radius 10
:position :absolute
:right 20})
(def clear-container
{:position :absolute
:right 20})
(defn close-button-container
[]
@@ -1,150 +1,126 @@
(ns status-im2.contexts.chat.photo-selector.view
(:require
[react-native.gesture :as gesture]
[react-native.safe-area :as safe-area]
[react-native.platform :as platform]
[status-im2.constants :as constants]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[quo2.components.notifications.info-count :as info-count]
[quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.linear-gradient :as linear-gradient]
[reagent.core :as reagent]
[status-im2.constants :as constants]
[status-im2.contexts.chat.photo-selector.style :as style]
[status-im.utils.core :as utils]
[quo.react]
[utils.re-frame :as rf]))
[status-im2.contexts.chat.photo-selector.album-selector.view :as album-selector]
utils.collection))
(defn show-toast
[]
(rf/dispatch [:toasts/upsert
{:id :random-id
:icon :info
:icon-color colors/danger-50-opa-40
:container-style {:top (when platform/ios? 20)}
:text (i18n/label :t/only-6-images)}]))
(defn on-press-confirm-selection
[selected]
[selected close]
(rf/dispatch [:chat.ui/clear-sending-images])
(doseq [item @selected]
(rf/dispatch [:chat.ui/camera-roll-pick item]))
(reset! selected [])
(rf/dispatch [:navigate-back]))
(doseq [item selected]
(rf/dispatch [:photo-selector/camera-roll-pick item]))
(close))
(defn bottom-gradient
[selected-images insets selected]
(when (or (seq @selected) (seq selected-images))
(defn confirm-button
[selected-images sending-image close]
(when (not= selected-images sending-image)
[linear-gradient/linear-gradient
{:colors [:black :transparent]
:start {:x 0 :y 1}
:end {:x 0 :y 0}
:style (style/gradient-container (:bottom insets))}
:style (style/gradient-container (safe-area/get-bottom))}
[quo/button
{:style {:align-self :stretch
:margin-horizontal 20
:margin-top 12}
:on-press #(on-press-confirm-selection selected)
:on-press #(on-press-confirm-selection selected-images close)
:accessibility-label :confirm-selection}
(i18n/label :t/confirm-selection)]]))
(defn clear-button
[selected]
(when (seq @selected)
[rn/touchable-opacity
{:on-press #(reset! selected [])
:style (style/clear-container)
:accessibility-label :clear}
[quo/text {:weight :medium} (i18n/label :t/clear)]]))
[album? selected]
(when (and (not album?) (seq @selected))
[rn/view {:style style/clear-container}
[quo/button
{:type :grey
:size 32
:accessibility-label :clear
:on-press #(reset! selected [])}
(i18n/label :t/clear)]]))
(defn remove-selected
[coll item]
(vec (remove #(= (:uri item) (:uri %)) coll)))
(defn image
(defn render-image
[item index _ {:keys [window-width selected]}]
[rn/touchable-opacity
{:active-opacity 1
:on-press (fn []
(if (some #(= (:uri item) (:uri %)) @selected)
(swap! selected remove-selected item)
(if (>= (count @selected) constants/max-album-photos)
(rf/dispatch [:toasts/upsert
{:id :random-id
:icon :info
:icon-color colors/danger-50-opa-40
:container-style {:top (when platform/ios? 20)}
:text (i18n/label :t/only-6-images)}])
(swap! selected conj item))))
:accessibility-label (str "image-" index)}
[rn/image
{:source {:uri (:uri item)}
:style (style/image window-width index)}]
(when (some #(= (:uri item) (:uri %)) @selected)
[rn/view {:style (style/overlay window-width)}])
(when (some #(= (:uri item) (:uri %)) @selected)
[info-count/info-count
{:style style/image-count
:accessibility-label (str "count-" index)}
(inc (utils/first-index #(= (:uri item) (:uri %)) @selected))])])
(defn album-title
[photos? selected temporary-selected insets close]
(fn []
(let [selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
[rn/touchable-opacity
{:style (style/title-container)
:active-opacity 1
:accessibility-label :album-title
:on-press (fn []
;; TODO: album-selector issue:
;; https://github.com/status-im/status-mobile/issues/15398
(if photos?
(do
(reset! temporary-selected @selected)
(rf/dispatch [:open-modal :album-selector {:insets insets}]))
(close)))}
[quo/text
{:weight :medium
:ellipsize-mode :tail
:number-of-lines 1
:style {:max-width 150}}
selected-album]
[rn/view {:style (style/chevron-container)}
[quo/icon (if photos? :i/chevron-down :i/chevron-up)
{:color (colors/theme-colors colors/neutral-100 colors/white)}]]])))
(let [item-selected? (some #(= (:uri item) (:uri %)) @selected)]
[rn/touchable-opacity
{:on-press (fn []
(if item-selected?
(swap! selected remove-selected item)
(if (>= (count @selected) constants/max-album-photos)
(show-toast)
(swap! selected conj item))))
:accessibility-label (str "image-" index)}
[rn/image
{:source {:uri (:uri item)}
:style (style/image window-width index)}]
(when item-selected?
[:<>
[rn/view {:style (style/overlay window-width)}]
[info-count/info-count
{:style style/image-count
:accessibility-label (str "count-" index)}
(inc (utils.collection/first-index #(= (:uri item) (:uri %)) @selected))]])]))
(defn photo-selector
[{:keys [scroll-enabled on-scroll close]}]
[:f>
(let [temporary-selected (reagent/atom [])
{:keys [insets]} (rf/sub [:get-screen-params])] ; used when switching albums
(fn []
(let [selected (reagent/atom []) ; currently selected
selected-images (rf/sub [:chats/sending-image])
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
(rn/use-effect
(fn []
(rf/dispatch [:chat.ui/camera-roll-get-photos 20 nil selected-album])
(if (seq selected-images)
(reset! selected (vec (vals selected-images)))
(reset! selected @temporary-selected)))
[selected-album])
[:f>
(fn []
(let [window-width (:width (rn/get-window))
camera-roll-photos (rf/sub [:camera-roll/photos])
end-cursor (rf/sub [:camera-roll/end-cursor])
loading? (rf/sub [:camera-roll/loading-more])
has-next-page? (rf/sub [:camera-roll/has-next-page])]
[rn/view {:style {:flex 1}}
[rn/view
{:style style/buttons-container}
[album-title true selected temporary-selected insets close]
[clear-button selected]]
[gesture/flat-list
{:key-fn identity
:render-fn image
:render-data {:window-width window-width :selected selected}
:data camera-roll-photos
:num-columns 3
:content-container-style {:width "100%"
:padding-bottom (+ (:bottom insets) 100)
:padding-top 64}
:on-scroll on-scroll
:scroll-enabled @scroll-enabled
:on-end-reached #(rf/dispatch [:camera-roll/on-end-reached end-cursor
selected-album loading?
has-next-page?])}]
[bottom-gradient selected-images insets selected]]))])))])
[{:keys [scroll-enabled on-scroll close] :as sheet}]
(rf/dispatch [:photo-selector/get-photos-for-selected-album])
(rf/dispatch [:photo-selector/camera-roll-get-albums])
(let [album? (reagent/atom false)
sending-image (into [] (vals (rf/sub [:chats/sending-image])))
selected-images (reagent/atom sending-image)
window-width (:width (rn/get-window))]
(fn []
(let [camera-roll-photos (rf/sub [:camera-roll/photos])
end-cursor (rf/sub [:camera-roll/end-cursor])
loading? (rf/sub [:camera-roll/loading-more])
has-next-page? (rf/sub [:camera-roll/has-next-page])
selected-album (or (rf/sub [:camera-roll/selected-album]) (i18n/label :t/recent))]
[rn/view {:style {:flex 1 :margin-top -20}}
[rn/view {:style style/buttons-container}
[quo/dropdown {:type :grey :size 32 :on-change #(swap! album? not) :selected @album?}
selected-album]
[clear-button @album? selected-images]]
(if @album?
[album-selector/album-selector sheet album? selected-album]
[:<>
[gesture/flat-list
{:key-fn identity
:render-fn render-image
:render-data {:window-width window-width :selected selected-images}
:data camera-roll-photos
:num-columns 3
:content-container-style {:width "100%"
:padding-bottom (+ (safe-area/get-bottom) 100)
:padding-top 64}
:on-scroll on-scroll
:scroll-enabled @scroll-enabled
:on-end-reached (fn []
(when (and (not loading?) has-next-page?)
(rf/dispatch [:photo-selector/camera-roll-loading-more true])
(rf/dispatch [:photo-selector/get-photos-for-selected-album
end-cursor])))}]
[confirm-button @selected-images sending-image close]])]))))
@@ -26,6 +26,7 @@
[content-width]
[rn/image
{:style {:resize-mode :stretch
:margin-top 32
:width content-width}
:source (resources/get-image :onboarding-illustration)}])
@@ -1,5 +1,8 @@
(ns status-im2.contexts.onboarding.enter-seed-phrase.style
(:require [quo2.foundations.colors :as colors]))
(:require [quo2.foundations.colors :as colors]
[react-native.safe-area :as safe-area]))
(def full-layout {:flex 1})
(def page-container
{:position :absolute
@@ -9,6 +12,27 @@
:right 0
:background-color colors/neutral-80-opa-80-blur})
(def form-container
{:flex 1
:padding-horizontal 20
:padding-vertical 12})
(def header-container
{:flex-direction :row
:justify-content :space-between})
(def word-count-container
{:justify-content :flex-end
:margin-bottom 2})
(def input-container
{:height 120
:margin-top 12
:margin-horizontal -20})
(defn continue-button
[keyboard-shown?]
{:margin-top :auto
:margin-bottom (when-not keyboard-shown? (safe-area/get-bottom))})
(def keyboard-container {:margin-top :auto})
@@ -1,6 +1,7 @@
(ns status-im2.contexts.onboarding.enter-seed-phrase.view
(:require [clojure.string :as string]
[quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
@@ -13,57 +14,152 @@
[utils.re-frame :as rf]
[utils.security.core :as security]))
(def button-disabled?
(comp not constants/seed-phrase-valid-length mnemonic/words-count))
(def ^:private max-seed-phrase-length
(apply max constants/seed-phrase-valid-length))
(defn clean-seed-phrase
[s]
(as-> s $
(string/lower-case $)
(string/split $ #"\s")
(filter #(not (string/blank? %)) $)
(string/join " " $)))
(defn- partial-word-in-dictionary?
[partial-word]
(some #(string/starts-with? % partial-word) mnemonic/dictionary))
(defn page
[{:keys [navigation-bar-top]}]
(let [seed-phrase (reagent/atom "")
error-message (reagent/atom "")
on-invalid-seed-phrase #(reset! error-message (i18n/label :t/custom-seed-phrase))]
(fn []
[rn/view {:style style/page-container}
[navigation-bar/navigation-bar {:top navigation-bar-top}]
[rn/view {:style {:padding-horizontal 20}}
[quo/text
{:weight :bold
:align :center}
(i18n/label :t/use-recovery-phrase)]
[quo/text
(i18n/label-pluralize (mnemonic/words-count @seed-phrase) :t/words-n)]
[rn/view {:style style/input-container}
[quo/recovery-phrase-input
{:on-change-text (fn [t]
(reset! seed-phrase (clean-seed-phrase t))
(reset! error-message ""))
:mark-errors? true
:error-pred (constantly false)
:word-limit 24
:auto-focus true
:accessibility-label :passphrase-input
:placeholder (i18n/label :t/seed-phrase-placeholder)
:auto-correct false}
@seed-phrase]]
[quo/button
{:disabled (button-disabled? @seed-phrase)
:on-press #(rf/dispatch [:onboarding-2/seed-phrase-entered
(security/mask-data @seed-phrase)
on-invalid-seed-phrase])}
(i18n/label :t/continue)]
(when (seq @error-message)
[quo/text @error-message])]])))
(defn- word-in-dictionary?
[word]
(some #(= % word) mnemonic/dictionary))
(def ^:private partial-word-not-in-dictionary?
(comp not partial-word-in-dictionary?))
(def ^:private word-not-in-dictionary?
(comp not word-in-dictionary?))
(defn- header
[seed-phrase-count]
[rn/view {:style style/header-container}
[quo/text {:weight :semi-bold :size :heading-1}
(i18n/label :t/use-recovery-phrase)]
[rn/view {:style style/word-count-container}
[quo/text
{:style {:color colors/white-opa-40}
:weight :regular
:size :paragraph-2}
(i18n/label-pluralize seed-phrase-count :t/words-n)]]])
(defn- clean-seed-phrase
[seed-phrase]
(-> seed-phrase
(string/lower-case)
(string/replace #", " " ")
(string/replace #"," " ")
(string/replace #"\s+" " ")
(string/trim)))
(defn- recovery-form
[{:keys [seed-phrase word-count error-state? all-words-valid? on-change-seed-phrase
keyboard-shown? on-submit]}]
(let [button-disabled? (or error-state?
(not (constants/seed-phrase-valid-length word-count))
(not all-words-valid?))]
[rn/view {:style style/form-container}
[header word-count]
[rn/view {:style style/input-container}
[quo/recovery-phrase-input
{:accessibility-label :passphrase-input
:placeholder (i18n/label :t/seed-phrase-placeholder)
:auto-capitalize :none
:auto-correct false
:auto-focus true
:mark-errors? true
:word-limit max-seed-phrase-length
:error-pred-current-word partial-word-not-in-dictionary?
:error-pred-written-words word-not-in-dictionary?
:on-change-text on-change-seed-phrase}
seed-phrase]]
[quo/button
{:style (style/continue-button keyboard-shown?)
:disabled button-disabled?
:on-press on-submit}
(i18n/label :t/continue)]]))
(defn keyboard-suggestions
[current-word]
(->> mnemonic/dictionary
(filter #(string/starts-with? % current-word))
(take 7)))
(defn screen
[]
(reagent/with-let [keyboard-shown? (reagent/atom false)
keyboard-show-listener (.addListener rn/keyboard
"keyboardDidShow"
#(reset! keyboard-shown? true))
keyboard-hide-listener (.addListener rn/keyboard
"keyboardDidHide"
#(reset! keyboard-shown? false))
invalid-seed-phrase? (reagent/atom false)
set-invalid-seed-phrase #(reset! invalid-seed-phrase? true)
seed-phrase (reagent/atom "")
on-change-seed-phrase (fn [new-phrase]
(when @invalid-seed-phrase?
(reset! invalid-seed-phrase? false))
(reset! seed-phrase new-phrase))
on-submit (fn []
(swap! seed-phrase clean-seed-phrase)
(rf/dispatch [:onboarding-2/seed-phrase-entered
(security/mask-data @seed-phrase)
set-invalid-seed-phrase]))]
(let [words-coll (mnemonic/passphrase->words @seed-phrase)
last-word (peek words-coll)
pick-suggested-word (fn [pressed-word]
(swap! seed-phrase str (subs pressed-word (count last-word)) " "))
;; Last word doesn't exist in dictionary while being written by the user, so
;; it's validated checking whether is a substring of a dictionary word or not.
last-partial-word-valid? (partial-word-in-dictionary? last-word)
last-word-valid? (word-in-dictionary? last-word)
butlast-words-valid? (every? word-in-dictionary? (butlast words-coll))
all-words-valid? (and butlast-words-valid? last-word-valid?)
word-count (mnemonic/words-count @seed-phrase)
words-exceeded? (> word-count max-seed-phrase-length)
error-in-words? (or (not last-partial-word-valid?)
(not butlast-words-valid?))
upper-case? (boolean (re-find #"[A-Z]" @seed-phrase))
suggestions-state (cond
(or error-in-words?
words-exceeded?
@invalid-seed-phrase?) :error
(string/blank? @seed-phrase) :info
(string/ends-with? @seed-phrase " ") :empty
:else :words)
suggestions-text (cond
upper-case? (i18n/label :t/seed-phrase-words-uppercase)
words-exceeded? (i18n/label :t/seed-phrase-words-exceeded)
error-in-words? (i18n/label :t/seed-phrase-error)
@invalid-seed-phrase? (i18n/label :t/seed-phrase-invalid)
:else (i18n/label :t/seed-phrase-info))]
[:<>
[recovery-form
{:seed-phrase @seed-phrase
:error-state? (= suggestions-state :error)
:all-words-valid? all-words-valid?
:on-change-seed-phrase on-change-seed-phrase
:word-count word-count
:on-submit on-submit
:keyboard-shown? @keyboard-shown?}]
(when @keyboard-shown?
[rn/view {:style style/keyboard-container}
[quo/predictive-keyboard
{:type suggestions-state
:blur? true
:text suggestions-text
:words (keyboard-suggestions last-word)
:on-press pick-suggested-word}]])])
(finally
(.remove keyboard-show-listener)
(.remove keyboard-hide-listener))))
(defn enter-seed-phrase
[]
(let [{:keys [top]} (safe-area/get-insets)]
[rn/view {:style {:flex 1}}
(let [{navigation-bar-top :top} (safe-area/get-insets)]
[rn/view {:style style/full-layout}
[background/view true]
[page {:navigation-bar-top top}]]))
[rn/keyboard-avoiding-view {:style style/page-container}
[navigation-bar/navigation-bar {:top navigation-bar-top}]
[screen]]]))
@@ -1,60 +1,23 @@
(ns status-im2.contexts.onboarding.intro.style
(:require
[react-native.platform :as platform]
[quo2.foundations.colors :as colors]))
(def progress-bar-container
{:background-color :transparent
:flex-direction :row
:margin-vertical 16})
(defn progress-bar-item
[index position end?]
{:height 2
:flex 1
:border-width 1
:border-color (if (= index position) colors/white colors/white-opa-10)
:margin-right (if end? 0 8)})
(def carousel
{:height 92
;; (padding-top) This insets issue needs a consistent implementation across all screens.
:padding-top (if platform/android? 0 44)
:position :absolute
:top 0
:bottom 0
:left 0
:right 0
:z-index 2
:background-color :transparent
:padding-vertical 12
:padding-horizontal 20})
(def carousel-text
{:background-color :transparent
:color colors/white})
(def page-container
{:flex 1
:justify-content :flex-end})
(def page-image
{:position :absolute
:top 0
:bottom 0
:left 0
:right 0
:width "100%"
:aspect-ratio 1})
(def text-container
{:flex 1
:max-width 180
:flex-wrap :wrap})
(def plain-text
{:flex 1
:color (colors/alpha colors/white 0.7)})
{:size :paragraph-2
:weight :regular
:color colors/white-opa-70})
(def highlighted-text
{:flex 1
:color colors/white})
{:flex 1
:size :paragraph-2
:weight :regular
:color colors/white})
@@ -21,17 +21,15 @@
(rf/dispatch [:hide-terms-of-services-opt-in-screen]))
:heading (i18n/label :t/new-to-status)
:accessibility-label :new-to-status-button}}
(i18n/label :t/you-already-use-status)
[quo/text
{:style style/plain-text}
(i18n/label :t/you-already-use-status)]
[quo/text
{:style style/text-container}
[quo/text
{:size :paragraph-2
:style style/plain-text
:weight :semi-bold}
{:style style/plain-text}
(i18n/label :t/by-continuing-you-accept)]
[quo/text
{:on-press #(rf/dispatch [:open-modal :privacy-policy])
:size :paragraph-2
:style style/highlighted-text
:weight :semi-bold}
:style style/highlighted-text}
(i18n/label :t/terms-of-service)]]]])
@@ -41,7 +41,7 @@
(defn view
[]
(let [pairing-status (rf/sub [:pairing/pairing-in-progress])
(let [pairing-status (rf/sub [:pairing/pairing-status])
profile-color (:color (rf/sub [:onboarding-2/profile]))]
[rn/view {:style style/page-container}
[background/view true]
@@ -10,6 +10,9 @@
[{:label "Muted?"
:key :muted?
:type :boolean}
{:label "Mark as read disabled?"
:key :mark-as-read-disabled?
:type :boolean}
{:label "Show red options?"
:key :show-red-options?
:type :boolean}])
@@ -31,9 +34,10 @@
[[{:icon :i/friend
:label "View channel members and details"
:on-press #(js/alert "View channel members and details")}
{:icon :i/communities
:label "Mark as read"
:on-press #(js/alert "Mark as read")}
{:icon :i/communities
:label "Mark as read"
:disabled? (:mark-as-read-disabled? @state)
:on-press #(js/alert "Mark as read")}
{:icon :i/muted
:label (if (:muted? @state) "Unmute channel" "Mute channel")
:on-press #(js/alert (if (:muted? @state) "Unmute channel" "Mute channel"))
@@ -1,76 +1,78 @@
(ns status-im2.contexts.quo-preview.dropdowns.dropdown
(:require [quo2.components.dropdowns.dropdown :as quo2]
[quo2.foundations.colors :as colors]
(:require [quo2.foundations.colors :as colors]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im2.contexts.quo-preview.preview :as preview]))
[status-im2.contexts.quo-preview.preview :as preview]
[quo2.core :as quo]))
(def descriptor
[{:label "Icon"
:key :icon
[{:label "Type:"
:key :type
:type :select
:options [{:key :main-icons/placeholder
:value "Placeholder"}
{:key :main-icons/locked
:value "Wallet"}]}
{:label "Disabled"
:key :disabled?
:type :boolean}
{:label "Default item"
:key :default-item
:type :text}
{:label "Use border?"
:key :use-border?
:type :boolean}
{:label "Border color"
:key :border-color
:type :select
:options (map
(fn [c]
{:key c
:value c})
(keys colors/customization))}
{:label "DD color"
:key :dd-color
:type :text}
{:label "Size"
:options [{:key :primary
:value "Primary"}
{:key :secondary
:value "Secondary"}
{:key :grey
:value "Grey"}
{:key :dark-grey
:value "Dark Grey"}
{:key :outline
:value "Outline"}
{:key :ghost
:value "Ghost"}
{:key :danger
:value "Danger"}
{:key :positive
:value "Positive"}]}
{:label "Size:"
:key :size
:type :select
:options [{:key :big
:value "big"}
{:key :medium
:value "medium"}
{:key :small
:value "small"}]}])
:options [{:key 56
:value "56"}
{:key 40
:value "40"}
{:key 32
:value "32"}
{:key 24
:value "24"}]}
{:label "Icon:"
:key :icon
:type :boolean}
{:label "Before icon:"
:key :before
:type :boolean}
{:label "Disabled:"
:key :disabled
:type :boolean}
{:label "Label"
:key :label
:type :text}])
(defn cool-preview
[]
(let [items ["Banana"
"Apple"
"COVID +18"
"Orange"
"Kryptonite"
"BMW"
"Meh"]
state (reagent/atom {:icon :main-icons/placeholder
:default-item "item1"
:use-border? false
:dd-color (colors/custom-color :purple 50)
:size :big})
selected-item (reagent/cursor state [:default-item])
on-select #(reset! selected-item %)]
(let [state (reagent/atom {:label "Press Me"
:size 40})
label (reagent/cursor state [:label])
before (reagent/cursor state [:before])
icon (reagent/cursor state [:icon])]
(fn []
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
[rn/view {:padding-bottom 150}
[preview/customizer state descriptor]
[rn/view
{:padding-vertical 60
:align-items :center}
[rn/text (str "Selected item: " @selected-item)]
[quo2/dropdown
(merge @state
{:on-select on-select
:items items})]]]])))
:flex-direction :row
:justify-content :center}
[quo/dropdown
(merge (dissoc @state
:theme
:before
:after)
{:on-press #(println "Hello world!")}
(when @before
{:before :i/placeholder}))
(if @icon :i/placeholder @label)]]]])))
(defn preview-dropdown
[]
@@ -0,0 +1,84 @@
(ns status-im2.contexts.quo-preview.empty-state.empty-state
(:require [quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im2.contexts.quo-preview.preview :as preview]
[status-im2.common.resources :as resources]))
(def descriptor
[{:label "Title:"
:key :title
:type :text}
{:label "Description:"
:key :description
:type :text}
{:label "Image:"
:key :image
:type :select
:options [{:key :no-contacts-light
:value "No contacts light"}
{:key :no-contacts-dark
:value "No contacts dark"}
{:key :no-messages-light
:value "No messages light"}
{:key :no-messages-dark
:value "No messages dark"}]}
{:label "Upper button text"
:key :upper-button-text
:type :text}
{:label "Lower button text"
:key :lower-button-text
:type :text}
{:label "Blur:"
:key :blur?
:type :boolean}])
(defn cool-preview
[]
(let [state (reagent/atom {:image :no-messages-light
:title "A big friendly title"
:description "Some cool description will be here"
:blur? false
:upper-button-text "Send community link"
:lower-button-text "Invite friends to Status"})]
(fn []
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
[rn/view {:padding-bottom 150}
[preview/customizer state descriptor]
[rn/view
{:style {:margin-vertical 24
:background-color (when (:blur? @state) colors/neutral-95)}}
[preview/blur-view
{:style {:width "100%"
:align-items :center
:top (if (:blur? @state) 32 16)
:position (if (:blur? @state)
:absolute
:relative)}
:height 300
:show-blur-background? (:blur? @state)
:blur-view-props (when (:blur? @state)
{:overlay-color colors/neutral-80-opa-80})}
[rn/view {:style {:flex 1 :width "100%"}}
[quo/empty-state
(-> @state
(assoc :upper-button
{:text (:upper-button-text @state)
:on-press #(js/alert "Upper button")})
(assoc :lower-button
{:text (:lower-button-text @state)
:on-press #(js/alert "Lower button")})
(update :image resources/get-image))]]]]]])))
(defn preview-empty-state
[]
[rn/view
{:style {:flex 1
:background-color (colors/theme-colors colors/white colors/neutral-95)}}
[rn/flat-list
{:style {:flex 1}
:keyboard-should-persist-taps :always
:header [cool-preview]
:key-fn str}]])
@@ -13,6 +13,7 @@
[status-im2.contexts.quo-preview.avatars.group-avatar :as group-avatar]
[status-im2.contexts.quo-preview.avatars.icon-avatar :as icon-avatar]
[status-im2.contexts.quo-preview.avatars.user-avatar :as user-avatar]
[status-im2.contexts.quo-preview.selectors.reactions :as selector-reactions]
[status-im2.contexts.quo-preview.avatars.wallet-user-avatar :as wallet-user-avatar]
[status-im2.contexts.quo-preview.banners.banner :as banner]
[status-im2.contexts.quo-preview.buttons.button :as button]
@@ -82,6 +83,7 @@
[status-im2.contexts.quo-preview.tabs.account-selector :as account-selector]
[status-im2.contexts.quo-preview.tabs.segmented-tab :as segmented]
[status-im2.contexts.quo-preview.tabs.tabs :as tabs]
[status-im2.contexts.quo-preview.empty-state.empty-state :as empty-state]
[status-im2.contexts.quo-preview.tags.context-tags :as context-tags]
[status-im2.contexts.quo-preview.tags.permission-tag :as permission-tag]
[status-im2.contexts.quo-preview.tags.status-tags :as status-tags]
@@ -186,6 +188,9 @@
:dropdowns [{:name :dropdown
:options {:topBar {:visible true}}
:component dropdown/preview-dropdown}]
:empty-state [{:name :empty-state
:options {:topBar {:visible true}}
:component empty-state/preview-empty-state}]
:info [{:name :info-message
:options {:topBar {:visible true}}
:component info-message/preview-info-message}
@@ -296,7 +301,10 @@
:component filter/preview}
{:name :selectors
:options {:topBar {:visible true}}
:component selectors/preview-selectors}]
:component selectors/preview-selectors}
{:name :select-reactions
:options {:topBar {:visible true}}
:component selector-reactions/preview}]
:settings [{:name :privacy-option
:options {:topBar {:visible true}}
:component privacy-option/preview-options}
@@ -1,8 +1,10 @@
(ns status-im2.contexts.quo-preview.reactions.react
(:require [quo2.components.reactions.reaction :as quo2.reaction]
(:require [clojure.string :as string]
[quo2.components.reactions.reaction :as quo2.reaction]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im2.constants :as constants]
[status-im2.contexts.quo-preview.preview :as preview]))
(def descriptor
@@ -12,29 +14,24 @@
{:label "Emoji"
:key :emoji
:type :select
:options [{:key :main-icons/love16
:value "Love"}
{:key :main-icons/thumbs-up16
:value "Thumbs Up"}
{:key :main-icons/thumbs-down16
:value "Thumbs Down"}
{:key :main-icons/laugh16
:value "Laugh"}
{:key :main-icons/sad16
:value "Sad"}]}
:options (for [reaction (vals constants/reactions)]
{:key reaction
:value (string/capitalize (name reaction))})}
{:label "Neutral"
:key :neutral?
:type :boolean}])
(defn cool-preview
[]
(let [state (reagent/atom {:emoji :main-icons/love16})]
(let [state (reagent/atom {:emoji :reaction/love})]
(fn []
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
[rn/view {:padding-bottom 150}
[preview/customizer state descriptor]
[rn/view
{:padding-vertical 60
:justify-content :center
:flex-direction :row
:align-items :center}
[quo2.reaction/reaction @state]
[quo2.reaction/add-reaction @state]]]])))
@@ -0,0 +1,30 @@
(ns status-im2.contexts.quo-preview.selectors.reactions
(:require [quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[status-im2.constants :as constants]))
(defn cool-preview
[]
[rn/view
[rn/view {:style {:margin-vertical 24}}
(into [rn/view
{:style {:flex 1
:margin-top 200
:flex-direction :row
:justify-content :center
:align-items :center}}]
(for [emoji (vals constants/reactions)]
^{:key emoji}
[quo/reactions emoji {:container-style {:margin-right 5}}]))]])
(defn preview
[]
[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}]])
@@ -10,15 +10,6 @@
[status-im.utils.platform :as utils.platform]
[status-im2.constants :as constants]))
(rf/defn local-pairing-completed
{:events [:syncing/pairing-completed]}
[{:keys [db]}]
(let [receiver? (= (get-in db [:syncing :role]) constants/local-pairing-role-receiver)]
(merge
{:db (dissoc db :syncing)}
(when receiver?
{:dispatch [:init-root :syncing-results]}))))
(rf/defn local-pairing-update-role
{:events [:syncing/update-role]}
[{:keys [db]} role]
+2 -1
View File
@@ -18,7 +18,8 @@
status-im2.contexts.syncing.events
status-im2.contexts.chat.events
status-im2.common.password-authentication.events
status-im2.contexts.communities.overview.events))
status-im2.contexts.communities.overview.events
status-im2.contexts.chat.photo-selector.events))
(re-frame/reg-cofx
:now
+3 -1
View File
@@ -105,7 +105,9 @@
:translucent true}
:navigationBar {:backgroundColor colors/black}
:layout {:componentBackgroundColor :transparent
:backgroundColor :transparent}
:backgroundColor :transparent
;; issue: https://github.com/wix/react-native-navigation/issues/7726
:orientation (if platform/ios? ["portrait" "landscape"] ["portrait"])}
:animations {:push {:sharedElementTransitions [{:fromId :shared-element
:toId :shared-element
:interpolation {:type :decelerate

Some files were not shown because too many files have changed in this diff Show More