diff --git a/app/mobile/App.js b/app/mobile/App.js index 3f6e4b6d..3a8f3215 100644 --- a/app/mobile/App.js +++ b/app/mobile/App.js @@ -7,6 +7,7 @@ import { Access } from 'src/access/Access'; import { Session } from 'src/session/Session'; import { Admin } from 'src/admin/Admin'; import { StoreContextProvider } from 'context/StoreContext'; +import { UploadContextProvider } from 'context/UploadContext'; import { AppContextProvider } from 'context/AppContext'; import { AccountContextProvider } from 'context/AccountContext'; import { ProfileContextProvider } from 'context/ProfileContext'; @@ -14,33 +15,42 @@ import { CardContextProvider } from 'context/CardContext'; import { ChannelContextProvider } from 'context/ChannelContext'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import { NavigationContainer } from '@react-navigation/native'; +import { ConversationContextProvider } from 'context/ConversationContext'; +import { LogBox } from 'react-native'; + +// silence warning: Sending `onAnimatedValueUpdate` with no listeners registered +//LogBox.ignoreLogs(['Sending']); export default function App() { return ( - - - - - - - - - } /> - } /> - } /> - } /> - } /> - } /> - - - - - - - - + + + + + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + + + + + ); } diff --git a/app/mobile/ios/Databag.xcodeproj/project.pbxproj b/app/mobile/ios/Databag.xcodeproj/project.pbxproj index 546293b5..a1032b72 100644 --- a/app/mobile/ios/Databag.xcodeproj/project.pbxproj +++ b/app/mobile/ios/Databag.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 7B93995628F163340002722F /* SplashScreen.storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B93995528F163330002722F /* SplashScreen.storyboard.storyboard */; }; 96905EF65AED1B983A6B3ABC /* libPods-Databag.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Databag.a */; }; B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; @@ -27,6 +28,7 @@ 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Databag.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Databag.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6C2E3173556A471DD304B334 /* Pods-Databag.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Databag.debug.xcconfig"; path = "Target Support Files/Pods-Databag/Pods-Databag.debug.xcconfig"; sourceTree = ""; }; 7A4D352CD337FB3A3BF06240 /* Pods-Databag.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Databag.release.xcconfig"; path = "Target Support Files/Pods-Databag/Pods-Databag.release.xcconfig"; sourceTree = ""; }; + 7B93995528F163330002722F /* SplashScreen.storyboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard.storyboard; path = Databag/SplashScreen.storyboard.storyboard; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Databag/SplashScreen.storyboard; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -56,6 +58,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + 7B93995528F163330002722F /* SplashScreen.storyboard.storyboard */, ); name = Databag; sourceTree = ""; @@ -164,8 +167,10 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1130; + ORGANIZATIONNAME = CoreDB; TargetAttributes = { 13B07F861A680F5B00A75B9A = { + DevelopmentTeam = 3P65PQ7SUR; LastSwiftMigration = 1250; }; }; @@ -196,6 +201,7 @@ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + 7B93995628F163340002722F /* SplashScreen.storyboard.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -302,26 +308,35 @@ baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-Databag.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 7; + DEVELOPMENT_TEAM = 3P65PQ7SUR; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "FB_SONARKIT_ENABLED=1", ); INFOPLIST_FILE = Databag/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Databag; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = org.name.Databag; + PRODUCT_BUNDLE_IDENTIFIER = org.coredb.databag; PRODUCT_NAME = Databag; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -331,20 +346,29 @@ baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-Databag.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 7; + DEVELOPMENT_TEAM = 3P65PQ7SUR; INFOPLIST_FILE = Databag/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Databag; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = org.name.Databag; + PRODUCT_BUNDLE_IDENTIFIER = org.coredb.databag; PRODUCT_NAME = Databag; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/app/mobile/ios/Databag.xcworkspace/contents.xcworkspacedata b/app/mobile/ios/Databag.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..e96d4877 --- /dev/null +++ b/app/mobile/ios/Databag.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/mobile/ios/Databag.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/mobile/ios/Databag.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/app/mobile/ios/Databag.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/1024.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..d8e937d4 Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/1024.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/120 1.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/120 1.png new file mode 100644 index 00000000..88daaf6e Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/120 1.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/120.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..88daaf6e Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/120.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/180.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..596d579a Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/180.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/40.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..8abae017 Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/40.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/58.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..60a483a5 Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/58.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/60.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..bc35c3d6 Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/60.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/80.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..d33168bf Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/80.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/87.png b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..fc8f4dcd Binary files /dev/null and b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/87.png differ diff --git a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/Contents.json b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/Contents.json index bf722cb9..ad54b31b 100644 --- a/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,38 +1,62 @@ { "images" : [ { + "filename" : "40.png", "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { + "filename" : "60.png", "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" + "scale" : "3x", + "size" : "20x20" }, { + "filename" : "58.png", "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { + "filename" : "87.png", "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" + "scale" : "3x", + "size" : "29x29" }, { + "filename" : "80.png", "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { + "filename" : "120.png", "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "120 1.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "expo" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/app/mobile/ios/Databag/Info.plist b/app/mobile/ios/Databag/Info.plist index bc4176fb..327c7b9a 100644 --- a/app/mobile/ios/Databag/Info.plist +++ b/app/mobile/ios/Databag/Info.plist @@ -14,16 +14,14 @@ $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleSignature - ???? CFBundleShortVersionString 1.0 + CFBundleSignature + ???? CFBundleVersion 1 LSRequiresIPhoneOS - NSPhotoLibraryUsageDescription - Used to set profile image and post photos NSAppTransportSecurity NSAllowsArbitraryLoads @@ -37,12 +35,18 @@ + NSMicrophoneUsageDescription + Required for build but not used + NSPhotoLibraryUsageDescription + Used to set profile image and post photos UILaunchStoryboardName - SplashScreen + SplashScreen.storyboard UIRequiredDeviceCapabilities armv7 + UIStatusBarStyle + UIStatusBarStyleDefault UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -51,7 +55,5 @@ UIViewControllerBasedStatusBarAppearance - UIStatusBarStyle - UIStatusBarStyleDefault diff --git a/app/mobile/ios/Databag/SplashScreen.storyboard.storyboard b/app/mobile/ios/Databag/SplashScreen.storyboard.storyboard new file mode 100644 index 00000000..24eec3df --- /dev/null +++ b/app/mobile/ios/Databag/SplashScreen.storyboard.storyboard @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/mobile/ios/Podfile.lock b/app/mobile/ios/Podfile.lock index e4f6a2d6..90948826 100644 --- a/app/mobile/ios/Podfile.lock +++ b/app/mobile/ios/Podfile.lock @@ -3,6 +3,10 @@ PODS: - DoubleConversion (1.1.6) - EXApplication (4.2.2): - ExpoModulesCore + - EXAV (12.0.4): + - ExpoModulesCore + - React-runtimeexecutor + - ReactCommon - EXConstants (13.2.4): - ExpoModulesCore - EXFileSystem (14.1.0): @@ -238,6 +242,8 @@ PODS: - React-jsinspector (0.69.5) - React-logger (0.69.5): - glog + - react-native-document-picker (8.1.1): + - React-Core - react-native-safe-area-context (4.3.3): - RCT-Folly - RCTRequired @@ -246,6 +252,11 @@ PODS: - ReactCommon/turbomodule/core - react-native-sqlite-storage (6.0.1): - React-Core + - react-native-video (5.2.1): + - React-Core + - react-native-video/Video (= 5.2.1) + - react-native-video/Video (5.2.1): + - React-Core - React-perflogger (0.69.5) - React-RCTActionSheet (0.69.5): - React-Core/RCTActionSheetHeaders (= 0.69.5) @@ -301,6 +312,25 @@ PODS: - ReactCommon/turbomodule/core (= 0.69.5) - React-runtimeexecutor (0.69.5): - React-jsi (= 0.69.5) + - ReactCommon (0.69.5): + - React-logger (= 0.69.5) + - ReactCommon/react_debug_core (= 0.69.5) + - ReactCommon/turbomodule (= 0.69.5) + - ReactCommon/react_debug_core (0.69.5): + - React-logger (= 0.69.5) + - ReactCommon/turbomodule (0.69.5): + - DoubleConversion + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-bridging (= 0.69.5) + - React-callinvoker (= 0.69.5) + - React-Core (= 0.69.5) + - React-cxxreact (= 0.69.5) + - React-jsi (= 0.69.5) + - React-logger (= 0.69.5) + - React-perflogger (= 0.69.5) + - ReactCommon/turbomodule/core (= 0.69.5) + - ReactCommon/turbomodule/samples (= 0.69.5) - ReactCommon/turbomodule/core (0.69.5): - DoubleConversion - glog @@ -312,7 +342,19 @@ PODS: - React-jsi (= 0.69.5) - React-logger (= 0.69.5) - React-perflogger (= 0.69.5) - - RNGestureHandler (2.6.1): + - ReactCommon/turbomodule/samples (0.69.5): + - DoubleConversion + - glog + - RCT-Folly (= 2021.06.28.00-v2) + - React-bridging (= 0.69.5) + - React-callinvoker (= 0.69.5) + - React-Core (= 0.69.5) + - React-cxxreact (= 0.69.5) + - React-jsi (= 0.69.5) + - React-logger (= 0.69.5) + - React-perflogger (= 0.69.5) + - ReactCommon/turbomodule/core (= 0.69.5) + - RNGestureHandler (2.7.0): - React-Core - RNImageCropPicker (0.38.0): - React-Core @@ -350,6 +392,8 @@ PODS: - React-RCTText - ReactCommon/turbomodule/core - Yoga + - RNSoundPlayer (0.13.2): + - React-Core - TOCropViewController (2.6.0) - Yoga (1.14.0) @@ -357,6 +401,7 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXApplication (from `../node_modules/expo-application/ios`) + - EXAV (from `../node_modules/expo-av/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) - EXFileSystem (from `../node_modules/expo-file-system/ios`) - EXFont (from `../node_modules/expo-font/ios`) @@ -382,8 +427,10 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`) + - react-native-video (from `../node_modules/react-native-video`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -399,6 +446,7 @@ DEPENDENCIES: - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNSoundPlayer (from `../node_modules/react-native-sound-player`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -413,6 +461,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EXApplication: :path: "../node_modules/expo-application/ios" + EXAV: + :path: "../node_modules/expo-av/ios" EXConstants: :path: "../node_modules/expo-constants/ios" EXFileSystem: @@ -461,10 +511,14 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" + react-native-document-picker: + :path: "../node_modules/react-native-document-picker" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-sqlite-storage: :path: "../node_modules/react-native-sqlite-storage" + react-native-video: + :path: "../node_modules/react-native-video" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -495,6 +549,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-image-crop-picker" RNReanimated: :path: "../node_modules/react-native-reanimated" + RNSoundPlayer: + :path: "../node_modules/react-native-sound-player" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -502,6 +558,7 @@ SPEC CHECKSUMS: boost: a7c83b31436843459a1961bfd74b96033dc77234 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 EXApplication: e418d737a036e788510f2c4ad6c10a7d54d18586 + EXAV: 596506c9bee54ad52f2f3b625cdaeb9d9f2dd6b7 EXConstants: 7c44785d41d8e959d527d23d29444277a4d1ee73 EXFileSystem: 927e0a8885aa9c49e50fc38eaba2c2389f2f1019 EXFont: a5d80bd9b3452b2d5abbce2487da89b0150e6487 @@ -527,8 +584,10 @@ SPEC CHECKSUMS: React-jsiexecutor: e42f0b46de293a026c2fb20e524d4fe09f81f575 React-jsinspector: e385fb7a1440ae3f3b2cd1a139ca5aadaab43c10 React-logger: 15c734997c06fe9c9b88e528fb7757601e7a56df + react-native-document-picker: f68191637788994baed5f57d12994aa32cf8bf88 react-native-safe-area-context: b456e1c40ec86f5593d58b275bd0e9603169daca react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261 + react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 React-perflogger: 367418425c5e4a9f0f80385ee1eaacd2a7348f8e React-RCTActionSheet: e4885e7136f98ded1137cd3daccc05eaed97d5a6 React-RCTAnimation: 7c5a74f301c9b763343ba98a3dd776ed2676993f @@ -541,9 +600,10 @@ SPEC CHECKSUMS: React-RCTVibration: 42b34fde72e42446d9b08d2b9a3ddc2fa9ac6189 React-runtimeexecutor: c778439c3c430a5719d027d3c67423b390a221fe ReactCommon: ab1003b81be740fecd82509c370a45b1a7dda0c1 - RNGestureHandler: 28ad20bf02257791f7f137b31beef34b9549f54b + RNGestureHandler: 7673697e7c0e9391adefae4faa087442bc04af33 RNImageCropPicker: ffbba608264885c241cbf3a8f78eb7aeeb978241 RNReanimated: 7faa787e8d4493fbc95fab2ad331fa7625828cfa + RNSoundPlayer: 369105c565b8fe6ea0a43fc882dc81eba444e842 TOCropViewController: 3105367e808b7d3d886a74ff59bf4804e7d3ab38 Yoga: c2b1f2494060865ac1f27e49639e72371b1205fa diff --git a/app/mobile/package.json b/app/mobile/package.json index 8a807c81..65422f68 100644 --- a/app/mobile/package.json +++ b/app/mobile/package.json @@ -13,20 +13,30 @@ "@react-navigation/drawer": "^6.5.0", "@react-navigation/native": "^6.0.13", "@react-navigation/stack": "^6.3.0", + "@stream-io/flat-list-mvcp": "^0.10.2", + "axios": "^1.1.0", "expo": "~46.0.9", + "expo-av": "^12.0.4", "expo-splash-screen": "~0.16.2", "expo-status-bar": "~1.4.0", + "moment": "^2.29.4", "react": "18.0.0", "react-dom": "18.0.0", "react-native": "0.69.5", "react-native-base64": "^0.2.1", - "react-native-gesture-handler": "^2.6.1", + "react-native-document-picker": "^8.1.1", + "react-native-gesture-handler": "^2.7.0", "react-native-image-crop-picker": "^0.38.0", "react-native-reanimated": "^2.10.0", "react-native-safe-area-context": "^4.3.3", "react-native-safe-area-view": "^1.1.1", + "react-native-snap-carousel": "4.0.0-beta.6", + "react-native-sound-player": "^0.13.2", "react-native-sqlite-storage": "^6.0.1", + "react-native-swipe-gestures": "^1.0.5", + "react-native-video": "^5.2.1", "react-native-web": "~0.18.7", + "react-native-wheel-color-picker": "^1.2.0", "react-router-dom": "6", "react-router-native": "^6.3.0" }, diff --git a/app/mobile/src/access/create/Create.jsx b/app/mobile/src/access/create/Create.jsx index f6a22ac6..854bfc24 100644 --- a/app/mobile/src/access/create/Create.jsx +++ b/app/mobile/src/access/create/Create.jsx @@ -39,7 +39,7 @@ export function Create() { + autoCorrect={false} autoCapitalize="none" placeholder="server" /> { (!state.server || !state.serverChecked) && ( @@ -57,7 +57,7 @@ export function Create() { + autoCorrect={false} autoCapitalize="none" placeholder="token" /> { (!validServer || !state.token || !state.tokenChecked) && ( @@ -75,7 +75,7 @@ export function Create() { + autoCorrect={false} autoCapitalize="none" placeholder="username" /> { (!validServer || !validToken || !state.username || !state.usernameChecked) && ( @@ -92,7 +92,7 @@ export function Create() { + autoCorrect={false} autoCapitalize="none" placeholder="password" /> @@ -102,7 +102,7 @@ export function Create() { + autoCorrect={false} secureTextEntry={true} autoCapitalize="none" placeholder="password" /> @@ -112,7 +112,7 @@ export function Create() { + autoCorrect={false} autoCapitalize="none" placeholder="confirm password" /> @@ -122,7 +122,7 @@ export function Create() { + autoCorrect={false} secureTextEntry={true} autoCapitalize="none" placeholder="confirm password" /> diff --git a/app/mobile/src/access/login/Login.jsx b/app/mobile/src/access/login/Login.jsx index 58623476..739649ae 100644 --- a/app/mobile/src/access/login/Login.jsx +++ b/app/mobile/src/access/login/Login.jsx @@ -35,14 +35,14 @@ export function Login() { + autoCorrect={false} autoCapitalize="none" placeholder="username@server" /> { state.showPassword && ( + autoCorrect={false} autoCapitalize="none" placeholder="password"/> @@ -52,7 +52,7 @@ export function Login() { + autoCorrect={false} secureTextEntry={true} autoCapitalize="none" placeholder="password" /> diff --git a/app/mobile/src/api/addCard.js b/app/mobile/src/api/addCard.js index 8109c94b..684a8238 100644 --- a/app/mobile/src/api/addCard.js +++ b/app/mobile/src/api/addCard.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function addCard(token, message) { - let card = await fetchWithTimeout(`/contact/cards?agent=${token}`, { method: 'POST', body: JSON.stringify(message)} ); +export async function addCard(server, token, message) { + let card = await fetchWithTimeout(`https://${server}/contact/cards?agent=${token}`, { method: 'POST', body: JSON.stringify(message)} ); checkResponse(card); return await card.json(); } diff --git a/app/mobile/src/api/addChannel.js b/app/mobile/src/api/addChannel.js index 46a81725..a9ebb69b 100644 --- a/app/mobile/src/api/addChannel.js +++ b/app/mobile/src/api/addChannel.js @@ -1,9 +1,9 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function addChannel(token, cards, subject, description ) { +export async function addChannel(server, token, cards, subject, description ) { let data = { subject, description }; let params = { dataType: 'superbasic', data: JSON.stringify(data), groups: [], cards }; - let channel = await fetchWithTimeout(`/content/channels?agent=${token}`, { method: 'POST', body: JSON.stringify(params)} ); + let channel = await fetchWithTimeout(`https://${server}/content/channels?agent=${token}`, { method: 'POST', body: JSON.stringify(params)} ); checkResponse(channel); return await channel.json(); } diff --git a/app/mobile/src/api/addChannelTopic.js b/app/mobile/src/api/addChannelTopic.js index 69f76d2a..2d5102a4 100644 --- a/app/mobile/src/api/addChannelTopic.js +++ b/app/mobile/src/api/addChannelTopic.js @@ -1,9 +1,9 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function addChannelTopic(token, channelId, message, assets ): string { +export async function addChannelTopic(server, token, channelId, message, assets ): string { if (message == null && (assets == null || assets.length === 0)) { - let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}`, + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?agent=${token}`, { method: 'POST', body: JSON.stringify({}) }); checkResponse(topic); let slot = await topic.json(); @@ -14,7 +14,7 @@ export async function addChannelTopic(token, channelId, message, assets ): strin if (value !== null) return value }), datatype: 'superbasictopic' }; - let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}&confirm=true`, + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?agent=${token}&confirm=true`, { method: 'POST', body: JSON.stringify(subject) }); checkResponse(topic); let slot = await topic.json(); @@ -22,7 +22,7 @@ export async function addChannelTopic(token, channelId, message, assets ): strin } else { - let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}`, + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?agent=${token}`, { method: 'POST', body: JSON.stringify({}) }); checkResponse(topic); let slot = await topic.json(); @@ -34,7 +34,7 @@ export async function addChannelTopic(token, channelId, message, assets ): strin const formData = new FormData(); formData.append('asset', asset.image); let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "icopy;photo"])); - let topicAsset = await fetch(`/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&agent=${token}`, { method: 'POST', body: formData }); + let topicAsset = await fetch(`https://${server}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&agent=${token}`, { method: 'POST', body: formData }); checkResponse(topicAsset); let assetEntry = await topicAsset.json(); message.assets.push({ @@ -49,7 +49,7 @@ export async function addChannelTopic(token, channelId, message, assets ): strin formData.append('asset', asset.video); let thumb = 'vthumb;video;' + asset.position; let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb])); - let topicAsset = await fetch(`/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&agent=${token}`, { method: 'POST', body: formData }); + let topicAsset = await fetch(`https://${server}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&agent=${token}`, { method: 'POST', body: formData }); checkResponse(topicAsset); let assetEntry = await topicAsset.json(); message.assets.push({ @@ -64,7 +64,7 @@ export async function addChannelTopic(token, channelId, message, assets ): strin const formData = new FormData(); formData.append('asset', asset.audio); let transform = encodeURIComponent(JSON.stringify(["acopy;audio"])); - let topicAsset = await fetch(`/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&agent=${token}`, { method: 'POST', body: formData }); + let topicAsset = await fetch(`https://${server}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&agent=${token}`, { method: 'POST', body: formData }); checkResponse(topicAsset); let assetEntry = await topicAsset.json(); message.assets.push({ @@ -80,11 +80,11 @@ export async function addChannelTopic(token, channelId, message, assets ): strin if (value !== null) return value }), datatype: 'superbasictopic' }; - let unconfirmed = await fetchWithTimeout(`/content/channels/${channelId}/topics/${slot.id}/subject?agent=${token}`, + let unconfirmed = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${slot.id}/subject?agent=${token}`, { method: 'PUT', body: JSON.stringify(subject) }); checkResponse(unconfirmed); - let confirmed = await fetchWithTimeout(`/content/channels/${channelId}/topics/${slot.id}/confirmed?agent=${token}`, + let confirmed = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${slot.id}/confirmed?agent=${token}`, { method: 'PUT', body: JSON.stringify('confirmed') }); checkResponse(confirmed); return slot.id; diff --git a/app/mobile/src/api/addContactChannelTopic.js b/app/mobile/src/api/addContactChannelTopic.js index 8e962b88..205cb751 100644 --- a/app/mobile/src/api/addContactChannelTopic.js +++ b/app/mobile/src/api/addContactChannelTopic.js @@ -1,13 +1,8 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function addContactChannelTopic(server, token, channelId, message, assets ) { - let host = ""; - if (server) { - host = `https://${server}` - } - if (message == null && (assets == null || assets.length === 0)) { - let topic = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics?contact=${token}`, + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?contact=${token}`, { method: 'POST', body: JSON.stringify({}) }); checkResponse(topic); let slot = await topic.json(); @@ -18,14 +13,14 @@ export async function addContactChannelTopic(server, token, channelId, message, if (value !== null) return value }), datatype: 'superbasictopic' }; - let topic = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics?contact=${token}&confirm=true`, + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?contact=${token}&confirm=true`, { method: 'POST', body: JSON.stringify(subject) }); checkResponse(topic); let slot = await topic.json(); return slot.id; } else { - let topic = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics?contact=${token}`, + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?contact=${token}`, { method: 'POST', body: JSON.stringify({}) }); checkResponse(topic); let slot = await topic.json(); @@ -37,7 +32,7 @@ export async function addContactChannelTopic(server, token, channelId, message, const formData = new FormData(); formData.append('asset', asset.image); let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "icopy;photo"])); - let topicAsset = await fetch(`${host}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&contact=${token}`, { method: 'POST', body: formData }); + let topicAsset = await fetch(`https://${server}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&contact=${token}`, { method: 'POST', body: formData }); checkResponse(topicAsset); let assetEntry = await topicAsset.json(); message.assets.push({ @@ -52,7 +47,7 @@ export async function addContactChannelTopic(server, token, channelId, message, formData.append('asset', asset.video); let thumb = "vthumb;video;" + asset.position let transform = encodeURIComponent(JSON.stringify(["vhd;video", "vlq;video", thumb])); - let topicAsset = await fetch(`${host}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&contact=${token}`, { method: 'POST', body: formData }); + let topicAsset = await fetch(`https://${server}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&contact=${token}`, { method: 'POST', body: formData }); checkResponse(topicAsset); let assetEntry = await topicAsset.json(); message.assets.push({ @@ -67,7 +62,7 @@ export async function addContactChannelTopic(server, token, channelId, message, const formData = new FormData(); formData.append('asset', asset.audio); let transform = encodeURIComponent(JSON.stringify(["acopy;audio"])); - let topicAsset = await fetch(`${host}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&contact=${token}`, { method: 'POST', body: formData }); + let topicAsset = await fetch(`https://${server}/content/channels/${channelId}/topics/${slot.id}/assets?transforms=${transform}&contact=${token}`, { method: 'POST', body: formData }); checkResponse(topicAsset); let assetEntry = await topicAsset.json(); message.assets.push({ @@ -83,11 +78,11 @@ export async function addContactChannelTopic(server, token, channelId, message, if (value !== null) return value }), datatype: 'superbasictopic' }; - let unconfirmed = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics/${slot.id}/subject?contact=${token}`, + let unconfirmed = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${slot.id}/subject?contact=${token}`, { method: 'PUT', body: JSON.stringify(subject) }); checkResponse(unconfirmed); - let confirmed = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics/${slot.id}/confirmed?contact=${token}`, + let confirmed = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${slot.id}/confirmed?contact=${token}`, { method: 'PUT', body: JSON.stringify('confirmed') }); checkResponse(confirmed); return slot.id; diff --git a/app/mobile/src/api/clearChannelCard.js b/app/mobile/src/api/clearChannelCard.js index 7e182fe9..c67d042f 100644 --- a/app/mobile/src/api/clearChannelCard.js +++ b/app/mobile/src/api/clearChannelCard.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function clearChannelCard(token, channelId, cardId ) { - let channel = await fetchWithTimeout(`/content/channels/${channelId}/cards/${cardId}?agent=${token}`, {method: 'DELETE'}); +export async function clearChannelCard(server, token, channelId, cardId ) { + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/cards/${cardId}?agent=${token}`, {method: 'DELETE'}); checkResponse(channel); return await channel.json(); } diff --git a/app/mobile/src/api/getCardCloseMessage.js b/app/mobile/src/api/getCardCloseMessage.js index a769c2f0..0363f027 100644 --- a/app/mobile/src/api/getCardCloseMessage.js +++ b/app/mobile/src/api/getCardCloseMessage.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getCardCloseMessage(token, cardId) { - let message = await fetchWithTimeout(`/contact/cards/${cardId}/closeMessage?agent=${token}`, { method: 'GET' }); +export async function getCardCloseMessage(server, token, cardId) { + let message = await fetchWithTimeout(`https://${server}/contact/cards/${cardId}/closeMessage?agent=${token}`, { method: 'GET' }); checkResponse(message); return await message.json(); } diff --git a/app/mobile/src/api/getCardOpenMessage.js b/app/mobile/src/api/getCardOpenMessage.js index 130a6d89..24705605 100644 --- a/app/mobile/src/api/getCardOpenMessage.js +++ b/app/mobile/src/api/getCardOpenMessage.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getCardOpenMessage(token, cardId) { - let message = await fetchWithTimeout(`/contact/cards/${cardId}/openMessage?agent=${token}`, { method: 'GET' }); +export async function getCardOpenMessage(server, token, cardId) { + let message = await fetchWithTimeout(`https://${server}/contact/cards/${cardId}/openMessage?agent=${token}`, { method: 'GET' }); checkResponse(message); return await message.json(); } diff --git a/app/mobile/src/api/getChannelTopic.js b/app/mobile/src/api/getChannelTopic.js index 550c73b4..73492778 100644 --- a/app/mobile/src/api/getChannelTopic.js +++ b/app/mobile/src/api/getChannelTopic.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getChannelTopic(token, channelId, topicId) { - let topic = await fetchWithTimeout(`/content/channels/${channelId}/topics/${topicId}/detail?agent=${token}`, +export async function getChannelTopic(server, token, channelId, topicId) { + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${topicId}/detail?agent=${token}`, { method: 'GET' }); checkResponse(topic) return await topic.json() diff --git a/app/mobile/src/api/getChannelTopicAssetUrl.js b/app/mobile/src/api/getChannelTopicAssetUrl.js index c20fb234..8cb7185a 100644 --- a/app/mobile/src/api/getChannelTopicAssetUrl.js +++ b/app/mobile/src/api/getChannelTopicAssetUrl.js @@ -1,4 +1,4 @@ -export function getChannelTopicAssetUrl(token, channelId, topicId, assetId) { - return `/content/channels/${channelId}/topics/${topicId}/assets/${assetId}?agent=${token}` +export function getChannelTopicAssetUrl(server, token, channelId, topicId, assetId) { + return `https://${server}/content/channels/${channelId}/topics/${topicId}/assets/${assetId}?agent=${token}` } diff --git a/app/mobile/src/api/getChannelTopics.js b/app/mobile/src/api/getChannelTopics.js index 69cdd9e4..ba9c5a2f 100644 --- a/app/mobile/src/api/getChannelTopics.js +++ b/app/mobile/src/api/getChannelTopics.js @@ -1,6 +1,6 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getChannelTopics(token, channelId, revision, count, begin, end) { +export async function getChannelTopics(server, token, channelId, revision, count, begin, end) { let rev = '' if (revision != null) { rev = `&revision=${revision}` @@ -17,7 +17,7 @@ export async function getChannelTopics(token, channelId, revision, count, begin, if (end != null) { edn = `&end=${end}` } - let topics = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}${rev}${cnt}${bgn}${edn}`, + let topics = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?agent=${token}${rev}${cnt}${bgn}${edn}`, { method: 'GET' }); checkResponse(topics) return { diff --git a/app/mobile/src/api/getContactChannelTopic.js b/app/mobile/src/api/getContactChannelTopic.js index 0715c122..9b921ee0 100644 --- a/app/mobile/src/api/getContactChannelTopic.js +++ b/app/mobile/src/api/getContactChannelTopic.js @@ -1,12 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function getContactChannelTopic(server, token, channelId, topicId) { - let host = ""; - if (server) { - host = `https://${server}`; - } - - let topic = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics/${topicId}/detail?contact=${token}`, + let topic = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${topicId}/detail?contact=${token}`, { method: 'GET' }); checkResponse(topic) return await topic.json() diff --git a/app/mobile/src/api/getContactChannelTopicAssetUrl.js b/app/mobile/src/api/getContactChannelTopicAssetUrl.js index ec531c2b..6051f74b 100644 --- a/app/mobile/src/api/getContactChannelTopicAssetUrl.js +++ b/app/mobile/src/api/getContactChannelTopicAssetUrl.js @@ -1,9 +1,4 @@ export function getContactChannelTopicAssetUrl(server, token, channelId, topicId, assetId) { - let host = ""; - if (server) { - host = `https://${server}`; - } - - return `${host}/content/channels/${channelId}/topics/${topicId}/assets/${assetId}?contact=${token}` + return `https://${server}/content/channels/${channelId}/topics/${topicId}/assets/${assetId}?contact=${token}` } diff --git a/app/mobile/src/api/getContactChannelTopics.js b/app/mobile/src/api/getContactChannelTopics.js index df5d621a..cf55dd36 100644 --- a/app/mobile/src/api/getContactChannelTopics.js +++ b/app/mobile/src/api/getContactChannelTopics.js @@ -1,11 +1,6 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function getContactChannelTopics(server, token, channelId, revision, count, begin, end) { - let host = ""; - if (server) { - host = `https://${server}`; - } - let rev = '' if (revision != null) { rev = `&revision=${revision}` @@ -22,7 +17,7 @@ export async function getContactChannelTopics(server, token, channelId, revision if (end != null) { edn = `&end=${end}` } - let topics = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics?contact=${token}${rev}${cnt}${bgn}${edn}`, + let topics = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?contact=${token}${rev}${cnt}${bgn}${edn}`, { method: 'GET' }); checkResponse(topics) return { diff --git a/app/mobile/src/api/getHandle.js b/app/mobile/src/api/getHandle.js new file mode 100644 index 00000000..4006df55 --- /dev/null +++ b/app/mobile/src/api/getHandle.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function getHandle(server, token, name) { + let available = await fetchWithTimeout(`https://${server}/account/username?agent=${token}&name=${encodeURIComponent(name)}`, { method: 'GET' }) + checkResponse(available) + return await available.json() +} + diff --git a/app/mobile/src/api/removeCard.js b/app/mobile/src/api/removeCard.js index 3effc5c8..2ba2d513 100644 --- a/app/mobile/src/api/removeCard.js +++ b/app/mobile/src/api/removeCard.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function removeCard(token, cardId) { - let card = await fetchWithTimeout(`/contact/cards/${cardId}?agent=${token}`, { method: 'DELETE' } ); +export async function removeCard(server, token, cardId) { + let card = await fetchWithTimeout(`https://${server}/contact/cards/${cardId}?agent=${token}`, { method: 'DELETE' } ); checkResponse(card); return await card.json(); } diff --git a/app/mobile/src/api/removeChannel.js b/app/mobile/src/api/removeChannel.js index 81cbd84a..836031be 100644 --- a/app/mobile/src/api/removeChannel.js +++ b/app/mobile/src/api/removeChannel.js @@ -1,8 +1,8 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function removeChannel(token, channelId) { +export async function removeChannel(server, token, channelId) { - let channel = await fetchWithTimeout(`/content/channels/${channelId}?agent=${token}`, + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}?agent=${token}`, { method: 'DELETE' }); checkResponse(channel); } diff --git a/app/mobile/src/api/removeChannelTopic.js b/app/mobile/src/api/removeChannelTopic.js index bfe0ece5..63504f5e 100644 --- a/app/mobile/src/api/removeChannelTopic.js +++ b/app/mobile/src/api/removeChannelTopic.js @@ -1,8 +1,8 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function removeChannelTopic(token, channelId, topicId) { +export async function removeChannelTopic(server, token, channelId, topicId) { - let channel = await fetchWithTimeout(`/content/channels/${channelId}/topics/${topicId}?agent=${token}`, + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${topicId}?agent=${token}`, { method: 'DELETE' }); checkResponse(channel); } diff --git a/app/mobile/src/api/removeContactChannel.js b/app/mobile/src/api/removeContactChannel.js index fc3c38c6..43b41e38 100644 --- a/app/mobile/src/api/removeContactChannel.js +++ b/app/mobile/src/api/removeContactChannel.js @@ -1,12 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function removeContactChannel(server, token, channelId) { - let host = ""; - if (server) { - host = `https://${server}`; - } - - let channel = await fetchWithTimeout(`${host}/content/channels/${channelId}?contact=${token}`, + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}?contact=${token}`, { method: 'DELETE' }); checkResponse(channel); } diff --git a/app/mobile/src/api/removeContactChannelTopic.js b/app/mobile/src/api/removeContactChannelTopic.js index ffd46a31..1178c330 100644 --- a/app/mobile/src/api/removeContactChannelTopic.js +++ b/app/mobile/src/api/removeContactChannelTopic.js @@ -1,12 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function removeContactChannelTopic(server, token, channelId, topicId) { - let host = ""; - if (server) { - host = `https://${server}`; - } - - let channel = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics/${topicId}?contact=${token}`, + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${topicId}?contact=${token}`, { method: 'DELETE' }); checkResponse(channel); } diff --git a/app/mobile/src/api/setCardCloseMessage.js b/app/mobile/src/api/setCardCloseMessage.js index f479224e..4e0a64b2 100644 --- a/app/mobile/src/api/setCardCloseMessage.js +++ b/app/mobile/src/api/setCardCloseMessage.js @@ -1,12 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function setCardCloseMessage(server, message) { - let host = ""; - if (server) { - host = `https://${server}`; - } - - let status = await fetchWithTimeout(`${host}/contact/closeMessage`, { method: 'PUT', body: JSON.stringify(message) }); + let status = await fetchWithTimeout(`https://${server}/contact/closeMessage`, { method: 'PUT', body: JSON.stringify(message) }); checkResponse(status); return await status.json(); } diff --git a/app/mobile/src/api/setCardOpenMessage.js b/app/mobile/src/api/setCardOpenMessage.js index f89bcd93..b9e39884 100644 --- a/app/mobile/src/api/setCardOpenMessage.js +++ b/app/mobile/src/api/setCardOpenMessage.js @@ -1,12 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function setCardOpenMessage(server, message) { - let host = ""; - if (server) { - host = `https://${server}`; - } - - let status = await fetchWithTimeout(`${host}/contact/openMessage`, { method: 'PUT', body: JSON.stringify(message) }); + let status = await fetchWithTimeout(`https://${server}/contact/openMessage`, { method: 'PUT', body: JSON.stringify(message) }); checkResponse(status); return await status.json(); } diff --git a/app/mobile/src/api/setCardStatus.js b/app/mobile/src/api/setCardStatus.js index 843bda2b..fde6cac4 100644 --- a/app/mobile/src/api/setCardStatus.js +++ b/app/mobile/src/api/setCardStatus.js @@ -1,19 +1,19 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function setCardConnecting(token, cardId) { - let card = await fetchWithTimeout(`/contact/cards/${cardId}/status?agent=${token}`, { method: 'PUT', body: JSON.stringify('connecting') } ); +export async function setCardConnecting(server, token, cardId) { + let card = await fetchWithTimeout(`https://${server}/contact/cards/${cardId}/status?agent=${token}`, { method: 'PUT', body: JSON.stringify('connecting') } ); checkResponse(card); return await card.json(); } -export async function setCardConnected(token, cardId, access, view, article, channel, profile) { - let card = await fetchWithTimeout(`/contact/cards/${cardId}/status?agent=${token}&token=${access}&viewRevision=${view}&articleRevision=${article}&channelRevision=${channel}&profileRevision=${profile}`, { method: 'PUT', body: JSON.stringify('connected') } ); +export async function setCardConnected(server, token, cardId, access, view, article, channel, profile) { + let card = await fetchWithTimeout(`https://${server}/contact/cards/${cardId}/status?agent=${token}&token=${access}&viewRevision=${view}&articleRevision=${article}&channelRevision=${channel}&profileRevision=${profile}`, { method: 'PUT', body: JSON.stringify('connected') } ); checkResponse(card); return await card.json(); } -export async function setCardConfirmed(token, cardId) { - let card = await fetchWithTimeout(`/contact/cards/${cardId}/status?agent=${token}`, { method: 'PUT', body: JSON.stringify('confirmed') } ); +export async function setCardConfirmed(server, token, cardId) { + let card = await fetchWithTimeout(`https://${server}/contact/cards/${cardId}/status?agent=${token}`, { method: 'PUT', body: JSON.stringify('confirmed') } ); checkResponse(card); return await card.json(); } diff --git a/app/mobile/src/api/setChannelCard.js b/app/mobile/src/api/setChannelCard.js index c9da3287..87acb271 100644 --- a/app/mobile/src/api/setChannelCard.js +++ b/app/mobile/src/api/setChannelCard.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function setChannelCard(token, channelId, cardId ) { - let channel = await fetchWithTimeout(`/content/channels/${channelId}/cards/${cardId}?agent=${token}`, {method: 'PUT'}); +export async function setChannelCard(server, token, channelId, cardId ) { + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/cards/${cardId}?agent=${token}`, {method: 'PUT'}); checkResponse(channel); return await channel.json(); } diff --git a/app/mobile/src/api/setChannelSubject.js b/app/mobile/src/api/setChannelSubject.js index 49c95e04..132056af 100644 --- a/app/mobile/src/api/setChannelSubject.js +++ b/app/mobile/src/api/setChannelSubject.js @@ -1,9 +1,9 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function setChannelSubject(token, channelId, subject ) { +export async function setChannelSubject(server, token, channelId, subject ) { let data = { subject }; let params = { dataType: 'superbasic', data: JSON.stringify(data) }; - let channel = await fetchWithTimeout(`/content/channels/${channelId}/subject?agent=${token}`, { method: 'PUT', body: JSON.stringify(params)} ); + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/subject?agent=${token}`, { method: 'PUT', body: JSON.stringify(params)} ); checkResponse(channel); return await channel.json(); } diff --git a/app/mobile/src/api/setChannelTopicSubject.js b/app/mobile/src/api/setChannelTopicSubject.js index 3ef43810..e59f7424 100644 --- a/app/mobile/src/api/setChannelTopicSubject.js +++ b/app/mobile/src/api/setChannelTopicSubject.js @@ -1,11 +1,11 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function setChannelTopicSubject(token, channelId, topicId, data) { +export async function setChannelTopicSubject(server, token, channelId, topicId, data) { let subject = { data: JSON.stringify(data, (key, value) => { if (value !== null) return value }), datatype: 'superbasictopic' }; - let channel = await fetchWithTimeout(`/content/channels/${channelId}/topics/${topicId}/subject?agent=${token}&confirm=true`, + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${topicId}/subject?agent=${token}&confirm=true`, { method: 'PUT', body: JSON.stringify(subject) }); checkResponse(channel); } diff --git a/app/mobile/src/api/setContactChannelTopicSubject.js b/app/mobile/src/api/setContactChannelTopicSubject.js index dce13224..21b886b5 100644 --- a/app/mobile/src/api/setContactChannelTopicSubject.js +++ b/app/mobile/src/api/setContactChannelTopicSubject.js @@ -1,16 +1,11 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; export async function setContactChannelTopicSubject(server, token, channelId, topicId, data) { - let host = ""; - if (server) { - host = `https://${server}`; - } - let subject = { data: JSON.stringify(data, (key, value) => { if (value !== null) return value }), datatype: 'superbasictopic' }; - let channel = await fetchWithTimeout(`${host}/content/channels/${channelId}/topics/${topicId}/subject?contact=${token}&confirm=true`, + let channel = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics/${topicId}/subject?contact=${token}&confirm=true`, { method: 'PUT', body: JSON.stringify(subject) }); checkResponse(channel); } diff --git a/app/mobile/src/constants/Colors.js b/app/mobile/src/constants/Colors.js index 4ea1cdc8..ff256256 100644 --- a/app/mobile/src/constants/Colors.js +++ b/app/mobile/src/constants/Colors.js @@ -1,6 +1,7 @@ export const Colors = { background: '#8fbea7', primary: '#448866', + titleBackground: '#f6f6f6', formBackground: '#f2f2f2', formFocus: '#f8f8f8', formHover: '#efefef', @@ -21,10 +22,10 @@ export const Colors = { itemDivider: '#eeeeee', - connected: '#44cc44', + connected: '#4488FF', connecting: '#dd88ff', - requested: '#4488ff', - pending: '#22aaaa', + requested: '#448844', + pending: '#ffaa22', confirmed: '#aaaaaa', error: '#ff4444', diff --git a/app/mobile/src/context/ConversationContext.js b/app/mobile/src/context/ConversationContext.js new file mode 100644 index 00000000..08cde892 --- /dev/null +++ b/app/mobile/src/context/ConversationContext.js @@ -0,0 +1,14 @@ +import { createContext } from 'react'; +import { useConversationContext } from './useConversationContext.hook'; + +export const ConversationContext = createContext({}); + +export function ConversationContextProvider({ children }) { + const { state, actions } = useConversationContext(); + return ( + + {children} + + ); +} + diff --git a/app/mobile/src/context/UploadContext.js b/app/mobile/src/context/UploadContext.js new file mode 100644 index 00000000..9ac1b2a8 --- /dev/null +++ b/app/mobile/src/context/UploadContext.js @@ -0,0 +1,14 @@ +import { createContext } from 'react'; +import { useUploadContext } from './useUploadContext.hook'; + +export const UploadContext = createContext({}); + +export function UploadContextProvider({ children }) { + const { state, actions } = useUploadContext(); + return ( + + {children} + + ); +} + diff --git a/app/mobile/src/context/useAppContext.hook.js b/app/mobile/src/context/useAppContext.hook.js index bd9ce601..995e89b8 100644 --- a/app/mobile/src/context/useAppContext.hook.js +++ b/app/mobile/src/context/useAppContext.hook.js @@ -22,7 +22,6 @@ export function useAppContext() { const card = useContext(CardContext); const channel = useContext(ChannelContext); - const delay = useRef(2); const ws = useRef(null); const updateState = (value) => { @@ -118,11 +117,8 @@ export function useAppContext() { ws.current.onopen = () => {} ws.current.onerror = () => {} setWebsocket(server, token); - if (delay.current < 15) { - delay.current += 1; - } } - }, delay.current * 1000) + }, 1000) } ws.current.onopen = () => { ws.current.send(JSON.stringify({ AppToken: token })) diff --git a/app/mobile/src/context/useCardContext.hook.js b/app/mobile/src/context/useCardContext.hook.js index 50353387..00c696db 100644 --- a/app/mobile/src/context/useCardContext.hook.js +++ b/app/mobile/src/context/useCardContext.hook.js @@ -1,20 +1,37 @@ import { useState, useRef, useContext } from 'react'; import { StoreContext } from 'context/StoreContext'; +import { UploadContext } from 'context/UploadContext'; import { getCards } from 'api/getCards'; import { getCardProfile } from 'api/getCardProfile'; import { getCardDetail } from 'api/getCardDetail'; import { getContactChannels } from 'api/getContactChannels'; -import { getContactChannelTopics } from 'api/getContactChannelTopics'; import { getContactChannelDetail } from 'api/getContactChannelDetail'; import { getContactChannelSummary } from 'api/getContactChannelSummary'; import { getCardImageUrl } from 'api/getCardImageUrl'; +import { addCard } from 'api/addCard'; +import { removeCard } from 'api/removeCard'; +import { setCardConnecting, setCardConnected, setCardConfirmed } from 'api/setCardStatus'; +import { getCardOpenMessage } from 'api/getCardOpenMessage'; +import { setCardOpenMessage } from 'api/setCardOpenMessage'; +import { getCardCloseMessage } from 'api/getCardCloseMessage'; +import { setCardCloseMessage } from 'api/setCardCloseMessage'; + +import { getContactChannelTopic } from 'api/getContactChannelTopic'; +import { getContactChannelTopics } from 'api/getContactChannelTopics'; +import { getContactChannelTopicAssetUrl } from 'api/getContactChannelTopicAssetUrl'; +import { addContactChannelTopic } from 'api/addContactChannelTopic'; +import { setContactChannelTopicSubject } from 'api/setContactChannelTopicSubject'; +import { removeContactChannel } from 'api/removeContactChannel'; +import { removeContactChannelTopic } from 'api/removeContactChannelTopic'; + export function useCardContext() { const [state, setState] = useState({ cards: new Map(), }); const store = useContext(StoreContext); + const upload = useContext(UploadContext); const session = useRef(null); const curRevision = useRef(null); @@ -27,6 +44,14 @@ export function useCardContext() { setState((s) => ({ ...s, ...value })) } + const getCard = (cardId) => { + const card = cards.current.get(cardId); + if (!card) { + throw new Error('cared not found'); + } + return card; + } + const setCard = (cardId, card) => { let updated = cards.current.get(cardId); if (updated == null) { @@ -76,6 +101,13 @@ export function useCardContext() { cards.current.set(cardId, card); } } + const setCardBlocked = (cardId, blocked) => { + let card = cards.current.get(cardId); + if (card) { + card.blocked = blocked; + cards.current.set(cardId, card); + } + } const clearCardChannels = (cardId) => { let card = cards.current.get(cardId); if (card) { @@ -147,6 +179,28 @@ export function useCardContext() { } } } + const setCardChannelSyncRevision = (cardId, channelId, revision) => { + let card = cards.current.get(cardId); + if (card) { + let channel = card.channels.get(channelId); + if (channel) { + channel.syncRevision = revision; + card.channels.set(channelId, channel); + cards.current.set(cardId, card); + } + } + } + const setCardChannelBlocked = (cardId, channelId, blocked) => { + let card = cards.current.get(cardId); + if (card) { + let channel = card.channels.get(channelId); + if (channel) { + channel.blocked = blocked; + card.channels.set(channelId, channel); + cards.current.set(cardId, card); + } + } + } const clearCardChannel = (cardId, channelId) => { let card = cards.current.get(cardId); if (card) { @@ -174,7 +228,6 @@ export function useCardContext() { else { const view = await store.actions.getCardItemView(guid, card.id); if (view == null) { - console.log('alert: expected card not synced'); let assembled = JSON.parse(JSON.stringify(card)); assembled.data.cardDetail = await getCardDetail(server, appToken, card.id); assembled.data.cardProfile = await getCardProfile(server, appToken, card.id); @@ -287,7 +340,7 @@ export function useCardContext() { await store.actions.setCardChannelItemSummary(guid, cardId, channel.id, topicRevision, summary); setCardChannelSummary(cardId, channel.id, summary, topicRevision); } - await store.actions.setCardChannelItemRevision(guid, cardId, channel.revision); + await store.actions.setCardChannelItemRevision(guid, cardId, channel.id, channel.revision); setCardChannelRevision(cardId, channel.id, channel.revision); } } @@ -325,7 +378,7 @@ export function useCardContext() { curRevision.current = rev; sync(); }, - setReadRevision: async (cardId, channelId, rev) => { + setChannelReadRevision: async (cardId, channelId, rev) => { await store.actions.setCardChannelItemReadRevision(session.current.guid, cardId, channelId, rev); setCardChannelReadRevision(cardId, channelId, rev); updateState({ cards: cards.current }); @@ -333,7 +386,143 @@ export function useCardContext() { getCardLogo: (cardId, revision) => { const { server, appToken } = session.current; return getCardImageUrl(server, appToken, cardId, revision); - } + }, + getByGuid: (guid) => { + let card; + cards.current.forEach((value, key, map) => { + if (value?.profile?.guid === guid) { + card = value; + } + }); + return card; + }, + addCard: async (message) => { + const { server, appToken } = session.current; + return await addCard(server, appToken, message); + }, + removeCard: async (cardId) => { + const { server, appToken } = session.current; + return await removeCard(server, appToken, cardId); + }, + setCardConnecting: async (cardId) => { + const { server, appToken } = session.current; + return await setCardConnecting(server, appToken, cardId); + }, + setCardConnected: async (cardId, token, rev) => { + const { server, appToken } = session.current; + return await setCardConnected(server, appToken, cardId, token, + rev.viewRevision, rev.articleRevision, rev.channelRevision, rev.profileRevision); + }, + setCardConfirmed: async (cardId) => { + const { server, appToken } = session.current; + return await setCardConfirmed(server, appToken, cardId); + }, + getCardOpenMessage: async (cardId) => { + const { server, appToken } = session.current; + return await getCardOpenMessage(server, appToken, cardId); + }, + setCardOpenMessage: async (server, message) => { + return await setCardOpenMessage(server, message); + }, + getCardCloseMessage: async (cardId) => { + const { server, appToken } = session.current; + return await getCardCloseMessage(server, appToken, cardId); + }, + setCardCloseMessage: async (server, message) => { + return await setCardCloseMessage(server, message); + }, + setCardBlocked: async (cardId) => { + const { guid } = session.current; + setCardBlocked(cardId, true); + await store.actions.setCardItemBlocked(guid, cardId); + updateState({ cards: cards.current }); + }, + clearCardBlocked: async (cardId) => { + const { guid } = session.current; + setCardBlocked(cardId, false); + await store.actions.clearCardItemBlocked(guid, cardId); + updateState({ cards: cards.current }); + }, + setSyncRevision: async (cardId, channelId, revision) => { + const { guid } = session.current; + await store.actions.setCardChannelItemSyncRevision(guid, cardId, channelId, revision); + setCardChannelSyncRevision(cardId, channelId, revision); + updateState({ cards: cards.current }); + }, + setChannelBlocked: async (cardId, channelId) => { + const { guid } = session.current; + await store.actions.setCardChannelItemBlocked(guid, cardId, channelId); + setCardChannelBlocked(cardId, channelId, true); + updateState({ cards: cards.current }); + }, + clearChannelBlocked: async (cardId, channelId) => { + const { guid } = session.current; + await store.actions.clearCardChannelItemBlocked(guid, cardId, channelId); + setCardChannelBlocked(cardId, channelId, false); + updateState({ cards: cards.current }); + }, + getChannelTopicItems: async (cardId, channelId) => { + const { guid } = session.current; + return await store.actions.getCardChannelTopicItems(guid, cardId, channelId); + }, + setChannelTopicItem: async (cardId, channelId, topicId, topic) => { + const { guid } = session.current; + return await store.actions.setCardChannelTopicItem(guid, cardId, channelId, topicId, topic); + }, + clearChannelTopicItem: async (cardId, channelId, topicId) => { + const { guid } = session.current; + return await store.actions.clearCardChannelTopicItem(guid, cardId, channelId, topicId); + }, + clearChannelTopicItems: async (cardId, channelId) => { + const { guid } = session.current; + return await store.actions.clearCardChannelTopicItems(guid, cardId, channelId); + }, + getChannelTopic: async (cardId, channelId, topicId) => { + const { detail, profile } = getCard(cardId); + return await getContactChannelTopic(profile.node, `${profile.guid}.${detail.token}`, channelId, topicId); + }, + getChannelTopics: async (cardId, channelId, revision) => { + const { detail, profile } = getCard(cardId); + return await getContactChannelTopics(profile.node, `${profile.guid}.${detail.token}`, channelId, revision); + }, + getChannelTopicAssetUrl: (cardId, channelId, topicId, assetId) => { + const { detail, profile } = getCard(cardId); + return getContactChannelTopicAssetUrl(profile.node, `${profile.guid}.${detail.token}`, channelId, topicId, assetId); + }, + addChannelTopic: async (cardId, channelId, message, files) => { + const { detail, profile } = getCard(cardId); + const node = profile.node; + const token = `${profile.guid}.${detail.token}`; + if (files?.length > 0) { + const topicId = await addContactChannelTopic(node, token, channelId, null, null); + upload.actions.addContactTopic(node, token, cardId, channelId, topicId, files, async (assets) => { + message.assets = assets; + await setContactChannelTopicSubject(node, token, channelId, topicId, message); + }, async () => { + try { + await removeContactChannelTopic(node, token, channelId, topicId); + } + catch (err) { + console.log(err); + } + }); + } + else { + await addContactChannelTopic(node, token, channelId, message, []); + } + }, + setChannelTopicSubject: async (cardId, channelId, topicId, data) => { + const { detail, profile } = getCard(cardId); + return await setContactChannelTopicSubject(profile.node, `${profile.guid}.${detail.token}`, channelId, topicId, data); + }, + removeChannel: async (cardId, channelId) => { + const { detail, profile } = getCard(cardId); + return await removeContactChannel(profile.node, `${profile.guid}.${detail.token}`, channelId); + }, + removeChannelTopic: async (cardId, channelId, topicId) => { + const { detail, profile } = getCard(cardId); + return await removeChannelTopic(profile.node, `${profile.guid}.${detail.token}`, channelId, topicId); + }, } return { state, actions } diff --git a/app/mobile/src/context/useChannelContext.hook.js b/app/mobile/src/context/useChannelContext.hook.js index 77412f52..65fb2a5b 100644 --- a/app/mobile/src/context/useChannelContext.hook.js +++ b/app/mobile/src/context/useChannelContext.hook.js @@ -1,14 +1,27 @@ import { useState, useRef, useContext } from 'react'; import { StoreContext } from 'context/StoreContext'; +import { UploadContext } from 'context/UploadContext'; import { getChannels } from 'api/getChannels'; import { getChannelDetail } from 'api/getChannelDetail'; import { getChannelSummary } from 'api/getChannelSummary'; +import { addChannel } from 'api/addChannel'; +import { removeChannel } from 'api/removeChannel'; +import { removeChannelTopic } from 'api/removeChannelTopic'; +import { setChannelTopicSubject } from 'api/setChannelTopicSubject'; +import { addChannelTopic } from 'api/addChannelTopic'; +import { getChannelTopics } from 'api/getChannelTopics'; +import { getChannelTopic } from 'api/getChannelTopic'; +import { getChannelTopicAssetUrl } from 'api/getChannelTopicAssetUrl'; +import { setChannelSubject } from 'api/setChannelSubject'; +import { setChannelCard } from 'api/setChannelCard'; +import { clearChannelCard } from 'api/clearChannelCard'; export function useChannelContext() { const [state, setState] = useState({ channels: new Map(), }); const store = useContext(StoreContext); + const upload = useContext(UploadContext); const session = useRef(null); const curRevision = useRef(null); @@ -33,7 +46,7 @@ export function useChannelContext() { update.topicRevision = channel?.data?.topicRevision; channels.current.set(channelId, update); } - const setChannelDetails = (channelId, detail, revision) => { + const setChannelDetail = (channelId, detail, revision) => { let channel = channels.current.get(channelId); if (channel) { channel.detail = detail; @@ -63,6 +76,20 @@ export function useChannelContext() { channels.current.set(channelId, channel); } } + const setChannelSyncRevision = (channelId, revision) => { + let channel = channels.current.get(channelId); + if (channel) { + channel.syncRevision = revision; + channels.current.set(channelId, channel); + } + } + const setChannelBlocked = (channelId, blocked) => { + let channel = channels.current.get(channelId); + if (channel) { + channel.blocked = blocked; + channels.current.set(channelId, channel); + } + } const sync = async () => { @@ -155,7 +182,97 @@ export function useChannelContext() { await store.actions.setChannelItemReadRevision(session.current.guid, channelId, rev); setChannelReadRevision(channelId, rev); updateState({ channels: channels.current }); - } + }, + setSyncRevision: async (channelId, revision) => { + const { guid } = session.current; + await store.actions.setChannelItemSyncRevision(guid, channelId, revision); + setChannelSyncRevision(channelId, revision); + updateState({ channels: channels.current }); + }, + setBlocked: async (channelId) => { + const { guid } = session.current; + await store.actions.setChannelItemBlocked(guid, channelId); + setChannelBlocked(channelId, 1); + updateState({ channels: channels.current }); + }, + clearBlocked: async (channelId) => { + const { guid } = session.current; + await store.actions.clearChannelItemBlocked(guid, channelId); + setChannelBlocked(channelId, 0); + updateState({ channels: channels.current }); + }, + getTopicItems: async (channelId) => { + const { guid } = session.current; + return await store.actions.getChannelTopicItems(guid, channelId); + }, + setTopicItem: async (channelId, topicId, topic) => { + const { guid } = session.current; + return await store.actions.setChannelTopicItem(guid, channelId, topicId, topic); + }, + clearTopicItem: async (channelId, topicId) => { + const { guid } = session.current; + return await store.actions.clearChannelTopicItem(guid, channelId, topicId); + }, + clearTopicItems: async (channelId) => { + const { guid } = session.current; + return await store.actions.clearChannelTopicItems(guid, channelId); + }, + getTopic: async (channelId, topicId) => { + const { server, appToken } = session.current; + return await getChannelTopic(server, appToken, channelId, topicId); + }, + getTopics: async (channelId, revision) => { + const { server, appToken } = session.current; + return await getChannelTopics(server, appToken, channelId, revision); + }, + getTopicAssetUrl: (channelId, topicId, assetId) => { + const { server, appToken } = session.current; + return getChannelTopicAssetUrl(server, appToken, channelId, topicId, assetId); + }, + addTopic: async (channelId, message, files) => { + const { server, appToken } = session.current; + if (files?.length > 0) { + const topicId = await addChannelTopic(server, appToken, channelId, null, null); + upload.actions.addTopic(server, appToken, channelId, topicId, files, async (assets) => { + message.assets = assets; + await setChannelTopicSubject(server, appToken, channelId, topicId, message); + }, async () => { + try { + await removeChannelTopic(server, appToken, channelId, topicId); + } + catch (err) { + console.log(err); + } + }); + } + else { + await addChannelTopic(server, appToken, channelId, message, []); + } + }, + setTopicSubject: async (channelId, topicId, data) => { + const { server, appToken } = session.current; + return await setChannelTopicSubject(server, appToken, channelId, topicId, data); + }, + setSubject: async (channelId, data) => { + const { server, appToken } = session.current; + return await setChannelSubject(server, appToken, channelId, data); + }, + remove: async (channelId) => { + const { server, appToken } = session.current; + return await removeChannel(server, appToken, channelId); + }, + removeTopic: async (channelId, topicId) => { + const { server, appToken } = session.current; + return await removeChannelTopic(server, appToken, channelId, topicId); + }, + setCard: async (channelId, cardId) => { + const { server, appToken } = session.current; + return await setChannelCard(server, appToken, channelId, cardId); + }, + clearCard: async (channelId, cardId) => { + const { server, appToken } = session.current; + return await clearChannelCard(server, appToken, channelId, cardId); + }, } return { state, actions } diff --git a/app/mobile/src/context/useConversationContext.hook.js b/app/mobile/src/context/useConversationContext.hook.js new file mode 100644 index 00000000..15c2df6f --- /dev/null +++ b/app/mobile/src/context/useConversationContext.hook.js @@ -0,0 +1,394 @@ +import { useState, useEffect, useRef, useContext } from 'react'; +import { StoreContext } from 'context/StoreContext'; +import { CardContext } from 'context/CardContext'; +import { ChannelContext } from 'context/ChannelContext'; +import { ProfileContext } from 'context/ProfileContext'; +import moment from 'moment'; + +export function useConversationContext() { + const [state, setState] = useState({ + topic: null, + subject: null, + logo: null, + revision: null, + contacts: [], + topics: new Map(), + createed: null, + host: null, + }); + const store = useContext(StoreContext); + const card = useContext(CardContext); + const channel = useContext(ChannelContext); + const profile = useContext(ProfileContext); + const topics = useRef(null); + const revision = useRef(0); + const force = useRef(false); + const detailRevision = useRef(0); + const syncing = useRef(false); + const conversationId = useRef(null); + const reset = useRef(false); + const setView = useRef(0); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })) + } + + const getTopicItems = async (cardId, channelId) => { + if (cardId) { + return await card.actions.getChannelTopicItems(cardId, channelId); + } + return await channel.actions.getTopicItems(channelId); + } + const setTopicItem = async (cardId, channelId, topic) => { + if (cardId) { + return await card.actions.setChannelTopicItem(cardId, channelId, topic); + } + return await channel.actions.setTopicItem(channelId, topic); + } + const clearTopicItem = async (cardId, channelId, topicId) => { + if (cardId) { + return await card.actions.clearChannelTopicItem(cardId, channelId, topicId); + } + return await channel.actions.clearTopicItem(channelId, topicId); + } + const getTopic = async (cardId, channelId, topicId) => { + if (cardId) { + return await card.actions.getChannelTopic(cardId, channelId, topicId); + } + return await channel.actions.getTopic(channelId, topicId); + } + const getTopics = async (cardId, channelId, revision) => { + if (cardId) { + return await card.actions.getChannelTopics(cardId, channelId, revision); + } + return await channel.actions.getTopics(channelId, revision) + } + const getTopicAssetUrl = (cardId, channelId, assetId) => { + if (cardId) { + return card.actions.getChannelTopicAssetUrl(cardId, channelId, topicId, assetId); + } + return channel.actions.getTopicAssetUrl(channelId, assetId); + } + const addTopic = async (cardId, channelId, message, asssets) => { + if (cardId) { + return await card.actions.addChannelTopic(cardId, channelId, message, assetId); + } + return await channel.actions.addTopic(channelId, message, assetId); + } + const setTopicSubject = async (cardId, channelId, topicId, data) => { + if (cardId) { + return await card.actions.setChannelTopicSubject(cardId, channelId, topicId, data); + } + return await channel.actions.setTopicSubject(channelId, topicId, data); + } + const remove = async (cardId, channelId) => { + if (cardId) { + return await card.actions.removeChannel(cardId, channelId); + } + return await channel.actions.remove(channelId); + } + const removeTopic = async (cardId, channelId, topicId) => { + if (cardId) { + return await card.actions.removeChannelTopic(cardId, channelId, topicId); + } + return await channel.actions.remvoeTopic(channelId, topicId); + } + const setSyncRevision = async (cardId, channelId, revision) => { + if (cardId) { + return await card.actions.setSyncRevision(cardId, channelId, revision); + } + return await channel.actions.setSyncRevision(channelId, revision); + } + + const sync = async () => { + const curView = setView.current; + if (!syncing.current) { + if (reset.current) { + revision.current = null; + detailRevision.current = null; + topics.current = null; + reset.current = false; + } + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + const channelItem = getChannel(cardId, channelId); + if (channelItem && (channelItem.revision !== revision.current || force.current)) { + syncing.current = true; + + try { + // set channel details + if (detailRevision.current != channelItem.detailRevision) { + if (curView === setView.current) { + setChannel(channelItem); + detailRevision.current = channelItem.detailRevision; + } + } + + // initial load from store + if (!topics.current) { + topics.current = new Map(); + const items = await getTopicItems(cardId, channelId); + items.forEach(item => { + topics.current.set(item.topicId, item); + }); + } + + // sync from server + if (channelItem.topicRevision != channelItem.syncRevision || force.current) { + force.current = false; + const res = await getTopics(cardId, channelId, channelItem.syncRevision) + for (const topic of res.topics) { + if (!topic.data) { + topics.current.delete(topic.id); + await clearTopicItem(cardId, channelId, topic.id); + } + else { + const cached = topics.current.get(topic.id); + if (!cached || cached.detailRevision != topic.data.detailRevision) { + if (!topic.data.topicDetail) { + const updated = await getTopic(cardId, channelId, topic.id); + topic.data = updated.data; + } + if (!topic.data) { + topics.current.delete(topic.id); + await clearTopicItem(cardId, channelId, topic.id); + } + else { + await setTopicItem(cardId, channelId, topic); + const { id, revision, data } = topic; + topics.current.set(id, { topicId: id, revision: revision, detailRevision: topic.data.detailRevision, detail: topic.data.topicDetail }); + } + } + } + } + await setSyncRevision(cardId, channelId, res.revision); + } + + // update revision + revision.current = channelItem.revision; + if (curView == setView.current) { + if (cardId) { + card.actions.setChannelReadRevision(cardId, channelId, revision.current); + } + else { + channel.actions.setReadRevision(channelId, revision.current); + } + updateState({ topics: topics.current }); + } + + syncing.current = false; + sync(); + } + catch(err) { + console.log(err); + syncing.current = false; + //TODO set to unsynced state + } + } + } + } + } + + const getCard = (guid) => { + let contact = null + card.state.cards.forEach((card, cardId, map) => { + if (card?.profile?.guid === guid) { + contact = card; + } + }); + return contact; + } + + const getChannel = (cardId, channelId) => { + if (cardId) { + const entry = card.state.cards.get(cardId); + return entry?.channels.get(channelId); + } + return channel.state.channels.get(channelId); + } + + const setChannel = (item) => { + let contacts = []; + let logo = null; + let topic = null; + let subject = null; + + let timestamp; + const date = new Date(item.detail.created * 1000); + const now = new Date(); + const offset = now.getTime() - date.getTime(); + if(offset < 86400000) { + timestamp = moment(date).format('h:mma'); + } + else if (offset < 31449600000) { + timestamp = moment(date).format('M/DD'); + } + else { + timestamp = moment(date).format('M/DD/YYYY'); + } + + if (!item) { + updateState({ contacts, logo, subject, topic }); + return; + } + + if (item.cardId) { + contacts.push(card.state.cards.get(item.cardId)); + } + if (item?.detail?.members) { + const profileGuid = profile.state.profile.guid; + item.detail.members.forEach(guid => { + if (profileGuid !== guid) { + const contact = getCard(guid); + contacts.push(contact); + } + }) + } + + if (contacts.length === 0) { + logo = 'solution'; + } + else if (contacts.length === 1) { + if (contacts[0]?.profile?.imageSet) { + logo = card.actions.getCardLogo(contacts[0].cardId, contacts[0].profileRevision); + } + else { + logo = 'avatar'; + } + } + else { + logo = 'appstore'; + } + + if (item?.detail?.data) { + try { + topic = JSON.parse(item?.detail?.data).subject; + subject = topic; + } + catch (err) { + console.log(err); + } + } + if (!subject) { + if (contacts.length) { + let names = []; + for (let contact of contacts) { + if (contact?.profile?.name) { + names.push(contact.profile.name); + } + else if (contact?.profile?.handle) { + names.push(contact?.profile?.handle); + } + } + subject = names.join(', '); + } + else { + subject = "Notes"; + } + } + + updateState({ topic, subject, logo, contacts, host: item.cardId, created: timestamp }); + } + + useEffect(() => { + sync(); + }, [card, channel]); + + const actions = { + setChannel: (selected) => { + if (selected == null) { + setView.current++; + conversationId.current = null; + reset.current = true; + updateState({ subject: null, logo: null, contacts: [], topics: new Map() }); + } + else if (selected.cardId !== conversationId.current?.cardId || selected.channelId !== conversationId.current?.channelId) { + setView.current++; + conversationId.current = selected; + reset.current = true; + updateState({ subject: null, logo: null, contacts: [], topics: new Map() }); + sync(); + const { cardId, channelId, revision } = selected; + if (cardId) { + card.actions.setChannelReadRevision(cardId, channelId, revision); + } + else { + channel.actions.setReadRevision(channelId, revision); + } + } + }, + getTopicAssetUrl: (topicId, assetId) => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + if (cardId) { + return card.actions.getChannelTopicAssetUrl(cardId, channelId, topicId, assetId); + } + else { + return channel.actions.getTopicAssetUrl(channelId, topicId, assetId); + } + } + return null; + }, + addTopic: async (message, files) => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + if (cardId) { + await card.actions.addChannelTopic(cardId, channelId, message, files); + } + else { + await channel.actions.addTopic(channelId, message, files); + } + force.current = true; + sync(); + } + }, + setSubject: async (subject) => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + if (cardId) { + throw new Error("can only set hosted channel subjects"); + } + await channel.actions.setSubject(channelId, subject); + } + }, + remove: async () => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + await remove(cardId, channelId); + } + }, + setCard: async (id) => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + if (cardId) { + throw new Error("can only set members on hosted channel"); + } + await channel.actions.setCard(channelId, id); + } + }, + clearCard: async (id) => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + if (cardId) { + throw new Error("can only clear members on hosted channel"); + } + await channel.actions.clearCard(channelId, id); + } + }, + setBlocked: async () => { + if (conversationId.current) { + const { cardId, channelId } = conversationId.current; + if (cardId) { + await card.actions.setChannelBlocked(cardId, channelId); + } + else { + await channel.actions.setBlocked(channelId); + } + } + }, + } + + return { state, actions } +} + + diff --git a/app/mobile/src/context/useProfileContext.hook.js b/app/mobile/src/context/useProfileContext.hook.js index 3fcd4764..9a8dbc95 100644 --- a/app/mobile/src/context/useProfileContext.hook.js +++ b/app/mobile/src/context/useProfileContext.hook.js @@ -3,6 +3,7 @@ import { getProfile } from 'api/getProfile'; import { setProfileData } from 'api/setProfileData'; import { setProfileImage } from 'api/setProfileImage'; import { getProfileImageUrl } from 'api/getProfileImageUrl'; +import { getHandle } from 'api/getHandle'; import { StoreContext } from 'context/StoreContext'; export function useProfileContext() { @@ -57,7 +58,7 @@ export function useProfileContext() { }, clearSession: () => { session.current = {}; - updateState({ profile: null }); + updateState({ profile: {} }); }, setRevision: (rev) => { curRevision.current = rev; @@ -71,6 +72,17 @@ export function useProfileContext() { const { server, appToken } = session.current; await setProfileImage(server, appToken, image); }, + getHandle: async (name) => { + const { server, appToken } = session.current; + return await getHandle(server, appToken, name); + }, + getImageUrl: () => { + const { server, appToken } = session.current; + if (!state.profile.image) { + return null; + } + return getProfileImageUrl(server, appToken, state.profile.revision); + }, } return { state, actions } diff --git a/app/mobile/src/context/useStoreContext.hook.js b/app/mobile/src/context/useStoreContext.hook.js index 56121205..78f90cb1 100644 --- a/app/mobile/src/context/useStoreContext.hook.js +++ b/app/mobile/src/context/useStoreContext.hook.js @@ -1,7 +1,7 @@ import { useEffect, useState, useRef, useContext } from 'react'; import SQLite from "react-native-sqlite-storage"; -const DATABAG_DB = 'databag_v033.db'; +const DATABAG_DB = 'databag_v039.db'; export function useStoreContext() { const [state, setState] = useState({}); @@ -12,10 +12,10 @@ export function useStoreContext() { } const initSession = async (guid) => { - await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, detail text, summary text, offsync integer, read_revision integer, unique(channel_id))`); + await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, blocked integer, sync_revision integer, detail text, summary text, offsync integer, read_revision integer, unique(channel_id))`); await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_topic_${guid} (channel_id text, topic_id text, revision integer, detail_revision integer, detail text, unique(channel_id, topic_id))`); - await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_${guid} (card_id text, revision integer, detail_revision integer, profile_revision integer, detail text, profile text, notified_view integer, notified_article integer, notified_profile integer, notified_channel integer, offsync integer, unique(card_id))`); - await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_${guid} (card_id text, channel_id text, revision integer, detail_revision integer, topic_revision integer, detail text, summary text, offsync integer, read_revision integer, unique(card_id, channel_id))`); + await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_${guid} (card_id text, revision integer, detail_revision integer, profile_revision integer, detail text, profile text, notified_view integer, notified_article integer, notified_profile integer, notified_channel integer, offsync integer, blocked integer, unique(card_id))`); + await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_${guid} (card_id text, channel_id text, revision integer, detail_revision integer, topic_revision integer, sync_revision integer, detail text, summary text, offsync integer, blocked integer, read_revision integer, unique(card_id, channel_id))`); await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_topic_${guid} (card_id text, channel_id text, topic_id text, revision integer, detail_revision integer, detail text, unique(card_id, channel_id, topic_id))`); } @@ -106,6 +106,12 @@ export function useStoreContext() { clearCardItemOffsync: async (guid, cardId) => { await db.current.executeSql(`UPDATE card_${guid} set offsync=? where card_id=?`, [0, cardId]); }, + setCardItemBlocked: async (guid, cardId) => { + await db.current.executeSql(`UPDATE card_${guid} set blocked=? where card_id=?`, [1, cardId]); + }, + clearCardItemBlocked: async (guid, cardId) => { + await db.current.executeSql(`UPDATE card_${guid} set blocked=? where card_id=?`, [0, cardId]); + }, setCardItemDetail: async (guid, cardId, revision, detail) => { await db.current.executeSql(`UPDATE card_${guid} set detail_revision=?, detail=? where card_id=?`, [revision, encodeObject(detail), cardId]); }, @@ -127,6 +133,7 @@ export function useStoreContext() { notifiedProfile: values[0].notified_profile, notifiedChannel: values[0].notified_channel, offsync: values[0].offsync, + blocked: values[0].blocked, }; }, getCardItemView: async (guid, cardId) => { @@ -141,7 +148,7 @@ export function useStoreContext() { }; }, getCardItems: async (guid) => { - const values = await getAppValues(db.current, `SELECT card_id, revision, detail_revision, profile_revision, detail, profile, notified_view, notified_profile, notified_article, notified_channel FROM card_${guid}`, []); + const values = await getAppValues(db.current, `SELECT card_id, revision, detail_revision, profile_revision, detail, profile, offsync, blocked, notified_view, notified_profile, notified_article, notified_channel FROM card_${guid}`, []); return values.map(card => ({ cardId: card.card_id, revision: card.revision, @@ -153,6 +160,8 @@ export function useStoreContext() { notifiedProfile: card.notified_profile, notifiedArticle: card.notified_article, notifiedChannel: card.notified_channel, + offsync: card.offsync, + blocked: card.blocked, })); }, @@ -177,6 +186,15 @@ export function useStoreContext() { setChannelItemReadRevision: async (guid, channelId, revision) => { await db.current.executeSql(`UPDATE channel_${guid} set read_revision=? where channel_id=?`, [revision, channelId]); }, + setChannelItemSyncRevision: async (guid, channelId, revision) => { + await db.current.executeSql(`UPDATE channel_${guid} set sync_revision=? where channel_id=?`, [revision, channelId]); + }, + setChannelItemBlocked: async (guid, channelId) => { + await db.current.executeSql(`UPDATE channel_${guid} set blocked=? where channel_id=?`, [1, channelId]); + }, + clearChannelItemBlocked: async (guid, channelId) => { + await db.current.executeSql(`UPDATE channel_${guid} set blocked=? where channel_id=?`, [0, channelId]); + }, setChannelItemDetail: async (guid, channelId, revision, detail) => { await db.current.executeSql(`UPDATE channel_${guid} set detail_revision=?, detail=? where channel_id=?`, [revision, encodeObject(detail), channelId]); }, @@ -195,18 +213,41 @@ export function useStoreContext() { }; }, getChannelItems: async (guid) => { - const values = await getAppValues(db.current, `SELECT channel_id, read_revision, revision, detail_revision, topic_revision, detail, summary FROM channel_${guid}`, []); + const values = await getAppValues(db.current, `SELECT channel_id, read_revision, revision, sync_revision, blocked, detail_revision, topic_revision, detail, summary FROM channel_${guid}`, []); return values.map(channel => ({ channelId: channel.channel_id, revision: channel.revision, readRevision: channel.read_revision, detailRevision: channel.detail_revision, topicRevision: channel.topic_revision, + syncRevision: channel.sync_revision, + blocked: channel.blocked, detail: decodeObject(channel.detail), summary: decodeObject(channel.summary), })); }, + + getChannelTopicItems: async (guid, channelId) => { + const values = await getAppValues(db.current, `SELECT topic_id, revision, detail_revision, detail FROM channel_topic_${guid} WHERE channel_id=?`, [channelId]); + return values.map(topic => ({ + topicId: topic.topic_id, + revision: topic.revision, + detailRevision: topic.detail_revision, + detail: decodeObject(topic.detail), + })); + }, + setChannelTopicItem: async (guid, channelId, topic) => { + const { id, revision, data } = topic; + await db.current.executeSql(`INSERT OR REPLACE INTO channel_topic_${guid} (channel_id, topic_id, revision, detail_revision, detail) values (?, ?, ?, ?, ?);`, [channelId, id, revision, data.detailRevision, encodeObject(data.topicDetail)]); + }, + clearChannelTopicItem: async (guid, channelId, topicId) => { + await db.current.executeSql(`DELETE FROM channel_topic_${guid} WHERE channel_id=? and topic_id=?`, [channelId, topicId]); + }, + clearChannelTopicItems: async (guid, channelId) => { + await db.current.executeSql(`DELETE FROM channel_topic_${guid} WHERE channel_id=?`, [channelId]); + }, + setCardChannelItem: async (guid, cardId, channel) => { const { id, revision, data } = channel; await db.current.executeSql(`INSERT OR REPLACE INTO card_channel_${guid} (card_id, channel_id, revision, detail_revision, topic_revision, detail, summary) values (?, ?, ?, ?, ?, ?, ?);`, [cardId, id, revision, data.detailRevision, data.topicRevision, encodeObject(data.channelDetail), encodeObject(data.channelSummary)]); @@ -220,6 +261,9 @@ export function useStoreContext() { setCardChannelItemReadRevision: async (guid, cardId, channelId, revision) => { await db.current.executeSql(`UPDATE card_channel_${guid} set read_revision=? where card_id=? and channel_id=?`, [revision, cardId, channelId]); }, + setCardChannelItemSyncRevision: async (guid, cardId, channelId, revision) => { + await db.current.executeSql(`UPDATE card_channel_${guid} set sync_revision=? where card_id=? and channel_id=?`, [revision, cardId, channelId]); + }, setCardChannelItemDetail: async (guid, cardId, channelId, revision, detail) => { await db.current.executeSql(`UPDATE card_channel_${guid} set detail_revision=?, detail=? where card_id=? and channel_id=?`, [revision, encodeObject(detail), cardId, channelId]); }, @@ -238,7 +282,7 @@ export function useStoreContext() { }; }, getCardChannelItems: async (guid) => { - const values = await getAppValues(db.current, `SELECT card_id, channel_id, read_revision, revision, detail_revision, topic_revision, detail, summary FROM card_channel_${guid}`, []); + const values = await getAppValues(db.current, `SELECT card_id, channel_id, read_revision, sync_revision, revision, blocked, detail_revision, topic_revision, detail, summary FROM card_channel_${guid}`, []); return values.map(channel => ({ cardId: channel.card_id, channelId: channel.channel_id, @@ -246,14 +290,36 @@ export function useStoreContext() { readRevision: channel.read_revision, detailRevision: channel.detail_revision, topicRevision: channel.topic_revision, + syncRevision: channel.sync_revision, detail: decodeObject(channel.detail), summary: decodeObject(channel.summary), + blocked: channel.blocked, })); }, clearCardChannelItems: async (guid, cardId) => { await db.current.executeSql(`DELETE FROM card_channel_${guid} WHERE card_id=?`, [cardId]); }, + getCardChannelTopicItems: async (guid, cardId, channelId) => { + const values = await getAppValues(db.current, `SELECT topic_id, revision, detail_revision, detail FROM card_channel_topic_${guid} WHERE card_id=? AND channel_id=?`, [cardId, channelId]); + return values.map(topic => ({ + topicId: topic.topic_id, + revision: topic.revision, + detailRevision: topic.detail_revision, + detail: decodeObject(topic.detail), + })); + }, + setCardChannelTopicItem: async (guid, cardId, channelId, topic) => { + const { id, revision, data } = topic; + await db.current.executeSql(`INSERT OR REPLACE INTO card_channel_topic_${guid} (card_id, channel_id, topic_id, revision, detail_revision, detail) values (?, ?, ?, ?, ?, ?);`, [cardId, channelId, id, revision, data.detailRevision, encodeObject(data.topicDetail)]); + }, + clearCardChannelTopicItem: async (guid, cardId, channelId, topicId) => { + await db.current.executeSql(`DELETE FROM card_channel_topic_${guid} WHERE card_id=? and channel_id=? and topic_id=?`, [cardId, channelId, topicId]); + }, + clearCardChannelTopicItems: async (guid, cardId, channelId) => { + await db.current.executeSql(`DELETE FROM card_channel_topic_${guid} WHERE card_id=? and channel_id=?`, [cardId, channelId]); + }, + } return { state, actions } } @@ -308,4 +374,3 @@ async function getAppValues(sql: SQLite.SQLiteDatabase, query: string, params) { } - diff --git a/app/mobile/src/context/useUploadContext.hook.js b/app/mobile/src/context/useUploadContext.hook.js new file mode 100644 index 00000000..ff53d82f --- /dev/null +++ b/app/mobile/src/context/useUploadContext.hook.js @@ -0,0 +1,234 @@ +import { useState, useRef } from 'react'; +import axios from 'axios'; + +export function useUploadContext() { + + const [state, setState] = useState({ + progress: new Map(), + }); + const channels = useRef(new Map()); + const index = useRef(0); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + }; + + const updateComplete = (channel, topic) => { + let topics = channels.current.get(channel); + if (topics) { + topics.delete(topic); + } + updateProgress(); + } + + const updateProgress = () => { + let progress = new Map(); + channels.current.forEach((topics, channel) => { + let assets = []; + topics.forEach((entry, topic, map) => { + let active = entry.active ? 1 : 0; + assets.push({ + upload: entry.index, + topicId: topic, + active: entry.active, + uploaded: entry.assets.length, + index: entry.assets.length + active, + count: entry.assets.length + entry.files.length + active, + error: entry.error, + }); + }); + if (assets.length) { + progress.set(channel, assets.sort((a, b) => (a.upload < b.upload) ? 1 : -1)); + } + }); + updateState({ progress }); + } + + const abort = (channelId, topicId) => { + const channel = channels.current.get(channelId); + if (channel) { + const topic = channel.get(topicId); + if (topic) { + topic.cancel.abort(); + channel.delete(topicId); + updateProgress(); + } + } + } + + const actions = { + addTopic: (node, token, channelId, topicId, files, success, failure) => { + const controller = new AbortController(); + const entry = { + index: index.current, + url: `https://${node}/content/channels/${channelId}/topics/${topicId}/assets?agent=${token}`, + files, + assets: [], + current: null, + error: false, + success, + failure, + cancel: controller, + } + index.current += 1; + const key = `:${channelId}`; + if (!channels.current.has(key)) { + channels.current.set(key, new Map()); + } + const topics = channels.current.get(key); + topics.set(topicId, entry); + upload(entry, updateProgress, () => { updateComplete(key, topicId) } ); + }, + cancelTopic: (channelId, topicId) => { + abort(`:${channelId}`, topicId); + }, + addContactTopic: (node, token, cardId, channelId, topicId, files, success, failure) => { + const controller = new AbortController(); + const entry = { + index: index.current, + url: `https://${node}/content/channels/${channelId}/topics/${topicId}/assets?contact=${token}`, + files, + assets: [], + current: null, + error: false, + success, + failure, + cancel: controller, + } + index.current += 1; + const key = `${cardId}:${channelId}`; + if (!channels.current.has(key)) { + channels.current.set(key, new Map()); + } + const topics = channels.current.get(key); + topics.set(topicId, entry); + upload(entry, updateProgress, () => { updateComplete(key, topicId) }); + }, + cancelContactTopic: (cardId, channelId, topicId) => { + abort(`${cardId}:${channelId}`, topicId); + }, + clearErrors: (cardId, channelId) => { + const key = cardId ? `${cardId}:${channelId}` : `:${channelId}`; + const topics = channels.current.get(key); + if (topics) { + topics.forEach((topic, topicId) => { + if (topic.error) { + topic.cancel.abort(); + topics.delete(topicId); + updateProgress(); + } + }); + } + }, + clear: () => { + channels.current.forEach((topics, channelId) => { + topics.forEach((assets, topicId) => { + assets.cancel.abort(); + }); + }); + channels.current.clear(); + updateProgress(); + } + } + + return { state, actions } +} + +async function upload(entry, update, complete) { + if (!entry.files?.length) { + entry.success(entry.assets); + complete(); + } + else { + const file = entry.files.shift(); + entry.active = {}; + try { + if (file.type === 'image') { + const formData = new FormData(); + if (file.data.startsWith('file:')) { + formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'}); + } + else { + formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'}); + } + let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "ilg;photo"])); + let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + signal: entry.cancel.signal, + onUploadProgress: (ev) => { + const { loaded, total } = ev; + entry.active = { loaded, total } + update(); + }, + }); + entry.assets.push({ + image: { + thumb: asset.data.find(item => item.transform === 'ithumb;photo').assetId, + full: asset.data.find(item => item.transform === 'ilg;photo').assetId, + } + }); + } + else if (file.type === 'video') { + const formData = new FormData(); + if (file.data.startsWith('file:')) { + formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'}); + } + else { + formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'}); + } + let thumb = 'vthumb;video;' + file.position; + let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb])); + let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + signal: entry.cancel.signal, + onUploadProgress: (ev) => { + const { loaded, total } = ev; + entry.active = { loaded, total } + update(); + }, + }); + entry.assets.push({ + video: { + thumb: asset.data.find(item => item.transform === thumb).assetId, + lq: asset.data.find(item => item.transform === 'vlq;video').assetId, + hd: asset.data.find(item => item.transform === 'vhd;video').assetId, + } + }); + } + else if (file.type === 'audio') { + const formData = new FormData(); + if (file.data.startsWith('file:')) { + formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'}); + } + else { + formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'}); + } + let transform = encodeURIComponent(JSON.stringify(["acopy;audio"])); + let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + signal: entry.cancel.signal, + onUploadProgress: (ev) => { + const { loaded, total } = ev; + entry.active = { loaded, total } + update(); + }, + }); + entry.assets.push({ + audio: { + label: file.label, + full: asset.data.find(item => item.transform === 'acopy;audio').assetId, + } + }); + } + entry.active = null; + upload(entry, update, complete); + } + catch (err) { + console.log(err); + entry.failure(); + entry.error = true; + update(); + } + } +} + diff --git a/app/mobile/src/session/Session.jsx b/app/mobile/src/session/Session.jsx index 6db496f4..2512d2e1 100644 --- a/app/mobile/src/session/Session.jsx +++ b/app/mobile/src/session/Session.jsx @@ -1,5 +1,5 @@ import { View, TouchableOpacity, Text } from 'react-native'; -import { useState } from 'react'; +import { useState, useEffect, useContext } from 'react'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createDrawerNavigator } from '@react-navigation/drawer'; @@ -9,13 +9,19 @@ import Ionicons from '@expo/vector-icons/AntDesign'; import { useSession } from './useSession.hook'; import { styles } from './Session.styled'; import Colors from 'constants/Colors'; -import { Profile } from './profile/Profile'; -import { Channels } from './channels/Channels'; -import { Cards } from './cards/Cards'; -import { Contact } from './contact/Contact'; -import { Details } from './details/Details'; -import { Conversation } from './conversation/Conversation'; +import { ProfileTitle, Profile } from './profile/Profile'; +import { CardsTitle, CardsBody, Cards } from './cards/Cards'; +import { useCards } from './cards/useCards.hook'; +import { RegistryTitle, RegistryBody, Registry } from './registry/Registry'; +import { useRegistry } from './registry/useRegistry.hook'; +import { Contact, ContactTitle } from './contact/Contact'; +import { Details, DetailsHeader, DetailsBody } from './details/Details'; +import { Conversation, ConversationHeader, ConversationBody } from './conversation/Conversation'; import { Welcome } from './welcome/Welcome'; +import { ChannelsTitle, ChannelsBody, Channels } from './channels/Channels'; +import { useChannels } from './channels/useChannels.hook'; +import { CommonActions } from '@react-navigation/native'; +import { ConversationContext } from 'context/ConversationContext'; const ConversationStack = createStackNavigator(); const ProfileStack = createStackNavigator(); @@ -24,135 +30,189 @@ const ProfileDrawer = createDrawerNavigator(); const ContactDrawer = createDrawerNavigator(); const DetailDrawer = createDrawerNavigator(); const CardDrawer = createDrawerNavigator(); +const RegistryDrawer = createDrawerNavigator(); const Tab = createBottomTabNavigator(); export function Session() { const { state, actions } = useSession(); - const openCards = (nav) => { - nav.openDrawer(); - } - const closeCards = (nav) => {} - const openProfile = (nav) => { - nav.openDrawer(); - } - const closeProfile = (nav) => {} - const openContact = (nav, cardId) => {} - const closeContact = (nav) => {} - const openConversation = (nav, cardId, channelId) => {} - const closeConversation = (nav) => {} - const openDetails = (nav, cardId, channeId) => {} - const closeDetails = (nav) => {} - // tabbed containers const ConversationStackScreen = () => { + + const [selected, setSelected] = useState(null); + const setConversation = (navigation, cardId, channelId, revision) => { + setSelected({ cardId, channelId, revision }); + navigation.navigate('conversation'); + } + const clearConversation = (navigation) => { + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [ + { name: 'channels' }, + ], + }) + ); + } + const setDetail = (navigation) => { + navigation.navigate('details'); + } + const clearDetail = (navigation) => { + navigation.goBack(); + } + + const channels = useChannels(); + const conversation = useContext(ConversationContext); + + useEffect(() => { + conversation.actions.setChannel(selected); + }, [selected]); + return ( - ({ headerShown: false })}> - - - + ({ headerShown: true, headerTintColor: Colors.primary })} + screenListeners={{ state: (e) => { if (e?.data?.state?.index === 0 && selected) { setSelected(null); }}, }}> + + }}> + {(props) => setConversation(props.navigation, cardId, channelId, revision)} />} + + + }}> + {(props) => } + + + }}> + {(props) => clearConversation(props.navigation)} />} + ); } - const ChannelsTabScreen = ({ navigation }) => { - return ( - openConversation(navigation, cardId, channelId)} /> - ) - } - const ConversationTabScreen = ({ navigation }) => { - return closeConversation(navigation)} openDetails={() => openDetails(navigation)} /> - } - const DetailsTabScreen = ({ navigation }) => { - return
closeDetails(navigation)} /> - } const ProfileStackScreen = () => { return ( - ({ headerShown: false })}> - + ({ headerShown: true, headerTintColor: Colors.primary })}> + }} /> ); } const ContactStackScreen = () => { - const [cardId, setCardId] = useState(null); - const setCardStack = (navigation, id) => { - setCardId(id); - navigation.navigate('card') + const [selected, setSelected] = useState(null); + const setCardStack = (navigation, contact) => { + setSelected(contact); + navigation.navigate('contact') } const clearCardStack = (navigation) => { navigation.goBack(); } + const setRegistryStack = (navigation) => { + navigation.navigate('registry'); + } + const clearRegistryStack = (navigation) => { + navigation.goBack(); + } + + const registry = useRegistry(); + const cards = useCards(); return ( - ({ headerShown: false })}> - - {(props) => setCardStack(props.navigation, cardId)} />} + ({ headerShow: true, headerTintColor: Colors.primary })} + initialRouteName="cards"> + + }}> + {(props) => setCardStack(props.navigation, contact)} />} - - {(props) => clearCardStack(props.navigation)} />} + + }}> + {(props) => clearCardStack(props.navigation)} />} + + + }}> + {(props) => setCardStack(props.navigation, contact)} />} ); } + const HomeScreen = ({ cardNav, registryNav, detailNav, contactNav, profileNav, setDetails, resetConversation, clearReset }) => { - // drawered containers - const CardDrawerContent = ({ navigation, setContact }) => { - return ( - - - - ) - } - const ProfileDrawerContent = ({ navigation }) => { - return ( - - closeProfile(navigation)} /> - - ) - } - const DetailDrawerContent = ({ navigation }) => { - return ( - -
closeDetails(navigation)} /> - - ) - } - const ContactDrawerContent = ({ navigation }) => { - const clearContact = () => { - navigation.closeDrawer(); + const [channel, setChannel] = useState(null); + const setConversation = (cardId, channelId, revision) => { + setChannel({ cardId, channelId, revision }); + }; + const clearConversation = () => { + setChannel(null); + }; + const setProfile = () => { + profileNav.openDrawer(); + }; + const setChannelDetails = (channel) => { + setDetails(channel); + detailNav.openDrawer(); + }; + + const openProfile = () => { + profileNav.openDrawer(); + } + const openCards = () => { + cardNav.openDrawer(); } - return ( - - - - ) - } + const conversation = useContext(ConversationContext); + + useEffect(() => { + if (resetConversation) { + detailNav.closeDrawer(); + setChannel(null); + setDetails(null); + clearReset(); + } + }, [resetConversation]); + + useEffect(() => { + conversation.actions.setChannel(channel); + }, [channel]); - const HomeScreen = ({ cardNav, detailNav, contactNav, profileNav }) => { return ( - - - openProfile(profileNav)}> + + + Profile - openCards(cardNav)}> + Contacts - + - openConversation(null, cardId, channelId)} /> + - { state.conversationId && ( - closeConversation(null)} openDetails={() => openDetails(detailNav)} /> + { channel && ( + )} - { !state.conversationId && ( + { !channel && ( )} @@ -160,60 +220,131 @@ export function Session() { ) } - const CardDrawerScreen = ({ detailNav, contactNav, profileNav, setContact }) => { + const CardDrawerScreen = ({ registryNav, detailNav, contactNav, profileNav, setContact, setDetails, clearReset, resetConversation }) => { - const setCardDrawer = (cardId) => { - setContact(cardId); + const openRegistry = () => { + registryNav.openDrawer(); + }; + setCardContact = (contact) => { + setContact(contact); contactNav.openDrawer(); - } + }; + + const params = { + profileNav, + registryNav, + detailNav, + contactNav, + setDetails, + setContact, + clearReset, + resetConversation, + }; return ( }> + drawerContent={(props) => }> - {(props) => } + {(props) => } ); }; - const ContactDrawerScreen = ({ detailNav, profileNav }) => { + const RegistryDrawerScreen = ({ detailNav, contactNav, profileNav, setContact, setDetails, clearReset, resetConversation }) => { - const [cardId, setCardId] = useState(null); - const setContact = (id) => { - setCardId(id); + const setRegistryContact = (contact) => { + setContact(contact); + contactNav.openDrawer(); + }; + + const params = { + profileNav, + detailNav, + contactNav, + setDetails, + setContact, + clearReset, + resetConversation, + }; + + return ( + }> + + {(props) => } + + + ); + }; + + const ContactDrawerScreen = ({ detailNav, profileNav, setDetails, resetConversation, clearReset }) => { + + const [selected, setSelected] = useState(null); + const setContact = (contact) => { + setSelected(contact); } + const params = { + profileNav, + detailNav, + setDetails, + setContact, + clearReset, + resetConversation, + }; + return ( }> - - {(props) => } + drawerContent={(props) => }> + + {(props) => } ); } - const ProfileDrawerScreen = ({ detailNav }) => { + const DetailDrawerScreen = ({ profileNav }) => { + + const [selected, setSelected] = useState(null); + const [resetConversation, setResetConversation] = useState(false); + const setDetails = (channel) => { + setSelected(channel); + }; + const clearConversation = () => { + setResetConversation(true); + } + const clearReset = () => { + setResetConversation(false); + } + + const params = { + profileNav, + setDetails, + clearReset, + resetConversation, + }; + return ( - }> - - {(props) => } - - +
} + > + + {(props) => } + + ); } return ( { state.tabbed === false && ( - }> - - {(props) => } - - + }> + + {(props) => } + + )} { state.tabbed === true && ( - - {(props) => ()} - - - {(props) => ()} - - - {(props) => ()} - + + + )} diff --git a/app/mobile/src/session/Session.styled.js b/app/mobile/src/session/Session.styled.js index 7d8c5d3f..cc1e1e40 100644 --- a/app/mobile/src/session/Session.styled.js +++ b/app/mobile/src/session/Session.styled.js @@ -22,8 +22,13 @@ export const styles = StyleSheet.create({ maxWidth: 500, }, conversation: { + width: '67%', + }, + drawer: { + width: '100%', height: '100%', - flexGrow: 1, + paddingLeft: 8, + backgroundColor: Colors.formBackground, }, options: { display: 'flex', diff --git a/app/mobile/src/session/cards/Cards.jsx b/app/mobile/src/session/cards/Cards.jsx index 1952c0c4..3f53d9f3 100644 --- a/app/mobile/src/session/cards/Cards.jsx +++ b/app/mobile/src/session/cards/Cards.jsx @@ -1,18 +1,121 @@ -import { useState, useContext } from 'react'; -import { View, TouchableOpacity, Text } from 'react-native'; +import { useContext } from 'react'; +import { FlatList, ScrollView, View, TextInput, TouchableOpacity, Text } from 'react-native'; +import { styles } from './Cards.styled'; +import { useCards } from './useCards.hook'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { AppContext } from 'context/AppContext'; +import Ionicons from '@expo/vector-icons/AntDesign'; +import { CardItem } from './cardItem/CardItem'; +import Colors from 'constants/Colors'; +import { useNavigation } from '@react-navigation/native'; -export function Cards({ navigation, openContact }) { +export function CardsTitle({ state, actions, openRegistry }) { + const navigation = useNavigation(); - const app = useContext(AppContext); - const [cardId, setCardId] = useState(0); - - const onPressCard = () => { - openContact(cardId); - setCardId(cardId + 1); - } - - return CARD + return ( + + { state.sorting && ( + + + + )} + { !state.sorting && ( + + + + )} + + + + + + openRegistry(navigation)}> + + New + + + ); +} + +export function CardsBody({ state, actions, openContact }) { + return ( + } + keyExtractor={item => item.cardId} + /> + ); +} + +export function Cards({ openRegistry, openContact }) { + const { state, actions } = useCards(); + return ( + + { state.tabbed && ( + <> + + { state.sorting && ( + + + + )} + { !state.sorting && ( + + + + )} + + + + + + + + New + + + } + keyExtractor={item => item.cardId} + /> + + )} + { !state.tabbed && ( + <> + + + { state.sorting && ( + + + + )} + { !state.sorting && ( + + + + )} + + + + + + + + + + + } + keyExtractor={item => item.cardId} + /> + + + )} + + ); } diff --git a/app/mobile/src/session/cards/Cards.styled.js b/app/mobile/src/session/cards/Cards.styled.js new file mode 100644 index 00000000..12ea6b75 --- /dev/null +++ b/app/mobile/src/session/cards/Cards.styled.js @@ -0,0 +1,109 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + backgroundColor: Colors.formBackground, + }, + title: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + topbar: { + borderTopWidth: 1, + borderBottomWidth: 1, + borderColor: Colors.divider, + paddingTop: 32, + paddingBottom: 6, + paddingLeft: 16, + paddingRight: 16, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + searcharea: { + borderBottomWidth: 1, + borderColor: Colors.divider, + }, + searchbar: { + display: 'flex', + flexDirection: 'row', + paddingTop: 16, + paddingLeft: 8, + paddingBottom: 8, + alignItems: 'center', + }, + inputwrapper: { + display: 'flex', + flexDirection: 'row', + borderRadius: 4, + backgroundColor: Colors.white, + alignItems: 'center', + flexGrow: 1, + flexShrink: 1, + marginRight: 8, + paddingTop: 4, + paddingBottom: 4, + }, + inputfield: { + flex: 1, + textAlign: 'center', + padding: 4, + color: Colors.text, + fontSize: 14, + }, + icon: { + paddingLeft: 8, + }, + cards: { + flexGrow: 1, + width: '100%', + paddingLeft: 16, + paddingRight: 16, + }, + addbottom: { + marginRight: 8, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: 8, + borderRadius: 4, + }, + bottomText: { + color: Colors.primary, + paddingLeft: 8, + }, + add: { + backgroundColor: Colors.primary, + marginLeft: 8, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: 8, + borderRadius: 4, + }, + newtext: { + paddingLeft: 8, + color: Colors.white, + }, + up: { + marginRight: 8, + }, + sort: { + paddingRight: 12, + transform: [ { rotate: "270deg" }, ] + }, + findarea: { + borderTopWidth: 1, + borderColor: Colors.divider, + } +}) + diff --git a/app/mobile/src/session/cards/cardItem/CardItem.jsx b/app/mobile/src/session/cards/cardItem/CardItem.jsx new file mode 100644 index 00000000..04efed97 --- /dev/null +++ b/app/mobile/src/session/cards/cardItem/CardItem.jsx @@ -0,0 +1,46 @@ +import { Text, TouchableOpacity, View } from 'react-native'; +import { Logo } from 'utils/Logo'; +import { styles } from './CardItem.styled'; +import { useCardItem } from './useCardItem.hook'; + +export function CardItem({ item, openContact }) { + + const { state, actions } = useCardItem(item); + + const select = () => { + openContact({ card: item.cardId }); + }; + + return ( + + { item.cardId && ( + + + + { item.name } + { item.handle } + + { item.status === 'connected' && ( + + )} + { item.status === 'requested' && ( + + )} + { item.status === 'connecting' && ( + + )} + { item.status === 'pending' && ( + + )} + { item.status === 'confirmed' && ( + + )} + + )} + { !item.cardId && ( + + )} + + ); +} + diff --git a/app/mobile/src/session/cards/cardItem/CardItem.styled.js b/app/mobile/src/session/cards/cardItem/CardItem.styled.js new file mode 100644 index 00000000..337cef53 --- /dev/null +++ b/app/mobile/src/session/cards/cardItem/CardItem.styled.js @@ -0,0 +1,64 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + width: '100%', + display: 'flex', + flexDirection: 'row', + height: 48, + alignItems: 'center', + borderBottomWidth: 1, + borderColor: Colors.itemDivider, + }, + detail: { + paddingLeft: 12, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + flexGrow: 1, + flexShrink: 1, + }, + space: { + height: 64, + }, + name: { + color: Colors.text, + fontSize: 14, + }, + handle: { + color: Colors.text, + fontSize: 12, + }, + connected: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.connected, + }, + requested: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.requested, + }, + connecting: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.connecting, + }, + pending: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.pending, + }, + confirmed: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.confirmed, + }, +}) + diff --git a/app/mobile/src/session/cards/cardItem/useCardItem.hook.js b/app/mobile/src/session/cards/cardItem/useCardItem.hook.js new file mode 100644 index 00000000..cb8796b5 --- /dev/null +++ b/app/mobile/src/session/cards/cardItem/useCardItem.hook.js @@ -0,0 +1,17 @@ +import { useState, useEffect, useRef, useContext } from 'react'; +import { useWindowDimensions } from 'react-native'; + +export function useCardItem(item) { + + const [state, setState] = useState({}); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/cards/useCards.hook.js b/app/mobile/src/session/cards/useCards.hook.js new file mode 100644 index 00000000..428ab80b --- /dev/null +++ b/app/mobile/src/session/cards/useCards.hook.js @@ -0,0 +1,107 @@ +import { useState, useEffect, useRef, useContext } from 'react'; +import { useWindowDimensions } from 'react-native'; +import { useNavigate } from 'react-router-dom'; +import { CardContext } from 'context/CardContext'; +import config from 'constants/Config'; + +export function useCards() { + + const [state, setState] = useState({ + tabbed: null, + cards: [], + filter: null, + sorting: false, + }); + + const dimensions = useWindowDimensions(); + const card = useContext(CardContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + if (dimensions.width > config.tabbedWidth) { + updateState({ tabbed: false }); + } + else { + updateState({ tabbed: true }); + } + }, [dimensions]); + + const setCardItem = (item) => { + const { profile, detail } = item; + + return { + cardId: item.cardId, + name: profile.name, + handle: `${profile.handle}@${profile.node}`, + status: detail.status, + offsync: item.offsync, + blocked: item.blocked, + updated: detail.statusUpdated, + logo: profile.imageSet ? card.actions.getCardLogo(item.cardId, profile.revision) : 'avatar', + } + }; + + useEffect(() => { + const cards = Array.from(card.state.cards.values()); + const items = cards.map(setCardItem); + const filtered = items.filter(item => { + if (!state.filter) { + return !item.blocked; + } + const lower = state.filter.toLowerCase(); + if (item.name) { + if (item.name.toLowerCase().includes(lower)) { + return true; + } + } + if (item.handle) { + if (item.handle.toLowerCase().includes(lower)) { + return true; + } + } + return false; + }) + if (state.sorting) { + filtered.sort((a, b) => { + if (a.name === b.name) { + return 0; + } + if (!a.name || (a.name < b.name)) { + return -1; + } + return 1; + }); + } + else { + filtered.sort((a, b) => { + if (a.updated === b.updated) { + return 0; + } + if (!a.updated || (a.updated < b.updated)) { + return 1; + } + return -1; + }); + } + filtered.push({cardId:''}); + updateState({ cards: filtered }); + }, [card, state.filter, state.sorting]); + + const actions = { + setFilter: (filter) => { + updateState({ filter }); + }, + sort: () => { + updateState({ sorting: true }); + }, + unsort: () => { + updateState({ sorting: false }); + }, + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/channels/Channels.jsx b/app/mobile/src/session/channels/Channels.jsx index d45c4338..a23d1bd9 100644 --- a/app/mobile/src/session/channels/Channels.jsx +++ b/app/mobile/src/session/channels/Channels.jsx @@ -5,46 +5,61 @@ import { useChannels } from './useChannels.hook'; import Ionicons from '@expo/vector-icons/AntDesign'; import { ChannelItem } from './channelItem/ChannelItem'; import Colors from 'constants/Colors'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; -export function Channels() { - const { state, actions } = useChannels(); +export function ChannelsTitle({ state, actions }) { return ( - - { state.tabbed && ( - - - - - - - - - New - - - )} - { !state.tabbed && ( - - - - - - - - )} - } - keyExtractor={item => (`${item.cardId}:${item.channelId}`)} - /> - { !state.tabbed && ( - - - New Topic - - )} + + + + + + + + + New + + + ); +} + +export function ChannelsBody({ state, actions, openConversation }) { + return ( + } + keyExtractor={item => (`${item.cardId}:${item.channelId}`)} + /> + ); + +} + +export function Channels({ openConversation }) { + const { state, actions } = useChannels(); + return ( + + + + + + + + + + } + keyExtractor={item => (`${item.cardId}:${item.channelId}`)} + /> + + + + + New Topic + + ); } diff --git a/app/mobile/src/session/channels/Channels.styled.js b/app/mobile/src/session/channels/Channels.styled.js index 44664974..bff21364 100644 --- a/app/mobile/src/session/channels/Channels.styled.js +++ b/app/mobile/src/session/channels/Channels.styled.js @@ -8,6 +8,12 @@ export const styles = StyleSheet.create({ display: 'flex', flexDirection: 'column', }, + title: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, topbar: { borderTopWidth: 1, borderBottomWidth: 1, @@ -21,6 +27,9 @@ export const styles = StyleSheet.create({ }, searchbar: { paddingRight: 8, + borderBottomWidth: 1, + borderColor: Colors.divider, + paddingBottom: 8, }, inputwrapper: { display: 'flex', @@ -29,18 +38,26 @@ export const styles = StyleSheet.create({ backgroundColor: Colors.white, alignItems: 'center', flexGrow: 1, + flexShrink: 1, + paddingTop: 4, + paddingBottom: 4, }, inputfield: { flex: 1, textAlign: 'center', padding: 4, color: Colors.text, - fontSize: 16, + fontSize: 14, }, icon: { paddingLeft: 8, }, + content: { + flexGrow: 1, + flexShrink: 1, + }, channels: { + flexShrink: 1, flexGrow: 1, width: '100%', paddingLeft: 16, @@ -68,6 +85,11 @@ export const styles = StyleSheet.create({ newtext: { paddingLeft: 8, color: Colors.white, - } + }, + bottomArea: { + paddingTop: 8, + borderTopWidth: 1, + borderColor: Colors.divider, + }, }) diff --git a/app/mobile/src/session/channels/channelItem/ChannelItem.jsx b/app/mobile/src/session/channels/channelItem/ChannelItem.jsx index a957aa35..3dff621b 100644 --- a/app/mobile/src/session/channels/channelItem/ChannelItem.jsx +++ b/app/mobile/src/session/channels/channelItem/ChannelItem.jsx @@ -1,15 +1,16 @@ -import { Text, TouchableOpacity, View } from 'react-native'; +import { Text, View } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; import { Logo } from 'utils/Logo'; import { styles } from './ChannelItem.styled'; import { useChannelItem } from './useChannelItem.hook'; -export function ChannelItem({ item }) { +export function ChannelItem({ item, openConversation }) { const { state, actions } = useChannelItem(item); return ( - - + openConversation(item.cardId, item.channelId, item.revision)}> + { item.subject } { item.message } diff --git a/app/mobile/src/session/channels/channelItem/ChannelItem.styled.js b/app/mobile/src/session/channels/channelItem/ChannelItem.styled.js index ac7682c1..72057e87 100644 --- a/app/mobile/src/session/channels/channelItem/ChannelItem.styled.js +++ b/app/mobile/src/session/channels/channelItem/ChannelItem.styled.js @@ -21,9 +21,11 @@ export const styles = StyleSheet.create({ }, subject: { color: Colors.text, + fontSize: 14, }, message: { color: Colors.disabled, + fontSize: 12, }, dot: { width: 8, diff --git a/app/mobile/src/session/channels/useChannels.hook.js b/app/mobile/src/session/channels/useChannels.hook.js index dda4bf24..1bb4950d 100644 --- a/app/mobile/src/session/channels/useChannels.hook.js +++ b/app/mobile/src/session/channels/useChannels.hook.js @@ -13,6 +13,7 @@ export function useChannels() { topic: null, channels: [], tabbed: null, + filter: null, }); const items = useRef([]); @@ -123,19 +124,42 @@ export function useChannels() { } } - return { cardId: item.cardId, channelId: item.channelId, contacts, logo, subject, message, updated, revision: item.revision }; + const timestamp = item?.summary?.lastTopic?.created; + + return { cardId: item.cardId, channelId: item.channelId, contacts, logo, subject, message, updated, revision: item.revision, timestamp, blocked: item.blocked === 1 }; } useEffect(() => { let merged = []; card.state.cards.forEach((card, cardId, map) => { - merged.push(...Array.from(card.channels.values())); + if (!card.blocked) { + merged.push(...Array.from(card.channels.values())); + } }); merged.push(...Array.from(channel.state.channels.values())); + + const items = merged.map(setChannelEntry); - merged.sort((a, b) => { - const aCreated = a?.summary?.lastTopic?.created; - const bCreated = b?.summary?.lastTopic?.created; + const filtered = items.filter(item => { + if (item.blocked === true) { + return false; + } + + if (!state.filter) { + return true; + } + const lower = state.filter.toLowerCase(); + if (item.subject) { + if (item.subject.toLowerCase().includes(lower)) { + return true; + } + } + return false; + }); + + const sorted = filtered.sort((a, b) => { + const aCreated = a?.timestamp; + const bCreated = b?.timestamp; if (aCreated === bCreated) { return 0; } @@ -145,13 +169,16 @@ export function useChannels() { return -1; }); - updateState({ channels: merged.map(item => setChannelEntry(item)) }); - }, [channel, card]); + updateState({ channels: sorted }); + }, [channel, card, state.filter]); const actions = { setTopic: (topic) => { updateState({ topic }); }, + setFilter: (filter) => { + updateState({ filter }); + }, }; return { state, actions }; diff --git a/app/mobile/src/session/contact/Contact.jsx b/app/mobile/src/session/contact/Contact.jsx index 0ebe441f..74704d06 100644 --- a/app/mobile/src/session/contact/Contact.jsx +++ b/app/mobile/src/session/contact/Contact.jsx @@ -1,12 +1,253 @@ import { useState, useContext } from 'react'; -import { View, TouchableOpacity, Text } from 'react-native'; +import { ScrollView, View, Alert, TouchableOpacity, Text } from 'react-native'; +import { useContact } from './useContact.hook'; +import { styles } from './Contact.styled'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Logo } from 'utils/Logo'; +import Ionicons from '@expo/vector-icons/AntDesign'; +import Colors from 'constants/Colors'; -export function Contact({ navigation, closeContact }) { - - const onPressCard = () => { - closeContact(); - } - - return CLOSE +export function ContactTitle({ contact, closeContact }) { + const { state, actions } = useContact(contact, closeContact); + return ({ `${state.handle}@${state.node}` }); +} + +export function Contact({ contact, closeContact }) { + + const { state, actions } = useContact(contact, closeContact); + + const getStatusText = (status) => { + if (status === 'confirmed') { + return 'saved'; + } + if (status === 'pending') { + return 'unknown contact request'; + } + if (status === 'connecting') { + return 'request sent'; + } + if (status === 'connected') { + return 'connected'; + } + if (status === 'requested') { + return 'request received'; + } + return 'unsaved'; + } + + const setContact = async (action) => { + try { + await action(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Failed to Update Contact', + 'Please try again.', + ); + } + } + + const disconnectContact = () => { + Alert.alert( + "Disconnecting Contact", + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Disconnect", onPress: () => { + setContact(actions.disconnectContact); + }} + ] + ); + } + + const saveAndConnect = () => { + setContact(actions.saveAndConnect); + } + + const saveContact = () => { + setContact(actions.saveContact); + } + + const ignoreContact = () => { + setContact(actions.ignoreContact); + } + + const deleteContact = () => { + Alert.alert( + "Deleting Contact", + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Delete", onPress: () => { + setContact(actions.deleteContact); + }} + ] + ); + } + + const closeDelete = () => { + Alert.alert( + "Deleting Contact", + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Delete", onPress: () => { + setContact(actions.closeDelete); + }} + ] + ); + } + + const blockContact = () => { + Alert.alert( + "Blocking Contact", + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Block", onPress: () => { + setContact(actions.blockContact); + }} + ] + ); + } + + const connectContact = () => { + setContact(actions.connectContact); + } + + const Body = () => { + return ( + + { `[${getStatusText(state.status)}]` } + + + + + + { state.name } + + + + { state.location } + + + + { state.description } + + + + { state.status === 'connected' && ( + <> + + Disconnect + + + Delete Contact + + + Block Contact + + + )} + { state.status === 'connecting' && ( + <> + + Cancel Request + + + Delete Contact + + + Block Contact + + + )} + { state.status === 'confirmed' && ( + <> + + Request Connection + + + Delete Contact + + + Block Contact + + + )} + { state.status === 'pending' && ( + <> + + Save and Connect + + + Save Contact + + + Ignore Request + + + Block Contact + + + )} + { state.status === 'requested' && ( + <> + + Accept Connection + + + Ignore Request + + + Deny Request + + + Delete Contact + + + Block Contact + + + )} + { state.status == null && ( + <> + + Save and Connect + + + Save Contact + + + )} + + + ); + } + + return ( + + { state.tabbed && ( + + )} + { !state.tabbed && ( + + + { `${state.handle}@${state.node}` } + + + + )} + + ) } diff --git a/app/mobile/src/session/contact/Contact.styled.js b/app/mobile/src/session/contact/Contact.styled.js new file mode 100644 index 00000000..13e8c006 --- /dev/null +++ b/app/mobile/src/session/contact/Contact.styled.js @@ -0,0 +1,103 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + paddingBottom: 32, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: Colors.formBackground, + }, + wrapper: { + backgroundColor: Colors.formBackground, + }, + title: { + fontSize: 18, + }, + drawer: { + paddingTop: 16, + }, + close: { + width: '100%', + display: 'flex', + alignItems: 'flex-end', + paddingRight: 32, + }, + header: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'center', + }, + status: { + color: Colors.grey, + paddingBottom: 20, + paddingTop: 4, + }, + headerText: { + fontSize: 16, + paddingRight: 4, + }, + camera: { + position: 'absolute', + bottom: 0, + left: 0, + padding: 8, + backgroundColor: Colors.lightgrey, + borderBottomLeftRadius: 8, + borderTopRightRadius: 8, + }, + gallery: { + position: 'absolute', + bottom: 0, + right: 0, + padding: 8, + backgroundColor: Colors.lightgrey, + borderBottomRightRadius: 8, + borderTopLeftRadius: 8, + }, + detail: { + paddingTop: 32, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + color: Colors.text, + }, + attribute: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + paddingBottom: 8, + }, + nametext: { + fontSize: 18, + paddingRight: 8, + fontWeight: 'bold', + }, + locationtext: { + fontSize: 16, + paddingLeft: 8, + }, + descriptiontext: { + fontSize: 16, + paddingLeft: 8 + }, + button: { + width: 192, + padding: 6, + backgroundColor: Colors.primary, + borderRadius: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginTop: 16, + }, + buttonText: { + color: Colors.white, + }, +}) + diff --git a/app/mobile/src/session/contact/useContact.hook.js b/app/mobile/src/session/contact/useContact.hook.js new file mode 100644 index 00000000..44a4342f --- /dev/null +++ b/app/mobile/src/session/contact/useContact.hook.js @@ -0,0 +1,167 @@ +import { useState, useEffect, useRef, useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { CardContext } from 'context/CardContext'; +import { useWindowDimensions } from 'react-native' +import { getListingMessage } from 'api/getListingMessage'; +import config from 'constants/Config'; + +export function useContact(contact, close) { + + const [state, setState] = useState({ + tabbed: null, + name: null, + handle: null, + node: null, + location: null, + description: null, + logo: null, + status: null, + cardId: null, + guid: null, + busy: false + }); + + const dimensions = useWindowDimensions(); + const card = useContext(CardContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + if (dimensions.width > config.tabbedWidth) { + updateState({ tabbed: false }); + } + else { + updateState({ tabbed: true }); + } + }, [dimensions]); + + useEffect(() => { + let stateSet = false; + if (contact?.card) { + const selected = card.state.cards.get(contact.card); + if (selected) { + const { profile, detail, cardId } = selected; + const { name, handle, node, location, description, guid, imageSet, revision } = profile; + const logo = imageSet ? card.actions.getCardLogo(cardId, revision) : 'avatar'; + updateState({ name, handle, node, location, description, logo, cardId, guid, status: detail.status }); + stateSet = true; + } + } + if (!stateSet && contact?.account) { + const { handle, name, node, logo, guid } = contact.account; + const selected = card.actions.getByGuid(guid); + if (selected) { + const { cardId, profile, detail } = selected; + const { name, handle, node, location, description, guid, imageSet, revision } = profile; + const logo = imageSet ? card.actions.getCardLogo(cardId, revision) : 'avatar'; + updateState({ name, handle, node, location, description, logo, cardId, guid, status: detail.status }); + stateSet = true; + } + else { + const { name, handle, node, location, description, logo, guid } = contact.account; + updateState({ name, handle, node, location, description, logo, guid, cardId: null, status: null }); + stateSet = true; + } + } + if (!stateSet) { + setState({}); + } + }, [contact, card]); + + const applyAction = async (action) => { + if (!state.busy) { + try { + updateState({ busy: true }); + await action(); + updateState({ busy: false }); + } + catch (err) { + console.log(err); + updateState({ busy: false }); + throw new Error("failed to update contact"); + } + } + else { + throw new Error("operation in progress"); + } + } + + const actions = { + saveAndConnect: async () => { + await applyAction(async () => { + let profile = await getListingMessage(state.node, state.guid); + let added = await card.actions.addCard(profile); + await card.actions.setCardConnecting(added.id); + let open = await card.actions.getCardOpenMessage(added.id); + let contact = await card.actions.setCardOpenMessage(state.node, open); + if (contact.status === 'connected') { + await card.actions.setCardConnected(added.id, contact.token, contact); + } + }); + }, + saveContact: async () => { + await applyAction(async () => { + let message = await getListingMessage(state.node, state.guid); + await card.actions.addCard(message); + }); + }, + disconnectContact: async () => { + await applyAction(async () => { + await card.actions.setCardConfirmed(state.cardId); + try { + let message = await card.actions.getCardCloseMessage(state.cardId); + await card.actions.setCardCloseMessage(state.node, message); + } + catch (err) { + console.log(err); + } + }); + }, + ignoreContact: async () => { + await applyAction(async () => { + await card.actions.setCardConfirmed(state.cardId); + }); + }, + closeDelete: async () => { + await applyAction(async () => { + await card.actions.setCardConfirmed(state.cardId); + try { + let message = await card.actions.getCardCloseMessage(state.cardId); + await card.actions.setCardCloseMessage(state.node, message); + } + catch (err) { + console.log(err); + } + await card.actions.removeCard(state.cardId); + close(); + }); + }, + deleteContact: async () => { + await applyAction(async () => { + await card.actions.removeCard(state.cardId); + close(); + }); + }, + connectContact: async () => { + await applyAction(async () => { + await card.actions.setCardConnecting(state.cardId); + let message = await card.actions.getCardOpenMessage(state.cardId); + let contact = await card.actions.setCardOpenMessage(state.node, message); + if (contact.status === 'connected') { + await card.actions.setCardConnected(state.cardId, contact.token, contact); + } + }); + }, + blockContact: async () => { + await applyAction(async () => { + await card.actions.setCardBlocked(state.cardId); + close(); + }); + }, + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/Conversation.jsx b/app/mobile/src/session/conversation/Conversation.jsx index 469194bc..e2da99f2 100644 --- a/app/mobile/src/session/conversation/Conversation.jsx +++ b/app/mobile/src/session/conversation/Conversation.jsx @@ -1,4 +1,104 @@ -export function Conversation() { - return <> +import { KeyboardAvoidingView, Platform, TextInput, View, TouchableOpacity, Text, } from 'react-native'; +import { FlatList, ScrollView } from '@stream-io/flat-list-mvcp'; +import { memo, useState, useRef, useEffect } from 'react'; +import { useConversation } from './useConversation.hook'; +import { styles } from './Conversation.styled'; +import { useNavigation } from '@react-navigation/native'; +import Ionicons from '@expo/vector-icons/AntDesign'; +import Colors from 'constants/Colors'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { AddTopic } from './addTopic/AddTopic'; +import { TopicItem } from './topicItem/TopicItem'; + +export function ConversationHeader({ closeConversation, openDetails }) { + const navigation = useNavigation(); + const { state, actions } = useConversation(); + + const setDetails = () => { + openDetails(navigation); + }; + const clearConversation = () => { + closeConversation(navigation); + }; + + return ( + + + { state.subject } + + + + + + ); } +const RenderItem = memo((props: { item: number }) => { + return () +}); + +const renderItemHandler = ({ item }: { item: number }) => { + return +} + +export function ConversationBody() { + const { state, actions } = useConversation(); + + const ref = useRef(); + + const latch = () => { + if (!state.momentum) { + actions.latch(); + ref.current.scrollToIndex({ animated: true, index: 0 }); + } + } + + const noop = () => {}; + + return ( + + item.topicId} + /> + + + + { !state.latched && ( + + + + )} + + + + ); +} + +export function Conversation({ closeConversation, openDetails }) { + const { state, actions } = useConversation(); + + return ( + + + { state.subject } + + + + + + + + + + + + ); +} diff --git a/app/mobile/src/session/conversation/Conversation.styled.js b/app/mobile/src/session/conversation/Conversation.styled.js new file mode 100644 index 00000000..1d7cb096 --- /dev/null +++ b/app/mobile/src/session/conversation/Conversation.styled.js @@ -0,0 +1,103 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + borderLeftWidth: 1, + borderColor: Colors.divider, + }, + header: { + width: '100%', + display: 'flex', + flexDirection: 'row', + borderBottomWidth: 1, + borderColor: Colors.divider, + padding: 8, + }, + body: { + flexGrow: 1, + flexShrink: 1, + width: '100%', + }, + title: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + subject: { + width: '100%', + flexGrow: 1, + flexShrink: 1, + textAlign: 'center', + paddingLeft: 16, + }, + subjectText: { + fontSize: 18, + textAlign: 'center', + }, + action: { + paddingLeft: 8, + }, + thread: { + flex: 1, + display: 'flex', + flexDirection: 'column', + }, + topics: { + flexShrink: 1, + flexGrow: 1, + minHeight: 0, + }, + close: { + flexGrow: 1, + justifyContent: 'flex-end', + alignItems: 'flex-end', + }, + add: { + borderTopWidth: 1, + borderColor: Colors.divider, + display: 'flex', + flexDirection: 'column', + }, + addButtons: { + display: 'flex', + flexDirection: 'row', + }, + addButton: { + width: 24, + height: 24, + }, + input: { + margin: 8, + padding: 8, + borderRadius: 4, + backgroundColor: Colors.white, + maxHeight: 64, + }, + addtopic: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + latchbar: { + position: 'absolute', + top: -26, + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + width: '100%', + }, + latch: { + backgroundColor: Colors.formBackground, + borderRadius: 12, + borderWidth: 1, + padding: 4, + borderColor: Colors.primary, + }, +}) + diff --git a/app/mobile/src/session/conversation/addTopic/AddTopic.jsx b/app/mobile/src/session/conversation/addTopic/AddTopic.jsx new file mode 100644 index 00000000..1bdf6e7c --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/AddTopic.jsx @@ -0,0 +1,236 @@ +import { ActivityIndicator, Modal, Image, FlatList, TextInput, Alert, View, TouchableOpacity, Text, } from 'react-native'; +import { useState, useRef } from 'react'; +import { useAddTopic } from './useAddTopic.hook'; +import { styles } from './AddTopic.styled'; +import AntIcons from '@expo/vector-icons/AntDesign'; +import MaterialIcons from '@expo/vector-icons/MaterialCommunityIcons'; +import Colors from 'constants/Colors'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import ImagePicker from 'react-native-image-crop-picker' +import { VideoFile } from './videoFile/VideoFile'; +import { AudioFile } from './audioFile/AudioFile'; +import { ImageFile } from './imageFile/ImageFile'; +import DocumentPicker from 'react-native-document-picker' +import ColorPicker from 'react-native-wheel-color-picker' + +export function AddTopic() { + + const { state, actions } = useAddTopic(); + const message = useRef(); + + const addImage = async () => { + try { + const full = await ImagePicker.openPicker({ mediaType: 'photo' }); + actions.addImage(full.path); + } + catch (err) { + console.log(err); + } + } + + const sendMessage = async () => { + try { + message.current.blur(); + await actions.addTopic(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Failed to Send Message', + 'Please try again.', + ) + } + } + + const addVideo = async () => { + try { + const full = await ImagePicker.openPicker({ mediaType: 'video' }); + actions.addVideo(full.path); + } + catch (err) { + console.log(err); + } + } + + const addAudio = async () => { + try { + const audio = await DocumentPicker.pickSingle({ + presentationStyle: 'fullScreen', + copyTo: 'cachesDirectory', + type: DocumentPicker.types.audio, + }) + actions.addAudio(audio.fileCopyUri, audio.name.replace(/\.[^/.]+$/, "")); + } catch (err) { + console.log(err); + } + } + + const remove = (item) => { + Alert.alert( + `Removing ${item.type} from message.`, + "Confirm?", + [ + { text: "Cancel", + onPress: () => {}, + }, + { text: "Remove", onPress: () => { + actions.removeAsset(item.key); + }} + ] + ); + } + + const renderAsset = ({ item }) => { + if (item.type === 'image') { + return ( + remove(item)} /> + ); + } + if (item.type === 'video') { + return ( + remove(item)} + setPosition={(position) => actions.setVideoPosition(item.key, position)} + /> + ) + } + if (item.type === 'audio') { + return ( + remove(item)} + setLabel={(label) => actions.setAudioLabel(item.key, label)} /> + ) + } + else { + return ( + + ); + } + } + + return ( + + { state.assets.length > 0 && ( + + )} + + + + + + + + + + + + + + + + + + + + + { state.busy && ( + + )} + { !state.busy && (state.message || state.assets.length > 0) && ( + + )} + { !state.busy && !(state.message || state.assets.length > 0) && ( + + )} + + + + + + Font Size: + + { state.size === 'small' && ( + + Small + + )} + { state.size !== 'small' && ( + actions.setFontSize('small')}> + Small + + )} + { state.size === 'medium' && ( + + Medium + + )} + { state.size !== 'medium' && ( + actions.setFontSize('medium')}> + Medium + + )} + { state.size === 'large' && ( + + Large + + )} + { state.size !== 'large' && ( + actions.setFontSize('large')}> + Large + + )} + + + + + Close + + + + + + + + + Font Color: + + + + + + Set Color: + + + + Close + + + + + + + ); +} + diff --git a/app/mobile/src/session/conversation/addTopic/AddTopic.styled.js b/app/mobile/src/session/conversation/addTopic/AddTopic.styled.js new file mode 100644 index 00000000..f8cb9359 --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/AddTopic.styled.js @@ -0,0 +1,141 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + add: { + borderTopWidth: 1, + borderColor: Colors.divider, + display: 'flex', + flexDirection: 'column', + }, + addButtons: { + display: 'flex', + flexDirection: 'row', + marginLeft: 12, + marginRight: 12, + marginBottom: 16, + }, + addButton: { + width: 36, + height: 36, + backgroundColor: Colors.white, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: Colors.divider, + borderRadius: 2, + marginLeft: 4, + marginRight: 4, + }, + input: { + marginLeft: 16, + marginRight: 16, + marginTop: 8, + marginBottom: 8, + padding: 8, + borderRadius: 4, + borderWidth: 1, + borderColor: Colors.divider, + backgroundColor: Colors.white, + maxHeight: 96, + minHeight: 52, + }, + space: { + height: 32, + flexGrow: 1, + }, + divider: { + borderWidth: 1, + borderColor: Colors.divider, + height: 32, + marginLeft: 8, + marginRight: 8, + }, + asset: { + width: 92, + height: 92, + marginRight: 8, + backgroundColor: 'yellow', + }, + carousel: { + paddingTop: 8, + paddingRight: 16, + paddingLeft: 16, + }, + editHeader: { + fontSize: 18, + paddingBottom: 16, + }, + editSize: { + width: '100%', + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 2, + }, + editColor: { + width: '100%', + height: 300, + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 2, + }, + editControls: { + display: 'flex', + flexDirection: 'row', + }, + editWrapper: { + display: 'flex', + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(52, 52, 52, 0.8)' + }, + editContainer: { + backgroundColor: Colors.formBackground, + padding: 16, + width: '80%', + maxWidth: 400, + }, + option: { + borderRadius: 8, + margin: 8, + borderColor: Colors.primary, + borderWidth: 1, + }, + optionText: { + padding: 8, + color: Colors.primary, + textAlign: 'center', + }, + selected: { + borderRadius: 8, + margin: 8, + borderColor: Colors.primary, + borderWidth: 1, + backgroundColor: Colors.primary, + }, + selectedText: { + padding: 8, + color: Colors.white, + textAlign: 'center', + }, + close: { + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 4, + padding: 8, + marginTop: 8, + width: 72, + display: 'flex', + alignItems: 'center', + }, + selection: { + flexGrow: 1, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, +}) + diff --git a/app/mobile/src/session/conversation/addTopic/audioFile/AudioFile.jsx b/app/mobile/src/session/conversation/addTopic/audioFile/AudioFile.jsx new file mode 100644 index 00000000..6cd15339 --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/audioFile/AudioFile.jsx @@ -0,0 +1,14 @@ +import { Image, View, TextInput, TouchableOpacity } from 'react-native'; +import audio from 'images/audio.png'; +import { styles } from './AudioFile.styled'; + +export function AudioFile({ path, remove, label, setLabel }) { + return ( + + + + + ) +} + diff --git a/app/mobile/src/session/conversation/addTopic/audioFile/AudioFile.styled.js b/app/mobile/src/session/conversation/addTopic/audioFile/AudioFile.styled.js new file mode 100644 index 00000000..305a7437 --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/audioFile/AudioFile.styled.js @@ -0,0 +1,25 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + audio: { + width: 92, + height: 92, + backgroundColor: Colors.white, + borderRadius: 4, + marginRight: 16, + display: 'flex', + alignItems: 'center', + }, + image: { + width: 92, + height: 92, + }, + input: { + position: 'absolute', + maxHeight: 50, + textAlign: 'center', + padding: 4, + } +}) + diff --git a/app/mobile/src/session/conversation/addTopic/imageFile/ImageFile.jsx b/app/mobile/src/session/conversation/addTopic/imageFile/ImageFile.jsx new file mode 100644 index 00000000..eaabf81d --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/imageFile/ImageFile.jsx @@ -0,0 +1,21 @@ +import { useRef, useEffect } from 'react'; +import { TouchableOpacity, View, Image } from 'react-native'; +import { useImageFile } from './useImageFile.hook'; +import { styles } from './ImageFile.styled'; +import Icons from '@expo/vector-icons/AntDesign'; +import Colors from 'constants/Colors'; + +export function ImageFile({ path, setPosition, remove }) { + + const { state, actions } = useImageFile(); + + useEffect(() => { + Image.getSize(path, actions.setInfo); + }, [path]); + + return ( + + + + ); +} diff --git a/app/mobile/src/session/conversation/addTopic/imageFile/ImageFile.styled.js b/app/mobile/src/session/conversation/addTopic/imageFile/ImageFile.styled.js new file mode 100644 index 00000000..9c879c72 --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/imageFile/ImageFile.styled.js @@ -0,0 +1,17 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + overlay: { + marginRight: 16, + position: 'absolute', + bottom: 0, + right: 0, + padding: 2, + borderTopLeftRadius: 4, + backgroundColor: Colors.white, + borderWidth: 1, + borderColor: Colors.divider, + }, +}) + diff --git a/app/mobile/src/session/conversation/addTopic/imageFile/useImageFile.hook.js b/app/mobile/src/session/conversation/addTopic/imageFile/useImageFile.hook.js new file mode 100644 index 00000000..c82d5c5c --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/imageFile/useImageFile.hook.js @@ -0,0 +1,22 @@ +import { useState, useRef, useEffect, useContext } from 'react'; +import { ConversationContext } from 'context/ConversationContext'; + +export function useImageFile() { + + const [state, setState] = useState({ + ratio: 1, + }); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + setInfo: (width, height) => { + updateState({ ratio: width / height }); + }, + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js new file mode 100644 index 00000000..f2e839cb --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js @@ -0,0 +1,113 @@ +import { useState, useRef, useEffect, useContext } from 'react'; +import { ConversationContext } from 'context/ConversationContext'; +import { Image } from 'react-native'; +import Colors from 'constants/Colors'; + +export function useAddTopic(cardId, channelId) { + + const [state, setState] = useState({ + message: null, + assets: [], + fontSize: false, + fontColor: false, + size: 'medium', + sizeSet: false, + color: Colors.text, + colorSet: false, + busy: false, + }); + + const assetId = useRef(0); + const conversation = useContext(ConversationContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + setMessage: (message) => { + updateState({ message }); + }, + addImage: (data) => { + assetId.current++; + Image.getSize(data, (width, height) => { + const asset = { key: assetId.current, type: 'image', data: data, ratio: width/height }; + updateState({ assets: [ ...state.assets, asset ] }); + }); + }, + addVideo: (data) => { + assetId.current++; + const asset = { key: assetId.current, type: 'video', data: data, ratio: 1, duration: 0, position: 0 }; + updateState({ assets: [ ...state.assets, asset ] }); + }, + addAudio: (data, label) => { + assetId.current++; + const asset = { key: assetId.current, type: 'audio', data: data, label }; + updateState({ assets: [ ...state.assets, asset ] }); + }, + setVideoPosition: (key, position) => { + updateState({ assets: state.assets.map((item) => { + if(item.key === key) { + return { ...item, position }; + } + return item; + }) + }); + }, + setAudioLabel: (key, label) => { + updateState({ assets: state.assets.map((item) => { + if(item.key === key) { + return { ...item, label }; + } + return item; + }) + }); + }, + removeAsset: (key) => { + updateState({ assets: state.assets.filter(item => (item.key !== key))}); + }, + showFontColor: () => { + updateState({ fontColor: true }); + }, + hideFontColor: () => { + updateState({ fontColor: false }); + }, + showFontSize: () => { + updateState({ fontSize: true }); + }, + hideFontSize: () => { + updateState({ fontSize: false }); + }, + setFontSize: (size) => { + updateState({ size, sizeSet: true }); + }, + setFontColor: (color) => { + updateState({ color, colorSet: true }); + }, + addTopic: async () => { + if (!state.busy) { + try { + updateState({ busy: true }); + let message = { + text: state.message, + textColor: state.colorSet ? state.color : null, + textSize: state.sizeSet ? state.size : null, + }; + await conversation.actions.addTopic(message, state.assets); + updateState({ busy: false, assets: [], message: null, + size: 'medium', sizeSet: false, + color: Colors.text, colorSet: false, + }); + } + catch(err) { + console.log(err); + updateState({ busy: false }); + throw new Error("failed to add message"); + } + } + }, + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/addTopic/videoFile/VideoFile.jsx b/app/mobile/src/session/conversation/addTopic/videoFile/VideoFile.jsx new file mode 100644 index 00000000..d46b3ca0 --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/videoFile/VideoFile.jsx @@ -0,0 +1,39 @@ +import { useRef, useEffect } from 'react'; +import { TouchableOpacity, View } from 'react-native'; +import Video from 'react-native-video'; +import { useVideoFile } from './useVideoFile.hook'; +import { styles } from './VideoFile.styled'; +import Icons from '@expo/vector-icons/MaterialCommunityIcons'; +import Colors from 'constants/Colors'; + +export function VideoFile({ path, setPosition, remove }) { + + const { state, actions } = useVideoFile(); + + const video = useRef(); + + useEffect(() => { + if (video.current) { + video.current.seek(state.position); + setPosition(state.position); + } + }, [state.position]); + + const setInfo = ({ naturalSize, duration }) => { + if (video.current) { + video.current.seek(0); + } + actions.setInfo(naturalSize.width, naturalSize.height, duration); + } + + return ( + + + ); +} diff --git a/app/mobile/src/session/conversation/addTopic/videoFile/VideoFile.styled.js b/app/mobile/src/session/conversation/addTopic/videoFile/VideoFile.styled.js new file mode 100644 index 00000000..db332c1e --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/videoFile/VideoFile.styled.js @@ -0,0 +1,14 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + overlay: { + marginRight: 16, + position: 'absolute', + bottom: 0, + right: 0, + padding: 2, + borderTopLeftRadius: 4, + }, +}) + diff --git a/app/mobile/src/session/conversation/addTopic/videoFile/useVideoFile.hook.js b/app/mobile/src/session/conversation/addTopic/videoFile/useVideoFile.hook.js new file mode 100644 index 00000000..05d99b74 --- /dev/null +++ b/app/mobile/src/session/conversation/addTopic/videoFile/useVideoFile.hook.js @@ -0,0 +1,31 @@ +import { useState, useRef, useEffect, useContext } from 'react'; +import { ConversationContext } from 'context/ConversationContext'; + +export function useVideoFile() { + + const [state, setState] = useState({ + duration: 0, + position: 0, + ratio: 1, + }); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + setInfo: (width, height, duration) => { + updateState({ ratio: width / height, duration: Math.floor(duration) }); + }, + setNextPosition: () => { + if (state.duration) { + const step = Math.floor(1 + state.duration / 20); + const position = (state.position + step ) % state.duration; + updateState({ position }); + } + }, + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/topicItem/TopicItem.jsx b/app/mobile/src/session/conversation/topicItem/TopicItem.jsx new file mode 100644 index 00000000..518d68be --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/TopicItem.jsx @@ -0,0 +1,105 @@ +import { FlatList, View, Text, TouchableOpacity, Modal } from 'react-native'; +import { useTopicItem } from './useTopicItem.hook'; +import { styles } from './TopicItem.styled'; +import { Logo } from 'utils/Logo'; +import Colors from 'constants/Colors'; +import { VideoThumb } from './videoThumb/VideoThumb'; +import { AudioThumb } from './audioThumb/AudioThumb'; +import { ImageThumb } from './imageThumb/ImageThumb'; +import { ImageAsset } from './imageAsset/ImageAsset'; +import { AudioAsset } from './audioAsset/AudioAsset'; +import { VideoAsset } from './videoAsset/VideoAsset'; +import AntIcons from '@expo/vector-icons/AntDesign'; +import Carousel from 'react-native-snap-carousel'; +import GestureRecognizer from 'react-native-swipe-gestures'; + +export function TopicItem({ item }) { + + const { state, actions } = useTopicItem(item); + + const renderAsset = (asset) => { + return ( + + { asset.item.image && ( + + )} + { asset.item.video && ( + + )} + { asset.item.audio && ( + actions.setActive(asset.dataIndex)} /> + )} + + ) + } + + const renderThumb = (thumb) => { + return ( + + { thumb.item.image && ( + actions.showCarousel(thumb.index)} /> + )} + { thumb.item.video && ( + actions.showCarousel(thumb.index)} /> + )} + { thumb.item.audio && ( + actions.showCarousel(thumb.index)} /> + )} + + ); + } + + return ( + + + + { state.name } + { state.timestamp } + + { state.status === 'confirmed' && ( + <> + { state.transform === 'complete' && state.assets && ( + + )} + { state.transform === 'incomplete' && ( + + )} + { state.transform === 'error' && ( + + )} + { state.message && ( + { state.message } + )} + + )} + { state.status !== 'confirmed' && ( + + )} + + + + + + + + + ); +} + diff --git a/app/mobile/src/session/conversation/topicItem/TopicItem.styled.js b/app/mobile/src/session/conversation/topicItem/TopicItem.styled.js new file mode 100644 index 00000000..92a785e1 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/TopicItem.styled.js @@ -0,0 +1,43 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + item: { + borderTopWidth: 1, + borderColor: Colors.white, + paddingTop: 8, + paddingBottom: 8, + }, + header: { + display: 'flex', + flexDirection: 'row', + paddingLeft: 16, + }, + name: { + paddingLeft: 8, + }, + timestamp: { + paddingLeft: 8, + fontSize: 11, + paddingTop: 2, + color: Colors.grey, + }, + carousel: { + paddingLeft: 52, + marginTop: 4, + marginBottom: 4, + }, + modal: { + width: '100%', + height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.9)', + }, + frame: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '100%', + }, +}) + diff --git a/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.jsx b/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.jsx new file mode 100644 index 00000000..58b48ce8 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.jsx @@ -0,0 +1,34 @@ +import { Text, Image, View, TouchableOpacity } from 'react-native'; +import Colors from 'constants/Colors'; +import { useAudioAsset } from './useAudioAsset.hook'; +import { styles } from './AudioAsset.styled'; +import audio from 'images/audio.png'; +import Icons from '@expo/vector-icons/MaterialCommunityIcons'; + +export function AudioAsset({ topicId, asset, active, setActive }) { + + const { state, actions } = useAudioAsset(topicId, asset); + + const play = () => { + actions.play(); + setActive(); + } + + return ( + + + { asset.label } + { state.playing && active && ( + + + + )} + { (!state.playing || !active) && ( + + + + )} + + ); +} + diff --git a/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.styled.js b/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.styled.js new file mode 100644 index 00000000..bb81ddf8 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.styled.js @@ -0,0 +1,23 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + background: { + backgroundColor: Colors.lightgrey, + borderRadius: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + label: { + position: 'absolute', + textAlign: 'center', + fontSize: 20, + paddingTop: 8, + top: 0, + }, + control: { + position: 'absolute', + } +}) + diff --git a/app/mobile/src/session/conversation/topicItem/audioAsset/useAudioAsset.hook.js b/app/mobile/src/session/conversation/topicItem/audioAsset/useAudioAsset.hook.js new file mode 100644 index 00000000..70dfcb07 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/audioAsset/useAudioAsset.hook.js @@ -0,0 +1,49 @@ +import { useState, useRef, useEffect, useContext } from 'react'; +import { ConversationContext } from 'context/ConversationContext'; +import { Image } from 'react-native'; +import { useWindowDimensions } from 'react-native'; +import SoundPlayer from 'react-native-sound-player' + +export function useAudioAsset(topicId, asset) { + + const [state, setState] = useState({ + length: null, + playing: false, + }); + + const conversation = useContext(ConversationContext); + const dimensions = useWindowDimensions(); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + if (dimensions.width < dimensions.height) { + updateState({ length: 0.8 * dimensions.width }); + } + else { + updateState({ length: 0.8 * dimensions.height }); + } + }, [dimensions]); + + useEffect(() => { + const url = conversation.actions.getTopicAssetUrl(topicId, asset.full); + updateState({ url, playing: false }); + return () => { SoundPlayer.stop() } + }, [topicId, conversation, asset]); + + const actions = { + play: () => { + SoundPlayer.playUrl(state.url); + updateState({ playing: true }); + }, + pause: () => { + SoundPlayer.stop(); + updateState({ playing: false }); + }, + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.jsx b/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.jsx new file mode 100644 index 00000000..9d31c7ed --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.jsx @@ -0,0 +1,21 @@ +import { View, Text, Image, TouchableOpacity } from 'react-native'; +import { styles } from './AudioThumb.styled'; +import Colors from 'constants/Colors'; +import audio from 'images/audio.png'; + +export function AudioThumb({ topicId, asset, onAssetView }) { + + return ( + + + { asset.label && ( + + { asset.label } + + )} + + ); + +} + + diff --git a/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.styled.js b/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.styled.js new file mode 100644 index 00000000..695265d2 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.styled.js @@ -0,0 +1,18 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + overlay: { + top: 0, + width: '100%', + display: 'flex', + alignItems: 'center', + position: 'absolute', + paddingRight: 16, + maxHeight: 50, + }, + label: { + textOverlay: 'center', + }, +}) + diff --git a/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx b/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx new file mode 100644 index 00000000..dd831a66 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx @@ -0,0 +1,13 @@ +import { Image } from 'react-native'; +import { useImageAsset } from './useImageAsset.hook'; +import { styles } from './ImageAsset.styled'; +import Colors from 'constants/Colors'; + +export function ImageAsset({ topicId, asset }) { + const { state, actions } = useImageAsset(topicId, asset); + + return ( + + ); +} + diff --git a/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.styled.js b/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.styled.js new file mode 100644 index 00000000..9c879c72 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.styled.js @@ -0,0 +1,17 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + overlay: { + marginRight: 16, + position: 'absolute', + bottom: 0, + right: 0, + padding: 2, + borderTopLeftRadius: 4, + backgroundColor: Colors.white, + borderWidth: 1, + borderColor: Colors.divider, + }, +}) + diff --git a/app/mobile/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js b/app/mobile/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js new file mode 100644 index 00000000..7d1f5dab --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js @@ -0,0 +1,58 @@ +import { useState, useRef, useEffect, useContext } from 'react'; +import { ConversationContext } from 'context/ConversationContext'; +import { Image } from 'react-native'; +import { useWindowDimensions } from 'react-native'; + +export function useImageAsset(topicId, asset) { + + const [state, setState] = useState({ + frameWidth: 1, + frameHeight: 1, + imageRatio: 1, + imageWidth: 1, + imageHeight: 1, + url: null, + }); + + const conversation = useContext(ConversationContext); + const dimensions = useWindowDimensions(); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + const frameRatio = state.frameWidth / state.frameHeight; + if (frameRatio > state.imageRatio) { + //height constrained + const height = 0.9 * state.frameHeight; + const width = height * state.imageRatio; + updateState({ imageWidth: width, imageHeight: height }); + } + else { + //width constrained + const width = 0.9 * state.frameWidth; + const height = width / state.imageRatio; + updateState({ imageWidth: width, imageHeight: height }); + } + }, [state.frameWidth, state.frameHeight, state.imageRatio]); + + useEffect(() => { + updateState({ frameWidth: dimensions.width, frameHeight: dimensions.height }); + }, [dimensions]); + + useEffect(() => { + const url = conversation.actions.getTopicAssetUrl(topicId, asset.full); + if (url) { + Image.getSize(url, (width, height) => { + updateState({ url, imageRatio: width / height }); + }); + } + }, [topicId, conversation, asset]); + + const actions = { + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.jsx b/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.jsx new file mode 100644 index 00000000..8f9c7ed6 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.jsx @@ -0,0 +1,17 @@ +import { Image, TouchableOpacity } from 'react-native'; +import { useImageThumb } from './useImageThumb.hook'; +import { styles } from './ImageThumb.styled'; +import Colors from 'constants/Colors'; + +export function ImageThumb({ topicId, asset, onAssetView }) { + const { state, actions } = useImageThumb(topicId, asset); + + return ( + + + + ); + +} + + diff --git a/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.styled.js b/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.styled.js new file mode 100644 index 00000000..9c879c72 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.styled.js @@ -0,0 +1,17 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + overlay: { + marginRight: 16, + position: 'absolute', + bottom: 0, + right: 0, + padding: 2, + borderTopLeftRadius: 4, + backgroundColor: Colors.white, + borderWidth: 1, + borderColor: Colors.divider, + }, +}) + diff --git a/app/mobile/src/session/conversation/topicItem/imageThumb/useImageThumb.hook.js b/app/mobile/src/session/conversation/topicItem/imageThumb/useImageThumb.hook.js new file mode 100644 index 00000000..3e030a1e --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/imageThumb/useImageThumb.hook.js @@ -0,0 +1,32 @@ +import { useState, useRef, useEffect, useContext } from 'react'; +import { ConversationContext } from 'context/ConversationContext'; +import { Image } from 'react-native'; + +export function useImageThumb(topicId, asset) { + + const [state, setState] = useState({ + ratio: 1, + url: null, + }); + + const conversation = useContext(ConversationContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + const url = conversation.actions.getTopicAssetUrl(topicId, asset.thumb); + if (url) { + Image.getSize(url, (width, height) => { + updateState({ url, ratio: width / height }); + }); + } + }, [topicId, conversation, asset]); + + const actions = { + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js new file mode 100644 index 00000000..0080f637 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js @@ -0,0 +1,141 @@ +import { useState, useEffect, useContext } from 'react'; +import { CardContext } from 'context/CardContext'; +import { ProfileContext } from 'context/ProfileContext'; +import moment from 'moment'; +import { useWindowDimensions } from 'react-native'; +import Colors from 'constants/Colors'; + +export function useTopicItem(item) { + + const [state, setState] = useState({ + name: null, + known: null, + logo: null, + timestamp: null, + message: null, + carousel: false, + carouselIndex: 0, + width: null, + height: null, + activeId: null, + fontSize: 14, + fontColor: Colors.text, + }); + + const profile = useContext(ProfileContext); + const card = useContext(CardContext); + const dimensions = useWindowDimensions(); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + updateState({ width: dimensions.width, height: dimensions.height }); + }, [dimensions]); + + useEffect(() => { + const { topicId, detail } = item; + const { guid, data, status, transform } = detail; + + let name, known, logo; + const identity = profile.state?.profile; + if (guid === identity.guid) { + known = true; + if (identity.name) { + name = identity.name; + } + else { + name = `${identity.handle}@${identity.node}`; + } + const img = profile.actions.getImageUrl(); + if (img) { + logo = img; + } + else { + logo = 'avatar'; + } + } + else { + const contact = card.actions.getByGuid(guid); + if (contact) { + if (contact.profile.imageSet) { + logo = card.actions.getCardLogo(contact.cardId, contact.revision); + } + else { + logo = 'avatar'; + } + + known = true; + if (contact.profile.name) { + name = contact.profile.name; + } + else { + name = `${contact.handle}@${contact.node}`; + } + } + else { + name = "unknown"; + known = false; + logo = 'avatar'; + } + } + + let message, assets, fontSize, fontColor; + try { + const data = JSON.parse(item.detail.data); + message = data.text; + assets = data.assets; + if (data.textSize === 'small') { + fontSize = 10; + } + else if (data.textSize === 'large') { + fontSize = 20; + } + else { + fontSize = 14; + } + if (data.textColor) { + fontColor = data.textColor; + } + else { + fontColor = Colors.text; + } + } + catch (err) { + console.log("empty message"); + } + + + let timestamp; + const date = new Date(item.detail.created * 1000); + const now = new Date(); + const offset = now.getTime() - date.getTime(); + if(offset < 86400000) { + timestamp = moment(date).format('h:mma'); + } + else if (offset < 31449600000) { + timestamp = moment(date).format('M/DD'); + } + else { + timestamp = moment(date).format('M/DD/YYYY'); + } + + updateState({ logo, name, known, message, fontSize, fontColor, timestamp, transform, status, assets }); + }, [card, item]); + + const actions = { + showCarousel: (index) => { + updateState({ carousel: true, carouselIndex: index }); + }, + hideCarousel: () => { + updateState({ carousel: false }); + }, + setActive: (activeId) => { + updateState({ activeId }); + }, + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx b/app/mobile/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx new file mode 100644 index 00000000..778189a7 --- /dev/null +++ b/app/mobile/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx @@ -0,0 +1,20 @@ +import { Image, View, TouchableOpacity } from 'react-native'; +import Colors from 'constants/Colors'; +import { Video, AVPlaybackStatus } from 'expo-av'; +import { useVideoAsset } from './useVideoAsset.hook'; + +export function VideoAsset({ topicId, asset }) { + + const { state, actions } = useVideoAsset(topicId, asset); + + return ( + <> + { state.url && ( + - Visible in Registry + setVisible(!state.searchable)} activeOpacity={1}> + Visible in Registry + - + + Change Login + + + Manage Blocked Contacts + + + Manage Blocked Topics + + Logout + ); + }; + + return ( + + { state.tabbed && ( + + + + )} + { !state.tabbed && ( + + + { `${state.handle}@${state.node}` } + + + + )} + + + + Blocked Contacts: + + + + + + Close + + + + + + + + + Blocked Topics: + + + + + + Close + + + + + - + Edit Details: + autoCapitalize="words" placeholder="Name" /> + autoCapitalize="words" placeholder="Location" /> + autoCapitalize="sentences" placeholder="Description" multiline={true} /> @@ -132,7 +237,7 @@ export function Profile() { - + - + Change Login: - - - - - - + autoCapitalize={'none'} placeholder="Username" /> + { state.checked && state.available && ( + + )} + { state.checked && !state.available && ( + + )} + { !state.showPassword && ( + + + + + + + )} + { state.showPassword && ( + + + + + + + )} + { !state.showConfirm && ( + + + + + + + )} + { state.showConfirm && ( + + + + + + + )} Cancel - - Save - + { enabled && ( + + Save + + )} + { !enabled && ( + + Save + + )} - + ) diff --git a/app/mobile/src/session/profile/Profile.styled.js b/app/mobile/src/session/profile/Profile.styled.js index c2e8dc70..02280261 100644 --- a/app/mobile/src/session/profile/Profile.styled.js +++ b/app/mobile/src/session/profile/Profile.styled.js @@ -11,9 +11,27 @@ export const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - header: { - paddingBottom: 32, + drawer: { paddingTop: 16, + backgroundColor: Colors.formBackground, + }, + titleText: { + fontSize: 18, + }, + title: { + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + flex: 1, + width: '100%', + textAlign: 'start', + alignItems: 'center', + justifyContent: 'center', + }, + body: { + paddingTop: 16, + }, + header: { display: 'flex', flexDirection: 'row', alignItems: 'flex-end', @@ -22,7 +40,7 @@ export const styles = StyleSheet.create({ headerText: { fontSize: 16, paddingRight: 4, - textDecorationLine: 'underline', + color: Colors.text, }, camera: { position: 'absolute', @@ -117,9 +135,15 @@ export const styles = StyleSheet.create({ maxWidth: 400, }, editHeader: { - fontSize: 20, + fontSize: 18, paddingBottom: 16, }, + editList: { + width: '100%', + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 2, + }, inputField: { width: '100%', borderWidth: 1, @@ -128,16 +152,28 @@ export const styles = StyleSheet.create({ padding: 8, marginBottom: 8, maxHeight: 92, + display: 'flex', + flexDirection: 'row', }, input: { - fontSize: 16, - width: '100%', + fontSize: 14, + flexGrow: 1, }, editControls: { display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', }, + close: { + borderWidth: 1, + borderColor: Colors.lightgrey, + borderRadius: 4, + padding: 8, + marginTop: 8, + width: 72, + display: 'flex', + alignItems: 'center', + }, cancel: { borderWidth: 1, borderColor: Colors.lightgrey, @@ -148,6 +184,18 @@ export const styles = StyleSheet.create({ display: 'flex', alignItems: 'center', }, + disabled: { + borderWidth: 1, + borderColor: Colors.lightgrey, + padding: 8, + borderRadius: 4, + width: 72, + display: 'flex', + alignItems: 'center', + }, + disabledText: { + color: Colors.disabled, + }, save: { padding: 8, borderRadius: 4, @@ -156,6 +204,12 @@ export const styles = StyleSheet.create({ display: 'flex', alignItems: 'center', }, + link: { + marginTop: 16, + }, + linkText: { + color: Colors.primary, + }, saveText: { color: Colors.white, } diff --git a/app/mobile/src/session/profile/blockedContacts/BlockedContacts.jsx b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.jsx new file mode 100644 index 00000000..e8c0df4b --- /dev/null +++ b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.jsx @@ -0,0 +1,48 @@ +import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native'; +import { styles } from './BlockedContacts.styled'; +import { useBlockedContacts } from './useBlockedContacts.hook'; +import { Logo } from 'utils/Logo'; + +export function BlockedContacts() { + + const { state, actions } = useBlockedContacts(); + + const unblock = (cardId) => { + Alert.alert( + 'Unblocking Contact', + 'Confirm?', + [ + { text: "Cancel", onPress: () => {}, }, + { text: "Unblock", onPress: () => actions.unblock(cardId) }, + ], + ); + }; + + const BlockedItem = ({ item }) => { + return ( + unblock(item.cardId)}> + + + { item.name } + { item.handle } + + + ) + } + + return ( + + { state.cards.length === 0 && ( + No Blocked Contacts + )} + { state.cards.length !== 0 && ( + } + keyExtractor={item => item.cardId} + /> + )} + + ); +} + diff --git a/app/mobile/src/session/profile/blockedContacts/BlockedContacts.styled.js b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.styled.js new file mode 100644 index 00000000..18a5c33d --- /dev/null +++ b/app/mobile/src/session/profile/blockedContacts/BlockedContacts.styled.js @@ -0,0 +1,43 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + backgroundColor: Colors.white, + display: 'flex', + width: '100%', + justifyContent: 'center', + fontSize: 14, + height: 200, + }, + default: { + textAlign: 'center', + color: Colors.grey, + }, + item: { + width: '100%', + display: 'flex', + flexDirection: 'row', + height: 48, + paddingLeft: 16, + alignItems: 'center', + borderBottomWidth: 1, + borderColor: Colors.itemDivider, + }, + detail: { + paddingLeft: 12, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + flexGrow: 1, + flexShrink: 1, + }, + name: { + color: Colors.text, + fontSize: 14, + }, + handle: { + color: Colors.text, + fontSize: 12, + }, +}); diff --git a/app/mobile/src/session/profile/blockedContacts/useBlockedContacts.hook.js b/app/mobile/src/session/profile/blockedContacts/useBlockedContacts.hook.js new file mode 100644 index 00000000..6fe77f4e --- /dev/null +++ b/app/mobile/src/session/profile/blockedContacts/useBlockedContacts.hook.js @@ -0,0 +1,53 @@ +import { useState, useEffect, useContext } from 'react'; +import { CardContext } from 'context/CardContext'; + +export function useBlockedContacts() { + + const [state, setState] = useState({ + cards: [], + }); + + const card = useContext(CardContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const setCardItem = (item) => { + const { profile } = item; + return { + cardId: item.cardId, + name: profile.name, + handle: `${profile.handle}@${profile.node}`, + blocked: item.blocked, + logo: profile.imageSet ? card.actions.getCardLogo(item.cardId, item.revision) : 'avatar', + } + }; + + useEffect(() => { + const cards = Array.from(card.state.cards.values()); + const items = cards.map(setCardItem); + const filtered = items.filter(item => { + return item.blocked; + }); + filtered.sort((a, b) => { + if (a.name === b.name) { + return 0; + } + if (!a.name || (a.name < b.name)) { + return -1; + } + return 1; + }); + updateState({ cards: filtered }); + }, [card]); + + const actions = { + unblock: async (cardId) => { + await card.actions.clearCardBlocked(cardId); + } + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx new file mode 100644 index 00000000..cb788d41 --- /dev/null +++ b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.jsx @@ -0,0 +1,47 @@ +import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native'; +import { styles } from './BlockedTopics.styled'; +import { useBlockedTopics } from './useBlockedTopics.hook'; +import { Logo } from 'utils/Logo'; + +export function BlockedTopics() { + + const { state, actions } = useBlockedTopics(); + + const unblock = (cardId, channelId) => { + Alert.alert( + 'Unblocking Contact', + 'Confirm?', + [ + { text: "Cancel", onPress: () => {}, }, + { text: "Unblock", onPress: () => actions.unblock(cardId, channelId) }, + ], + ); + }; + + const BlockedItem = ({ item }) => { + return ( + unblock(item.cardId, item.channelId)}> + + { item.name } + { item.created } + + + ) + } + + return ( + + { state.channels.length === 0 && ( + No Blocked Topics + )} + { state.channels.length !== 0 && ( + } + keyExtractor={item => item.id} + /> + )} + + ); +} + diff --git a/app/mobile/src/session/profile/blockedTopics/BlockedTopics.styled.js b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.styled.js new file mode 100644 index 00000000..c29efdcb --- /dev/null +++ b/app/mobile/src/session/profile/blockedTopics/BlockedTopics.styled.js @@ -0,0 +1,46 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + backgroundColor: Colors.white, + display: 'flex', + width: '100%', + justifyContent: 'center', + fontSize: 14, + height: 200, + }, + default: { + textAlign: 'center', + color: Colors.grey, + }, + item: { + width: '100%', + display: 'flex', + flexDirection: 'row', + height: 32, + paddingLeft: 16, + alignItems: 'center', + borderBottomWidth: 1, + borderColor: Colors.itemDivider, + }, + detail: { + paddingLeft: 12, + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + width: '100%', + }, + name: { + color: Colors.text, + fontSize: 14, + flexGrow: 1, + flexShrink: 1, + minWidth: 0, + }, + created: { + color: Colors.text, + fontSize: 12, + paddingRight: 16, + }, +}); diff --git a/app/mobile/src/session/profile/blockedTopics/useBlockedTopics.hook.js b/app/mobile/src/session/profile/blockedTopics/useBlockedTopics.hook.js new file mode 100644 index 00000000..ff817e48 --- /dev/null +++ b/app/mobile/src/session/profile/blockedTopics/useBlockedTopics.hook.js @@ -0,0 +1,122 @@ +import { useState, useEffect, useContext } from 'react'; +import { CardContext } from 'context/CardContext'; +import { ChannelContext } from 'context/ChannelContext'; +import { ProfileContext } from 'context/ProfileContext'; +import moment from 'moment'; + +export function useBlockedTopics() { + + const [state, setState] = useState({ + channels: [] + }); + + const profile = useContext(ProfileContext); + const card = useContext(CardContext); + const channel = useContext(ChannelContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const getCard = (guid) => { + let contact = null + card.state.cards.forEach((card, cardId, map) => { + if (card?.profile?.guid === guid) { + contact = card; + } + }); + return contact; + } + + const setChannelItem = (item) => { + let timestamp; + const date = new Date(item.detail.created * 1000); + const now = new Date(); + const offset = now.getTime() - date.getTime(); + if(offset < 86400000) { + timestamp = moment(date).format('h:mma'); + } + else if (offset < 31449600000) { + timestamp = moment(date).format('M/DD'); + } + else { + timestamp = moment(date).format('M/DD/YYYY'); + } + + let contacts = []; + if (item.cardId) { + contacts.push(card.state.cards.get(item.cardId)); + } + if (item?.detail?.members) { + const profileGuid = profile.state.profile.guid; + item.detail.members.forEach(guid => { + if (profileGuid !== guid) { + const contact = getCard(guid); + contacts.push(contact); + } + }) + } + + let subject; + if (item?.detail?.data) { + try { + topic = JSON.parse(item?.detail?.data).subject; + subject = topic; + } + catch (err) { + console.log(err); + } + } + if (!subject) { + if (contacts.length) { + let names = []; + for (let contact of contacts) { + if (contact?.profile?.name) { + names.push(contact.profile.name); + } + else if (contact?.profile?.handle) { + names.push(contact?.profile?.handle); + } + } + subject = names.join(', '); + } + else { + subject = "Notes"; + } + } + + return { + id: `${item.cardId}:${item.channelId}`, + cardId: item.cardId, + channelId: item.channelId, + name: subject, + blocked: item.blocked, + created: timestamp, + } + }; + + useEffect(() => { + let merged = []; + card.state.cards.forEach((card, cardId, map) => { + merged.push(...Array.from(card.channels.values())); + }); + merged.push(...Array.from(channel.state.channels.values())); + const items = merged.map(setChannelItem); + const filtered = items.filter(item => item.blocked); + updateState({ channels: filtered }); + }, [card, channel]); + + const actions = { + unblock: async (cardId, channelId) => { + if (cardId) { + await card.actions.clearChannelBlocked(cardId, channelId); + } + else { + await channel.actions.clearBlocked(channelId); + } + } + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/profile/useProfile.hook.js b/app/mobile/src/session/profile/useProfile.hook.js index 0c71699e..9e29199b 100644 --- a/app/mobile/src/session/profile/useProfile.hook.js +++ b/app/mobile/src/session/profile/useProfile.hook.js @@ -1,8 +1,10 @@ import { useState, useEffect, useRef, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useWindowDimensions } from 'react-native'; import { ProfileContext } from 'context/ProfileContext'; import { AccountContext } from 'context/AccountContext'; import { AppContext } from 'context/AppContext'; +import config from 'constants/Config'; export function useProfile() { @@ -22,17 +24,35 @@ export function useProfile() { editHandle: null, editPassword: null, editConfirm: null, + checked: true, + available: true, + showPassword: false, + showConfirm: false, + blockedChannels: false, + blockedCards: false, + tabbed: null, }); const app = useContext(AppContext); + const dimensions = useWindowDimensions(); const account = useContext(AccountContext); const profile = useContext(ProfileContext); const navigate = useNavigate(); + const debounce = useRef(null); const updateState = (value) => { setState((s) => ({ ...s, ...value })); } + useEffect(() => { + if (dimensions.width > config.tabbedWidth) { + updateState({ tabbed: false }); + } + else { + updateState({ tabbed: true }); + } + }, [dimensions]); + useEffect(() => { const { name, handle, node, location, description, image } = profile.state.profile; const imageSource = image ? profile.state.imageUrl : 'avatar'; @@ -49,12 +69,25 @@ export function useProfile() { app.actions.logout(); navigate('/'); }, - setVisible: async (visible) => { - await account.actions.setSearchable(visible); + setVisible: async (searchable) => { + updateState({ searchable }); + await account.actions.setSearchable(searchable); }, setProfileImage: async (data) => { await profile.actions.setProfileImage(data); }, + showBlockedChannels: () => { + updateState({ blockedChannels: true }); + }, + hideBlockedChannels: () => { + updateState({ blockedChannels: false }); + }, + showBlockedCards: () => { + updateState({ blockedCards: true }); + }, + hideBlockedCards: () => { + updateState({ blockedCards: false }); + }, showLoginEdit: () => { updateState({ showLoginEdit: true }); }, @@ -76,8 +109,38 @@ export function useProfile() { setEditDescription: (editDescription) => { updateState({ editDescription }); }, + showPassword: () => { + updateState({ showPassword: true }); + }, + hidePassword: () => { + updateState({ showPassword: false }); + }, + showConfirm: () => { + updateState({ showConfirm: true }); + }, + hideConfirm: () => { + updateState({ showConfirm: false }); + }, setEditHandle: (editHandle) => { - updateState({ editHandle }); + updateState({ editHandle, checked: false }); + + if (debounce.current != null) { + clearTimeout(debounce.current); + } + debounce.current = setTimeout(async () => { + try { + if (editHandle === state.handle) { + updateState({ available: true, checked: true }); + } + else { + const available = await profile.actions.getHandle(editHandle); + updateState({ available, checked: true }); + } + } + catch (err) { + console.log(err); + } + }, 1000); }, setEditPassword: (editPassword) => { updateState({ editPassword }); @@ -85,8 +148,11 @@ export function useProfile() { setEditConfirm: (editConfirm) => { updateState({ editConfirm }); }, - saveDetails: () => { - profile.actions.setProfileData(state.editName, state.editLocation, state.editDescription); + saveDetails: async () => { + await profile.actions.setProfileData(state.editName, state.editLocation, state.editDescription); + }, + saveLogin: async () => { + await account.actions.setLogin(state.editHandle, state.editPassword); }, }; diff --git a/app/mobile/src/session/registry/Registry.jsx b/app/mobile/src/session/registry/Registry.jsx new file mode 100644 index 00000000..2fe6cba0 --- /dev/null +++ b/app/mobile/src/session/registry/Registry.jsx @@ -0,0 +1,138 @@ +import { useContext } from 'react'; +import { ActivityIndicator, Alert, FlatList, ScrollView, View, TextInput, TouchableOpacity, Text } from 'react-native'; +import { styles } from './Registry.styled'; +import { useRegistry } from './useRegistry.hook'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Ionicons from '@expo/vector-icons/AntDesign'; +import { RegistryItem } from './registryItem/RegistryItem'; +import Colors from 'constants/Colors'; + +export function RegistryTitle({ state, actions }) { + + const search = async () => { + try { + await actions.search(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Server Listing Failed', + 'Please try again.' + ); + } + } + + return ( + + + + + + { state.busy && ( + + + + )} + { !state.busy && ( + + + + )} + + ); +} + +export function RegistryBody({ state, actions, openContact }) { + return ( + } + keyExtractor={item => item.guid} + /> + ); +} + + +export function Registry({ closeRegistry, openContact }) { + + const search = async () => { + try { + await actions.search(); + } + catch (err) { + console.log(err); + Alert.alert( + 'Server Listing Failed', + 'Please try again.' + ); + } + } + + const { state, actions } = useRegistry(); + return ( + + { state.tabbed && ( + <> + + { state.busy && ( + + + + )} + { !state.busy && ( + + + + )} + + + + + + + + + } + keyExtractor={item => item.guid} + /> + + )} + { !state.tabbed && ( + <> + + + + { state.busy && ( + + + + )} + { !state.busy && ( + + + + )} + + + + + + + + } + keyExtractor={item => item.guid} + /> + + + )} + + ); +} + diff --git a/app/mobile/src/session/registry/Registry.styled.js b/app/mobile/src/session/registry/Registry.styled.js new file mode 100644 index 00000000..30b424bf --- /dev/null +++ b/app/mobile/src/session/registry/Registry.styled.js @@ -0,0 +1,113 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + backgroundColor: Colors.formBackground, + }, + title: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + topbar: { + borderTopWidth: 1, + borderBottomWidth: 1, + borderColor: Colors.divider, + paddingTop: 6, + paddingBottom: 6, + paddingLeft: 16, + paddingRight: 16, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + searcharea: { + borderBottomWidth: 1, + borderColor: Colors.divider, + }, + searchbar: { + display: 'flex', + flexDirection: 'row', + paddingTop: 16, + paddingLeft: 8, + paddingBottom: 8, + alignItems: 'center', + }, + inputwrapper: { + display: 'flex', + flexDirection: 'row', + borderRadius: 4, + backgroundColor: Colors.white, + alignItems: 'center', + flexGrow: 1, + flexShrink: 1, + marginRight: 8, + paddingTop: 4, + paddingBottom: 4, + marginLeft: 8, + }, + inputfield: { + flex: 1, + textAlign: 'center', + padding: 4, + color: Colors.text, + fontSize: 14, + }, + icon: { + paddingLeft: 8, + }, + accounts: { + flexGrow: 1, + flexShrink: 1, + width: '100%', + paddingLeft: 16, + paddingRight: 16, + minHeight: 0, + }, + addbottom: { + marginRight: 8, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: 8, + borderRadius: 4, + }, + bottomText: { + color: Colors.primary, + paddingLeft: 8, + }, + search: { + backgroundColor: Colors.primary, + marginLeft: 8, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: 8, + borderRadius: 4, + }, + close: { + marginRight: 8, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: 8, + borderRadius: 4, + }, + newtext: { + paddingLeft: 8, + color: Colors.white, + }, + findarea: { + borderTopWidth: 1, + borderColor: Colors.divider, + } +}) + diff --git a/app/mobile/src/session/registry/registryItem/RegistryItem.jsx b/app/mobile/src/session/registry/registryItem/RegistryItem.jsx new file mode 100644 index 00000000..5a0d88d0 --- /dev/null +++ b/app/mobile/src/session/registry/registryItem/RegistryItem.jsx @@ -0,0 +1,31 @@ +import { Text, TouchableOpacity, View } from 'react-native'; +import { Logo } from 'utils/Logo'; +import { styles } from './RegistryItem.styled'; +import { useRegistryItem } from './useRegistryItem.hook'; + +export function RegistryItem({ item, openContact }) { + + const { state, actions } = useRegistryItem(item); + + const select = () => { + openContact({ account: item }); + } + + return ( + + { item.guid && ( + + + + { item.name } + { `${item.handle}@${item.node}` } + + + )} + { !item.guid && ( + + )} + + ); +} + diff --git a/app/mobile/src/session/registry/registryItem/RegistryItem.styled.js b/app/mobile/src/session/registry/registryItem/RegistryItem.styled.js new file mode 100644 index 00000000..91b94788 --- /dev/null +++ b/app/mobile/src/session/registry/registryItem/RegistryItem.styled.js @@ -0,0 +1,64 @@ +import { StyleSheet } from 'react-native'; +import { Colors } from 'constants/Colors'; + +export const styles = StyleSheet.create({ + container: { + width: '100%', + display: 'flex', + flexDirection: 'row', + height: 48, + alignItems: 'center', + borderBottomWidth: 1, + borderColor: Colors.itemDivider, + }, + space: { + height: 64 + }, + detail: { + paddingLeft: 12, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + flexGrow: 1, + flexShrink: 1, + }, + name: { + color: Colors.text, + fontSize: 14, + }, + handle: { + color: Colors.text, + fontSize: 12, + }, + connected: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.connected, + }, + requested: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.requested, + }, + connecting: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.connecting, + }, + pending: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.pending, + }, + confirmed: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: Colors.confirmed, + }, +}) + diff --git a/app/mobile/src/session/registry/registryItem/useRegistryItem.hook.js b/app/mobile/src/session/registry/registryItem/useRegistryItem.hook.js new file mode 100644 index 00000000..75f816f1 --- /dev/null +++ b/app/mobile/src/session/registry/registryItem/useRegistryItem.hook.js @@ -0,0 +1,17 @@ +import { useState, useEffect, useRef, useContext } from 'react'; +import { useWindowDimensions } from 'react-native'; + +export function useRegistryItem(item) { + + const [state, setState] = useState({}); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/registry/useRegistry.hook.js b/app/mobile/src/session/registry/useRegistry.hook.js new file mode 100644 index 00000000..09fc0664 --- /dev/null +++ b/app/mobile/src/session/registry/useRegistry.hook.js @@ -0,0 +1,82 @@ +import { useState, useEffect, useRef, useContext } from 'react'; +import { useWindowDimensions } from 'react-native'; +import { useNavigate } from 'react-router-dom'; +import { ProfileContext } from 'context/ProfileContext'; +import { getListing } from 'api/getListing'; +import { getListingImageUrl } from 'api/getListingImageUrl'; +import config from 'constants/Config'; + +export function useRegistry() { + + const [state, setState] = useState({ + tabbed: null, + accounts: [], + server: null, + busy: false, + }); + + const dimensions = useWindowDimensions(); + const profile = useContext(ProfileContext); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + if (dimensions.width > config.tabbedWidth) { + updateState({ tabbed: false }); + } + else { + updateState({ tabbed: true }); + } + }, [dimensions]); + + useEffect(() => { + const server = profile.state.profile.node; + updateState({ server }); + getAccounts(server); + }, [profile]); + + const setAccountItem = (item) => { + const { guid, name, handle, node, location, description } = item; + const logo = item.imageSet ? getListingImageUrl(node, guid) : 'avatar'; + return { guid, name, handle, node, location, description, guid, logo }; + }; + + const getAccounts = async (server, ignore) => { + if (!state.busy) { + try { + updateState({ busy: true }); + const accounts = await getListing(server, true); + const filtered = accounts.filter(item => { + if (item.guid === profile.state.profile.guid) { + return false; + } + return true; + }); + const items = filtered.map(setAccountItem); + items.push({guid:''}); + updateState({ busy: false, accounts: items }); + } + catch (err) { + console.log(err); + updateState({ busy: false, accounts: [] }); + if (!ignore) { + throw new Error('failed list accounts'); + } + } + } + }; + + const actions = { + setServer: (server) => { + updateState({ server, accounts: [] }); + }, + search: async () => { + await getAccounts(state.server, false); + } + }; + + return { state, actions }; +} + diff --git a/app/mobile/src/session/welcome/Welcome.jsx b/app/mobile/src/session/welcome/Welcome.jsx index ab0c4fc4..df597242 100644 --- a/app/mobile/src/session/welcome/Welcome.jsx +++ b/app/mobile/src/session/welcome/Welcome.jsx @@ -7,10 +7,6 @@ import session from 'images/session.png'; export function Welcome() { - useEffect(() => { - console.log("WELCOME"); - }, []); - return ( Welcome to Databag diff --git a/app/mobile/yarn.lock b/app/mobile/yarn.lock index dc625b63..31d6d586 100644 --- a/app/mobile/yarn.lock +++ b/app/mobile/yarn.lock @@ -753,7 +753,7 @@ "@babel/plugin-transform-object-assign@^7.16.7": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.18.6.tgz#7830b4b6f83e1374a5afb9f6111bcfaea872cdd2" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.18.6.tgz" integrity sha512-mQisZ3JfqWh2gVXvfqYCAAyRs6+7oev+myBsTwW5RnPhYXOTuCEw2oe3YgxlXMViXUS53lG8koulI7mJ+8JE+A== dependencies: "@babel/helper-plugin-utils" "^7.18.6" @@ -1063,7 +1063,7 @@ "@egjs/hammerjs@^2.0.17": version "2.0.17" - resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" + resolved "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz" integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A== dependencies: "@types/hammerjs" "^2.0.36" @@ -1686,7 +1686,7 @@ resolved "https://registry.npmjs.org/@react-native/assets/-/assets-1.0.0.tgz" integrity sha512-KrwSpS1tKI70wuKl68DwJZYEvXktDHdZMG0k2AXD/rJVSlB23/X2CB2cutVR0HwNMJIal9HOUOBB2rVfa6UGtQ== -"@react-native/normalize-color@2.0.0", "@react-native/normalize-color@^2.0.0": +"@react-native/normalize-color@*", "@react-native/normalize-color@2.0.0", "@react-native/normalize-color@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@react-native/normalize-color/-/normalize-color-2.0.0.tgz" integrity sha512-Wip/xsc5lw8vsBlmY2MO/gFLp3MvuZ2baBZjDeTjjndMgM0h5sxz7AZR62RDPGgstp8Np7JzjvVqVT7tpFZqsw== @@ -1698,7 +1698,7 @@ "@react-navigation/bottom-tabs@^6.4.0": version "6.4.0" - resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.4.0.tgz#63743874648f92adedf37186cb7cedcd47826ee9" + resolved "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-6.4.0.tgz" integrity sha512-90CapiXjiWudbCiki9e6fOr/CECQRguIxv5OD7IBfbAMGX5GGiJpX8aqiHAz2DxpAz31v4JZcUr945+lFhXBfA== dependencies: "@react-navigation/elements" "^1.3.6" @@ -1707,7 +1707,7 @@ "@react-navigation/core@^6.4.0": version "6.4.0" - resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.0.tgz#c44d33a8d8ef010a102c7f831fc8add772678509" + resolved "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.0.tgz" integrity sha512-tpc0Ak/DiHfU3LlYaRmIY7vI4sM/Ru0xCet6runLUh9aABf4wiLgxyFJ5BtoWq6xFF8ymYEA/KWtDhetQ24YiA== dependencies: "@react-navigation/routers" "^6.1.3" @@ -1719,7 +1719,7 @@ "@react-navigation/drawer@^6.5.0": version "6.5.0" - resolved "https://registry.yarnpkg.com/@react-navigation/drawer/-/drawer-6.5.0.tgz#6f73a04deca2ce046626a60d9a59b11e8cc97167" + resolved "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-6.5.0.tgz" integrity sha512-ma3qPjAfbwF07xd1w1gaWdcvYWmT4F+Z098q2J7XGbHw8yTGQYiNTnD1NMKerXwxM24vui2tMuFHA54F1rIvHQ== dependencies: "@react-navigation/elements" "^1.3.6" @@ -1728,12 +1728,12 @@ "@react-navigation/elements@^1.3.6": version "1.3.6" - resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.6.tgz#fa700318528db93f05144b1be4b691b9c1dd1abe" + resolved "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.6.tgz" integrity sha512-pNJ8R9JMga6SXOw6wGVN0tjmE6vegwPmJBL45SEMX2fqTfAk2ykDnlJHodRpHpAgsv0DaI8qX76z3A+aqKSU0w== "@react-navigation/native@^6.0.13": version "6.0.13" - resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.0.13.tgz#ec504120e193ea6a7f24ffa765a1338be5a3160a" + resolved "https://registry.npmjs.org/@react-navigation/native/-/native-6.0.13.tgz" integrity sha512-CwaJcAGbhv3p3ECablxBkw8QBCGDWXqVRwQ4QbelajNW623m3sNTC9dOF6kjp8au6Rg9B5e0KmeuY0xWbPk79A== dependencies: "@react-navigation/core" "^6.4.0" @@ -1743,14 +1743,14 @@ "@react-navigation/routers@^6.1.3": version "6.1.3" - resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-6.1.3.tgz#1df51959e9a67c44367462e8b929b7360a5d2555" + resolved "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.3.tgz" integrity sha512-idJotMEzHc3haWsCh7EvnnZMKxvaS4YF/x2UyFBkNFiEFUaEo/1ioQU6qqmVLspdEv4bI/dLm97hQo7qD8Yl7Q== dependencies: nanoid "^3.1.23" "@react-navigation/stack@^6.3.0": version "6.3.0" - resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-6.3.0.tgz#3b268c5c61eba17fff1ed711e20ea94a9d5a1809" + resolved "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.0.tgz" integrity sha512-CCzdXkt57t3ikfV8TQIA7p4srf/o35ncT22ciGOAwZorB1M7Lqga18tsEqkk9R3qENl12a1Ei6VC7dkZezDXQQ== dependencies: "@react-navigation/elements" "^1.3.6" @@ -1782,6 +1782,11 @@ resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@stream-io/flat-list-mvcp@^0.10.2": + version "0.10.2" + resolved "https://registry.npmjs.org/@stream-io/flat-list-mvcp/-/flat-list-mvcp-0.10.2.tgz" + integrity sha512-jebEKP7pfRF8/tVSqNM6qdvisfOtMnMlzGYTWldoOnIq9/6DS1BU4ilzBuH6O7iBUu4bDokrMCNJgA2b2EKW/A== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" @@ -1791,12 +1796,12 @@ "@types/hammerjs@^2.0.36": version "2.0.41" - resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa" + resolved "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz" integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA== "@types/invariant@^2.2.35": version "2.2.35" - resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be" + resolved "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz" integrity sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg== "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": @@ -1823,6 +1828,32 @@ resolved "https://registry.npmjs.org/@types/node/-/node-18.7.15.tgz" integrity sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ== +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-addons-shallow-compare@^0.14.22": + version "0.14.22" + resolved "https://registry.npmjs.org/@types/react-addons-shallow-compare/-/react-addons-shallow-compare-0.14.22.tgz" + integrity sha512-krgFRorWtbVJLzpJsJD6O27Lew3YHuemVZbL9RFvq8TF1w9DbrHjiiLuIyWIL6AjunBkUrQlErfbUv1TYKiK9w== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "18.0.21" + resolved "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz" + integrity sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz" @@ -2066,6 +2097,15 @@ atob@^2.1.2: resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +axios@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/axios/-/axios-1.1.0.tgz" + integrity sha512-hsJgcqz4JY7f+HZ4cWTrPZ6tZNCNFPTRx1MjRqu/hbpgpHdSCUpLVuplc+jE/h7dOvyANtw/ERA3HC2Rz/QoMg== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz" @@ -2570,7 +2610,7 @@ color-string@^1.5.3, color-string@^1.9.0: color@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz" integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: color-convert "^2.0.1" @@ -2698,6 +2738,11 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1: browserslist "^4.21.3" semver "7.0.0" +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" @@ -2771,6 +2816,11 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" +csstype@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + dag-map@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/dag-map/-/dag-map-1.0.2.tgz" @@ -2906,6 +2956,15 @@ depd@~1.1.2: resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +deprecated-react-native-prop-types@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz" + integrity sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA== + dependencies: + "@react-native/normalize-color" "*" + invariant "*" + prop-types "*" + destroy@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" @@ -2928,6 +2987,11 @@ electron-to-chromium@^1.4.202: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.242.tgz" integrity sha512-nPdgMWtjjWGCtreW/2adkrB2jyHjClo9PtVhR6rW+oxa4E4Wom642Tn+5LslHP3XPL5MCpkn5/UEY60EXylNeQ== +eme-encryption-scheme-polyfill@^2.0.1: + version "2.1.1" + resolved "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.1.1.tgz" + integrity sha512-njD17wcUrbqCj0ArpLu5zWXtaiupHb/2fIUQGdInf83GlI+Q6mmqaPGLdrke4savKAu15J/z1Tg/ivDgl14g0g== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -2938,6 +3002,13 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encoding@^0.1.11: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" @@ -2999,7 +3070,7 @@ escape-string-regexp@^1.0.5: escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== esprima@^4.0.0, esprima@~4.0.0: @@ -3071,6 +3142,13 @@ expo-asset@~8.6.1: path-browserify "^1.0.0" url-parse "^1.5.9" +expo-av@^12.0.4: + version "12.0.4" + resolved "https://registry.npmjs.org/expo-av/-/expo-av-12.0.4.tgz" + integrity sha512-wq3wx6J1aacZEPZce9TK1+o4YTAOWyb5cJ4CqfsgHcXeUdHyO3qbva/5uecigpEYlCxOlWYFhAz2T0dQ7nWSpQ== + dependencies: + "@expo/config-plugins" "~5.0.0" + expo-constants@~13.2.2, expo-constants@~13.2.4: version "13.2.4" resolved "https://registry.npmjs.org/expo-constants/-/expo-constants-13.2.4.tgz" @@ -3195,7 +3273,7 @@ extglob@^2.0.4: fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.2.5, fast-glob@^3.2.9: @@ -3235,6 +3313,19 @@ fbjs-css-vars@^1.0.0: resolved "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz" integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== +fbjs@^0.8.4: + version "0.8.18" + resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.18.tgz" + integrity sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA== + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.30" + fbjs@^3.0.0, fbjs@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz" @@ -3272,7 +3363,7 @@ fill-range@^7.0.1: filter-obj@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== finalhandler@1.1.2: @@ -3345,6 +3436,11 @@ flow-parser@^0.121.0: resolved "https://registry.npmjs.org/flow-parser/-/flow-parser-0.121.0.tgz" integrity sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + fontfaceobserver@^2.1.0: version "2.3.0" resolved "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz" @@ -3364,6 +3460,15 @@ form-data@^3.0.1: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz" @@ -3651,12 +3756,12 @@ history@^5.2.0: hoist-non-react-statics@^2.3.1: version "2.5.5" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== hoist-non-react-statics@^3.3.0: version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" @@ -3702,6 +3807,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" @@ -3778,7 +3890,7 @@ internal-ip@4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -invariant@^2.2.4: +invariant@*, invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -3970,7 +4082,7 @@ is-root@^2.1.0: resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== -is-stream@^1.1.0: +is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== @@ -4031,6 +4143,14 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz" + integrity sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA== + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz" @@ -4247,6 +4367,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +keymirror@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz" + integrity sha512-vIkZAFWoDijgQT/Nvl2AHCMmnegN2ehgTPYuyy2hWQkQSntI0S7ESYqdLkoSe1HyEBFHHkCgSIvVdSEiWwKvCg== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" @@ -4322,7 +4447,7 @@ lodash.debounce@^4.0.8: lodash.isequal@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== lodash.throttle@^4.1.1: @@ -4839,6 +4964,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -4874,7 +5004,7 @@ mz@^2.7.0: nanoid@^3.1.23: version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== nanomatch@^1.2.9: @@ -4938,6 +5068,14 @@ node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node- dependencies: whatwg-url "^5.0.0" +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-forge@^1.2.1, node-forge@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" @@ -5378,15 +5516,20 @@ prompts@^2.3.2, prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2: +prop-types@*, prop-types@^15.7.2: version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: loose-envify "^1.4.0" object-assign "^4.1.1" react-is "^16.13.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" @@ -5407,7 +5550,7 @@ qs@6.7.0: query-string@^7.0.0: version "7.1.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1" + resolved "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz" integrity sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w== dependencies: decode-uri-component "^0.2.0" @@ -5450,6 +5593,14 @@ rc@~1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-addons-shallow-compare@15.6.2: + version "15.6.2" + resolved "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz" + integrity sha512-yAV9tOObmKPiohqne1jiMcx6kDjfz7GeL8K9KHgI+HvDsbrRv148uyUzrPc6GwepZnQcJ59Q3lp1ghrkyPwtjg== + dependencies: + fbjs "^0.8.4" + object-assign "^4.1.0" + react-devtools-core@4.24.0: version "4.24.0" resolved "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.0.tgz" @@ -5473,7 +5624,7 @@ react-dom@18.0.0: react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== react-is@^17.0.1: @@ -5496,10 +5647,22 @@ react-native-codegen@^0.69.2: jscodeshift "^0.13.1" nullthrows "^1.1.1" -react-native-gesture-handler@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.6.1.tgz#66c40c8d720eb4729b301836a40fd34d14ec840f" - integrity sha512-0MXjRgNCrsQJSo3B9oXORw5spdm/9dkDbP2JU/3zrVyV9/MnRz5Oo3oy7hREKYWVMF9Gk2UpsCquFLRFQxeSxQ== +react-native-document-picker@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-8.1.1.tgz" + integrity sha512-mH0oghd7ndgU9/1meVJdqts1sAkOfUQW1qbrqTTsvR5f2K9r0BAj/X02dve5IBMOMZvlGd7qWrNVuIFg5AUXWg== + dependencies: + invariant "^2.2.4" + +react-native-elevation@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/react-native-elevation/-/react-native-elevation-1.0.0.tgz" + integrity sha512-BWIKcEYtzjRV6GpkX0Km5/w2E7fgIcywiQOT7JZTc5NSbv/YI9kpFinB9lRFsOoRVGmiqq/O3VfP/oH2clIiBA== + +react-native-gesture-handler@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.7.0.tgz" + integrity sha512-0jr3FNm2R3gv/v6XTtENgjv0fewD6LEct8EWmXw/oHw36M3YiIIpxnW57thL+0YiKwyLBXN0QHL4JZbs/heW2Q== dependencies: "@egjs/hammerjs" "^2.0.17" hoist-non-react-statics "^3.3.0" @@ -5514,12 +5677,12 @@ react-native-gradle-plugin@^0.0.7: react-native-image-crop-picker@^0.38.0: version "0.38.0" - resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.38.0.tgz#3f67a0ec40618e3cd6e05d3e7b90e70d01eaddf8" + resolved "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.38.0.tgz" integrity sha512-FaLASXOP7R23pi20vMiVlXl0Y7cwTdl7y7yBqrlrsSH9gl9ibsU5y4mYWPYRbe8x9F/3zPGUE+1F0Gj/QF/peg== react-native-reanimated@^2.10.0: version "2.10.0" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.10.0.tgz#ed53be66bbb553b5b5e93e93ef4217c87b8c73db" + resolved "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.10.0.tgz" integrity sha512-jKm3xz5nX7ABtHzzuuLmawP0pFWP77lXNdIC6AWOceBs23OHUaJ29p4prxr/7Sb588GwTbkPsYkDqVFaE3ezNQ== dependencies: "@babel/plugin-transform-object-assign" "^7.16.7" @@ -5532,21 +5695,49 @@ react-native-reanimated@^2.10.0: react-native-safe-area-context@^4.3.3: version "4.3.3" - resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.3.3.tgz#a0f1e3116ded39efc1b78a46a6d89c71169827e4" + resolved "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.3.3.tgz" integrity sha512-xwsloGLDUzeTN40TIh4Te/zRePSnBAuWlLIiEW3RYE9gHHYslqQWpfK7N24SdAQEH3tHZ+huoYNjo2GQJO/vnQ== react-native-safe-area-view@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-1.1.1.tgz#9833e34c384d0513f4831afcd1e54946f13897b2" + resolved "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-1.1.1.tgz" integrity sha512-bbLCtF+tqECyPWlgkWbIwx4vDPb0GEufx/ZGcSS4UljMcrpwluachDXoW9DBxhbMCc6k1V0ccqHWN7ntbRdERQ== dependencies: hoist-non-react-statics "^2.3.1" +react-native-snap-carousel@4.0.0-beta.6: + version "4.0.0-beta.6" + resolved "https://registry.npmjs.org/react-native-snap-carousel/-/react-native-snap-carousel-4.0.0-beta.6.tgz" + integrity sha512-jr6Kesrn+/047FwL32JRwAL6/l4Soz2uw73rFM5LfoGTgZhPatUsw60eTE3bznqFewPhoFIKezYe2m3LypTnIA== + dependencies: + "@types/react-addons-shallow-compare" "^0.14.22" + react-addons-shallow-compare "15.6.2" + +react-native-sound-player@^0.13.2: + version "0.13.2" + resolved "https://registry.npmjs.org/react-native-sound-player/-/react-native-sound-player-0.13.2.tgz" + integrity sha512-m457mjp496sARAED7epYzbhorfiLZ3j6HzJn7zEL9RU+ZJKcans/0gFNUEBvlh31fSDZRliQ2WwE4mJ6836bNg== + react-native-sqlite-storage@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/react-native-sqlite-storage/-/react-native-sqlite-storage-6.0.1.tgz" integrity sha512-1tDFjrint6X6qSYKf3gDyz+XB+X79jfiL6xTugKHPRtF0WvqMtVgdLuNqZunIXjNEvNtNVEbXaeZ6MsguFu00A== +react-native-swipe-gestures@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz" + integrity sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw== + +react-native-video@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/react-native-video/-/react-native-video-5.2.1.tgz" + integrity sha512-aJlr9MeTuQ0LpZ4n+EC9RvhoKeiPbLtI2Rxy8u7zo/wzGevbRpWHSBj9xZ5YDBXnAVXzuqyNIkGhdw7bfdIBZw== + dependencies: + deprecated-react-native-prop-types "^2.2.0" + keymirror "^0.1.1" + prop-types "^15.7.2" + shaka-player "^2.5.9" + react-native-web@~0.18.7: version "0.18.9" resolved "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.9.tgz" @@ -5560,6 +5751,13 @@ react-native-web@~0.18.7: postcss-value-parser "^4.2.0" styleq "^0.1.2" +react-native-wheel-color-picker@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/react-native-wheel-color-picker/-/react-native-wheel-color-picker-1.2.0.tgz" + integrity sha512-j4IcN7so9dZAkXyrPTTaPqCKsjkGBZkd5F7HqLo0OTRB1EZX3Ww5VMKsKjloxv6Omv193wGOhwfG20ec2KnxJQ== + dependencies: + react-native-elevation "^1.0.0" + react-native@0.69.5: version "0.69.5" resolved "https://registry.npmjs.org/react-native/-/react-native-0.69.5.tgz" @@ -5904,7 +6102,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -6019,6 +6217,13 @@ setprototypeof@1.2.0: resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +shaka-player@^2.5.9: + version "2.5.23" + resolved "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.23.tgz" + integrity sha512-3MC9k0OXJGw8AZ4n/ZNCZS2yDxx+3as5KgH6Tx4Q5TRboTBBCu6dYPI5vp1DxKeyU12MBN1Zcbs7AKzXv2EnCg== + dependencies: + eme-encryption-scheme-polyfill "^2.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" @@ -6171,7 +6376,7 @@ source-map@^0.7.3: split-on-first@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== split-string@^3.0.1, split-string@^3.0.2: @@ -6237,12 +6442,12 @@ stream-buffers@2.2.x: strict-uri-encode@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== string-hash-64@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322" + resolved "https://registry.npmjs.org/string-hash-64/-/string-hash-64-1.0.3.tgz" integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw== string-width@^4.1.0, string-width@^4.2.0: @@ -6712,7 +6917,7 @@ url-parse@^1.5.9: use-latest-callback@^0.1.5: version "0.1.5" - resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51" + resolved "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.1.5.tgz" integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ== use-sync-external-store@^1.0.0: @@ -6781,7 +6986,7 @@ walker@^1.0.7: warn-once@^0.1.0: version "0.1.1" - resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43" + resolved "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz" integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q== wcwidth@^1.0.1: @@ -6796,7 +7001,7 @@ webidl-conversions@^3.0.0: resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -whatwg-fetch@^3.0.0: +whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: version "3.6.2" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==