diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e8ec2d8..7c25f86 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + diff --git a/assets/images/Bg_asset.svg b/assets/images/Bg_asset.svg index cb8f401..dae594f 100644 --- a/assets/images/Bg_asset.svg +++ b/assets/images/Bg_asset.svg @@ -1,142 +1,125 @@ - - + + - - - + + + - + - - - - + + + + - - - - - - + + + - - - - - + + + + + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - + - + - + - + - + - + - + diff --git a/ios/EphemeraMobileChat.xcodeproj/project.pbxproj b/ios/EphemeraMobileChat.xcodeproj/project.pbxproj index 46c9fae..cc0c4dd 100644 --- a/ios/EphemeraMobileChat.xcodeproj/project.pbxproj +++ b/ios/EphemeraMobileChat.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 5709B34CF0A7D63546082F79 /* Pods-EphemeraMobileChat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EphemeraMobileChat.release.xcconfig"; path = "Target Support Files/Pods-EphemeraMobileChat/Pods-EphemeraMobileChat.release.xcconfig"; sourceTree = ""; }; 5B7EB9410499542E8C5724F5 /* Pods-EphemeraMobileChat-EphemeraMobileChatTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EphemeraMobileChat-EphemeraMobileChatTests.debug.xcconfig"; path = "Target Support Files/Pods-EphemeraMobileChat-EphemeraMobileChatTests/Pods-EphemeraMobileChat-EphemeraMobileChatTests.debug.xcconfig"; sourceTree = ""; }; 5DCACB8F33CDC322A6C60F78 /* libPods-EphemeraMobileChat.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-EphemeraMobileChat.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 68B9EB672BF7DE32006398E1 /* EphemeraMobileChat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = EphemeraMobileChat.entitlements; path = EphemeraMobileChat/EphemeraMobileChat.entitlements; sourceTree = ""; }; 78611DF31EED3FF745D00249 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-EphemeraMobileChat/ExpoModulesProvider.swift"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = EphemeraMobileChat/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-EphemeraMobileChat-EphemeraMobileChatTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EphemeraMobileChat-EphemeraMobileChatTests.release.xcconfig"; path = "Target Support Files/Pods-EphemeraMobileChat-EphemeraMobileChatTests/Pods-EphemeraMobileChat-EphemeraMobileChatTests.release.xcconfig"; sourceTree = ""; }; @@ -90,6 +91,7 @@ 13B07FAE1A68108700A75B9A /* EphemeraMobileChat */ = { isa = PBXGroup; children = ( + 68B9EB672BF7DE32006398E1 /* EphemeraMobileChat.entitlements */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 13B07FB51A68108700A75B9A /* Images.xcassets */, @@ -539,8 +541,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = EphemeraMobileChat/EphemeraMobileChat.entitlements; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = QB5WFHTRW9; + DEVELOPMENT_TEAM = FY4NZR34Z3; ENABLE_BITCODE = NO; INFOPLIST_FILE = EphemeraMobileChat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -555,7 +558,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = org.ephemera.EphemeraMobileChat; PRODUCT_NAME = EphemeraMobileChat; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -569,8 +572,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = EphemeraMobileChat/EphemeraMobileChat.entitlements; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = QB5WFHTRW9; + DEVELOPMENT_TEAM = FY4NZR34Z3; INFOPLIST_FILE = EphemeraMobileChat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -584,7 +588,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = org.ephemera.EphemeraMobileChat; PRODUCT_NAME = EphemeraMobileChat; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/EphemeraMobileChat/AppDelegate.h b/ios/EphemeraMobileChat/AppDelegate.h index a7ebb51..bc108d6 100644 --- a/ios/EphemeraMobileChat/AppDelegate.h +++ b/ios/EphemeraMobileChat/AppDelegate.h @@ -1,7 +1,8 @@ #import #import #import +#import -@interface AppDelegate : EXAppDelegateWrapper +@interface AppDelegate : EXAppDelegateWrapper @end diff --git a/ios/EphemeraMobileChat/AppDelegate.mm b/ios/EphemeraMobileChat/AppDelegate.mm index 26624ff..934515f 100644 --- a/ios/EphemeraMobileChat/AppDelegate.mm +++ b/ios/EphemeraMobileChat/AppDelegate.mm @@ -2,6 +2,8 @@ #import #import +#import +#import @implementation AppDelegate @@ -11,6 +13,9 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; + // Define UNUserNotificationCenter + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = self; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @@ -33,4 +38,33 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(N return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; } +// Required for the register event. +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} +// Required for the notification event. You must call the completion handler after handling the remote notification. +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo +fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler +{ + [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} +// Required for the registrationError event. +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error]; +} +// Required for localNotification event +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler +{ + [RNCPushNotificationIOS didReceiveNotificationResponse:response]; +} +//Called when a notification is delivered to a foreground app. +-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler +{ + completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge); +} + @end diff --git a/ios/EphemeraMobileChat/EphemeraMobileChat.entitlements b/ios/EphemeraMobileChat/EphemeraMobileChat.entitlements new file mode 100644 index 0000000..903def2 --- /dev/null +++ b/ios/EphemeraMobileChat/EphemeraMobileChat.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/ios/EphemeraMobileChat/Info.plist b/ios/EphemeraMobileChat/Info.plist index 20da6df..212c135 100644 --- a/ios/EphemeraMobileChat/Info.plist +++ b/ios/EphemeraMobileChat/Info.plist @@ -25,7 +25,7 @@ CFBundleURLSchemes - xmtp-chat + ephemera-chat @@ -42,6 +42,10 @@ NSLocationWhenInUseUsageDescription + UIBackgroundModes + + remote-notification + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/ios/Podfile b/ios/Podfile index a834381..eb8d2a4 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,14 +1,45 @@ require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") -# Resolve react_native_pods.rb with node to allow for hoisting -require Pod::Executable.execute_command('node', ['-p', - 'require.resolve( - "react-native/scripts/react_native_pods.rb", - {paths: [process.argv[1]]}, - )', __dir__]).strip +def node_require(script) + # Resolve script with node to allow for hoisting + require Pod::Executable.execute_command('node', ['-p', + "require.resolve( + '#{script}', + {paths: [process.argv[1]]}, + )", __dir__]).strip +end + +node_require('react-native/scripts/react_native_pods.rb') +node_require('react-native-permissions/scripts/setup.rb') platform :ios, '16.0' prepare_react_native_project! +setup_permissions([ + # 'AppTrackingTransparency', + # 'Bluetooth', + # 'Calendars', + # 'CalendarsWriteOnly', + # 'Camera', + # 'Contacts', + # 'FaceID', + # 'LocationAccuracy', + # 'LocationAlways', + # 'LocationWhenInUse', + # 'MediaLibrary', + # 'Microphone', + # 'Motion', + 'Notifications', + # 'PhotoLibrary', + # 'PhotoLibraryAddOnly', + # 'Reminders', + # 'Siri', + # 'SpeechRecognition', + # 'StoreKit', +]) + +# node_require('react-native/scripts/react_native_pods.rb') +# node_require('react-native-permissions/scripts/setup.rb') + # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded # diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4f24dc7..192dce3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -10,8 +10,8 @@ PODS: - CoinbaseWalletSDKExpo (1.0.10): - CoinbaseWalletSDK/CrossPlatform (= 1.0.4) - ExpoModulesCore - - Connect-Swift (0.3.0): - - SwiftProtobuf (~> 1.20.3) + - Connect-Swift (0.12.0): + - SwiftProtobuf (~> 1.25.2) - DoubleConversion (1.1.6) - EXApplication (5.8.3): - ExpoModulesCore @@ -122,7 +122,7 @@ PODS: - libwebp/sharpyuv (1.3.2) - libwebp/webp (1.3.2): - libwebp/sharpyuv - - LibXMTP (0.4.2-beta5) + - LibXMTP (0.4.4-beta5) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.5): @@ -1202,6 +1202,8 @@ PODS: - React-Core - RNCClipboard (1.13.2): - React-Core + - RNCPushNotificationIOS (1.11.0): + - React-Core - RNDeviceInfo (10.12.0): - React-Core - RNFastImage (8.6.3): @@ -1210,6 +1212,10 @@ PODS: - SDWebImageWebPCoder (~> 0.8.4) - RNLocalize (3.0.6): - React-Core + - RNPermissions (4.1.5): + - React-Core + - RNReactNativeHapticFeedback (2.2.0): + - React-Core - RNReanimated (3.6.2): - glog - RCT-Folly (= 2022.05.16.00) @@ -1230,7 +1236,7 @@ PODS: - secp256k1.swift (0.1.4) - simdjson (3.1.0-wmelon1) - SocketRocket (0.6.1) - - SwiftProtobuf (1.20.3) + - SwiftProtobuf (1.25.2) - WatermelonDB (0.27.1): - React - simdjson @@ -1239,16 +1245,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.8.15): - - Connect-Swift (= 0.3.0) + - XMTP (0.10.11): + - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.4.2-beta5) + - LibXMTP (= 0.4.4-beta5) - web3.swift - - XMTPReactNative (1.28.0-beta.13): + - XMTPReactNative (1.33.1-beta.1): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.8.15) + - XMTP (= 0.10.11) - Yoga (1.14.0) DEPENDENCIES: @@ -1348,9 +1354,12 @@ DEPENDENCIES: - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" + - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) - RNFastImage (from `../node_modules/react-native-fast-image`) - RNLocalize (from `../node_modules/react-native-localize`) + - RNPermissions (from `../node_modules/react-native-permissions`) + - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) @@ -1537,12 +1546,18 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: :path: "../node_modules/@react-native-clipboard/clipboard" + RNCPushNotificationIOS: + :path: "../node_modules/@react-native-community/push-notification-ios" RNDeviceInfo: :path: "../node_modules/react-native-device-info" RNFastImage: :path: "../node_modules/react-native-fast-image" RNLocalize: :path: "../node_modules/react-native-localize" + RNPermissions: + :path: "../node_modules/react-native-permissions" + RNReactNativeHapticFeedback: + :path: "../node_modules/react-native-haptic-feedback" RNReanimated: :path: "../node_modules/react-native-reanimated" RNScreens: @@ -1565,7 +1580,7 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 CoinbaseWalletSDK: ea1f37512bbc69ebe07416e3b29bf840f5cc3152 CoinbaseWalletSDKExpo: c79420eb009f482f768c23b6768fc5b2d7c98777 - Connect-Swift: d38eedc1907d440314f8d26d5a038a00cbb0f6f1 + Connect-Swift: 1de2ef4a548c59ecaeb9120812dfe0d6e07a0d47 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 EXApplication: 137189a3f149b4e8e546884629392c3efc94cbd3 EXConstants: 988aa430ca0f76b43cd46b66e7fae3287f9cc2fc @@ -1594,7 +1609,7 @@ SPEC CHECKSUMS: hermes-engine: b12d9bb1b7cee546f5e48212e7ea7e3c1665a367 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 - LibXMTP: 3cbd1d0dd44ae3648f571a0e81bbe73565759e67 + LibXMTP: e2fb601691981900099551ff3e05621bd73dccf1 Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 506311d0494023c2f7e0b62cc1f31b7370fa3cfb @@ -1653,9 +1668,12 @@ SPEC CHECKSUMS: ReactCommon: 1da3fc14d904883c46327b3322325eebf60a720a RNCAsyncStorage: 618d03a5f52fbccb3d7010076bc54712844c18ef RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d + RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 RNDeviceInfo: db5c64a060e66e5db3102d041ebe3ef307a85120 RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNLocalize: 4222a3756cdbe2dc9a5bdf445765a4d2572107cb + RNPermissions: 5b96247c15864f9d89d7f51eeb0f2b736a2b212d + RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 RNReanimated: 4f0931c29b1535a3a40a6c06797b1d9d39f50754 RNScreens: 17e2f657f1b09a71ec3c821368a04acbb7ebcb46 RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 @@ -1664,13 +1682,13 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 simdjson: e6bfae9ce4bcdc80452d388d593816f1ca2106f3 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1 + SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 WatermelonDB: 842d22ba555425aa9f3ce551239a001200c539bc web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 00fe090825e6bc5991870c1925befc06b1a30b78 - XMTPReactNative: cf4227bf733870dc0331f3925cea852cafd43928 + XMTP: 1deb40ac712ba315dcfdecd590a9b924d8c2241a + XMTPReactNative: 6f6d54c67a140a5abaf70b2f3ec1757561edc9f5 Yoga: 2a16e58450c48e110211dae1159fb114bbcdcfc0 -PODFILE CHECKSUM: 605f5beea10db7b4d01fe2949787bef356832da3 +PODFILE CHECKSUM: 47641fd2f7a6fc6169785131e7615d136e56edd8 COCOAPODS: 1.15.2 diff --git a/package.json b/package.json index df205f1..a695dda 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@react-native-clipboard/clipboard": "^1.13.2", "@react-native-community/blur": "^4.3.2", "@react-native-community/netinfo": "^11.2.1", + "@react-native-community/push-notification-ios": "^1.11.0", "@react-navigation/native": "^6.1.9", "@react-navigation/native-stack": "^6.9.17", "@tanstack/query-sync-storage-persister": "^5.36.1", @@ -30,7 +31,7 @@ "@thirdweb-dev/react-native": "^0.5.4", "@thirdweb-dev/react-native-compat": "^0.5.4", "@xmtp/frames-client": "^0.5.1", - "@xmtp/react-native-sdk": "1.28.0-beta.13", + "@xmtp/react-native-sdk": "1.33.1-beta.1", "aws-sdk": "^2.1540.0", "ethers": "^5", "expo": ">=50.0.0-0 <51.0.0", @@ -46,11 +47,14 @@ "react-native-encrypted-storage": "^4.0.3", "react-native-fast-image": "^8.6.3", "react-native-get-random-values": "^1.10.0", + "react-native-haptic-feedback": "^2.2.0", "react-native-image-picker": "^7.1.0", "react-native-linear-gradient": "^2.8.3", "react-native-localize": "^3.0.4", "react-native-mmkv": "^2.11.0", "react-native-modal": "^13.0.1", + "react-native-permissions": "^4.1.5", + "react-native-push-notification": "^8.1.1", "react-native-qrcode-svg": "^6.2.0", "react-native-randombytes": "^3.6.1", "react-native-reanimated": "^3.6.1", @@ -72,6 +76,7 @@ "@react-native/typescript-config": "^0.73.1", "@types/jest": "^29.5.11", "@types/react": "^18.2.6", + "@types/react-native-push-notification": "^8.1.4", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.6.3", "detox": "^20.17.0", diff --git a/src/components/ConversationInput.tsx b/src/components/ConversationInput.tsx index e4a5a2e..a0bb642 100644 --- a/src/components/ConversationInput.tsx +++ b/src/components/ConversationInput.tsx @@ -126,6 +126,7 @@ export const ConversationInput: FC = ({ alignItems={'center'} borderBottomRightRadius={0}> { - // Bug related to Android and icons rendered by native-base - const rightIcon = Platform.OS === 'ios' ? props.rightIcon : undefined; - const leftIcon = Platform.OS === 'ios' ? props.leftIcon : undefined; - - return ( - - ); + return ; }; diff --git a/src/components/messageContent/MessageOptionsContainer.tsx b/src/components/messageContent/MessageOptionsContainer.tsx index 1a59d69..fc54055 100644 --- a/src/components/messageContent/MessageOptionsContainer.tsx +++ b/src/components/messageContent/MessageOptionsContainer.tsx @@ -6,7 +6,9 @@ import React, { useContext, useState, } from 'react'; +import {StyleSheet} from 'react-native'; import {GroupContext} from '../../context/GroupContext'; +import {translate} from '../../i18n'; import {colors} from '../../theme/colors'; import {Button as AppButton} from '../common/Button'; import {Text} from '../common/Text'; @@ -55,8 +57,16 @@ export const MessageOptionsContainer: FC< [group, messageId], ); + const handleLongPress = useCallback(() => { + setShown(true); + }, [setShown]); + + const handlePress = useCallback(() => { + setShown(false); + }, [setShown]); + return ( - setShown(prev => !prev)}> + 0 && ( - {reactions.map(({content, count, addedByUser}) => ( - handleRemoveReplyPress(content) - : () => handleReactPress(content) - } - key={content} - style={{ - backgroundColor: addedByUser - ? colors.actionPrimary - : undefined, - borderRadius: 16, - height: 24, - padding: 4, - }}> - { + if (count <= 0) { + return null; + } + return ( + handleRemoveReplyPress(content) + : () => handleReactPress(content) } - typography="text-xs/semi-bold" - marginX={1}> - {content} {count} - - - ))} + key={content} + style={[ + styles.reaction, + addedByUser && styles.reactionFromUser, + ]}> + + {content} {count} + + + ); + })} )} {shown && ( - Reply + {translate('reply')} handleReactPress('👍')} variant={'ghost'}> 👍 @@ -110,3 +121,15 @@ export const MessageOptionsContainer: FC< ); }; + +const styles = StyleSheet.create({ + reaction: { + borderRadius: 16, + height: 24, + padding: 4, + }, + reactionFromUser: { + backgroundColor: colors.actionPrimary, + }, + reactionNotFromUser: {}, +}); diff --git a/src/consts/PushNotifications.ts b/src/consts/PushNotifications.ts new file mode 100644 index 0000000..d681d29 --- /dev/null +++ b/src/consts/PushNotifications.ts @@ -0,0 +1,8 @@ +import Config from 'react-native-config'; + +export const PUSH_SERVER = Config.PUSH_SERVER; + +// TODO: For multi client support should be prefixes instead +export const CHANNEL_ID = 'xmtp-react-native-example-dm'; +// TODO: Translations and ens? +export const CHANNEL_NAME = 'Ephemera React Native Example'; diff --git a/src/declarations/react-native-config.d.ts b/src/declarations/react-native-config.d.ts index 4c5c1a6..17ee494 100644 --- a/src/declarations/react-native-config.d.ts +++ b/src/declarations/react-native-config.d.ts @@ -6,6 +6,7 @@ declare module 'react-native-config' { AWS_S3_BUCKET: string; AWS_ACCESS_KEY_ID: string; AWS_SECRET_ACCESS_KEY: string; + PUSH_SERVER: string; } export const Config: NativeConfig; diff --git a/src/hooks/useListMessages.ts b/src/hooks/useListMessages.ts index 41e4837..788dca2 100644 --- a/src/hooks/useListMessages.ts +++ b/src/hooks/useListMessages.ts @@ -1,4 +1,5 @@ import {useQueryClient} from '@tanstack/react-query'; +import {XMTPPush} from '@xmtp/react-native-sdk'; import {useEffect} from 'react'; import {ListMessages} from '../models/ListMessages'; import {QueryKeys} from '../queries/QueryKeys'; @@ -15,6 +16,9 @@ export const useListMessages = () => { return; } client.conversations.streamGroups(async newGroup => { + console.log('NEW GROUP:', newGroup); + const pushClient = new XMTPPush(client); + pushClient.subscribe([newGroup.topic]); let content = ''; try { const groupMessages = await newGroup.messages(); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 99d0aed..7f58fb2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -28,6 +28,7 @@ "connect_your_wallet": "Connect your wallet", "you_can_connect_or_create": "You can connect or create a wallet via Wallet Connect.", "whats_a_wallet": "What's a wallet?", + "no_private_keys_will_be_shared": "No private keys will be shared", "step_1_of_2": "Step 1 of 2", "create_your_xmtp_identity": "Create your XMTP identity", diff --git a/src/navigation/linkingDefinition.ts b/src/navigation/linkingDefinition.ts index 27988b6..5d80707 100644 --- a/src/navigation/linkingDefinition.ts +++ b/src/navigation/linkingDefinition.ts @@ -3,7 +3,7 @@ import {ScreenNames} from './ScreenNames'; import {RootStackParams} from './StackParams'; export const linkingDefinition: LinkingOptions = { - prefixes: ['xmtp-chat://'], + prefixes: ['ephemera-chat://'], config: { screens: { [ScreenNames.OnboardingConnectWallet]: 'onboarding_connect_wallet', diff --git a/src/providers/ClientProvider.tsx b/src/providers/ClientProvider.tsx index 6a90d0c..815309d 100644 --- a/src/providers/ClientProvider.tsx +++ b/src/providers/ClientProvider.tsx @@ -1,6 +1,6 @@ import {useAddress, useConnectionStatus} from '@thirdweb-dev/react-native'; import {Client} from '@xmtp/react-native-sdk'; -import React, {FC, PropsWithChildren, useEffect, useState} from 'react'; +import {FC, PropsWithChildren, useEffect, useState} from 'react'; import {AppConfig} from '../consts/AppConfig'; import { SupportedContentTypes, @@ -33,10 +33,17 @@ export const ClientProvider: FC = ({children}) => { if (!keys) { return setLoading(false); } + // const keyBytes = new Uint8Array([ + // 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, + // 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, + // 135, 145, + // ]); Client.createFromKeyBundle(keys, { codecs: supportedContentTypes, enableAlphaMls: true, env: AppConfig.XMTP_ENV, + // dbEncryptionKey: keyBytes, + appVersion: 'Testing/0.0.0', }) .then(newClient => { setClient(newClient as Client); diff --git a/src/screens/AccountSettingsScreen.tsx b/src/screens/AccountSettingsScreen.tsx index b863c01..4529351 100644 --- a/src/screens/AccountSettingsScreen.tsx +++ b/src/screens/AccountSettingsScreen.tsx @@ -1,19 +1,25 @@ import Clipboard from '@react-native-clipboard/clipboard'; +import {useFocusEffect} from '@react-navigation/native'; import { useAddress, useDisconnect, useENS, useWallet, } from '@thirdweb-dev/react-native'; -import {} from 'ethers'; import {Box, FlatList, HStack, SectionList, Switch, VStack} from 'native-base'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { + AppState, ListRenderItem, Platform, Pressable, SectionListRenderItem, } from 'react-native'; +import { + checkNotifications, + openSettings, + requestNotifications, +} from 'react-native-permissions'; import {AvatarWithFallback} from '../components/AvatarWithFallback'; import {Button} from '../components/common/Button'; import {Drawer} from '../components/common/Drawer'; @@ -126,10 +132,50 @@ export const AccountSettingsScreen = () => { const [walletsShown, setWalletsShown] = useState(false); const disconnect = useDisconnect(); const address = useAddress(); + const [notificationsEnabled, setNotificationsEnabled] = useState(false); + const appState = useRef(AppState.currentState); + + const checkNotificationsCallback = useCallback(() => { + checkNotifications().then(({status}) => { + if (status === 'granted' || status === 'limited') { + setNotificationsEnabled(true); + } + }); + }, [setNotificationsEnabled]); const toggleNotifications = useCallback(() => { - return null; - }, []); + if (notificationsEnabled) { + openSettings(); + } else { + requestNotifications(['alert', 'sound']).then(({status}) => { + console.log('status', status); + if (status === 'granted' || status === 'limited') { + setNotificationsEnabled(true); + } else { + openSettings(); + } + }); + } + }, [notificationsEnabled]); + + useFocusEffect(checkNotificationsCallback); + + useEffect(() => { + const subscription = AppState.addEventListener('change', nextAppState => { + if ( + appState.current.match(/inactive|background/) && + nextAppState === 'active' + ) { + checkNotificationsCallback(); + } + + appState.current = nextAppState; + }); + + return () => { + subscription.remove(); + }; + }, [checkNotificationsCallback]); const listItems = useMemo((): {section: Section; data: ListItem[]}[] => { return [ @@ -139,7 +185,7 @@ export const AccountSettingsScreen = () => { { text: translate('notifications'), onPress: toggleNotifications, - value: true, + value: notificationsEnabled, }, ], }, @@ -169,7 +215,13 @@ export const AccountSettingsScreen = () => { ], }, ]; - }, [toggleNotifications, disconnect, address, setClient]); + }, [ + toggleNotifications, + notificationsEnabled, + address, + setClient, + disconnect, + ]); const renderItem: SectionListRenderItem = ({ section, diff --git a/src/screens/ConversationListScreen.tsx b/src/screens/ConversationListScreen.tsx index 189cb75..a5b94bc 100644 --- a/src/screens/ConversationListScreen.tsx +++ b/src/screens/ConversationListScreen.tsx @@ -243,23 +243,26 @@ export const ConversationListScreen = () => { } ListEmptyComponent={ list === 'ALL_MESSAGES' && !isLoading ? ( - -
- -
- - {translate('youve_got_no_messages')} - - - {translate('start_a_conversation_to_get_going')} - -
+ + +
+ +
+ + {translate('youve_got_no_messages')} + + + {translate('start_a_conversation_to_get_going')} + +
+
) : null } renderItem={renderItem} diff --git a/src/screens/NewConversationScreen.tsx b/src/screens/NewConversationScreen.tsx index 37547a9..55f602b 100644 --- a/src/screens/NewConversationScreen.tsx +++ b/src/screens/NewConversationScreen.tsx @@ -1,8 +1,8 @@ import {useRoute} from '@react-navigation/native'; import {useQueryClient} from '@tanstack/react-query'; import {Box} from 'native-base'; -import React, {useCallback} from 'react'; -import {Alert, Platform} from 'react-native'; +import {useCallback} from 'react'; +import {Alert, KeyboardAvoidingView, Platform} from 'react-native'; import {Asset} from 'react-native-image-picker'; import {ConversationHeader} from '../components/ConversationHeader'; import {ConversationInput} from '../components/ConversationInput'; @@ -23,41 +23,56 @@ export const NewConversationScreen = () => { const onSend = useCallback( async (message: {text?: string; asset?: Asset}) => { - // TODO: Error Handling - const canMessage = await client?.canGroupMessage(addresses); - if (!canMessage && Platform.OS === 'android') { - Alert.alert('You do not have permission to message this group'); - return; + try { + const canMessage = await client?.canGroupMessage(addresses); + for (const address of addresses) { + const lower = address.toLowerCase(); + if (!canMessage?.[lower]) { + Alert.alert(`${address} cannot be added to a group`); + return; + } + } + console.log('here11118', addresses); + client?.conversations + ?.newGroup(addresses) + .then(group => { + console.log('here11118', message); + group + .send(message.text ?? '') + .then(() => { + queryClient.setQueryData( + [QueryKeys.List, client?.address], + prev => { + return [ + { + group, + display: message.text ?? 'Image', + lastMessageTime: Date.now(), + isRequest: false, + }, + ...(prev ?? []), + ]; + }, + ); + }) + .catch(err => { + Alert.alert('Error sending message', err.message); + // console.log('error on new', err); + }) + .finally(() => { + replace(ScreenNames.Group, {id: group.id}); + }); + }) + .catch(err => { + Alert.alert('Error creating group', err.message); + }); + } catch (error: any) { + Alert.alert( + 'An Error has occurred', + (typeof error === 'object' && 'message' in error && error?.message) || + '', + ); } - client?.conversations - ?.newGroup(addresses) - .then(group => { - // The client is not notified of a group they create, so we add it to the list here - group - .send(message as {text: string}) - .then(() => { - queryClient.setQueryData( - [QueryKeys.List, client?.address], - prev => { - return [ - { - group, - display: message.text ?? 'Image', - lastMessageTime: Date.now(), - isRequest: false, - }, - ...(prev ?? []), - ]; - }, - ); - }) - .finally(() => { - replace(ScreenNames.Group, {id: group.id}); - }); - }) - .catch(err => { - console.log('error on new', err); - }); }, [addresses, client, queryClient, replace], ); @@ -82,7 +97,11 @@ export const NewConversationScreen = () => { /> )} - + + + ); diff --git a/src/screens/OnboardingConnectWalletScreen.tsx b/src/screens/OnboardingConnectWalletScreen.tsx index 1ffceb9..04b8d03 100644 --- a/src/screens/OnboardingConnectWalletScreen.tsx +++ b/src/screens/OnboardingConnectWalletScreen.tsx @@ -1,4 +1,3 @@ -import {BlurView} from '@react-native-community/blur'; import { WalletConfig, // coinbaseWallet, @@ -8,10 +7,11 @@ import { useConnect, walletConnect, } from '@thirdweb-dev/react-native'; -import {VStack} from 'native-base'; +import {Box, VStack} from 'native-base'; import React, {useCallback, useEffect, useState} from 'react'; -import {Alert, Image, Linking} from 'react-native'; +import {Alert, Image, Linking, StyleSheet} from 'react-native'; import Config from 'react-native-config'; +import LinearGradient from 'react-native-linear-gradient'; import {WalletOptionButton} from '../components/WalletOptionButton'; import {Button} from '../components/common/Button'; import {Icon} from '../components/common/Icon'; @@ -68,38 +68,60 @@ export const OnboardingConnectWalletScreen = () => { return ( <> - - - + + + + + - + paddingX={'24px'} + paddingTop={'80px'}> + {translate('your_interoperable_web3_inbox')} - + {translate( 'youre_just_a_few_steps_away_from_secure_wallet_to_wallet_messaging', )} <> + + {translate('no_private_keys_will_be_shared')} + @@ -159,3 +181,17 @@ export const OnboardingConnectWalletScreen = () => { ); }; + +const styles = StyleSheet.create({ + image: { + justifyContent: 'center', + alignItems: 'center', + width: '100%', + }, + linearGradient: { + width: '100%', + height: 125, + position: 'absolute', + top: 500, + }, +}); diff --git a/src/screens/OnboardingEnableIdentityScreen.tsx b/src/screens/OnboardingEnableIdentityScreen.tsx index 8f01060..609d00f 100644 --- a/src/screens/OnboardingEnableIdentityScreen.tsx +++ b/src/screens/OnboardingEnableIdentityScreen.tsx @@ -1,8 +1,8 @@ import {useDisconnect, useSigner} from '@thirdweb-dev/react-native'; import {Client} from '@xmtp/react-native-sdk'; -import {VStack} from 'native-base'; -import React, {useCallback, useEffect, useState} from 'react'; -import {Alert, DeviceEventEmitter, Image} from 'react-native'; +import {StatusBar, VStack} from 'native-base'; +import {useCallback, useEffect, useState} from 'react'; +import {Alert, DeviceEventEmitter, Image, Platform} from 'react-native'; import {Button} from '../components/common/Button'; import {Icon} from '../components/common/Icon'; import {Screen} from '../components/common/Screen'; @@ -15,6 +15,7 @@ import {useTypedNavigation} from '../hooks/useTypedNavigation'; import {translate} from '../i18n'; import {ScreenNames} from '../navigation/ScreenNames'; import {saveClientKeys} from '../services/encryptedStorage'; +import {PushNotificatons} from '../services/pushNotifications'; import {colors} from '../theme/colors'; type Step = 'CREATE_IDENTITY' | 'ENABLE_IDENTITY'; @@ -54,6 +55,11 @@ export const OnboardingEnableIdentityScreen = () => { return; } try { + // const keyBytes = new Uint8Array([ + // 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, + // 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, + // 135, 145, + // ]); const client = await Client.create(signer, { enableAlphaMls: true, env: AppConfig.XMTP_ENV, @@ -65,9 +71,14 @@ export const OnboardingEnableIdentityScreen = () => { await createIdentityPromise(); }, codecs: supportedContentTypes, + // dbEncryptionKey: keyBytes, }); + if (Platform.OS !== 'android') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _ = new PushNotificatons(client); + } const keys = await client.exportKeyBundle(); - const address = await signer.getAddress(); + const address = client.address; saveClientKeys(address as `0x${string}`, keys); setClient(client); } catch (e: any) { @@ -92,10 +103,11 @@ export const OnboardingEnableIdentityScreen = () => { }, [navigate, disconnect]); return ( - + +