diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 59029b199dbd..4e8b32a1e2db 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,5 +1,6 @@ {:lint-as {status-im.utils.views/defview clojure.core/defn status-im.utils.views/letsubs clojure.core/let + reagent.core/with-let clojkure.core/let status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all quo.react/with-deps-check clojure.core/fn quo.previews.preview/list-comp clojure.core/for diff --git a/.env.e2e b/.env.e2e index 6aa2afec4345..0c9ac735e1b0 100644 --- a/.env.e2e +++ b/.env.e2e @@ -27,5 +27,5 @@ MAX_IMAGES_BATCH=5 APN_TOPIC=im.status.ethereum.pr VERIFY_TRANSACTION_CHAIN_ID=3 COMMUNITIES_ENABLED=1 -COMMUNITIES_MANAGEMENT_ENABLED=0 DATABASE_MANAGEMENT_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=1 diff --git a/.env.jenkins b/.env.jenkins index a72164606ae5..885a120516c6 100644 --- a/.env.jenkins +++ b/.env.jenkins @@ -27,5 +27,5 @@ BLANK_PREVIEW=0 MAX_IMAGES_BATCH=5 GOOGLE_FREE=0 COMMUNITIES_ENABLED=1 -COMMUNITIES_MANAGEMENT_ENABLED=0 DATABASE_MANAGEMENT_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=1 diff --git a/.env.nightly b/.env.nightly index 6e9693b1c8d0..2bc1be05f882 100644 --- a/.env.nightly +++ b/.env.nightly @@ -22,3 +22,4 @@ MAX_IMAGES_BATCH=5 BLANK_PREVIEW=0 COMMUNITIES_ENABLED=1 DATABASE_MANAGEMENT_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=1 diff --git a/RELEASES.md b/RELEASES.md index 5937fcb5e90e..1f10485f2315 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,47 @@ +## 1.11 +### iOS +#### Excerpt +V1.11 also makes some improvements to the overall Status experience. Improved compatibility with DApps creates a more seamless experience with your favorite DeFi, NFT, and other decentralized applications. Improvements to Status Nodes and fixes to Android notifications have been made for better performance with private, group chats and public chats. + +Update in the App Store or Google Play if you do not have auto updates enabled. + +For the full changelog, see Github https://github.com/status-im/status-react/milestone/49?closed=1 + +#### Added +- Add Giphy url support in chat +- The Graph (GRT) erc-20 token added to the default list + +#### Changed +- Improved compatibility with DApps +- Sync with Status Nodes improvements + +#### Fixed +- Fixes to Android notifications +- UI fixes +- Sluggish performance on private group chats bug + +### Android +#### Excerpt +V1.11 also makes some improvements to the overall Status experience. Improved compatibility with DApps creates a more seamless experience with your favorite DeFi, NFT, and other decentralized applications. Improvements to Status Nodes and fixes to Android notifications have been made for better performance with private, group chats and public chats. + +Update in the App Store or Google Play if you do not have auto updates enabled. + +For the full changelog, see Github https://github.com/status-im/status-react/milestone/49?closed=1 + +#### Added +- Migrate existing account to Keycard +- Add Giphy url support in chat +- The Graph (GRT) erc-20 token added to the default list + +#### Changed +- Improved compatibility with DApps +- Sync with Status Nodes improvements + +#### Fixed +- Fixes to Android notifications +- UI fixes +- Sluggish performance on private group chats bug + ## 1.10 ### iOS #### Excerpt diff --git a/ci/Jenkinsfile.combined b/ci/Jenkinsfile.combined index 736dbc71ca18..26cdb4a00aaa 100644 --- a/ci/Jenkinsfile.combined +++ b/ci/Jenkinsfile.combined @@ -26,7 +26,7 @@ pipeline { booleanParam( name: 'PUBLISH', description: 'Trigger publishing of build results for nightly or release.', - defaultValue: params.PUBLISH ?: false, + defaultValue: getPublishDefault(params.PUBLISH), ) } @@ -120,3 +120,13 @@ def List genChoices(String previousChoice, List defaultChoices) { choices.add(0, previousChoice) return choices } + +/* Helper that makes PUBLISH default to 'false' unless: + * - The build is for a release branch + * - A user explicitly specified a value + * Since release builds create and re-create GitHub drafts every time. */ +def Boolean getPublishDefault(Boolean previousValue) { + if (env.JOB_NAME.startsWith('status-react/release')) { return true } + if (previousValue != null) { return previousValue } + return false +} diff --git a/doc/RELEASE_CHECKLIST.md b/doc/RELEASE_CHECKLIST.md index 089e38a1fe68..5bf34cf618b2 100644 --- a/doc/RELEASE_CHECKLIST.md +++ b/doc/RELEASE_CHECKLIST.md @@ -8,9 +8,14 @@ - [ ] Privacy policy reviewed and updated - [ ] App translations for key features merged and tested - [ ] Countries to be excluded +- [ ] Changes to our FAQ made #### Before publishing - [ ] Translations for comms available - [ ] Promotional content ready - [ ] Update Draft with release on https://github.com/status-im/status-react/releases - [ ] Add release to DO store and [update site](https://notes.status.im/s/status-release-upload#) (Jakub - requires credentials to upload) + +#### At time of publishing +- [ ] validate draft release on github release page https://github.com/status-im/status-react/releases +- [ ] update https://github.com/status-im/status-react/blob/develop/RELEASES.md with release notes diff --git a/ios/Bridge.swift b/ios/Bridge.swift new file mode 100644 index 000000000000..56b71f2f9ab0 --- /dev/null +++ b/ios/Bridge.swift @@ -0,0 +1,9 @@ +// +// Bridge.swift +// StatusIm +// +// Created by Andrea Franz on 02/12/2020. +// Copyright © 2020 Status. All rights reserved. +// + +import Foundation diff --git a/ios/Podfile b/ios/Podfile index 4d7348123a6d..80fb99be2560 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -19,16 +19,20 @@ abstract_target 'Status' do pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone.podspec" pod 'Permission-Camera', :path => "#{permissions_path}/Camera.podspec" + pod "react-native-status-keycard", path: "../node_modules/react-native-status-keycard" + pod "Keycard", git: "https://github.com/status-im/Keycard.swift.git" + pod 'secp256k1', git: "https://github.com/status-im/secp256k1.swift.git", submodules: true + target 'StatusIm' do target 'StatusImTests' do inherit! :complete # Pods for testing end end - + target 'StatusImPR' do end - + use_flipper! post_install do |installer| flipper_post_install(installer) @@ -36,4 +40,5 @@ abstract_target 'Status' do use_native_modules! -end \ No newline at end of file +end + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4c2adbca6fb0..99b65012f379 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,8 +2,9 @@ PODS: - boost-for-react-native (1.63.0) - BVLinearGradient (2.5.6): - React - - CocoaAsyncSocket (7.6.4) + - CocoaAsyncSocket (7.6.5) - CocoaLibEvent (1.0.0) + - CryptoSwift (1.3.8) - DoubleConversion (1.1.6) - FBLazyVector (0.63.4) - FBReactNativeSpec (0.63.4): @@ -69,6 +70,18 @@ PODS: - DoubleConversion - glog - glog (0.3.5) + - Keycard (3.0.4): + - CryptoSwift + - secp256k1 + - libwebp (1.1.0): + - libwebp/demux (= 1.1.0) + - libwebp/mux (= 1.1.0) + - libwebp/webp (= 1.1.0) + - libwebp/demux (1.1.0): + - libwebp/webp + - libwebp/mux (1.1.0): + - libwebp/demux + - libwebp/webp (1.1.0) - OpenSSL-Universal (1.0.2.20): - OpenSSL-Universal/Static (= 1.0.2.20) - OpenSSL-Universal/Static (1.0.2.20) @@ -266,6 +279,9 @@ PODS: - React - react-native-splash-screen (3.2.0): - React + - react-native-status-keycard (2.5.30): + - Keycard + - React - react-native-webview (10.9.2): - React-Core - React-RCTActionSheet (0.63.4): @@ -369,13 +385,20 @@ PODS: - React - RNSVG (9.13.6): - React + - SDWebImage (5.10.2): + - SDWebImage/Core (= 5.10.2) + - SDWebImage/Core (5.10.2) + - SDWebImageWebPCoder (0.6.1): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.7) + - secp256k1 (0.1.6) - SQLCipher (3.4.2): - SQLCipher/standard (= 3.4.2) - SQLCipher/common (3.4.2) - SQLCipher/standard (3.4.2): - SQLCipher/common - SSZipArchive (2.2.3) - - TOCropViewController (2.5.5) + - TOCropViewController (2.6.0) - TouchID (4.4.1): - React - Yoga (1.14.0) @@ -408,6 +431,7 @@ DEPENDENCIES: - FlipperKit/SKIOSNetworkPlugin (~> 0.54.0) - Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - Keycard (from `https://github.com/status-im/Keycard.swift.git`) - Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera.podspec`) - Permission-Microphone (from `../node_modules/react-native-permissions/ios/Microphone.podspec`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) @@ -433,6 +457,7 @@ DEPENDENCIES: - react-native-shake (from `../node_modules/react-native-shake`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) + - react-native-status-keycard (from `../node_modules/react-native-status-keycard`) - react-native-webview (from `../node_modules/react-native-webview`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -461,6 +486,7 @@ DEPENDENCIES: - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) + - secp256k1 (from `https://github.com/status-im/secp256k1.swift.git`) - SQLCipher (~> 3.0) - SSZipArchive - TouchID (from `../node_modules/react-native-touch-id`) @@ -471,6 +497,7 @@ SPEC REPOS: - boost-for-react-native - CocoaAsyncSocket - CocoaLibEvent + - CryptoSwift - Flipper - Flipper-DoubleConversion - Flipper-Folly @@ -497,6 +524,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + Keycard: + :git: https://github.com/status-im/Keycard.swift.git Permission-Camera: :path: "../node_modules/react-native-permissions/ios/Camera.podspec" Permission-Microphone: @@ -543,6 +572,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/slider" react-native-splash-screen: :path: "../node_modules/react-native-splash-screen" + react-native-status-keycard: + :path: "../node_modules/react-native-status-keycard" react-native-webview: :path: "../node_modules/react-native-webview" React-RCTActionSheet: @@ -599,16 +630,29 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-screens" RNSVG: :path: "../node_modules/react-native-svg" + secp256k1: + :git: https://github.com/status-im/secp256k1.swift.git + :submodules: true TouchID: :path: "../node_modules/react-native-touch-id" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + Keycard: + :commit: 36e260cfafc2755a47f1e5f542858ceb0c6c37df + :git: https://github.com/status-im/Keycard.swift.git + secp256k1: + :commit: 46a1fa30d9b8babeae85ff519050f42394ab5fcc + :git: https://github.com/status-im/secp256k1.swift.git + :submodules: true + SPEC CHECKSUMS: boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 - CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845 + CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f + CryptoSwift: 01b0f0cba1d5c212e5a335ff6c054fb75a204f00 DoubleConversion: cde416483dac037923206447da6e1454df403714 FBLazyVector: 3bb422f41b18121b71783a905c10e58606f7dc3e FBReactNativeSpec: f2c97f2529dd79c083355182cc158c9f98f4bd6e @@ -621,6 +665,8 @@ SPEC CHECKSUMS: FlipperKit: ab353d41aea8aae2ea6daaf813e67496642f3d7d Folly: b73c3869541e86821df3c387eb0af5f65addfab4 glog: cee4319f395bad5865ef3f32466c2e0ae677432c + Keycard: dd96182888da0aacf4de821b641103143bbb26cc + libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3 OpenSSL-Universal: ff34003318d5e1163e9529b08470708e389ffcdd Permission-Camera: afad27bf90337684d4a86f3825112d648c8c4d3b Permission-Microphone: 0ffabc3fe1c75cfb260525ee3f529383c9f4368c @@ -645,6 +691,7 @@ SPEC CHECKSUMS: react-native-shake: de052eaa3eadc4a326b8ddd7ac80c06e8d84528c react-native-slider: 12bd76d3d568c9c5500825db54123d44b48e4ad4 react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 + react-native-status-keycard: a001766cf8f27de56406ac712a52a982f1f28744 react-native-webview: 4e96d493f9f90ba4f03b28933f30b2964df07e39 React-RCTActionSheet: 89a0ca9f4a06c1f93c26067af074ccdce0f40336 React-RCTAnimation: 1bde3ecc0c104c55df246eda516e0deb03c4e49b @@ -673,13 +720,16 @@ SPEC CHECKSUMS: RNReanimated: 89f5e0a04d1dd52fbf27e7e7030d8f80a646a3fc RNScreens: b748efec66e095134c7166ca333b628cd7e6f3e2 RNSVG: 8ba35cbeb385a52fd960fd28db9d7d18b4c2974f + SDWebImage: b969dcfc02c40a5da71eac0b03b8f1a0c794a86f + SDWebImageWebPCoder: d0dac55073088d24b2ac1b191a71a8f8d0adac21 + secp256k1: f61d67e6fdcb85fd727acf1bf35ace6036db540c SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990 SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9 - TOCropViewController: da59f531f8ac8a94ef6d6c0fc34009350f9e8bfe + TOCropViewController: 3105367e808b7d3d886a74ff59bf4804e7d3ab38 TouchID: ba4c656d849cceabc2e4eef722dea5e55959ecf4 Yoga: 4bd86afe9883422a7c4028c00e34790f560923d6 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 8752b77562edc2969e7016627fa83c23a152cf3c +PODFILE CHECKSUM: 5d4b89aa09f8d53bc530d173622952e85d5d7662 -COCOAPODS: 1.10.0 +COCOAPODS: 1.10.1 diff --git a/ios/StatusIm.xcodeproj/project.pbxproj b/ios/StatusIm.xcodeproj/project.pbxproj index 910988255f14..401a9c51b1ab 100644 --- a/ios/StatusIm.xcodeproj/project.pbxproj +++ b/ios/StatusIm.xcodeproj/project.pbxproj @@ -40,6 +40,9 @@ 57C854A7993C47A3B1AECD32 /* Inter-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = C6B1215047604CD59A4C74D6 /* Inter-MediumItalic.otf */; }; 68E19BBDF749E72ED1F9DEF5 /* libPods-Status-StatusIm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 132CE3B093884B0FB239A0AB /* libPods-Status-StatusIm.a */; }; 6C137817D5298C82BC79177C /* libPods-Status-StatusImPR.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D4CB5416994C913628B8754 /* libPods-Status-StatusImPR.a */; }; + 65F6941925780A4F00A45E76 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F6941825780A4F00A45E76 /* Bridge.swift */; }; + 65F6941A25780A4F00A45E76 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F6941825780A4F00A45E76 /* Bridge.swift */; }; + 65F6941B25780A4F00A45E76 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F6941825780A4F00A45E76 /* Bridge.swift */; }; 70ADBB5ECF934DCF8A0E4919 /* Inter-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 1426DF592BA248FC81D955CB /* Inter-Regular.otf */; }; 74B758FC20D7C00B003343C3 /* launch-image-universal.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74B758FB20D7C00B003343C3 /* launch-image-universal.storyboard */; }; 8391E8E0E93C41A98AAA6631 /* Inter-SemiBoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = A4F2BBE8D4DD4140A6CCAC39 /* Inter-SemiBoldItalic.otf */; }; @@ -124,6 +127,11 @@ 4C16DE0B1F89508700AA10DB /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 4E586E1B0E544F64AA9F5BD1 /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 5D4CB5416994C913628B8754 /* libPods-Status-StatusImPR.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Status-StatusImPR.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5E9C2936B3890356C5BE1078 /* Pods-StatusIm-StatusImTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StatusIm-StatusImTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-StatusIm-StatusImTests/Pods-StatusIm-StatusImTests.debug.xcconfig"; sourceTree = ""; }; + 65F693BD2578002500A45E76 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; }; + 65F693BF2578003600A45E76 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/iOSSupport/System/Library/Frameworks/CoreNFC.framework; sourceTree = DEVELOPER_DIR; }; + 65F6941725780A4E00A45E76 /* StatusImTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StatusImTests-Bridging-Header.h"; sourceTree = ""; }; + 65F6941825780A4F00A45E76 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = ""; }; 69291A6222A63434694EA2A6 /* Pods-Status-StatusImPR.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status-StatusImPR.release.xcconfig"; path = "Pods/Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR.release.xcconfig"; sourceTree = ""; }; 693A62DB37BC4CD5A30E5C96 /* Inter-SemiBold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-SemiBold.otf"; path = "../resources/fonts/Inter-SemiBold.otf"; sourceTree = ""; }; 74B758FB20D7C00B003343C3 /* launch-image-universal.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "launch-image-universal.storyboard"; sourceTree = ""; }; @@ -279,6 +287,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 65F6941825780A4F00A45E76 /* Bridge.swift */, 3AAD2AB624A3A5F10075D594 /* StatusImPR */, 13B07FAE1A68108700A75B9A /* StatusIm */, 832341AE1AAA6A7D00B99B32 /* Libraries */, @@ -287,6 +296,7 @@ A97BA941B2FB44B4B66EE6D3 /* Frameworks */, 1E7837547A9A40E18AD63CF3 /* Resources */, 5C1C8762251D6EF495FB2384 /* Pods */, + 65F6941725780A4E00A45E76 /* StatusImTests-Bridging-Header.h */, ); indentWidth = 2; sourceTree = ""; @@ -314,6 +324,8 @@ A97BA941B2FB44B4B66EE6D3 /* Frameworks */ = { isa = PBXGroup; children = ( + 65F693BD2578002500A45E76 /* CoreNFC.framework */, + 65F693BF2578003600A45E76 /* CoreNFC.framework */, 3A8F8EA924A4D31600BF206D /* GameKit.framework */, 4C16DE0B1F89508700AA10DB /* JavaScriptCore.framework */, B24FC7FE1DE7195F00D694FF /* MessageUI.framework */, @@ -724,6 +736,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 65F6941A25780A4F00A45E76 /* Bridge.swift in Sources */, 00E356F31AD99517003FC87E /* StatusImTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -732,6 +745,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 65F6941925780A4F00A45E76 /* Bridge.swift in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 3A2626CF245C3F2200D5F94B /* Dummy.swift in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, @@ -742,6 +756,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 65F6941B25780A4F00A45E76 /* Bridge.swift in Sources */, 3AAD2ABC24A3A60E0075D594 /* AppDelegate.m in Sources */, 3AAD2ABD24A3A60E0075D594 /* Dummy.swift in Sources */, 3AAD2ABE24A3A60E0075D594 /* main.m in Sources */, @@ -766,6 +781,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_ID_SUFFIX = .debug; BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; DEVELOPMENT_TEAM = DTX7Z4U3YA; FRAMEWORK_SEARCH_PATHS = ( @@ -777,13 +793,16 @@ "$(inherited)", ); INFOPLIST_FILE = StatusImTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "StatusImTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StatusIm.app/StatusIm"; }; name = Debug; @@ -795,6 +814,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_ID_SUFFIX = ""; BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COPY_PHASE_STRIP = NO; DEVELOPMENT_TEAM = DTX7Z4U3YA; @@ -803,13 +823,15 @@ "$(inherited)", ); INFOPLIST_FILE = StatusImTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = im.status.ethereum; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "StatusImTests-Bridging-Header.h"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StatusIm.app/StatusIm"; }; name = Release; @@ -1137,7 +1159,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-splash-screen/ios/**", ); - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LIBRARY_SEARCH_PATHS = ( "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", @@ -1211,7 +1233,7 @@ "$(SRCROOT)/../node_modules/react-native-image-resizer/ios/RCTImageResizer", "$(SRCROOT)/../node_modules/react-native-splash-screen/ios/**", ); - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LIBRARY_SEARCH_PATHS = ( "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"", diff --git a/ios/StatusIm/Info.plist b/ios/StatusIm/Info.plist index 741e2746e6a1..bc9264967ddf 100644 --- a/ios/StatusIm/Info.plist +++ b/ios/StatusIm/Info.plist @@ -127,5 +127,12 @@ UIViewControllerBasedStatusBarAppearance + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A00000080400010101 + A00000080400010301 + + NFCReaderUsageDescription + Enable Keycard diff --git a/ios/StatusIm/StatusIm.entitlements b/ios/StatusIm/StatusIm.entitlements index 0407edb0e512..00074aa87262 100644 --- a/ios/StatusIm/StatusIm.entitlements +++ b/ios/StatusIm/StatusIm.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.nfc.readersession.formats + + NDEF + TAG + aps-environment development com.apple.developer.associated-domains diff --git a/ios/StatusImPR/Info.plist b/ios/StatusImPR/Info.plist index 05ec5f0ef1df..b93d2c175cc9 100644 --- a/ios/StatusImPR/Info.plist +++ b/ios/StatusImPR/Info.plist @@ -133,5 +133,12 @@ UIViewControllerBasedStatusBarAppearance + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A00000080400010101 + A00000080400010301 + + NFCReaderUsageDescription + Enable Keycard diff --git a/ios/StatusImPR/StatusImPR.entitlements b/ios/StatusImPR/StatusImPR.entitlements index 88fdb457af71..cc0f12eac464 100644 --- a/ios/StatusImPR/StatusImPR.entitlements +++ b/ios/StatusImPR/StatusImPR.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.nfc.readersession.formats + + NDEF + TAG + aps-environment development com.apple.developer.associated-domains diff --git a/ios/StatusImTests-Bridging-Header.h b/ios/StatusImTests-Bridging-Header.h new file mode 100644 index 000000000000..1b2cb5d6d09f --- /dev/null +++ b/ios/StatusImTests-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/ios/StatusImTests/Info.plist b/ios/StatusImTests/Info.plist index 48e836043587..51031e10b169 100644 --- a/ios/StatusImTests/Info.plist +++ b/ios/StatusImTests/Info.plist @@ -20,5 +20,12 @@ ???? CFBundleVersion 1 + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A00000080400010101 + A00000080400010301 + + NFCReaderUsageDescription + Enable Keycard diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index 015a1f52f7ae..7c53f00633f0 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -122,7 +122,7 @@ - (void)handleSignal:(NSString *)signal NSURL *commonKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"]; NSURL *keystoreDir = [commonKeystoreDir URLByAppendingPathComponent:keyUID]; - + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { @@ -301,6 +301,37 @@ - (void)handleSignal:(NSString *)signal callback(@[result]); } +//////////////////////////////////////////////////////////////////// hashMessage +RCT_EXPORT_METHOD(hashMessage:(NSString *)message + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"hashMessage() method called"); +#endif + NSString *result = StatusgoHashMessage(message); + callback(@[result]); +} + +//////////////////////////////////////////////////////////////////// hashTypedData +RCT_EXPORT_METHOD(hashTypedData:(NSString *)data + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"hashTypedData() method called"); +#endif + NSString *result = StatusgoHashTypedData(data); + callback(@[result]); +} + +//////////////////////////////////////////////////////////////////// sendTransactionWithSignature +RCT_EXPORT_METHOD(sendTransactionWithSignature:(NSString *)txArgsJSON + signature:(NSString *)signature + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"sendTransactionWithSignature() method called"); +#endif + NSString *result = StatusgoSendTransactionWithSignature(txArgsJSON, signature); + callback(@[result]); +} + //////////////////////////////////////////////////////////////////// multiAccountImportMnemonic RCT_EXPORT_METHOD(multiAccountImportMnemonic:(NSString *)json callback:(RCTResponseSenderBlock)callback) { @@ -327,7 +358,7 @@ -(NSString *) getKeyUID:(NSString *)jsonString { JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; - + return [json valueForKey:@"key-uid"]; } @@ -456,10 +487,10 @@ - (NSURL *) getKeyStoreDir:(NSString *)keyUID { NSURL *rootUrl =[[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject]; - + NSURL *oldKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"]; NSURL *multiaccountKeystoreDir = [oldKeystoreDir URLByAppendingPathComponent:keyUID]; - + return multiaccountKeystoreDir; } @@ -473,12 +504,12 @@ - (void) migrateKeystore:(NSString *)accountData NSString *keyUID = [self getKeyUID:accountData]; NSURL *oldKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"]; NSURL *multiaccountKeystoreDir = [self getKeyStoreDir:keyUID]; - + NSArray *keys = [fileManager contentsOfDirectoryAtPath:multiaccountKeystoreDir.path error:nil]; if (keys.count == 0) { NSString *migrationResult = StatusgoMigrateKeyStoreDir(accountData, password, oldKeystoreDir.path, multiaccountKeystoreDir.path); NSLog(@"keystore migration result %@", migrationResult); - + NSString *initKeystoreResult = StatusgoInitKeystore(multiaccountKeystoreDir.path); NSLog(@"InitKeyStore result %@", initKeystoreResult); } @@ -503,7 +534,7 @@ - (void) migrateKeystore:(NSString *)accountData NSLog(@"LoginWithKeycard() method called"); #endif [self migrateKeystore:accountData password:password]; - + NSString *result = StatusgoLoginWithKeycard(accountData, password, chatKey); NSLog(@"%@", result); diff --git a/package.json b/package.json index 6ad186b1fd37..2860ac55f639 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "start": "react-native start", + "ios": "react-native run-ios", "app:compile:android": "shadow-cljs compile android", "app:watch": "shadow-cljs watch android", "app:packager": "react-native start --host 0.0.0.0 --port 8081", @@ -60,7 +61,7 @@ "react-native-screens": "^2.10.1", "react-native-shake": "^3.3.1", "react-native-splash-screen": "^3.2.0", - "react-native-status-keycard": "git+https://github.com/status-im/react-native-status-keycard.git#v2.5.28", + "react-native-status-keycard": "git+https://github.com/status-im/react-native-status-keycard.git#v2.5.31", "react-native-svg": "^9.8.4", "react-native-touch-id": "^4.4.1", "react-native-webview": "git+https://github.com/status-im/react-native-webview.git#v10.9.2", diff --git a/resources/images/icons/activity@2x.png b/resources/images/icons/activity@2x.png new file mode 100644 index 000000000000..3e972cb58004 Binary files /dev/null and b/resources/images/icons/activity@2x.png differ diff --git a/resources/images/icons/activity@3x.png b/resources/images/icons/activity@3x.png new file mode 100644 index 000000000000..628aac87fd79 Binary files /dev/null and b/resources/images/icons/activity@3x.png differ diff --git a/resources/images/icons/animals-nature@2x.png b/resources/images/icons/animals-nature@2x.png new file mode 100644 index 000000000000..162182d359ff Binary files /dev/null and b/resources/images/icons/animals-nature@2x.png differ diff --git a/resources/images/icons/animals-nature@3x.png b/resources/images/icons/animals-nature@3x.png new file mode 100644 index 000000000000..760fbb6d543e Binary files /dev/null and b/resources/images/icons/animals-nature@3x.png differ diff --git a/resources/images/icons/flags@2x.png b/resources/images/icons/flags@2x.png new file mode 100644 index 000000000000..41426612bdd5 Binary files /dev/null and b/resources/images/icons/flags@2x.png differ diff --git a/resources/images/icons/flags@3x.png b/resources/images/icons/flags@3x.png new file mode 100644 index 000000000000..5ac420ecfbb4 Binary files /dev/null and b/resources/images/icons/flags@3x.png differ diff --git a/resources/images/icons/food@2x.png b/resources/images/icons/food@2x.png new file mode 100644 index 000000000000..fb14a08150e1 Binary files /dev/null and b/resources/images/icons/food@2x.png differ diff --git a/resources/images/icons/food@3x.png b/resources/images/icons/food@3x.png new file mode 100644 index 000000000000..31dfb3e1fcf6 Binary files /dev/null and b/resources/images/icons/food@3x.png differ diff --git a/resources/images/icons/objects@2x.png b/resources/images/icons/objects@2x.png new file mode 100644 index 000000000000..411c4ab353e8 Binary files /dev/null and b/resources/images/icons/objects@2x.png differ diff --git a/resources/images/icons/objects@3x.png b/resources/images/icons/objects@3x.png new file mode 100644 index 000000000000..21cb98d3fcc6 Binary files /dev/null and b/resources/images/icons/objects@3x.png differ diff --git a/resources/images/icons/smileys@2x.png b/resources/images/icons/smileys@2x.png new file mode 100644 index 000000000000..f77d693a2b17 Binary files /dev/null and b/resources/images/icons/smileys@2x.png differ diff --git a/resources/images/icons/smileys@3x.png b/resources/images/icons/smileys@3x.png new file mode 100644 index 000000000000..1d11ca2a85c1 Binary files /dev/null and b/resources/images/icons/smileys@3x.png differ diff --git a/resources/images/icons/symbols@2x.png b/resources/images/icons/symbols@2x.png new file mode 100644 index 000000000000..00a39f566773 Binary files /dev/null and b/resources/images/icons/symbols@2x.png differ diff --git a/resources/images/icons/symbols@3x.png b/resources/images/icons/symbols@3x.png new file mode 100644 index 000000000000..d6e0a0325064 Binary files /dev/null and b/resources/images/icons/symbols@3x.png differ diff --git a/resources/images/icons/tiny_delivered@2x.png b/resources/images/icons/tiny_delivered@2x.png new file mode 100644 index 000000000000..8b89b8dd34cb Binary files /dev/null and b/resources/images/icons/tiny_delivered@2x.png differ diff --git a/resources/images/icons/tiny_delivered@3x.png b/resources/images/icons/tiny_delivered@3x.png new file mode 100644 index 000000000000..cb212df60665 Binary files /dev/null and b/resources/images/icons/tiny_delivered@3x.png differ diff --git a/resources/images/icons/travel@2x.png b/resources/images/icons/travel@2x.png new file mode 100644 index 000000000000..9ab016865437 Binary files /dev/null and b/resources/images/icons/travel@2x.png differ diff --git a/resources/images/icons/travel@3x.png b/resources/images/icons/travel@3x.png new file mode 100644 index 000000000000..a15b97cba55f Binary files /dev/null and b/resources/images/icons/travel@3x.png differ diff --git a/src/quo/components/bottom_sheet/view.cljs b/src/quo/components/bottom_sheet/view.cljs index bb605ed7f048..078d1ea4016e 100644 --- a/src/quo/components/bottom_sheet/view.cljs +++ b/src/quo/components/bottom_sheet/view.cljs @@ -31,6 +31,7 @@ disable-drag? :disableDrag? show-handle? :showHandle? visible? :visible? + transparent :transparent backdrop-dismiss? :backdropDismiss? back-button-cancel :backButtonCancel children :children @@ -189,6 +190,7 @@ (when back-button-cancel (close-sheet)))} [rn/view {:style styles/container + :opacity (if transparent 0 1) :pointer-events :box-none} [gesture-handler/tap-gesture-handler (merge {:enabled backdrop-dismiss?} tap-gesture-handler) diff --git a/src/quo/components/list/footer.cljs b/src/quo/components/list/footer.cljs index e3ba4a30c592..dd3058ad948d 100644 --- a/src/quo/components/list/footer.cljs +++ b/src/quo/components/list/footer.cljs @@ -1,11 +1,14 @@ (ns quo.components.list.footer (:require [quo.react-native :as rn] [quo.design-system.spacing :as spacing] - [quo.components.text :as text])) - -(defn footer [& children] - [rn/view {:style (merge (:base spacing/padding-horizontal) - (:small spacing/padding-vertical))} - (into [text/text {:color :secondary}] - children)]) + [quo.components.text :as text] + [reagent.core :as reagent])) +(defn footer [] + (let [this (reagent/current-component) + {:keys [color] + :or {color :secondary}} (reagent/props this)] + [rn/view {:style (merge (:base spacing/padding-horizontal) + (:small spacing/padding-vertical))} + (into [text/text {:color color}] + (reagent/children this))])) diff --git a/src/quo/components/list/header.cljs b/src/quo/components/list/header.cljs index 522fc3d4c75b..05f8a758f153 100644 --- a/src/quo/components/list/header.cljs +++ b/src/quo/components/list/header.cljs @@ -1,11 +1,15 @@ (ns quo.components.list.header - (:require [quo.react-native :as rn] + (:require [reagent.core :as reagent] + [quo.react-native :as rn] [quo.design-system.spacing :as spacing] [quo.components.text :as text])) -(defn header [& children] - [rn/view {:style (merge (:base spacing/padding-horizontal) - (:x-tiny spacing/padding-vertical))} - (into [text/text {:color :secondary - :style {:margin-top 10}}] - children)]) +(defn header [] + (let [this (reagent/current-component) + {:keys [color] + :or {color :secondary}} (reagent/props this)] + [rn/view {:style (merge (:base spacing/padding-horizontal) + (:x-tiny spacing/padding-vertical))} + (into [text/text {:color color + :style {:margin-top 10}}] + (reagent/children this))])) diff --git a/src/quo/components/list/index.cljs b/src/quo/components/list/index.cljs new file mode 100644 index 000000000000..90d99d6da739 --- /dev/null +++ b/src/quo/components/list/index.cljs @@ -0,0 +1,16 @@ +(ns quo.components.list.index + (:require [quo.react-native :as rn] + [quo.components.text :as text] + [quo.design-system.colors :as colors])) + +(defn index [{:keys [title]}] + [rn/view {:style {:padding-right 16}} + [rn/view {:style {:border-top-width 1 + :border-bottom-width 1 + :border-right-width 1 + :border-color (colors/get-color :border-01) + :padding-vertical 3 + :padding-horizontal 16 + :border-top-right-radius 16 + :border-bottom-right-radius 16}} + [text/text title]]]) diff --git a/src/quo/core.cljs b/src/quo/core.cljs index 2f848197ed2b..5b932daf5027 100644 --- a/src/quo/core.cljs +++ b/src/quo/core.cljs @@ -9,6 +9,7 @@ [quo.components.list.header :as list-header] [quo.components.list.footer :as list-footer] [quo.components.list.item :as list-item] + [quo.components.list.index :as list-index] [quo.components.controls.view :as controls] [quo.components.bottom-sheet.view :as bottom-sheet] [quo.components.separator :as separator] @@ -23,6 +24,7 @@ (def list-header list-header/header) (def list-footer list-footer/footer) (def list-item list-item/list-item) +(def list-index list-index/index) (def bottom-sheet bottom-sheet/bottom-sheet) (def switch controls/switch) (def radio controls/radio) diff --git a/src/quo/react_native.cljs b/src/quo/react_native.cljs index 1759e0f0f682..eee8912a1b46 100644 --- a/src/quo/react_native.cljs +++ b/src/quo/react_native.cljs @@ -15,6 +15,8 @@ (def image (reagent/adapt-react-class (.-Image rn))) (def text (reagent/adapt-react-class (.-Text ^js rn))) +(defn resolve-asset-source [uri] (js->clj (.resolveAssetSource ^js (.-Image ^js rn) uri) :keywordize-keys true)) + (def scroll-view (reagent/adapt-react-class (.-ScrollView ^js rn))) (def modal (reagent/adapt-react-class (.-Modal ^js rn))) (def refresh-control (reagent/adapt-react-class (.-RefreshControl ^js rn))) @@ -90,9 +92,9 @@ ;; Flat-list (def ^:private rn-flat-list (reagent/adapt-react-class (.-FlatList ^js rn))) -(defn- wrap-render-fn [f] +(defn- wrap-render-fn [f render-data] (fn [data] - (reagent/as-element [f (.-item ^js data) (.-index ^js data) (.-separators ^js data)]))) + (reagent/as-element [f (.-item ^js data) (.-index ^js data) (.-separators ^js data) render-data]))) (defn- wrap-key-fn [f] (fn [data index] @@ -100,10 +102,10 @@ (f data index))) (defn base-list-props - [{:keys [key-fn render-fn empty-component header footer separator data] :as props}] + [{:keys [key-fn render-fn empty-component header footer separator data render-data] :as props}] (merge {:data (to-array data)} (when key-fn {:keyExtractor (wrap-key-fn key-fn)}) - (when render-fn {:renderItem (wrap-render-fn render-fn)}) + (when render-fn {:renderItem (wrap-render-fn render-fn render-data)}) (when separator {:ItemSeparatorComponent (fn [] (reagent/as-element separator))}) (when empty-component {:ListEmptyComponent (fn [] (reagent/as-element empty-component))}) (when header {:ListHeaderComponent (reagent/as-element header)}) diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs index 53d052a45670..bec7d9c1a964 100644 --- a/src/status_im/chat/models/message.cljs +++ b/src/status_im/chat/models/message.cljs @@ -209,12 +209,18 @@ (chat-model/join-time-messages-checked-for-chats (keys grouped-messages))))))) ;;;; Send message +(fx/defn update-db-message-status + [{:keys [db] :as cofx} chat-id message-id status] + (when (get-in db [:messages chat-id message-id]) + (fx/merge cofx + {:db (assoc-in db + [:messages chat-id message-id :outgoing-status] + status)}))) + (fx/defn update-message-status [{:keys [db] :as cofx} chat-id message-id status] (fx/merge cofx - {:db (assoc-in db - [:messages chat-id message-id :outgoing-status] - status)} + (update-db-message-status chat-id message-id status) (data-store.messages/update-outgoing-status message-id status))) (fx/defn resend-message diff --git a/src/status_im/communities/core.cljs b/src/status_im/communities/core.cljs index 1f4de03c8e25..ab47c9052a17 100644 --- a/src/status_im/communities/core.cljs +++ b/src/status_im/communities/core.cljs @@ -2,6 +2,8 @@ (:require [re-frame.core :as re-frame] [clojure.walk :as walk] + [clojure.string :as string] + [clojure.set :as clojure.set] [taoensso.timbre :as log] [status-im.utils.fx :as fx] [status-im.constants :as constants] @@ -9,35 +11,50 @@ [status-im.transport.filters.core :as models.filters] [status-im.bottom-sheet.core :as bottom-sheet] [status-im.data-store.chats :as data-store.chats] - [status-im.ethereum.json-rpc :as json-rpc])) + [status-im.ethereum.json-rpc :as json-rpc] + [status-im.ui.components.colors :as colors] + [status-im.navigation :as navigation])) + +(def crop-size 1000) (def featured [{:name "Status" :id constants/status-community-id}]) -(def access-no-membership 1) -(def access-invitation-only 2) -(def access-on-request 3) +(defn <-request-to-join-community-rpc [r] + (clojure.set/rename-keys r {:communityId :community-id + :publicKey :public-key + :chatId :chat-id})) + +(defn <-requests-to-join-community-rpc [requests] + (reduce (fn [acc r] + (assoc acc (:id r) (<-request-to-join-community-rpc r))) + {} + requests)) (defn <-chats-rpc [chats] (reduce-kv (fn [acc k v] (assoc acc (name k) (-> v - (update :members walk/stringify-keys) - (assoc :identity {:display-name (get-in v [:identity :display_name]) - :description (get-in v [:identity :description])} - :id (name k))))) + (assoc :can-post? (:canPost v)) + (dissoc :canPost) + (update :members walk/stringify-keys)))) {} chats)) -(defn <-rpc [{:keys [description] :as c}] - (let [identity (:identity description)] - (-> c - (update-in [:description :members] walk/stringify-keys) - (assoc-in [:description :identity] {:display-name (:display_name identity) - :description (:description identity)}) - (update-in [:description :chats] <-chats-rpc)))) +(defn <-rpc [c] + (-> c + (clojure.set/rename-keys {:canRequestAccess :can-request-access? + :canManageUsers :can-manage-users? + :canJoin :can-join? + :requestedToJoinAt :requested-to-join-at + :isMember :is-member?}) + (update :members walk/stringify-keys) + (update :chats <-chats-rpc))) + +(defn fetch-community-id-input [{:keys [db]}] + (:communities/community-id-input db)) (fx/defn handle-chats [cofx chats] (models.chat/ensure-chats cofx chats)) @@ -48,6 +65,10 @@ (fx/defn handle-removed-filters [cofx filters] (models.filters/handle-filters-removed cofx (map models.filters/responses->filters filters))) +(fx/defn handle-request-to-join [{:keys [db]} r] + (let [{:keys [id community-id] :as request} (<-request-to-join-community-rpc r)] + {:db (assoc-in db [:communities/requests-to-join community-id id] request)})) + (fx/defn handle-removed-chats [{:keys [db]} chat-ids] {:db (reduce (fn [db chat-id] (update db :chats dissoc chat-id)) @@ -83,27 +104,30 @@ (handle-response cofx response)) (fx/defn joined - {:events [::joined]} + {:events [::joined ::requested-to-join]} [cofx response] (handle-response cofx response)) (fx/defn export - [cofx community-id on-success] - {::json-rpc/call [{:method "wakuext_exportCommunity" - :params [community-id] - :on-success on-success - :on-error #(do - (log/error "failed to export community" community-id %) - (re-frame/dispatch [::failed-to-export %]))}]}) + {:events [::export-pressed]} + [cofx community-id] + {::json-rpc/call [{:method "wakuext_exportCommunity" + :params [community-id] + :on-success #(re-frame/dispatch [:show-popover {:view :export-community + :community-key %}]) + :on-error #(do + (log/error "failed to export community" community-id %) + (re-frame/dispatch [::failed-to-export %]))}]}) + (fx/defn import-community {:events [::import]} - [cofx community-key on-success] - {::json-rpc/call [{:method "wakuext_importCommunity" - :params [community-key] - :on-success on-success - :on-error #(do - (log/error "failed to import community" %) - (re-frame/dispatch [::failed-to-import %]))}]}) + [cofx community-key] + {::json-rpc/call [{:method "wakuext_importCommunity" + :params [community-key] + :on-success #(re-frame/dispatch [::community-imported %]) + :on-error #(do + (log/error "failed to import community" %) + (re-frame/dispatch [::failed-to-import %]))}]}) (fx/defn join {:events [::join]} @@ -115,15 +139,25 @@ (log/error "failed to join community" community-id %) (re-frame/dispatch [::failed-to-join %]))}]}) +(fx/defn request-to-join + {:events [::request-to-join]} + [cofx community-id] + {::json-rpc/call [{:method "wakuext_requestToJoinCommunity" + :params [{:communityId community-id}] + :on-success #(re-frame/dispatch [::requested-to-join %]) + :on-error #(do + (log/error "failed to request to join community" community-id %) + (re-frame/dispatch [::failed-to-request-to-join %]))}]}) + (fx/defn leave {:events [::leave]} [cofx community-id] - {::json-rpc/call [{:method "wakuext_leaveCommunity" - :params [community-id] + {::json-rpc/call [{:method "wakuext_leaveCommunity" + :params [community-id] :on-success #(re-frame/dispatch [::left %]) - :on-error #(do - (log/error "failed to leave community" community-id %) - (re-frame/dispatch [::failed-to-leave %]))}]}) + :on-error #(do + (log/error "failed to leave community" community-id %) + (re-frame/dispatch [::failed-to-leave %]))}]}) (fx/defn fetch [_] {::json-rpc/call [{:method "wakuext_communities" @@ -136,184 +170,265 @@ (fx/defn chat-created {:events [::chat-created]} [cofx community-id user-pk] - {::json-rpc/call [{:method "wakuext_sendChatMessage" - :params [{:chatId user-pk - :text "Upgrade here to see an invitation to community" - :communityId community-id - :contentType constants/content-type-community}] + {::json-rpc/call [{:method "wakuext_sendChatMessage" + :params [{:chatId user-pk + :text "Upgrade here to see an invitation to community" + :communityId community-id + :contentType constants/content-type-community}] :on-success #(re-frame/dispatch [:transport/message-sent % 1]) :on-failure #(log/error "failed to send a message" %)}]}) -(fx/defn invite-user [cofx - community-id - user-pk - on-success-event - on-failure-event] - - (fx/merge cofx - {::json-rpc/call [{:method "wakuext_inviteUserToCommunity" - :params [community-id - user-pk] - :on-success #(re-frame/dispatch [on-success-event %]) - :on-error #(do - (log/error "failed to invite-user community" %) - (re-frame/dispatch [on-failure-event %]))}]} - (models.chat/upsert-chat {:chat-id user-pk - :active (get-in cofx [:db :chats user-pk :active])} - #(re-frame/dispatch [::chat-created community-id user-pk])))) - -(fx/defn create [{:keys [db]} - community-name - community-description - community-membership - on-success-event - on-failure-event] - (let [membership (js/parseInt community-membership) - my-public-key (get-in db [:multiaccount :public-key])] - {::json-rpc/call [{:method "wakuext_createCommunity" - :params [{:identity {:display_name community-name - :description community-description} - :members {my-public-key {}} - :permissions {:access membership}}] - :on-success #(re-frame/dispatch [on-success-event %]) - :on-error #(do - (log/error "failed to create community" %) - (re-frame/dispatch [on-failure-event %]))}]})) - -(defn create-channel [community-id - community-channel-name - community-channel-description - on-success-event - on-failure-event] - {::json-rpc/call [{:method "wakuext_createCommunityChat" - :params [community-id - {:identity {:display_name community-channel-name - :description community-channel-description} - :permissions {:access access-no-membership}}] - :on-success #(re-frame/dispatch [on-success-event %]) - :on-error #(do - (log/error "failed to create community channel" %) - (re-frame/dispatch [on-failure-event %]))}]}) - -(def no-membership-access 1) -(def invitation-only-access 2) -(def on-request-access 3) +(fx/defn invite-users + {:events [::invite-people-confirmation-pressed]} + [cofx user-pk contacts] + (let [community-id (fetch-community-id-input cofx) + pks (if (seq user-pk) + (conj contacts user-pk) + contacts)] + (when (seq pks) + {::json-rpc/call [{:method "wakuext_inviteUsersToCommunity" + :params [{:communityId community-id + :users pks}] + :on-success #(re-frame/dispatch [::people-invited %]) + :on-error #(do + (log/error "failed to invite-user community" %) + (re-frame/dispatch [::failed-to-invite-people %]))}]}))) +(fx/defn share-community + {:events [::share-community-confirmation-pressed]} + [cofx user-pk contacts] + (let [community-id (fetch-community-id-input cofx) + pks (if (seq user-pk) + (conj contacts user-pk) + contacts)] + (when (seq pks) + {::json-rpc/call [{:method "wakuext_shareCommunity" + :params [{:communityId community-id + :users pks}] + :on-success #(re-frame/dispatch [::people-invited %]) + :on-error #(do + (log/error "failed to invite-user community" %) + (re-frame/dispatch [::failed-to-share-community %]))}]}))) + +(fx/defn create + {:events [::create-confirmation-pressed]} + [{:keys [db]}] + (let [{:keys [name description membership image]} (get db :communities/create) + my-public-key (get-in db [:multiaccount :public-key])] + ;; If access is ENS only, we set the access to require approval and set the rule + ;; of ens only + (let [params (cond-> {:name name + :description description + :membership (or membership constants/community-no-membership-access) + :color (rand-nth colors/chat-colors) + :image (string/replace-first (str image) #"file://" "") + :imageAx 0 + :imageAy 0 + :imageBx crop-size + :imageBy crop-size} + (= membership constants/community-rule-ens-only) + (assoc :membership constants/community-on-request-access + :ens-only true))] + + {::json-rpc/call [{:method "wakuext_createCommunity" + :params [params] + :on-success #(re-frame/dispatch [::community-created %]) + :on-error #(do + (log/error "failed to create community" %) + (re-frame/dispatch [::failed-to-create-community %]))}]}))) + +(fx/defn edit + {:events [::edit-confirmation-pressed]} + [{:keys [db]}] + (let [{:keys [name description membership]} (get db :communities/create) + my-public-key (get-in db [:multiaccount :public-key])] + (log/error "Edit community is not yet implemented") + ;; {::json-rpc/call [{:method "wakuext_editCommunity" + ;; :params [{:identity {:display_name name + ;; :description description} + ;; :permissions {:access membership}}] + ;; :on-success #(re-frame/dispatch [::community-edited %]) + ;; :on-error #(do + ;; (log/error "failed to create community" %) + ;; (re-frame/dispatch [::failed-to-edit-community %]))}]} + )) + +(fx/defn create-channel + {:events [::create-channel-confirmation-pressed]} + [cofx community-channel-name community-channel-description] + (let [community-id (fetch-community-id-input cofx)] + {::json-rpc/call [{:method "wakuext_createCommunityChat" + :params [community-id + {:identity {:display_name community-channel-name + :color (rand-nth colors/chat-colors) + :description community-channel-description} + :permissions {:access constants/community-channel-access-no-membership}}] + :on-success #(re-frame/dispatch [::community-channel-created %]) + :on-error #(do + (log/error "failed to create community channel" %) + (re-frame/dispatch [::failed-to-create-community-channel %]))}]})) (defn require-membership? [permissions] - (not= no-membership-access (:access permissions))) + (not= constants/community-no-membership-access (:access permissions))) (def community-id-length 68) -;; TODO: test this -(defn can-post? [{:keys [admin] :as community} pk local-chat-id] - (let [chat-id (subs local-chat-id community-id-length) - can-access-community? (or (get-in community [:description :members pk]) - (not (require-membership? (get-in community [:description :permissions]))))] - (or admin - (get-in community [:description :chats chat-id :members pk]) - (and can-access-community? - (not (require-membership? (get-in community [:description :chats chat-id :permissions]))))))) + +(defn can-post? [community _ local-chat-id] + (let [chat-id (subs local-chat-id community-id-length)] + (get-in community [:chats chat-id :can-post?]))) (fx/defn reset-community-id-input [{:keys [db]} id] {:db (assoc db :communities/community-id-input id)}) -(defn fetch-community-id-input [{:keys [db]}] - (:communities/community-id-input db)) - -(fx/defn import-pressed - {:events [::import-pressed]} - [cofx] - (bottom-sheet/show-bottom-sheet cofx {:view :import-community})) - -(fx/defn create-pressed - {:events [::create-pressed]} - [cofx] - (bottom-sheet/show-bottom-sheet cofx {:view :create-community})) - (fx/defn invite-people-pressed {:events [::invite-people-pressed]} [cofx id] (fx/merge cofx (reset-community-id-input id) - (bottom-sheet/show-bottom-sheet {:view :invite-people-community}))) + (bottom-sheet/hide-bottom-sheet) + (navigation/navigate-to :invite-people-community {:invite? true}))) + +(fx/defn share-community-pressed + {:events [::share-community-pressed]} + [cofx id] + (fx/merge cofx + (reset-community-id-input id) + (bottom-sheet/hide-bottom-sheet) + (navigation/navigate-to :invite-people-community {}))) (fx/defn create-channel-pressed {:events [::create-channel-pressed]} [cofx id] (fx/merge cofx (reset-community-id-input id) - (bottom-sheet/show-bottom-sheet {:view :create-community-channel}))) + (navigation/navigate-to :create-community-channel nil))) (fx/defn community-created {:events [::community-created]} [cofx response] (fx/merge cofx - (bottom-sheet/hide-bottom-sheet) + (navigation/navigate-back) + (handle-response response))) + +(fx/defn community-edited + {:events [::community-edited]} + [cofx response] + (fx/merge cofx + (navigation/navigate-back) (handle-response response))) +(fx/defn open-create-community + {:events [::open-create-community]} + [{:keys [db] :as cofx}] + (fx/merge cofx + {:db (assoc db :communities/create {})} + (navigation/navigate-to :community-create nil))) + +(fx/defn open-edit-community + {:events [::open-edit-community]} + [{:keys [db] :as cofx} id] + (let [{:keys [identity permissions]} (get-in db [:communities id :description]) + {:keys [display-name description image]} identity + {:keys [access]} permissions] + (fx/merge cofx + {:db (assoc db :communities/create {:name display-name + :description description + :image image + :membership access})} + (navigation/navigate-to :communities {:screen :community-edit})))) + (fx/defn community-imported {:events [::community-imported]} [cofx response] (fx/merge cofx - (bottom-sheet/hide-bottom-sheet) + (navigation/navigate-back) (handle-response response))) (fx/defn people-invited {:events [::people-invited]} [cofx response] (fx/merge cofx - (bottom-sheet/hide-bottom-sheet) + (navigation/navigate-back) (handle-response response))) (fx/defn community-channel-created {:events [::community-channel-created]} [cofx response] + (fx/merge cofx + (navigation/navigate-back) + (handle-response response))) + +(fx/defn create-field + {:events [::create-field]} + [{:keys [db]} field value] + {:db (assoc-in db [:communities/create field] value)}) + +(fx/defn member-ban + {:events [::member-ban]} + [cofx community-id public-key] + (log/error "Community member ban is not yet implemented")) + +(fx/defn member-kicked + {:events [::member-kicked]} + [cofx response] + (fx/merge cofx (bottom-sheet/hide-bottom-sheet) (handle-response response))) -(fx/defn handle-export-pressed - {:events [::export-pressed]} +(fx/defn member-kick + {:events [::member-kick]} + [cofx community-id public-key] + {::json-rpc/call [{:method "wakuext_removeUserFromCommunity" + :params [community-id public-key] + :on-success #(re-frame/dispatch [::member-kicked %]) + :on-error #(log/error "failed to remove user from community" community-id public-key %)}]}) + +(fx/defn delete-community + {:events [::delete-community]} [cofx community-id] - (export cofx community-id - #(re-frame/dispatch [:show-popover {:view :export-community - :community-key %}]))) + (log/error "Community delete is not yet implemented")) -(fx/defn import-confirmation-pressed - {:events [::import-confirmation-pressed]} - [cofx community-key] - (import-community - cofx - community-key - #(re-frame/dispatch [::community-imported %]))) +(fx/defn requests-to-join-fetched + {:events [::requests-to-join-fetched]} + [{:keys [db]} community-id requests] + {:db (assoc-in db [:communities/requests-to-join community-id] (<-requests-to-join-community-rpc requests))}) -(fx/defn create-confirmation-pressed - {:events [::create-confirmation-pressed]} - [cofx community-name community-description membership] - (create - cofx - community-name - community-description - membership - ::community-created - ::failed-to-create-community)) - -(fx/defn create-channel-confirmation-pressed - {:events [::create-channel-confirmation-pressed]} - [cofx community-channel-name community-channel-description] - (create-channel - (fetch-community-id-input cofx) - community-channel-name - community-channel-description - ::community-channel-created - ::failed-to-create-community-channel)) - -(fx/defn invite-people-confirmation-pressed - {:events [::invite-people-confirmation-pressed]} - [cofx user-pk] - (invite-user - cofx - (fetch-community-id-input cofx) - user-pk - ::people-invited - ::failed-to-invite-people)) +(fx/defn fetch-requests-to-join + {:events [::fetch-requests-to-join]} + [cofx community-id] + {::json-rpc/call [{:method "wakuext_pendingRequestsToJoinForCommunity" + :params [community-id] + :on-success #(re-frame/dispatch [::requests-to-join-fetched community-id %]) + :on-error #(log/error "failed to fetch requests-to-join" community-id %)}]}) + +(defn fetch-requests-to-join! [community-id] + (re-frame/dispatch [::fetch-requests-to-join community-id])) + +(fx/defn request-to-join-accepted + {:events [::request-to-join-accepted]} + [{:keys [db] :as cofx} community-id request-id response] + (fx/merge cofx + {:db (update-in db [:communities/requests-to-join community-id] dissoc request-id)} + (handle-response response))) + +(fx/defn request-to-join-declined + {:events [::request-to-join-declined]} + [{:keys [db] :as cofx} community-id request-id] + {:db (update-in db [:communities/requests-to-join community-id] dissoc request-id)}) + +(fx/defn accept-request-to-join-pressed + {:events [:communities.ui/accept-request-to-join-pressed]} + [cofx community-id request-id] + {::json-rpc/call [{:method "wakuext_acceptRequestToJoinCommunity" + :params [{:id request-id}] + :on-success #(re-frame/dispatch [::request-to-join-accepted community-id request-id %]) + :on-error #(log/error "failed to accept requests-to-join" community-id request-id %)}]}) + +(fx/defn decline-request-to-join-pressed + {:events [:communities.ui/decline-request-to-join-pressed]} + [cofx community-id request-id] + {::json-rpc/call [{:method "wakuext_declineRequestToJoinCommunity" + :params [{:id request-id}] + :on-success #(re-frame/dispatch [::request-to-join-declined community-id request-id %]) + :on-error #(log/error "failed to decline requests-to-join" community-id request-id)}]}) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 750862653de0..23f2202a9533 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -27,6 +27,8 @@ (def ^:const timeline-chat-type 5) (def ^:const community-chat-type 6) +(def request-to-join-pending-state 1) + (def reactions {emoji-reaction-love (:love resources/reactions) emoji-reaction-thumbs-up (:thumbs-up resources/reactions) emoji-reaction-thumbs-down (:thumbs-down resources/reactions) @@ -83,6 +85,17 @@ (def ^:const status-create-address "status_createaddress") +(def ^:const community-no-membership-access 1) +(def ^:const community-invitation-only-access 2) +(def ^:const community-on-request-access 3) + +;; Community rules for joining +(def ^:const community-rule-ens-only "ens-only") + +(def ^:const community-channel-access-no-membership 1) +(def ^:const community-channel-access-invitation-only 2) +(def ^:const community-channel-access-on-request 3) + ; BIP44 Wallet Root Key, the extended key from which any wallet can be derived (def ^:const path-wallet-root "m/44'/60'/0'/0") ; EIP1581 Root Key, the extended key from which any whisper key/encryption key can be derived diff --git a/src/status_im/contact/core.cljs b/src/status_im/contact/core.cljs index dec0935e1c70..23051aec36de 100644 --- a/src/status_im/contact/core.cljs +++ b/src/status_im/contact/core.cljs @@ -123,13 +123,17 @@ (fx/defn name-verified {:events [:contacts/ens-name-verified]} [{:keys [db now] :as cofx} public-key ens-name] - (let [contact (-> (get-in db [:contacts/contacts public-key] - (build-contact cofx public-key)) + (let [contact (-> (or (get-in db [:contacts/contacts public-key]) + (build-contact cofx public-key)) (assoc :name ens-name - :last-ens-clock-value now - :ens-verified-at now :ens-verified true))] - (upsert-contact cofx contact))) + (fx/merge cofx + {:db (-> db + (update-in [:contacts/contacts public-key] merge contact)) + ::json-rpc/call [{:method "wakuext_ensVerified" + :params [public-key ens-name] + :on-success #(log/debug "ens name verified successuful")}]} + (transport.filters/load-contact contact)))) (fx/defn update-nickname {:events [:contacts/update-nickname]} diff --git a/src/status_im/ethereum/json_rpc.cljs b/src/status_im/ethereum/json_rpc.cljs index ade0873da8c4..328bbae0bf8a 100644 --- a/src/status_im/ethereum/json_rpc.cljs +++ b/src/status_im/ethereum/json_rpc.cljs @@ -117,12 +117,19 @@ "multiaccounts_deleteIdentityImage" {} "wakuext_createCommunity" {} "wakuext_createCommunityChat" {} - "wakuext_inviteUserToCommunity" {} + "wakuext_inviteUsersToCommunity" {} + "wakuext_shareCommunity" {} + "wakuext_removeUserFromCommunity" {} + "wakuext_requestToJoinCommunity" {} + "wakuext_acceptRequestToJoinCommunity" {} + "wakuext_declineRequestToJoinCommunity" {} + "wakuext_pendingRequestsToJoinForCommunity" {} "wakuext_joinCommunity" {} "wakuext_leaveCommunity" {} "wakuext_communities" {} "wakuext_importCommunity" {} "wakuext_exportCommunity" {} + "wakuext_ensVerified" {} "status_chats" {} "localnotifications_switchWalletNotifications" {} "localnotifications_notificationPreferences" {} diff --git a/src/status_im/keycard/card.cljs b/src/status_im/keycard/card.cljs index 41dd1c2ab1ba..f3c35cef8f28 100644 --- a/src/status_im/keycard/card.cljs +++ b/src/status_im/keycard/card.cljs @@ -1,61 +1,57 @@ (ns status-im.keycard.card (:require [re-frame.core :as re-frame] - [status-im.keycard.ios-keycard :as ios-keycard] [status-im.keycard.keycard :as keycard] [status-im.keycard.real-keycard :as real-keycard] [status-im.keycard.simulated-keycard :as simulated-keycard] [status-im.utils.config :as config] - [status-im.utils.platform :as platform] [taoensso.timbre :as log])) (defonce card (if config/keycard-test-menu-enabled? (simulated-keycard/SimulatedKeycard.) - (if platform/android? - (real-keycard/RealKeycard.) - (ios-keycard/IOSKeycard.)))) + (real-keycard/RealKeycard.))) (defn check-nfc-support [] - (log/info "[keycard] check-nfc-support") + (log/debug "[keycard] check-nfc-support") (keycard/check-nfc-support card {:on-success (fn [response] - (log/info "[keycard response] check-nfc-support") + (log/debug "[keycard response] check-nfc-support") (re-frame/dispatch [:keycard.callback/check-nfc-support-success response]))})) (defn check-nfc-enabled [] - (log/info "[keycard] check-nfc-enabled") + (log/debug "[keycard] check-nfc-enabled") (keycard/check-nfc-enabled card {:on-success (fn [response] - (log/info "[keycard response] check-nfc-enabled") + (log/debug "[keycard response] check-nfc-enabled") (re-frame/dispatch [:keycard.callback/check-nfc-enabled-success response]))})) (defn open-nfc-settings [] - (log/info "[keycard] open-nfc-settings") + (log/debug "[keycard] open-nfc-settings") (keycard/open-nfc-settings card)) (defn remove-event-listener [event] - (log/info "[keycard] remove-event-listener") + (log/debug "[keycard] remove-event-listener") (keycard/remove-event-listener card event)) (defn on-card-disconnected [callback] - (log/info "[keycard] on-card-disconnected") + (log/debug "[keycard] on-card-disconnected") (keycard/on-card-disconnected card callback)) (defn on-card-connected [callback] - (log/info "[keycard] on-card-connected") + (log/debug "[keycard] on-card-connected") (keycard/on-card-connected card callback)) (defn remove-event-listeners [] - (log/info "[keycard] remove-event-listeners") + (log/debug "[keycard] remove-event-listeners") (keycard/remove-event-listeners card)) (defn register-card-events [] - (log/info "[keycard] register-card-events") + (log/debug "[keycard] register-card-events") (keycard/register-card-events card {:on-card-connected @@ -64,6 +60,12 @@ :on-card-disconnected #(re-frame/dispatch [:keycard.callback/on-card-disconnected]) + :on-nfc-user-cancelled + #(re-frame/dispatch [:keycard.callback/on-nfc-user-cancelled]) + + :on-nfc-timeout + #(re-frame/dispatch [:keycard.callback/on-nfc-timeout]) + :on-nfc-enabled #(re-frame/dispatch [:keycard.callback/check-nfc-enabled-success true]) @@ -75,188 +77,188 @@ :error (.-message object)}) (defn get-application-info [{:keys [on-success] :as args}] - (log/info "[keycard] get-application-info") + (log/debug "[keycard] get-application-info") (keycard/get-application-info card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] get-application-info") + (log/debug "[keycard response succ] get-application-info") (re-frame/dispatch [:keycard.callback/on-get-application-info-success response on-success])) :on-failure (fn [response] - (log/info "[keycard response fail] get-application-info") + (log/debug "[keycard response fail] get-application-info") (re-frame/dispatch [:keycard.callback/on-get-application-info-error (error-object->map response)]))}))) (defn install-applet [] - (log/info "[keycard] install-applet") + (log/debug "[keycard] install-applet") (keycard/install-applet card {:on-success (fn [response] - (log/info "[keycard response succ] install-applet") + (log/debug "[keycard response succ] install-applet") (re-frame/dispatch [:keycard.callback/on-install-applet-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] install-applet") + (log/debug "[keycard response fail] install-applet") (re-frame/dispatch [:keycard.callback/on-install-applet-error (error-object->map response)]))})) (defn init-card [pin] - (log/info "[keycard] init-card") + (log/debug "[keycard] init-card") (keycard/init-card card {:pin pin :on-success (fn [response] - (log/info "[keycard response succ] init-card") + (log/debug "[keycard response succ] init-card") (re-frame/dispatch [:keycard.callback/on-init-card-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] init-card") + (log/debug "[keycard response fail] init-card") (re-frame/dispatch [:keycard.callback/on-init-card-error (error-object->map response)]))})) (defn install-applet-and-init-card [pin] - (log/info "[keycard] install-applet-and-init-card") + (log/debug "[keycard] install-applet-and-init-card") (keycard/install-applet-and-init-card card {:pin pin :on-success (fn [response] - (log/info "[keycard response succ] install-applet-and-init-card") + (log/debug "[keycard response succ] install-applet-and-init-card") #(re-frame/dispatch [:keycard.callback/on-install-applet-and-init-card-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] install-applet-and-init-card") + (log/debug "[keycard response fail] install-applet-and-init-card") (re-frame/dispatch [:keycard.callback/on-install-applet-and-init-card-error (error-object->map response)]))})) (defn pair [args] - (log/info "[keycard] pair") + (log/debug "[keycard] pair") (keycard/pair card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] pair") + (log/debug "[keycard response succ] pair") (re-frame/dispatch [:keycard.callback/on-pair-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] pair") + (log/debug "[keycard response fail] pair") (re-frame/dispatch [:keycard.callback/on-pair-error (error-object->map response)]))}))) (defn generate-and-load-key [args] - (log/info "[keycard] generate-and-load-key") + (log/debug "[keycard] generate-and-load-key") (keycard/generate-and-load-key card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] generate-and-load-key") + (log/debug "[keycard response succ] generate-and-load-key") (re-frame/dispatch [:keycard.callback/on-generate-and-load-key-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] generate-and-load-key") + (log/debug "[keycard response fail] generate-and-load-key") (re-frame/dispatch [:keycard.callback/on-generate-and-load-key-error (error-object->map response)]))}))) (defn unblock-pin [args] - (log/info "[keycard] unblock-pin") + (log/debug "[keycard] unblock-pin") (keycard/unblock-pin card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] unblock-pin") + (log/debug "[keycard response succ] unblock-pin") (re-frame/dispatch [:keycard.callback/on-unblock-pin-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] unblock-pin") + (log/debug "[keycard response fail] unblock-pin") (re-frame/dispatch [:keycard.callback/on-unblock-pin-error (error-object->map response)]))}))) (defn verify-pin [args] - (log/info "[keycard] verify-pin") + (log/debug "[keycard] verify-pin") (keycard/verify-pin card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] verify-pin") + (log/debug "[keycard response succ] verify-pin") (re-frame/dispatch [:keycard.callback/on-verify-pin-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] verify-pin") + (log/debug "[keycard response fail] verify-pin") (re-frame/dispatch [:keycard.callback/on-verify-pin-error (error-object->map response)]))}))) (defn change-pin [args] - (log/info "[keycard] change-pin") + (log/debug "[keycard] change-pin") (keycard/change-pin card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] change-pin") + (log/debug "[keycard response succ] change-pin") (re-frame/dispatch [:keycard.callback/on-change-pin-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] change-pin") + (log/debug "[keycard response fail] change-pin") (re-frame/dispatch [:keycard.callback/on-change-pin-error (error-object->map response)]))}))) (defn unpair [args] - (log/info "[keycard] unpair") + (log/debug "[keycard] unpair") (keycard/unpair card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] unpair") + (log/debug "[keycard response succ] unpair") (re-frame/dispatch [:keycard.callback/on-unpair-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] unpair") + (log/debug "[keycard response fail] unpair") (re-frame/dispatch [:keycard.callback/on-unpair-error (error-object->map response)]))}))) (defn delete [] - (log/info "[keycard] delete") + (log/debug "[keycard] delete") (keycard/delete card {:on-success (fn [response] - (log/info "[keycard response succ] delete") + (log/debug "[keycard response succ] delete") (re-frame/dispatch [:keycard.callback/on-delete-success response])) :on-failure @@ -267,107 +269,126 @@ (error-object->map response)]))})) (defn remove-key [args] - (log/info "[keycard] remove-key") + (log/debug "[keycard] remove-key") (keycard/remove-key card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] remove-key") + (log/debug "[keycard response succ] remove-key") (re-frame/dispatch [:keycard.callback/on-remove-key-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] remove-key") + (log/debug "[keycard response fail] remove-key") (re-frame/dispatch [:keycard.callback/on-remove-key-error (error-object->map response)]))}))) (defn remove-key-with-unpair [args] - (log/info "[keycard] remove-key-with-unpair") + (log/debug "[keycard] remove-key-with-unpair") (keycard/remove-key-with-unpair card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] remove-key-with-unpair") + (log/debug "[keycard response succ] remove-key-with-unpair") (re-frame/dispatch [:keycard.callback/on-remove-key-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] remove-key-with-unpair") + (log/debug "[keycard response fail] remove-key-with-unpair") (re-frame/dispatch [:keycard.callback/on-remove-key-error (error-object->map response)]))}))) (defn export-key [args] - (log/info "[keycard] export-key") + (log/debug "[keycard] export-key") (keycard/export-key card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] export-key") + (log/debug "[keycard response succ] export-key") (re-frame/dispatch [:keycard.callback/on-export-key-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] export-key") + (log/debug "[keycard response fail] export-key") (re-frame/dispatch [:keycard.callback/on-export-key-error (error-object->map response)]))}))) (defn unpair-and-delete [args] - (log/info "[keycard] unpair-and-delete") + (log/debug "[keycard] unpair-and-delete") (keycard/unpair-and-delete card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] unpair-and-delete") + (log/debug "[keycard response succ] unpair-and-delete") (re-frame/dispatch [:keycard.callback/on-delete-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] unpair-and-delete") + (log/debug "[keycard response fail] unpair-and-delete") (re-frame/dispatch [:keycard.callback/on-delete-error (error-object->map response)]))}))) +(defn import-keys [{:keys [on-success] :as args}] + (log/debug "[keycard] import-keys") + (keycard/import-keys + card + (assoc + args + :on-success + (fn [response] + (log/debug "[keycard response succ] import-keys") + (re-frame/dispatch + [(or on-success :keycard.callback/on-generate-and-load-key-success) + response])) + :on-failure + (fn [response] + (log/warn "[keycard response fail] import-keys" + (error-object->map response)) + (re-frame/dispatch [:keycard.callback/on-get-keys-error + (error-object->map response)]))))) + (defn get-keys [{:keys [on-success] :as args}] - (log/info "[keycard] get-keys") + (log/debug "[keycard] get-keys") (keycard/get-keys card - (merge + (assoc args - {:on-success - (fn [response] - (log/info "[keycard response succ] get-keys") - (re-frame/dispatch - [(or on-success :keycard.callback/on-get-keys-success) - response])) - :on-failure - (fn [response] - (log/info "[keycard response fail] get-keys" - (error-object->map response)) - (re-frame/dispatch [:keycard.callback/on-get-keys-error - (error-object->map response)]))}))) + :on-success + (fn [response] + (log/debug "[keycard response succ] get-keys") + (re-frame/dispatch + [(or on-success :keycard.callback/on-get-keys-success) + response])) + :on-failure + (fn [response] + (log/warn "[keycard response fail] get-keys" + (error-object->map response)) + (re-frame/dispatch [:keycard.callback/on-get-keys-error + (error-object->map response)]))))) (defn sign [{:keys [on-success on-failure] :as args}] - (log/info "[keycard] sign") + (log/debug "[keycard] sign") (keycard/sign card (merge args {:on-success (fn [response] - (log/info "[keycard response succ] sign") + (log/debug "[keycard response succ] sign") (if on-success (on-success response) (re-frame/dispatch [:keycard.callback/on-sign-success response]))) :on-failure (fn [response] - (log/info "[keycard response fail] sign") + (log/debug "[keycard response fail] sign") (if on-failure (on-failure response) (re-frame/dispatch @@ -375,35 +396,35 @@ (error-object->map response)])))}))) (defn install-cash-applet [] - (log/info "[keycard] install-cash-applet") + (log/debug "[keycard] install-cash-applet") (keycard/install-cash-applet card {:on-success (fn [response] - (log/info "[keycard response succ] install-cash-applet") + (log/debug "[keycard response succ] install-cash-applet") (re-frame/dispatch [:keycard.callback/on-install-applet-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] install-cash-applet") + (log/debug "[keycard response fail] install-cash-applet") (re-frame/dispatch [:keycard.callback/on-install-applet-error (error-object->map response)]))})) (defn sign-typed-data [{:keys [hash]}] - (log/info "[keycard] sign-typed-data") + (log/debug "[keycard] sign-typed-data") (keycard/sign-typed-data card {:hash hash :on-success (fn [response] - (log/info "[keycard response succ] sign-typed-data") + (log/debug "[keycard response succ] sign-typed-data") (re-frame/dispatch [:keycard.callback/on-sign-success response])) :on-failure (fn [response] - (log/info "[keycard response fail] sign-typed-data") + (log/debug "[keycard response fail] sign-typed-data") (re-frame/dispatch [:keycard.callback/on-sign-error (error-object->map response)]))})) @@ -416,3 +437,12 @@ (defn send-transaction-with-signature [args] (keycard/send-transaction-with-signature card args)) + +(defn start-nfc [args] + (keycard/start-nfc card args)) + +(defn stop-nfc [args] + (keycard/stop-nfc card args)) + +(defn set-nfc-message [args] + (keycard/set-nfc-message card args)) diff --git a/src/status_im/keycard/change_pin.cljs b/src/status_im/keycard/change_pin.cljs index 9e96b3c3b342..5e23db4ac458 100644 --- a/src/status_im/keycard/change_pin.cljs +++ b/src/status_im/keycard/change_pin.cljs @@ -96,23 +96,27 @@ {:events [:keycard.callback/on-change-pin-error]} [{:keys [db] :as cofx} error] (log/debug "[keycard] change pin error" error) - (let [tag-was-lost? (= "Tag was lost." (:error error)) - pairing (common/get-pairing db)] + (let [tag-was-lost? (common/tag-lost? (:error error)) + pairing (common/get-pairing db) + pin-retries (common/pin-retries (:error error))] (fx/merge cofx (if tag-was-lost? (fx/merge cofx {:db (assoc-in db [:keycard :pin :status] nil)} (common/set-on-card-connected :keycard/change-pin)) - (if (re-matches common/pin-mismatch-error (:error error)) + (if-not (nil? pin-retries) (fx/merge cofx - {:db (update-in db [:keycard :pin] merge {:status :error - :enter-step :current - :puk [] - :current [] - :original [] - :confirmation [] - :sign [] - :error-label :t/pin-mismatch})} - (navigation/navigate-to-cofx :enter-pin-settings nil) - (common/get-application-info pairing nil)) + {:db (-> db + (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries) + (update-in [:keycard :pin] assoc + :status :error + :enter-step :current + :puk [] + :current [] + :original [] + :confirmation [] + :sign [] + :error-label :t/pin-mismatch))} + (when (zero? pin-retries) (common/frozen-keycard-popup)) + (navigation/navigate-to-cofx :enter-pin-settings nil)) (common/show-wrong-keycard-alert true)))))) diff --git a/src/status_im/keycard/common.cljs b/src/status_im/keycard/common.cljs index cbf78582bd05..eb28349ca891 100644 --- a/src/status_im/keycard/common.cljs +++ b/src/status_im/keycard/common.cljs @@ -10,11 +10,15 @@ [status-im.utils.keychain.core :as keychain] [status-im.utils.types :as types] [taoensso.timbre :as log] - [status-im.bottom-sheet.core :as bottom-sheet])) + [status-im.bottom-sheet.core :as bottom-sheet] + [status-im.utils.platform :as platform])) (def default-pin "000000") -(def pin-mismatch-error #"Unexpected error SW, 0x63C\d+") +(def pin-mismatch-error #"Unexpected error SW, 0x63C(\d+)|wrongPIN\(retryCounter: (\d+)\)") + +(defn pin-retries [error] + (when-let [matched-error (re-matches pin-mismatch-error error)] (js/parseInt (second (filter some? matched-error))))) (fx/defn dispatch-event [_ event] @@ -51,7 +55,10 @@ :no-pairing-slots)) (defn tag-lost? [error] - (= error "Tag was lost.")) + (or + (= error "Tag was lost.") + (= error "NFCError:100") + (re-matches #".*NFCError:100.*" error))) (defn find-multiaccount-by-keycard-instance-uid [db keycard-instance-uid] @@ -168,7 +175,7 @@ :on-connect ::on-card-connected :on-disconnect ::on-card-disconnected}))) -(fx/defn show-connection-sheet +(fx/defn show-connection-sheet-component [{:keys [db] :as cofx} {:keys [on-card-connected on-card-read handler] {:keys [on-cancel] :or {on-cancel [::cancel-sheet-confirm]}} @@ -182,7 +189,8 @@ cofx {:dismiss-keyboard true} (bottom-sheet/show-bottom-sheet - {:view {:show-handle? false + {:view {:transparent platform/ios? + :show-handle? false :backdrop-dismiss? false :disable-drag? true :back-button-cancel false @@ -195,7 +203,21 @@ (when connected? handler)))) -(fx/defn hide-connection-sheet +(fx/defn show-connection-sheet + [{:keys [db] :as cofx} args] + (let [nfc-running? (get-in db [:keycard :nfc-running?])] + (log/debug "show connection; already running?" nfc-running?) + (if nfc-running? + (show-connection-sheet-component cofx args) + {:keycard/start-nfc-and-show-connection-sheet args}))) + +(fx/defn on-nfc-ready-for-sheet + {:events [:keycard.callback/show-connection-sheet]} + [cofx args] + (log/debug "on-nfc-ready-for-sheet") + (show-connection-sheet-component cofx args)) + +(fx/defn hide-connection-sheet-component [{:keys [db] :as cofx}] (fx/merge cofx {:db (assoc-in db [:keycard :card-read-in-progress?] false)} @@ -203,6 +225,17 @@ (restore-on-card-read) (bottom-sheet/hide-bottom-sheet))) +(fx/defn hide-connection-sheet + [cofx] + (log/debug "hide-connection-sheet") + {:keycard/stop-nfc-and-hide-connection-sheet nil}) + +(fx/defn on-nfc-ready-to-close-sheet + {:events [:keycard.callback/hide-connection-sheet]} + [cofx] + (log/debug "on-nfc-ready-to-close-sheet") + (hide-connection-sheet-component cofx)) + (fx/defn clear-pin [{:keys [db] :as cofx}] (fx/merge @@ -286,7 +319,8 @@ (defn- tag-lost-exception? [code error] (or (= code "android.nfc.TagLostException") - (= error "Tag was lost."))) + (= error "Tag was lost.") + (= error "NFCError:100"))) (fx/defn process-error [{:keys [db]} code error] (when-not (tag-lost-exception? code error) @@ -322,6 +356,9 @@ (-> db (assoc-in [:keycard :pin :status] nil) (assoc-in [:keycard :pin :login] []) + (update-in [:keycard :application-info] assoc + :puk-retry-counter 5 + :pin-retry-counter 3) (assoc-in [:keycard :multiaccount] (update account-data :whisper-public-key ethereum/normalized-hex)) (assoc-in [:keycard :flow] nil) @@ -331,7 +368,6 @@ :identicon identicon :name name)) - :keycard/get-application-info {:pairing (get-pairing db key-uid)} :keycard/login-with-keycard {:multiaccount-data multiaccount-data :password encryption-public-key :chat-key whisper-private-key @@ -342,28 +378,37 @@ (clear-on-card-read) (hide-connection-sheet)))) +(fx/defn frozen-keycard-popup + [{:keys [db] :as cofx}] + (if (:multiaccounts/login db) + (fx/merge + cofx + {:db (assoc-in db [:keycard :pin :status] :frozen-card)} + hide-connection-sheet) + {:db (assoc db :popover/popover {:view :frozen-card})})) + (fx/defn on-get-keys-error {:events [:keycard.callback/on-get-keys-error]} [{:keys [db] :as cofx} error] (log/debug "[keycard] get keys error: " error) (let [tag-was-lost? (tag-lost? (:error error)) key-uid (get-in db [:keycard :application-info :key-uid]) - flow (get-in db [:keycard :flow])] + flow (get-in db [:keycard :flow]) + pin-retries-count (pin-retries (:error error))] (if tag-was-lost? {:db (assoc-in db [:keycard :pin :status] nil)} - (if (re-matches pin-mismatch-error (:error error)) + (if-not (nil? pin-retries-count) (fx/merge cofx - {:keycard/get-application-info - {:pairing (get-pairing db key-uid)} - - :db - (update-in db [:keycard :pin] merge - {:status :error - :login [] - :import-multiaccount [] - :error-label :t/pin-mismatch})} + {:db (-> db + (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries-count) + (update-in [:keycard :pin] assoc + :status :error + :login [] + :import-multiaccount [] + :error-label :t/pin-mismatch))} (hide-connection-sheet) + (when (zero? pin-retries-count) (frozen-keycard-popup)) (when (= flow :import) (navigation/navigate-to-cofx :keycard-recovery-pin nil))) (show-wrong-keycard-alert true))))) @@ -384,15 +429,6 @@ {:keycard/get-application-info {:pairing pairing' :on-success on-card-read}})) -(fx/defn frozen-keycard-popup - [{:keys [db] :as cofx}] - (if (:multiaccounts/login db) - (fx/merge - cofx - {:db (assoc-in db [:keycard :pin :status] :frozen-card)} - hide-connection-sheet) - {:db (assoc db :popover/popover {:view :frozen-card})})) - (fx/defn on-get-application-info-success {:events [:keycard.callback/on-get-application-info-success]} [{:keys [db] :as cofx} info on-success] @@ -426,7 +462,6 @@ hide-connection-sheet) (when on-success' (dispatch-event cofx on-success'))))))) - (fx/defn on-get-application-info-error {:events [:keycard.callback/on-get-application-info-error]} [{:keys [db] :as cofx} error] diff --git a/src/status_im/keycard/common_test.cljs b/src/status_im/keycard/common_test.cljs index 1b92c619a24e..6f09863830d9 100644 --- a/src/status_im/keycard/common_test.cljs +++ b/src/status_im/keycard/common_test.cljs @@ -5,7 +5,7 @@ (deftest test-show-connection-sheet (testing "the card is not connected yet" (let [db {:keycard {:card-connected? false}} - res (common/show-connection-sheet + res (common/show-connection-sheet-component {:db db} {:on-card-connected :do-something :handler (fn [{:keys [db]}] @@ -16,7 +16,7 @@ (is (true? (get-in res [:db :bottom-sheet/show?]))))) (testing "the card is connected before the interaction" (let [db {:keycard {:card-connected? true}} - res (common/show-connection-sheet + res (common/show-connection-sheet-component {:db db} {:on-card-connected :do-something :handler (fn [{:keys [db]}] @@ -30,13 +30,13 @@ (is (thrown? js/Error - (common/show-connection-sheet + (common/show-connection-sheet-component {:db {}} {:handler (fn [_])})))) (testing "handler is not specified" (is (thrown? js/Error - (common/show-connection-sheet + (common/show-connection-sheet-component {:db {}} {:on-card-connected :do-something}))))) diff --git a/src/status_im/keycard/core.cljs b/src/status_im/keycard/core.cljs index d495aad311de..e0b78a898006 100644 --- a/src/status_im/keycard/core.cljs +++ b/src/status_im/keycard/core.cljs @@ -135,18 +135,19 @@ (let [pairing (common/get-pairing db) reset-pin (get-in db [:keycard :pin :reset])] (fx/merge cofx - {:keycard/get-application-info - {:pairing pairing} - - :db - (update-in db [:keycard :pin] merge - {:status :after-unblocking - :enter-step :login - :login reset-pin - :confirmation [] - :puk [] - :puk-restore? true - :error-label nil})} + {:db + (-> db + (update-in [:keycard :application-info] assoc + :puk-retry-counter 5 + :pin-retry-counter 3) + (update-in [:keycard :pin] assoc + :status :after-unblocking + :enter-step :login + :login reset-pin + :confirmation [] + :puk [] + :puk-restore? true + :error-label nil))} (common/hide-connection-sheet) (common/clear-on-card-connected) (common/clear-on-card-read)))) @@ -155,19 +156,20 @@ {:events [:keycard.callback/on-unblock-pin-error]} [{:keys [db] :as cofx} error] (let [pairing (common/get-pairing db) - tag-was-lost? (common/tag-lost? (:error error))] + tag-was-lost? (common/tag-lost? (:error error)) + puk-retries (common/pin-retries (:error error))] (log/debug "[keycard] unblock pin error" error) (when-not tag-was-lost? (fx/merge cofx - {:keycard/get-application-info - {:pairing pairing} - - :db - (update-in db [:keycard :pin] merge - {:status :error - :error-label :t/puk-mismatch - :enter-step :puk - :puk []})} + {:db + (-> db + (assoc-in [:keycard :application-info :puk-retry-counter] puk-retries) + (update-in [:keycard :pin] merge + {:status (if (zero? puk-retries) :blocked-card :error) + :error-label :t/puk-mismatch + :enter-step :puk + :puk []}))} + (common/hide-connection-sheet))))) (fx/defn clear-on-verify-handlers @@ -188,8 +190,9 @@ (common/clear-on-card-read) ;; TODO(Ferossgp): Each pin input should handle this event on it's own, ;; now for simplicity do not hide bottom sheet when generating key - ;; but should be refactored. - (when-not (= on-verified :keycard/generate-and-load-key) + ;; and exporting key but should be refactored. + (when-not (contains? #{:keycard/generate-and-load-key + :wallet.accounts/generate-new-keycard-account} on-verified) (common/hide-connection-sheet)) (when-not (contains? #{:keycard/unpair :keycard/generate-and-load-key @@ -207,28 +210,30 @@ (let [tag-was-lost? (common/tag-lost? (:error error)) setup? (boolean (get-in db [:keycard :setup-step])) on-verified-failure (get-in db [:keycard :pin :on-verified-failure]) - exporting? (get-in db [:keycard :on-export-success])] + exporting? (get-in db [:keycard :on-export-success]) + pin-retries (common/pin-retries (:error error))] (log/debug "[keycard] verify pin error" error) (when-not tag-was-lost? - (if (re-matches common/pin-mismatch-error (:error error)) + (if-not (nil? pin-retries) (fx/merge cofx - {:db (update-in db [:keycard :pin] - merge - {:status :error - :enter-step :current - :puk [] - :current [] - :original [] - :confirmation [] - :sign [] - :error-label :t/pin-mismatch})} + {:db (-> db + (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries) + (update-in [:keycard :pin] assoc + :status :error + :enter-step :current + :puk [] + :current [] + :original [] + :confirmation [] + :sign [] + :error-label :t/pin-mismatch))} (common/hide-connection-sheet) (when (and (not setup?) (not on-verified-failure)) (if exporting? (navigation/navigate-back) (navigation/navigate-to-cofx :enter-pin-settings nil))) - (common/get-application-info (common/get-pairing db) nil) + (when (zero? pin-retries) (common/frozen-keycard-popup)) (when on-verified-failure (fn [_] {:utils/dispatch-later [{:dispatch [on-verified-failure] @@ -414,7 +419,7 @@ (assoc-in [:keycard :setup-step] next-step) (assoc-in [:keycard :secrets :pairing] pairing) (assoc-in [:keycard :secrets :paired-on] paired-on))} - (common/hide-connection-sheet) + (when-not (and (= flow :recovery) (= next-step :card-ready)) (common/hide-connection-sheet)) (when multiaccount (set-multiaccount-pairing multiaccount pairing paired-on)) (when (= flow :login) @@ -525,6 +530,21 @@ (log/debug "[keycard] card disconnected") {:db (assoc-in db [:keycard :card-connected?] false)}) +(fx/defn on-nfc-user-cancelled + {:events [:keycard.callback/on-nfc-user-cancelled]} + [{:keys [db]} _] + (log/debug "[keycard] nfc user cancelled") + {:dispatch [:signing.ui/cancel-is-pressed]}) + +(fx/defn on-nfc-timeout + {:events [:keycard.callback/on-nfc-timeout]} + [{:keys [db]} _] + (log/debug "[keycard] nfc timeout") + {:db (-> db + (assoc-in [:keycard :nfc-running?] false) + (assoc-in [:keycard :card-connected?] false)) + :keycard/start-nfc nil}) + (fx/defn on-register-card-events {:events [:keycard.callback/on-register-card-events]} [{:keys [db]} listeners] @@ -552,4 +572,40 @@ {:events [:keycard.ui/pin-numpad-delete-button-pressed]} [{:keys [db]} step] (when-not (empty? (get-in db [:keycard :pin step])) - {:db (update-in db [:keycard :pin step] pop)})) \ No newline at end of file + {:db (update-in db [:keycard :pin step] pop)})) + +(fx/defn start-nfc + {:events [:keycard.ui/start-nfc]} + [cofx] + {:keycard/start-nfc nil}) + +(fx/defn stop-nfc + {:events [:keycard.ui/stop-nfc]} + [cofx] + {:keycard/stop-nfc nil + :keycard.callback/on-card-disconnected nil}) + +(fx/defn start-nfc-success + {:events [:keycard.callback/start-nfc-success]} + [{:keys [db]} _] + (log/debug "[keycard] nfc started success") + {:db (assoc-in db [:keycard :nfc-running?] true)}) + +(fx/defn start-nfc-failure + {:events [:keycard.callback/start-nfc-failure]} + [{:keys [db]} _] + (log/debug "[keycard] nfc failed starting")) ;; leave current value on :nfc-running + +(fx/defn stop-nfc-success + {:events [:keycard.callback/stop-nfc-success]} + [{:keys [db]} _] + (log/debug "[keycard] nfc stopped success") + (log/debug "[keycard] setting card-connected? and nfc-running? to false") + {:db (-> db + (assoc-in [:keycard :nfc-running?] false) + (assoc-in [:keycard :card-connected?] false))}) + +(fx/defn stop-nfc-failure + {:events [:keycard.callback/stop-nfc-failure]} + [{:keys [db]} _] + (log/debug "[keycard] nfc failed stopping")) ;; leave current value on :nfc-running diff --git a/src/status_im/keycard/export_key.cljs b/src/status_im/keycard/export_key.cljs index d0b10e913101..59faf6690eb7 100644 --- a/src/status_im/keycard/export_key.cljs +++ b/src/status_im/keycard/export_key.cljs @@ -8,26 +8,29 @@ {:events [:keycard.callback/on-export-key-error]} [{:keys [db] :as cofx} error] (log/debug "[keycard] export key error" error) - (let [tag-was-lost? (common/tag-lost? (:error error))] + (let [tag-was-lost? (common/tag-lost? (:error error)) + pin-retries (common/pin-retries (:error error))] (cond tag-was-lost? (fx/merge cofx {:db (assoc-in db [:keycard :pin :status] nil)} (common/set-on-card-connected :wallet.accounts/generate-new-keycard-account)) - (re-matches common/pin-mismatch-error (:error error)) + (not (nil? pin-retries)) (fx/merge cofx - {:db (update-in db [:keycard :pin] merge {:status :error - :enter-step :export-key - :puk [] - :current [] - :original [] - :confirmation [] - :sign [] - :export-key [] - :error-label :t/pin-mismatch})} + {:db (-> db + (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries) + (update-in [:keycard :pin] assoc + :status :error + :enter-step :export-key + :puk [] + :current [] + :original [] + :confirmation [] + :sign [] + :export-key [] + :error-label :t/pin-mismatch))} (common/hide-connection-sheet) - (common/get-application-info (common/get-pairing db) nil)) - + (when (zero? pin-retries) (common/frozen-keycard-popup))) :else (fx/merge cofx (common/show-wrong-keycard-alert true) diff --git a/src/status_im/keycard/fx.cljs b/src/status_im/keycard/fx.cljs index 655297f15d1a..c81e59196131 100644 --- a/src/status_im/keycard/fx.cljs +++ b/src/status_im/keycard/fx.cljs @@ -3,10 +3,58 @@ [status-im.utils.types :as types] [status-im.keycard.card :as card] [status-im.native-module.core :as status] - [status-im.utils.platform :as platform] ["react-native" :refer (BackHandler)] + [taoensso.timbre :as log] ["@react-native-community/async-storage" :default AsyncStorage])) +(re-frame/reg-fx + :keycard/start-nfc + (fn [] + (log/debug "fx start-nfc") + (card/start-nfc + {:on-success #(re-frame/dispatch [:keycard.callback/start-nfc-success]) + :on-failure #(re-frame/dispatch [:keycard.callback/start-nfc-failure])}))) + +(re-frame/reg-fx + :keycard/stop-nfc + (fn [] + (log/debug "fx stop-nfc") + (card/stop-nfc + {:on-success #(re-frame/dispatch [:keycard.callback/stop-nfc-success]) + :on-failure #(re-frame/dispatch [:keycard.callback/stop-nfc-failure])}))) + +(re-frame/reg-fx + :keycard/set-nfc-message + card/set-nfc-message) + +(re-frame/reg-fx + :keycard/start-nfc-and-show-connection-sheet + (fn [args] + (log/debug "fx start-nfc-and-show-connection-sheet") + (card/start-nfc + {:on-success + (fn [] + (log/debug "nfc started successfully. next: show-connection-sheet") + (re-frame/dispatch [:keycard.callback/start-nfc-success]) + (re-frame/dispatch [:keycard.callback/show-connection-sheet args])) + :on-failure + (fn [] + (log/debug "nfc failed star starting. not calling show-connection-sheet") + (re-frame/dispatch [:keycard.callback/start-nfc-failure]))}))) + +(re-frame/reg-fx + :keycard/stop-nfc-and-hide-connection-sheet + (fn [] + (log/debug "fx stop-nfc-and-hide-connection-sheet") + (card/stop-nfc + {:on-success + (fn [] + (re-frame/dispatch [:keycard.callback/stop-nfc-success]) + (re-frame/dispatch [:keycard.callback/hide-connection-sheet])) + :on-failure + (fn [] + (re-frame/dispatch [:keycard.callback/stop-nfc-failure]))}))) + (re-frame/reg-fx :keycard/get-application-info card/get-application-info) @@ -83,6 +131,10 @@ :keycard/unpair-and-delete card/unpair-and-delete) +(re-frame/reg-fx + :keycard/import-keys + card/import-keys) + (re-frame/reg-fx :keycard/get-keys card/get-keys) @@ -112,11 +164,10 @@ (re-frame/reg-fx :keycard/retrieve-pairings (fn [] - (when platform/android? - (.. AsyncStorage - (getItem "status-keycard-pairings") - (then #(re-frame/dispatch [:keycard.callback/on-retrieve-pairings-success - (types/deserialize %)])))))) + (.. AsyncStorage + (getItem "status-keycard-pairings") + (then #(re-frame/dispatch [:keycard.callback/on-retrieve-pairings-success + (types/deserialize %)]))))) ;; TODO: Should act differently on different views (re-frame/reg-fx diff --git a/src/status_im/keycard/ios_keycard.cljs b/src/status_im/keycard/ios_keycard.cljs index 60fcdfd73e55..f5c738924ce3 100644 --- a/src/status_im/keycard/ios_keycard.cljs +++ b/src/status_im/keycard/ios_keycard.cljs @@ -26,6 +26,7 @@ (remove-key-with-unpair [this args]) (export-key [this args]) (unpair-and-delete [this args]) + (import-keys [this args]) (get-keys [this args]) (sign [this args]) (save-multiaccount-and-login [this args]) diff --git a/src/status_im/keycard/keycard.cljs b/src/status_im/keycard/keycard.cljs index 0d47ccd789cf..76d9a1ae0f61 100644 --- a/src/status_im/keycard/keycard.cljs +++ b/src/status_im/keycard/keycard.cljs @@ -1,6 +1,9 @@ (ns status-im.keycard.keycard) (defprotocol Keycard + (start-nfc [this args]) + (stop-nfc [this args]) + (set-nfc-message [this args]) (check-nfc-support [this args]) (check-nfc-enabled [this args]) (open-nfc-settings [this]) @@ -25,6 +28,7 @@ (remove-key-with-unpair [this args]) (export-key [this args]) (unpair-and-delete [this args]) + (import-keys [this args]) (get-keys [this args]) (sign [this args]) (sign-typed-data [this args]) diff --git a/src/status_im/keycard/login.cljs b/src/status_im/keycard/login.cljs index 7bbe328d1af1..260ea821d963 100644 --- a/src/status_im/keycard/login.cljs +++ b/src/status_im/keycard/login.cljs @@ -123,7 +123,7 @@ (and (zero? pin-retry-counter) (or (nil? puk-retry-counter) - (= 5 puk-retry-counter))) + (pos? puk-retry-counter))) nil #_(frozen-keycard-popup cofx) :else diff --git a/src/status_im/keycard/real_keycard.cljs b/src/status_im/keycard/real_keycard.cljs index adf1a35ef6c8..9491984b3fe8 100644 --- a/src/status_im/keycard/real_keycard.cljs +++ b/src/status_im/keycard/real_keycard.cljs @@ -5,11 +5,36 @@ [status-im.native-module.core :as status] [status-im.ethereum.core :as ethereum] [status-im.keycard.keycard :as keycard] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [status-im.utils.platform :as platform])) + +(defonce event-emitter (if platform/ios? + (new (.-NativeEventEmitter rn) status-keycard) + (.-DeviceEventEmitter rn))) -(defonce event-emitter (.-DeviceEventEmitter rn)) (defonce active-listeners (atom [])) +(defn start-nfc [{:keys [on-success on-failure prompt-message]}] + (log/debug "start-nfc") + (.. status-keycard + (startNFC (str prompt-message)) + (then on-success) + (catch on-failure))) + +(defn stop-nfc [{:keys [on-success on-failure error-message]}] + (log/debug "stop-nfc") + (.. status-keycard + (stopNFC (str error-message)) + (then on-success) + (catch on-failure))) + +(defn set-nfc-message [{:keys [on-success on-failure status-message]}] + (log/debug "set-nfc-message") + (.. status-keycard + (setNFCMessage (str status-message)) + (then on-success) + (catch on-failure))) + (defn check-nfc-support [{:keys [on-success]}] (.. status-keycard nfcIsSupported @@ -24,7 +49,7 @@ (.openNfcSettings status-keycard)) (defn remove-event-listeners [] - (doseq [event ["keyCardOnConnected" "keyCardOnDisconnected"]] + (doseq [event ["keyCardOnConnected" "keyCardOnDisconnected", "keyCardOnNFCUserCancelled", "keyCardOnNFCTimeout"]] (.removeAllListeners ^js event-emitter event))) (defn remove-event-listener @@ -39,6 +64,14 @@ [callback] (.addListener ^js event-emitter "keyCardOnDisconnected" callback)) +(defn on-nfc-user-cancelled + [callback] + (.addListener ^js event-emitter "keyCardOnNFCUserCancelled" callback)) + +(defn on-nfc-timeout + [callback] + (.addListener ^js event-emitter "keyCardOnNFCTimeout" callback)) + (defn on-nfc-enabled [callback] (.addListener ^js event-emitter "keyCardOnNFCEnabled" callback)) @@ -54,6 +87,8 @@ (reset! active-listeners [(on-card-connected (:on-card-connected args)) (on-card-disconnected (:on-card-disconnected args)) + (on-nfc-user-cancelled (:on-nfc-user-cancelled args)) + (on-nfc-timeout (:on-nfc-timeout args)) (on-nfc-enabled (:on-nfc-enabled args)) (on-nfc-disabled (:on-nfc-disabled args))])) @@ -176,6 +211,14 @@ (then on-success) (catch on-failure)))) +(defn import-keys + [{:keys [pairing pin on-success on-failure]}] + (when (and pairing (not-empty pin)) + (.. status-keycard + (importKeys pairing pin) + (then on-success) + (catch on-failure)))) + (defn get-keys [{:keys [pairing pin on-success on-failure]}] (when (and pairing (not-empty pin)) @@ -226,6 +269,12 @@ (defrecord RealKeycard [] keycard/Keycard + (keycard/start-nfc [this args] + (start-nfc args)) + (keycard/stop-nfc [this args] + (stop-nfc args)) + (keycard/set-nfc-message [this args] + (set-nfc-message args)) (keycard/check-nfc-support [this args] (check-nfc-support args)) (keycard/check-nfc-enabled [this args] @@ -274,6 +323,8 @@ (export-key args)) (keycard/unpair-and-delete [this args] (unpair-and-delete args)) + (keycard/import-keys [this args] + (import-keys args)) (keycard/get-keys [this args] (get-keys args)) (keycard/sign [this args] diff --git a/src/status_im/keycard/recovery.cljs b/src/status_im/keycard/recovery.cljs index 4dd7823e038f..cc54101b2573 100644 --- a/src/status_im/keycard/recovery.cljs +++ b/src/status_im/keycard/recovery.cljs @@ -215,6 +215,7 @@ (update :instance-uid #(get-in db [:keycard :multiaccount :instance-uid] %)))) (assoc-in [:keycard :multiaccount-wallet-address] (:wallet-address account-data)) (assoc-in [:keycard :multiaccount-whisper-public-key] (:whisper-public-key account-data)) + (assoc-in [:keycard :pin :status] nil) (assoc-in [:keycard :application-info :key-uid] (ethereum/normalized-hex (:key-uid account-data))) (update :keycard dissoc :recovery-phrase) @@ -246,11 +247,12 @@ (fx/merge cofx {:db (-> db (assoc-in [:keycard :multiaccount :instance-uid] instance-uid) + (assoc-in [:keycard :pin :status] :verifying) (assoc-in [:keycard :secrets] {:pairing pairing' :paired-on (utils.datetime/timestamp)})) - :keycard/get-keys {:pairing pairing' - :pin pin - :on-success :keycard.callback/on-generate-and-load-key-success}}))) + :keycard/import-keys {:pairing pairing' + :pin pin + :on-success :keycard.callback/on-generate-and-load-key-success}}))) (fx/defn load-recovering-key-screen {:events [:keycard/load-recovering-key-screen]} diff --git a/src/status_im/keycard/sign.cljs b/src/status_im/keycard/sign.cljs index 77bb37db22e6..c97806aefa34 100644 --- a/src/status_im/keycard/sign.cljs +++ b/src/status_im/keycard/sign.cljs @@ -21,7 +21,7 @@ data (get-in db [:keycard :data]) typed? (get-in db [:keycard :typed?]) pin (common/vector->string (get-in db [:keycard :pin :sign])) - from (get-in db [:signing/tx :from :address]) + from (or (get-in db [:signing/tx :from :address]) (get-in db [:signing/tx :message :from]) (ethereum/default-address db)) path (reduce (fn [_ {:keys [address path]}] (when (= from address) @@ -198,20 +198,20 @@ [{:keys [db] :as cofx} error] (log/debug "[keycard] sign error: " error) (let [tag-was-lost? (common/tag-lost? (:error error)) - pin-retries (get-in db [:keycard :application-info :pin-retry-counter])] + pin-retries (common/pin-retries (:error error))] (when-not tag-was-lost? - (if (re-matches common/pin-mismatch-error (:error error)) + (if (not (nil? pin-retries)) (fx/merge cofx {:db (-> db - (assoc-in [:keycard :application-info :pin-retry-counter] (dec pin-retries)) - (update-in [:keycard :pin] merge {:status :error - :sign [] - :error-label :t/pin-mismatch}) + (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries) + (update-in [:keycard :pin] assoc + :status :error + :sign [] + :error-label :t/pin-mismatch) (assoc-in [:signing/sign :keycard-step] :pin))} (common/hide-connection-sheet) - (common/get-application-info (common/get-pairing db) nil) - (when (zero? (dec pin-retries)) - (common/frozen-keycard-popup))) + (when (zero? pin-retries) (common/frozen-keycard-popup))) + (fx/merge cofx (common/hide-connection-sheet) (common/show-wrong-keycard-alert true)))))) diff --git a/src/status_im/keycard/simulated_keycard.cljs b/src/status_im/keycard/simulated_keycard.cljs index d0469af3abf7..ab328dfcb814 100644 --- a/src/status_im/keycard/simulated_keycard.cljs +++ b/src/status_im/keycard/simulated_keycard.cljs @@ -14,6 +14,7 @@ (def initial-state {:card-connected? false + :nfc-started? false :application-info {:initialized? false}}) (defonce state (atom initial-state)) @@ -68,6 +69,16 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn start-nfc [{:keys [on-success]}] + (when (get @state :card-connected?) (connect-card)) + (later #(on-success true))) + +(defn stop-nfc [{:keys [on-success]}] + (later #(on-success true))) + +(defn set-nfc-message [{:keys [on-success]}] + (later #(on-success true))) + (defn check-nfc-support [{:keys [on-success]}] (later #(on-success true))) @@ -87,18 +98,22 @@ id)) (defn register-card-events [args] + (log/debug "register-card-events") (on-card-connected (:on-card-connected args)) (on-card-disconnected (:on-card-disconnected args))) (defn remove-event-listener [id] + (log/debug "remove-event-listener") (swap! state update :on-card-connected dissoc id) (swap! state update :on-card-disconnected dissoc id)) (defn remove-event-listeners [] + (log/debug "remove-event-listeners") (swap! state dissoc :on-card-connected) (swap! state dissoc :on-card-disconnected)) (defn get-application-info [{:keys [on-success]}] + (log/debug "get-application-info") (later #(on-success (get @state :application-info)))) (defn install-applet [_]) @@ -321,6 +336,8 @@ #js {:code "EUNSPECIFIED" :message "Unexpected error SW, 0x63C2"}))))) +(def import-keys get-keys) + (defn sign [{:keys [pin hash data path typed? on-success on-failure]}] (if (= pin (get @state :pin)) (later @@ -390,6 +407,15 @@ (defrecord SimulatedKeycard [] keycard/Keycard + (keycard/start-nfc [this args] + (log/debug "simulated card start-nfc") + (start-nfc args)) + (keycard/stop-nfc [this args] + (log/debug "simulated card stop-nfc") + (stop-nfc args)) + (keycard/set-nfc-message [this args] + (log/debug "simulated card set-nfc-message") + (set-nfc-message args)) (keycard/check-nfc-support [this args] (log/debug "simulated card check-nfc-support") (check-nfc-support args)) @@ -459,6 +485,9 @@ (keycard/unpair-and-delete [this args] (log/debug "simulated card unpair-and-delete") (unpair-and-delete args)) + (keycard/import-keys [this args] + (log/debug "simulated card import-keys") + (import-keys args)) (keycard/get-keys [this args] (log/debug "simulated card get-keys") (get-keys args)) diff --git a/src/status_im/multiaccounts/create/core.cljs b/src/status_im/multiaccounts/create/core.cljs index 6016f2b50217..f70ba8778877 100644 --- a/src/status_im/multiaccounts/create/core.cljs +++ b/src/status_im/multiaccounts/create/core.cljs @@ -14,7 +14,6 @@ [status-im.navigation :as navigation] [status-im.utils.config :as config] [status-im.utils.fx :as fx] - [status-im.utils.platform :as platform] [status-im.utils.security :as security] [status-im.utils.signing-phrase.core :as signing-phrase] [status-im.utils.types :as types] @@ -30,17 +29,14 @@ (defn decrement-step [step] (let [inverted (map-invert step-kw-to-num)] (if (and (= step :create-code) - (or (not platform/android?) - (not (nfc/nfc-supported?)))) + (not (nfc/nfc-supported?))) :choose-key (inverted (dec (step-kw-to-num step)))))) (defn inc-step [step] (let [inverted (map-invert step-kw-to-num)] (if (and (= step :choose-key) - (or (not (or platform/android? - config/keycard-test-menu-enabled?)) - (not (nfc/nfc-supported?)))) + (not (nfc/nfc-supported?))) :create-code (inverted (inc (step-kw-to-num step)))))) diff --git a/src/status_im/multiaccounts/recover/core.cljs b/src/status_im/multiaccounts/recover/core.cljs index dae5736eed8d..1071bd4ed50a 100644 --- a/src/status_im/multiaccounts/recover/core.cljs +++ b/src/status_im/multiaccounts/recover/core.cljs @@ -14,11 +14,9 @@ [status-im.utils.fx :as fx] [status-im.utils.security :as security] [status-im.utils.types :as types] - [status-im.utils.platform :as platform] [status-im.utils.utils :as utils] [status-im.bottom-sheet.core :as bottom-sheet] - [taoensso.timbre :as log] - [status-im.utils.config :as config])) + [taoensso.timbre :as log])) (defn existing-account? [multiaccounts key-uid] @@ -234,9 +232,7 @@ assoc :step :select-key-storage :forward-action :multiaccounts.recover/select-storage-next-pressed :selected-storage-type :default)} - (if (and (or platform/android? - config/keycard-test-menu-enabled?) - (nfc/nfc-supported?)) + (if (nfc/nfc-supported?) (navigation/navigate-to-cofx :recover-multiaccount-select-storage nil) (select-storage-next-pressed)))) diff --git a/src/status_im/notifications/local.cljs b/src/status_im/notifications/local.cljs index 3f0c6c3b9d9c..0893e318d910 100644 --- a/src/status_im/notifications/local.cljs +++ b/src/status_im/notifications/local.cljs @@ -17,7 +17,8 @@ [status-im.ui.screens.chat.components.reply :as reply] [clojure.string :as clojure.string] [status-im.chat.models :as chat.models] - [status-im.constants :as constants])) + [status-im.constants :as constants] + [status-im.utils.identicon :as identicon])) (def default-erc20-token {:symbol :ERC20 @@ -107,30 +108,36 @@ :user-info notification :message description})) +(defn chat-by-message + [{:keys [chats]} {:keys [localChatId from]}] + (if-let [chat (get chats localChatId)] + (assoc chat :chat-id localChatId) + (assoc (get chats from) :chat-id from))) + (defn show-message-pn? - [{{:keys [app-state multiaccount]} :db :as cofx} - {{:keys [message chat]} :body}] - (let [chat-id (get chat :id) - chat-type (get chat :chatType)] - (and - (or (= app-state "background") - (not (chat.models/foreground-chat? cofx chat-id))) - (or (contains? #{constants/one-to-one-chat-type - constants/private-group-chat-type} - chat-type) - (contains? (set (get message :mentions)) - (get multiaccount :public-key)))))) + [{{:keys [app-state]} :db :as cofx} + {{:keys [chat-id]} :chat}] + (or (= app-state "background") + (not (chat.models/foreground-chat? cofx chat-id)))) (defn create-message-notification - ([cofx notification] - (when (or (nil? cofx) - (show-message-pn? cofx notification)) - (create-message-notification notification))) - ([{{:keys [message contact chat]} :body}] - (let [chat-type (get chat :chatType) - chat-id (get chat :id) - contact-name @(re-frame/subscribe - [:contacts/contact-name-by-identity (get contact :id)]) + ([{:keys [db] :as cofx} {{:keys [message]} :body :as notification}] + (when-not (nil? cofx) + (let [chat (chat-by-message db message) + contact-id (get message :from) + contact (get-in db [:contacts/contacts contact-id]) + notification (assoc notification + :chat chat + :contact-id contact-id + :contact contact)] + (when (show-message-pn? cofx notification) + (create-message-notification notification))))) + ([{{:keys [message]} :body + {:keys [chat-type chat-id] :as chat} :chat + {:keys [identicon]} :contact + contact-id :contact-id}] + (let [contact-name @(re-frame/subscribe + [:contacts/contact-name-by-identity contact-id]) group-chat? (not= chat-type constants/one-to-one-chat-type) title (clojure.string/join " " @@ -148,11 +155,11 @@ "#") (get chat :name)))))] {:type "message" - :chatType (str (get chat :chatType)) + :chatType (str chat-type) :from title :chatId chat-id :alias title - :identicon (get contact :identicon) + :identicon (or identicon (identicon/identicon contact-id)) :whisperTimestamp (get message :whisperTimestamp) :text (reply/get-quoted-text-with-mentions (:parsedText message))}))) diff --git a/src/status_im/signals/core.cljs b/src/status_im/signals/core.cljs index 86b0ff9baca0..43dd25230af5 100644 --- a/src/status_im/signals/core.cljs +++ b/src/status_im/signals/core.cljs @@ -7,6 +7,7 @@ [status-im.transport.filters.core :as transport.filters] [status-im.transport.message.core :as transport.message] [status-im.notifications.local :as local-notifications] + [status-im.chat.models.message :as models.message] [status-im.utils.fx :as fx] [taoensso.timbre :as log])) @@ -54,6 +55,8 @@ "node.login" (status-node-started cofx (js->clj event-js :keywordize-keys true)) "envelope.sent" (transport.message/update-envelopes-status cofx (:ids (js->clj event-js :keywordize-keys true)) :sent) "envelope.expired" (transport.message/update-envelopes-status cofx (:ids (js->clj event-js :keywordize-keys true)) :not-sent) + "message.delivered" (let [{:keys [chatID messageID] :as event-cljs} (js->clj event-js :keywordize-keys true)] + (models.message/update-db-message-status cofx chatID messageID :delivered)) "mailserver.request.completed" (mailserver/handle-request-completed cofx (js->clj event-js :keywordize-keys true)) "mailserver.request.expired" (when (multiaccounts.model/logged-in? cofx) (mailserver/resend-request cofx {:request-id (.-hash event-js)})) diff --git a/src/status_im/signing/core.cljs b/src/status_im/signing/core.cljs index 0c62cd2b4999..10c5a55e4200 100644 --- a/src/status_im/signing/core.cljs +++ b/src/status_im/signing/core.cljs @@ -9,6 +9,7 @@ [status-im.ethereum.tokens :as tokens] [status-im.keycard.common :as keycard.common] [status-im.i18n.i18n :as i18n] + [status-im.keycard.card :as keycard.card] [status-im.native-module.core :as status] [status-im.signing.keycard :as signing.keycard] [status-im.utils.fx :as fx] @@ -203,6 +204,8 @@ :formatted-data (if typed? (types/json->clj data) (ethereum/hex->text data)) :keycard-step (when pinless? :connect)})} (when pinless? + (keycard.card/start-nfc {:on-success #(re-frame/dispatch [:keycard.callback/start-nfc-success]) + :on-failure #(re-frame/dispatch [:keycard.callback/start-nfc-failure])}) (signing.keycard/hash-message {:data data :typed? true :on-completed #(re-frame/dispatch [:keycard/store-hash-and-sign-typed %])}))) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 53de9fe87d87..6d4296891be1 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -213,6 +213,11 @@ (reg-root-key-sub :buy-crypto/on-ramps :buy-crypto/on-ramps) ;; communities + +(reg-root-key-sub :communities/create :communities/create) +(reg-root-key-sub :communities/requests-to-join :communities/requests-to-join) +(reg-root-key-sub :communities/community-id-input :communities/community-id-input) + (re-frame/reg-sub :communities (fn [db] @@ -225,6 +230,17 @@ :else []))) +(re-frame/reg-sub + :communities/section-list + :<- [:communities] + (fn [communities] + (->> (vals communities) + (group-by (comp (fnil string/upper-case "") first :name)) + (sort-by (fn [[title]] title)) + (map (fn [[title data]] + {:title title + :data data}))))) + (re-frame/reg-sub :communities/community :<- [:communities] @@ -232,16 +248,25 @@ (get communities id))) (re-frame/reg-sub - :communities/status-community + :communities/communities :<- [:search/home-filter] :<- [:communities] (fn [[search-filter communities]] - (let [status-community (get communities constants/status-community-id)] - (when (and (:joined status-community) - (or (string/blank? search-filter) - (string/includes? (string/lower-case - (get-in status-community [:description :identity :display-name])) search-filter))) - status-community)))) + (filterv + (fn [{:keys [name joined id]}] + (and joined + (or config/communities-management-enabled? + (= id constants/status-community-id)) + (or (empty? search-filter) + (string/includes? (string/lower-case (str name)) search-filter)))) + (vals communities)))) + +(re-frame/reg-sub + :communities/edited-community + :<- [:communities] + :<- [:communities/community-id-input] + (fn [[communities community-id]] + (get communities community-id))) (re-frame/reg-sub :communities/current-community @@ -260,6 +285,16 @@ 0 chats))) +(re-frame/reg-sub + :communities/requests-to-join-for-community + :<- [:communities/requests-to-join] + (fn [requests [_ community-id]] + (->> + (get requests community-id {}) + vals + (filter (fn [{:keys [state]}] + (= state constants/request-to-join-pending-state)))))) + ;;GENERAL ============================================================================================================== @@ -1198,9 +1233,21 @@ :home-items :<- [:search/home-filter] :<- [:search/filtered-chats] - (fn [[search-filter filtered-chats]] - {:search-filter search-filter - :chats filtered-chats})) + :<- [:communities/communities] + (fn [[search-filter filtered-chats communities]] + (let [communities-count (count communities) + chats-count (count filtered-chats) + ;; If we have both communities & chats we want to display + ;; a separator between them + + communities-with-separator (if (and (pos? communities-count) + (pos? chats-count)) + (update communities + (dec communities-count) + assoc :last? true) + communities)] + {:search-filter search-filter + :items (concat communities-with-separator filtered-chats)}))) ;;PAIRING ============================================================================================================== diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs index bc8c48bdd5b1..d14fc6a12cee 100644 --- a/src/status_im/transport/message/core.cljs +++ b/src/status_im/transport/message/core.cljs @@ -28,6 +28,9 @@ (fx/defn handle-community [cofx community] (models.communities/handle-community cofx community)) +(fx/defn handle-request-to-join-community [cofx request] + (models.communities/handle-request-to-join cofx request)) + (fx/defn handle-reactions [cofx reactions] (models.reactions/receive-signal cofx reactions)) @@ -44,6 +47,7 @@ {:events [::process]} [cofx ^js response-js] (let [^js communities (.-communities response-js) + ^js requests-to-join-community (.-requestsToJoinCommunity response-js) ^js chats (.-chats response-js) ^js contacts (.-contacts response-js) ^js installations (.-installations response-js) @@ -72,6 +76,11 @@ (fx/merge cofx {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} (handle-community (types/js->clj community)))) + (seq requests-to-join-community) + (let [request (.pop requests-to-join-community)] + (fx/merge cofx + {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} + (handle-request-to-join-community (types/js->clj request)))) (seq chats) (let [chats-clj (types/js->clj chats)] (js-delete response-js "chats") diff --git a/src/status_im/ui/components/unviewed_indicator.cljs b/src/status_im/ui/components/unviewed_indicator.cljs new file mode 100644 index 000000000000..bc0c37caa94d --- /dev/null +++ b/src/status_im/ui/components/unviewed_indicator.cljs @@ -0,0 +1,10 @@ +(ns status-im.ui.components.unviewed-indicator + (:require [status-im.ui.components.badge :as badge] + [status-im.ui.components.react :as react])) + +(defn unviewed-indicator [c] + (when (pos? c) + [react/view {:padding-left 16 + :justify-content :flex-end + :align-items :flex-end} + [badge/message-counter c]])) diff --git a/src/status_im/ui/screens/bottom_sheets/views.cljs b/src/status_im/ui/screens/bottom_sheets/views.cljs index f88a5fe6a8ad..577f5b0148bd 100644 --- a/src/status_im/ui/screens/bottom_sheets/views.cljs +++ b/src/status_im/ui/screens/bottom_sheets/views.cljs @@ -4,7 +4,6 @@ [status-im.ui.screens.home.sheet.views :as home.sheet] [status-im.ui.screens.keycard.views :as keycard] [status-im.ui.screens.about-app.views :as about-app] - [status-im.ui.screens.communities.views :as communities] [status-im.ui.screens.multiaccounts.recover.views :as recover.views] [quo.core :as quo])) @@ -33,20 +32,8 @@ (= view :learn-more) (merge about-app/learn-more) - (= view :create-community) - (merge communities/create-sheet) - - (= view :import-community) - (merge communities/import-sheet) - - (= view :create-community-channel) - (merge communities/create-channel-sheet) - - (= view :invite-people-community) - (merge communities/invite-people-sheet) - (= view :recover-sheet) (merge recover.views/bottom-sheet))] [quo/bottom-sheet opts (when content - [content])])) \ No newline at end of file + [content])])) diff --git a/src/status_im/ui/screens/chat/message/message.cljs b/src/status_im/ui/screens/chat/message/message.cljs index 5a573c68db30..d2d1ce844c83 100644 --- a/src/status_im/ui/screens/chat/message/message.cljs +++ b/src/status_im/ui/screens/chat/message/message.cljs @@ -2,7 +2,6 @@ (:require [re-frame.core :as re-frame] [status-im.constants :as constants] [status-im.i18n.i18n :as i18n] - [status-im.communities.core :as communities] [status-im.utils.config :as config] [status-im.react-native.resources :as resources] [status-im.ui.components.colors :as colors] @@ -43,13 +42,15 @@ :align-items :flex-end}) (when (and outgoing justify-timestamp?) [icons/icon (case outgoing-status - :sending :tiny-icons/tiny-pending - :sent :tiny-icons/tiny-sent - :not-sent :tiny-icons/tiny-warning + :sending :tiny-icons/tiny-pending + :sent :tiny-icons/tiny-sent + :not-sent :tiny-icons/tiny-warning + :delivered :tiny-icons/tiny-delivered :tiny-icons/tiny-pending) - {:width 16 - :height 12 - :color colors/white}]) + {:width 16 + :height 12 + :color colors/white + :accessibility-label (name outgoing-status)}]) [react/text {:style (style/message-timestamp-text outgoing)} timestamp-str]])) @@ -214,7 +215,7 @@ (chat.utils/format-author contact-with-names opts))) (defview community-content [{:keys [community-id] :as message}] - (letsubs [{:keys [joined verified] :as community} [:communities/community community-id]] + (letsubs [{:keys [name description verified] :as community} [:communities/community community-id]] (when (and config/communities-enabled? community) @@ -251,25 +252,24 @@ :style {:width 40 :height 40}}] - (let [display-name (get-in community [:description :identity :display-name])] - [chat-icon/chat-icon-view-chat-list - display-name - true - display-name - colors/default-community-color]))] + [chat-icon/chat-icon-view-chat-list + name + true + name + colors/default-community-color])] [react/view {:padding-right 14} [react/text {:style {:font-weight "700" :font-size 17}} - (get-in community [:description :identity :display-name])] - [react/text (get-in community [:description :identity :description])]]] + name] + [react/text description]]] [react/view {:border-width 1 :padding-vertical 8 :border-bottom-left-radius 10 :border-bottom-right-radius 10 :border-color colors/gray-lighter} - [react/touchable-highlight {:on-press #(re-frame/dispatch [(if joined ::communities/leave ::communities/join) (:id community)])} + [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to :community {:community-id (:id community)}])} [react/text {:style {:text-align :center - :color colors/blue}} (if joined (i18n/label :t/leave) (i18n/label :t/join))]]]]))) + :color colors/blue}} (i18n/label :t/view)]]]]))) (defn message-content-wrapper "Author, userpic and delivery wrapper" diff --git a/src/status_im/ui/screens/communities/community.cljs b/src/status_im/ui/screens/communities/community.cljs new file mode 100644 index 000000000000..736259dc1a2e --- /dev/null +++ b/src/status_im/ui/screens/communities/community.cljs @@ -0,0 +1,245 @@ +(ns status-im.ui.screens.communities.community + (:require [status-im.ui.components.topbar :as topbar] + [quo.react-native :as rn] + [status-im.ui.components.toolbar :as toolbar] + [quo.core :as quo] + [status-im.constants :as constants] + [status-im.utils.handlers :refer [>evt (datetime/timestamp) (+ (* requested-at 1000) request-cooldown-ms))) + +(defn toolbar-content [id display-name color images show-members-count? members] + (let [thumbnail-image (get-in images [:thumbnail :uri])] + [rn/view {:style {:flex 1 + :align-items :center + :flex-direction :row}} + [rn/view {:padding-right 10} + (cond + (= id constants/status-community-id) + [rn/image {:source (resources/get-image :status-logo) + :style {:width 40 + :height 40}}] + (seq thumbnail-image) + [photos/photo thumbnail-image {:size 40}] + + :else + [chat-icon.screen/chat-icon-view-toolbar + id + true + display-name + (or color (rand-nth colors/chat-colors))])] + [rn/view {:style {:flex 1 :justify-content :center}} + [quo/text {:number-of-lines 1 + :accessibility-label :community-name-text} + display-name] + (when show-members-count? + [quo/text {:number-of-lines 1 + :size :small + :color :secondary} + (i18n/label-pluralize members :t/community-members {:count members})])]])) + +(defn hide-sheet-and-dispatch [event] + (>evt [:bottom-sheet/hide]) + (>evt event)) + +(defn community-actions [{:keys [id + permissions + can-manage-users? name images color]}] + (let [can-invite? (and can-manage-users? (not= (:access permissions) constants/community-no-membership-access)) + can-share? (not= (:access permissions) constants/community-invitation-only-access) + thumbnail-image (get-in images [:thumbnail :uri])] + [:<> + [quo/list-item + {:title name + :on-press #(hide-sheet-and-dispatch [:navigate-to :community-management {:community-id id}]) + :chevron true + :icon (cond + (= id constants/status-community-id) + [rn/image {:source (resources/get-image :status-logo) + :style {:width 40 + :height 40}}] + (seq thumbnail-image) + [photos/photo thumbnail-image {:size 40}] + + :else + [chat-icon.screen/chat-icon-view-chat-sheet + name + true + name + (or color (rand-nth colors/chat-colors))])}] + (when (and config/communities-management-enabled? can-manage-users?) + [:<> + [quo/list-item + {:theme :accent + :title (i18n/label :t/export-key) + :accessibility-label :community-export-key + :icon :main-icons/objects + :on-press #(hide-sheet-and-dispatch [::communities/export-pressed id])}] + [quo/list-item + {:theme :accent + :title (i18n/label :t/create-channel) + :accessibility-label :community-create-channel + :icon :main-icons/channel + :on-press #(hide-sheet-and-dispatch [::communities/create-channel-pressed id])}]]) + (when can-invite? + [quo/list-item + {:theme :accent + :title (i18n/label :t/invite-people) + :icon :main-icons/share + :accessibility-label :community-invite-people + :on-press #(>evt [::communities/invite-people-pressed id])}]) + (when can-share? + [quo/list-item + {:theme :accent + :title (i18n/label :t/share) + :icon :main-icons/share + :accessibility-label :community-share + :on-press #(>evt [::communities/share-community-pressed id])}]) + [quo/list-item + {:theme :accent + :title (i18n/label :t/leave-community) + :accessibility-label :leave + :icon :main-icons/arrow-left + :on-press #(do + (>evt [:bottom-sheet/hide]) + (>evt [:navigate-to :home]) + (>evt [::communities/leave id]))}]])) + +(defn welcome-blank-page [] + [rn/view {:style {:padding 16 :flex 1 :flex-direction :row :align-items :center :justify-content :center}} + [quo/text {:align :center + :color :secondary} + (i18n/label :t/welcome-community-blank-message)]]) + +(defn community-chat-item [home-item] + [inner-item/home-list-item home-item]) + +(defn community-chat-list [chats] + (if (empty? chats) + [welcome-blank-page] + [list/flat-list + {:key-fn :chat-id + :content-container-style {:padding-vertical 8} + :keyboard-should-persist-taps :always + :data chats + :render-fn community-chat-item + :footer [rn/view {:height 68}]}])) + +(defn community-channel-list [id] + (let [chats (> chats + (= id constants/status-community-id) + (map #(assoc % :color colors/blue)))] + [community-chat-list chats])) + +(defn channel-preview-item [{:keys [id color name]}] + (let [color (or color (rand-nth colors/chat-colors))] + [quo/list-item + {:icon [chat-icon.screen/chat-icon-view-chat-list + id true name color false false] + :title [rn/view {:flex-direction :row + :flex 1 + :padding-right 16 + :align-items :center} + [icons/icon :main-icons/tiny-group + {:color colors/black + :width 15 + :height 15 + :container-style {:width 15 + :height 15 + :margin-right 2}}] + [quo/text {:weight :medium + :accessibility-label :chat-name-text + :ellipsize-mode :tail + :number-of-lines 1} + (utils/truncate-str name 30)]] + :title-accessibility-label :chat-name-text}])) + +(defn community-channel-preview-list [_ chats-without-id] + (let [chats (reduce-kv + (fn [acc k v] + (conj acc (assoc v :id (name k)))) + [] + chats-without-id)] + [list/flat-list + {:key-fn :id + :content-container-style {:padding-vertical 8} + :keyboard-should-persist-taps :always + :data chats + :render-fn channel-preview-item}])) + +(defn community [route] + (let [{:keys [community-id]} (get-in route [:route :params]) + {:keys [id + chats + name + images + members + permissions + color + joined + can-request-access? + can-join? + requested-to-join-at + admin] + :as community} (evt [:bottom-sheet/show-sheet + {:content (fn [] + [community-actions community]) + :height 256}])}])}] + (if joined + [community-channel-list id] + [community-channel-preview-list id chats]) + (when-not joined + (cond + can-join? + [toolbar/toolbar + {:show-border? true + :center [quo/button {:on-press #(>evt [::communities/join id]) + :type :secondary} + (i18n/label :t/join)]}] + can-request-access? + (if (and (pos? requested-to-join-at) + (not (can-request-access-again? requested-to-join-at))) + [toolbar/toolbar + {:show-border? true + :left [quo/text {:color :secondary} (i18n/label :t/membership-request-pending)]}] + [toolbar/toolbar + {:show-border? true + :center [quo/button {:on-press #(>evt [::communities/request-to-join id]) + :type :secondary} + (i18n/label :t/request-access)]}]) + :else + [toolbar/toolbar + {:show-border? true + :center [quo/button {:on-press #(>evt [::communities/join id]) + :type :secondary} + (i18n/label :t/follow)]}]))])) diff --git a/src/status_im/ui/screens/communities/create.cljs b/src/status_im/ui/screens/communities/create.cljs new file mode 100644 index 000000000000..74f6f969cf73 --- /dev/null +++ b/src/status_im/ui/screens/communities/create.cljs @@ -0,0 +1,170 @@ +(ns status-im.ui.screens.communities.create + (:require [quo.react-native :as rn] + [status-im.i18n.i18n :as i18n] + [quo.core :as quo] + [clojure.string :as str] + [status-im.utils.handlers :refer [>evt evt [::communities/create-field :image (.-path ^js %)]) + crop-opts)) + +(defn take-pic [] + (react/show-image-picker-camera + #(>evt [::communities/create-field :image (.-path ^js %)]) + crop-opts)) + +(defn bottom-sheet [has-picture] + (fn [] + [:<> + [quo/list-item {:accessibility-label :take-photo + :theme :accent + :icon :main-icons/camera + :title (i18n/label :t/community-image-take) + :on-press #(do + (>evt [:bottom-sheet/hide]) + (take-pic))}] + [quo/list-item {:accessibility-label :pick-photo + :icon :main-icons/gallery + :theme :accent + :title (i18n/label :t/community-image-pick) + :on-press #(do + (>evt [:bottom-sheet/hide]) + (pick-pic))}] + (when has-picture + [quo/list-item {:accessibility-label :remove-photo + :icon :main-icons/delete + :theme :accent + :title (i18n/label :t/community-image-remove) + :on-press #(do + (>evt [:bottom-sheet/hide]))}])])) + +(defn photo-picker [] + (let [{:keys [image]} (evt [:bottom-sheet/show-sheet + {:content (bottom-sheet (boolean image))}])} + [rn/view {:style {:width 128 + :height 128}} + [rn/view {:style {:flex 1 + :border-radius 64 + :background-color (colors/get-color :ui-01) + :justify-content :center + :align-items :center}} + (if image + [rn/image {:source (utils.image/source image) + :style {:width 128 + :height 128 + :border-radius 64} + :resize-mode :cover + :accessibility-label :community-image}] + [:<> + [icons/icon :main-icons/photo {:color (colors/get-color :icon-02)}] + [quo/text {:color :secondary} + (i18n/label :t/community-thumbnail-upload)]])] + [rn/view {:style {:position :absolute + :top 0 + :right 7}} + [rn/view {:style {:width 40 + :height 40 + :background-color (colors/get-color :interactive-01) + :border-radius 20 + :align-items :center + :justify-content :center + :shadow-offset {:width 0 :height 1} + :shadow-radius 6 + :shadow-opacity 1 + :shadow-color (colors/get-color :shadow-01) + :elevation 2}} + [icons/icon :main-icons/add {:color colors/white}]]]]]])) + +(defn countable-label [{:keys [label value max-length]}] + [rn/view {:style {:padding-bottom 10 + :justify-content :space-between + :align-items :flex-end + :flex-direction :row + :flex-wrap :nowrap}} + [quo/text label] + [quo/text {:size :small + :color (if (> (count value) max-length) + :negative + :secondary)} + (str (count value) "/" max-length)]]) + +(defn form [] + (let [{:keys [name description membership]} (evt [::communities/create-field :name %]) + :auto-focus true}]] + [rn/view {:style {:padding-bottom 16 + :padding-top 10 + :padding-horizontal 16}} + [countable-label {:label (i18n/label :t/give-a-short-description-community) + :value description + :max-length max-description-length}] + [quo/text-input + {:placeholder (i18n/label :t/give-a-short-description-community) + :multiline true + :default-value description + :on-change-text #(>evt [::communities/create-field :description %])}]] + [quo/list-header {:color :main} + (i18n/label :t/community-thumbnail-image)] + [photo-picker] + [:<> + [quo/separator {:style {:margin-vertical 10}}] + [quo/list-item {:title (i18n/label :t/membership-button) + :accessory-text (i18n/label (get-in memberships/options [membership :title] :t/membership-none)) + :accessory :text + :on-press #(>evt [:navigate-to :community-membership]) + :chevron true + :size :small}] + [quo/list-footer + (i18n/label (get-in memberships/options [membership :description] :t/membership-none-placeholder))]]])) + +(defn view [] + (let [{:keys [name description]} (evt [::communities/create-confirmation-pressed])} + (i18n/label :t/create)]}]])) diff --git a/src/status_im/ui/screens/communities/create_channel.cljs b/src/status_im/ui/screens/communities/create_channel.cljs new file mode 100644 index 000000000000..b4235c48aa8c --- /dev/null +++ b/src/status_im/ui/screens/communities/create_channel.cljs @@ -0,0 +1,36 @@ +(ns status-im.ui.screens.communities.create-channel + (:require [clojure.string :as str] + [reagent.core :as reagent] + [quo.react-native :as rn] + [quo.core :as quo] + [status-im.i18n.i18n :as i18n] + [status-im.ui.components.toolbar :as toolbar] + [status-im.utils.handlers :refer [>evt]] + [status-im.communities.core :as communities] + [status-im.ui.components.topbar :as topbar])) + +(defn valid? [community-name] + (not (str/blank? community-name))) + +(defn create-channel [] + (let [channel-name (reagent/atom "")] + (fn [] + [:<> + [topbar/topbar {:title (i18n/label :t/create-channel-title)}] + [rn/scroll-view {:style {:flex 1} + :content-container-style {:padding-vertical 16}} + [rn/view {:style {:padding-bottom 16 + :padding-top 10 + :padding-horizontal 16}} + [quo/text-input + {:label (i18n/label :t/name-your-channel) + :placeholder (i18n/label :t/name-your-channel-placeholder) + :on-change-text #(reset! channel-name %) + :auto-focus true}]]] + [toolbar/toolbar + {:show-border? true + :center + [quo/button {:disabled (not (valid? @channel-name)) + :type :secondary + :on-press #(>evt [::communities/create-channel-confirmation-pressed @channel-name])} + (i18n/label :t/create)]}]]))) diff --git a/src/status_im/ui/screens/communities/edit.cljs b/src/status_im/ui/screens/communities/edit.cljs new file mode 100644 index 000000000000..3d2024ea9666 --- /dev/null +++ b/src/status_im/ui/screens/communities/edit.cljs @@ -0,0 +1,21 @@ +(ns status-im.ui.screens.communities.edit + (:require [status-im.ui.components.topbar :as topbar] + [quo.core :as quo] + [status-im.i18n.i18n :as i18n] + [status-im.ui.screens.communities.create :as community.create] + [status-im.utils.handlers :refer [>evt + [topbar/topbar {:title (i18n/label :t/community-edit-title)}] + [community.create/form] + [toolbar/toolbar + {:show-border? true + :center + [quo/button {:disabled (not (community.create/valid? name description)) + :type :secondary + :on-press #(>evt [::communities/edit-confirmation-pressed])} + (i18n/label :t/save)]}]])) diff --git a/src/status_im/ui/screens/communities/import.cljs b/src/status_im/ui/screens/communities/import.cljs new file mode 100644 index 000000000000..604341c7a8c0 --- /dev/null +++ b/src/status_im/ui/screens/communities/import.cljs @@ -0,0 +1,32 @@ +(ns status-im.ui.screens.communities.import + (:require [quo.react-native :as rn] + [reagent.core :as reagent] + [quo.core :as quo] + [status-im.i18n.i18n :as i18n] + [status-im.utils.handlers :refer [>evt]] + [status-im.communities.core :as communities] + [status-im.ui.components.topbar :as topbar] + [status-im.ui.components.toolbar :as toolbar])) + +(defn view [] + (let [community-key (reagent/atom "")] + (fn [] + [rn/view {:style {:flex 1}} + [topbar/topbar {:title (i18n/label :t/import-community-title)}] + [rn/scroll-view {:style {:flex 1} + :content-container-style {:padding 16}} + [rn/view {:style {:padding-bottom 16 + :padding-top 10}} + [quo/text-input + {:label (i18n/label :t/community-key) + :placeholder (i18n/label :t/community-key-placeholder) + :on-change-text #(reset! community-key %) + :default-value @community-key + :auto-focus true}]]] + + [toolbar/toolbar + {:show-border? true + :center [quo/button {:disabled (= @community-key "") + :type :secondary + :on-press #(>evt [::communities/import @community-key])} + (i18n/label :t/import)]}]]))) diff --git a/src/status_im/ui/screens/communities/invite.cljs b/src/status_im/ui/screens/communities/invite.cljs new file mode 100644 index 000000000000..f6414b5575b3 --- /dev/null +++ b/src/status_im/ui/screens/communities/invite.cljs @@ -0,0 +1,79 @@ +(ns status-im.ui.screens.communities.invite + (:require [reagent.core :as reagent] + [quo.react-native :as rn] + [quo.core :as quo] + [status-im.i18n.i18n :as i18n] + [status-im.constants :as constants] + [status-im.ui.components.toolbar :as toolbar] + [status-im.utils.handlers :refer [>evt + [rn/view {:style {:padding-horizontal 16 + :padding-vertical 8}} + [quo/text-input + {:label (i18n/label :t/enter-user-pk) + :placeholder (i18n/label :t/enter-user-pk) + :on-change-text #(reset! user-pk %) + :default-value @user-pk + :auto-focus true}]] + [quo/separator {:style {:margin-vertical 8}}] + [quo/list-header (i18n/label :t/contacts)]]) + +(defn contacts-list-item [{:keys [public-key active] :as contact} _ _ {:keys [selected]}] + (let [[first-name second-name] (multiaccounts/contact-two-names contact true)] + [quo/list-item + {:title first-name + :subtitle second-name + :icon [chat-icon.screen/contact-icon-contacts-tab + (multiaccounts/displayed-photo contact)] + :accessory :checkbox + :active active + :on-press (fn [] + (if active + (swap! selected disj public-key) + (swap! selected conj public-key)))}])) + +(defn invite [] + (let [user-pk (reagent/atom "") + contacts-selected (reagent/atom #{})] + (fn [route] + (let [contacts-data ( + [topbar/topbar {:title (i18n/label (if can-invite? + :t/community-invite-title + :t/community-share-title))}] + [rn/flat-list {:style {:flex 1} + :content-container-style {:padding-vertical 16} + :header [header user-pk] + :render-data {:selected contacts-selected} + :render-fn contacts-list-item + :key-fn (fn [{:keys [active public-key]}] + (str public-key active)) + :data contacts}] + [toolbar/toolbar + {:show-border? true + :center + [quo/button {:disabled (and (str/blank? @user-pk) + (zero? (count selected))) + :type :secondary + :on-press #(>evt [(if can-invite? + ::communities/invite-people-confirmation-pressed + ::communities/share-community-confirmation-pressed) @user-pk selected])} + (i18n/label (if can-invite? :t/invite :t/share))]}]])))) diff --git a/src/status_im/ui/screens/communities/members.cljs b/src/status_im/ui/screens/communities/members.cljs new file mode 100644 index 000000000000..b23d65f0f02e --- /dev/null +++ b/src/status_im/ui/screens/communities/members.cljs @@ -0,0 +1,113 @@ +(ns status-im.ui.screens.communities.members + (:require [quo.react-native :as rn] + [quo.core :as quo] + [reagent.core :as reagent] + [status-im.constants :as constants] + [status-im.ui.components.react :as react] + [status-im.utils.handlers :refer [evt]] + [status-im.ui.components.chat-icon.screen :as chat-icon] + [status-im.ui.components.unviewed-indicator :as unviewed-indicator] + [status-im.multiaccounts.core :as multiaccounts] + [status-im.ui.components.topbar :as topbar] + [status-im.i18n.i18n :as i18n] + [status-im.communities.core :as communities])) + +(defn hide-sheet-and-dispatch [event] + (>evt [:bottom-sheet/hide]) + (>evt event)) + +(defn member-sheet [{:keys [public-key] :as member} community-id can-kick-users?] + [:<> + [quo/list-item + {:theme :accent + :icon [chat-icon/contact-icon-contacts-tab + (multiaccounts/displayed-photo member)] + :title (multiaccounts/displayed-name member) + :subtitle (i18n/label :t/view-profile) + :accessibility-label :view-chat-details-button + :chevron true + :on-press #(hide-sheet-and-dispatch [:chat.ui/show-profile public-key])}] + (when can-kick-users? + [:<> + [quo/separator {:style {:margin-vertical 8}}] + [quo/list-item {:theme :negative + :icon :main-icons/arrow-left + :title (i18n/label :t/member-kick) + :on-press #(>evt [::communities/member-kick community-id public-key])}] + ; ban not implemented + #_[quo/list-item {:theme :negative + :icon :main-icons/cancel + :title (i18n/label :t/member-ban) + :on-press #(>evt [::communities/member-ban community-id public-key])}]])]) + +(defn render-member [public-key _ _ {:keys [community-id + my-public-key + can-kick-users?]}] + (let [{:keys [nickname] :as member} (or (evt [:bottom-sheet/show-sheet + {:content (fn [] [member-sheet member community-id can-kick-users?])}]) + :type :icon + :theme :icon + :accessibility-label :menu-option} + :main-icons/more])}])) + +(defn header [community-id] + [:<> + [quo/list-item {:icon :main-icons/share + :title (i18n/label :t/invite-people) + :accessibility-label :community-invite-people + :theme :accent + :on-press #(>evt [::communities/invite-people-pressed community-id])}] + [quo/separator {:style {:margin-vertical 8}}]]) + +(defn requests-to-join [community-id] + (let [requests ( + [quo/list-item {:chevron true + :accessory + [react/view {:flex-direction :row} + (when (pos? requests-count) + [unviewed-indicator/unviewed-indicator requests-count])] + :on-press #(>evt [:navigate-to :community-requests-to-join {:community-id community-id}]) + :title (i18n/label :t/membership-requests)}] + [quo/separator {:style {:margin-vertical 8}}]])) + +(defn members [route] + (let [{:keys [community-id]} (get-in route [:route :params]) + my-public-key ( + [topbar/topbar {:title (i18n/label :t/community-members-title) + + :subtitle (str (count members))}] + [header community-id] + (when (and can-manage-users? (= constants/community-on-request-access (:access permissions))) + [requests-to-join community-id]) + [rn/flat-list {:data (keys members) + :render-data {:community-id community-id + :my-public-key my-public-key + :can-kick-users? (and can-manage-users? + (not= (:access permissions) + constants/community-no-membership-access)) + :can-manage-users? can-manage-users?} + :key-fn identity + :render-fn render-member}]])) + +(defn members-container [route] + (reagent/create-class + {:display-name "community-members-view" + :component-did-mount (fn [] + (communities/fetch-requests-to-join! (get-in route [:route :params :community-id]))) + :reagent-render members})) diff --git a/src/status_im/ui/screens/communities/membership.cljs b/src/status_im/ui/screens/communities/membership.cljs new file mode 100644 index 000000000000..3482b43387b7 --- /dev/null +++ b/src/status_im/ui/screens/communities/membership.cljs @@ -0,0 +1,50 @@ +(ns status-im.ui.screens.communities.membership + (:require [quo.react-native :as rn] + [status-im.ui.components.topbar :as topbar] + [status-im.ui.components.toolbar :as toolbar] + [quo.core :as quo] + [status-im.i18n.i18n :as i18n] + [status-im.utils.handlers :refer [>evt + [quo/list-item {:title (i18n/label title) + :size :small + :accessory :radio + :active selected + :on-press on-select}] + [quo/list-footer + (i18n/label description)] + [quo/separator {:style {:margin-vertical 8}}]]) + +(defn membership [] + (let [{:keys [membership]} ( + [topbar/topbar {:title (i18n/label :t/membership-title)}] + [rn/scroll-view {} + (doall + (for [[id o] options] + ^{:key (str "option-" id)} + [option o {:selected (= id membership) + :on-select #(>evt [::communities/create-field :membership id])}]))] + [toolbar/toolbar + {:show-border? true + :center [quo/button {:type :secondary + :on-press #(>evt [:navigate-back])} + (i18n/label :t/done)]}]])) diff --git a/src/status_im/ui/screens/communities/profile.cljs b/src/status_im/ui/screens/communities/profile.cljs new file mode 100644 index 000000000000..53538d399948 --- /dev/null +++ b/src/status_im/ui/screens/communities/profile.cljs @@ -0,0 +1,96 @@ +(ns status-im.ui.screens.communities.profile + (:require [quo.core :as quo] + [status-im.utils.handlers :refer [>evt + [quo/animated-header {:left-accessories [{:icon :main-icons/arrow-left + :accessibility-label :back-button + :on-press #(>evt [:navigate-back])}] + :right-accessories [{:icon :main-icons/share + :accessibility-label :invite-button + :on-press #(>evt [::communities/share-community-pressed community-id])}] + :extended-header (profile-header/extended-header + {:title name + :color (or color (rand-nth colors/chat-colors)) + :photo (if (= community-id constants/status-community-id) + (:uri + (rn/resolve-asset-source + (resources/get-image :status-logo))) + (get-in community [:images :large :uri])) + :subtitle (when show-members-count? (i18n/label-pluralize members-count :t/community-members {:count members-count}))}) + :use-insets true} + [:<> + [quo/list-footer {:color :main} + (get-in description [:identity :description])] + [quo/separator {:style {:margin-vertical 8}}] + (when show-members-count? + [quo/list-item {:chevron true + :accessory + [react/view {:flex-direction :row} + (when (pos? members-count) + [quo/text {:color :secondary} (str members-count)]) + [unviewed-indicator/unviewed-indicator (count requests-to-join)]] + :on-press #(>evt [:navigate-to :community-members {:community-id community-id}]) + :title (i18n/label :t/members-label) + :icon :main-icons/group-chat}]) + (when (and admin roles) + [quo/list-item {:chevron true + :title (i18n/label :t/commonuity-role) + :icon :main-icons/objects}]) + (when notifications + [quo/list-item {:chevron true + :title (i18n/label :t/chat-notification-preferences) + :icon :main-icons/notification}]) + (when (or show-members-count? notifications (and admin roles)) + [quo/separator {:style {:margin-vertical 8}}]) + ;; Disable as not implemented yet + (when false + [quo/list-item {:theme :accent + :icon :main-icons/edit + :title (i18n/label :t/edit-community) + :on-press #(>evt [::communities/open-edit-community community-id])}]) + [quo/list-item {:theme :accent + :icon :main-icons/arrow-left + :title (i18n/label :t/leave-community) + :on-press #(>evt [::communities/leave community-id])}] + ;; Disable as not implemented yet + (when false + [quo/list-item {:theme :negative + :icon :main-icons/delete + :title (i18n/label :t/delete) + :on-press #(>evt [::communities/delete-community community-id])}])]]])) + +(defn management-container [route] + (reagent/create-class + {:display-name "community-profile-view" + :component-did-mount (fn [] + (communities/fetch-requests-to-join! (get-in route [:route :params :community-id]))) + :reagent-render management})) + + + diff --git a/src/status_im/ui/screens/communities/requests_to_join.cljs b/src/status_im/ui/screens/communities/requests_to_join.cljs new file mode 100644 index 000000000000..d7610c1d194c --- /dev/null +++ b/src/status_im/ui/screens/communities/requests_to_join.cljs @@ -0,0 +1,64 @@ +(ns status-im.ui.screens.communities.requests-to-join + (:require [quo.react-native :as rn] + [quo.core :as quo] + [reagent.core :as reagent] + [re-frame.core :as re-frame] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.react :as react] + [status-im.utils.handlers :refer [evt]] + [status-im.ui.components.chat-icon.screen :as chat-icon] + [status-im.multiaccounts.core :as multiaccounts] + [status-im.ui.components.icons.icons :as icons] + [status-im.ui.components.topbar :as topbar] + [status-im.i18n.i18n :as i18n] + [status-im.communities.core :as communities])) + +(defn hide-sheet-and-dispatch [event] + (>evt [:bottom-sheet/hide]) + (>evt event)) + +(defn request-actions [community-id request-id] + [react/view {:flex-direction :row} + [react/touchable-highlight {:on-press #(re-frame/dispatch [:communities.ui/accept-request-to-join-pressed community-id request-id])} + [icons/icon :main-icons/checkmark-circle {:width 35 + :height 35 + :color colors/green}]] + [react/touchable-highlight {:on-press #(re-frame/dispatch [:communities.ui/decline-request-to-join-pressed community-id request-id])} + [icons/icon :main-icons/cancel {:width 35 + :height 35 + :container-style {:padding-left 10} + :color colors/red}]]]) + +(defn render-request [{:keys [id public-key]} _ _ {:keys [community-id + can-manage-users?]}] + (let [member (or ( + [topbar/topbar {:title (i18n/label :t/community-requests-to-join-title) + :subtitle (str (count requests))}] + [rn/flat-list {:data requests + :render-data {:community-id community-id + :can-manage-users? can-manage-users?} + :key-fn :id + :render-fn render-request}]]))) + +(defn requests-to-join-container [route] + (reagent/create-class + {:display-name "community-requests-to-join-view" + :component-did-mount (fn [] + (communities/fetch-requests-to-join! (get-in route [:route :params :community-id]))) + :reagent-render requests-to-join})) diff --git a/src/status_im/ui/screens/communities/views.cljs b/src/status_im/ui/screens/communities/views.cljs index aa654781f4de..b7818ec42fd8 100644 --- a/src/status_im/ui/screens/communities/views.cljs +++ b/src/status_im/ui/screens/communities/views.cljs @@ -1,423 +1,189 @@ -(ns status-im.ui.screens.communities.views (:require-macros [status-im.utils.views :as views]) - (:require - [reagent.core :as reagent] - [re-frame.core :as re-frame] - [quo.core :as quo] - [status-im.i18n.i18n :as i18n] - [status-im.utils.core :as utils] - [status-im.utils.config :as config] - [status-im.constants :as constants] - [status-im.communities.core :as communities] - [status-im.ui.screens.home.views.inner-item :as inner-item] - [status-im.ui.screens.home.styles :as home.styles] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.copyable-text :as copyable-text] - [status-im.react-native.resources :as resources] - [status-im.ui.components.topbar :as topbar] - [status-im.ui.components.icons.icons :as icons] - [status-im.ui.components.colors :as colors] - [status-im.ui.components.chat-icon.screen :as chat-icon.screen] - [status-im.ui.components.toolbar :as toolbar] - [status-im.ui.components.react :as react])) +(ns status-im.ui.screens.communities.views + (:require + [quo.core :as quo] + [status-im.i18n.i18n :as i18n] + [status-im.utils.core :as utils] + [status-im.utils.config :as config] + [status-im.constants :as constants] + [status-im.communities.core :as communities] + [status-im.utils.handlers :refer [>evt evt [:bottom-sheet/hide]) + (>evt event)) + +(defn community-unviewed-count [id] + (when-not (zero? (evt [:dismiss-keyboard]) + (>evt [:navigate-to :community {:community-id id}])) + ;; TODO: actions + ;; :on-long-press #(>evt [:bottom-sheet/show-sheet + ;; nil]) + }] + (when last? + [quo/separator])]) + +(defn community-list-item [{:keys [id permissions members name description] :as community}] + (let [members-count (count members) + show-members-count? (not= (:access permissions) constants/community-no-membership-access)] + [quo/list-item + {:icon [community-icon community] :title [react/view {:flex-direction :row - :flex 1} - [react/view {:flex-direction :row - :flex 1 - :padding-right 16 - :align-items :center} - [quo/text {:weight :medium - :accessibility-label :community-name-text - :ellipsize-mode :tail - :number-of-lines 1} - (utils/truncate-str (:display-name identity) 30)]]] + :flex 1 + :padding-right 16 + :align-items :center} + [quo/text {:weight :medium + :accessibility-label :community-name-text + :ellipsize-mode :tail + :number-of-lines 1} + (utils/truncate-str name 30)]] :title-accessibility-label :community-name-text - :subtitle [react/view {:flex-direction :row} - [react/view {:flex 1} - [quo/text - (utils/truncate-str (:description identity) 30)]]] + :subtitle [react/view + [quo/text {:number-of-lines 1} + description] + [quo/text {:number-of-lines 1 + :color :secondary} + (if show-members-count? + (i18n/label-pluralize members-count :t/community-members {:count members-count}) + (i18n/label :t/open-membership))]] :on-press #(do - (re-frame/dispatch [:dismiss-keyboard]) - (re-frame/dispatch [:navigate-to :community id]))}])) + (>evt [:dismiss-keyboard]) + (>evt [:navigate-to :community {:community-id id}]))}])) (defn communities-actions [] - [react/view + [:<> [quo/list-item {:theme :accent :title (i18n/label :t/import-community) :accessibility-label :community-import-community - :icon :main-icons/check - :on-press #(hide-sheet-and-dispatch [::communities/import-pressed])}] + :icon :main-icons/objects + :on-press #(hide-sheet-and-dispatch [:navigate-to :community-import])}] [quo/list-item {:theme :accent :title (i18n/label :t/create-community) :accessibility-label :community-create-community - :icon :main-icons/check - :on-press #(hide-sheet-and-dispatch [::communities/create-pressed])}]]) - -(views/defview communities [] - (views/letsubs [communities [:communities]] + :icon :main-icons/add + :on-press #(hide-sheet-and-dispatch [::communities/open-create-community])}]]) + +(defn communities-home-list [communities] + [list/flat-list + {:key-fn :id + :keyboard-should-persist-taps :always + :data communities + :render-fn community-home-list-item}]) + +(defn communities-list [communities] + [list/section-list + {:content-container-style {:padding-vertical 8} + :key-fn :id + :keyboard-should-persist-taps :always + :sticky-section-headers-enabled false + :sections communities + :render-section-header-fn quo/list-index + :render-fn community-list-item}]) + +(defn communities [] + (let [communities ( {:title (i18n/label :t/communities)} + [topbar/topbar (cond-> {:title (i18n/label :t/communities) + :modal? true} config/communities-management-enabled? (assoc :right-accessories [{:icon :main-icons/more :accessibility-label :chat-menu-button :on-press - #(re-frame/dispatch [:bottom-sheet/show-sheet - {:content (fn [] - [communities-actions]) - :height 256}])}]))] - [react/scroll-view {:style {:flex 1} - :content-container-style {:padding-vertical 8}} - [list/flat-list - {:key-fn :id - :keyboard-should-persist-taps :always - :data (vals communities) - :render-fn (fn [community] [community-list-item community])}]] + #(>evt [:bottom-sheet/show-sheet + {:content (fn [] + [communities-actions]) + :height 256}])}]))] + [communities-list communities] (when config/communities-management-enabled? [toolbar/toolbar {:show-border? true - :center [quo/button {:on-press #(re-frame/dispatch [::communities/create-pressed])} - (i18n/label :t/create)]}])])) - -(defn valid? [community-name community-description] - (and (not= "" community-name) - (not= "" community-description))) - -(defn import-community [] - (let [community-key (reagent/atom "")] - (fn [] - [react/view {:style {:padding-left 16 - :padding-right 8}} - [react/view {:style {:padding-horizontal 20}} - [quo/text-input - {:label (i18n/label :t/community-key) - :placeholder (i18n/label :t/community-key-placeholder) - :on-change-text #(reset! community-key %) - :auto-focus true}]] - [react/view {:style {:padding-top 20 - :padding-horizontal 20}} - [quo/button {:disabled (= @community-key "") - :on-press #(re-frame/dispatch [::communities/import-confirmation-pressed @community-key])} - (i18n/label :t/import)]]]))) - -(defn create [] - (let [community-name (reagent/atom "") - membership (reagent/atom 1) - community-description (reagent/atom "")] - (fn [] - [react/view {:style {:padding-left 16 - :padding-right 8}} - [react/view {:style {:padding-horizontal 20}} - [quo/text-input - {:label (i18n/label :t/name-your-community) - :placeholder (i18n/label :t/name-your-community-placeholder) - :on-change-text #(reset! community-name %) - :auto-focus true}]] - [react/view {:style {:padding-horizontal 20}} - [quo/text-input - {:label (i18n/label :t/give-a-short-description-community) - :placeholder (i18n/label :t/give-a-short-description-community) - :multiline true - :number-of-lines 4 - :on-change-text #(reset! community-description %)}]] - [react/view {:style {:padding-horizontal 20}} - [quo/text-input - {:label (i18n/label :t/membership-type) - :placeholder (i18n/label :t/membership-type-placeholder) - :on-change-text #(reset! membership %)}]] - - [react/view {:style {:padding-top 20 - :padding-horizontal 20}} - [quo/button {:disabled (not (valid? @community-name @community-description)) - :on-press #(re-frame/dispatch [::communities/create-confirmation-pressed @community-name @community-description @membership])} - (i18n/label :t/create)]]]))) - -(def create-sheet - {:content create}) - -(def import-sheet - {:content import-community}) - -(defn create-channel [] - (let [channel-name (reagent/atom "") - channel-description (reagent/atom "")] - (fn [] - [react/view {:style {:padding-left 16 - :padding-right 8}} - [react/view {:style {:padding-horizontal 20}} - [quo/text-input - {:label (i18n/label :t/name-your-channel) - :placeholder (i18n/label :t/name-your-channel-placeholder) - :on-change-text #(reset! channel-name %) - :auto-focus true}]] - [react/view {:style {:padding-horizontal 20}} - [quo/text-input - {:label (i18n/label :t/give-a-short-description-channel) - :placeholder (i18n/label :t/give-a-short-description-channel) - :multiline true - :number-of-lines 4 - :on-change-text #(reset! channel-description %)}]] - - (when config/communities-management-enabled? - [react/view {:style {:padding-top 20 - :padding-horizontal 20}} - [quo/button {:disabled (not (valid? @channel-name @channel-description)) - :on-press #(re-frame/dispatch [::communities/create-channel-confirmation-pressed @channel-name @channel-description])} - (i18n/label :t/create)]])]))) - -(def create-channel-sheet - {:content create-channel}) - -(defn invite-people [] - (let [user-pk (reagent/atom "")] - (fn [] - [react/view {:style {:padding-left 16 - :padding-right 8}} - [react/view {:style {:padding-horizontal 20}} - [quo/text-input - {:label (i18n/label :t/enter-user-pk) - :placeholder (i18n/label :t/enter-user-pk) - :on-change-text #(reset! user-pk %) - :auto-focus true}]] - [react/view {:style {:padding-top 20 - :padding-horizontal 20}} - [quo/button {:disabled (= "" user-pk) - :on-press #(re-frame/dispatch [::communities/invite-people-confirmation-pressed @user-pk])} - (i18n/label :t/invite)]]]))) - -(def invite-people-sheet - {:content invite-people}) - -(defn community-actions [id admin] - [react/view - (when (and config/communities-management-enabled? admin) - [quo/list-item - {:theme :accent - :title (i18n/label :t/export-key) - :accessibility-label :community-export-key - :icon :main-icons/check - :on-press #(hide-sheet-and-dispatch [::communities/export-pressed id])}]) - (when (and config/communities-management-enabled? admin) - [quo/list-item - {:theme :accent - :title (i18n/label :t/create-channel) - :accessibility-label :community-create-channel - :icon :main-icons/check - :on-press #(hide-sheet-and-dispatch [::communities/create-channel-pressed id])}]) - (when (and config/communities-management-enabled? admin) - [quo/list-item - {:theme :accent - :title (i18n/label :t/invite-people) - :accessibility-label :community-invite-people - :icon :main-icons/close - :on-press #(re-frame/dispatch [::communities/invite-people-pressed id])}]) - [quo/list-item - {:theme :accent - :title (i18n/label :t/leave) - :accessibility-label :leave - :icon :main-icons/close - :on-press #(do - (re-frame/dispatch [:navigate-to :home]) - (re-frame/dispatch [:bottom-sheet/hide]) - (re-frame/dispatch [::communities/leave id]))}]]) - -(defn toolbar-content [id display-name color] - [react/view {:style {:flex 1 - :align-items :center - :flex-direction :row}} - [react/view {:margin-right 10} - (if (= id constants/status-community-id) - [react/image {:source (resources/get-image :status-logo) - :style {:width 40 - :height 40}}] - [chat-icon.screen/chat-icon-view-toolbar - id - true - display-name - (or color - (rand-nth colors/chat-colors))])] - [react/view {:style {:flex 1 :justify-content :center}} - [react/text {:style {:typography :main-medium - :font-size 15 - :line-height 22} - :number-of-lines 1 - :accessibility-label :community-name-text} - display-name]]]) - -(defn topbar [id display-name color admin joined] - [topbar/topbar - {:content [toolbar-content id display-name color] - :navigation {:on-press #(re-frame/dispatch [:navigate-back])} - :right-accessories (when (or admin joined) - [{:icon :main-icons/more - :accessibility-label :community-menu-button - :on-press - #(re-frame/dispatch [:bottom-sheet/show-sheet - {:content (fn [] - [community-actions id admin]) - :height 256}])}])}]) - -(defn welcome-blank-page [] - [react/view {:style {:flex 1 :flex-direction :row :align-items :center :justify-content :center}} - [react/i18n-text {:style home.styles/welcome-blank-text :key :welcome-blank-message}]]) - -(views/defview community-unviewed-count [id] - (views/letsubs [unviewed-count [:communities/unviewed-count id]] - (when-not (zero? unviewed-count) - [react/view {:style {:background-color colors/blue - :border-radius 6 - :margin-right 5 - :margin-top 2 - :width 12 - :height 12} - :accessibility-label :unviewed-messages-public}]))) - -(defn status-community [{:keys [id description]}] - [quo/list-item - {:icon [react/image {:source (resources/get-image :status-logo) - :style {:width 40 - :height 40}}] - :title [react/view {:flex-direction :row - :flex 1} - [react/view {:flex-direction :row - :flex 1 - :padding-right 16 - :align-items :center} - [quo/text {:weight :medium - :accessibility-label :chat-name-text - :font-size 17 - :ellipsize-mode :tail - :number-of-lines 1} - (get-in description [:identity :display-name])]] - [react/view {:flex-direction :row - :flex 1 - :justify-content :flex-end - :align-items :center} - [community-unviewed-count id]]] - - :title-accessibility-label :chat-name-text - :on-press #(do - (re-frame/dispatch [:dismiss-keyboard]) - (re-frame/dispatch [:navigate-to :community id])) - ;; TODO: actions - :on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet - nil])}]) - -(defn channel-preview-item [{:keys [id identity]}] - [quo/list-item - {:icon [chat-icon.screen/chat-icon-view-chat-list - id true (:display-name identity) colors/blue false false] - :title [react/view {:flex-direction :row - :flex 1} - [react/view {:flex-direction :row - :flex 1 - :padding-right 16 - :align-items :center} - [icons/icon :main-icons/tiny-group - {:color colors/black - :width 15 - :height 15 - :container-style {:width 15 - :height 15 - :margin-right 2}}] - [quo/text {:weight :medium - :accessibility-label :chat-name-text - :ellipsize-mode :tail - :number-of-lines 1} - (utils/truncate-str (:display-name identity) 30)]]] - :title-accessibility-label :chat-name-text - :subtitle [react/view {:flex-direction :row} - [react/text-class {:style home.styles/last-message-text - :number-of-lines 1 - :ellipsize-mode :tail - :accessibility-label :chat-message-text} (:description identity)]]}]) - -(defn community-channel-preview-list [_ description] - (let [chats (reduce-kv - (fn [acc k v] - (conj acc (assoc v :id (name k)))) - [] - (get-in description [:chats]))] - [list/flat-list - {:key-fn :id - :keyboard-should-persist-taps :always - :data chats - :render-fn channel-preview-item}])) - -(defn community-chat-list [chats] - (if (empty? chats) - [welcome-blank-page] - [list/flat-list - {:key-fn :chat-id - :keyboard-should-persist-taps :always - :data chats - :render-fn (fn [home-item] [inner-item/home-list-item (assoc home-item :color colors/blue)]) - :footer [react/view {:height 68}]}])) - -(views/defview community-channel-list [id] - (views/letsubs [chats [:chats/by-community-id id]] - [community-chat-list chats])) - -(views/defview community [route] - (views/letsubs [{:keys [id description joined admin]} [:communities/community (get-in route [:route :params])]] - [react/view {:style {:flex 1}} - [topbar - id - (get-in description [:identity :display-name]) - (get-in description [:identity :color]) - admin - joined] - (if joined - [community-channel-list id] - [community-channel-preview-list id description]) - (when-not joined - [react/view {:style {:padding-top 20 - :margin-bottom 10 - :padding-horizontal 20}} - [quo/button {:on-press #(re-frame/dispatch [::communities/join id])} - (i18n/label :t/join)]])])) - -(views/defview export-community [] - (views/letsubs [{:keys [community-key]} [:popover/popover]] - [react/view {} - [react/view {:style {:padding-top 16 :padding-horizontal 16}} - [copyable-text/copyable-text-view - {:label :t/community-key - :container-style {:margin-top 12 :margin-bottom 4} - :copied-text community-key} - [quo/text {:number-of-lines 1 - :ellipsize-mode :middle - :accessibility-label :chat-key - :monospace true} - community-key]]]])) + :center [quo/button {:on-press #(>evt [::communities/open-create-community]) + :type :secondary} + (i18n/label :t/create)]}])])) + +(defn export-community [] + (let [{:keys [community-key]} (evt [:navigate-to :community {:community-id id}]) :accessibility-label :chat-item} - [react/view {:padding-right 8 :padding-vertical 8} - [react/view {:border-color colors/gray-lighter :border-radius 36 :border-width 1 :padding-horizontal 8 :padding-vertical 5} - [react/text {:style {:color colors/blue :typography :main-medium}} name]]]]) + [react/view {:padding-right 8 + :padding-vertical 8} + [react/view {:border-color colors/gray-lighter + :border-radius 36 + :border-width 1 + :padding-horizontal 8 + :padding-vertical 5} + [quo/text {:color :link} name]]]]) diff --git a/src/status_im/ui/screens/home/sheet/views.cljs b/src/status_im/ui/screens/home/sheet/views.cljs index f82ef5fe4a60..a57a29a7b360 100644 --- a/src/status_im/ui/screens/home/sheet/views.cljs +++ b/src/status_im/ui/screens/home/sheet/views.cljs @@ -53,7 +53,7 @@ :title (i18n/label :t/communities-alpha) :accessibility-label :communities-button :icon :main-icons/communities - :on-press #(hide-sheet-and-dispatch [:navigate-to :communities])}]) + :on-press #(hide-sheet-and-dispatch [:navigate-to :communities {:screen :communities}])}]) [invite/list-item {:accessibility-label :chats-menu-invite-friends-button}]]) diff --git a/src/status_im/ui/screens/home/views.cljs b/src/status_im/ui/screens/home/views.cljs index 51eae3abf06d..ee866933446a 100644 --- a/src/status_im/ui/screens/home/views.cljs +++ b/src/status_im/ui/screens/home/views.cljs @@ -145,15 +145,17 @@ (re-frame/dispatch [:set :public-group-topic nil]) (re-frame/dispatch [:search/home-filter-changed nil]))}])]))) -(defn render-fn [home-item] - [inner-item/home-list-item home-item]) +(defn render-fn [{:keys [chat-id] :as home-item}] + ;; We use `chat-id` to distinguish communities from chats + (if chat-id + [inner-item/home-list-item home-item] + [communities.views/community-home-list-item home-item])) -(defn communities-and-chats [chats status-community loading? search-filter hide-home-tooltip?] +(defn communities-and-chats [items loading? search-filter hide-home-tooltip?] (if loading? [react/view {:flex 1 :align-items :center :justify-content :center} [react/activity-indicator {:animating true}]] - (if (and (empty? chats) - (not status-community) + (if (and (empty? items) (empty? search-filter) hide-home-tooltip? (not @search-active?)) @@ -161,33 +163,26 @@ [list/flat-list {:key-fn :chat-id :keyboard-should-persist-taps :always - :data chats + :data items :render-fn render-fn - :header [:<> - (when (or (seq chats) @search-active? (seq search-filter)) - [search-input-wrapper search-filter chats]) - [referral-item/list-item] - (when (and (empty? chats) - (not status-community) - (or @search-active? (seq search-filter))) - [start-suggestion search-filter]) - (when status-community - ;; We only support one community now, Status - [communities.views/status-community status-community]) - (when (and status-community - (seq chats)) - [quo/separator])] + :header [:<> + (when (or (seq items) @search-active? (seq search-filter)) + [search-input-wrapper search-filter items]) + [referral-item/list-item] + (when + (and (empty? items) + (or @search-active? (seq search-filter))) + [start-suggestion search-filter])] :footer (if (and (not hide-home-tooltip?) (not @search-active?)) [home-tooltip-view] [react/view {:height 68}])}]))) (views/defview chats-list [] - (views/letsubs [status-community [:communities/status-community] - loading? [:chats/loading?] - {:keys [chats search-filter]} [:home-items] + (views/letsubs [loading? [:chats/loading?] + {:keys [items + search-filter]} [:home-items] {:keys [hide-home-tooltip?]} [:multiaccount]] - [react/scroll-view - [communities-and-chats chats status-community loading? search-filter hide-home-tooltip?]])) + [communities-and-chats items loading? search-filter hide-home-tooltip?])) (views/defview plus-button [] (views/letsubs [logging-in? [:multiaccounts/login]] diff --git a/src/status_im/ui/screens/intro/views.cljs b/src/status_im/ui/screens/intro/views.cljs index 7962f64ea1c3..a1b64f158f36 100644 --- a/src/status_im/ui/screens/intro/views.cljs +++ b/src/status_im/ui/screens/intro/views.cljs @@ -12,11 +12,9 @@ [status-im.ui.components.react :as react] [status-im.ui.components.topbar :as topbar] [status-im.ui.screens.intro.styles :as styles] - [status-im.utils.config :as config] [status-im.ui.components.toolbar :as toolbar] [status-im.utils.gfycat.core :as gfy] [status-im.utils.identicon :as identicon] - [status-im.utils.platform :as platform] [status-im.utils.security :as security] [status-im.utils.debounce :refer [dispatch-and-chill]] [quo.core :as quo] @@ -103,10 +101,7 @@ {:accessibility-label (keyword (str "select-storage-" type)) :on-press #(re-frame/dispatch [:intro-wizard/on-key-storage-selected - (if (or platform/android? - config/keycard-test-menu-enabled?) - type - :default)])} + type])} [react/view (assoc (styles/list-item selected?) :align-items :flex-start :padding-top 16 diff --git a/src/status_im/ui/screens/keycard/recovery/views.cljs b/src/status_im/ui/screens/keycard/recovery/views.cljs index 0ad463f57152..78b2f9502d14 100644 --- a/src/status_im/ui/screens/keycard/recovery/views.cljs +++ b/src/status_im/ui/screens/keycard/recovery/views.cljs @@ -135,7 +135,8 @@ (i18n/label :t/keycard-free-pairing-slots {:n free-pairing-slots})]])] [react/view [react/view {:padding 16 - :justify-content :center} + :justify-content :center + :margin-bottom 100} [quo/text-input {:on-change-text #(re-frame/dispatch [:keycard.onboarding.pair.ui/input-changed %]) :auto-focus true diff --git a/src/status_im/ui/screens/keycard/views.cljs b/src/status_im/ui/screens/keycard/views.cljs index cd15812165c5..8f475c347903 100644 --- a/src/status_im/ui/screens/keycard/views.cljs +++ b/src/status_im/ui/screens/keycard/views.cljs @@ -13,6 +13,7 @@ [status-im.ui.screens.chat.photos :as photos] [status-im.ui.screens.keycard.pin.views :as pin.views] [status-im.ui.screens.keycard.styles :as styles] + [status-im.ui.screens.intro.styles :as intro-styles] [status-im.constants :as constants] [status-im.keycard.login :as keycard.login] [status-im.ui.screens.keycard.frozen-card.view :as frozen-card.view]) @@ -200,10 +201,9 @@ [react/text (i18n/label :t/keycard-can-use-with-new-passcode)]] (when-not hide-login-actions? [react/view - {:style {:width 160 + {:style {:width 260 :margin-bottom 15}} - [react/view {:flex-direction :row - :height 52} + [react/view intro-styles/buttons-container [quo/button {:on-press #(re-frame/dispatch [::keycard.login/login-after-reset])} (i18n/label :t/open)]]])]) diff --git a/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs b/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs index c93fb543b3aa..7287a00e9f18 100644 --- a/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs +++ b/src/status_im/ui/screens/multiaccounts/key_storage/views.cljs @@ -103,7 +103,7 @@ (defview seed-phrase [] (letsubs [{:keys [seed-word-count seed-shape-invalid?]} [:multiaccounts/key-storage]] - [react/keyboard-avoiding-view {:flex 1} + [react/keyboard-avoiding-view {:style {:display :flex :flex 1 :flex-direction :column}} [local-topbar (i18n/label :t/enter-seed-phrase)] [multiaccounts.views/seed-phrase-input {:on-change-event [::multiaccounts.key-storage/seed-phrase-input-changed] @@ -114,14 +114,15 @@ :margin-bottom 8 :text-align :center}} (i18n/label :t/multiaccounts-recover-enter-phrase-text)] - [toolbar/toolbar {:show-border? true - :right [quo/button - {:type :secondary - :disabled (or seed-shape-invalid? - (nil? seed-shape-invalid?)) - :on-press #(re-frame/dispatch [::multiaccounts.key-storage/choose-storage-pressed]) - :after :main-icons/next} - (i18n/label :t/choose-storage)]}]])) + [react/keyboard-avoiding-view {:style {:flex 1}} + [toolbar/toolbar {:show-border? true + :right [quo/button + {:type :secondary + :disabled (or seed-shape-invalid? + (nil? seed-shape-invalid?)) + :on-press #(re-frame/dispatch [::multiaccounts.key-storage/choose-storage-pressed]) + :after :main-icons/next} + (i18n/label :t/choose-storage)]}]]])) (defn keycard-subtitle [] [react/view diff --git a/src/status_im/ui/screens/multiaccounts/recover/views.cljs b/src/status_im/ui/screens/multiaccounts/recover/views.cljs index c5698b981fdb..435bf8e2e6df 100644 --- a/src/status_im/ui/screens/multiaccounts/recover/views.cljs +++ b/src/status_im/ui/screens/multiaccounts/recover/views.cljs @@ -10,7 +10,6 @@ [status-im.utils.security] [status-im.ui.components.colors :as colors] [quo.core :as quo] - [status-im.utils.platform :as platform] [status-im.react-native.resources :as resources] [status-im.ui.components.icons.icons :as icons])) @@ -52,7 +51,6 @@ ;; Show manage storage link when on login screen, only on android devices ;; and the selected account is not paired with keycard (when (and (= view-id :login) - platform/android? (not acc-to-login-keycard-pairing)) [quo/list-item {:theme :accent @@ -67,23 +65,21 @@ :accessibility-label :enter-seed-phrase-button :icon :main-icons/text :on-press #(hide-sheet-and-dispatch [::multiaccounts.recover/enter-phrase-pressed])}] - (when (or platform/android? - config/keycard-test-menu-enabled?) - [quo/list-item - {:theme :accent - :title (i18n/label :t/recover-with-keycard) - :accessibility-label :recover-with-keycard-button - :icon [react/view {:border-width 1 - :border-radius 20 - :border-color colors/blue-light - :background-color colors/blue-light - :justify-content :center - :align-items :center - :width 40 - :height 40} - [react/image {:source (resources/get-image :keycard-logo-blue) - :style {:width 24 :height 24}}]] - :on-press #(hide-sheet-and-dispatch [::keycard/recover-with-keycard-pressed])}]) + [quo/list-item + {:theme :accent + :title (i18n/label :t/recover-with-keycard) + :accessibility-label :recover-with-keycard-button + :icon [react/view {:border-width 1 + :border-radius 20 + :border-color colors/blue-light + :background-color colors/blue-light + :justify-content :center + :align-items :center + :width 40 + :height 40} + [react/image {:source (resources/get-image :keycard-logo-blue) + :style {:width 24 :height 24}}]] + :on-press #(hide-sheet-and-dispatch [::keycard/recover-with-keycard-pressed])}] (when config/database-management-enabled? [quo/list-item {:theme :accent :on-press #(hide-sheet-and-dispatch [:multiaccounts.login.ui/import-db-submitted]) diff --git a/src/status_im/ui/screens/network/network_details/views.cljs b/src/status_im/ui/screens/network/network_details/views.cljs index f01ed9114d3a..2634a32c25f1 100644 --- a/src/status_im/ui/screens/network/network_details/views.cljs +++ b/src/status_im/ui/screens/network/network_details/views.cljs @@ -6,7 +6,8 @@ [status-im.ui.screens.network.styles :as st] [status-im.ui.screens.network.views :as network-settings] [status-im.ui.components.react :as react] - [status-im.ui.components.topbar :as topbar]) + [status-im.ui.components.topbar :as topbar] + [status-im.utils.debounce :refer [dispatch-and-chill]]) (:require-macros [status-im.utils.views :as views])) (views/defview network-details [] @@ -24,7 +25,7 @@ {:name name :connected? connected?}] (when-not connected? - [react/touchable-highlight {:on-press #(re-frame/dispatch [::network/connect-network-pressed id])} + [react/touchable-highlight {:on-press #(dispatch-and-chill [::network/connect-network-pressed id] 1000)} [react/view st/connect-button-container [react/view {:style st/connect-button :accessibility-label :network-connect-button} diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs index cf717aa551c8..bd956d0ba1ab 100644 --- a/src/status_im/ui/screens/profile/user/views.cljs +++ b/src/status_im/ui/screens/profile/user/views.cljs @@ -11,7 +11,6 @@ [status-im.ui.components.qr-code-viewer.views :as qr-code-viewer] [status-im.ui.components.react :as react] [status-im.ui.screens.profile.user.styles :as styles] - [status-im.utils.platform :as platform] [status-im.utils.config :as config] [status-im.utils.gfycat.core :as gfy] [status-im.utils.universal-links.utils :as universal-links] @@ -128,9 +127,7 @@ :accessibility-label :sync-settings-button :chevron true :on-press #(re-frame/dispatch [:navigate-to :sync-settings])}] - (when (and (or platform/android? - config/keycard-test-menu-enabled?) - keycard-pairing) + (when keycard-pairing [quo/list-item {:icon :main-icons/keycard :title (i18n/label :t/keycard) diff --git a/src/status_im/ui/screens/routing/chat_stack.cljs b/src/status_im/ui/screens/routing/chat_stack.cljs index 79283ae34bd8..32657aaaf002 100644 --- a/src/status_im/ui/screens/routing/chat_stack.cljs +++ b/src/status_im/ui/screens/routing/chat_stack.cljs @@ -5,12 +5,24 @@ [status-im.ui.screens.group.views :as group] [status-im.ui.screens.referrals.public-chat :as referrals.public-chat] [status-im.ui.screens.communities.views :as communities] + [status-im.ui.screens.communities.community :as community] + [status-im.ui.screens.communities.create :as communities.create] + [status-im.ui.screens.communities.import :as communities.import] + [status-im.ui.screens.communities.profile :as community.profile] + [status-im.ui.screens.communities.edit :as community.edit] + [status-im.ui.screens.communities.create-channel :as create-channel] + [status-im.ui.screens.communities.membership :as membership] + [status-im.ui.screens.communities.members :as members] + [status-im.ui.screens.communities.requests-to-join :as requests-to-join] + [status-im.ui.screens.communities.invite :as invite] [status-im.ui.screens.profile.group-chat.views :as profile.group-chat] [status-im.ui.components.tabbar.styles :as tabbar.styles] - [status-im.ui.screens.stickers.views :as stickers])) + [status-im.ui.screens.stickers.views :as stickers] + [status-im.utils.config :as config])) (defonce stack (navigation/create-stack)) (defonce group-stack (navigation/create-stack)) +(defonce communities-stack (navigation/create-stack)) (defn chat-stack [] [stack {:initial-route-name :home @@ -20,14 +32,6 @@ :component home/home} {:name :referral-enclav :component referrals.public-chat/view} - {:name :communities - :transition :presentation-ios - :insets {:bottom true} - :component communities/communities} - {:name :community - :transition :presentation-ios - :insets {:bottom true} - :component communities/community} {:name :chat :component chat/chat} {:name :group-chat-profile @@ -38,7 +42,50 @@ {:name :stickers :component stickers/packs} {:name :stickers-pack - :component stickers/pack}]]) + :component stickers/pack} + ;; Community + {:name :community + :component community/community} + {:name :community-management + :insets {:top false} + :component community.profile/management-container} + {:name :community-members + :component members/members-container} + {:name :community-requests-to-join + :component requests-to-join/requests-to-join-container} + {:name :create-community-channel + :component create-channel/create-channel} + {:name :invite-people-community + :component invite/invite}]]) + +(defn communities [] + [communities-stack {:header-mode :none} + (concat + [{:name :communities + :insets {:bottom true + :top false} + :component communities/communities} + {:name :community-import + :insets {:bottom true + :top false} + :component communities.import/view} + {:name :invite-people-community + :insets {:bottom true + :top false} + :component invite/invite}] + (when config/communities-management-enabled? + [{:name :community-edit + :insets {:bottom true + :top false} + :component community.edit/edit} + {:name :community-create + :insets {:bottom true + :top false} + :component communities.create/view} + {:name :community-membership + :insets {:bottom true + :top false} + :component membership/membership}]))]) (defn new-group-chat [] [group-stack {:header-mode :none diff --git a/src/status_im/ui/screens/routing/main.cljs b/src/status_im/ui/screens/routing/main.cljs index 6af5d24e4593..9add1267ac58 100644 --- a/src/status_im/ui/screens/routing/main.cljs +++ b/src/status_im/ui/screens/routing/main.cljs @@ -100,6 +100,9 @@ {:name :create-group-chat :transition :presentation-ios :component chat-stack/new-group-chat} + {:name :communities + :transition :presentation-ios + :component chat-stack/communities} {:name :referral-invite :transition :presentation-ios :insets {:bottom true} diff --git a/src/status_im/ui/screens/signing/views.cljs b/src/status_im/ui/screens/signing/views.cljs index 549c5a0c3fdb..11d67a3a31a3 100644 --- a/src/status_im/ui/screens/signing/views.cljs +++ b/src/status_im/ui/screens/signing/views.cljs @@ -16,6 +16,7 @@ [status-im.ui.screens.signing.sheets :as sheets] [status-im.ethereum.tokens :as tokens] [status-im.utils.types :as types] + [status-im.utils.platform :as platform] [clojure.string :as string] [quo.core :as quo] [quo.gesture-handler :as gh] @@ -107,8 +108,9 @@ enter-step [:keycard/pin-enter-step] status [:keycard/pin-status] retry-counter [:keycard/retry-counter]] - (let [enter-step (or enter-step :sign)] - [react/view + (let [enter-step (or enter-step :sign) + margin-bottom (if platform/ios? 40 0)] + [react/view {:margin-bottom margin-bottom} [pin.views/pin-view {:pin pin :retry-counter retry-counter diff --git a/src/status_im/ui/screens/wallet/add_new/views.cljs b/src/status_im/ui/screens/wallet/add_new/views.cljs index c33ad836923b..b379ce1a27bc 100644 --- a/src/status_im/ui/screens/wallet/add_new/views.cljs +++ b/src/status_im/ui/screens/wallet/add_new/views.cljs @@ -124,7 +124,7 @@ [topbar/topbar {:navigation :none :right-accessories - [{:label :t/cancel + [{:label (i18n/label :t/cancel) :on-press #(re-frame/dispatch [:keycard/new-account-pin-sheet-hide])}]}] [pin.views/pin-view {:pin pin diff --git a/src/status_im/utils/handlers.cljs b/src/status_im/utils/handlers.cljs index 3be3eb52b8c2..94ff7765489b 100644 --- a/src/status_im/utils/handlers.cljs +++ b/src/status_im/utils/handlers.cljs @@ -52,3 +52,7 @@ name [debug-handlers-names (re-frame/inject-cofx :now) interceptors] handler))) + +(def evt re-frame/dispatch) diff --git a/status-go-version.json b/status-go-version.json index 785fd94f79dc..09acf949199e 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -3,6 +3,6 @@ "owner": "status-im", "repo": "status-go", "version": "feature/private-profile-photos", - "commit-sha1": "ff8d248f4f42e0e9eb1b4a2aa6450ffe817c8672", - "src-sha256": "1g2nrm99dmhbgj0aqik0y1s1m362hkaj8rb3rwi0n0ncdz29lbrq" + "commit-sha1": "28a87ec7bb3e317968312a9b9b8935e1a7abe62c", + "src-sha256": "0gdycf0s3wpl4jpgwgb42sm3zfdgbxmhyblz0gvb591mgw5k29yh" } diff --git a/test/appium/support/api/network_api.py b/test/appium/support/api/network_api.py index e21d40b663c1..0cdad60d90ce 100644 --- a/test/appium/support/api/network_api.py +++ b/test/appium/support/api/network_api.py @@ -84,8 +84,8 @@ def find_transaction_by_unique_amount(self, address, amount, token=False, decima 'Transaction with amount %s is not found in list of %s, address is %s during %ss' % (amount, additional_info, address, wait_time)) else: - counter += 10 - time.sleep(10) + counter += 30 + time.sleep(30) try: if token: transactions = self.get_token_transactions(address) @@ -100,7 +100,8 @@ def find_transaction_by_unique_amount(self, address, amount, token=False, decima return transaction except TypeError as e: self.log("Failed iterate transactions: " + str(e)) - continue + pytest.fail("No valid JSON response from Etherscan: %s " % str(e)) + # continue def wait_for_confirmation_of_transaction(self, address, amount, confirmations=12, token=False): start_time = time.time() diff --git a/test/appium/tests/atomic/account_management/test_keycard.py b/test/appium/tests/atomic/account_management/test_keycard.py index 6d6fccfb98b9..67cbb6fe67ad 100644 --- a/test/appium/tests/atomic/account_management/test_keycard.py +++ b/test/appium/tests/atomic/account_management/test_keycard.py @@ -142,6 +142,7 @@ def test_keycard_interruption_creating_onboarding_flow(self): @marks.testrail_id(6246) @marks.medium + @marks.flaky def test_keycard_interruption_access_key_onboarding_flow(self): sign_in = SignInView(self.driver) sign_in.get_started_button.click() @@ -247,7 +248,6 @@ def test_keycard_can_recover_keycard_account_card_pairing(self): @marks.medium def test_keycard_can_recover_keycard_account_offline_and_add_watch_only_acc(self): sign_in = SignInView(self.driver) - recovered_user = transaction_senders['A'] sign_in.toggle_airplane_mode() sign_in.just_fyi('Recover multiaccount offline') @@ -291,7 +291,7 @@ def test_keycard_can_recover_keycard_account_offline_and_add_watch_only_acc(self wallet_view.just_fyi('Check that balance is changed after go back to WI-FI') sign_in.toggle_mobile_data() - for asset in ('LXS', 'ADI', 'STT'): + for asset in ('ADI', 'STT'): wallet_view.asset_by_name(asset).scroll_to_element() wallet_view.wait_balance_is_changed(asset, wait_time=60) diff --git a/test/appium/tests/atomic/account_management/test_profile.py b/test/appium/tests/atomic/account_management/test_profile.py index 6ccdb87874f5..9485792efda3 100644 --- a/test/appium/tests/atomic/account_management/test_profile.py +++ b/test/appium/tests/atomic/account_management/test_profile.py @@ -554,20 +554,6 @@ def test_set_primary_ens_custom_domain(self): self.errors.verify_no_errors() - @marks.testrail_id(5302) - @marks.high - @marks.skip - # TODO: skip until profile picture change feature is enabled - def test_set_profile_picture(self): - sign_in_view = SignInView(self.driver) - sign_in_view.create_user() - profile_view = sign_in_view.profile_button.click() - profile_view.edit_profile_picture(file_name='sauce_logo.png') - profile_view.home_button.click() - sign_in_view.profile_button.click() - profile_view.swipe_down() - if not profile_view.profile_picture.is_element_image_equals_template('sauce_logo_profile.png'): - self.driver.fail('Profile picture was not updated') @marks.testrail_id(5475) @marks.low @@ -622,6 +608,45 @@ def test_deny_device_storage_access_changing_profile_photo(self): class TestProfileMultipleDevice(MultipleDeviceTestCase): + @marks.testrail_id(6646) + @marks.high + def test_set_profile_picture(self): + self.create_drivers(2) + home_1, home_2 = SignInView(self.drivers[0]).create_user(), SignInView(self.drivers[1]).create_user() + profile_1 = home_1.profile_button.click() + public_key_1 = profile_1.get_public_key_and_username() + + profile_1.just_fyi("Set user Profile image from Gallery") + profile_1.edit_profile_picture(file_name='sauce_logo.png') + home_1.profile_button.click() + profile_1.swipe_down() + + if not profile_1.profile_picture.is_element_image_similar_to_template('sauce_logo_profile.png'): + self.drivers[0].fail('Profile picture was not updated') + + profile_1.just_fyi("Check user profile updated in chat") + home = profile_1.home_button.click() + message = "Text message" + public_chat_name = home.get_random_chat_name() + home_2.add_contact(public_key=public_key_1) + home_2.home_button.click() + public_chat_2 = home_2.join_public_chat(public_chat_name) + public_chat_1 = home.join_public_chat(public_chat_name) + public_chat_1.chat_message_input.send_keys(message) + public_chat_1.send_message_button.click() + if not public_chat_2.chat_element_by_text(message).member_photo.is_element_image_similar_to_template('sauce_logo.png'): + self.drivers[0].fail('Profile picture was not updated in chat') + + profile_1.just_fyi("Set user Profile image by taking Photo") + home_1.profile_button.click() + profile_1.edit_profile_picture(file_name='sauce_logo.png', update_by='Make Photo') + home_1.home_button.click(desired_view='chat') + public_chat_1.chat_message_input.send_keys(message) + public_chat_1.send_message_button.click() + + if public_chat_2.chat_element_by_text(message).member_photo.is_element_image_similar_to_template('sauce_logo.png'): + self.drivers[0].fail('Profile picture was not updated in chat after making photo') + @marks.testrail_id(5432) @marks.medium def test_custom_bootnodes(self): diff --git a/test/appium/tests/atomic/chats/test_one_to_one.py b/test/appium/tests/atomic/chats/test_one_to_one.py index 8adcffe5d9ee..7b7c7612c993 100644 --- a/test/appium/tests/atomic/chats/test_one_to_one.py +++ b/test/appium/tests/atomic/chats/test_one_to_one.py @@ -14,27 +14,6 @@ class TestMessagesOneToOneChatMultiple(MultipleDeviceTestCase): - @marks.testrail_id(5305) - @marks.critical - def test_text_message_1_1_chat(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - device_1_home, device_2_home = device_1.create_user(), device_2.create_user() - profile_1 = device_1_home.profile_button.click() - default_username_1 = profile_1.default_username_text.text - device_1_home = profile_1.get_back_to_home_view() - device_2_public_key = device_2_home.get_public_key_and_username() - device_2_home.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key) - - message = 'hello' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - - device_2_chat = device_2_home.get_chat(default_username_1).click() - device_2_chat.chat_element_by_text(message).wait_for_visibility_of_element() - @marks.testrail_id(6283) @marks.high def test_push_notification_1_1_chat(self): @@ -132,65 +111,33 @@ def test_offline_is_shown_messaging_1_1_chat(self): chat_1 = chat_element.click() chat_1.chat_element_by_text(message_2).wait_for_visibility_of_element(180) - @marks.testrail_id(5338) - @marks.critical - def test_messaging_in_different_networks(self): - self.create_drivers(2) - sign_in_1, sign_in_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - home_1, home_2 = sign_in_1.create_user(), sign_in_2.create_user() - profile_1 = home_1.profile_button.click() - default_username_1 = profile_1.default_username_text.text - home_1 = profile_1.get_back_to_home_view() - public_key_2 = home_2.get_public_key_and_username() - profile_2 = home_2.get_profile_view() - profile_2.switch_network() - - chat_1 = home_1.add_contact(public_key_2) - message = 'test message' - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - - chat_2 = home_2.get_chat(default_username_1).click() - chat_2.chat_element_by_text(message).wait_for_visibility_of_element() - - public_chat_name = home_1.get_random_chat_name() - chat_1.get_back_to_home_view() - home_1.join_public_chat(public_chat_name) - chat_2.get_back_to_home_view() - home_2.join_public_chat(public_chat_name) - - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - chat_2.chat_element_by_text(message).wait_for_visibility_of_element() - @marks.testrail_id(5315) @marks.high - def test_send_non_english_message_to_newly_added_contact(self): + def test_send_non_english_message_to_newly_added_contact_on_different_networks(self): self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - - device_1_home, device_2_home = device_1.create_user(), device_2.create_user() + device_1_home, device_2_home = SignInView(self.drivers[0]).create_user(), SignInView(self.drivers[1]).create_user() profile_1 = device_1_home.profile_button.click() - default_username_1 = profile_1.default_username_text.text - device_1_home = profile_1.get_back_to_home_view() + profile_1.switch_network() + profile_1.just_fyi("Getting public keys and usernames for both users") + device_1_home.profile_button.click() + default_username_1 = profile_1.default_username_text.text + profile_1.home_button.double_click() # Skip until edit-profile feature returned - # profile_1 = device_1_home.profile_button.click() # profile_1.edit_profile_picture('sauce_logo.png') # profile_1.home_button.click() - - device_2_public_key = device_2_home.get_public_key_and_username() + device_2_public_key, default_username_2 = device_2_home.get_public_key_and_username(return_username=True) device_2_home.home_button.click() - device_1_chat = device_1_home.add_contact(device_2_public_key) + profile_1.just_fyi("Add user to contacts and send messages on different language") + device_1_chat = device_1_home.add_contact(device_2_public_key + ' ') messages = ['hello', '¿Cómo estás tu año?', 'ё, доброго вечерочка', '® æ ç ♥'] + timestamp_message = messages[3] for message in messages: device_1_chat.send_message(message) - - chat_element = device_2_home.get_chat(default_username_1) - chat_element.wait_for_visibility_of_element() - device_2_chat = chat_element.click() + device_2_chat = device_2_home.get_chat(default_username_1).click() + sent_time_variants = device_1_chat.convert_device_time_to_chat_timestamp() for message in messages: if not device_2_chat.chat_element_by_text(message).is_element_displayed(): self.errors.append("Message with test '%s' was not received" % message) @@ -198,8 +145,30 @@ def test_send_non_english_message_to_newly_added_contact(self): self.errors.append('Add to contacts button is not shown') if device_2_chat.user_name_text.text != default_username_1: self.errors.append("Default username '%s' is not shown in one-to-one chat" % default_username_1) + + profile_1.just_fyi("Check timestamps for sender and receiver") + for chat in device_1_chat, device_2_chat: + chat.verify_message_is_under_today_text(timestamp_message, self.errors) + timestamp = chat.chat_element_by_text(timestamp_message).timestamp_message.text + if timestamp not in sent_time_variants: + self.errors.append("Timestamp is not shown, expected '%s', in fact '%s'" % (sent_time_variants.join(","), timestamp)) + + device_2_home.just_fyi("Add user to contact and verify his default username") + device_2_chat.add_to_contacts.click() device_2_chat.chat_options.click() device_2_chat.view_profile_button.click() + if not device_2_chat.remove_from_contacts.is_element_displayed(): + self.errors.append("Remove from contacts in not shown after adding contact from 1-1 chat bar") + device_2_chat.back_button.click() + device_2_chat.home_button.double_click() + device_2_home.plus_button.click() + device_2_contacts = device_2_home.start_new_chat_button.click() + if not device_2_contacts.element_by_text(default_username_1).is_element_displayed(): + self.errors.append('%s is not added to contacts' % default_username_1) + if device_1_chat.user_name_text.text != default_username_2: + self.errors.append("Default username '%s' is not shown in one-to-one chat" % default_username_2) + device_1_chat.chat_options.click() + device_1_chat.view_profile_button.click() # TODO: skip until edit-profile feature returned @@ -396,56 +365,9 @@ def test_send_audio_message_with_push_notification_check(self): self.errors.verify_no_errors() - - @marks.testrail_id(5316) - @marks.critical - def test_add_to_contacts(self): - self.create_drivers(2) - device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - - device_1_home, device_2_home = device_1.create_user(), device_2.create_user() - profile_1 = device_1_home.profile_button.click() - default_username_1 = profile_1.default_username_text.text - device_1_home = profile_1.get_back_to_home_view() - - device_2_public_key = device_2_home.get_public_key_and_username() - profile_2 = device_2_home.get_profile_view() - # TODO: skip until edit image profile is enabled - # file_name = 'sauce_logo.png' - # profile_2.edit_profile_picture(file_name) - default_username_2 = profile_2.default_username_text.text - profile_2.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key + ' ') - message = 'hello' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - - chat_element = device_2_home.get_chat(default_username_1) - chat_element.wait_for_visibility_of_element() - device_2_chat = chat_element.click() - if not device_2_chat.chat_element_by_text(message).is_element_displayed(): - self.errors.append("Message with text '%s' was not received" % message) - device_2_chat.add_to_contacts.click() - - device_2_chat.get_back_to_home_view() - device_2_home.plus_button.click() - device_2_contacts = device_2_home.start_new_chat_button.click() - if not device_2_contacts.element_by_text(default_username_1).is_element_displayed(): - self.errors.append('%s is not added to contacts' % default_username_1) - - if device_1_chat.user_name_text.text != default_username_2: - self.errors.append("Default username '%s' is not shown in one-to-one chat" % default_username_2) - device_1_chat.chat_options.click() - device_1_chat.view_profile_button.click() - # TODO: skip until edit image profile is enabled - # if not device_1_chat.contact_profile_picture.is_element_image_equals_template(file_name): - # self.errors.append("Updated profile picture is not shown in one-to-one chat") - self.errors.verify_no_errors() - @marks.testrail_id(5373) @marks.high - def test_send_and_open_links(self): + def test_send_and_open_links_with_previews(self): self.create_drivers(2) device_1, device_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) @@ -456,10 +378,10 @@ def test_send_and_open_links(self): public_key_2 = home_2.get_public_key_and_username() home_2.home_button.click() + home_1.just_fyi("Check that link can be opened from 1-1 chat") chat_1 = home_1.add_contact(public_key_2) url_message = 'http://status.im' - chat_1.chat_message_input.send_keys(url_message) - chat_1.send_message_button.click() + chat_1.send_message(url_message) chat_1.get_back_to_home_view() chat_2 = home_2.get_chat(default_username_1).click() chat_2.element_starts_with_text(url_message, 'button').click() @@ -471,17 +393,50 @@ def test_send_and_open_links(self): web_view.back_to_home_button.click() chat_2.home_button.click() + home_1.just_fyi("Check that link can be opened from public chat") chat_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(7)) - home_1.join_public_chat(chat_name) - home_2.join_public_chat(chat_name) - chat_2.chat_message_input.send_keys(url_message) - chat_2.send_message_button.click() + chat_1 = home_1.join_public_chat(chat_name) + chat_2 = home_2.join_public_chat(chat_name) + chat_2.send_message(url_message) chat_1.element_starts_with_text(url_message, 'button').click() web_view = chat_1.open_in_status_button.click() try: web_view.element_by_text('Private, Secure Communication').find_element() except TimeoutException: self.errors.append('Device 1: URL was not opened from 1-1 chat') + home_1.home_button.click(desired_view='chat') + + preview_urls = {'github_pr':{'url':'https://github.com/status-im/status-react/pull/11707', + 'txt':'Update translations by jinhojang6 · Pull Request #11707 · status-im/status-react', + 'subtitle' : 'GitHub'}, + 'yotube':{'url':'https://www.youtube.com/watch?v=XN-SVmuJH2g&list=PLbrz7IuP1hrgNtYe9g6YHwHO6F3OqNMao', + 'txt':'Status & Keycard – Hardware-Enforced Security', + 'subtitle': 'YouTube'}} + + home_1.just_fyi("Check enabling and sending first gif") + giphy_url = 'https://giphy.com/gifs/this-is-fine-QMHoU66sBXqqLqYvGO' + chat_2.send_message(giphy_url) + chat_2.element_by_translation_id("dont-ask").click() + chat_1.element_by_translation_id("enable").wait_and_click() + chat_1.element_by_translation_id("enable-all").wait_and_click() + chat_1.back_button.click() + if not chat_1.get_preview_message_by_text(giphy_url).preview_image: + self.errors.append("No preview is shown for %s" % giphy_url) + for key in preview_urls: + home_2.just_fyi("Checking %s preview case" % key) + data = preview_urls[key] + chat_2.send_message(data['url']) + message = chat_1.get_preview_message_by_text(data['url']) + if message.preview_title.text != data['txt']: + self.errors.append("Title '%s' does not match expected" % message.preview_title.text) + if message.preview_subtitle.text != data['subtitle']: + self.errors.append("Subtitle '%s' does not match expected" % message.preview_subtitle.text) + + home_2.just_fyi("Check if after do not ask again previews are not shown and no enable button appear") + if chat_2.element_by_translation_id("enable").is_element_displayed(): + self.errors.append("Enable button is still shown after clicking on 'Den't ask again'") + if chat_2.get_preview_message_by_text(giphy_url).preview_image: + self.errors.append("Preview is shown for sender without permission") self.errors.verify_no_errors() @marks.testrail_id(5362) @@ -577,62 +532,6 @@ def test_markdown_support_in_messages(self): self.errors.verify_no_errors() - @marks.testrail_id(5385) - @marks.high - def test_timestamp_in_chats(self): - self.create_drivers(2) - sign_in_1, sign_in_2 = SignInView(self.drivers[0]), SignInView(self.drivers[1]) - device_1_home, device_2_home = sign_in_1.create_user(), sign_in_2.create_user() - device_2_public_key = device_2_home.get_public_key_and_username() - device_2_home.home_button.click() - device_1_profile = device_1_home.profile_button.click() - default_username_1 = device_1_profile.default_username_text.text - device_1_profile.home_button.click() - - device_1_chat = device_1_home.add_contact(device_2_public_key) - - device_1_chat.just_fyi('check user picture and timestamps in chat for sender and recipient in 1-1 chat') - message = 'test text' - device_1_chat.chat_message_input.send_keys(message) - device_1_chat.send_message_button.click() - sent_time = device_1_chat.convert_device_time_to_chat_timestamp() - - if not device_1_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in 1-1 chat for the sender') - if device_1_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is displayed in 1-1 chat for the sender') - - device_2_chat = device_2_home.get_chat(default_username_1).click() - if not device_2_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in 1-1 chat for the recipient') - if device_2_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is displayed in 1-1 chat for the recipient') - for chat in device_1_chat, device_2_chat: - chat.verify_message_is_under_today_text(message, self.errors) - - device_1_chat.just_fyi('check user picture and timestamps in chat for sender and recipient in public chat') - chat_name = device_1_home.get_random_chat_name() - for chat in device_1_chat, device_2_chat: - home_view = chat.get_back_to_home_view() - home_view.join_public_chat(chat_name) - - device_2_chat.chat_message_input.send_keys(message) - device_2_chat.send_message_button.click() - sent_time = device_2_chat.convert_device_time_to_chat_timestamp() - if not device_2_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in public chat for the sender') - if device_2_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is displayed in public chat for the sender') - - if not device_1_chat.chat_element_by_text(message).contains_text(sent_time): - self.errors.append('Timestamp is not displayed in public chat for the recipient') - if not device_1_chat.chat_element_by_text(message).member_photo.is_element_displayed(): - self.errors.append('Member photo is not displayed in public chat for the recipient') - for chat in device_1_chat, device_2_chat: - chat.verify_message_is_under_today_text(message, self.errors) - - self.errors.verify_no_errors() - class TestMessagesOneToOneChatSingle(SingleDeviceTestCase): @@ -723,7 +622,6 @@ def test_send_emoji(self): self.errors.append('Message with emoji was not sent in 1-1 chat') self.errors.verify_no_errors() - @marks.testrail_id(5783) @marks.critical def test_can_use_purchased_stickers_on_recovered_account(self): @@ -785,7 +683,6 @@ def test_start_chat_with_ens_mention_in_one_to_one(self): self.errors.append('No redirect to user profile after tapping on message with mention (nickname) in 1-1 chat') self.errors.verify_no_errors() - @marks.testrail_id(6298) @marks.medium def test_can_scan_qr_with_chat_key_from_home_start_chat(self): diff --git a/test/appium/tests/atomic/chats/test_public.py b/test/appium/tests/atomic/chats/test_public.py index 1aea8820b8f3..e6704a4938f1 100644 --- a/test/appium/tests/atomic/chats/test_public.py +++ b/test/appium/tests/atomic/chats/test_public.py @@ -1,4 +1,3 @@ - import emoji import random from dateutil import parser @@ -41,9 +40,14 @@ def test_public_chat_messaging(self): chat_1, chat_2 = home_1.join_public_chat(public_chat_name), home_2.join_public_chat(public_chat_name) message = 'hello' - chat_1.chat_message_input.send_keys(message) - chat_1.send_message_button.click() - + chat_1.send_message(message) + + sent_time_variants = chat_1.convert_device_time_to_chat_timestamp() + for chat in chat_1, chat_2: + chat.verify_message_is_under_today_text(message, self.errors) + timestamp = chat.chat_element_by_text(message).timestamp_message.text + if timestamp not in sent_time_variants: + self.errors.append("Timestamp is not shown, expected '%s', in fact '%s'" % (sent_time_variants.join(','), timestamp)) if chat_2.chat_element_by_text(message).username.text != default_username_1: self.errors.append("Default username '%s' is not shown next to the received message" % default_username_1) diff --git a/test/appium/tests/atomic/dapps_and_browsing/test_dapps.py b/test/appium/tests/atomic/dapps_and_browsing/test_dapps.py index 67d05a8c7ce1..6d23e4bfb8d0 100644 --- a/test/appium/tests/atomic/dapps_and_browsing/test_dapps.py +++ b/test/appium/tests/atomic/dapps_and_browsing/test_dapps.py @@ -77,7 +77,6 @@ def test_webview_camera_permission(self): camera_dapp.browser_refresh_page_button.click() camera_dapp.allow_button.click() if camera_dapp.camera_image_in_dapp.is_element_image_similar_to_template('blank_camera_image.png'): - camera_dapp.camera_image_in_dapp.save_new_screenshot_of_element('blank_camera_image3.png') self.driver.fail("Even camera access Accepted to Dapp, - camera view is not shown") camera_dapp.just_fyi("Relogin and check camera access still needs to be allowed") diff --git a/test/appium/tests/base_test_case.py b/test/appium/tests/base_test_case.py index 8e241cf91e3c..393645f7eae8 100644 --- a/test/appium/tests/base_test_case.py +++ b/test/appium/tests/base_test_case.py @@ -68,7 +68,7 @@ def capabilities_sauce_lab(self): desired_caps['deviceName'] = 'Android GoogleAPI Emulator' desired_caps['deviceOrientation'] = "portrait" desired_caps['commandTimeout'] = 600 - desired_caps['idleTimeout'] = 1000 + desired_caps['idleTimeout'] = 600 desired_caps['unicodeKeyboard'] = True desired_caps['automationName'] = 'UiAutomator2' desired_caps['setWebContentDebuggingEnabled'] = True @@ -143,10 +143,6 @@ def number(self): return test_suite_data.current_test.testruns[-1].jobs[self.session_id] def info(self, text: str): - # if "Base" not in text: - # text = 'Device %s: %s' % (self.number, text) - # logging.info(text) - # test_suite_data.current_test.testruns[-1].steps.append(text) text = 'Device %s: %s ' % (self.number, text) logging.info(text) test_suite_data.current_test.testruns[-1].steps.append(text) diff --git a/test/appium/views/base_element.py b/test/appium/views/base_element.py index ad1e9549d394..097e8cd20752 100644 --- a/test/appium/views/base_element.py +++ b/test/appium/views/base_element.py @@ -28,6 +28,7 @@ def __init__(self, driver, **kwargs): self.suffix = None self.id = None self.class_name = None + self.AndroidUIAutomator = None self.webview = None self.__dict__.update(kwargs) @@ -52,6 +53,9 @@ def set_locator(self): elif self.class_name: self.by = MobileBy.CLASS_NAME self.locator = self.class_name + elif self.AndroidUIAutomator: + self.by = MobileBy.ANDROID_UIAUTOMATOR + self.locator = self.AndroidUIAutomator elif self.webview: self.locator = '//*[@text="{0}"] | //*[@content-desc="{desc}"]'.format(self.webview, desc=self.webview) if self.prefix: diff --git a/test/appium/views/base_view.py b/test/appium/views/base_view.py index 82d1bacd135f..a359c49c0767 100644 --- a/test/appium/views/base_view.py +++ b/test/appium/views/base_view.py @@ -215,6 +215,7 @@ def __init__(self, driver): self.no_button = Button(self.driver, translation_id="no") self.back_button = BackButton(self.driver) self.allow_button = AllowButton(self.driver) + self.allow_all_the_time = Button(self.driver, xpath="//*[@text='Allow all the time']") self.deny_button = Button(self.driver, translation_id="deny", uppercase=True) self.continue_button = Button(self.driver, translation_id="continue", uppercase=True) self.ok_button = Button(self.driver, xpath="//*[@text='OK' or @text='Ok']") diff --git a/test/appium/views/chat_view.py b/test/appium/views/chat_view.py index 2e52f2898da1..a74ce886d4b5 100644 --- a/test/appium/views/chat_view.py +++ b/test/appium/views/chat_view.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta import dateutil.parser import time @@ -11,6 +11,15 @@ from views.profile_view import ProfilePictureElement +class CommandsButton(Button): + def __init__(self, driver): + super().__init__(driver, accessibility_id="show-extensions-icon") + + def click(self): + self.click_until_presence_of_element(SendCommand(self.driver)) + return self.navigate() + + class SendCommand(Button): def __init__(self, driver): super().__init__(driver, translation_id="send-transaction") @@ -74,6 +83,7 @@ def __init__(self, driver): def navigate(self): return ChatView(self.driver) + class ChatOptionsButton(Button): def __init__(self, driver): super().__init__(driver, accessibility_id="chat-menu-button") @@ -174,7 +184,7 @@ def replied_to_username_text(self): class RepliedToUsernameText(Text): def __init__(self, driver, parent_locator: str): super().__init__(driver, prefix=parent_locator, - xpath="%s/preceding-sibling::*[1]/android.widget.TextView[1]") + xpath="/preceding-sibling::*[1]/android.widget.TextView[1]") try: return RepliedToUsernameText(self.driver, self.message_locator).text except NoSuchElementException: @@ -240,6 +250,40 @@ def get_user_from_group_info(self, username: str): return Text(self.driver, xpath="//*[@text='%s']" % username) +class PreviewMessage(ChatElementByText): + def __init__(self, driver, text:str): + super().__init__(driver, text=text) + self.locator+="/android.view.ViewGroup/android.view.ViewGroup/" + + @staticmethod + def return_element_or_empty(obj): + try: + return obj.find_element() + except NoSuchElementException: + return '' + + @property + def preview_image(self): + class PreviewImage(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="android.widget.ImageView") + return PreviewMessage.return_element_or_empty(PreviewImage(self.driver, self.locator)) + + @property + def preview_title(self): + class PreviewTitle(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="android.widget.TextView[1]") + return PreviewMessage.return_element_or_empty(PreviewTitle(self.driver, self.locator)) + + @property + def preview_subtitle(self): + class PreviewSubTitle(SilentButton): + def __init__(self, driver, parent_locator: str): + super().__init__(driver, prefix=parent_locator, xpath="android.widget.TextView[2]") + return PreviewMessage.return_element_or_empty(PreviewSubTitle(self.driver, self.locator)) + + class TransactionMessage(ChatElementByText): def __init__(self, driver, text:str): super().__init__(driver, text=text) @@ -347,7 +391,7 @@ def __init__(self, driver): self.cancel_reply_button = Button(self.driver, accessibility_id="cancel-message-reply") self.chat_item = Button(self.driver, accessibility_id="chat-item") self.chat_name_editbox = EditBox(self.driver, accessibility_id="chat-name-input") - self.commands_button = Button(self.driver, accessibility_id="show-extensions-icon") + self.commands_button = CommandsButton(self.driver) self.send_command = SendCommand(self.driver) self.request_command = RequestCommand(self.driver) @@ -435,6 +479,10 @@ def get_incoming_transaction(self, account=None): account = self.status_account_name return IncomingTransaction(self.driver, account) + def get_preview_message_by_text(self, text=None): + self.driver.info('**Getting preview message for link:%s**' % text) + return PreviewMessage(self.driver, text) + def delete_chat(self): self.driver.info("**Delete chat via options**") @@ -513,10 +561,11 @@ def get_user_options(self, username: str): def chat_element_by_text(self, text): - self.driver.info("**Looking for a message by text: '%s'**" % text) + self.driver.info("**Looking for a message by text: %s**" % text) return ChatElementByText(self.driver, text) def verify_message_is_under_today_text(self, text, errors): + self.driver.info("**Veryfying that '%s' is under today**" % text) message_element = self.chat_element_by_text(text) message_element.wait_for_visibility_of_element() message_location = message_element.find_element().location['y'] @@ -613,11 +662,15 @@ def set_nickname(self, nickname): self.nickname_input_field.send_keys(nickname) self.element_by_text('Done').click() - def convert_device_time_to_chat_timestamp(self): + def convert_device_time_to_chat_timestamp(self) -> list: sent_time_object = dateutil.parser.parse(self.driver.device_time) timestamp = datetime.strptime("%s:%s" % (sent_time_object.hour, sent_time_object.minute), '%H:%M').strftime("%I:%M %p") - timestamp = timestamp[1:] if timestamp[0] == '0' else timestamp - return timestamp + timestamp_obj = datetime.strptime(timestamp, '%I:%M %p') + possible_timestamps_obj = [timestamp_obj + timedelta(0,0,0,0,1), timestamp_obj, timestamp_obj - timedelta(0,0,0,0,1)] + timestamps = list(map(lambda x : x.strftime("%I:%M %p"), possible_timestamps_obj)) + final_timestamps = [t[1:] if t[0] == '0' else t for t in timestamps] + return final_timestamps + def set_new_status(self, status='something is happening', image=False): self.driver.info("**Setting new status:%s, image set is: %s**" % (status, str(image))) diff --git a/test/appium/views/elements_templates/sauce_logo_profile.png b/test/appium/views/elements_templates/sauce_logo_profile.png index 7e58e6074b77..440008dc3f00 100644 Binary files a/test/appium/views/elements_templates/sauce_logo_profile.png and b/test/appium/views/elements_templates/sauce_logo_profile.png differ diff --git a/test/appium/views/profile_view.py b/test/appium/views/profile_view.py index 0fd28559bffc..5d96ced03fc1 100644 --- a/test/appium/views/profile_view.py +++ b/test/appium/views/profile_view.py @@ -171,6 +171,11 @@ def __init__(self, driver): self.confirm_edit_button = Button(self.driver, accessibility_id="done-button") self.select_from_gallery_button = Button(self.driver, translation_id="profile-pic-pick") self.capture_button = Button(self.driver, translation_id="image-source-make-photo") + self.take_photo_button = Button(self.driver, accessibility_id="take-photo") + self.crop_photo_button = Button(self.driver, accessibility_id="Crop") + self.decline_photo_crop = Button(self.driver, accessibility_id="Navigate up") + self.shutter_button = Button(self.driver, accessibility_id="Shutter") + self.accept_photo_button = Button(self.driver, accessibility_id="Done") # ENS self.username_in_ens_chat_settings_text = EditBox(self.driver, @@ -327,20 +332,45 @@ def backup_recovery_phrase(self): self.driver.info("**Seed phrase is backed up!**") return recovery_phrase - def edit_profile_picture(self, file_name: str): + def edit_profile_picture(self, file_name: str, update_by = "Gallery"): + self.driver.info("**Setting custom profile image**") if not AbstractTestCase().environment == 'sauce': raise NotImplementedError('Test case is implemented to run on SauceLabs only') self.profile_picture.click() self.profile_picture.template = file_name - self.select_from_gallery_button.click() + if update_by == "Gallery": + self.select_from_gallery_button.click() + if self.allow_button.is_element_displayed(sec=5): + self.allow_button.click() + image_name = "sauce_logo.png, 4.64 kB, Nov 4, 2020" + if file_name == 'sauce_logo_red.png': + image_name = "sauce_logo_red.png, 624 kB, Nov 4, 2020" + + image_full_content = self.get_image_in_storage_by_name(image_name) + if not image_full_content.is_element_displayed(2): + self.show_roots_button.click() + for element_text in 'Images', 'DCIM': + self.element_by_text(element_text).click() + image_full_content.click() + else: + ## take by Photo + self.take_photo() + self.click_system_back_button() + self.take_photo() + self.accept_photo_button.click() + self.crop_photo_button.click() + self.driver.info("**Custom profile image has been set**") + + + def take_photo(self): + self.take_photo_button.click() if self.allow_button.is_element_displayed(sec=5): self.allow_button.click() - picture = self.element_by_text(file_name) - if not picture.is_element_displayed(2): - self.show_roots_button.click() - for element_text in 'Images', 'DCIM': - self.element_by_text(element_text).click() - picture.click() + if self.allow_all_the_time.is_element_displayed(sec=5): + self.allow_all_the_time.click() + if self.element_by_text("NEXT").is_element_displayed(sec=5): + self.element_by_text("NEXT").click() + self.shutter_button.click() def logout(self): self.driver.info("**Logging out**") @@ -351,11 +381,15 @@ def logout(self): def mail_server_by_name(self, server_name): return MailServerElement(self.driver, server_name) + def get_image_in_storage_by_name(self, image_name=str()): + return SilentButton(self.driver, xpath="//*[@content-desc='%s']" % image_name) + def get_toggle_device_by_name(self, device_name): - return SilentButton(self.driver, xpath="//android.widget.TextView[contains(@text,'%s')]/../android.widget.Switch" % device_name) + self.driver.info("**Selecting device %s for sync**" % device_name) + return SilentButton(self.driver, xpath="//android.widget.TextView[contains(@text,'%s')]/..//android.widget.CheckBox" % device_name) def discover_and_advertise_device(self, device_name): - self.driver.info("**Discover and advertise '%s'**" % device_name) + self.driver.info("**Discover and advertise %s**" % device_name) self.sync_settings_button.click() self.devices_button.scroll_to_element() self.devices_button.click() @@ -375,7 +409,7 @@ def retry_to_connect_to_mailserver(self): self.driver.fail("Failed to connect after %s attempts" % i) def connect_existing_status_ens(self, name): - self.driver.info("**Connect existing ENS '%s'**" % name) + self.driver.info("**Connect existing ENS: %s**" % name) profile = self.profile_button.click() profile.switch_network('Mainnet with upstream RPC') self.profile_button.click() diff --git a/translations/en.json b/translations/en.json index 328feda80db0..7bf31a3467d7 100644 --- a/translations/en.json +++ b/translations/en.json @@ -146,17 +146,64 @@ "close-app-title": "Warning!", "command-button-send": "Send", "communities": "Communities", + "community-members": { + "one": "{{count}} member", + "other": "{{count}} members" + }, + "members-label": "Members", + "open-membership": "Open membership", + "member-kick": "Kick member", + "membership-requests": "Membership requests", + "community-members-title": "Members", + "community-requests-to-join-title": "Membership requests", "name-your-channel": "Name your channel", + "name-your-channel-placeholder": "Channel name", "give-a-short-description": "Give a short description", "communities-alpha": "Communities (alpha)", "communities-verified": "✓ Verified Status Community", + "request-access": "Request access", + "membership-request-pending": "Membership request pending", "create-community": "Create a community", + "edit-community": "Edit community", + "community-edit-title": "Edit community", + "community-invite-title": "Invite", + "community-share-title": "Share", + "invite": "Invite", "create-channel": "Create a channel", "import-community": "Import a community", + "import-community-title": "Import a community", "name-your-community": "Name your community", "name-your-community-placeholder": "A catchy name", "give-a-short-description-community": "Give it a short description", + "new-community-title": "New community", + "membership-title": "Membership requirement", + "create-channel-title": "New channel", + "community-thumbnail-image": "Thumbnail image", + "community-thumbnail-upload": "Upload", + "community-image-take": "Take a photo", + "community-image-pick": "Pick an image", + "community-image-delete": "", + "community-color": "Community colour", + "community-color-placeholder": "Pick a colour", + "membership-button": "Membership requirement", + "membership-none": "None", + "membership-none-placeholder": "You can require new members to meet certain criteria before they can join. This can be changed at any time", + "membership-approval": "Require approval", + "membership-approval-description": "Your community is free to join, but new members are required to be approved by the community creator first", + "membership-invite": "Require invite from another member", + "membership-invite-description": "Your community can only be joined by an invitation from existing community members", + "membership-ens": "Require ENS username", + "membership-ens-description": "Your community requires an ENS username to be able to join", + "membership-free": "No requirement", + "membership-free-description": "Your community is free for anyone to join", + "community-roles": "Roles", + "community-key": "Community private key", + "community-key-placeholder": "Type your community private key", + "leave-community": "Leave community", + "enter-user-pk": "Enter user public key", + "import": "Import", "complete-hardwallet-setup": "This card is now linked. You need it to sign transactions and unlock your keys", + "chat-notification-preferences": "Notification settings", "completed": "Completed", "confirm": "Confirm", "confirmation-request": "Confirmation request", @@ -479,7 +526,8 @@ "ethereum-node-started-incorrectly-title": "Ethereum node started incorrectly", "etherscan-lookup": "Look up on Etherscan", "export-account": "Export account", - "export-key": "Export key", + "export-key": "Export private key", + "community-private-key": "Community private key", "failed": "Failed", "faq": "Frequently asked questions", "fetch-messages": "↓ Fetch messages", @@ -490,6 +538,7 @@ "fleet": "Fleet", "fleet-settings": "Fleet settings", "follow-your-interests": "Jump into a public chat and meet new people", + "follow": "Follow", "free": "↓ Free", "from": "From", "gas-limit": "Gas limit", @@ -1177,6 +1226,7 @@ "validation-amount-invalid-number": "Amount is not a valid number", "validation-amount-is-too-precise": "Amount is too precise. Max number of decimals is {{decimals}}.", "version": "App version", + "view": "View", "view-cryptokitties": "View in CryptoKitties", "view-cryptostrikers": "View in CryptoStrikers", "view-etheremon": "View in Etheremon", @@ -1215,6 +1265,8 @@ "welcome-to-status": "Welcome to Status!", "welcome-to-status-description": "Set up your crypto wallet, invite friends to chat and browse decentralized apps", "welcome-blank-message": "Your chats will appear here. To start new chats press the ⊕ button", + "welcome-community-blank-message": "Your chats will appear here. To start new chats click on the 3 dots above and select \"Create a channel\"", + "welcome-blank-community-message": "Your communities will appear here.", "seed-phrase-placeholder": "Seed phrase...", "word-count": "Word count", "word-n": "Word #{{number}}", diff --git a/yarn.lock b/yarn.lock index dd2d7d234dcf..b333934b70a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6751,9 +6751,9 @@ react-native-splash-screen@^3.2.0: resolved "https://registry.yarnpkg.com/react-native-splash-screen/-/react-native-splash-screen-3.2.0.tgz#d47ec8557b1ba988ee3ea98d01463081b60fff45" integrity sha512-Ls9qiNZzW/OLFoI25wfjjAcrf2DZ975hn2vr6U9gyuxi2nooVbzQeFoQS5vQcbCt9QX5NY8ASEEAtlLdIa6KVg== -"react-native-status-keycard@git+https://github.com/status-im/react-native-status-keycard.git#v2.5.28": - version "2.5.28" - resolved "git+https://github.com/status-im/react-native-status-keycard.git#6d95a0d85dd1062d2565eec89d589ecb0a56282f" +"react-native-status-keycard@git+https://github.com/status-im/react-native-status-keycard.git#v2.5.31": + version "2.5.31" + resolved "git+https://github.com/status-im/react-native-status-keycard.git#67ba4d5596ae3f7fd123c0f1c925e98e8def493f" react-native-svg@^9.8.4: version "9.13.6"