Merge branch 'main' of https://github.com/balzack/databag into main
@ -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,15 +15,22 @@ 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 (
|
||||
<StoreContextProvider>
|
||||
<UploadContextProvider>
|
||||
<CardContextProvider>
|
||||
<ChannelContextProvider>
|
||||
<AccountContextProvider>
|
||||
<ProfileContextProvider>
|
||||
<ConversationContextProvider>
|
||||
<AppContextProvider>
|
||||
<SafeAreaProvider>
|
||||
<NativeRouter>
|
||||
@ -37,10 +45,12 @@ export default function App() {
|
||||
</NativeRouter>
|
||||
</SafeAreaProvider>
|
||||
</AppContextProvider>
|
||||
</ConversationContextProvider>
|
||||
</ProfileContextProvider>
|
||||
</AccountContextProvider>
|
||||
</ChannelContextProvider>
|
||||
</CardContextProvider>
|
||||
</UploadContextProvider>
|
||||
</StoreContextProvider>
|
||||
);
|
||||
}
|
||||
|
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
7B93995528F163330002722F /* SplashScreen.storyboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard.storyboard; path = Databag/SplashScreen.storyboard.storyboard; sourceTree = "<group>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Databag/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
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 = "<group>";
|
||||
@ -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;
|
||||
|
10
app/mobile/ios/Databag.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Databag.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
After Width: | Height: | Size: 239 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 14 KiB |
BIN
app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/40.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/58.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/60.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/80.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
app/mobile/ios/Databag/Images.xcassets/AppIcon.appiconset/87.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
@ -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
|
||||
}
|
||||
}
|
@ -14,16 +14,14 @@
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Used to set profile image and post photos</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
@ -37,12 +35,18 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Required for build but not used</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Used to set profile image and post photos</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<string>SplashScreen.storyboard</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
@ -51,7 +55,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
91
app/mobile/ios/Databag/SplashScreen.storyboard.storyboard
Normal file
@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document
|
||||
type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB"
|
||||
version="3.0"
|
||||
toolsVersion="16096"
|
||||
targetRuntime="iOS.CocoaTouch"
|
||||
propertyAccessControl="none"
|
||||
useAutolayout="YES"
|
||||
launchScreen="YES"
|
||||
useTraitCollections="YES"
|
||||
useSafeAreas="YES"
|
||||
colorMatched="YES"
|
||||
initialViewController="EXPO-VIEWCONTROLLER-1"
|
||||
>
|
||||
<device id="retina5_5" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EXPO-SCENE-1">
|
||||
<objects>
|
||||
<viewController
|
||||
storyboardIdentifier="SplashScreenViewController"
|
||||
id="EXPO-VIEWCONTROLLER-1"
|
||||
sceneMemberID="viewController"
|
||||
>
|
||||
<view
|
||||
key="view"
|
||||
userInteractionEnabled="NO"
|
||||
contentMode="scaleToFill"
|
||||
insetsLayoutMarginsFromSafeArea="NO"
|
||||
id="EXPO-ContainerView"
|
||||
userLabel="ContainerView"
|
||||
>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<imageView
|
||||
userInteractionEnabled="NO"
|
||||
contentMode="scaleAspectFill"
|
||||
horizontalHuggingPriority="251"
|
||||
verticalHuggingPriority="251"
|
||||
insetsLayoutMarginsFromSafeArea="NO"
|
||||
image="SplashScreenBackground"
|
||||
translatesAutoresizingMaskIntoConstraints="NO"
|
||||
id="EXPO-SplashScreenBackground"
|
||||
userLabel="SplashScreenBackground"
|
||||
>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
</imageView>
|
||||
<imageView
|
||||
clipsSubviews="YES"
|
||||
userInteractionEnabled="NO"
|
||||
contentMode="scaleAspectFit"
|
||||
horizontalHuggingPriority="251"
|
||||
verticalHuggingPriority="251"
|
||||
translatesAutoresizingMaskIntoConstraints="NO"
|
||||
image="SplashScreen"
|
||||
id="EXPO-SplashScreen"
|
||||
userLabel="SplashScreen"
|
||||
>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="1gX-mQ-vu6"/>
|
||||
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="6tX-OG-Sck"/>
|
||||
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="ABX-8g-7v4"/>
|
||||
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="jkI-2V-eW5"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="2VS-Uz-0LU"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="LhH-Ei-DKo"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="I6l-TP-6fn"/>
|
||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="nbp-HC-eaG"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="140.625" y="129.4921875"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="SplashScreen" width="414" height="736"/>
|
||||
<image name="SplashScreenBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -39,7 +39,7 @@ export function Create() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="database" size={18} color="#888888" />
|
||||
<TextInput style={styles.inputfield} value={state.server} onChangeText={actions.setServer}
|
||||
autoCapitalize="none" placeholder="server" />
|
||||
autoCorrect={false} autoCapitalize="none" placeholder="server" />
|
||||
<View style={styles.space}>
|
||||
{ (!state.server || !state.serverChecked) && (
|
||||
<Text style={styles.required}>✻</Text>
|
||||
@ -57,7 +57,7 @@ export function Create() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="key" size={18} color="#888888" />
|
||||
<TextInput style={styles.inputfield} value={state.token} onChangeText={actions.setToken}
|
||||
autoCapitalize="none" placeholder="token" />
|
||||
autoCorrect={false} autoCapitalize="none" placeholder="token" />
|
||||
<View style={styles.space}>
|
||||
{ (!validServer || !state.token || !state.tokenChecked) && (
|
||||
<Text style={styles.required}>✻</Text>
|
||||
@ -75,7 +75,7 @@ export function Create() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="user" size={18} color="#888888" />
|
||||
<TextInput style={styles.inputfield} value={state.username} onChangeText={actions.setUsername}
|
||||
autoCapitalize="none" placeholder="username" />
|
||||
autoCorrect={false} autoCapitalize="none" placeholder="username" />
|
||||
<View style={styles.space}>
|
||||
{ (!validServer || !validToken || !state.username || !state.usernameChecked) && (
|
||||
<Text style={styles.required}>✻</Text>
|
||||
@ -92,7 +92,7 @@ export function Create() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="lock" size={18} color="#888888" />
|
||||
<TextInput style={styles.inputfield} value={state.password} onChangeText={actions.setPassword}
|
||||
autoCapitalize="none" placeholder="password" />
|
||||
autoCorrect={false} autoCapitalize="none" placeholder="password" />
|
||||
<TouchableOpacity onPress={actions.hidePassword}>
|
||||
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
|
||||
</TouchableOpacity>
|
||||
@ -102,7 +102,7 @@ export function Create() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="lock" size={18} color="#888888" />
|
||||
<TextInput style={styles.inputfield} value={state.password} onChangeText={actions.setPassword}
|
||||
secureTextEntry={true} autoCapitalize="none" placeholder="password" />
|
||||
autoCorrect={false} secureTextEntry={true} autoCapitalize="none" placeholder="password" />
|
||||
<TouchableOpacity onPress={actions.showPassword}>
|
||||
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
|
||||
</TouchableOpacity>
|
||||
@ -112,7 +112,7 @@ export function Create() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="lock" size={18} color="#888888" />
|
||||
<TextInput style={styles.inputfield} value={state.confirm} onChangeText={actions.setConfirm}
|
||||
autoCapitalize="none" placeholder="confirm password" />
|
||||
autoCorrect={false} autoCapitalize="none" placeholder="confirm password" />
|
||||
<TouchableOpacity onPress={actions.hideConfirm}>
|
||||
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
|
||||
</TouchableOpacity>
|
||||
@ -122,7 +122,7 @@ export function Create() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="lock" size={18} color="#888888" />
|
||||
<TextInput style={styles.inputfield} value={state.confirm} onChangeText={actions.setConfirm}
|
||||
secureTextEntry={true} autoCapitalize="none" placeholder="confirm password" />
|
||||
autoCorrect={false} secureTextEntry={true} autoCapitalize="none" placeholder="confirm password" />
|
||||
<TouchableOpacity onPress={actions.showConfirm}>
|
||||
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
|
||||
</TouchableOpacity>
|
||||
|
@ -35,14 +35,14 @@ export function Login() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="user" size={18} color="#aaaaaa" />
|
||||
<TextInput style={styles.inputfield} value={state.login} onChangeText={actions.setLogin}
|
||||
autoCapitalize="none" placeholder="username@server" />
|
||||
autoCorrect={false} autoCapitalize="none" placeholder="username@server" />
|
||||
<View style={styles.space} />
|
||||
</View>
|
||||
{ state.showPassword && (
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="lock" size={18} color="#aaaaaa" />
|
||||
<TextInput style={styles.inputfield} value={state.password} onChangeText={actions.setPassword}
|
||||
autoCapitalize="none" placeholder="password"/>
|
||||
autoCorrect={false} autoCapitalize="none" placeholder="password"/>
|
||||
<TouchableOpacity onPress={actions.hidePassword}>
|
||||
<Ionicons style={styles.icon} name="eye" size={18} color="#aaaaaa" />
|
||||
</TouchableOpacity>
|
||||
@ -52,7 +52,7 @@ export function Login() {
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="lock" size={18} color="#aaaaaa" />
|
||||
<TextInput style={styles.inputfield} value={state.password} onChangeText={actions.setPassword}
|
||||
secureTextEntry={true} autoCapitalize="none" placeholder="password" />
|
||||
autoCorrect={false} secureTextEntry={true} autoCapitalize="none" placeholder="password" />
|
||||
<TouchableOpacity onPress={actions.showPassword}>
|
||||
<Ionicons style={styles.icon} name="eyeo" size={18} color="#aaaaaa" />
|
||||
</TouchableOpacity>
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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}`
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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}`
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
8
app/mobile/src/api/getHandle.js
Normal file
@ -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()
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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',
|
||||
|
||||
|
14
app/mobile/src/context/ConversationContext.js
Normal file
@ -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 (
|
||||
<ConversationContext.Provider value={{ state, actions }}>
|
||||
{children}
|
||||
</ConversationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
14
app/mobile/src/context/UploadContext.js
Normal file
@ -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 (
|
||||
<UploadContext.Provider value={{ state, actions }}>
|
||||
{children}
|
||||
</UploadContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -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 }))
|
||||
|
@ -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 }
|
||||
|
@ -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 }
|
||||
|
394
app/mobile/src/context/useConversationContext.hook.js
Normal file
@ -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 }
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
234
app/mobile/src/context/useUploadContext.hook.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<ConversationStack.Navigator screenOptions={({ route }) => ({ headerShown: false })}>
|
||||
<ConversationStack.Screen name="channels" component={ChannelsTabScreen} />
|
||||
<ConversationStack.Screen name="conversation" component={ConversationTabScreen} />
|
||||
<ConversationStack.Screen name="details" component={DetailsTabScreen} />
|
||||
<ConversationStack.Navigator
|
||||
initialRouteName="channels"
|
||||
screenOptions={({ route }) => ({ headerShown: true, headerTintColor: Colors.primary })}
|
||||
screenListeners={{ state: (e) => { if (e?.data?.state?.index === 0 && selected) { setSelected(null); }}, }}>
|
||||
<ConversationStack.Screen name="channels" options={{
|
||||
headerStyle: { backgroundColor: Colors.titleBackground },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitle: (props) => <ChannelsTitle state={channels.state} actions={channels.actions} />
|
||||
}}>
|
||||
{(props) => <ChannelsBody state={channels.state} actions={channels.actions} openConversation={(cardId, channelId, revision) => setConversation(props.navigation, cardId, channelId, revision)} />}
|
||||
</ConversationStack.Screen>
|
||||
<ConversationStack.Screen name="conversation" options={{
|
||||
headerStyle: { backgroundColor: Colors.titleBackground },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitle: (props) => <ConversationHeader channel={selected} closeConversation={clearConversation} openDetails={setDetail} />
|
||||
}}>
|
||||
{(props) => <ConversationBody channel={selected} />}
|
||||
</ConversationStack.Screen>
|
||||
<ConversationStack.Screen name="details" options={{
|
||||
headerStyle: { backgroundColor: Colors.titleBackground },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitle: (props) => <DetailsHeader channel={selected} />
|
||||
}}>
|
||||
{(props) => <DetailsBody channel={selected} clearConversation={() => clearConversation(props.navigation)} />}
|
||||
</ConversationStack.Screen>
|
||||
</ConversationStack.Navigator>
|
||||
);
|
||||
}
|
||||
const ChannelsTabScreen = ({ navigation }) => {
|
||||
return (
|
||||
<Channels openConversation={(cardId, channelId) => openConversation(navigation, cardId, channelId)} />
|
||||
)
|
||||
}
|
||||
const ConversationTabScreen = ({ navigation }) => {
|
||||
return <Conversation closeConversation={() => closeConversation(navigation)} openDetails={() => openDetails(navigation)} />
|
||||
}
|
||||
const DetailsTabScreen = ({ navigation }) => {
|
||||
return <Details closeDetails={() => closeDetails(navigation)} />
|
||||
}
|
||||
const ProfileStackScreen = () => {
|
||||
return (
|
||||
<ProfileStack.Navigator screenOptions={({ route }) => ({ headerShown: false })}>
|
||||
<ProfileStack.Screen name="profile" component={Profile} />
|
||||
<ProfileStack.Navigator screenOptions={({ route }) => ({ headerShown: true, headerTintColor: Colors.primary })}>
|
||||
<ProfileStack.Screen name="profile" component={Profile} options={{ headerStyle: { backgroundColor: Colors.titleBackground }, headerTitle: (props) => <ProfileTitle {...props} /> }} />
|
||||
</ProfileStack.Navigator>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<ContactStack.Navigator screenOptions={({ route }) => ({ headerShown: false })}>
|
||||
<ContactStack.Screen name="cards">
|
||||
{(props) => <Cards openContact={(cardId) => setCardStack(props.navigation, cardId)} />}
|
||||
<ContactStack.Navigator screenOptions={({ route }) => ({ headerShow: true, headerTintColor: Colors.primary })}
|
||||
initialRouteName="cards">
|
||||
<ContactStack.Screen name="cards" options={{
|
||||
headerStyle: { backgroundColor: Colors.titleBackground },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitle: (props) => <CardsTitle state={cards.state} actions={cards.actions} openRegistry={setRegistryStack} />
|
||||
}}>
|
||||
{(props) => <CardsBody state={cards.state} actions={cards.actions} openContact={(contact) => setCardStack(props.navigation, contact)} />}
|
||||
</ContactStack.Screen>
|
||||
<ContactStack.Screen name="card">
|
||||
{(props) => <Contact cardId={cardId} closeContact={() => clearCardStack(props.navigation)} />}
|
||||
<ContactStack.Screen name="contact" options={{
|
||||
headerStyle: { backgroundColor: Colors.titleBackground },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitle: (props) => <ContactTitle contact={selected} {...props} />
|
||||
}}>
|
||||
{(props) => <Contact contact={selected} closeContact={() => clearCardStack(props.navigation)} />}
|
||||
</ContactStack.Screen>
|
||||
<ContactStack.Screen name="registry" options={{
|
||||
headerStyle: { backgroundColor: Colors.titleBackground },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitle: (props) => <RegistryTitle state={registry.state} actions={registry.actions} contact={selected} {...props} />
|
||||
}}>
|
||||
{(props) => <RegistryBody state={registry.state} actions={registry.actions} openContact={(contact) => setCardStack(props.navigation, contact)} />}
|
||||
</ContactStack.Screen>
|
||||
</ContactStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const HomeScreen = ({ cardNav, registryNav, detailNav, contactNav, profileNav, setDetails, resetConversation, clearReset }) => {
|
||||
|
||||
// drawered containers
|
||||
const CardDrawerContent = ({ navigation, setContact }) => {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Cards navigation={navigation} openContact={setContact} />
|
||||
</SafeAreaView>
|
||||
)
|
||||
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 ProfileDrawerContent = ({ navigation }) => {
|
||||
return (
|
||||
<SafeAreaView edges={['right']}>
|
||||
<Profile closeProfile={() => closeProfile(navigation)} />
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
const DetailDrawerContent = ({ navigation }) => {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Details closeDetails={() => closeDetails(navigation)} />
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
const ContactDrawerContent = ({ navigation }) => {
|
||||
const clearContact = () => {
|
||||
navigation.closeDrawer();
|
||||
const openCards = () => {
|
||||
cardNav.openDrawer();
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Contact closeContact={clearContact} />
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<View style={styles.home}>
|
||||
<SafeAreaView edges={['top', 'bottom', 'left']} style={styles.sidebar}>
|
||||
<View style={styles.options}>
|
||||
<TouchableOpacity style={styles.option} onPress={() => openProfile(profileNav)}>
|
||||
<SafeAreaView edges={['top', 'bottom']} style={styles.sidebar}>
|
||||
<SafeAreaView edges={['left']} style={styles.options}>
|
||||
<TouchableOpacity style={styles.option} onPress={openProfile}>
|
||||
<Ionicons style={styles.icon} name={'user'} size={20} />
|
||||
<Text>Profile</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.option} onPress={() => openCards(cardNav)}>
|
||||
<TouchableOpacity style={styles.option} onPress={openCards}>
|
||||
<Ionicons style={styles.icon} name={'contacts'} size={20} />
|
||||
<Text>Contacts</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<View style={styles.channels}>
|
||||
<Channels openConversation={(cardId, channelId) => openConversation(null, cardId, channelId)} />
|
||||
<Channels openConversation={setConversation} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<View style={styles.conversation}>
|
||||
{ state.conversationId && (
|
||||
<Conversation closeConversation={() => closeConversation(null)} openDetails={() => openDetails(detailNav)} />
|
||||
{ channel && (
|
||||
<Conversation closeConversation={clearConversation} openDetails={setChannelDetails} />
|
||||
)}
|
||||
{ !state.conversationId && (
|
||||
{ !channel && (
|
||||
<Welcome />
|
||||
)}
|
||||
</View>
|
||||
@ -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 (
|
||||
<CardDrawer.Navigator screenOptions={{ drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front', drawerStyle: { width: state.baseWidth } }}
|
||||
drawerContent={(props) => <CardDrawerContent setContact={setCardDrawer} {...props} />}>
|
||||
drawerContent={(props) => <Cards openContact={setCardContact} openRegistry={openRegistry} />}>
|
||||
<CardDrawer.Screen name="home">
|
||||
{(props) => <HomeScreen cardNav={props.navigation} detailNav={detailNav} contactNav={contactNav} profileNav={profileNav} />}
|
||||
{(props) => <HomeScreen cardNav={props.navigation} {...params} />}
|
||||
</CardDrawer.Screen>
|
||||
</CardDrawer.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<RegistryDrawer.Navigator screenOptions={{ drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front', drawerStyle: { width: state.baseWidth } }}
|
||||
drawerContent={(props) => <Registry openContact={setRegistryContact} />}>
|
||||
<RegistryDrawer.Screen name="card">
|
||||
{(props) => <CardDrawerScreen registryNav={props.navigation} {...params} />}
|
||||
</RegistryDrawer.Screen>
|
||||
</RegistryDrawer.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<ContactDrawer.Navigator screenOptions={{ drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front', drawerStyle: { width: state.subWidth } }}
|
||||
drawerContent={(props) => <ContactDrawerContent cardId={cardId} {...props} />}>
|
||||
<ContactDrawer.Screen name="profile">
|
||||
{(props) => <CardDrawerScreen detailNav={detailNav} profileNav={profileNav} contactNav={props.navigation} setContact={setContact} />}
|
||||
drawerContent={(props) => <Contact contact={selected} />}>
|
||||
<ContactDrawer.Screen name="registry">
|
||||
{(props) => <RegistryDrawerScreen {...params} contactNav={props.navigation} setContact={setContact} />}
|
||||
</ContactDrawer.Screen>
|
||||
</ContactDrawer.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<ProfileDrawer.Navigator screenOptions={{ drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front', drawerStyle: { width: state.subWidth } }}
|
||||
drawerContent={(props) => <ProfileDrawerContent {...props} />}>
|
||||
<ProfileDrawer.Screen name="card">
|
||||
{(props) => <ContactDrawerScreen detailNav={detailNav} profileNav={props.navigation}/>}
|
||||
</ProfileDrawer.Screen>
|
||||
</ProfileDrawer.Navigator>
|
||||
<DetailDrawer.Navigator screenOptions={{ drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front', drawerStyle: { width: state.subWidth } }}
|
||||
drawerContent={(props) => <Details channel={selected} clearConversation={clearConversation} />}
|
||||
>
|
||||
<DetailDrawer.Screen name="contact">
|
||||
{(props) => <ContactDrawerScreen {...params} detailNav={props.navigation} />}
|
||||
</DetailDrawer.Screen>
|
||||
</DetailDrawer.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{ state.tabbed === false && (
|
||||
<DetailDrawer.Navigator screenOptions={{ drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front', drawerStyle: { width: state.subWidth } }}
|
||||
drawerContent={(props) => <DetailDrawerContent {...props} />}>
|
||||
<DetailDrawer.Screen name="contact">
|
||||
{(props) => <ProfileDrawerScreen detailNav={props.navigation} />}
|
||||
</DetailDrawer.Screen>
|
||||
</DetailDrawer.Navigator>
|
||||
<ProfileDrawer.Navigator screenOptions={{ drawerPosition: 'right', headerShown: false, swipeEnabled: false, drawerType: 'front', drawerStyle: { width: state.subWidth } }}
|
||||
drawerContent={(props) => <Profile />}>
|
||||
<ProfileDrawer.Screen name="detail">
|
||||
{(props) => <DetailDrawerScreen profileNav={props.navigation}/>}
|
||||
</ProfileDrawer.Screen>
|
||||
</ProfileDrawer.Navigator>
|
||||
)}
|
||||
{ state.tabbed === true && (
|
||||
<Tab.Navigator
|
||||
@ -235,15 +366,9 @@ export function Session() {
|
||||
tabBarActiveTintColor: Colors.white,
|
||||
tabBarInactiveTintColor: Colors.disabled,
|
||||
})}>
|
||||
<Tab.Screen name="Conversation">
|
||||
{(props) => (<SafeAreaView style={styles.tabframe} edges={['top']}><ConversationStackScreen /></SafeAreaView>)}
|
||||
</Tab.Screen>
|
||||
<Tab.Screen name="Profile">
|
||||
{(props) => (<SafeAreaView style={styles.tabframe} edges={['top']}><ProfileStackScreen /></SafeAreaView>)}
|
||||
</Tab.Screen>
|
||||
<Tab.Screen name="Contacts">
|
||||
{(props) => (<SafeAreaView style={styles.tabframe} edges={['top']}><ContactStackScreen /></SafeAreaView>)}
|
||||
</Tab.Screen>
|
||||
<Tab.Screen name="Conversation" component={ConversationStackScreen} />
|
||||
<Tab.Screen name="Profile" component={ProfileStackScreen} />
|
||||
<Tab.Screen name="Contacts" component={ContactStackScreen} />
|
||||
</Tab.Navigator>
|
||||
)}
|
||||
</View>
|
||||
|
@ -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',
|
||||
|
@ -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 (
|
||||
<View style={styles.title}>
|
||||
{ state.sorting && (
|
||||
<TouchableOpacity style={styles.sort} onPress={actions.unsort}>
|
||||
<Ionicons style={styles.icon} name="menufold" size={18} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ !state.sorting && (
|
||||
<TouchableOpacity style={styles.sort} onPress={actions.sort}>
|
||||
<Ionicons style={styles.icon} name="menufold" size={18} color={Colors.disabled} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="search1" size={16} color={Colors.disabled} />
|
||||
<TextInput style={styles.inputfield} value={state.filter} onChangeText={actions.setFilter}
|
||||
autoCapitalize="none" placeholderTextColor={Colors.disabled} placeholder="Contacts" />
|
||||
<View style={styles.space} />
|
||||
</View>
|
||||
<TouchableOpacity style={styles.add} onPress={() => openRegistry(navigation)}>
|
||||
<Ionicons name={'adduser'} size={16} color={Colors.white} style={[styles.box, { transform: [ { rotateY: "180deg" }, ]} ]}/>
|
||||
<Text style={styles.newtext}>New</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <TouchableOpacity onPress={onPressCard}><Text>CARD</Text></TouchableOpacity>
|
||||
export function CardsBody({ state, actions, openContact }) {
|
||||
return (
|
||||
<FlatList style={styles.cards}
|
||||
data={state.cards}
|
||||
renderItem={({ item }) => <CardItem item={item} openContact={openContact} />}
|
||||
keyExtractor={item => item.cardId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Cards({ openRegistry, openContact }) {
|
||||
const { state, actions } = useCards();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{ state.tabbed && (
|
||||
<>
|
||||
<View style={styles.topbar}>
|
||||
{ state.sorting && (
|
||||
<TouchableOpacity style={styles.sort} onPress={actions.unsort}>
|
||||
<Ionicons style={styles.icon} name="menufold" size={18} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ !state.sorting && (
|
||||
<TouchableOpacity style={styles.sort} onPress={actions.sort}>
|
||||
<Ionicons style={styles.icon} name="menufold" size={18} color={Colors.disabled} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="search1" size={16} color={Colors.disabled} />
|
||||
<TextInput style={styles.inputfield} value={state.filter} onChangeText={actions.setFilter}
|
||||
autoCapitalize="none" placeholderTextColor={Colors.disabled} placeholder="Contacts" />
|
||||
<View style={styles.space} />
|
||||
</View>
|
||||
<TouchableOpacity style={styles.add} onPress={openRegistry}>
|
||||
<Ionicons name={'adduser'} size={16} color={Colors.white} style={[styles.box, { transform: [ { rotateY: "180deg" }, ]} ]}/>
|
||||
<Text style={styles.newtext}>New</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<FlatList style={styles.cards}
|
||||
data={state.cards}
|
||||
renderItem={({ item }) => <CardItem item={item} openContact={openContact} />}
|
||||
keyExtractor={item => item.cardId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{ !state.tabbed && (
|
||||
<>
|
||||
<View style={styles.searcharea}>
|
||||
<SafeAreaView edges={['top', 'right']} style={styles.searchbar}>
|
||||
{ state.sorting && (
|
||||
<TouchableOpacity style={styles.sort} onPress={actions.unsort}>
|
||||
<Ionicons style={styles.icon} name="menufold" size={18} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ !state.sorting && (
|
||||
<TouchableOpacity style={styles.sort} onPress={actions.sort}>
|
||||
<Ionicons style={styles.icon} name="menufold" size={18} color={Colors.disabled} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="search1" size={16} color={Colors.disabled} />
|
||||
<TextInput style={styles.inputfield} value={state.filter} onChangeText={actions.setFilter}
|
||||
autoCapitalize="none" placeholderTextColor={Colors.disabled} placeholder="Contacts" />
|
||||
</View>
|
||||
<TouchableOpacity style={styles.add} onPress={openRegistry}>
|
||||
<Ionicons name={'adduser'} size={16} color={Colors.white} style={[styles.box, { transform: [ { rotateY: "180deg" }, ]} ]}/>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
<SafeAreaView edges={['right']} style={styles.searcharea}>
|
||||
<FlatList style={styles.cards}
|
||||
data={state.cards}
|
||||
renderItem={({ item }) => <CardItem item={item} openContact={openContact} />}
|
||||
keyExtractor={item => item.cardId}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
109
app/mobile/src/session/cards/Cards.styled.js
Normal file
@ -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,
|
||||
}
|
||||
})
|
||||
|
46
app/mobile/src/session/cards/cardItem/CardItem.jsx
Normal file
@ -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 (
|
||||
<View>
|
||||
{ item.cardId && (
|
||||
<TouchableOpacity style={styles.container} activeOpacity={1} onPress={select}>
|
||||
<Logo src={item.logo} width={32} height={32} radius={6} />
|
||||
<View style={styles.detail}>
|
||||
<Text style={styles.name} numberOfLines={1} ellipsizeMode={'tail'}>{ item.name }</Text>
|
||||
<Text style={styles.handle} numberOfLines={1} ellipsizeMode={'tail'}>{ item.handle }</Text>
|
||||
</View>
|
||||
{ item.status === 'connected' && (
|
||||
<View style={styles.connected} />
|
||||
)}
|
||||
{ item.status === 'requested' && (
|
||||
<View style={styles.requested} />
|
||||
)}
|
||||
{ item.status === 'connecting' && (
|
||||
<View style={styles.connecting} />
|
||||
)}
|
||||
{ item.status === 'pending' && (
|
||||
<View style={styles.pending} />
|
||||
)}
|
||||
{ item.status === 'confirmed' && (
|
||||
<View style={styles.confirmed} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ !item.cardId && (
|
||||
<View style={styles.space} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
64
app/mobile/src/session/cards/cardItem/CardItem.styled.js
Normal file
@ -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,
|
||||
},
|
||||
})
|
||||
|
17
app/mobile/src/session/cards/cardItem/useCardItem.hook.js
Normal file
@ -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 };
|
||||
}
|
||||
|
107
app/mobile/src/session/cards/useCards.hook.js
Normal file
@ -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 };
|
||||
}
|
||||
|
@ -5,17 +5,15 @@ 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 (
|
||||
<View style={styles.container}>
|
||||
{ state.tabbed && (
|
||||
<View style={styles.topbar}>
|
||||
<View style={styles.title}>
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="search1" size={16} color={Colors.text} />
|
||||
<TextInput style={styles.inputfield} value={state.topic} onChangeText={actions.setTopic}
|
||||
autoCapitalize="none" placeholderTextColor={Colors.text} placeholder="Topic" />
|
||||
<Ionicons style={styles.icon} name="search1" size={16} color={Colors.disabled} />
|
||||
<TextInput style={styles.inputfield} value={state.filter} onChangeText={actions.setFilter}
|
||||
autoCapitalize="none" placeholderTextColor={Colors.disabled} placeholder="Topics" />
|
||||
<View style={styles.space} />
|
||||
</View>
|
||||
<TouchableOpacity style={styles.add}>
|
||||
@ -23,28 +21,45 @@ export function Channels() {
|
||||
<Text style={styles.newtext}>New</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
{ !state.tabbed && (
|
||||
<View style={styles.searchbar}>
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="search1" size={16} color={Colors.text} />
|
||||
<TextInput style={styles.inputfield} value={state.topic} onChangeText={actions.setTopic}
|
||||
autoCapitalize="none" placeholderTextColor={Colors.text} placeholder="Topic" />
|
||||
<View style={styles.space} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
export function ChannelsBody({ state, actions, openConversation }) {
|
||||
return (
|
||||
<FlatList style={styles.channels}
|
||||
data={state.channels}
|
||||
renderItem={({ item }) => <ChannelItem item={item} />}
|
||||
renderItem={({ item }) => <ChannelItem item={item} openConversation={openConversation} />}
|
||||
keyExtractor={item => (`${item.cardId}:${item.channelId}`)}
|
||||
/>
|
||||
{ !state.tabbed && (
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export function Channels({ openConversation }) {
|
||||
const { state, actions } = useChannels();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SafeAreaView edges={['left']} style={styles.searchbar}>
|
||||
<View style={styles.inputwrapper}>
|
||||
<Ionicons style={styles.icon} name="search1" size={16} color={Colors.disabled} />
|
||||
<TextInput style={styles.inputfield} value={state.topic} onChangeText={actions.setTopic}
|
||||
autoCapitalize="none" placeholderTextColor={Colors.disabled} placeholder="Topics" />
|
||||
<View style={styles.space} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<SafeAreaView style={styles.channels} edges={['left']}>
|
||||
<FlatList
|
||||
data={state.channels}
|
||||
renderItem={({ item }) => <ChannelItem item={item} openConversation={openConversation} />}
|
||||
keyExtractor={item => (`${item.cardId}:${item.channelId}`)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
<SafeAreaView style={styles.bottomArea} edges={['left']}>
|
||||
<TouchableOpacity style={styles.addbottom}>
|
||||
<Ionicons name={'message1'} size={16} color={Colors.white} />
|
||||
<Text style={styles.newtext}>New Topic</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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 (
|
||||
<TouchableOpacity style={styles.container} activeOpacity={1} onPress={actions.setRead}>
|
||||
<Logo src={item.logo} width={40} height={40} radius={6} />
|
||||
<TouchableOpacity style={styles.container} activeOpacity={1} onPress={() => openConversation(item.cardId, item.channelId, item.revision)}>
|
||||
<Logo src={item.logo} width={32} height={32} radius={6} />
|
||||
<View style={styles.detail}>
|
||||
<Text style={styles.subject} numberOfLines={1} ellipsizeMode={'tail'}>{ item.subject }</Text>
|
||||
<Text style={styles.message} numberOfLines={1} ellipsizeMode={'tail'}>{ item.message }</Text>
|
||||
|
@ -21,9 +21,11 @@ export const styles = StyleSheet.create({
|
||||
},
|
||||
subject: {
|
||||
color: Colors.text,
|
||||
fontSize: 14,
|
||||
},
|
||||
message: {
|
||||
color: Colors.disabled,
|
||||
fontSize: 12,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
|
@ -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) => {
|
||||
if (!card.blocked) {
|
||||
merged.push(...Array.from(card.channels.values()));
|
||||
}
|
||||
});
|
||||
merged.push(...Array.from(channel.state.channels.values()));
|
||||
|
||||
merged.sort((a, b) => {
|
||||
const aCreated = a?.summary?.lastTopic?.created;
|
||||
const bCreated = b?.summary?.lastTopic?.created;
|
||||
const items = merged.map(setChannelEntry);
|
||||
|
||||
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 };
|
||||
|
@ -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();
|
||||
export function ContactTitle({ contact, closeContact }) {
|
||||
const { state, actions } = useContact(contact, closeContact);
|
||||
return (<Text style={styles.title}>{ `${state.handle}@${state.node}` }</Text>);
|
||||
}
|
||||
|
||||
return <TouchableOpacity onPress={onPressCard}><Text>CLOSE</Text></TouchableOpacity>
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.status}>{ `[${getStatusText(state.status)}]` }</Text>
|
||||
<View style={{ width: 128 }}>
|
||||
<Logo src={state.logo} width={128} height={128} radius={8} />
|
||||
</View>
|
||||
<View style={styles.detail}>
|
||||
<View style={styles.attribute}>
|
||||
<Text style={styles.nametext}>{ state.name }</Text>
|
||||
</View>
|
||||
<View style={styles.attribute}>
|
||||
<Ionicons name="enviromento" size={14} color={Colors.text} />
|
||||
<Text style={styles.locationtext}>{ state.location }</Text>
|
||||
</View>
|
||||
<View style={styles.attribute}>
|
||||
<Ionicons name="book" size={14} color={Colors.text} />
|
||||
<Text style={styles.descriptiontext}>{ state.description }</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.controls}>
|
||||
{ state.status === 'connected' && (
|
||||
<>
|
||||
<TouchableOpacity style={styles.button} onPress={disconnectContact}>
|
||||
<Text style={styles.buttonText}>Disconnect</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={closeDelete}>
|
||||
<Text style={styles.buttonText}>Delete Contact</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={blockContact}>
|
||||
<Text style={styles.buttonText}>Block Contact</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
{ state.status === 'connecting' && (
|
||||
<>
|
||||
<TouchableOpacity style={styles.button} onPress={disconnectContact}>
|
||||
<Text style={styles.buttonText}>Cancel Request</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={closeDelete}>
|
||||
<Text style={styles.buttonText}>Delete Contact</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={blockContact}>
|
||||
<Text style={styles.buttonText}>Block Contact</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
{ state.status === 'confirmed' && (
|
||||
<>
|
||||
<TouchableOpacity style={styles.button} onPress={connectContact}>
|
||||
<Text style={styles.buttonText}>Request Connection</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={deleteContact}>
|
||||
<Text style={styles.buttonText}>Delete Contact</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={blockContact}>
|
||||
<Text style={styles.buttonText}>Block Contact</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
{ state.status === 'pending' && (
|
||||
<>
|
||||
<TouchableOpacity style={styles.button} onPress={saveAndConnect}>
|
||||
<Text style={styles.buttonText}>Save and Connect</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={saveContact}>
|
||||
<Text style={styles.buttonText}>Save Contact</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={deleteContact}>
|
||||
<Text style={styles.buttonText}>Ignore Request</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={blockContact}>
|
||||
<Text style={styles.buttonText}>Block Contact</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
{ state.status === 'requested' && (
|
||||
<>
|
||||
<TouchableOpacity style={styles.button} onPress={connectContact}>
|
||||
<Text style={styles.buttonText}>Accept Connection</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={ignoreContact}>
|
||||
<Text style={styles.buttonText}>Ignore Request</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={disconnectContact}>
|
||||
<Text style={styles.buttonText}>Deny Request</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={deleteContact}>
|
||||
<Text style={styles.buttonText}>Delete Contact</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={blockContact}>
|
||||
<Text style={styles.buttonText}>Block Contact</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
{ state.status == null && (
|
||||
<>
|
||||
<TouchableOpacity style={styles.button} onPress={saveAndConnect}>
|
||||
<Text style={styles.buttonText}>Save and Connect</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={saveContact}>
|
||||
<Text style={styles.buttonText}>Save Contact</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.wrapper}>
|
||||
{ state.tabbed && (
|
||||
<Body />
|
||||
)}
|
||||
{ !state.tabbed && (
|
||||
<SafeAreaView style={styles.drawer} edges={['top', 'bottom', 'right']}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerText}>{ `${state.handle}@${state.node}` }</Text>
|
||||
</View>
|
||||
<Body />
|
||||
</SafeAreaView>
|
||||
)}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
|
103
app/mobile/src/session/contact/Contact.styled.js
Normal file
@ -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,
|
||||
},
|
||||
})
|
||||
|
167
app/mobile/src/session/contact/useContact.hook.js
Normal file
@ -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 };
|
||||
}
|
||||
|
@ -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 (
|
||||
<View style={styles.title}>
|
||||
<View style={styles.subject}>
|
||||
<Text style={styles.subjectText} numberOfLines={1} ellipsizeMode={'tail'}>{ state.subject }</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.action} onPress={setDetails}>
|
||||
<Ionicons name="setting" size={26} color={Colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const RenderItem = memo((props: { item: number }) => {
|
||||
return (<TopicItem item={props.item} />)
|
||||
});
|
||||
|
||||
const renderItemHandler = ({ item }: { item: number }) => {
|
||||
return <RenderItem item={item} />
|
||||
}
|
||||
|
||||
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 (
|
||||
<KeyboardAvoidingView style={styles.thread} behavior="padding" keyboardVerticalOffset="100"
|
||||
enabled={Platform.OS === 'ios' ? true : false}>
|
||||
<FlatList style={styles.topics}
|
||||
ref={ref}
|
||||
data={state.topics}
|
||||
onMomentumScrollEnd={ Platform.OS === 'ios' ? noop : actions.unlatch }
|
||||
onScrollBeginDrag={ Platform.OS !== 'ios' ? noop : actions.unlatch }
|
||||
maintainVisibleContentPosition={ state.latched ? null : { minIndexForVisibile: 2, } }
|
||||
inverted={true}
|
||||
renderItem={renderItemHandler}
|
||||
keyExtractor={item => item.topicId}
|
||||
/>
|
||||
<View>
|
||||
<AddTopic />
|
||||
<View style={styles.latchbar}>
|
||||
{ !state.latched && (
|
||||
<TouchableOpacity style={styles.latch} onPress={latch}>
|
||||
<Ionicons name="unlock" size={16} color={Colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
export function Conversation({ closeConversation, openDetails }) {
|
||||
const { state, actions } = useConversation();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SafeAreaView edges={['right']} style={styles.header}>
|
||||
<Text style={styles.subjectText} numberOfLines={1} ellipsizeMode={'tail'}>{ state.subject }</Text>
|
||||
<TouchableOpacity style={styles.action} onPress={openDetails}>
|
||||
<Ionicons name="setting" size={24} color={Colors.primary} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.close} onPress={closeConversation}>
|
||||
<Ionicons name="close" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
<SafeAreaView edges={['bottom']} style={styles.body}>
|
||||
<ConversationBody />
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
103
app/mobile/src/session/conversation/Conversation.styled.js
Normal file
@ -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,
|
||||
},
|
||||
})
|
||||
|
236
app/mobile/src/session/conversation/addTopic/AddTopic.jsx
Normal file
@ -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 (
|
||||
<ImageFile path={item.data} remove={() => remove(item)} />
|
||||
);
|
||||
}
|
||||
if (item.type === 'video') {
|
||||
return (
|
||||
<VideoFile path={item.data}
|
||||
remove={() => remove(item)}
|
||||
setPosition={(position) => actions.setVideoPosition(item.key, position)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (item.type === 'audio') {
|
||||
return (
|
||||
<AudioFile path={item.data} label={item.label} remove={() => remove(item)}
|
||||
setLabel={(label) => actions.setAudioLabel(item.key, label)} />
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<View style={styles.asset}></View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.add} edges={['right']}>
|
||||
{ state.assets.length > 0 && (
|
||||
<FlatList style={styles.carousel}
|
||||
data={state.assets}
|
||||
horizontal={true}
|
||||
renderItem={renderAsset}
|
||||
/>
|
||||
)}
|
||||
<TextInput style={styles.input} value={state.message} onChangeText={actions.setMessage} ref={message}
|
||||
onSubmitEditing={sendMessage} returnKeyType="send"
|
||||
autoCapitalize="sentences" placeholder="New Message" multiline={true} />
|
||||
<View style={styles.addButtons}>
|
||||
<TouchableOpacity style={styles.addButton} onPress={addImage}>
|
||||
<AntIcons name="picture" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.addButton} onPress={addVideo}>
|
||||
<MaterialIcons name="video-outline" size={24} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.addButton} onPress={addAudio}>
|
||||
<MaterialIcons name="music-box-outline" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.divider} />
|
||||
<TouchableOpacity style={styles.addButton} onPress={actions.showFontSize}>
|
||||
<MaterialIcons name="format-size" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.addButton} onPress={actions.showFontColor}>
|
||||
<MaterialIcons name="palette-outline" size={20} color={Colors.text} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.space} />
|
||||
<TouchableOpacity style={styles.addButton} onPress={sendMessage}>
|
||||
{ state.busy && (
|
||||
<ActivityIndicator color={Colors.white} />
|
||||
)}
|
||||
{ !state.busy && (state.message || state.assets.length > 0) && (
|
||||
<MaterialIcons name="send-outline" size={20} color={Colors.text} />
|
||||
)}
|
||||
{ !state.busy && !(state.message || state.assets.length > 0) && (
|
||||
<MaterialIcons name="send-outline" size={20} color={Colors.lightgrey} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={state.fontSize}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={actions.hideFontSize}
|
||||
>
|
||||
<View style={styles.editWrapper}>
|
||||
<View style={styles.editContainer}>
|
||||
<Text style={styles.editHeader}>Font Size:</Text>
|
||||
<View style={styles.editSize}>
|
||||
{ state.size === 'small' && (
|
||||
<View style={styles.selected}>
|
||||
<Text style={styles.selectedText}>Small</Text>
|
||||
</View>
|
||||
)}
|
||||
{ state.size !== 'small' && (
|
||||
<TouchableOpacity style={styles.option} onPress={() => actions.setFontSize('small')}>
|
||||
<Text style={styles.optionText}>Small</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ state.size === 'medium' && (
|
||||
<View style={styles.selected}>
|
||||
<Text style={styles.selectedText}>Medium</Text>
|
||||
</View>
|
||||
)}
|
||||
{ state.size !== 'medium' && (
|
||||
<TouchableOpacity style={styles.option} onPress={() => actions.setFontSize('medium')}>
|
||||
<Text style={styles.optionText}>Medium</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ state.size === 'large' && (
|
||||
<View style={styles.selected}>
|
||||
<Text style={styles.selectedText}>Large</Text>
|
||||
</View>
|
||||
)}
|
||||
{ state.size !== 'large' && (
|
||||
<TouchableOpacity style={styles.option} onPress={() => actions.setFontSize('large')}>
|
||||
<Text style={styles.optionText}>Large</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.editControls}>
|
||||
<View style={styles.selection} />
|
||||
<TouchableOpacity style={styles.close} onPress={actions.hideFontSize}>
|
||||
<Text>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={state.fontColor}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={actions.hideFontColor}
|
||||
>
|
||||
<View style={styles.editWrapper}>
|
||||
<View style={styles.editContainer}>
|
||||
<Text style={styles.editHeader}>Font Color:</Text>
|
||||
<View style={styles.editColor}>
|
||||
<ColorPicker
|
||||
color={state.color}
|
||||
onColorChange={actions.setFontColor}
|
||||
onColorChangeComplete={actions.setFontColor}
|
||||
swatched={false}
|
||||
style={{flex: 1, padding: 8}} />
|
||||
</View>
|
||||
<View style={styles.editControls}>
|
||||
<View style={styles.selection}>
|
||||
<Text>Set Color:</Text>
|
||||
<View style={{ marginLeft: 6, borderRadius: 4, width: 16, height: 16, backgroundColor: state.color }} />
|
||||
</View>
|
||||
<TouchableOpacity style={styles.close} onPress={actions.hideFontColor}>
|
||||
<Text>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
141
app/mobile/src/session/conversation/addTopic/AddTopic.styled.js
Normal file
@ -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',
|
||||
},
|
||||
})
|
||||
|
@ -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 (
|
||||
<TouchableOpacity style={styles.audio} onLongPress={remove}>
|
||||
<Image source={audio} resizeMode={'cover'} style={styles.image} />
|
||||
<TextInput style={ styles.input } value={ label } onChangeText={setLabel}
|
||||
multiline={true} autoCapitalize={'none'} placeholder="Audio Label" />
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
})
|
||||
|
@ -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 (
|
||||
<TouchableOpacity activeOpacity={1} onLongPress={remove}>
|
||||
<Image source={{ uri: path }} style={{ width: 92 * state.ratio, height: 92, marginRight: 16 }} resizeMode={'cover'} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -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 };
|
||||
}
|
||||
|
113
app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js
Normal file
@ -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 };
|
||||
}
|
||||
|
@ -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 (
|
||||
<TouchableOpacity onPress={actions.setNextPosition} onLongPress={remove}>
|
||||
<Video source={{ uri: path }} style={{ width: 92 * state.ratio, height: 92, marginRight: 16 }} resizeMode={'cover'} paused={true}
|
||||
onLoad={setInfo} ref={(ref) => video.current = ref}
|
||||
/>
|
||||
<View style={styles.overlay}>
|
||||
<Icons name="arrow-right" size={20} color={Colors.white} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -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 };
|
||||
}
|
||||
|
105
app/mobile/src/session/conversation/topicItem/TopicItem.jsx
Normal file
@ -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 (
|
||||
<TouchableOpacity style={styles.frame} activeOpacity={1}>
|
||||
{ asset.item.image && (
|
||||
<ImageAsset topicId={item.topicId} asset={asset.item.image} />
|
||||
)}
|
||||
{ asset.item.video && (
|
||||
<VideoAsset topicId={item.topicId} asset={asset.item.video} />
|
||||
)}
|
||||
{ asset.item.audio && (
|
||||
<AudioAsset topicId={item.topicId} asset={asset.item.audio} active={state.activeId == asset.dataIndex}
|
||||
setActive={() => actions.setActive(asset.dataIndex)} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const renderThumb = (thumb) => {
|
||||
return (
|
||||
<View>
|
||||
{ thumb.item.image && (
|
||||
<ImageThumb topicId={item.topicId} asset={thumb.item.image} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
{ thumb.item.video && (
|
||||
<VideoThumb topicId={item.topicId} asset={thumb.item.video} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
{ thumb.item.audio && (
|
||||
<AudioThumb topicId={item.topicId} asset={thumb.item.audio} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.item}>
|
||||
<View style={styles.header}>
|
||||
<Logo src={state.logo} width={28} height={28} radius={6} />
|
||||
<Text style={styles.name}>{ state.name }</Text>
|
||||
<Text style={styles.timestamp}>{ state.timestamp }</Text>
|
||||
</View>
|
||||
{ state.status === 'confirmed' && (
|
||||
<>
|
||||
{ state.transform === 'complete' && state.assets && (
|
||||
<FlatList style={styles.carousel}
|
||||
data={state.assets}
|
||||
horizontal={true}
|
||||
renderItem={renderThumb}
|
||||
/>
|
||||
)}
|
||||
{ state.transform === 'incomplete' && (
|
||||
<AntIcons name="cloudo" size={32} color={Colors.background} />
|
||||
)}
|
||||
{ state.transform === 'error' && (
|
||||
<AntIcons name="cloudo" size={32} color={Colors.alert} />
|
||||
)}
|
||||
{ state.message && (
|
||||
<Text style={{ paddingLeft: 52, fontSize: state.fontSize, color: state.fontColor }}>{ state.message }</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{ state.status !== 'confirmed' && (
|
||||
<AntIcons name="cloudo" size={32} color={Colors.divider} />
|
||||
)}
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={state.carousel}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={actions.hideCarousel}
|
||||
>
|
||||
<View style={styles.modal}>
|
||||
<GestureRecognizer onSwipeUp={actions.hideCarousel} onSwipeDown={actions.hideCarousel}>
|
||||
<Carousel
|
||||
data={state.assets}
|
||||
firstItem={state.carouselIndex}
|
||||
renderItem={renderAsset}
|
||||
sliderWidth={state.width}
|
||||
itemWidth={state.width}
|
||||
/>
|
||||
</GestureRecognizer>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -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%',
|
||||
},
|
||||
})
|
||||
|
@ -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 (
|
||||
<View style={styles.background}>
|
||||
<Image source={audio} style={{ width: state.length, height: state.length }} resizeMode={'cover'} />
|
||||
<Text style={styles.label}>{ asset.label }</Text>
|
||||
{ state.playing && active && (
|
||||
<TouchableOpacity style={styles.control} onPress={actions.pause}>
|
||||
<Icons name="stop-circle-outline" size={92} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ (!state.playing || !active) && (
|
||||
<TouchableOpacity style={styles.control} onPress={play}>
|
||||
<Icons name="play-circle-outline" size={92} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -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',
|
||||
}
|
||||
})
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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 (
|
||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||
<Image source={audio} style={{ borderRadius: 4, width: 92, height: 92, marginRight: 16, backgroundColor: Colors.lightgrey }} resizeMode={'cover'} />
|
||||
{ asset.label && (
|
||||
<View style={styles.overlay}>
|
||||
<Text style={styles.label}>{ asset.label }</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
},
|
||||
})
|
||||
|
@ -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 (
|
||||
<Image source={{ uri: state.url }} style={{ borderRadius: 4, width: state.imageWidth, height: state.imageHeight }} resizeMode={'cover'} />
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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 (
|
||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||
<Image source={{ uri: state.url }} style={{ borderRadius: 4, width: 92 * state.ratio, height: 92, marginRight: 16 }} resizeMode={'cover'} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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 && (
|
||||
<Video source={{ uri: state.url }} style={{ width: state.width, height: state.height }} resizeMode={'cover'}
|
||||
onReadyForDisplay={(e) => actions.setResolution(e.naturalSize.width, e.naturalSize.height)}
|
||||
useNativeControls={state.controls} resizeMode="contain" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|