Compare commits

..
21 Commits
Author SHA1 Message Date
Erik Seppanen 43ad3ccb95 Lookup/add a contact from home screen (#14477) 2023-01-24 17:20:34 -05:00
Ibrahem Khalil 7f2623a23e [14849] Only allow dragging inside bottom sheet when no handle is shown (#14850) 2023-01-24 20:27:57 +02:00
Parvesh Monu 080b13c304 fix some buttons are not responding after theme change (#14811) 2023-01-24 20:28:41 +05:30
Roman Volosovskyi df2dd56cfb [#14728] Fix sender avatar in reply 2023-01-24 12:04:24 +01:00
Brian Sztamfater bd3c724c66 feat: record audio complete flow
Signed-off-by: Brian Sztamfater <brian@status.im>
2023-01-23 14:04:06 -03:00
Icaro Motta 206730a777 Show "Added to group chat" notifications (#14785)
Partially implements https://github.com/status-im/status-mobile/issues/14712
Fixes #14744

### Summary

This PR implements the first, among what will probably be many different kinds of membership notifications. For this PR, I started with implementing a particular flow for private group chats because it's already supported by `status-go` (albeit I had to make some changes, see [PR in status-go](https://github.com/status-im/status-go/pull/3088).

1. `A` and `B` are mutual contacts.
2. `A` creates a private group chat with `B` as member.
3. `B` sees the group chat in the app, but doesn't interact with it.
4. `B` reinstalls the app (remember to back up the seed phrase).
5. `A` mentions `B` in the group chat.
6. `B` should see a group chat notification, which can be accepted/declined.

- [x] Also fixes #14744

### Demo

In the video I'm simulating the steps outlined in the *Summary*, but using the approach described in *Steps to test*, because it's way easier to iterate during development.

[demo.webm](https://user-images.githubusercontent.com/46027/212470798-c135d229-948d-4ba5-98db-ee73cc5495cd.webm)

### Review Notes

Some changes had to be made in `status-go` ([PR](https://github.com/status-im/status-go/pull/3088)), namely:

- According to [Figma](https://www.figma.com/file/eDfxTa9IoaCMUy5cLTp0ys/Shell-for-Mobile?node-id=3806%3A586901&t=xLTAjLXjG1UtorpI-0), users should be able to see `accepted` group chat notifications. Until now, `status-go` hardcoded that `accepted` notifications would *not* be returned in query results, and so it would be impossible to show them to users. This was changed and now the RPC endpoint accepts an additional filter. The implementation on the backend is backwards compatible so as to not break Status desktop.
- The `Membership` tab needs to display various types of notifications (group chat, community, etc), but the membership type doesn't exist on the backend. To overcome this constraint, this PR makes the membership type a logical/virtual type, i.e. a Clojure set of types. `status-go` was changed to support querying for multiple notification types (also backwards compatible).

#### Platforms

- Android
- iOS

### Steps to test

Please, follow the steps described in the Summary and you should be able to test.

But during development, I followed these steps (recommended by @cammellos). I documented them here for reference.

1. Checkout `feature/e2e` in status-go. Apply the diff below.
2. `cd cmd/e2e && ./e2e`
3. This will create a temporary account automatically, let's call it `A`.
4. On another device, create account `B`.
5. Follow the steps documented in https://github.com/status-im/status-go/blob/bdc406ea2e6eb990ce5c0fed7ea2e84b63c92139/cmd/e2e/README.md#L2 in order for user `A` to create a group chat with `B` as member. Don't make `A` and `B` mutual contacts.
6. On `B`'s device, a notification should appear, and `B` should be able to accept or decline the "invitation" (actually *invitation* is another concept and related to another feature).

```diff
modified   cmd/e2e/main.go
@@ -283,6 +283,11 @@ func defaultNodeConfig(installationID string) (*params.NodeConfig, error) {
    nodeConfig.NetworkID = 1
    nodeConfig.LogLevel = "ERROR"
    nodeConfig.DataDir = "/ethereum/mainnet_rpc"
+        nodeConfig.HTTPEnabled = true
+        nodeConfig.HTTPPort = 8545
+        nodeConfig.HTTPHost = "localhost"
+        nodeConfig.HTTPVirtualHosts = []string{"localhost"}
+
         nodeConfig.APIModules = "wakuext,ext,waku"

    nodeConfig.UpstreamConfig = params.UpstreamRPCConfig{
modified   protocol/messenger_group_chat.go
@@ -26,17 +26,17 @@ func (m *Messenger) validateAddedGroupMembers(members []string) error {
        }

        contact, _ := m.allContacts.Load(contactID)
-		if contact == nil || !(contact.Added && contact.HasAddedUs) {
-			return ErrGroupChatAddedContacts
-		}
+                if contact == nil {
+                  contact, err = buildContactFromPkString(contactID)
+                  if err != nil {
+                    return err
+                  }
+                }
    }
    return nil
 }

 func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, members []string) (*MessengerResponse, error) {
-	if err := m.validateAddedGroupMembers(members); err != nil {
-		return nil, err
-	}

    var response MessengerResponse
    logger := m.logger.With(zap.String("site", "CreateGroupChatWithMembers"))
```
2023-01-23 13:54:51 -03:00
Parvesh Monu b3a03119d1 Implement unread badge for bottom tabs (#14856) 2023-01-23 20:23:41 +05:30
flexsurfer d79d2e9d36 sanitize quo2 (#14859) 2023-01-23 14:41:55 +01:00
Jamie Caprani 098821d20b chore: get community img from real data (#14765) 2023-01-23 03:17:50 -08:00
Jamie Caprani 92a180c477 add shadows to foundations (#14839)
* chore: add shadows to foundations
2023-01-23 03:06:26 -08:00
Mohamed Javid 6722b45076 [Fix] Admin Notification marked unread after closing and reopening AC (#14824)
* [Fix][#14823] Admin Notification unread issue

* [Fix][#14823] Changed dispatch of event from the PR feedback

* [Fix][#14823] Organize dispatch of event from the PR feedback

* [Fix][#14823] Organize dispatch of event from the PR feedback
2023-01-23 18:56:58 +08:00
Andrea Maria Piana 897a5eb201 [Fixes: #14794] Use toast instead of old pin modal
Instead of using the old modal, we show a toast when the limit of 3
messages is reached.
2023-01-23 10:44:15 +00:00
Parvesh Monu d3667ad683 fix nil value in reanimated style crashing at runtime (#14855) 2023-01-23 15:55:24 +05:30
Ibrahem Khalil dc9454defa Fix using functions as identifiers (#14848) 2023-01-21 13:28:45 +02:00
Omar Basem 53495dc893 Images Album (2) (#14755)
* feat: images album (2)
2023-01-20 16:51:44 +04:00
Jamie Caprani 967c869486 chore: fix component tests and permission drawer preview (#14831) 2023-01-19 15:03:53 -08:00
flexsurfer f0272f2e77 move chat events (#14835) 2023-01-19 19:35:14 +01:00
Churikova Tetiana b8dfa6b645 e2e: offline messages + edit 2023-01-19 19:01:14 +01:00
Omar Basem 6c14fd1cb9 Scroll page animations (#14695)
* feat: scroll page animations
2023-01-19 16:46:05 +04:00
flexsurfer e8e8547879 cleanup setup (#14827) 2023-01-19 12:15:28 +01:00
flexsurfer 2899819e95 rollback user profile (#14828) 2023-01-19 12:02:05 +01:00
173 changed files with 4257 additions and 2833 deletions
-1
View File
@@ -351,7 +351,6 @@ component-test: export COMPONENT_TEST := true
component-test: export BABEL_ENV := test
component-test: ##@test Run tests once in NodeJS
# Here we create the gyp bindings for nodejs
yarn install
yarn shadow-cljs compile component-test && \
jest --config=test/jest/jest.config.js
Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

+1 -1
View File
@@ -38,7 +38,7 @@
{:mobile
{:target :react-native
:output-dir "app"
:init-fn status-im2.setup.core/init
:init-fn status-im2.core/init
;; When false, the Shadow-CLJS watcher won't automatically refresh
;; the target files (a.k.a hot reload). When false, you can manually
;; reload by calling `shadow.cljs.devtools.api/watch-compile-all!`.
+48 -12
View File
@@ -2,39 +2,75 @@ import { useDerivedValue, interpolate } from 'react-native-reanimated';
// Generic Worklets
// 1. kebab-case styles are not working for worklets
// so we have to convert kebab case styles into camel case styles
// 2. remove keys with nil value, else useAnimatedStyle will throw an error
// https://github.com/status-im/status-mobile/issues/14756
export function applyAnimationsToStyle(animations, style) {
return function() {
'worklet'
var animatedStyle = {}
// Normal Style
for (var key in style) {
if (key == "transform") {
var transforms = style[key];
var filteredTransforms = []
for (var transform of transforms) {
var transformKey = Object.keys(transform)[0];
var transformValue = transform[transformKey];
if(transformValue !== null) {
filteredTransforms.push(
{[transformKey.replace(/-./g, x=>x[1].toUpperCase())]: transformValue}
);
}
}
animatedStyle[key] = filteredTransforms;
} else {
var value = style[key];
if (value !== null) {
animatedStyle[key.replace(/-./g, x=>x[1].toUpperCase())] = value;
}
}
}
// Animations
for (var key in animations) {
if (key == "transform") {
var transforms = animations[key];
var animatedTransforms = []
for (var transform of transforms) {
var transformKey = Object.keys(transform)[0];
animatedTransforms.push({
[transformKey]: transform[transformKey].value
})
var transformValue = transform[transformKey].value;
if (transformValue !== null) {
animatedTransforms.push(
{[transformKey.replace(/-./g, x=>x[1].toUpperCase())]: transformValue}
);
}
}
animatedStyle[key] = animatedTransforms;
} else {
animatedStyle[key] = animations[key].value;
var animatedValue = animations[key].value;
if (animatedValue !== null) {
animatedStyle[key.replace(/-./g, x=>x[1].toUpperCase())] = animatedValue;
}
}
}
return Object.assign(animatedStyle, style);
return animatedStyle;
};
};
export function interpolateValue(sharedValue, inputRange, outputRange) {
export function interpolateValue(sharedValue, inputRange, outputRange, extrapolation) {
return useDerivedValue(
function () {
'worklet'
return interpolate(sharedValue.value, inputRange, outputRange);
return interpolate(sharedValue.value, inputRange, outputRange, extrapolation);
}
);
}
+3 -4
View File
@@ -1,6 +1,5 @@
(ns quo2.components.code.snippet
(:require ["react-native" :as react-native]
[cljs-bean.core :as bean]
(:require [cljs-bean.core :as bean]
[clojure.string :as string]
[oops.core :as oops]
[quo2.components.buttons.button :as button]
@@ -168,8 +167,8 @@
:on-copy-press #(when on-copy-press
(on-copy-press children))})
;; Default props to adapt Highlighter for react-native.
:CodeTag react-native/View
:PreTag react-native/View
;;:CodeTag react-native/View
;;:PreTag react-native/View
:show-line-numbers false
:style #js {}
:custom-style #js {:backgroundColor nil}}
@@ -1,6 +1,5 @@
(ns quo2.components.community.token-gating
(:require [quo.react-native :as rn]
[quo2.components.avatars.channel-avatar :as channel-avatar]
(:require [quo2.components.avatars.channel-avatar :as channel-avatar]
[quo2.components.buttons.button :as button]
[quo2.components.icon :as icon]
[quo2.components.info.information-box :as information-box]
@@ -8,7 +7,8 @@
[quo2.components.tags.token-tag :as token-tag]
[quo2.foundations.colors :as colors]
[utils.i18n :as i18n]
[react-native.fast-image :as fast-image]))
[react-native.fast-image :as fast-image]
[react-native.core :as rn]))
(def ^:private token-tag-horizontal-spacing 7)
(def token-tag-vertical-spacing 5)
@@ -1,6 +1,6 @@
(ns quo2.components.drawers.--tests--.action-drawers-component-spec
(ns quo2.components.drawers.action-drawers.component-spec
(:require ["@testing-library/react-native" :as rtl]
[quo2.components.drawers.action-drawers :as action-drawer]
[quo2.components.drawers.action-drawers.view :as action-drawer]
[reagent.core :as reagent]))
(defn render-action-drawer
@@ -50,4 +50,4 @@
:add-divider? true
:accessibility-label :first-element}]])
(-> (js/expect (rtl/screen.getAllByLabelText "divider"))
(.toBeTruthy))))
(.toBeTruthy))))
@@ -0,0 +1,41 @@
(ns quo2.components.drawers.action-drawers.style
(:require [quo2.foundations.colors :as colors]))
(def divider
{:border-top-width 1
:border-top-color (colors/theme-colors
colors/neutral-10
colors/neutral-90)
:margin-top 8
:margin-bottom 7
:align-items :center
:flex-direction :row})
(defn container
[sub-label]
{:border-radius 12
:height (if sub-label 58 50)
:margin-horizontal 8})
(defn row-container
[sub-label]
{:height (if sub-label 58 50)
:margin-horizontal 12
:flex-direction :row})
(def left-icon
{:height 20
:margin-top :auto
:margin-bottom :auto
:margin-right 12
:width 20})
(def text-container
{:flex 1
:justify-content :center})
(def right-icon
{:height 20
:margin-top :auto
:margin-bottom :auto
:width 20})
@@ -1,8 +1,9 @@
(ns quo2.components.drawers.action-drawers
(ns quo2.components.drawers.action-drawers.view
(:require [quo2.components.icon :as icon]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]))
[react-native.core :as rn]
[quo2.components.drawers.action-drawers.style :as style]))
(defn- get-icon-color
[danger?]
@@ -12,14 +13,7 @@
(def divider
[rn/view
{:style {:border-top-width 1
:border-top-color (colors/theme-colors
colors/neutral-10
colors/neutral-90)
:margin-top 8
:margin-bottom 7
:align-items :center
:flex-direction :row}
{:style style/divider
:accessible true
:accessibility-label :divider}])
@@ -38,32 +32,20 @@
(when add-divider? divider)
[rn/touchable-highlight
{:accessibility-label accessibility-label
:style {:border-radius 12
:height (if sub-label 58 50)
:margin-horizontal 8}
:style (style/container sub-label)
:underlay-color (colors/theme-colors colors/neutral-5 colors/neutral-90)
:on-press on-press}
[rn/view
{:style
{:height (if sub-label 58 50)
:margin-horizontal 12
:flex-direction :row}}
{:style (style/row-container sub-label)}
[rn/view
{:accessibility-label :left-icon-for-action
:accessible true
:style
{:height 20
:margin-top :auto
:margin-bottom :auto
:margin-right 12
:width 20}}
:style style/left-icon}
[icon/icon icon
{:color (get-icon-color danger?)
:size 20}]]
[rn/view
{:style
{:flex 1
:justify-content :center}}
{:style style/text-container}
[text/text
{:size :paragraph-1
:weight :medium
@@ -79,11 +61,7 @@
sub-label])]
(when right-icon
[rn/view
{:style
{:height 20
:margin-top :auto
:margin-bottom :auto
:width 20}
{:style style/right-icon
:accessible true
:accessibility-label :right-icon-for-action}
[icon/icon right-icon
@@ -1,10 +1,10 @@
(ns quo2.components.drawers.permission-context.--tests--.permission-context-component-spec
(ns quo2.components.drawers.permission-context.component-spec
(:require [quo2.components.drawers.permission-context.view :as permission-context]
[react-native.core :as rn]
[test-helpers.component :as h]))
(h/describe "permission context"
(h/test
(h/test "it tests the default render"
(h/render [permission-context/view
[rn/text
{:accessibility-label :accessibility-id}
@@ -3,9 +3,13 @@
(def radius 20)
(def container
{:padding-top 16
{:flex-direction :row
:background-color (colors/theme-colors colors/white colors/neutral-90)
:height 82
:padding-top 16
:padding-bottom 48
:padding-horizontal 20
:justify-content :center
:padding-right :auto
:shadow-offset {:width 0
:height 2}
:shadow-radius radius
@@ -1,8 +1,12 @@
(ns quo2.components.drawers.permission-context.view
(:require [react-native.core :as rn]
[quo2.foundations.colors :as colors]
[quo2.components.drawers.permission-context.style :as style]))
(defn view
[children]
[rn/view {:style style/container}
[children on-press]
[rn/touchable-highlight
{:on-press on-press
:underlay-color (colors/theme-colors :transparent colors/neutral-95-opa-70)
:style style/container}
children])
@@ -3,8 +3,7 @@
[quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[quo2.components.messages.author.style :as style]
[react-native.core :as rn]
[status-im.utils.utils :as utils]))
[react-native.core :as rn]))
(def middle-dot "·")
@@ -35,7 +34,7 @@
profile-name])])))
(defn author
[{:keys [profile-name nickname chat-key ens-name time-str contact? verified? untrustworthy?]}]
[{:keys [profile-name nickname short-chat-key ens-name time-str contact? verified? untrustworthy?]}]
[:f>
(fn []
(let [ens? (-> ens-name string/blank? not)
@@ -85,7 +84,7 @@
{:monospace true
:size :paragraph-2
:style style/chat-key-text}
(utils/get-shortened-address chat-key)])
short-chat-key])
(when-not ens?
[text/text
{:monospace true
@@ -0,0 +1,159 @@
(ns quo2.components.record-audio.record-audio.--tests--.record-audio-component-spec
(:require [quo2.components.record-audio.record-audio.view :as record-audio]
[status-im.audio.core :as audio]
[test-helpers.component :as h]))
(h/describe "record audio component"
(h/before-each
(fn []
(h/use-fake-timers)))
(h/after-each
(fn []
(h/clear-all-timers)
(h/use-real-timers)))
(h/test "renders record-audio"
(h/render [record-audio/record-audio])
(-> (h/expect (h/get-by-test-id "record-audio"))
(.toBeTruthy)))
(h/test "record-audio on-start-recording works"
(let [event (js/jest.fn)]
(h/render [record-audio/record-audio {:on-start-recording event}])
(h/fire-event
:on-start-should-set-responder
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(-> (h/expect event)
(.toHaveBeenCalledTimes 1))))
(h/test "record-audio on-reviewing-audio works"
(let [event (js/jest.fn)]
(h/render [record-audio/record-audio {:on-reviewing-audio event}])
(with-redefs [audio/start-recording (fn [_ on-start _]
(on-start))]
(h/fire-event
:on-start-should-set-responder
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(h/advance-timers-by-time 500)
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(-> (h/expect event)
(.toHaveBeenCalledTimes 1)))))
(h/test "record-audio on-send works after reviewing audio"
(let [event (js/jest.fn)]
(h/render [record-audio/record-audio {:on-send event}])
(with-redefs [audio/start-recording (fn [_ on-start _]
(on-start))
audio/get-recorder-file-path (fn [] "audio-file-path")]
(h/fire-event
:on-start-should-set-responder
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(h/advance-timers-by-time 500)
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 80
:locationY 80}})
(-> (js/expect event)
(.toHaveBeenCalledTimes 1))
(-> (js/expect event)
(.toHaveBeenCalledWith "audio-file-path")))))
(h/test "record-audio on-send works after sliding to the send button"
(let [event (js/jest.fn)]
(h/render [record-audio/record-audio {:on-send event}])
(with-redefs [audio/start-recording (fn [_ on-start _]
(on-start))
audio/stop-recording (fn [_ on-stop _]
(on-stop))
audio/get-recorder-file-path (fn [] "audio-file-path")]
(h/fire-event
:on-start-should-set-responder
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(h/advance-timers-by-time 500)
(h/fire-event
:on-responder-move
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 80
:locationY -30
:pageX 80
:pageY -30}})
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 40
:locationY 80}})
(-> (js/expect event)
(.toHaveBeenCalledTimes 1))
(-> (js/expect event)
(.toHaveBeenCalledWith "audio-file-path")))))
(h/test "record-audio on-cancel works after reviewing audio"
(let [event (js/jest.fn)]
(h/render [record-audio/record-audio {:on-cancel event}])
(with-redefs [audio/start-recording (fn [_ on-start _]
(on-start))]
(h/fire-event
:on-start-should-set-responder
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(h/advance-timers-by-time 500)
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 40
:locationY 80}})
(-> (js/expect event)
(.toHaveBeenCalledTimes 1)))))
(h/test "cord-audio on-cancel works after sliding to the cancel button"
(let [event (js/jest.fn)]
(h/render [record-audio/record-audio {:on-cancel event}])
(with-redefs [audio/start-recording (fn [_ on-start _]
(on-start))
audio/stop-recording (fn [_ on-stop _]
(on-stop))]
(h/fire-event
:on-start-should-set-responder
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX 70
:locationY 70}})
(h/advance-timers-by-time 500)
(h/fire-event
:on-responder-move
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX -30
:locationY 80
:pageX -30
:pageY 80}})
(h/fire-event
:on-responder-release
(h/get-by-test-id "record-audio")
{:nativeEvent {:locationX -10
:locationY 70}})
(-> (js/expect event)
(.toHaveBeenCalledTimes 1))))))
@@ -0,0 +1,88 @@
(ns quo2.components.record-audio.record-audio.buttons.delete-button
(:require [quo2.components.icon :as icons]
[quo2.components.record-audio.record-audio.style :as style]
[quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]
[react-native.core :refer [use-effect]]
[quo2.components.record-audio.record-audio.helpers :refer
[animate-linear-with-delay
animate-easing-with-delay
animate-linear
set-value]]))
(defn delete-button
[recording? ready-to-delete? reviewing-audio?]
[:f>
(fn []
(let [opacity (reanimated/use-shared-value 0)
translate-x (reanimated/use-shared-value 20)
scale (reanimated/use-shared-value 1)
connector-opacity (reanimated/use-shared-value 0)
connector-width (reanimated/use-shared-value 24)
connector-height (reanimated/use-shared-value 12)
border-radius-first-half (reanimated/use-shared-value 8)
border-radius-second-half (reanimated/use-shared-value 8)
start-x-animation (fn []
(animate-linear-with-delay translate-x 12 50 133.33)
(animate-easing-with-delay connector-opacity 1 0 93.33)
(animate-easing-with-delay connector-width 56 83.33 80)
(animate-easing-with-delay connector-height 56 83.33 80)
(animate-easing-with-delay border-radius-first-half 28 83.33 80)
(animate-easing-with-delay border-radius-second-half 28 83.33 80))
reset-x-animation (fn []
(animate-linear translate-x 0 100)
(set-value connector-opacity 0)
(set-value connector-width 24)
(set-value connector-height 12)
(set-value border-radius-first-half 8)
(set-value border-radius-second-half 16))
fade-in-animation (fn []
(animate-linear translate-x 0 200)
(animate-linear opacity 1 200))
fade-out-animation (fn []
(animate-linear
translate-x
(if @reviewing-audio? 35 20)
200)
(if @reviewing-audio?
(animate-linear scale 0.75 200)
(animate-linear opacity 0 200))
(set-value connector-opacity 0)
(set-value connector-width 24)
(set-value connector-height 12)
(set-value border-radius-first-half 8)
(set-value border-radius-second-half 16))
fade-out-reset-animation (fn []
(animate-linear opacity 0 200)
(animate-linear-with-delay translate-x 20 0 200)
(animate-linear-with-delay scale 1 0 200))]
(use-effect (fn []
(if @recording?
(fade-in-animation)
(fade-out-animation)))
[@recording?])
(use-effect (fn []
(when-not @reviewing-audio?
(fade-out-reset-animation)))
[@reviewing-audio?])
(use-effect (fn []
(cond
@ready-to-delete?
(start-x-animation)
@recording?
(reset-x-animation)))
[@ready-to-delete?])
[:<>
[reanimated/view {:style (style/delete-button-container opacity)}
[reanimated/view
{:style (style/delete-button-connector connector-opacity
connector-width
connector-height
border-radius-first-half
border-radius-second-half)}]]
[reanimated/view
{:style (style/delete-button scale translate-x opacity)
:pointer-events :none}
[icons/icon :i/delete
{:color colors/white
:size 20}]]]))])
@@ -0,0 +1,85 @@
(ns quo2.components.record-audio.record-audio.buttons.lock-button
(:require [quo2.components.icon :as icons]
[quo2.components.record-audio.record-audio.style :as style]
[quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]
[react-native.core :refer [use-effect]]
[quo2.components.record-audio.record-audio.helpers :refer
[animate-linear-with-delay
animate-easing-with-delay
animate-linear
set-value]]))
(defn lock-button
[recording? ready-to-lock? locked?]
[:f>
(fn []
(let [translate-x-y (reanimated/use-shared-value 20)
opacity (reanimated/use-shared-value 0)
connector-opacity (reanimated/use-shared-value 0)
width (reanimated/use-shared-value 24)
height (reanimated/use-shared-value 12)
border-radius-first-half (reanimated/use-shared-value 8)
border-radius-second-half (reanimated/use-shared-value 8)
start-x-y-animation (fn []
(animate-linear-with-delay translate-x-y 8 50 116.66)
(animate-easing-with-delay connector-opacity 1 0 80)
(animate-easing-with-delay width 56 83.33 63.33)
(animate-easing-with-delay height 56 83.33 63.33)
(animate-easing-with-delay border-radius-first-half
28
83.33
63.33)
(animate-easing-with-delay border-radius-second-half
28
83.33
63.33))
reset-x-y-animation (fn []
(animate-linear translate-x-y 0 100)
(set-value connector-opacity 0)
(set-value width 24)
(set-value height 12)
(set-value border-radius-first-half 8)
(set-value border-radius-second-half 16))
fade-in-animation (fn []
(animate-linear translate-x-y 0 220)
(animate-linear opacity 1 220))
fade-out-animation (fn []
(animate-linear translate-x-y 20 200)
(animate-linear opacity 0 200)
(set-value connector-opacity 0)
(set-value width 24)
(set-value height 12)
(set-value border-radius-first-half 8)
(set-value border-radius-second-half 16))]
(use-effect (fn []
(if @recording?
(fade-in-animation)
(fade-out-animation)))
[@recording?])
(use-effect (fn []
(cond
@ready-to-lock?
(start-x-y-animation)
(and @recording? (not @locked?))
(reset-x-y-animation)))
[@ready-to-lock?])
(use-effect (fn []
(if @locked?
(fade-out-animation)
(reset-x-y-animation)))
[@locked?])
[:<>
[reanimated/view {:style (style/lock-button-container opacity)}
[reanimated/view
{:style (style/lock-button-connector connector-opacity
width
height
border-radius-first-half
border-radius-second-half)}]]
[reanimated/view
{:style (style/lock-button translate-x-y opacity)
:pointer-events :none}
[icons/icon (if @ready-to-lock? :i/locked :i/unlocked)
{:color (colors/theme-colors colors/black colors/white)
:size 20}]]]))])
@@ -0,0 +1,28 @@
(ns quo2.components.record-audio.record-audio.buttons.record-button
(:require [quo2.components.icon :as icons]
[quo2.components.record-audio.record-audio.style :as style]
[quo2.foundations.colors :as colors]
[react-native.core :as rn :refer [use-effect]]
[react-native.reanimated :as reanimated]
[quo2.components.buttons.button :as button]
[quo2.components.record-audio.record-audio.helpers :refer [set-value]]))
(defn record-button
[recording? reviewing-audio?]
[:f>
(fn []
(let [opacity (reanimated/use-shared-value 1)
show-animation #(set-value opacity 1)
hide-animation #(set-value opacity 0)]
(use-effect (fn []
(if (or @recording? @reviewing-audio?)
(hide-animation)
(show-animation)))
[@recording? @reviewing-audio?])
[reanimated/view {:style (style/record-button-container opacity)}
[button/button
{:type :outline
:size 32
:width 32
:accessibility-label :mic-button}
[icons/icon :i/audio {:color (colors/theme-colors colors/neutral-100 colors/white)}]]]))])
@@ -0,0 +1,212 @@
(ns quo2.components.record-audio.record-audio.buttons.record-button-big
(:require [quo.react :refer [memo]]
[quo2.components.icon :as icons]
[quo2.components.record-audio.record-audio.style :as style]
[quo2.foundations.colors :as colors]
[react-native.core :as rn :refer [use-effect]]
[react-native.reanimated :as reanimated]
[status-im.audio.core :as audio]
[taoensso.timbre :as log]
[cljs-bean.core :as bean]
[reagent.core :as reagent]
[quo2.components.record-audio.record-audio.helpers :refer
[animate-linear
animate-linear-with-delay
animate-linear-with-delay-loop
animate-easing
set-value]]))
(def ^:private scale-to-each 1.8)
(def ^:private scale-to-total 2.6)
(def ^:private scale-padding 0.16)
(def ^:private opacity-from-lock 1)
(def ^:private opacity-from-default 0.5)
(def ^:private signal-anim-duration 3900)
(def ^:private signal-anim-duration-2 1950)
(def ^:private record-audio-worklets (js/require "../src/js/record_audio_worklets.js"))
(defn- ring-scale
[scale substract]
(.ringScale ^js record-audio-worklets
scale
substract))
(def ^:private animated-ring
(reagent/adapt-react-class
(memo
(fn [props]
(let [{:keys [scale opacity color]} (bean/bean props)]
(reagent/as-element
[reanimated/view {:style (style/animated-circle scale opacity color)}]))))))
(defn record-button-big
[recording? ready-to-send? ready-to-lock? ready-to-delete? record-button-is-animating?
record-button-at-initial-position? locked? reviewing-audio? recording-timer recording-length-ms
clear-timeout touch-active? recorder-ref reload-recorder-fn idle? on-send on-cancel]
[:f>
(fn []
(let [scale (reanimated/use-shared-value 1)
opacity (reanimated/use-shared-value 0)
opacity-from (if @ready-to-lock? opacity-from-lock opacity-from-default)
animations (map
(fn [index]
(let [ring-scale (ring-scale scale (* scale-padding index))]
{:scale ring-scale
:opacity (reanimated/interpolate ring-scale
[1 scale-to-each]
[opacity-from 0])}))
(range 0 5))
rings-color (cond
@ready-to-lock? (colors/theme-colors colors/neutral-80-opa-5-opaque
colors/neutral-80)
@ready-to-delete? colors/danger-50
:else colors/primary-50)
translate-y (reanimated/use-shared-value 0)
translate-x (reanimated/use-shared-value 0)
button-color colors/primary-50
icon-color (if (and (not (colors/dark?)) @ready-to-lock?) colors/black colors/white)
icon-opacity (reanimated/use-shared-value 1)
red-overlay-opacity (reanimated/use-shared-value 0)
gray-overlay-opacity (reanimated/use-shared-value 0)
complete-animation (fn []
(cond
(and @ready-to-lock? (not @record-button-is-animating?))
(do
(reset! locked? true)
(reset! ready-to-lock? false))
(and (not @locked?) (not @reviewing-audio?))
(audio/stop-recording
@recorder-ref
(fn []
(cond
@ready-to-send?
(when on-send
(on-send (audio/get-recorder-file-path @recorder-ref)))
@ready-to-delete?
(when on-cancel
(on-cancel)))
(reload-recorder-fn)
(reset! recording? false)
(reset! ready-to-send? false)
(reset! ready-to-delete? false)
(reset! ready-to-lock? false)
(reset! idle? true)
(js/setTimeout #(reset! idle? false) 1000)
(js/clearInterval @recording-timer)
(reset! recording-length-ms 0)
(log/debug "[record-audio] stop recording - success"))
#(log/error "[record-audio] stop recording - error: " %))))
start-animation (fn []
(set-value opacity 1)
(animate-linear scale 2.6 signal-anim-duration)
;; TODO: Research if we can implement this with withSequence method
;; from Reanimated 2
;; GitHub issue [#14561]:
;; https://github.com/status-im/status-mobile/issues/14561
(reset! clear-timeout
(js/setTimeout
(fn []
(set-value scale scale-to-each)
(animate-linear-with-delay-loop scale
scale-to-total
signal-anim-duration-2
0))
signal-anim-duration)))
stop-animation (fn []
(set-value opacity 0)
(reanimated/cancel-animation scale)
(set-value scale 1)
(when @clear-timeout (js/clearTimeout @clear-timeout)))
start-y-animation (fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(animate-easing translate-y -64 250)
(animate-linear-with-delay icon-opacity 0 33.33 76.66)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
reset-y-animation (fn []
(animate-easing translate-y 0 300)
(animate-linear icon-opacity 1 500)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500))
start-x-animation (fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(animate-easing translate-x -64 250)
(animate-linear-with-delay icon-opacity 0 33.33 76.66)
(animate-linear red-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
250))
reset-x-animation (fn []
(animate-easing translate-x 0 300)
(animate-linear icon-opacity 1 500)
(animate-linear red-overlay-opacity 0 100)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
500))
start-x-y-animation (fn []
(reset! record-button-at-initial-position? false)
(reset! record-button-is-animating? true)
(animate-easing translate-y -44 200)
(animate-easing translate-x -44 200)
(animate-linear-with-delay icon-opacity 0 33.33 33.33)
(animate-linear gray-overlay-opacity 1 33.33)
(js/setTimeout (fn []
(reset! record-button-is-animating? false)
(when-not @touch-active? (complete-animation)))
200))
reset-x-y-animation (fn []
(animate-easing translate-y 0 300)
(animate-easing translate-x 0 300)
(animate-linear icon-opacity 1 500)
(animate-linear gray-overlay-opacity 0 800)
(js/setTimeout (fn []
(reset! record-button-at-initial-position? true))
800))]
(use-effect (fn []
(cond
@recording?
(start-animation)
(not @ready-to-lock?)
(stop-animation)))
[@recording?])
(use-effect (fn []
(if @ready-to-lock?
(start-x-y-animation)
(reset-x-y-animation)))
[@ready-to-lock?])
(use-effect (fn []
(if @ready-to-send?
(start-y-animation)
(reset-y-animation)))
[@ready-to-send?])
(use-effect (fn []
(if @ready-to-delete?
(start-x-animation)
(reset-x-animation)))
[@ready-to-delete?])
[reanimated/view
{:style (style/record-button-big-container translate-x translate-y opacity)
:pointer-events :none}
[:<>
(map-indexed
(fn [id animation]
^{:key id}
[animated-ring
{:scale (:scale animation)
:opacity (:opacity animation)
:color rings-color}])
animations)]
[rn/view {:style (style/record-button-big-body button-color)}
[reanimated/view {:style (style/record-button-big-red-overlay red-overlay-opacity)}]
[reanimated/view {:style (style/record-button-big-gray-overlay gray-overlay-opacity)}]
[reanimated/view {:style (style/record-button-big-icon-container icon-opacity)}
(if @locked?
[rn/view {:style style/stop-icon}]
[icons/icon :i/audio {:color icon-color}])]]]))])
@@ -0,0 +1,90 @@
(ns quo2.components.record-audio.record-audio.buttons.send-button
(:require [quo2.components.icon :as icons]
[quo2.components.record-audio.record-audio.style :as style]
[quo2.foundations.colors :as colors]
[react-native.reanimated :as reanimated]
[react-native.core :refer [use-effect]]
[quo2.components.record-audio.record-audio.helpers :refer
[animate-linear
animate-linear-with-delay
animate-easing-with-delay
set-value]]))
(defn send-button
[recording? ready-to-send? reviewing-audio?]
[:f>
(fn []
(let [opacity (reanimated/use-shared-value 0)
translate-y (reanimated/use-shared-value 20)
connector-opacity (reanimated/use-shared-value 0)
width (reanimated/use-shared-value 12)
height (reanimated/use-shared-value 24)
border-radius-first-half (reanimated/use-shared-value 16)
border-radius-second-half (reanimated/use-shared-value 8)
start-y-animation (fn []
(animate-linear-with-delay translate-y 12 50 133.33)
(animate-easing-with-delay connector-opacity 1 0 93.33)
(animate-easing-with-delay width 56 83.33 80)
(animate-easing-with-delay height 56 83.33 80)
(animate-easing-with-delay border-radius-first-half 28 83.33 80)
(animate-easing-with-delay border-radius-second-half 28 83.33 80))
reset-y-animation (fn []
(animate-linear translate-y 0 100)
(set-value connector-opacity 0)
(set-value width 12)
(set-value height 24)
(set-value border-radius-first-half 16)
(set-value border-radius-second-half 8))
fade-in-animation (fn []
(animate-linear translate-y 0 200)
(animate-linear opacity 1 200))
fade-out-animation (fn []
(animate-linear
translate-y
(if @reviewing-audio? 76 20)
200)
(when-not @reviewing-audio?
(animate-linear opacity 0 200))
(set-value connector-opacity 0)
(set-value width 24)
(set-value height 12)
(set-value border-radius-first-half 8)
(set-value border-radius-second-half 16))
fade-out-reset-animation (fn []
(animate-linear opacity 0 200)
(animate-linear-with-delay translate-y 20 0 200)
(set-value connector-opacity 0)
(set-value width 24)
(set-value height 12)
(set-value border-radius-first-half 8)
(set-value border-radius-second-half 16))]
(use-effect (fn []
(if @recording?
(fade-in-animation)
(fade-out-animation)))
[@recording?])
(use-effect (fn []
(when-not @reviewing-audio?
(fade-out-reset-animation)))
[@reviewing-audio?])
(use-effect (fn []
(cond
@ready-to-send?
(start-y-animation)
@recording? (reset-y-animation)))
[@ready-to-send?])
[:<>
[reanimated/view {:style (style/send-button-container opacity)}
[reanimated/view
{:style (style/send-button-connector connector-opacity
width
height
border-radius-first-half
border-radius-second-half)}]]
[reanimated/view
{:style (style/send-button translate-y opacity)
:pointer-events :none}
[icons/icon :i/arrow-up
{:color colors/white
:size 20
:container-style style/send-icon-container}]]]))])
@@ -0,0 +1,50 @@
(ns quo2.components.record-audio.record-audio.helpers
(:require [react-native.reanimated :as reanimated]))
(defn animate-linear
[shared-value value duration]
(reanimated/animate-shared-value-with-timing
shared-value
value
duration
:linear))
(defn animate-linear-with-delay
[shared-value value duration delay]
(reanimated/animate-shared-value-with-delay
shared-value
value
duration
:linear
delay))
(defn animate-linear-with-delay-loop
[shared-value value duration delay]
(reanimated/animate-shared-value-with-delay-repeat
shared-value
value
duration
:linear
delay
-1))
(defn animate-easing
[shared-value value duration]
(reanimated/animate-shared-value-with-timing
shared-value
value
duration
:easing1))
(defn animate-easing-with-delay
[shared-value value duration delay]
(reanimated/animate-shared-value-with-delay
shared-value
value
duration
:easing1
delay))
(defn set-value
[shared-value value]
(reanimated/set-shared-value shared-value value))
@@ -199,9 +199,10 @@
:z-index 0}))
(defn delete-button
[translate-x opacity]
[scale translate-x opacity]
(reanimated/apply-animations-to-style
{:transform [{:translateX translate-x}]
{:transform [{:translateX translate-x}
{:scale scale}]
:opacity opacity}
{:width 32
:height 32
@@ -221,8 +222,66 @@
{:margin-bottom 32
:margin-right 32}))
(def input-container
(def button-container
{:width 140
:height 140
:align-items :flex-end
:justify-content :flex-end})
:justify-content :flex-end
:position :absolute
:right -10})
(def bar-container
{:flex 1
:height 128})
(defn recording-bar-container
[]
{:height 4
:border-radius 2
:background-color (colors/theme-colors colors/neutral-20 colors/neutral-80)
:overflow :hidden
:position :absolute
:left 80
:right 148
:bottom 34})
(defn recording-bar
[fill-percentage ready-to-delete?]
{:width (str fill-percentage "%")
:height 4
:border-radius 2
:background-color (if ready-to-delete?
(colors/theme-colors colors/danger-50 colors/danger-60)
(colors/theme-colors colors/primary-50 colors/primary-60))})
(defn timer-container
[reviewing-audio?]
{:position :absolute
:left (if reviewing-audio? 67 20)
:bottom 28.5
:flex-direction :row
:align-items :center})
(defn timer-circle
[]
{:width 8
:height 8
:border-radius 4
:margin-right 6
:background-color (colors/theme-colors colors/danger-50 colors/danger-60)})
(defn timer-text
[]
{:color (colors/theme-colors colors/danger-50 colors/danger-60)})
(defn play-button
[]
{:position :absolute
:bottom 20
:left 20
:width 32
:height 32
:border-radius 16
:align-items :center
:justify-content :center
:background-color (colors/theme-colors colors/neutral-10 colors/neutral-90)})
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,82 @@
(ns quo2.components.record-audio.soundtrack.--tests--.soundtrack-component-spec
(:require [quo2.components.record-audio.soundtrack.view :as soundtrack]
[test-helpers.component :as h]
[reagent.core :as reagent]
[status-im.audio.core :as audio]))
(h/describe "soundtrack component"
(h/before-each
(fn []
(h/use-fake-timers)))
(h/after-each
(fn []
(h/clear-all-timers)
(h/use-real-timers)))
(h/test "renders soundtrack"
(with-redefs [audio/get-player-duration (fn [] 2000)]
(let [player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [soundtrack/soundtrack
{:player-ref player-ref
:audio-current-time-ms audio-current-time-ms}])
(-> (h/expect (h/get-by-test-id "soundtrack"))
(.toBeTruthy)))))
(h/test "soundtrack on-sliding-start works"
(with-redefs [audio/get-player-duration (fn [] 2000)]
(let [seeking-audio? (reagent/atom false)
player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [soundtrack/soundtrack
{:seeking-audio? seeking-audio?
:player-ref player-ref
:audio-current-time-ms audio-current-time-ms}])
(h/fire-event
:on-sliding-start
(h/get-by-test-id "soundtrack"))
(-> (h/expect @seeking-audio?)
(.toBe true)))))
(h/test "soundtrack on-sliding-complete works"
(with-redefs [audio/get-player-duration (fn [] 2000)
audio/seek-player (js/jest.fn)]
(let [seeking-audio? (reagent/atom false)
player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [soundtrack/soundtrack
{:seeking-audio? seeking-audio?
:player-ref player-ref
:audio-current-time-ms audio-current-time-ms}])
(h/fire-event
:on-sliding-start
(h/get-by-test-id "soundtrack"))
(h/fire-event
:on-sliding-complete
(h/get-by-test-id "soundtrack")
1000)
(-> (h/expect @seeking-audio?)
(.toBe false))
(-> (h/expect audio/seek-player)
(.toHaveBeenCalledTimes 1)))))
(h/test "soundtrack on-value-change when seeking audio works"
(with-redefs [audio/get-player-duration (fn [] 2000)
audio/seek-player (js/jest.fn)]
(let [seeking-audio? (reagent/atom false)
player-ref (reagent/atom {})
audio-current-time-ms (reagent/atom 0)]
(h/render [soundtrack/soundtrack
{:seeking-audio? seeking-audio?
:player-ref player-ref
:audio-current-time-ms audio-current-time-ms}])
(h/fire-event
:on-sliding-start
(h/get-by-test-id "soundtrack"))
(h/fire-event
:on-value-change
(h/get-by-test-id "soundtrack")
1000)
(-> (h/expect @audio-current-time-ms)
(.toBe 1000))))))
@@ -0,0 +1,16 @@
(ns quo2.components.record-audio.soundtrack.style
(:require [react-native.platform :as platform]))
(defn player-slider-container
[]
(merge
{:position :absolute
:left (if platform/ios? 115 104)
:right (if platform/ios? 108 92)
:bottom (if platform/ios? 16 27)}
(when platform/android?
;; Workaround to increase the thickness of the slider track on Android
;; which is currently not supported by the Slider library and remove
;; the thumb shadow that appears when dragging.
{:transform [{:scaleY 2}]
:background-color :transparent})))
@@ -0,0 +1,38 @@
(ns quo2.components.record-audio.soundtrack.view
(:require [quo2.components.record-audio.soundtrack.style :as style]
[quo2.foundations.colors :as colors]
[status-im.audio.core :as audio]
[taoensso.timbre :as log]
[react-native.platform :as platform]
[react-native.slider :as slider]))
(def ^:private thumb-light (js/require "../resources/images/icons2/12x12/thumb-light.png"))
(def ^:private thumb-dark (js/require "../resources/images/icons2/12x12/thumb-dark.png"))
(defn soundtrack
[{:keys [audio-current-time-ms player-ref seeking-audio?]}]
[:f>
(fn []
(let [audio-duration-ms (audio/get-player-duration @player-ref)]
[:<>
[slider/slider
{:test-ID "soundtrack"
:style (style/player-slider-container)
:minimum-value 0
:maximum-value audio-duration-ms
:value @audio-current-time-ms
:on-sliding-start #(reset! seeking-audio? true)
:on-sliding-complete (fn [seek-time]
(reset! seeking-audio? false)
(audio/seek-player
@player-ref
seek-time
#(log/debug "[record-audio] on seek - seek time: " seek-time)
#(log/error "[record-audio] on seek - error: " %)))
:on-value-change #(when @seeking-audio?
(reset! audio-current-time-ms %))
:thumb-image (if (colors/dark?) thumb-dark thumb-light)
:minimum-track-tint-color (colors/theme-colors colors/primary-50 colors/primary-60)
:maximum-track-tint-color (colors/theme-colors
(if platform/ios? colors/neutral-20 colors/neutral-40)
(if platform/ios? colors/neutral-80 colors/neutral-60))}]]))])
+1 -1
View File
@@ -64,7 +64,7 @@
[rn/image
{:style {:width 20
:border-radius 10
:background-color :red
:background-color :white
:height 20}
:source photo}]
[rn/view
+66 -64
View File
@@ -1,11 +1,12 @@
(ns quo2.components.tags.tags
(:require [reagent.core :as reagent]
[quo.react-native :as rn]
[oops.core :refer [oget]]
[status-im.ui.components.react :as react]
[status-im.utils.core :as utils]
[quo2.components.tags.tag :as tag]
[utils.number :as number-utils]))
[utils.number :as number-utils]
[react-native.core :as rn]
[react-native.masked-view :as masked-view]
[react-native.linear-gradient :as linear-gradient]
[utils.collection :as utils.collection]))
(def default-tab-size 32)
@@ -76,9 +77,9 @@
size default-tab-size}
:as props}]
(let [maybe-mask-wrapper (if fade-end?
[react/masked-view
[masked-view/masked-view
{:mask-element (reagent/as-element
[react/linear-gradient
[linear-gradient/linear-gradient
{:colors [:black :transparent]
:locations [(get @fading :fade-end-percentage)
1]
@@ -92,64 +93,65 @@
(conj
maybe-mask-wrapper
[rn/flat-list
(merge (dissoc props
:default-active
:fade-end-percentage
:fade-end?
:on-change
:scroll-on-press?
:size)
(when scroll-on-press?
{:initial-scroll-index (utils/first-index #(= @active-tab-id (:id %)) data)})
{:ref #(reset! flat-list-ref %)
:extra-data (str @active-tab-id)
:horizontal true
:scroll-event-throttle scroll-event-throttle
:shows-horizontal-scroll-indicator false
:data data
:key-fn (comp str :id)
:on-scroll (fn [^js e]
(when fade-end?
(let [offset-x (oget e "nativeEvent.contentOffset.x")
content-width (oget e "nativeEvent.contentSize.width")
layout-width (oget e "nativeEvent.layoutMeasurement.width")
new-percentage (calculate-fade-end-percentage
{:offset-x offset-x
:content-width content-width
:layout-width layout-width
:max-fade-percentage fade-end-percentage})]
;; Avoid unnecessary re-rendering.
(when (not= new-percentage (get @fading :fade-end-percentage))
(swap! fading assoc :fade-end-percentage new-percentage))))
(when on-scroll
(on-scroll e)))
:render-fn (fn [{:keys [id label resource]} index]
[rn/view
{:style {:margin-right (if (= size default-tab-size) 12 8)
:padding-right (when (= index (dec (count data)))
(get-in props [:style :padding-left]))}}
[tag/tag
{:id id
:size size
:active (= id @active-tab-id)
:resource resource
:blurred? blurred?
:icon-color icon-color
:disabled? disabled?
:label label
:type type
:labelled? labelled?
:on-press (fn [id]
(reset! active-tab-id id)
(when scroll-on-press?
(.scrollToIndex ^js @flat-list-ref
#js
{:animated true
:index index
:viewPosition 0.5}))
(when on-change
(on-change id)))}
label]])})])
(merge
(dissoc props
:default-active
:fade-end-percentage
:fade-end?
:on-change
:scroll-on-press?
:size)
(when scroll-on-press?
{:initial-scroll-index (utils.collection/first-index #(= @active-tab-id (:id %)) data)})
{:ref #(reset! flat-list-ref %)
:extra-data (str @active-tab-id)
:horizontal true
:scroll-event-throttle scroll-event-throttle
:shows-horizontal-scroll-indicator false
:data data
:key-fn (comp str :id)
:on-scroll (fn [^js e]
(when fade-end?
(let [offset-x (oget e "nativeEvent.contentOffset.x")
content-width (oget e "nativeEvent.contentSize.width")
layout-width (oget e "nativeEvent.layoutMeasurement.width")
new-percentage (calculate-fade-end-percentage
{:offset-x offset-x
:content-width content-width
:layout-width layout-width
:max-fade-percentage fade-end-percentage})]
;; Avoid unnecessary re-rendering.
(when (not= new-percentage (get @fading :fade-end-percentage))
(swap! fading assoc :fade-end-percentage new-percentage))))
(when on-scroll
(on-scroll e)))
:render-fn (fn [{:keys [id label resource]} index]
[rn/view
{:style {:margin-right (if (= size default-tab-size) 12 8)
:padding-right (when (= index (dec (count data)))
(get-in props [:style :padding-left]))}}
[tag/tag
{:id id
:size size
:active (= id @active-tab-id)
:resource resource
:blurred? blurred?
:icon-color icon-color
:disabled? disabled?
:label label
:type type
:labelled? labelled?
:on-press (fn [id]
(reset! active-tab-id id)
(when scroll-on-press?
(.scrollToIndex ^js @flat-list-ref
#js
{:animated true
:index index
:viewPosition 0.5}))
(when on-change
(on-change id)))}
label]])})])
[rn/view {:style {:flex-direction :row}}
(for [{:keys [label id resource]} data]
^{:key id}
+18 -6
View File
@@ -20,7 +20,8 @@
quo2.components.dividers.date
quo2.components.dividers.divider-label
quo2.components.dividers.new-messages
quo2.components.drawers.action-drawers
quo2.components.drawers.action-drawers.view
quo2.components.drawers.permission-context.view
quo2.components.dropdowns.dropdown
quo2.components.header
quo2.components.icon
@@ -41,6 +42,7 @@
quo2.components.notifications.info-count
quo2.components.notifications.notification-dot
quo2.components.notifications.toast
quo2.components.profile.profile-card.view
quo2.components.reactions.reaction
quo2.components.selectors.disclaimer.view
quo2.components.selectors.filter.view
@@ -52,8 +54,10 @@
quo2.components.tabs.tabs
quo2.components.tags.context-tags
quo2.components.tags.status-tags
quo2.components.profile.profile-card.view
quo2.components.tags.tags))
quo2.components.tags.permission-tag
quo2.components.tags.tag
quo2.components.tags.tags
quo2.components.tags.token-tag))
(def toast quo2.components.notifications.toast/toast)
(def button quo2.components.buttons.button/button)
@@ -63,7 +67,6 @@
(def separator quo2.components.separator/separator)
(def counter quo2.components.counter.counter/counter)
(def header quo2.components.header/header)
(def action-drawer quo2.components.drawers.action-drawers/action-drawer)
(def dropdown quo2.components.dropdowns.dropdown/dropdown)
(def info-message quo2.components.info.info-message/info-message)
(def information-box quo2.components.info.information-box/information-box)
@@ -71,7 +74,6 @@
(def system-message quo2.components.messages.system-message/system-message)
(def reaction quo2.components.reactions.reaction/reaction)
(def add-reaction quo2.components.reactions.reaction/add-reaction)
(def tags quo2.components.tags.tags/tags)
(def user-avatar-tag quo2.components.tags.context-tags/user-avatar-tag)
(def context-tag quo2.components.tags.context-tags/context-tag)
(def group-avatar-tag quo2.components.tags.context-tags/group-avatar-tag)
@@ -80,7 +82,6 @@
(def tabs quo2.components.tabs.tabs/tabs)
(def account-selector quo2.components.tabs.account-selector/account-selector)
(def floating-shell-button quo2.components.navigation.floating-shell-button/floating-shell-button)
(def status-tag quo2.components.tags.status-tags/status-tag)
(def page-nav quo2.components.navigation.page-nav/page-nav)
(def disclaimer quo2.components.selectors.disclaimer.view/view)
(def checkbox quo2.components.selectors.selectors/checkbox)
@@ -118,6 +119,10 @@
(def new-messages quo2.components.dividers.new-messages/new-messages)
(def divider-date quo2.components.dividers.date/date)
;;;; DRAWERS
(def action-drawer quo2.components.drawers.action-drawers.view/action-drawer)
(def permission-context quo2.components.drawers.permission-context.view/view)
;;;; LIST ITEMS
(def channel-list-item quo2.components.list-items.channel/list-item)
(def menu-item quo2.components.list-items.menu-item/menu-item)
@@ -135,3 +140,10 @@
;;;; SETTINGS
(def privacy-option quo2.components.settings.privacy-option/card)
(def account quo2.components.settings.accounts.view/account)
;;;; TAGS
(def tag quo2.components.tags.tag/tag)
(def tags quo2.components.tags.tags/tags)
(def permission-tag quo2.components.tags.permission-tag/tag)
(def status-tag quo2.components.tags.status-tags/status-tag)
(def token-tag quo2.components.tags.token-tag/tag)
+5 -3
View File
@@ -3,8 +3,10 @@
[quo2.components.buttons.--tests--.buttons-component-spec]
[quo2.components.counter.--tests--.counter-component-spec]
[quo2.components.dividers.--tests--.divider-label-component-spec]
[quo2.components.drawers.--tests--.action-drawers-component-spec]
[quo2.components.drawers.permission-context.--tests--.permission-context-component-spec]
[quo2.components.drawers.action-drawers.component-spec]
[quo2.components.drawers.permission-context.component-spec]
[quo2.components.markdown.--tests--.text-component-spec]
[quo2.components.selectors.--tests--.selectors-component-spec]
[quo2.components.selectors.filter.component-spec]))
[quo2.components.selectors.filter.component-spec]
[quo2.components.record-audio.record-audio.--tests--.record-audio-component-spec]
[quo2.components.record-audio.soundtrack.--tests--.soundtrack-component-spec]))
+71
View File
@@ -0,0 +1,71 @@
(ns quo2.foundations.shadows
(:require [quo2.foundations.colors :as colors]
[quo2.theme :as theme]))
(defn- get-inverted
[inverted? number]
(if inverted? (* -1 number) number))
(defn- get-scales
[inverted?]
(if (theme/dark?)
{:shadow-1 {:shadow-color (colors/alpha colors/neutral-100 0.5)
:shadow-offset {:width 0
:height (get-inverted inverted? 4)}
:elevation 3
:shadow-opacity 1
:shadow-radius 20}
:shadow-2 {:shadow-color (colors/alpha colors/neutral-100 0.64)
:shadow-offset {:width 0
:height (get-inverted inverted? 4)}
:elevation 4
:shadow-opacity 1
:shadow-radius 20}
:shadow-3 {:shadow-color (colors/alpha colors/neutral-100 0.64)
:shadow-offset {:width 0
:height (get-inverted inverted? 12)}
:elevation 8
:shadow-opacity 1
:shadow-radius 20}
:shadow-4 {:shadow-color (colors/alpha colors/neutral-100 0.72)
:shadow-offset {:width 0
:height (get-inverted inverted? 16)}
:shadow-opacity 1
:shadow-radius 20
:elevation 15}}
{:shadow-1 {:shadow-color (colors/alpha colors/neutral-100 0.04)
:shadow-offset {:width 0
:height (get-inverted inverted? 4)}
:elevation 1
:shadow-opacity 1
:shadow-radius 16}
:shadow-2 {:shadow-color (colors/alpha colors/neutral-100 0.08)
:shadow-offset {:width 0
:height (get-inverted inverted? 4)}
:elevation 2
:shadow-opacity 1
:shadow-radius 16}
:shadow-3 {:shadow-color (colors/alpha colors/neutral-100 0.12)
:shadow-offset {:width 0
:height (get-inverted inverted? 12)}
:elevation 5
:shadow-opacity 1
:shadow-radius 16}
:shadow-4 {:shadow-color (colors/alpha colors/neutral-100 0.16)
:shadow-offset {:width 0
:height (get-inverted inverted? 16)}
:shadow-opacity 1
:shadow-radius 16
:elevation 13}}))
(def normal-scale (get-scales false))
(def inverted-scale (get-scales true))
(def inner-shadow
{:shadow-color (colors/alpha colors/neutral-100 0.08)
:shadow-offset {:width 0
:height 0}
:shadow-opacity 1
:shadow-radius 16
:elevation 13})
+10
View File
@@ -0,0 +1,10 @@
(ns react-native.clipboard
(:require ["@react-native-community/clipboard" :default Clipboard]))
(defn set-string
[text]
(.setString ^js Clipboard text))
(defn get-string
[callback]
(.then (.getString ^js Clipboard) #(callback %)))
+15 -3
View File
@@ -94,11 +94,12 @@
(defn use-effect
([effect-fn]
(use-effect effect-fn []))
([effect-fn deps]
(react/useEffect
#(let [ret (effect-fn)]
(if (fn? ret) ret js/undefined))))
([effect-fn deps]
(react/useEffect effect-fn (bean/->js deps))))
(if (fn? ret) ret js/undefined))
(bean/->js deps))))
(defn use-effect-once
[effect-fn]
@@ -117,3 +118,14 @@
{:ease-in-ease-out (-> ^js layout-animation .-Presets .-easeInEaseOut)
:linear (-> ^js layout-animation .-Presets .-linear)
:spring (-> ^js layout-animation .-Presets .-spring)})
(def find-node-handle (.-findNodeHandle ^js react-native))
(defn selectable-text-input-manager
[]
(when (exists? (.-NativeModules ^js react-native))
(.-RNSelectableTextInputManager ^js (.-NativeModules ^js react-native))))
(defonce selectable-text-input
(reagent/adapt-react-class
(.requireNativeComponent ^js react-native "RNSelectableTextInput")))
+40
View File
@@ -0,0 +1,40 @@
(ns react-native.permissions
(:require ["react-native-permissions" :refer (check requestMultiple PERMISSIONS RESULTS)]
[react-native.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)))))
(defn permission-granted?
[permission on-result on-error]
(-> (check (get permissions-map permission))
(.then #(on-result (not (#{(.-BLOCKED RESULTS) (.-DENIED RESULTS)} %))))
(.catch #(on-error %))))
+13 -16
View File
@@ -1,6 +1,7 @@
(ns react-native.reanimated
(:require ["react-native" :as rn]
["react-native-linear-gradient" :default LinearGradient]
["@react-native-community/blur" :as blur]
["react-native-reanimated" :default reanimated :refer
(useSharedValue useAnimatedStyle
withTiming
@@ -13,7 +14,6 @@
SlideInUp
SlideOutUp
LinearTransition)]
[camel-snake-kebab.core :as csk]
[reagent.core :as reagent]
[utils.collection]))
@@ -30,6 +30,7 @@
(def touchable-opacity (create-animated-component (.-TouchableOpacity ^js rn)))
(def linear-gradient (create-animated-component LinearGradient))
(def blur-view (create-animated-component (.-BlurView blur)))
;; Hooks
(def use-shared-value useSharedValue)
@@ -60,31 +61,27 @@
(defn set-shared-value
[anim val]
(when anim
(when (and anim (some? val))
(set! (.-value anim) val)))
;; Worklets
(def worklet-factory (js/require "../src/js/worklet_factory.js"))
(defn interpolate
[shared-value input-range output-range]
(.interpolateValue ^js worklet-factory
shared-value
(clj->js input-range)
(clj->js output-range)))
([shared-value input-range output-range]
(interpolate shared-value input-range output-range nil))
([shared-value input-range output-range extrapolation]
(.interpolateValue ^js worklet-factory
shared-value
(clj->js input-range)
(clj->js output-range)
(clj->js extrapolation))))
;;;; Component Animations
;; kebab-case styles are not working for worklets
;; so first convert kebab case styles into camel case styles
(defn apply-animations-to-style
[animations style]
(let [animations (utils.collection/map-keys csk/->camelCaseString animations)
style (apply dissoc
(utils.collection/map-keys csk/->camelCaseString style)
(keys animations))]
(use-animated-style
(.applyAnimationsToStyle ^js worklet-factory (clj->js animations) (clj->js style)))))
(use-animated-style
(.applyAnimationsToStyle ^js worklet-factory (clj->js animations) (clj->js style))))
;; Animators
(defn animate-shared-value-with-timing
+5
View File
@@ -0,0 +1,5 @@
(ns react-native.slider
(:require ["@react-native-community/slider" :default Slider]
[reagent.core :as reagent]))
(def slider (reagent/adapt-react-class Slider))
View File
+2 -2
View File
@@ -2,7 +2,7 @@
(:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.add-new.db :as db]
[status-im.chat.models :as chat]
[status-im2.contexts.chat.events :as chat]
[status-im.contact.core :as contact]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.ens :as ens]
@@ -91,7 +91,7 @@
(i18n/label :t/use-valid-contact-code)
:yourself
(i18n/label :t/can-not-add-yourself))
:on-dismiss #(re-frame/dispatch [:pop-to-root-tab :chat-stack])}})))
:on-dismiss #(re-frame/dispatch [:pop-to-root-tab :shell-stack])}})))
(rf/defn qr-code-scanned
{:events [:contact/qr-code-scanned]}
+3 -3
View File
@@ -1,7 +1,7 @@
(ns status-im.add-new.db
(:require [cljs.spec.alpha :as spec]
[status-im.ethereum.ens :as ens]
[status-im.utils.db :as utils.db]))
[status-im2.utils.validators :as validators]))
(defn own-public-key?
[{:keys [multiaccount]} public-key]
@@ -10,7 +10,7 @@
(defn validate-pub-key
[db public-key]
(cond
(or (not (utils.db/valid-public-key? public-key))
(or (not (validators/valid-public-key? public-key))
(= public-key ens/default-key))
:invalid
(own-public-key? db public-key)
@@ -27,4 +27,4 @@
[topic]
(and topic
(spec/valid? ::topic topic)
(not (utils.db/valid-public-key? topic))))
(not (validators/valid-public-key? topic))))
-95
View File
@@ -1,95 +0,0 @@
(ns status-im.chat.db
(:require [status-im2.constants :as constants]))
(defn group-chat-name
[{:keys [public? name]}]
(str (when public? "#") name))
(defn intersperse-datemark
"Reduce step which expects the input list of messages to be sorted by clock value.
It makes best effort to group them by day.
We cannot sort them by :timestamp, as that represents the clock of the sender
and we have no guarantees on the order.
We naively and arbitrarly group them assuming that out-of-order timestamps
fall in the previous bucket.
A sends M1 to B with timestamp 2000-01-01T00:00:00
B replies M2 with timestamp 1999-12-31-23:59:59
M1 needs to be displayed before M2
so we bucket both in 1999-12-31"
[{:keys [acc last-timestamp last-datemark]} {:keys [whisper-timestamp datemark] :as msg}]
(cond
(empty? acc) ; initial element
{:last-timestamp whisper-timestamp
:last-datemark datemark
:acc (conj acc msg)}
(and (not= last-datemark datemark) ; not the same day
(< whisper-timestamp last-timestamp)) ; not out-of-order
{:last-timestamp whisper-timestamp
:last-datemark datemark
:acc (conj acc
{:value last-datemark ; intersperse datemark message
:type :datemark}
msg)}
:else
{:last-timestamp (min whisper-timestamp last-timestamp) ; use last datemark
:last-datemark last-datemark
:acc (conj acc (assoc msg :datemark last-datemark))}))
(defn add-datemarks
"Add a datemark in between an ordered seq of messages when two datemarks are not
the same. Ignore messages with out-of-order timestamps"
[messages]
(when (seq messages)
(let [messages-with-datemarks (:acc (reduce intersperse-datemark {:acc []} messages))]
; Append last datemark
(conj messages-with-datemarks
{:value (:datemark (peek messages-with-datemarks))
:type :datemark}))))
(defn last-gap
"last-gap is a special gap that is put last in the message stream"
[chat-id synced-from]
{:message-id "0x123"
:message-type constants/message-type-gap
:chat-id chat-id
:content-type constants/content-type-gap
:gap-ids #{:first-gap}
:gap-parameters {:from synced-from}})
(defn collapse-gaps
"collapse-gaps will take an array of messages and collapse any gap next to
each other in a single gap.
It will also append one last gap if the last message is a non-gap"
[messages chat-id synced-from now chat-type joined loading-messages?]
(let [messages-with-gaps (reduce
(fn [acc {:keys [gap-parameters message-id] :as message}]
(let [last-element (peek acc)]
(cond
;; If it's a message, just add
(empty? gap-parameters)
(conj acc message)
;; Both are gaps, merge them
(and
(seq (:gap-parameters last-element))
(seq gap-parameters))
(conj (pop acc) (update last-element :gap-ids conj message-id))
;; it's a gap
:else
(conj acc (assoc message :gap-ids #{message-id})))))
[]
messages)]
(if (or loading-messages? ; it's loading messages from the database
(nil? synced-from) ; it's still syncing
(= constants/timeline-chat-type chat-type) ; it's a timeline chat
(= constants/profile-chat-type chat-type) ; it's a profile chat
(and (not (nil? synced-from)) ; it's not more than a month
(<= synced-from (- (quot now 1000) constants/one-month)))
(and (= constants/private-group-chat-type chat-type) ; it's a private group chat
(or (not (pos? joined)) ; we haven't joined
(>= (quot joined 1000) synced-from))) ; the history goes before we joined
(:gap-ids (peek messages-with-gaps))) ; there's already a gap on top of the chat history
messages-with-gaps ; don't add an extra gap
(conj messages-with-gaps (last-gap chat-id synced-from)))))
-44
View File
@@ -1,44 +0,0 @@
(ns status-im.chat.db-test
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im.chat.db :as db]))
(deftest group-chat-name
(testing "it prepends # if it's a public chat"
(is (= "#withhash"
(db/group-chat-name {:group-chat true
:chat-id "1"
:public? true
:name "withhash"}))))
(testing "it leaves the name unchanged if it's a group chat"
(is (= "unchanged"
(db/group-chat-name {:group-chat true
:chat-id "1"
:name "unchanged"})))))
(deftest intersperse-datemarks
(testing "it mantains the order even when timestamps are across days"
(let [message-1 {:datemark "Dec 31, 1999"
:whisper-timestamp 946641600000} ; 1999}
message-2 {:datemark "Jan 1, 2000"
:whisper-timestamp 946728000000} ; 2000 this will displayed in 1999
message-3 {:datemark "Dec 31, 1999"
:whisper-timestamp 946641600000} ; 1999
message-4 {:datemark "Jan 1, 2000"
:whisper-timestamp 946728000000} ; 2000
ordered-messages [message-4
message-3
message-2
message-1]
[m1 d1 m2 m3 m4 d2] (db/add-datemarks ordered-messages)]
(is (= "Jan 1, 2000"
(:datemark m1)))
(is (= {:type :datemark
:value "Jan 1, 2000"}
d1))
(is (= "Dec 31, 1999"
(:datemark m2)
(:datemark m3)
(:datemark m4)))
(is (= {:type :datemark
:value "Dec 31, 1999"}
d2)))))
+9 -507
View File
@@ -1,344 +1,12 @@
(ns status-im.chat.models
(:require [clojure.set :as set]
[quo.design-system.colors :as colors]
(:require [utils.i18n :as i18n]
[re-frame.core :as re-frame]
[status-im.add-new.db :as new-public-chat.db]
[status-im.chat.models.loading :as loading]
[status-im2.contexts.chat.messages.list.events :as message-list]
[status-im2.constants :as constants]
[status-im.data-store.chats :as chats-store]
[status-im.data-store.contacts :as contacts-store]
[utils.i18n :as i18n]
[status-im.mailserver.core :as mailserver]
[status-im.multiaccounts.model :as multiaccounts.model]
[status-im2.contexts.chat.messages.list.state :as chat.state]
[status-im.utils.clocks :as utils.clocks]
[utils.re-frame :as rf]
[status-im.utils.utils :as utils]
[status-im2.contexts.chat.messages.delete-message-for-me.events :as delete-for-me]
[status-im2.contexts.chat.messages.delete-message.events :as delete-message]
[status-im2.navigation.events :as navigation]
[taoensso.timbre :as log]))
[taoensso.timbre :as log]
[status-im.add-new.db :as new-public-chat.db]
[status-im.data-store.chats :as chats-store]))
(defn- get-chat
[cofx chat-id]
(get-in cofx [:db :chats chat-id]))
(defn multi-user-chat?
([chat]
(:group-chat chat))
([cofx chat-id]
(multi-user-chat? (get-chat cofx chat-id))))
(def one-to-one-chat?
(complement multi-user-chat?))
(defn public-chat?
([chat]
(:public? chat))
([cofx chat-id]
(public-chat? (get-chat cofx chat-id))))
(defn community-chat?
([{:keys [chat-type]}]
(= chat-type constants/community-chat-type))
([cofx chat-id]
(community-chat? (get-chat cofx chat-id))))
(defn active-chat?
[cofx chat-id]
(let [chat (get-chat cofx chat-id)]
(:active chat)))
(defn foreground-chat?
[{{:keys [current-chat-id view-id]} :db} chat-id]
(and (= current-chat-id chat-id)
(= view-id :chat)))
(defn group-chat?
([chat]
(and (multi-user-chat? chat)
(not (public-chat? chat))))
([cofx chat-id]
(group-chat? (get-chat cofx chat-id))))
(defn timeline-chat?
([chat]
(:timeline? chat))
([cofx chat-id]
(timeline-chat? (get-chat cofx chat-id))))
(defn profile-chat?
([chat]
(:profile-public-key chat))
([cofx chat-id]
(profile-chat? (get-chat cofx chat-id))))
(defn set-chat-ui-props
"Updates ui-props in active chat by merging provided kvs into them"
[{:keys [current-chat-id] :as db} kvs]
(update-in db [:chat-ui-props current-chat-id] merge kvs))
(defn- create-new-chat
[chat-id {:keys [db now]}]
(let [name (get-in db [:contacts/contacts chat-id :name])]
{:chat-id chat-id
:name (or name "")
:color (rand-nth colors/chat-colors)
:chat-type constants/one-to-one-chat-type
:group-chat false
:timestamp now
:contacts #{chat-id}
:last-clock-value 0}))
(defn map-chats
[{:keys [db] :as cofx}]
(fn [val]
(assoc
(merge
(or (get (:chats db) (:chat-id val))
(create-new-chat (:chat-id val) cofx))
val)
:invitation-admin
(:invitation-admin val))))
(defn filter-chats
[db]
(fn [val]
(and (not (get-in db [:chats (:chat-id val)])) (:public? val))))
(rf/defn leave-removed-chat
[{{:keys [view-id current-chat-id chats]} :db
:as cofx}]
(when (and (= view-id :chat)
(not (contains? chats current-chat-id)))
(navigation/navigate-back cofx)))
(rf/defn ensure-chats
"Add chats to db and update"
[{:keys [db] :as cofx} chats]
(let [{:keys [all-chats chats-home-list removed-chats]}
(reduce
(fn [acc {:keys [chat-id profile-public-key timeline? community-id active muted] :as chat}]
(if (not (or active muted))
(update acc :removed-chats conj chat-id)
(cond-> acc
(and (not profile-public-key) (not timeline?) (not community-id) active)
(update :chats-home-list conj chat-id)
:always
(assoc-in [:all-chats chat-id] chat))))
{:all-chats {}
:chats-home-list #{}
:removed-chats #{}}
(map (map-chats cofx) chats))]
(rf/merge
cofx
(merge {:db (-> db
(update :chats merge all-chats)
(update :chats-home-list set/union chats-home-list)
(update :chats #(apply dissoc % removed-chats))
(update :chats-home-list set/difference removed-chats))}
(when (not-empty removed-chats)
{:clear-message-notifications
[removed-chats
(get-in db [:multiaccount :remote-push-notifications-enabled?])]}))
leave-removed-chat)))
(rf/defn clear-history
"Clears history of the particular chat"
[{:keys [db] :as cofx} chat-id remove-chat?]
(let [{:keys [last-message public?
deleted-at-clock-value]}
(get-in db [:chats chat-id])
last-message-clock-value (if (and public? remove-chat?)
0
(or (:clock-value last-message)
deleted-at-clock-value
(utils.clocks/send 0)))]
{:db (-> db
(assoc-in [:messages chat-id] {})
(update-in [:message-lists] dissoc chat-id)
(update :chats
(fn [chats]
(if (contains? chats chat-id)
(update chats
chat-id
merge
{:last-message nil
:unviewed-messages-count 0
:unviewed-mentions-count 0
:deleted-at-clock-value last-message-clock-value})
chats))))}))
(rf/defn clear-history-handler
"Clears history of the particular chat"
{:events [:chat.ui/clear-history]}
[{:keys [db] :as cofx} chat-id remove-chat?]
(rf/merge cofx
{:db db
:json-rpc/call [{:method "wakuext_clearHistory"
:params [{:id chat-id}]
:on-success #(re-frame/dispatch [::history-cleared chat-id %])
:on-error #(log/error "failed to clear history " chat-id %)}]}
(clear-history chat-id remove-chat?)))
(rf/defn chat-deactivated
{:events [::chat-deactivated]}
[_ chat-id]
(log/debug "chat deactivated" chat-id))
(rf/defn deactivate-chat
"Deactivate chat in db, no side effects"
[{:keys [db now] :as cofx} chat-id]
(rf/merge
cofx
{:db (-> (if (get-in db [:chats chat-id :muted])
(assoc-in db [:chats chat-id :active] false)
(update db :chats dissoc chat-id))
(update :chats-home-list disj chat-id)
(assoc :current-chat-id nil))
:json-rpc/call [{:method "wakuext_deactivateChat"
:params [{:id chat-id}]
:on-success #(re-frame/dispatch [::chat-deactivated chat-id])
:on-error #(log/error "failed to create public chat" chat-id %)}]}
(clear-history chat-id true)))
(rf/defn offload-messages
{:events [:offload-messages]}
[{:keys [db]} chat-id]
(merge {:db (-> db
(update :messages dissoc chat-id)
(update :message-lists dissoc chat-id)
(update :pagination-info dissoc chat-id))}
(when (and (= chat-id constants/timeline-chat-id) (= (:view-id db) :status))
{:dispatch [:init-timeline-chat]})))
(rf/defn close-chat
{:events [:close-chat]}
[{:keys [db] :as cofx} navigate-to-shell?]
(when-let [chat-id (:current-chat-id db)]
(chat.state/reset-visible-item)
(rf/merge cofx
(merge
{:db (dissoc db :current-chat-id)}
(let [community-id (get-in db [:chats chat-id :community-id])]
;; When navigating back from community chat to community, update switcher card
(when (and community-id (not navigate-to-shell?))
{:dispatch [:shell/add-switcher-card
:community {:community-id community-id}]})))
(delete-for-me/sync-all)
(delete-message/send-all)
(offload-messages chat-id))))
(rf/defn force-close-chat
[{:keys [db] :as cofx} chat-id]
(do
(chat.state/reset-visible-item)
(rf/merge cofx
{:db (dissoc db :current-chat-id)}
(offload-messages chat-id))))
(rf/defn remove-chat
"Removes chat completely from app, producing all necessary effects for that"
{:events [:chat.ui/remove-chat]}
[{:keys [db now] :as cofx} chat-id]
(rf/merge cofx
{:clear-message-notifications
[[chat-id] (get-in db [:multiaccount :remote-push-notifications-enabled?])]
:dispatch [:shell/close-switcher-card chat-id]}
(deactivate-chat chat-id)
(offload-messages chat-id)))
(rf/defn show-more-chats
{:events [:chat.ui/show-more-chats]}
[{:keys [db]}]
(when (< (:home-items-show-number db) (count (:chats db)))
{:db (update db :home-items-show-number + 40)}))
(rf/defn preload-chat-data
"Takes chat-id and coeffects map, returns effects necessary when navigating to chat"
{:events [:chat.ui/preload-chat-data]}
[cofx chat-id]
(loading/load-messages cofx chat-id))
(rf/defn navigate-to-chat
"Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data"
{:events [:chat.ui/navigate-to-chat]}
[{db :db :as cofx} chat-id]
(rf/merge cofx
{:dispatch [:navigate-to :chat]}
(navigation/change-tab :chat)
(when-not (= (:view-id db) :community)
(navigation/pop-to-root-tab :chat-stack))
(close-chat false)
(force-close-chat chat-id)
(fn [{:keys [db]}]
{:db (assoc db :current-chat-id chat-id)})
(preload-chat-data chat-id)
#(when (group-chat? cofx chat-id)
(loading/load-chat % chat-id))))
(rf/defn navigate-to-chat-nav2
"Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data"
{:events [:chat.ui/navigate-to-chat-nav2]}
[{db :db :as cofx} chat-id from-shell?]
(rf/merge cofx
{:dispatch [:navigate-to-nav2 :chat chat-id from-shell?]}
(when-not (= (:view-id db) :community)
(navigation/pop-to-root-tab :shell-stack))
(close-chat false)
(force-close-chat chat-id)
(fn [{:keys [db]}]
{:db (assoc db :current-chat-id chat-id)})
(preload-chat-data chat-id)
#(when (group-chat? cofx chat-id)
(loading/load-chat % chat-id))))
(rf/defn handle-clear-history-response
{:events [::history-cleared]}
[{:keys [db]} chat-id response]
(let [chat (chats-store/<-rpc (first (:chats response)))]
{:db (assoc-in db [:chats chat-id] chat)}))
(rf/defn handle-one-to-one-chat-created
{:events [::one-to-one-chat-created]}
[{:keys [db]} chat-id response]
(let [chat (chats-store/<-rpc (first (:chats response)))
contact-rpc (first (:contacts response))
contact (when contact-rpc (contacts-store/<-rpc contact-rpc))]
{:db (cond-> db
contact
(assoc-in [:contacts/contacts chat-id] contact)
:always
(assoc-in [:chats chat-id] chat)
:always
(update :chats-home-list conj chat-id))
:dispatch [:chat.ui/navigate-to-chat-nav2 chat-id]}))
(rf/defn navigate-to-user-pinned-messages
"Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data"
{:events [:chat.ui/navigate-to-pinned-messages]}
[cofx chat-id]
(navigation/navigate-to cofx :chat-pinned-messages {:chat-id chat-id}))
(rf/defn start-chat
"Start a chat, making sure it exists"
{:events [:chat.ui/start-chat]}
[{:keys [db] :as cofx} chat-id ens-name]
;; don't allow to open chat with yourself
(when (not= (multiaccounts.model/current-public-key cofx) chat-id)
{:json-rpc/call [{:method "wakuext_createOneToOneChat"
:params [{:id chat-id :ensName ens-name}]
:on-success #(re-frame/dispatch [::one-to-one-chat-created chat-id %])
:on-error #(log/error "failed to create one-to-on chat" chat-id %)}]}))
(defn profile-chat-topic
[public-key]
(str "@" public-key))
(defn my-profile-chat-topic
[db]
(profile-chat-topic (get-in db [:multiaccount :public-key])))
;; OLD
(rf/defn handle-public-chat-created
{:events [::public-chat-created]}
@@ -346,7 +14,7 @@
{:db (-> db
(assoc-in [:chats chat-id] (chats-store/<-rpc (first (:chats response))))
(update :chats-home-list conj chat-id))
:dispatch [:chat.ui/navigate-to-chat chat-id]})
:dispatch [:chat/navigate-to-chat chat-id]})
(rf/defn create-public-chat-go
[_ chat-id]
@@ -360,174 +28,8 @@
{:events [:chat.ui/start-public-chat]}
[cofx topic]
(if (new-public-chat.db/valid-topic? topic)
(if (active-chat? cofx topic)
(navigate-to-chat cofx topic)
(create-public-chat-go
cofx
topic))
(create-public-chat-go
cofx
topic)
{:utils/show-popup {:title (i18n/label :t/cant-open-public-chat)
:content (i18n/label :t/invalid-public-chat-topic)}}))
(rf/defn profile-chat-created
{:events [::profile-chat-created]}
[{:keys [db] :as cofx} chat-id response navigate-to?]
(rf/merge
cofx
{:db db}
#(when response
(let [chat (chats-store/<-rpc (first (:chats response)))]
{:db (assoc-in db [:chats chat-id] chat)}))
#(when navigate-to?
{:dispatch-n [[:chat.ui/preload-chat-data chat-id]
[:open-modal :profile]]})))
(rf/defn start-profile-chat
"Starts a new profile chat"
{:events [:start-profile-chat]}
[cofx profile-public-key navigate-to?]
(let [chat-id (profile-chat-topic profile-public-key)]
(if (active-chat? cofx chat-id)
{:dispatch [::profile-chat-created chat-id nil navigate-to?]}
{:json-rpc/call [{:method "wakuext_createProfileChat"
:params [{:id profile-public-key}]
:on-success #(re-frame/dispatch [::profile-chat-created chat-id % navigate-to?])
:on-error #(log/error "failed to create profile chat" chat-id %)}]})))
(rf/defn disable-chat-cooldown
"Turns off chat cooldown (protection against message spamming)"
{:events [:chat/disable-cooldown]}
[{:keys [db]}]
{:db (assoc db :chat/cooldown-enabled? false)})
;; effects
(re-frame/reg-fx
:show-cooldown-warning
(fn [_]
(utils/show-popup nil
(i18n/label :cooldown/warning-message)
#())))
(rf/defn mute-chat-failed
{:events [::mute-chat-failed]}
[{:keys [db] :as cofx} chat-id muted? error]
(log/error "mute chat failed" chat-id error)
{:db (assoc-in db [:chats chat-id :muted] (not muted?))})
(rf/defn mute-chat-toggled-successfully
{:events [::mute-chat-toggled-successfully]}
[_ chat-id]
(log/debug "muted chat successfully" chat-id))
(rf/defn mute-chat
{:events [::mute-chat-toggled]}
[{:keys [db] :as cofx} chat-id muted?]
(let [method (if muted? "wakuext_muteChat" "wakuext_unmuteChat")]
{:db (assoc-in db [:chats chat-id :muted] muted?)
:json-rpc/call [{:method method
:params [chat-id]
:on-error #(re-frame/dispatch [::mute-chat-failed chat-id muted? %])
:on-success #(re-frame/dispatch [::mute-chat-toggled-successfully chat-id])}]}))
(rf/defn show-profile
{:events [:chat.ui/show-profile]}
[{:keys [db] :as cofx} identity ens-name]
(let [my-public-key (get-in db [:multiaccount :public-key])]
(when (not= my-public-key identity)
(rf/merge
cofx
{:db (-> db
(assoc :contacts/identity identity)
(assoc :contacts/ens-name ens-name))}
(start-profile-chat identity true)))))
(rf/defn clear-history-pressed
{:events [:chat.ui/clear-history-pressed]}
[_ chat-id]
{:ui/show-confirmation
{:title (i18n/label :t/clear-history-title)
:content (i18n/label :t/clear-history-confirmation-content)
:confirm-button-text (i18n/label :t/clear-history-action)
:on-accept #(do
(re-frame/dispatch [:bottom-sheet/hide])
(re-frame/dispatch [:chat.ui/clear-history chat-id false]))}})
(rf/defn gaps-failed
{:events [::gaps-failed]}
[{:keys [db]} chat-id gap-ids error]
(log/error "failed to fetch gaps" chat-id gap-ids error)
{:db (dissoc db :mailserver/fetching-gaps-in-progress)})
(rf/defn sync-chat-from-sync-from-failed
{:events [::sync-chat-from-sync-from-failed]}
[{:keys [db]} chat-id error]
(log/error "failed to sync chat" chat-id error)
{:db (dissoc db :mailserver/fetching-gaps-in-progress)})
(rf/defn sync-chat-from-sync-from-success
{:events [::sync-chat-from-sync-from-success]}
[{:keys [db] :as cofx} chat-id synced-from]
(log/debug "synced success" chat-id synced-from)
{:db
(-> db
(assoc-in [:chats chat-id :synced-from] synced-from)
(dissoc :mailserver/fetching-gaps-in-progress))})
(rf/defn gaps-filled
{:events [::gaps-filled]}
[{:keys [db] :as cofx} chat-id message-ids]
(rf/merge
cofx
{:db (-> db
(update-in [:messages chat-id] (fn [messages] (apply dissoc messages message-ids)))
(dissoc :mailserver/fetching-gaps-in-progress))}
(message-list/rebuild-message-list chat-id)))
(rf/defn fill-gaps
[cofx chat-id gap-ids]
{:json-rpc/call [{:method "wakuext_fillGaps"
:params [chat-id gap-ids]
:on-success #(re-frame/dispatch [::gaps-filled chat-id gap-ids %])
:on-error #(re-frame/dispatch [::gaps-failed chat-id gap-ids %])}]})
(rf/defn sync-chat-from-sync-from
[cofx chat-id]
(log/debug "syncing chat from sync from")
{:json-rpc/call [{:method "wakuext_syncChatFromSyncedFrom"
:params [chat-id]
:on-success #(re-frame/dispatch [::sync-chat-from-sync-from-success chat-id %])
:on-error #(re-frame/dispatch [::sync-chat-from-sync-from-failed chat-id %])}]})
(rf/defn chat-ui-fill-gaps
{:events [:chat.ui/fill-gaps]}
[{:keys [db] :as cofx} chat-id gap-ids]
(let [use-status-nodes? (mailserver/fetch-use-mailservers? {:db db})]
(log/info "filling gaps if use-status-nodes = true" chat-id gap-ids)
(when use-status-nodes?
(rf/merge cofx
{:db (assoc db :mailserver/fetching-gaps-in-progress gap-ids)}
(if (= gap-ids #{:first-gap})
(sync-chat-from-sync-from chat-id)
(fill-gaps chat-id gap-ids))))))
(rf/defn chat-ui-remove-chat-pressed
{:events [:chat.ui/remove-chat-pressed]}
[_ chat-id]
{:ui/show-confirmation
{:title (i18n/label :t/delete-confirmation)
:content (i18n/label :t/delete-chat-confirmation)
:confirm-button-text (i18n/label :t/delete)
:on-accept #(do
(re-frame/dispatch [:bottom-sheet/hide])
(re-frame/dispatch [:chat.ui/remove-chat chat-id]))}})
(rf/defn decrease-unviewed-count
{:events [:chat/decrease-unviewed-count]}
[{:keys [db]} chat-id {:keys [count countWithMentions]}]
{:db (-> db
;; There might be some other requests being fired,
;; so we need to make sure the count has not been set to
;; 0 in the meantime
(update-in [:chats chat-id :unviewed-messages-count]
#(max (- % count) 0))
(update-in [:chats chat-id :unviewed-mentions-count]
#(max (- % countWithMentions) 0)))})
+63
View File
@@ -0,0 +1,63 @@
(ns status-im.chat.models.gaps
(:require [utils.re-frame :as rf]
[taoensso.timbre :as log]
[status-im2.contexts.chat.messages.list.events :as message-list]
[status-im.mailserver.core :as mailserver]))
(rf/defn gaps-filled
{:events [:gaps/filled]}
[{:keys [db] :as cofx} chat-id message-ids]
(rf/merge
cofx
{:db (-> db
(update-in [:messages chat-id] (fn [messages] (apply dissoc messages message-ids)))
(dissoc :mailserver/fetching-gaps-in-progress))}
(message-list/rebuild-message-list chat-id)))
(rf/defn gaps-failed
{:events [:gaps/failed]}
[{:keys [db]} chat-id gap-ids error]
(log/error "failed to fetch gaps" chat-id gap-ids error)
{:db (dissoc db :mailserver/fetching-gaps-in-progress)})
(rf/defn sync-chat-from-sync-from-failed
{:events [::sync-chat-from-sync-from-failed]}
[{:keys [db]} chat-id error]
(log/error "failed to sync chat" chat-id error)
{:db (dissoc db :mailserver/fetching-gaps-in-progress)})
(rf/defn sync-chat-from-sync-from-success
{:events [::sync-chat-from-sync-from-success]}
[{:keys [db] :as cofx} chat-id synced-from]
(log/debug "synced success" chat-id synced-from)
{:db
(-> db
(assoc-in [:chats chat-id :synced-from] synced-from)
(dissoc :mailserver/fetching-gaps-in-progress))})
(rf/defn fill-gaps
[_ chat-id gap-ids]
{:json-rpc/call [{:method "wakuext_fillGaps"
:params [chat-id gap-ids]
:on-success #(rf/dispatch [:gaps/filled chat-id gap-ids %])
:on-error #(rf/dispatch [:gaps/failed chat-id gap-ids %])}]})
(rf/defn sync-chat-from-sync-from
[_ chat-id]
(log/debug "syncing chat from sync from")
{:json-rpc/call [{:method "wakuext_syncChatFromSyncedFrom"
:params [chat-id]
:on-success #(rf/dispatch [::sync-chat-from-sync-from-success chat-id %])
:on-error #(rf/dispatch [::sync-chat-from-sync-from-failed chat-id %])}]})
(rf/defn chat-ui-fill-gaps
{:events [:chat.ui/fill-gaps]}
[{:keys [db] :as cofx} chat-id gap-ids]
(let [use-status-nodes? (mailserver/fetch-use-mailservers? {:db db})]
(log/info "filling gaps if use-status-nodes = true" chat-id gap-ids)
(when use-status-nodes?
(rf/merge cofx
{:db (assoc db :mailserver/fetching-gaps-in-progress gap-ids)}
(if (= gap-ids #{:first-gap})
(sync-chat-from-sync-from chat-id)
(fill-gaps chat-id gap-ids))))))
+22 -33
View File
@@ -3,7 +3,6 @@
["react-native-blob-util" :default ReactNativeBlobUtil]
[clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.chat.models :as chat]
[utils.i18n :as i18n]
[status-im.ui.components.permissions :as permissions]
[status-im.ui.components.react :as react]
@@ -98,10 +97,10 @@
(re-frame/reg-fx
::image-selected
(fn [[uri chat-id]]
(fn [[image chat-id]]
(resize-and-call
uri
#(re-frame/dispatch [:chat.ui/image-selected chat-id uri %]))))
(:uri image)
#(re-frame/dispatch [:chat.ui/image-selected chat-id image %]))))
(re-frame/reg-fx
::camera-roll-get-photos
@@ -112,9 +111,18 @@
(-> (if end-cursor
(.getPhotos
CameraRoll
#js {:first num :after end-cursor :assetType "Photos" :groupTypes "All"})
#js
{:first num
:after end-cursor
:assetType "Photos"
:groupTypes "All"
:include (clj->js ["imageSize"])})
(.getPhotos CameraRoll
#js {:first num :assetType "Photos" :groupTypes "All"}))
#js
{:first num
:assetType "Photos"
:groupTypes "All"
: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])))
@@ -151,7 +159,7 @@
[{: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 :uri]) photos)))
(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))}))
@@ -167,21 +175,17 @@
(let [current-chat-id (or chat-id (:current-chat-id db))]
(clear-sending-images cofx current-chat-id)))
(rf/defn cancel-sending-image-timeline
{:events [:chat.ui/cancel-sending-image-timeline]}
[{:keys [db] :as cofx}]
(cancel-sending-image cofx (chat/my-profile-chat-topic db)))
(rf/defn image-selected
{:events [:chat.ui/image-selected]}
[{:keys [db]} current-chat-id original uri]
{:db (update-in db [:chat/inputs current-chat-id :metadata :sending-image original] merge {:uri uri})})
{:db
(update-in db [:chat/inputs current-chat-id :metadata :sending-image uri] merge original {:uri uri})})
(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 original)}))
{: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]}
@@ -191,11 +195,6 @@
(when (< (count images) config/max-images-batch)
{::chat-open-image-picker current-chat-id})))
(rf/defn chat-open-image-picker-timeline
{:events [:chat.ui/open-image-picker-timeline]}
[{:keys [db] :as cofx}]
(chat-open-image-picker cofx (chat/my-profile-chat-topic db)))
(rf/defn chat-show-image-picker-camera
{:events [:chat.ui/show-image-picker-camera]}
[{:keys [db]} chat-id]
@@ -204,27 +203,17 @@
(when (< (count images) config/max-images-batch)
{::chat-open-image-picker-camera current-chat-id})))
(rf/defn chat-show-image-picker-camera-timeline
{:events [:chat.ui/show-image-picker-camera-timeline]}
[{:keys [db] :as cofx}]
(chat-show-image-picker-camera cofx (chat/my-profile-chat-topic db)))
(rf/defn camera-roll-pick
{:events [:chat.ui/camera-roll-pick]}
[{:keys [db]} uri chat-id]
[{: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 [uri current-chat-id]}
::image-selected [image current-chat-id]}
(when (and (< (count images) config/max-images-batch)
(not (get images uri)))
{::image-selected [uri current-chat-id]}))))
(rf/defn camera-roll-pick-timeline
{:events [:chat.ui/camera-roll-pick-timeline]}
[{:keys [db] :as cofx} uri]
(camera-roll-pick cofx uri (chat/my-profile-chat-topic db)))
(not (some #(= (:uri image) (:uri %)) images)))
{::image-selected [image current-chat-id]}))))
(rf/defn save-image-to-gallery
{:events [:chat.ui/save-image-to-gallery]}
+27 -74
View File
@@ -3,14 +3,12 @@
[clojure.string :as string]
[goog.object :as object]
[re-frame.core :as re-frame]
[status-im.chat.models :as chat]
[status-im.chat.models.mentions :as mentions]
[status-im.chat.models.message :as chat.message]
[status-im.chat.models.message-content :as message-content]
[status-im2.constants :as constants]
[utils.re-frame :as rf]
[utils.i18n :as i18n]
[utils.datetime :as datetime]
[status-im.utils.utils :as utils]
[taoensso.timbre :as log]
[status-im.ui.screens.chat.components.input :as input]))
@@ -25,6 +23,14 @@
(.-char ^js emoji-map)
original))))
;; effects
(re-frame/reg-fx
:show-cooldown-warning
(fn [_]
(utils/show-popup nil
(i18n/label :cooldown/warning-message)
#())))
(rf/defn set-chat-input-text
"Set input text for current-chat. Takes db and input text and cofx
as arguments and returns new fx. Always clear all validation messages."
@@ -33,13 +39,6 @@
(let [current-chat-id (or chat-id (:current-chat-id db))]
{:db (assoc-in db [:chat/inputs current-chat-id :input-text] (text->emoji new-input))}))
(rf/defn set-timeline-input-text
{:events [:chat.ui/set-timeline-input-text]}
[{db :db} new-input]
{:db (assoc-in db
[:chat/inputs (chat/my-profile-chat-topic db) :input-text]
(text->emoji new-input))})
(rf/defn select-mention
{:events [:chat.ui/select-mention]}
[{:keys [db] :as cofx} text-input-ref {:keys [alias name searched-text match] :as user}]
@@ -74,45 +73,11 @@
:end end}))
(mentions/recheck-at-idxs {alias user}))))
(defn- start-cooldown
[{:keys [db]} cooldowns]
{:dispatch-later [{:dispatch [:chat/disable-cooldown]
:ms (constants/cooldown-periods-ms
cooldowns
constants/default-cooldown-period-ms)}]
:show-cooldown-warning nil
:db (assoc db
:chat/cooldowns (if
(=
constants/cooldown-reset-threshold
cooldowns)
0
cooldowns)
:chat/spam-messages-frequency 0
:chat/cooldown-enabled? true)})
(rf/defn process-cooldown
"Process cooldown to protect against message spammers"
[{{:keys [chat/last-outgoing-message-sent-at
chat/cooldowns
chat/spam-messages-frequency
current-chat-id]
:as db}
:db
:as cofx}]
(when (chat/public-chat? cofx current-chat-id)
(let [spamming-fast? (< (- (datetime/timestamp) last-outgoing-message-sent-at)
(+ constants/spam-interval-ms (* 1000 cooldowns)))
spamming-frequently? (= constants/spam-message-frequency-threshold
spam-messages-frequency)]
(cond-> {:db (assoc db
:chat/last-outgoing-message-sent-at (datetime/timestamp)
:chat/spam-messages-frequency (if spamming-fast?
(inc spam-messages-frequency)
0))}
(and spamming-fast? spamming-frequently?)
(start-cooldown (inc cooldowns))))))
(rf/defn disable-chat-cooldown
"Turns off chat cooldown (protection against message spamming)"
{:events [:chat/disable-cooldown]}
[{:keys [db]}]
{:db (assoc db :chat/cooldown-enabled? false)})
(rf/defn reply-to-message
"Sets reference to previous chat message and focuses on input"
@@ -182,15 +147,20 @@
:ens-name preferred-name})))
(defn build-image-messages
[{db :db} chat-id]
[{db :db} chat-id input-text]
(let [images (get-in db [:chat/inputs chat-id :metadata :sending-image])
album-id (str (random-uuid))]
(mapv (fn [[_ {:keys [uri]}]]
(mapv (fn [[_ {:keys [uri width height]}]]
{:chat-id chat-id
:album-id album-id
:content-type constants/content-type-image
:image-path (utils/safe-replace uri #"file://" "")
:text (i18n/label :t/update-to-see-image {"locale" "en"})})
:image-width width
:image-height height
;; TODO: message not received if text field is
;; nil or empty, issue:
;; https://github.com/status-im/status-mobile/issues/14754
:text (or input-text "placeholder")})
images)))
(rf/defn clean-input
@@ -216,27 +186,13 @@
(rf/defn send-messages
[{:keys [db] :as cofx} input-text current-chat-id]
(let [image-messages (build-image-messages cofx current-chat-id)
text-message (build-text-message cofx input-text current-chat-id)
(let [image-messages (build-image-messages cofx current-chat-id input-text)
text-message (when-not (seq image-messages)
(build-text-message cofx input-text current-chat-id))
messages (keep identity (conj image-messages text-message))]
(when (seq messages)
(rf/merge cofx
(clean-input (:current-chat-id db))
(process-cooldown)
(chat.message/send-messages messages)))))
(rf/defn send-my-status-message
"when not empty, proceed by sending text message with public key topic"
{:events [:profile.ui/send-my-status-message]}
[{db :db :as cofx}]
(let [current-chat-id (chat/my-profile-chat-topic db)
{:keys [input-text]} (get-in db [:chat/inputs current-chat-id])
image-messages (build-image-messages cofx current-chat-id)
text-message (build-text-message cofx input-text current-chat-id)
messages (keep identity (conj image-messages text-message))]
(when (seq messages)
(rf/merge cofx
(clean-input current-chat-id)
(chat.message/send-messages messages)))))
(rf/defn send-audio-message
@@ -274,8 +230,7 @@
:js-response true
:on-error #(log/error "failed to edit message " %)
:on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]}
(cancel-message-edit)
(process-cooldown)))
(cancel-message-edit)))
(rf/defn send-current-message
"Sends message from current chat input"
@@ -304,8 +259,7 @@
:on-success #(re-frame/dispatch [:transport/message-sent %])}]}
(mentions/clear-mentions)
(mentions/clear-cursor)
(clean-input (:current-chat-id db))
(process-cooldown)))
(clean-input (:current-chat-id db))))
(rf/defn cancel-contact-request
"Cancels contact request"
@@ -316,8 +270,7 @@
{:db (assoc-in db [:chat/inputs current-chat-id :metadata :sending-contact-request] nil)}
(mentions/clear-mentions)
(mentions/clear-cursor)
(clean-input (:current-chat-id db))
(process-cooldown))))
(clean-input (:current-chat-id db)))))
(rf/defn chat-send-sticker
{:events [:chat/send-sticker]}
+2 -72
View File
@@ -1,79 +1,9 @@
(ns status-im.chat.models.input-test
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im2.constants :as constants]
[status-im.chat.models.input :as input]
[utils.datetime :as datetime]))
(:require [cljs.test :refer-macros [deftest is]]
[status-im.chat.models.input :as input]))
(deftest text->emoji
(is (nil? (input/text->emoji nil)))
(is (= "" (input/text->emoji "")))
(is (= "test" (input/text->emoji "test")))
(is (= "word1 \uD83D\uDC4D word2" (input/text->emoji "word1 :+1: word2"))))
(deftest process-cooldown-fx
(let [db {:current-chat-id "chat"
:chats {"chat" {:public? true}}
:chat/cooldowns 0
:chat/spam-messages-frequency 0
:chat/cooldown-enabled? false}]
(with-redefs [datetime/timestamp (constantly 1527675198542)]
(testing "no spamming detected"
(let [expected {:db (assoc db :chat/last-outgoing-message-sent-at 1527675198542)}
actual (input/process-cooldown {:db db})]
(is (= expected actual))))
(testing "spamming detected in 1-1"
(let [db (assoc db
:chats {"chat" {:public? false}}
:chat/spam-messages-frequency constants/spam-message-frequency-threshold
:chat/last-outgoing-message-sent-at (- 1527675198542 900))
expected nil
actual (input/process-cooldown {:db db})]
(is (= expected actual))))
(testing "spamming detected"
(let [db (assoc db
:chat/last-outgoing-message-sent-at (- 1527675198542 900)
:chat/spam-messages-frequency constants/spam-message-frequency-threshold)
expected {:db (assoc db
:chat/last-outgoing-message-sent-at 1527675198542
:chat/cooldowns 1
:chat/spam-messages-frequency 0
:chat/cooldown-enabled? true)
:show-cooldown-warning nil
:dispatch-later [{:dispatch [:chat/disable-cooldown]
:ms (constants/cooldown-periods-ms 1)}]}
actual (input/process-cooldown {:db db})]
(is (= expected actual))))
(testing "spamming detected twice"
(let [db (assoc db
:chat/cooldowns 1
:chat/last-outgoing-message-sent-at (- 1527675198542 900)
:chat/spam-messages-frequency constants/spam-message-frequency-threshold)
expected {:db (assoc db
:chat/last-outgoing-message-sent-at 1527675198542
:chat/cooldowns 2
:chat/spam-messages-frequency 0
:chat/cooldown-enabled? true)
:show-cooldown-warning nil
:dispatch-later [{:dispatch [:chat/disable-cooldown]
:ms (constants/cooldown-periods-ms 2)}]}
actual (input/process-cooldown {:db db})]
(is (= expected actual))))
(testing "spamming reaching cooldown threshold"
(let [db (assoc db
:chat/cooldowns (dec constants/cooldown-reset-threshold)
:chat/last-outgoing-message-sent-at (- 1527675198542 900)
:chat/spam-messages-frequency constants/spam-message-frequency-threshold)
expected {:db (assoc db
:chat/last-outgoing-message-sent-at 1527675198542
:chat/cooldowns 0
:chat/spam-messages-frequency 0
:chat/cooldown-enabled? true)
:show-cooldown-warning nil
:dispatch-later [{:dispatch [:chat/disable-cooldown]
:ms (constants/cooldown-periods-ms 3)}]}
actual (input/process-cooldown {:db db})]
(is (= expected actual)))))))
+4 -5
View File
@@ -4,7 +4,6 @@
[status-im2.constants :as constants]
[status-im.data-store.chats :as data-store.chats]
[status-im.data-store.messages :as data-store.messages]
[status-im2.contexts.activity-center.events :as activity-center]
[taoensso.timbre :as log]
[utils.re-frame :as rf]))
@@ -69,15 +68,15 @@
(rf/defn handle-mark-all-read-successful
{:events [::mark-all-read-successful]}
[cofx]
(activity-center/notifications-fetch-unread-count cofx))
[_]
{:dispatch [:activity-center.notifications/fetch-unread-count]})
(rf/defn handle-mark-all-read-in-community-successful
{:events [::mark-all-read-in-community-successful]}
[{:keys [db] :as cofx} chat-ids]
(rf/merge cofx
{:db (reduce mark-chat-all-read db chat-ids)}
(activity-center/notifications-fetch-unread-count)))
{:db (reduce mark-chat-all-read db chat-ids)
:dispatch [:activity-center.notifications/fetch-unread-count]}))
(rf/defn handle-mark-all-read
{:events [:chat.ui/mark-all-read-pressed :chat/mark-all-as-read]}
-40
View File
@@ -1,11 +1,9 @@
(ns status-im.chat.models.message
(:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.chat.models :as chat-model]
[status-im.chat.models.loading :as chat.loading]
[status-im.chat.models.mentions :as mentions]
[status-im2.contexts.chat.messages.list.events :as message-list]
[status-im2.constants :as constants]
[status-im.data-store.messages :as data-store.messages]
[status-im.transport.message.protocol :as protocol]
[status-im2.contexts.chat.messages.list.state :as view.state]
@@ -56,20 +54,6 @@
{:db db}
messages))
(defn timeline-message?
[db chat-id]
(and
(get-in db [:pagination-info constants/timeline-chat-id :messages-initialized?])
(or
(= chat-id (chat-model/my-profile-chat-topic db))
(when-let [pub-key (get-in db [:chats chat-id :profile-public-key])]
(get-in db [:contacts/contacts pub-key :added])))))
(defn get-timeline-message
[db chat-id message-js]
(when (timeline-message? db chat-id)
(data-store.messages/<-rpc (types/js->clj message-js))))
(defn add-message
[{:keys [db] :as acc} message-js chat-id message-id cursor-clock-value]
(let [{:keys [replace from clock-value] :as message}
@@ -148,30 +132,6 @@
(when (seq senders)
[{:ms 100 :dispatch [:chat/add-senders-to-chat-users (vals senders)]}]))}))
(defn reduce-js-statuses
[db ^js message-js]
(let [chat-id (.-localChatId message-js)
profile-initialized (get-in db [:pagination-info chat-id :messages-initialized?])
timeline-message (timeline-message? db chat-id)
old-message (get-in db [:messages chat-id (.-id message-js)])]
(if (and (or profile-initialized timeline-message) (nil? old-message))
(let [{:keys [message-id] :as message} (data-store.messages/<-rpc (types/js->clj message-js))]
(cond-> db
profile-initialized
(update-in [:messages chat-id] assoc message-id message)
profile-initialized
(update-in [:message-lists chat-id] message-list/add message)
timeline-message
(update-in [:messages constants/timeline-chat-id] assoc message-id message)
timeline-message
(update-in [:message-lists constants/timeline-chat-id] message-list/add message)))
db)))
(rf/defn process-statuses
{:events [:process-statuses]}
[{:keys [db]} statuses]
{:db (reduce reduce-js-statuses db statuses)})
(rf/defn update-db-message-status
[{:keys [db] :as cofx} chat-id message-id status]
(when (get-in db [:messages chat-id message-id])
+10 -6
View File
@@ -558,16 +558,20 @@
(rf/defn request-to-join-accepted
{:events [::request-to-join-accepted]}
[{:keys [db] :as cofx} community-id request-id response-js]
(rf/merge cofx
{:db (update-in db [:communities/requests-to-join community-id] dissoc request-id)}
(handle-response response-js)))
(rf/merge
cofx
{:db (update-in db [:communities/requests-to-join community-id] dissoc request-id)
:dispatch-n [[:sanitize-messages-and-process-response response-js]
[:activity-center.notifications/mark-as-read request-id]]}))
(rf/defn request-to-join-declined
{:events [::request-to-join-declined]}
[{:keys [db] :as cofx} community-id request-id response-js]
(rf/merge cofx
{:db (update-in db [:communities/requests-to-join community-id] dissoc request-id)}
(handle-response response-js)))
(rf/merge
cofx
{:db (update-in db [:communities/requests-to-join community-id] dissoc request-id)
:dispatch-n [[:sanitize-messages-and-process-response response-js]
[:activity-center.notifications/mark-as-read request-id]]}))
(rf/defn accept-request-to-join-pressed
{:events [:communities.ui/accept-request-to-join-pressed]}
+1 -1
View File
@@ -76,7 +76,7 @@
(re-frame/dispatch [:hide-popover])))
;; reset navigation to avoid going back to non existing one to one chat
(if from-one-to-one-chat?
(navigation/pop-to-root-tab :chat-stack)
(navigation/pop-to-root-tab :shell-stack)
(navigation/navigate-back)))))
(rf/defn contact-unblocked
+1 -13
View File
@@ -1,16 +1,9 @@
(ns status-im.contact.chat
(:require [re-frame.core :as re-frame]
[status-im.chat.models :as chat]
[status-im.contact.core :as contact]
[utils.re-frame :as rf]
[status-im2.navigation.events :as navigation]))
(rf/defn send-message-pressed
{:events [:contact.ui/send-message-pressed]
:interceptors [(re-frame/inject-cofx :random-id-generator)]}
[cofx {:keys [public-key ens-name]}]
(chat/start-chat cofx public-key ens-name))
(rf/defn contact-code-submitted
{:events [:contact.ui/contact-code-submitted]
:interceptors [(re-frame/inject-cofx :random-id-generator)]}
@@ -19,11 +12,6 @@
(rf/merge cofx
#(if new-contact?
(contact/add-contact % public-key nickname ens-name)
(chat/start-chat % public-key ens-name))
{:dispatch [:chat.ui/start-chat public-key ens-name]})
#(when new-contact?
(navigation/navigate-back %)))))
(rf/defn pinned-messages-pressed
{:events [:contact.ui/pinned-messages-pressed]}
[cofx public-key]
(chat/navigate-to-user-pinned-messages cofx public-key))
+3 -5
View File
@@ -39,8 +39,6 @@
blocked (:blocked contact)
was-blocked (contact.db/blocked? db public-key)]
(cond-> acc
(and added (not was-added))
(conj [:start-profile-chat public-key])
(and (not (:has-added-us contact))
(= constants/contact-request-state-none (:contact-request-state contact)))
@@ -51,7 +49,7 @@
(and blocked (not was-blocked))
(conj [::contact.block/contact-blocked contact chats]))))
[[:offload-messages constants/timeline-chat-id]
[[:chat/offload-messages constants/timeline-chat-id]
[:activity-center.notifications/fetch-unread-count]]
contacts)]
(merge
@@ -93,7 +91,7 @@
ens-name
#(do
(re-frame/dispatch [:sanitize-messages-and-process-response %])
(re-frame/dispatch [:offload-messages constants/timeline-chat-id])))))
(re-frame/dispatch [:chat/offload-messages constants/timeline-chat-id])))))
(rf/defn remove-contact
"Remove a contact from current account's contact list"
@@ -109,7 +107,7 @@
{:method "wakuext_retractContactRequest"
:params [{:contactId public-key}]
:on-success #(log/debug "contact removed successfully")}]
:dispatch [:offload-messages constants/timeline-chat-id]})
:dispatch [:chat/offload-messages constants/timeline-chat-id]})
(rf/defn accept-contact-request
{:events [:contact-requests.ui/accept-request]}
+2
View File
@@ -36,6 +36,8 @@
:deleted :deleted?
:deletedForMe :deleted-for-me?
:albumId :album-id
:imageWidth :image-width
:imageHeight :image-height
:new :new?})
(update :quoted-message
@@ -9,6 +9,7 @@
(set/rename-keys visibility-status-update
{:publicKey :public-key
:statusType :status-type}))
(defn <-rpc-settings
[settings]
(-> settings
+10 -9
View File
@@ -35,15 +35,16 @@
:on-error #(cb "0x")}))
(defn pubkey
[chain-id ens-name cb]
{:pre [(is-valid-eth-name? ens-name)]}
(json-rpc/call {:method "ens_publicKeyOf"
:params [chain-id ens-name]
:on-success cb
;;at some point infura started to return execution reverted error instead of "0x"
;;result
;;our code expects "0x" result
:on-error #(cb "0x")}))
([chain-id ens-name on-success on-error]
{:pre [(is-valid-eth-name? ens-name)]}
(json-rpc/call {:method "ens_publicKeyOf"
:params [chain-id ens-name]
:on-success on-success
:on-error on-error}))
;; At some point, infura started to return "execution reverted" error
;; instead of "0x" result. Our code expects "0x" result.
([chain-id ens-name cb]
(pubkey chain-id ens-name cb #(cb "0x"))))
(defn owner
[chain-id ens-name cb]
+8 -12
View File
@@ -2,13 +2,12 @@
(:require
clojure.set
[re-frame.core :as re-frame]
status-im.add-new.core
[status-im.async-storage.core :as async-storage]
status-im.backup.core
status-im.bootnodes.core
status-im.browser.core
status-im.browser.permissions
[status-im.chat.models :as chat]
status-im.chat.models
status-im.chat.models.images
status-im.chat.models.input
status-im.chat.models.loading
@@ -112,10 +111,13 @@
(rf/defn system-theme-mode-changed
{:events [:system-theme-mode-changed]}
[cofx _]
(when (multiaccounts.model/logged-in? cofx)
{:multiaccounts.ui/switch-theme (get-in cofx [:db :multiaccount :appearance])
:dispatch [:reload-new-ui]}))
[{:keys [db] :as cofx} _]
(let [current-theme-type (get-in cofx [:db :multiaccount :appearance])]
(when (and (multiaccounts.model/logged-in? cofx)
(= current-theme-type status-im2.constants/theme-type-system))
{:multiaccounts.ui/switch-theme
[(get-in db [:multiaccount :appearance])
(:view-id db) true]})))
(def authentication-options
{:reason (i18n/label :t/biometric-auth-reason-login)})
@@ -184,12 +186,6 @@
[{:keys [db]} dimensions]
{:db (assoc db :dimensions/window (dimensions/window dimensions))})
(rf/defn init-timeline-chat
{:events [:init-timeline-chat]}
[{:keys [db] :as cofx}]
(when-not (get-in db [:pagination-info constants/timeline-chat-id :messages-initialized?])
(chat/preload-chat-data cofx constants/timeline-chat-id)))
(rf/defn on-will-focus
{:events [:screens/on-will-focus]}
[{:keys [db] :as cofx} view-id]
+6 -6
View File
@@ -5,7 +5,7 @@
[utils.i18n :as i18n]
[oops.core :as oops]
[re-frame.core :as re-frame]
[status-im.chat.models :as models.chat]
[status-im2.contexts.chat.events :as chat.events]
[status-im2.constants :as constants]
[status-im2.contexts.activity-center.events :as activity-center]
[status-im2.navigation.events :as navigation]
@@ -15,7 +15,7 @@
{:events [:navigate-chat-updated]}
[cofx chat-id]
(when (get-in cofx [:db :chats chat-id])
(models.chat/navigate-to-chat-nav2 cofx chat-id nil)))
(chat.events/navigate-to-chat cofx chat-id nil)))
(rf/defn handle-chat-removed
{:events [:chat-removed]}
@@ -23,7 +23,7 @@
(rf/merge cofx
{:db (dissoc (:db cofx) :current-chat-id)
:dispatch-n [[:sanitize-messages-and-process-response response]
[:pop-to-root-tab :chat-stack]]}
[:pop-to-root-tab :shell-stack]]}
(activity-center/notifications-fetch-unread-count)))
(rf/defn handle-chat-update
@@ -76,7 +76,7 @@
(rf/defn create-from-link
[cofx {:keys [chat-id invitation-admin chat-name]}]
(if (get-in cofx [:db :chats chat-id])
{:dispatch [:chat.ui/navigate-to-chat-nav2 chat-id]}
{:dispatch [:chat/navigate-to-chat chat-id]}
{:json-rpc/call [{:method "wakuext_createGroupChatFromInvitation"
:params [chat-name chat-id invitation-admin]
:js-response true
@@ -123,8 +123,8 @@
{:events [:group-chats.ui/remove-chat-confirmed]}
[cofx chat-id]
(rf/merge cofx
(models.chat/deactivate-chat chat-id)
(navigation/pop-to-root-tab :chat-stack)))
(chat.events/deactivate-chat chat-id)
(navigation/pop-to-root-tab :shell-stack)))
(def not-blank?
(complement string/blank?))
+17 -21
View File
@@ -3,9 +3,9 @@
[clojure.string :as string]
[day8.re-frame.test :as rf-test]
[re-frame.core :as rf]
[status-im.chat.models :as chat.models]
[status-im.ethereum.core :as ethereum]
status-im.events
status-im2.events
[status-im.multiaccounts.logout.core :as logout]
[status-im.transport.core :as transport]
[status-im.utils.test :as utils.test]
@@ -250,8 +250,8 @@
(assert-messenger-started)
(rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat
(rf-test/wait-for
[:status-im.chat.models/one-to-one-chat-created]
(rf/dispatch-sync [:chat.ui/navigate-to-chat-nav2 chat-id])
[:chat/one-to-one-chat-created]
(rf/dispatch-sync [:chat/navigate-to-chat chat-id])
(is (= chat-id @(rf/subscribe [:chats/current-chat-id])))
(logout!)
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in
@@ -275,20 +275,17 @@
(assert-messenger-started)
(rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat
(rf-test/wait-for
[:status-im.chat.models/one-to-one-chat-created]
(rf/dispatch-sync [:chat.ui/navigate-to-chat-nav2 chat-id])
[:chat/one-to-one-chat-created]
(rf/dispatch-sync [:chat/navigate-to-chat chat-id])
(is (= chat-id @(rf/subscribe [:chats/current-chat-id])))
(is @(rf/subscribe [:chats/chat chat-id]))
(rf/dispatch-sync [:chat.ui/remove-chat-pressed chat-id])
(rf/dispatch-sync [:chat.ui/show-remove-confirmation chat-id])
(rf/dispatch-sync [:chat.ui/remove-chat chat-id])
(rf-test/wait-for
[::chat.models/chat-deactivated]
(is (not @(rf/subscribe [:chats/chat chat-id])))
(logout!)
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not
; in an
; inconsistent state between tests
(assert-logout)))))))))
(logout!)
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not
; in an
; inconsistent state between tests
(assert-logout))))))))
(deftest mute-chat-test
(log/info "========= mute-chat-test ==================")
@@ -306,18 +303,17 @@
(assert-messenger-started)
(rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat
(rf-test/wait-for
[:status-im.chat.models/one-to-one-chat-created]
(rf/dispatch-sync [:chat.ui/navigate-to-chat-nav2 chat-id])
[:chat/one-to-one-chat-created]
(rf/dispatch-sync [:chat/navigate-to-chat chat-id])
(is (= chat-id @(rf/subscribe [:chats/current-chat-id])))
(is @(rf/subscribe [:chats/chat chat-id]))
(rf/dispatch-sync [::chat.models/mute-chat-toggled chat-id true])
(rf/dispatch-sync [:chat.ui/mute chat-id true])
(rf-test/wait-for
[::chat.models/mute-chat-toggled-successfully]
[:chat/mute-successfully]
(is @(rf/subscribe [:chats/muted chat-id]))
(rf/dispatch-sync [::chat.models/mute-chat-toggled chat-id false])
(rf/dispatch-sync [:chat.ui/mute chat-id false])
(rf-test/wait-for
[::chat.models/mute-chat-toggled-successfully]
[:chat/mute-successfully]
(is (not @(rf/subscribe [:chats/muted chat-id])))
(logout!)
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is
+21 -7
View File
@@ -8,11 +8,15 @@
[status-im.native-module.core :as native-module]
[status-im.theme.core :as theme]
[utils.re-frame :as rf]
[quo2.foundations.colors :as colors]
[status-im2.constants :as constants]
[status-im.utils.gfycat.core :as gfycat]
[status-im.utils.identicon :as identicon]
[status-im.utils.utils :as utils]
[status-im2.setup.hot-reload :as hot-reload]
[status-im2.common.theme.core :as utils.theme]
[taoensso.timbre :as log]))
[taoensso.timbre :as log]
[status-im2.contexts.shell.animation :as shell.animation]))
;; validate that the given mnemonic was generated from Status Dictionary
(re-frame/reg-fx
@@ -173,17 +177,27 @@
(re-frame/reg-fx
:multiaccounts.ui/switch-theme
(fn [theme-id]
(let [theme (if (or (= 2 theme-id) (and (= 0 theme-id) (utils.theme/dark-mode?)))
:dark
:light)]
(theme/change-theme theme))))
(fn [[theme-type view-id reload-ui?]]
(let [[theme status-bar-theme nav-bar-color]
;; Status bar theme represents status bar icons colors, so opposite to app theme
(if (or (= theme-type constants/theme-type-dark)
(and (= theme-type constants/theme-type-system)
(utils.theme/dark-mode?)))
[:dark :light colors/neutral-100]
[:light :dark colors/white])]
(theme/change-theme theme)
(re-frame/dispatch [:change-root-status-bar-style
(if (shell.animation/home-stack-open?) status-bar-theme :light)])
(when reload-ui?
(hot-reload/reload)
(when-not (= view-id :shell-stack)
(re-frame/dispatch [:change-root-nav-bar-color nav-bar-color]))))))
(rf/defn switch-appearance
{:events [:multiaccounts.ui/appearance-switched]}
[cofx theme]
(rf/merge cofx
{:multiaccounts.ui/switch-theme theme}
{:multiaccounts.ui/switch-theme [theme :appearance true]}
(multiaccounts.update/multiaccount-update :appearance theme {})))
(rf/defn switch-profile-picture-show-to
+7 -14
View File
@@ -42,7 +42,7 @@
[status-im2.contexts.activity-center.events :as activity-center]
[status-im2.contexts.chat.messages.link-preview.events :as link-preview]
[status-im2.navigation.events :as navigation]
[status-im2.setup.log :as logging]
[status-im2.common.log :as logging]
[taoensso.timbre :as log]
[utils.security.core :as security]))
@@ -351,7 +351,7 @@
(rf/defn initialize-appearance
[cofx]
{:multiaccounts.ui/switch-theme (get-in cofx [:db :multiaccount :appearance])})
{:multiaccounts.ui/switch-theme [(get-in cofx [:db :multiaccount :appearance]) nil false]})
(rf/defn get-group-chat-invitations
[_]
@@ -424,7 +424,7 @@
:key-uid
(fn [stored-key-uid]
(when (= stored-key-uid key-uid)
(re-frame/dispatch [:chat.ui/navigate-to-chat-nav2 chat-id])))))))))
(re-frame/dispatch [:chat/navigate-to-chat chat-id])))))))))
(rf/defn check-last-chat
{:events [::check-last-chat]}
@@ -490,7 +490,7 @@
"Decides which root should be initialised depending on user and app state"
[db]
(if (get db :tos/accepted?)
(re-frame/dispatch [:init-root (if config/new-ui-enabled? :shell-stack :chat-stack)])
(re-frame/dispatch [:init-root :shell-stack])
(re-frame/dispatch [:init-root :tos])))
(rf/defn login-only-events
@@ -539,14 +539,7 @@
(multiaccounts/switch-preview-privacy-mode-flag)
(link-preview/request-link-preview-whitelist)
(logging/set-log-level (:log-level multiaccount))
(if config/new-ui-enabled?
(navigation/init-root :shell-stack)
;; if it's a first account, the ToS will be accepted at welcome carousel
;; if not a first account, the ToS might have been accepted by other account logins
(if (or first-account? tos-accepted?)
(navigation/init-root :onboarding-notification)
(navigation/init-root :tos))))))
(navigation/init-root :shell-stack))))
(defn- keycard-setup?
[cofx]
@@ -564,7 +557,7 @@
:multiaccount)
(assoc :tos-accept-next-root
(if login-only?
:chat-stack
:shell-stack
:onboarding-notification))
(assoc :logged-in-since now)
(assoc :view-id :home)))
@@ -737,7 +730,7 @@
(rf/defn welcome-lets-go
{:events [:welcome-lets-go]}
[_]
{:init-root-fx :chat-stack})
{:init-root-fx :shell-stack})
(rf/defn multiaccount-selected
{:events [:multiaccounts.login.ui/multiaccount-selected]}
+1 -1
View File
@@ -7,7 +7,7 @@
[utils.re-frame :as rf]
[status-im.utils.keychain.core :as keychain]
[status-im.wallet.core :as wallet]
[status-im2.setup.events :as init]))
[status-im2.events :as init]))
(rf/defn logout-method
{:events [::logout-method]}
+3 -3
View File
@@ -1,7 +1,7 @@
(ns status-im.native-module.core
(:require ["react-native" :as react-native]
[re-frame.core :as re-frame]
[status-im.utils.db :as utils.db]
[status-im2.utils.validators :as validators]
[status-im.utils.platform :as platform]
[status-im.utils.react-native :as react-native-utils]
[status-im.utils.types :as types]
@@ -471,14 +471,14 @@
(defn generate-gfycat
"Generate a 3 words random name based on the user public-key, synchronously"
[public-key]
{:pre [(utils.db/valid-public-key? public-key)]}
{:pre [(validators/valid-public-key? public-key)]}
(log/debug "[native-module] generate-gfycat")
(.generateAlias ^js (status) public-key))
(defn generate-gfycat-async
"Generate a 3 words random name based on the user public-key, asynchronously"
[public-key callback]
{:pre [(utils.db/valid-public-key? public-key)]}
{:pre [(validators/valid-public-key? public-key)]}
(.generateAliasAsync ^js (status) public-key callback))
(defn identicon
+1 -1
View File
@@ -303,7 +303,7 @@
(re-frame/dispatch [:set :current-tab tab-key])
(when (= @state/root-comp-id comp)
(when (= :chat tab-key)
(re-frame/dispatch [:close-chat]))
(re-frame/dispatch [:chat/close]))
(when platform/android?
(.popToRoot Navigation (name comp))))
(reset! state/root-comp-id comp)))))
+6 -2
View File
@@ -5,7 +5,6 @@
[quo.platform :as platform]
[re-frame.core :as re-frame]
[status-im.async-storage.core :as async-storage]
[status-im.chat.models :as chat.models]
[status-im.ethereum.decode :as decode]
[status-im.ethereum.tokens :as tokens]
[utils.i18n :as i18n]
@@ -105,6 +104,11 @@
:user-info notification
:message description}))
(defn foreground-chat?
[{{:keys [current-chat-id view-id]} :db} chat-id]
(and (= current-chat-id chat-id)
(= view-id :chat)))
(defn show-message-pn?
[{{:keys [app-state multiaccount]} :db :as cofx}
notification]
@@ -113,7 +117,7 @@
(and
(not= notification-author (:public-key multiaccount))
(or (= app-state "background")
(not (chat.models/foreground-chat? cofx chat-id))))))
(not (foreground-chat? cofx chat-id))))))
(defn create-notification
([notification]
+9
View File
@@ -84,3 +84,12 @@
{:events [:profile/share-profile-link]}
[_ value]
{:profile/share-profile-link value})
(rf/defn show-profile
{: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)
{:db (-> db
(assoc :contacts/identity identity)
(assoc :contacts/ens-name ens-name))})))
+3 -3
View File
@@ -47,7 +47,7 @@
(rf/defn handle-private-chat
[{:keys [db] :as cofx} {:keys [chat-id]}]
(if-not (new-chat.db/own-public-key? db chat-id)
(chat/start-chat cofx chat-id nil)
{:dispatch [:chat.ui/start-chat chat-id]}
{:utils/show-popup {:title (i18n/label :t/unable-to-read-this-code)
:content (i18n/label :t/can-not-add-yourself)}}))
@@ -76,7 +76,7 @@
:else
{:utils/show-popup {:title (i18n/label :t/unable-to-read-this-code)
:content (i18n/label :t/ens-name-not-found)
:on-dismiss #(re-frame/dispatch [:pop-to-root-tab :chat-stack])}})))
:on-dismiss #(re-frame/dispatch [:pop-to-root-tab :shell-stack])}})))
(rf/defn handle-eip681
[cofx data]
@@ -116,7 +116,7 @@
:event ::match-scanned-value})
{:dispatch [:navigate-back]
:utils/show-popup {:title (i18n/label :t/unable-to-read-this-code)
:on-dismiss #(re-frame/dispatch [:pop-to-root-tab :chat-stack])}})))
:on-dismiss #(re-frame/dispatch [:pop-to-root-tab :shell-stack])}})))
(rf/defn on-scan
{:events [::on-scan-success]}
@@ -5,6 +5,7 @@
{:empty-chats-header (js/require "../resources/images/ui/empty-chats-header.png")
:welcome (js/require "../resources/images/ui/welcome.jpg")
:welcome-dark (js/require "../resources/images/ui/welcome-dark.jpg")
:add-new-contact (js/require "../resources/images/ui/add-contact.png")
:chat (js/require "../resources/images/ui/chat.jpg")
:chat-dark (js/require "../resources/images/ui/chat-dark.jpg")
:wallet (js/require "../resources/images/ui/wallet.jpg")
+7 -7
View File
@@ -3,13 +3,13 @@
[clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.add-new.db :as public-chat.db]
[status-im.chat.models :as chat.models]
[status-im2.contexts.chat.events :as chat.events]
[status-im2.constants :as constants]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.eip681 :as eip681]
[status-im.ethereum.ens :as ens]
[status-im.ethereum.stateofus :as stateofus]
[status-im.utils.db :as utils.db]
[status-im2.utils.validators :as validators]
[status-im.utils.http :as http]
[status-im.utils.wallet-connect :as wallet-connect]
[taoensso.timbre :as log]
@@ -71,7 +71,7 @@
(defn match-contact-async
[chain {:keys [user-id ens-name]} callback]
(let [valid-key (and (utils.db/valid-public-key? user-id)
(let [valid-key (and (validators/valid-public-key? user-id)
(not= user-id ens/default-key))]
(cond
valid-key
@@ -113,15 +113,15 @@
(not (string/blank? chat-name))
(> (count chat-id-parts) 1)
(not (string/blank? (first chat-id-parts)))
(utils.db/valid-public-key? admin-pk)
(utils.db/valid-public-key? (last chat-id-parts)))
(validators/valid-public-key? admin-pk)
(validators/valid-public-key? (last chat-id-parts)))
{:type :group-chat
:chat-id chat-id
:invitation-admin admin-pk
:chat-name chat-name}
(and (not (string/blank? chat-id))
(chat.models/group-chat? (get chats chat-id)))
(chat.events/group-chat? (get chats chat-id)))
(let [{:keys [chat-name invitation-admin]} (get chats chat-id)]
{:type :group-chat
:chat-id chat-id
@@ -228,7 +228,7 @@
(= handler :group-chat)
(cb (match-group-chat chats query-params))
(utils.db/valid-public-key? uri)
(validators/valid-public-key? uri)
(match-contact-async chain {:user-id uri} cb)
(= handler :community-requests)
+2 -7
View File
@@ -2,7 +2,7 @@
(:require
[clojure.string :as string]
[status-im.browser.core :as browser]
[status-im.chat.models :as models.chat]
[status-im2.contexts.chat.events :as chat.events]
[status-im.chat.models.message :as models.message]
[status-im.chat.models.reactions :as models.reactions]
[status-im.communities.core :as models.communities]
@@ -61,7 +61,7 @@
(js-delete response-js "chats")
(rf/merge cofx
(process-next response-js sync-handler)
(models.chat/ensure-chats (map data-store.chats/<-rpc (types/js->clj chats)))))
(chat.events/ensure-chats (map data-store.chats/<-rpc (types/js->clj chats)))))
(seq messages)
(models.message/receive-many cofx response-js)
@@ -200,12 +200,10 @@
message-type (.-messageType message-js)
from (.-from message-js)
mentioned (.-mentioned message-js)
profile (models.chat/profile-chat? {:db db} chat-id)
new (.-new message-js)
current (= current-chat-id chat-id)
should-update-unviewed? (and (not current)
new
(not profile)
(not (= message-type
constants/message-type-private-group-system-message))
(not (= from (multiaccounts.model/current-public-key {:db db}))))
@@ -215,9 +213,6 @@
current
(update :messages conj message-js)
profile
(update :statuses conj message-js)
;;update counter
should-update-unviewed?
(update-in [:db :chats chat-id :unviewed-messages-count] inc)
@@ -6,6 +6,8 @@
(defn build-message
[{:keys [chat-id
album-id
image-width
image-height
text
response-to
ens-name
@@ -17,6 +19,8 @@
content-type]}]
{:chatId chat-id
:albumId album-id
:imageWidth image-width
:imageHeight image-height
:text text
:responseTo response-to
:ensName ens-name
@@ -20,7 +20,7 @@
[status-im.ui.components.react :as react]
[status-im.ui.components.topbar :as topbar]
[status-im.ui.screens.chat.photos :as photos]
[status-im.utils.db :as utils.db]
[status-im2.utils.validators :as validators]
[status-im.utils.gfycat.core :as gfycat]
[status-im.utils.identicon :as identicon]
[status-im.utils.utils :as utils]
@@ -172,7 +172,7 @@
#(do
(reset! search-value %)
(re-frame/dispatch [:set-in [:contacts/new-identity :state] :empty])
(debounce/debounce-and-dispatch [:new-chat/set-new-identity %] 600))
(debounce/debounce-and-dispatch [:contacts/set-new-identity %] 600))
:on-submit-editing
#(when (= state :valid)
(debounce/dispatch-and-chill [:contact.ui/contact-code-submitted false nil] 3000))
@@ -301,7 +301,7 @@
(fn []
(let [{:keys [state ens-name public-key error]} @(re-frame/subscribe [:contacts/new-identity])
blocked? (and
(utils.db/valid-public-key? (or public-key ""))
(validators/valid-public-key? (or public-key ""))
@(re-frame/subscribe [:contacts/contact-blocked?
public-key]))]
[react/view {:style {:flex 1}}
@@ -326,7 +326,7 @@
{:on-change-text
#(do
(re-frame/dispatch [:set-in [:contacts/new-identity :state] :searching])
(debounce/debounce-and-dispatch [:new-chat/set-new-identity %] 600))
(debounce/debounce-and-dispatch [:contacts/set-new-identity %] 600))
:on-submit-editing
#(when (= state :valid)
(debounce/dispatch-and-chill [:contact.ui/contact-code-submitted true @entered-nickname]
@@ -5,17 +5,12 @@
[re-frame.core :as re-frame]
[utils.i18n :as i18n]
[status-im.react-native.resources :as resources]
[status-im.ui.components.react :as react]
[status-im2.config :as config]))
[status-im.ui.components.react :as react]))
(defn button
[label icon theme selected?]
[react/touchable-highlight
{:on-press (fn []
(re-frame/dispatch [:multiaccounts.ui/appearance-switched theme])
(re-frame/dispatch (if config/new-ui-enabled? [:reload-new-ui] [:init-root :chat-stack]))
(re-frame/dispatch [:navigate-change-tab :profile])
(js/setTimeout #(re-frame/dispatch [:navigate-to :appearance]) 1000))}
{:on-press #(re-frame/dispatch [:multiaccounts.ui/appearance-switched theme])}
[react/view
(merge {:align-items :center :padding 8 :border-radius 20}
(when selected?
@@ -7,11 +7,6 @@
[status-im.ui.screens.chat.styles.input.gap :as style]
[utils.datetime :as datetime]))
(defn on-press
[chat-id gap-ids]
(fn []
(re-frame/dispatch [:chat.ui/fill-gaps chat-id gap-ids])))
(views/defview gap
[{:keys [gap-ids chat-id gap-parameters public? community?]}]
(views/letsubs [in-progress? [:chats/fetching-gap-in-progress?
@@ -25,7 +20,7 @@
[react/view {:style (when-not in-progress? style/gap-container)}
[react/touchable-highlight
{:on-press (when (and (not in-progress?) use-status-nodes? connected?)
(on-press chat-id gap-ids))
(re-frame/dispatch [:chat.ui/fill-gaps chat-id gap-ids]))
:style {:height (if in-progress? window-height 48)}}
[react/view {:style style/label-container}
(if in-progress?
+3 -3
View File
@@ -40,7 +40,7 @@
:title (i18n/label :t/delete-chat)
:accessibility-label :delete-chat-button
:icon :main-icons/delete
:on-press #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id])}]]))
:on-press #(re-frame/dispatch [:chat.ui/show-remove-confirmation chat-id])}]]))
(defn public-chat-accents
[chat-id]
@@ -69,13 +69,13 @@
:title (i18n/label :t/clear-history)
:accessibility-label :clear-history-button
:icon :main-icons/close
:on-press #(re-frame/dispatch [:chat.ui/clear-history-pressed chat-id])}]
:on-press #(re-frame/dispatch [:chat.ui/show-clear-history-confirmation chat-id])}]
[quo/list-item
{:theme :negative
:title (i18n/label :t/delete-chat)
:accessibility-label :delete-chat-button
:icon :main-icons/delete
:on-press #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id])}]]))
:on-press #(re-frame/dispatch [:chat.ui/show-remove-confirmation chat-id])}]]))
(defn community-chat-accents
[]
@@ -64,4 +64,5 @@
:accessory :text
:accessory-text (count pinned-messages)
:chevron true
:on-press #(re-frame/dispatch [:contact.ui/pinned-messages-pressed chat-id])}]])]))))
:on-press #(re-frame/dispatch [:chat.ui/navigate-to-pinned-messages
chat-id])}]])]))))
@@ -171,7 +171,7 @@
(assoc home-item :public? true)
{:on-press (fn []
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch [:chat.ui/navigate-to-chat-nav2 chat-id])
(rf/dispatch [:chat/navigate-to-chat chat-id])
(rf/dispatch [:search/home-filter-changed nil]))
:on-long-press #(rf/dispatch [:bottom-sheet/show-sheet
{:content (fn []
+9 -10
View File
@@ -7,7 +7,6 @@
[quo2.foundations.colors :as quo2.colors]
[re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im.add-new.core :as new-chat]
[status-im.add-new.db :as db]
[utils.i18n :as i18n]
[status-im.qr-scanner.core :as qr-scanner]
@@ -82,15 +81,15 @@
:on-blur (fn []
(when chats-empty
(re-frame/dispatch [:search/home-filter-changed nil]))
(re-frame/dispatch [::new-chat/clear-new-identity]))
(re-frame/dispatch [:contacts/clear-new-identity]))
:on-focus (fn [search-filter]
(when-not search-filter
(re-frame/dispatch [:search/home-filter-changed ""])
(re-frame/dispatch [::new-chat/clear-new-identity])))
(re-frame/dispatch [:contacts/clear-new-identity])))
:on-change (fn [text]
(re-frame/dispatch [:search/home-filter-changed text])
(re-frame/dispatch [:set-in [:contacts/new-identity :state] :searching])
(debounce/debounce-and-dispatch [:new-chat/set-new-identity text] 300))}]])
(debounce/debounce-and-dispatch [:contacts/set-new-identity text] 300))}]])
(defn search-input-wrapper-old
[search-filter chats-empty]
@@ -104,11 +103,11 @@
:on-blur (fn []
(when chats-empty
(re-frame/dispatch [:search/home-filter-changed nil]))
(re-frame/dispatch [::new-chat/clear-new-identity]))
(re-frame/dispatch [:contacts/clear-new-identity]))
:on-focus (fn [search-filter]
(when-not search-filter
(re-frame/dispatch [:search/home-filter-changed ""])
(re-frame/dispatch [::new-chat/clear-new-identity])))
(re-frame/dispatch [:contacts/clear-new-identity])))
:on-change (fn [text]
(re-frame/dispatch [:search/home-filter-changed text])
(re-frame/dispatch [:set-in [:contacts/new-identity :state] :searching])
@@ -151,7 +150,7 @@
home-item
{:on-press (fn []
(re-frame/dispatch [:dismiss-keyboard])
(re-frame/dispatch [:chat.ui/navigate-to-chat-nav2 chat-id])
(re-frame/dispatch [:chat/navigate-to-chat chat-id])
(re-frame/dispatch [:search/home-filter-changed nil]))
:on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet
{:content (fn []
@@ -166,7 +165,7 @@
home-item
{:on-press (fn []
(re-frame/dispatch [:dismiss-keyboard])
(re-frame/dispatch [:chat.ui/navigate-to-chat-nav2 chat-id])
(re-frame/dispatch [:chat/navigate-to-chat chat-id])
(re-frame/dispatch [:search/home-filter-changed nil]))
:on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet
{:content (fn []
@@ -193,7 +192,7 @@
[list/flat-list
{:key-fn chat-list-key-fn
:getItemLayout get-item-layout
:on-end-reached #(re-frame/dispatch [:chat.ui/show-more-chats])
:on-end-reached #(re-frame/dispatch [:chat/show-more-chats])
:keyboard-should-persist-taps :always
:data items
:render-fn render-fn
@@ -219,7 +218,7 @@
[list/flat-list
{:key-fn chat-list-key-fn
:getItemLayout get-item-layout
:on-end-reached #(re-frame/dispatch [:chat.ui/show-more-chats])
:on-end-reached #(re-frame/dispatch [:chat/show-more-chats])
:keyboard-should-persist-taps :always
:data items
:render-fn render-fn-old
@@ -5,12 +5,12 @@
[quo.design-system.colors :as colors]
[re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im.chat.models :as chat.models]
[utils.i18n :as i18n]
[status-im.multiaccounts.core :as multiaccounts]
[status-im.ui.components.chat-icon.screen :as chat-icon]
[status-im.ui.components.icons.icons :as icons]
[status-im.ui.components.keyboard-avoid-presentation :as kb-presentation]
[status-im.ui.components.profile-header.view :as profile-header]
[status-im.ui.components.react :as react]
[status-im.ui.components.toolbar :as toolbar]
[status-im.ui.components.topbar :as topbar]
@@ -24,7 +24,7 @@
(concat [{:label (i18n/label :t/chat)
:icon :main-icons/message
:disabled (not mutual?)
:action #(re-frame/dispatch [:contact.ui/send-message-pressed
:action #(re-frame/dispatch [:chat.ui/start-chat
{:public-key public-key
:ens-name ens-name}])
:accessibility-label :start-conversation-button}]
@@ -47,8 +47,7 @@
:selected muted?
:disabled blocked?
:action (when-not blocked?
#(re-frame/dispatch [::chat.models/mute-chat-toggled public-key
(not muted?)]))}]
#(re-frame/dispatch [:chat.ui/mute public-key (not muted?)]))}]
[{:label (i18n/label (if blocked? :t/unblock :t/block))
:negative true
:selected blocked?
@@ -101,7 +100,7 @@
:accessory :text
:accessory-text pin-count
:disabled (zero? pin-count)
:on-press #(re-frame/dispatch [:contact.ui/pinned-messages-pressed public-key])
:on-press #(re-frame/dispatch [:chat.ui/navigate-to-pinned-messages public-key])
:chevron true}])
(defn nickname-settings
@@ -207,6 +206,11 @@
[]
(let [{:keys [public-key name ens-verified] :as contact} @(re-frame/subscribe
[:contacts/current-contact])
muted? @(re-frame/subscribe [:chats/muted
public-key])
pinned-messages @(re-frame/subscribe [:chats/pinned
public-key])
[first-name second-name] (multiaccounts/contact-two-names contact true)
on-share #(re-frame/dispatch
[:show-popover
(merge
@@ -222,4 +226,27 @@
:on-press on-share}]
:left-accessories [{:icon :main-icons/close
:accessibility-label :back-button
:on-press #(re-frame/dispatch [:navigate-back])}]}]])))
:on-press #(re-frame/dispatch [:navigate-back])}]}]
[:<>
[(profile-header/extended-header
{:on-press on-share
:bottom-separator false
:title first-name
:photo (multiaccounts/displayed-photo contact)
:monospace (not ens-verified)
:subtitle second-name
:public-key public-key})]
[react/view
{:height 1 :background-color colors/gray-lighter :margin-top 8}]
[nickname-settings contact]
[pin-settings public-key (count pinned-messages)]
[react/view {:height 1 :background-color colors/gray-lighter}]
[react/view
{:padding-top 17
:flex-direction :row
:align-items :stretch
:flex 1}
(for [{:keys [label] :as action} (actions contact muted?)
:when label]
^{:key label}
[button-item action])]]])))
@@ -215,7 +215,7 @@
:accessory :text
:accessory-text (count pinned-messages)
:chevron true
:on-press #(re-frame/dispatch [:contact.ui/pinned-messages-pressed chat-id])}]
:on-press #(re-frame/dispatch [:chat.ui/navigate-to-pinned-messages chat-id])}]
(when member?
[quo/list-item
{:theme :negative
+2 -10
View File
@@ -1,7 +1,6 @@
(ns status-im.ui.screens.screens
(:require
[quo.design-system.colors :as colors]
[status-im.add-new.core :as new-chat.events]
[utils.i18n :as i18n]
[status-im.ui.components.icons.icons :as icons]
[status-im.ui.screens.about-app.views :as about-app]
@@ -545,7 +544,7 @@
;[Chat] New Chat
{:name :new-chat
:on-focus [::new-chat.events/new-chat-focus]
:on-focus [:contacts/new-chat-focus]
;;TODO accessories
:options {:topBar {:visible false}}
:component new-chat/new-chat}
@@ -569,7 +568,7 @@
:component contact/nickname}
{:name :new-chat-aio
:on-focus [::new-chat.events/new-chat-focus]
:on-focus [:contacts/new-chat-focus]
;;TODO accessories
:options {:topBar {:visible false}}
:component new-chat-aio/contact-selection-list}
@@ -595,13 +594,6 @@
:component communities.invite/invite
:insets {:bottom true}}
;New Contact
{:name :new-contact
:on-focus [::new-chat.events/new-chat-focus]
;;TODO accessories
:options {:topBar {:visible false}}
:component new-chat/new-contact}
;[Wallet] Recipient
{:name :recipient
:insets {:bottom true}
@@ -53,7 +53,7 @@
:on-change (fn [text]
(re-frame/dispatch [:search/recipient-filter-changed text])
(re-frame/dispatch [:set-in [:contacts/new-identity :state] :searching])
(debounce/debounce-and-dispatch [:new-chat/set-new-identity text] 300))}]]))
(debounce/debounce-and-dispatch [:contacts/set-new-identity text] 300))}]]))
(defn section
[_ _ _]
@@ -11,10 +11,10 @@
;; so we need some magic here with paddings so close button isn't cut
[rn/view {:padding-top 12 :padding-bottom 8 :padding-right 12}
[rn/image
{:source {:uri (first item)}
{:source {:uri (:uri (val item))}
:style style/small-image}]
[rn/touchable-opacity
{:on-press #(rf/dispatch [:chat.ui/image-unselected (first item)])
{:on-press (fn [] (rf/dispatch [:chat.ui/image-unselected (val item)]))
:style style/remove-photo-container
:hit-slop {:right 5
:left 5
@@ -1,22 +1,20 @@
(ns status-im.ui2.screens.chat.composer.input
(:require ["react-native" :as react-native]
[clojure.string :as string]
(:require [clojure.string :as string]
[oops.core :as oops]
[quo.design-system.colors :as quo.colors]
[quo.react]
[quo.react-native :as rn]
[quo2.foundations.colors :as colors]
[re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im2.constants :as chat.constants]
[status-im.chat.models.mentions :as mentions]
[utils.i18n :as i18n]
[status-im.ui.components.react :as react]
[utils.re-frame :as rf]
[status-im.utils.platform :as platform]
[status-im.utils.utils :as utils.utils]
[utils.transforms :as transforms]
[quo2.foundations.typography :as typography]))
[quo2.foundations.typography :as typography]
[react-native.background-timer :as background-timer]
[react-native.platform :as platform]
[react-native.core :as rn]
[react-native.clipboard :as clipboard]
[quo.react :as quo.react]))
(defonce input-texts (atom {}))
(defonce input-text-content-heights (atom {}))
@@ -94,7 +92,7 @@
(when platform/ios?
(reset!
timeout-id
(utils.utils/set-timeout
(background-timer/set-timeout
#(rf/dispatch [::mentions/on-selection-change
{:start start
:end end}
@@ -128,7 +126,7 @@
;; happens during typing because it is not needed for mention
;; suggestions calculation
(when (and platform/ios? @timeout-id)
(utils.utils/clear-timeout @timeout-id))
(background-timer/clear-timeout @timeout-id))
(when platform/android?
(reset! last-text-change (js/Date.now)))
@@ -169,7 +167,7 @@
:min-height 34
:margin 0
:flex-shrink 1
:color (:text-01 @quo.colors/theme)
:color (colors/theme-colors colors/neutral-100 colors/white)
:margin-horizontal 20}
(if platform/android?
{:padding-vertical 8
@@ -195,7 +193,7 @@
:blur-on-submit false
:auto-focus false
:max-length chat.constants/max-text-size
:placeholder-text-color (:text-02 @quo.colors/theme)
:placeholder-text-color (colors/theme-colors colors/neutral-40 colors/white-opa-30)
:placeholder (if cooldown-enabled?
(i18n/label :cooldown/text-input-disabled)
(i18n/label :t/type-a-message))
@@ -230,14 +228,6 @@
[rn/text-input props
children])))
(defn selectable-text-input-manager
[]
(when (exists? (.-NativeModules react-native))
(.-RNSelectableTextInputManager ^js (.-NativeModules react-native))))
(defonce rn-selectable-text-input
(reagent/adapt-react-class (.requireNativeComponent react-native "RNSelectableTextInput")))
(declare first-level-menu-items second-level-menu-items)
(defn update-input-text
@@ -269,24 +259,24 @@
;https://lightrun.com/answers/facebook-react-native-textinput-controlled-selection-broken-on-both-ios-and-android
;use native invoke instead! do not use setNativeProps! e.g. (.setNativeProps ^js text-input (clj->js
;{:selection {:start selection-start :end selection-end}}))
(let [manager (selectable-text-input-manager)]
(let [manager (rn/selectable-text-input-manager)]
(oops/ocall manager :setSelection text-input-handle selection-start selection-end)))
(def first-level-menus
{:cut (fn [{:keys [content] :as params}]
(let [new-text (calculate-input-text params "")]
(react/copy-to-clipboard content)
(clipboard/set-string content)
(update-input-text params new-text)))
:copy-to-clipboard (fn [{:keys [content]}]
(react/copy-to-clipboard content))
(clipboard/set-string content))
:paste (fn [params]
(let [callback (fn [paste-content]
(let [content (string/trim paste-content)
new-text (calculate-input-text params content)]
(update-input-text params new-text)))]
(react/get-from-clipboard callback)))
(clipboard/get-string callback)))
:biu (fn [{:keys [first-level text-input-handle menu-items selection-start
selection-end]}]
@@ -340,7 +330,7 @@
menu-items (reagent/atom first-level-menu-items)
first-level (reagent/atom true)
selection-event (atom nil)
manager (selectable-text-input-manager)]
manager (rn/selectable-text-input-manager)]
(reagent/create-class
{:component-did-mount
(fn [this]
@@ -395,6 +385,6 @@
:style (dissoc style :margin-horizontal)
:on-selection-change on-selection-change
:on-selection on-selection})]
[rn-selectable-text-input {:menuItems @menu-items :style style}
[rn/selectable-text-input {:menuItems @menu-items :style style}
[rn/text-input props
children]]))})))
@@ -1,39 +0,0 @@
(ns status-im.ui2.screens.chat.pin-limit-popover.style
(:require [quo2.foundations.colors :as colors]))
(defn pin-popover
[width]
{:width (- width 16)
:margin-left 8
:background-color (colors/theme-colors colors/neutral-80-opa-90 colors/white-opa-90)
:flex-direction :row
:border-radius 16
:padding 12})
(defn pin-alert-container
[]
{:background-color (colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-40)
:width 36
:height 36
:border-radius 18
:justify-content :center
:align-items :center})
(def pin-alert-circle
{:width 18
:height 18
:border-radius 9
:border-color colors/danger-50-opa-40
:border-width 1
:justify-content :center
:align-items :center})
(def view-pinned-messages
{:background-color colors/primary-60
:border-radius 8
:justify-content :center
:align-items :center
:padding-horizontal 8
:padding-vertical 4
:align-self :flex-start
:margin-top 10})
@@ -1,63 +0,0 @@
(ns status-im.ui2.screens.chat.pin-limit-popover.view
(:require [utils.i18n :as i18n]
[quo.react :as react]
[quo2.core :as quo]
[quo2.foundations.colors :as colors]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[status-im.ui2.screens.chat.pin-limit-popover.style :as style]
[utils.re-frame :as rf]))
;; TODO (flexsurfer) this should be an in-app notification component in quo2
;; https://github.com/status-im/status-mobile/issues/14527
(defn pin-limit-popover
[chat-id]
[:f>
(fn []
(let [width (rf/sub [:dimensions/window-width])
show-pin-limit-modal? (rf/sub [:chats/pin-modal chat-id])
opacity-animation (reanimated/use-shared-value 0)
z-index-animation (reanimated/use-shared-value -1)]
(react/effect!
#(do
(reanimated/set-shared-value opacity-animation
(reanimated/with-timing (if show-pin-limit-modal? 1 0)))
(reanimated/set-shared-value z-index-animation
(reanimated/with-timing (if show-pin-limit-modal? 10 -1)))))
(when show-pin-limit-modal?
[reanimated/view
{:style (reanimated/apply-animations-to-style
{:opacity opacity-animation
:z-index z-index-animation}
(style/pin-popover width))
:accessibility-label :pin-limit-popover}
[rn/view {:style (style/pin-alert-container)}
[rn/view {:style style/pin-alert-circle}
[rn/text {:style {:color colors/danger-50}} "!"]]]
[rn/view {:style {:margin-left 8}}
[quo/text {:weight :semi-bold :color (colors/theme-colors colors/white colors/neutral-100)}
(i18n/label :t/cannot-pin-title)]
[quo/text {:size :paragraph-2 :color (colors/theme-colors colors/white colors/neutral-100)}
(i18n/label :t/cannot-pin-desc)]
[rn/touchable-opacity
{:accessibility-label :view-pinned-messages
:active-opacity 1
:on-press (fn []
(rf/dispatch [:pin-message/hide-pin-limit-modal chat-id])
(rf/dispatch [:bottom-sheet/show-sheet :pinned-messages-list
chat-id]))
:style style/view-pinned-messages}
[quo/text {:size :paragraph-2 :weight :medium :color colors/white}
(i18n/label :t/view-pinned-messages)]]]
[rn/touchable-opacity
{:accessibility-label :close-pin-limit-popover
:active-opacity 1
:on-press #(rf/dispatch [:pin-message/hide-pin-limit-modal chat-id])
:style {:position :absolute
:top 16
:right 16}}
[quo/icon :i/close
{:color (colors/theme-colors colors/white colors/neutral-100)
:size 12}]]])))])
-5
View File
@@ -1,5 +0,0 @@
(ns status-im.utils.db)
(defn valid-public-key?
[s]
(and (string? s) (not-empty s) (boolean (re-matches #"0x04[0-9a-f]{128}" s))))
+1 -1
View File
@@ -13,7 +13,7 @@
[utils.datetime :as datetime]
[status-im.utils.platform :as platform]
[status-im.utils.types :as types]
[status-im2.setup.log :as log]))
[status-im2.common.log :as log]))
(def report-email "error-reports@status.im")
@@ -65,7 +65,7 @@
(log/info "universal-links: handling private chat" chat-id)
(when chat-id
(if-not (new-chat.db/own-public-key? db chat-id)
(chat/start-chat cofx chat-id nil)
{:dispatch [:chat.ui/start-chat chat-id]}
{:utils/show-popup {:title (i18n/label :t/unable-to-read-this-code)
:content (i18n/label :t/can-not-add-yourself)}})))
@@ -96,7 +96,7 @@
(rf/defn handle-community-chat
[cofx {:keys [chat-id]}]
(log/info "universal-links: handling community chat" chat-id)
{:dispatch [:chat.ui/navigate-to-chat-nav2 chat-id]})
{:dispatch [:chat/navigate-to-chat chat-id]})
(rf/defn handle-public-chat
[cofx {:keys [topic]}]
+37 -26
View File
@@ -83,6 +83,16 @@
(and @expanded? (< end-pan-y collapse-threshold))
(reset! expanded? false))))))))
(defn handle-comp
[window-width]
[rn/view
{:style {:width window-width
:position :absolute
:background-color :transparent
:top 0
:height 20}}
[rn/view {:style (styles/handle)}]])
(defn bottom-sheet
[props children]
(let [{on-cancel :on-cancel
@@ -140,7 +150,9 @@
expandable?
show-bottom-sheet?
expanded?
close-bottom-sheet)]
close-bottom-sheet)
handle-comp [gesture/gesture-detector {:gesture bottom-sheet-gesture}
[handle-comp window-width]]]
(react/effect! #(do
(cond
@@ -200,28 +212,27 @@
{:style (reanimated/apply-animations-to-style
{:opacity bg-opacity}
styles/backdrop)}]]
[gesture/gesture-detector {:gesture bottom-sheet-gesture}
[reanimated/view
{:style (reanimated/apply-animations-to-style
{:transform [{:translateY translate-y}]}
{:width window-width
:height window-height})}
[rn/view {:style styles/container}
(when selected-item
[rn/view {:style (styles/selected-background)}
[selected-item]])
[rn/view {:style (styles/background)}
[rn/keyboard-avoiding-view
{:behaviour (if platform/ios? :padding :height)
:style {:flex 1}}
[rn/view
{:style (styles/content-style insets)
:on-layout (when-not (and
(some? @content-height)
(> @content-height 0))
on-content-layout)}
children]]
(when show-handle?
[rn/view {:style (styles/handle)}])]]]]]))])]))
(cond->> [reanimated/view
{:style (reanimated/apply-animations-to-style
{:transform [{:translateY translate-y}]}
{:width window-width
:height window-height})}
[rn/view {:style styles/container}
(when selected-item
[rn/view {:style (styles/selected-background)}
[selected-item]])
[rn/view {:style (styles/background)}
[rn/keyboard-avoiding-view
{:behaviour (if platform/ios? :padding :height)
:style {:flex 1}}
[rn/view
{:style (styles/content-style insets)
:on-layout (when-not (and
(some? @content-height)
(> @content-height 0))
on-content-layout)}
children]]
(when show-handle?
handle-comp)]]]
(not show-handle?)
(conj [gesture/gesture-detector {:gesture bottom-sheet-gesture}]))]))])]))
+11 -25
View File
@@ -1,9 +1,7 @@
(ns status-im2.common.home.actions.view
(:require [utils.i18n :as i18n]
[quo2.components.drawers.action-drawers :as drawer]
[status-im.chat.models :as chat.models]
[status-im2.common.confirmation-drawer.view :as confirmation-drawer] ;;TODO move to
;;status-im2
[quo2.core :as quo]
[status-im2.common.confirmation-drawer.view :as confirmation-drawer]
[status-im2.constants :as constants]
[utils.re-frame :as rf]))
@@ -43,11 +41,11 @@
(defn mute-chat-action
[chat-id]
(hide-sheet-and-dispatch [::chat.models/mute-chat-toggled chat-id true]))
(hide-sheet-and-dispatch [:chat.ui/mute chat-id true]))
(defn unmute-chat-action
[chat-id]
(hide-sheet-and-dispatch [::chat.models/mute-chat-toggled chat-id false]))
(hide-sheet-and-dispatch [:chat.ui/mute chat-id false]))
(defn clear-history-action
[{:keys [chat-id] :as item}]
@@ -219,17 +217,6 @@
:sub-label nil
:chevron? true}))
;; TODO(OmarBasem): Requires design input.
(defn pinned-messages-entry
[]
(entry {:icon :i/pin
:label (i18n/label :t/pinned-messages)
:on-press #(js/alert "TODO: to be implemented, requires design input")
:danger? false
:accessibility-label :pinned-messages
:sub-label nil
:chevron? true}))
(defn remove-from-contacts-entry
[contact]
(entry {:icon :i/remove-user
@@ -385,9 +372,8 @@
[(mark-as-read-entry chat-id)
(mute-chat-entry chat-id)
(notifications-entry false)
(if inside-chat?
(fetch-messages-entry)
(pinned-messages-entry))
(when inside-chat?
(fetch-messages-entry))
(when (or (not group-chat) public?)
(show-qr-entry))
(when-not group-chat
@@ -409,7 +395,7 @@
(defn one-to-one-actions
[{:keys [chat-id] :as item} inside-chat?]
[drawer/action-drawer
[quo/action-drawer
[[(view-profile-entry chat-id)
(edit-nickname-entry chat-id)]
(notification-actions item inside-chat?)
@@ -417,7 +403,7 @@
(defn public-chat-actions
[{:keys [chat-id] :as item} inside-chat?]
[drawer/action-drawer
[quo/action-drawer
[[(group-details-entry chat-id)
(when inside-chat?
(add-members-entry))]
@@ -426,7 +412,7 @@
(defn private-group-chat-actions
[item inside-chat?]
[drawer/action-drawer
[quo/action-drawer
[(group-actions item inside-chat?)
(notification-actions item inside-chat?)
(destructive-actions item)]])
@@ -434,7 +420,7 @@
(defn contact-actions
[{:keys [public-key] :as contact} {:keys [chat-id admin?] :as extra-data}]
(let [current-pub-key (rf/sub [:multiaccount/public-key])]
[drawer/action-drawer
[quo/action-drawer
[[(view-profile-entry public-key)
(remove-from-contacts-entry contact)
(rename-entry)
@@ -462,7 +448,7 @@
[{:keys [admins] :as group}]
(let [current-pub-key (rf/sub [:multiaccount/public-key])
admin? (get admins current-pub-key)]
[drawer/action-drawer
[quo/action-drawer
[(when admin? [(edit-name-image-entry)])
[(notifications-entry admin?)]
(destructive-actions group)]]))

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