Merge branch 'main' of https://github.com/balzack/databag into main

This commit is contained in:
Roland Osborne 2022-10-11 13:57:17 -07:00
commit e67f704d53
128 changed files with 6386 additions and 476 deletions

View File

@ -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>
);
}

View File

@ -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;

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -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
}
}

View File

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

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

View File

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

View File

@ -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"
},

View File

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

View File

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

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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;

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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()

View File

@ -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}`
}

View File

@ -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 {

View File

@ -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()

View File

@ -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}`
}

View File

@ -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 {

View 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()
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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',

View 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>
);
}

View 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>
);
}

View File

@ -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 }))

View File

@ -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 }

View File

@ -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 }

View 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 }
}

View File

@ -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 }

View File

@ -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) {
}

View 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();
}
}
}

View File

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

View File

@ -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',

View File

@ -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>
);
}

View 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,
}
})

View 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>
);
}

View 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,
},
})

View 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 };
}

View 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 };
}

View File

@ -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>
);
}

View File

@ -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,
},
})

View File

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

View File

@ -21,9 +21,11 @@ export const styles = StyleSheet.create({
},
subject: {
color: Colors.text,
fontSize: 14,
},
message: {
color: Colors.disabled,
fontSize: 12,
},
dot: {
width: 8,

View File

@ -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 };

View File

@ -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>
)
}

View 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,
},
})

View 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 };
}

View File

@ -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>
);
}

View 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,
},
})

View 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>
);
}

View 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',
},
})

View File

@ -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>
)
}

View File

@ -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,
}
})

View File

@ -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>
);
}

View File

@ -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,
},
})

View File

@ -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 };
}

View 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 };
}

View File

@ -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>
);
}

View File

@ -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,
},
})

View File

@ -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 };
}

View 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>
);
}

View File

@ -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%',
},
})

View File

@ -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>
);
}

View File

@ -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',
}
})

View File

@ -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 };
}

View File

@ -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>
);
}

View File

@ -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',
},
})

View File

@ -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'} />
);
}

View File

@ -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,
},
})

View File

@ -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 };
}

View File

@ -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>
);
}

View File

@ -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,
},
})

View File

@ -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 };
}

View File

@ -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 };
}

View File

@ -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" />
)}
</>
);
}

Some files were not shown because too many files have changed in this diff Show More