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==