merging back refactored profile screen

This commit is contained in:
Roland Osborne 2023-02-21 15:52:54 -08:00
parent d91a120ec8
commit afc8c45721
18 changed files with 1533 additions and 11 deletions

View File

@ -567,7 +567,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++14";
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
@ -639,7 +639,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++14";
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;

View File

@ -428,6 +428,15 @@ PODS:
- RNFBApp
- RNGestureHandler (2.9.0):
- React-Core
- RNImageCropPicker (0.39.0):
- React-Core
- React-RCTImage
- RNImageCropPicker/QBImagePickerController (= 0.39.0)
- TOCropViewController
- RNImageCropPicker/QBImagePickerController (0.39.0):
- React-Core
- React-RCTImage
- TOCropViewController
- RNReanimated (2.14.4):
- DoubleConversion
- FBLazyVector
@ -460,6 +469,7 @@ PODS:
- React-RCTImage
- RNVectorIcons (9.2.0):
- React-Core
- TOCropViewController (2.6.1)
- Yoga (1.14.0)
DEPENDENCIES:
@ -506,6 +516,7 @@ DEPENDENCIES:
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
@ -525,6 +536,7 @@ SPEC REPOS:
- libevent
- nanopb
- PromisesObjC
- TOCropViewController
EXTERNAL SOURCES:
boost:
@ -609,6 +621,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-firebase/messaging"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNImageCropPicker:
:path: "../node_modules/react-native-image-crop-picker"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
@ -672,9 +686,11 @@ SPEC CHECKSUMS:
RNFBApp: 4f8ea53443d52c7db793234d2398a357fc6cfbf1
RNFBMessaging: c686471358d20d54f716a8b7b7f10f8944c966ec
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332
RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
PODFILE CHECKSUM: 8a4cbf61c865c6e404d389f26395fa04926b8f43

View File

@ -20,11 +20,13 @@
"@react-navigation/stack": "^6.3.14",
"axios": "^1.3.3",
"crypto-js": "^4.1.1",
"moment": "^2.29.4",
"react": "18.2.0",
"react-native": "0.71.3",
"react-native-base64": "^0.2.1",
"react-native-device-info": "^10.4.0",
"react-native-gesture-handler": "^2.9.0",
"react-native-image-crop-picker": "^0.39.0",
"react-native-reanimated": "^2.14.4",
"react-native-rsa-native": "^2.0.5",
"react-native-safe-area-context": "^4.5.0",

View File

@ -16,7 +16,7 @@ import messaging from '@react-native-firebase/messaging';
export function useAppContext() {
const [state, setState] = useState({
session: null,
status: 'disconnected',
status: null,
loggingOut: false,
adminToken: null,
version: getVersion(),

View File

@ -1,4 +1,4 @@
import { View, TouchableOpacity, StatusBar, Text, Image } from 'react-native';
import { View, ScrollView, TouchableOpacity, StatusBar, Text, Image } from 'react-native';
import { useState, useEffect, useContext } from 'react';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
@ -9,7 +9,7 @@ import Ionicons from 'react-native-vector-icons/AntDesign';
import { useSession } from './useSession.hook';
import { styles } from './Session.styled';
import Colors from 'constants/Colors';
import { Profile } from './profile/Profile';
import { Profile, ProfileHeader, ProfileBody } from './profile/Profile';
import { CardsTitle, CardsBody, Cards } from './cards/Cards';
import { RegistryTitle, RegistryBody, Registry } from './registry/Registry';
import { Contact, ContactTitle } from './contact/Contact';
@ -83,7 +83,9 @@ export function Session() {
const ProfileStackScreen = () => {
return (
<ProfileStack.Navigator screenOptions={({ route }) => (screenParams)}>
<ProfileStack.Screen name="profile" component={Profile} options={{ headerStyle: { backgroundColor: Colors.titleBackground }}} />
<ProfileStack.Screen name="profile" options={{ ...stackParams, headerTitle: () => <ProfileHeader /> }}>
{(props) => <ScrollView><ProfileBody /></ScrollView>}
</ProfileStack.Screen>
</ProfileStack.Navigator>
);
}
@ -268,8 +270,9 @@ export function Session() {
{ state.firstRun == false && (
<View style={styles.container}>
{ state.tabbed === false && (
<ProfileDrawer.Navigator screenOptions={{ ...drawerParams, drawerStyle: { width: '45%' } }}
drawerContent={(props) => <Profile />}>
<ProfileDrawer.Navigator screenOptions={{ ...drawerParams, drawerStyle: { width: '45%' } }} drawerContent={(props) => (
<ScrollView><SafeAreaView style={styles.drawer} edges={['top', 'bottom', 'right']}><Profile /></SafeAreaView></ScrollView>
)}>
<ProfileDrawer.Screen name="detail">
{(props) => <DetailDrawerScreen navParams={{ profileNav: props.navigation }} />}
</ProfileDrawer.Screen>

View File

@ -1,6 +1,590 @@
import { Text } from 'react-native';
import { ActivityIndicator, KeyboardAvoidingView, Modal, View, Switch, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
import Ionicons from 'react-native-vector-icons/AntDesign';
import ImagePicker from 'react-native-image-crop-picker'
import { Colors } from 'constants/Colors';
import { useProfile } from './useProfile.hook';
import { Logo } from 'utils/Logo';
import { styles } from './Profile.styled';
import { BlockedTopics } from './blockedTopics/BlockedTopics';
import { BlockedContacts } from './blockedContacts/BlockedContacts';
import { BlockedMessages } from './blockedMessages/BlockedMessages';
export function Profile({ navigation }) {
return <Text>Profile</Text>;
export function ProfileHeader() {
const { state, actions } = useProfile();
return (
<Text style={styles.headerText}>{ `${state.handle}@${state.node}` }</Text>
)
}
export function ProfileBody() {
const { state, actions } = useProfile();
const logout = async () => {
Alert.alert(
"Logging Out",
"Confirm?",
[
{ text: "Cancel",
onPress: () => {},
},
{ text: "Logout", onPress: () => {
actions.logout();
}}
]
);
}
const remove = async () => {
try {
await actions.remove();
}
catch (err) {
console.log(err);
Alert.alert(
'Failed to Delete Account',
'Please try again.'
)
}
}
const onGallery = async () => {
try {
const full = await ImagePicker.openPicker({ mediaType: 'photo', width: 256, height: 256 });
const crop = await ImagePicker.openCropper({ path: full.path, width: 256, height: 256, cropperCircleOverlay: true, includeBase64: true });
await actions.setProfileImage(crop.data);
}
catch (err) {
console.log(err);
}
}
const setNotifications = async (notify) => {
try {
await actions.setNotifications(notify);
}
catch (err) {
console.log(err);
Alert.alert(
'Account Update Failed',
'Please try again.',
);
}
}
const setVisible = async (visible) => {
try {
await actions.setVisible(visible);
}
catch (err) {
console.log(err);
Alert.alert(
'Account Update Failed',
'Please try again.'
);
}
}
const saveSeal = async () => {
try {
await actions.saveSeal();
actions.hideSealEdit();
}
catch (err) {
console.log(err);
Alert.alert(
'Failed to Update Topic Sealing',
'Please try again.',
)
}
}
const saveDetails = async () => {
try {
await actions.saveDetails();
actions.hideDetailEdit();
}
catch (err) {
console.log(err);
Alert.alert(
'Failed to Save Details',
'Please try again.'
)
}
}
const saveLogin = async () => {
try {
await actions.saveLogin();
actions.hideLoginEdit();
}
catch (err) {
console.log(err);
Alert.alert(
'Failed to Change Login',
'Please try again.'
)
}
}
return (
<View style={styles.body}>
<View style={styles.logo}>
<View>
<Logo src={state.imageSource} width={128} height={128} radius={8} />
<TouchableOpacity style={styles.gallery} onPress={onGallery}>
<Ionicons name="picture" size={14} color={Colors.white} />
</TouchableOpacity>
</View>
</View>
<View style={styles.alert}>
{ state.disconnected && (
<Text style={styles.alertText}>Disconnected</Text>
)}
</View>
<TouchableOpacity style={styles.detail} onPress={actions.showEditDetails}>
<View style={styles.attribute}>
{ state.name && (
<Text style={styles.nametext}>{ state.name }</Text>
)}
{ !state.name && (
<Text style={styles.nonametext}>Name</Text>
)}
<Ionicons name="edit" size={16} color={Colors.text} />
</View>
<View style={styles.attribute}>
<View style={styles.icon}>
<Ionicons name="enviromento" size={14} color={Colors.text} />
</View>
{ state.location && (
<Text style={styles.locationtext}>{ state.location }</Text>
)}
{ !state.location && (
<Text style={styles.nolocationtext}>Location</Text>
)}
</View>
<View style={styles.attribute}>
<View style={styles.icon}>
<Ionicons name="book" size={14} color={Colors.text} />
</View>
{ state.description && (
<Text style={styles.descriptiontext}>{ state.description }</Text>
)}
{ !state.description && (
<Text style={styles.nodescriptiontext}>Description</Text>
)}
</View>
</TouchableOpacity>
<View style={styles.enable}>
<TouchableOpacity onPress={() => setVisible(!state.searchable)} activeOpacity={1}>
<Text style={styles.enableText}>Visible in Registry</Text>
</TouchableOpacity>
<Switch style={styles.enableSwitch} value={state.searchable} onValueChange={setVisible} trackColor={styles.switch}/>
</View>
<View style={styles.enable}>
<TouchableOpacity onPress={() => setNotifications(!state.pushEnabled)} activeOpacity={1}>
<Text style={styles.enableText}>Enable Notifications</Text>
</TouchableOpacity>
<Switch style={styles.enableSwitch} value={state.pushEnabled} onValueChange={setNotifications} trackColor={styles.switch}/>
</View>
{ state.sealable && (
<TouchableOpacity style={styles.link} onPress={actions.showSealEdit}>
<Ionicons name="setting" size={14} color={Colors.primary} />
<Text style={styles.linkText}>Sealed Topics</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.logout} onPress={actions.showLoginEdit}>
<Ionicons name="lock" size={16} color={Colors.primary} />
<Text style={styles.logoutText}>Change Login</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.logout} activeOpacity={1} onPress={logout}>
<Ionicons name="logout" size={16} color={Colors.primary} />
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.delete} activeOpacity={1} onPress={actions.showDelete}>
<Ionicons name="delete" size={16} color={Colors.alert} />
<Text style={styles.deleteText}>Delete Account</Text>
</TouchableOpacity>
<Text style={styles.blockedLabel}>Manage Blocked:</Text>
<View style={styles.blocked}>
<TouchableOpacity style={styles.link} onPress={actions.showBlockedCards}>
<Text style={styles.linkText}>Contacts</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.link} onPress={actions.showBlockedChannels}>
<Text style={styles.linkText}>Topics</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.link} onPress={actions.showBlockedMessages}>
<Text style={styles.linkText}>Messages</Text>
</TouchableOpacity>
</View>
<Modal
animationType="fade"
transparent={true}
visible={state.showDelete}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideDelete}
>
<KeyboardAvoidingView behavior="height" style={styles.modalWrapper}>
<View style={styles.modalContainer}>
<Text style={styles.modalHeader}>Deleting Your Account</Text>
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.confirmDelete} onChangeText={actions.setConfirmDelete}
autoCapitalize="none" placeholder="Type 'delete' to Confirm" placeholderTextColor={Colors.grey} />
</View>
<View style={styles.modalControls}>
<TouchableOpacity style={styles.cancel} onPress={actions.hideDelete}>
<Text>Cancel</Text>
</TouchableOpacity>
{ state.confirmDelete === 'delete' && (
<TouchableOpacity style={styles.remove} onPress={remove}>
<Text style={styles.removeText}>Delete</Text>
</TouchableOpacity>
)}
{ state.confirmDelete !== 'delete' && (
<TouchableOpacity style={styles.unconfirmed}>
<Text style={styles.removeText}>Delete</Text>
</TouchableOpacity>
)}
</View>
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.blockedCards}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideBlockedCards}
>
<KeyboardAvoidingView behavior="height" style={styles.modalWrapper}>
<View style={styles.modalContainer}>
<Text style={styles.modalHeader}>Blocked Contacts:</Text>
<View style={styles.modalList}>
<BlockedContacts />
</View>
<View style={styles.modalControls}>
<TouchableOpacity style={styles.close} onPress={actions.hideBlockedCards}>
<Text>Close</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.blockedChannels}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideBlockedChannels}
>
<KeyboardAvoidingView behavior="height" style={styles.modalWrapper}>
<View style={styles.modalContainer}>
<Text style={styles.modalHeader}>Blocked Topics:</Text>
<View style={styles.modalList}>
<BlockedTopics />
</View>
<View style={styles.modalControls}>
<TouchableOpacity style={styles.close} onPress={actions.hideBlockedChannels}>
<Text>Close</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.blockedMessages}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideBlockedMessages}
>
<KeyboardAvoidingView behavior="height" style={styles.modalWrapper}>
<View style={styles.modalContainer}>
<Text style={styles.modalHeader}>Blocked Messages:</Text>
<View style={styles.modalList}>
<BlockedMessages />
</View>
<View style={styles.modalControls}>
<TouchableOpacity style={styles.close} onPress={actions.hideBlockedMessages}>
<Text>Close</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.editDetails}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideEditDetails}
>
<KeyboardAvoidingView behavior="height" style={styles.modalWrapper}>
<View style={styles.modalContainer}>
<Text style={styles.modalHeader}>Edit Details:</Text>
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editName} onChangeText={actions.setEditName}
autoCapitalize="words" placeholder="Name" placeholderTextColor={Colors.grey} />
</View>
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editLocation} onChangeText={actions.setEditLocation}
autoCapitalize="words" placeholder="Location" placeholderTextColor={Colors.grey} />
</View>
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editDescription} onChangeText={actions.setEditDescription}
autoCapitalize="sentences" placeholder="Description" multiline={true}
placeholderTextColor={Colors.grey} />
</View>
<View style={styles.modalControls}>
<TouchableOpacity style={styles.cancel} onPress={actions.hideEditDetails}>
<Text>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.save} onPress={saveDetails}>
<Text style={styles.saveText}>Save</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.editSeal}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideSealEdit}
>
<KeyboardAvoidingView behavior="height" style={styles.modalWrapper}>
<View style={styles.modalContainer}>
<Text style={styles.modalHeader}>Sealed Topics:</Text>
<View style={styles.sealable}>
<TouchableOpacity onPress={() => actions.setSealable(!state.sealable)} activeOpacity={1}>
<Text style={styles.sealableText}>Enable Sealed Topics</Text>
</TouchableOpacity>
<Switch style={styles.sealableSwitch} value={state.sealable} onValueChange={actions.setSealable} trackColor={styles.switch}/>
</View>
{ state.sealMode === 'unlocking' && (
<>
{ !state.showSealUnlock && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealUnlock} onChangeText={actions.setSealUnlock}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Password for Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showSealUnlock}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showSealUnlock && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealUnlock} onChangeText={actions.setSealUnlock}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Password for Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hideSealUnlock}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
</>
)}
{ (state.sealMode === 'updating' || state.sealMode === 'enabling') && (
<>
{ !state.showSealPassword && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealPassword} onChangeText={actions.setSealPassword}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Password for Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showSealPassword}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showSealPassword && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealPassword} onChangeText={actions.setSealPassword}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Password for Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hideSealPassword}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ !state.showSealConfirm && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealConfirm} onChangeText={actions.setSealConfirm}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Confirm Password"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showSealConfirm}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showSealConfirm && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealConfirm} onChangeText={actions.setSealConfirm}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Confirm Password"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hideSealConfirm}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
<Text style={styles.notice}>saving can take a minute</Text>
</>
)}
{ state.sealMode === 'disabling' && (
<View style={styles.inputField}>
<Ionicons style={styles.warn} name="exclamationcircleo" size={18} color="#888888" />
<TextInput style={styles.input} value={state.sealDelete} onChangeText={actions.setSealDelete}
autoCapitalize={'none'} placeholder="Type 'delete' to remove sealing key"
placeholderTextColor={Colors.grey} />
</View>
)}
{ state.sealMode === 'unlocked' && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={'xxxxxxxx'} editable={false} secureTextEntry={true} />
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
<TouchableOpacity style={styles.sealUpdate} onPress={actions.updateSeal} />
</View>
)}
<View style={styles.modalControls}>
<TouchableOpacity style={styles.cancel} onPress={actions.hideSealEdit}>
<Text>Cancel</Text>
</TouchableOpacity>
{ state.canSaveSeal && (
<>
{ state.sealMode !== 'unlocking' && state.sealMode !== 'unlocked' && (
<TouchableOpacity style={styles.save} onPress={saveSeal}>
<Text style={styles.saveText}>Save</Text>
</TouchableOpacity>
)}
{ state.sealMode === 'unlocked' && (
<TouchableOpacity style={styles.save} onPress={saveSeal}>
<Text style={styles.saveText}>Forget</Text>
</TouchableOpacity>
)}
{ state.sealMode === 'unlocking' && (
<TouchableOpacity style={styles.save} onPress={saveSeal}>
<Text style={styles.saveText}>Unlock</Text>
</TouchableOpacity>
)}
</>
)}
{ !state.canSaveSeal && (
<>
{ state.sealMode !== 'unlocking' && (
<View style={styles.disabled}>
<Text style={styles.disabledText}>Save</Text>
</View>
)}
{ state.sealMode === 'unlocking' && (
<View style={styles.disabled}>
<Text style={styles.disabledText}>Unlock</Text>
</View>
)}
</>
)}
</View>
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.editLogin}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideLoginEdit}
>
<KeyboardAvoidingView behavior="height" style={styles.modalWrapper}>
<View style={styles.modalContainer}>
<Text style={styles.modalHeader}>Change Login:</Text>
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editHandle} onChangeText={actions.setEditHandle}
autoCapitalize={'none'} placeholder="Username" placeholderTextColor={Colors.grey} />
{ state.checked && state.available && (
<Ionicons style={styles.icon} name="checkcircleo" size={18} color={Colors.background} />
)}
{ state.checked && !state.available && (
<Ionicons style={styles.icon} name="exclamationcircleo" size={18} color={Colors.alert} />
)}
</View>
{ !state.showPassword && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editPassword} onChangeText={actions.setEditPassword}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Password"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showPassword}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showPassword && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editPassword} onChangeText={actions.setEditPassword}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Password"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hidePassword}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ !state.showConfirm && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editConfirm} onChangeText={actions.setEditConfirm}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Confirm"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showConfirm}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showConfirm && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.editConfirm} onChangeText={actions.setEditConfirm}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Confirm"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hideConfirm}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
<View style={styles.modalControls}>
<TouchableOpacity style={styles.cancel} onPress={actions.hideLoginEdit}>
<Text>Cancel</Text>
</TouchableOpacity>
{ (state.checked && state.available && state.editConfirm === state.editPassword && state.editPassword) && (
<TouchableOpacity style={styles.save} onPress={saveLogin}>
<Text style={styles.saveText}>Save</Text>
</TouchableOpacity>
)}
{ !(state.checked && state.available && state.editConfirm === state.editPassword && state.editPassword) && (
<View style={styles.disabled}>
<Text style={styles.disabledText}>Save</Text>
</View>
)}
</View>
</View>
</KeyboardAvoidingView>
</Modal>
</View>
);
}
export function Profile() {
return (
<View>
<ProfileHeader />
<ProfileBody />
</View>
);
}

View File

@ -0,0 +1,241 @@
import { StyleSheet } from 'react-native';
import { Colors } from 'constants/Colors';
export const styles = StyleSheet.create({
body: {
display: 'flex',
flexGrow: 1,
},
button: {
paddingRight: 16,
},
headerText: {
fontSize: 18,
overflow: 'hidden',
textAlign: 'center',
},
logo: {
marginTop: 16,
alignItems: 'center',
justifyContent: 'center',
width: '100%',
display: 'flex',
},
alert: {
height: 16,
width: '100%',
alignItems: 'center',
},
alertText: {
color: Colors.alert,
},
logout: {
marginTop: 16,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
logoutText: {
marginLeft: 8,
color: Colors.primary,
},
delete: {
marginTop: 16,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
deleteText: {
marginLeft: 8,
color: Colors.alert,
},
modalWrapper: {
display: 'flex',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(52, 52, 52, 0.8)'
},
modalContainer: {
backgroundColor: Colors.formBackground,
padding: 16,
width: '80%',
maxWidth: 400,
},
modalHeader: {
fontSize: 18,
paddingBottom: 16,
},
modalList: {
width: '100%',
borderWidth: 1,
borderColor: Colors.lightgrey,
borderRadius: 2,
},
modalControls: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
},
cancel: {
borderWidth: 1,
borderColor: Colors.lightgrey,
borderRadius: 4,
padding: 8,
marginRight: 8,
width: 72,
display: 'flex',
alignItems: 'center',
},
unconfirmed: {
backgroundColor: Colors.lightgrey,
borderRadius: 4,
padding: 8,
width: 72,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
remove: {
backgroundColor: Colors.error,
borderRadius: 4,
padding: 8,
width: 72,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
removeText: {
color: Colors.white,
},
inputField: {
width: '100%',
borderWidth: 1,
borderColor: Colors.lightgrey,
borderRadius: 4,
padding: 8,
marginBottom: 8,
maxHeight: 92,
display: 'flex',
flexDirection: 'row',
},
gallery: {
position: 'absolute',
bottom: 0,
right: 0,
padding: 8,
backgroundColor: Colors.lightgrey,
borderBottomRightRadius: 8,
borderTopLeftRadius: 8,
},
detail: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
color: Colors.text,
paddingLeft: 32,
paddingRight: 32,
marginTop: 16,
marginBottom: 16,
},
attribute: {
display: 'flex',
flexDirection: 'row',
paddingBottom: 8,
},
nonametext: {
fontSize: 18,
paddingRight: 8,
fontWeight: 'bold',
color: Colors.grey,
},
nametext: {
fontSize: 18,
paddingRight: 8,
fontWeight: 'bold',
},
locationtext: {
fontSize: 16,
paddingLeft: 8,
color: Colors.text,
},
nolocationtext: {
fontSize: 16,
paddingLeft: 8,
color: Colors.grey,
},
descriptiontext: {
fontSize: 16,
paddingLeft: 8,
color: Colors.text,
},
nodescriptiontext: {
fontSize: 16,
paddingLeft: 8,
color: Colors.grey,
},
save: {
padding: 8,
borderRadius: 4,
backgroundColor: Colors.primary,
width: 72,
display: 'flex',
alignItems: 'center',
},
saveText: {
color: Colors.white,
},
blocked: {
alignSelf: 'center',
borderColor: Colors.lightgrey,
borderWidth: 1,
borderRadius: 4,
padding: 8,
marginBottom: 8,
display: 'flex',
flexDirection: 'row',
},
blockedLabel: {
marginTop: 24,
alignSelf: 'center',
color: Colors.grey,
},
enable: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
enableText: {
color: Colors.primary,
},
enableSwitch: {
transform: [{ scaleX: .6 }, { scaleY: .6 }],
},
link: {
marginLeft: 8,
marginRight: 8,
},
linkText: {
color: Colors.primary,
},
close: {
borderWidth: 1,
borderColor: Colors.lightgrey,
borderRadius: 4,
padding: 8,
marginTop: 8,
width: 72,
display: 'flex',
alignItems: 'center',
},
});

View File

@ -0,0 +1,48 @@
import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native';
import { styles } from './BlockedContacts.styled';
import { useBlockedContacts } from './useBlockedContacts.hook';
import { Logo } from 'utils/Logo';
export function BlockedContacts() {
const { state, actions } = useBlockedContacts();
const unblock = (cardId) => {
Alert.alert(
'Unblocking Contact',
'Confirm?',
[
{ text: "Cancel", onPress: () => {}, },
{ text: "Unblock", onPress: () => actions.unblock(cardId) },
],
);
};
const BlockedItem = ({ item }) => {
return (
<TouchableOpacity style={styles.item} onPress={() => unblock(item.cardId)}>
<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>
</TouchableOpacity>
)
}
return (
<View style={styles.container}>
{ state.cards.length === 0 && (
<Text style={styles.default}>No Blocked Contacts</Text>
)}
{ state.cards.length !== 0 && (
<FlatList
data={state.cards}
renderItem={({item}) => <BlockedItem item={item} />}
keyExtractor={item => item.cardId}
/>
)}
</View>
);
}

View File

@ -0,0 +1,43 @@
import { StyleSheet } from 'react-native';
import { Colors } from 'constants/Colors';
export const styles = StyleSheet.create({
container: {
backgroundColor: Colors.white,
display: 'flex',
width: '100%',
justifyContent: 'center',
fontSize: 14,
height: 200,
},
default: {
textAlign: 'center',
color: Colors.grey,
},
item: {
width: '100%',
display: 'flex',
flexDirection: 'row',
height: 48,
paddingLeft: 16,
alignItems: 'center',
borderBottomWidth: 1,
borderColor: Colors.itemDivider,
},
detail: {
paddingLeft: 12,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flexGrow: 1,
flexShrink: 1,
},
name: {
color: Colors.text,
fontSize: 14,
},
handle: {
color: Colors.text,
fontSize: 12,
},
});

View File

@ -0,0 +1,53 @@
import { useState, useEffect, useContext } from 'react';
import { CardContext } from 'context/CardContext';
export function useBlockedContacts() {
const [state, setState] = useState({
cards: [],
});
const card = useContext(CardContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const setCardItem = (item) => {
const { profile } = item;
return {
cardId: item.cardId,
name: profile?.name,
handle: `${profile?.handle}@${profile?.node}`,
blocked: item.blocked,
logo: profile?.imageSet ? card.actions.getCardImageUrl(item.cardId) : 'avatar',
}
};
useEffect(() => {
const cards = Array.from(card.state.cards.values());
const items = cards.map(setCardItem);
const filtered = items.filter(item => {
return item.blocked;
});
filtered.sort((a, b) => {
if (a.name === b.name) {
return 0;
}
if (!a.name || (a.name < b.name)) {
return -1;
}
return 1;
});
updateState({ cards: filtered });
}, [card]);
const actions = {
unblock: async (cardId) => {
await card.actions.clearCardBlocked(cardId);
}
};
return { state, actions };
}

View File

@ -0,0 +1,47 @@
import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native';
import { styles } from './BlockedMessages.styled';
import { useBlockedMessages } from './useBlockedMessages.hook';
import { Logo } from 'utils/Logo';
export function BlockedMessages() {
const { state, actions } = useBlockedMessages();
const unblock = (cardId, channelId, topicId) => {
Alert.alert(
'Unblocking Message',
'Confirm?',
[
{ text: "Cancel", onPress: () => {}, },
{ text: "Unblock", onPress: () => actions.unblock(cardId, channelId, topicId) },
],
);
};
const BlockedItem = ({ item }) => {
return (
<TouchableOpacity style={styles.item} onPress={() => unblock(item.cardId, item.channelId, item.topicId)}>
<View style={styles.detail}>
<Text style={styles.name} numberOfLines={1} ellipsizeMode={'tail'}>{ item.name }</Text>
<Text style={styles.created} numberOfLines={1} ellipsizeMode={'tail'}>{ item.timestamp }</Text>
</View>
</TouchableOpacity>
)
}
return (
<View style={styles.container}>
{ state.messages.length === 0 && (
<Text style={styles.default}>No Blocked Messages</Text>
)}
{ state.messages.length !== 0 && (
<FlatList
data={state.messages}
renderItem={({item}) => <BlockedItem item={item} />}
keyExtractor={item => item.id}
/>
)}
</View>
);
}

View File

@ -0,0 +1,46 @@
import { StyleSheet } from 'react-native';
import { Colors } from 'constants/Colors';
export const styles = StyleSheet.create({
container: {
backgroundColor: Colors.white,
display: 'flex',
width: '100%',
justifyContent: 'center',
fontSize: 14,
height: 200,
},
default: {
textAlign: 'center',
color: Colors.grey,
},
item: {
width: '100%',
display: 'flex',
flexDirection: 'row',
height: 32,
paddingLeft: 16,
alignItems: 'center',
borderBottomWidth: 1,
borderColor: Colors.itemDivider,
},
detail: {
paddingLeft: 12,
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
width: '100%',
},
name: {
color: Colors.text,
fontSize: 14,
flexGrow: 1,
flexShrink: 1,
minWidth: 0,
},
created: {
color: Colors.text,
fontSize: 12,
paddingRight: 16,
},
});

View File

@ -0,0 +1,96 @@
import { useState, useEffect, useContext } from 'react';
import { StoreContext } from 'context/StoreContext';
import { ChannelContext } from 'context/ChannelContext';
import { CardContext } from 'context/CardContext';
import { ProfileContext } from 'context/ProfileContext';
import { ConversationContext } from 'context/ConversationContext';
import moment from 'moment';
export function useBlockedMessages() {
const [state, setState] = useState({
messages: []
});
const store = useContext(StoreContext);
const card = useContext(CardContext);
const channel = useContext(ChannelContext);
const profile = useContext(ProfileContext);
const conversation = useContext(ConversationContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const setItem = (item) => {
let name, nameSet
if (item.detail.guid === profile.state.identity.guid) {
const identity = profile.state.identity;
if (identity.name) {
name = identity.name;
}
else {
name = `${identity.handle}@${identity.node}`;
}
nameSet = true;
}
else {
const contact = card.actions.getByGuid(item.detail.guid);
if (contact) {
if (contact?.profile?.name) {
name = contact.profile.name;
}
else {
name = `${contact.profile.handle}@${contact.profile.node}`;
}
nameSet = true;
}
else {
name = 'unknown';
nameSet = false;
}
}
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');
}
const { cardId, channelId, topicId } = item;
return { name, nameSet, timestamp, cardId, channelId, topicId, id: `${cardId}:${channelId}:${topicId}` };
};
const loadBlocked = async () => {
//TODO
}
useEffect(() => {
loadBlocked();
}, []);
const actions = {
unblock: async (cardId, channelId, topicId) => {
const id = `${cardId}:${channelId}:${topicId}`;
if (cardId) {
card.actions.clearChannelTopicBlocked(cardId, channelId, topicId);
}
else {
channel.actions.clearTopicBlocked(channelId, topicId);
}
conversation.actions.unblockTopic(cardId, channelId, topicId);
updateState({ messages: state.messages.filter(item => item.id !== id) });
}
};
return { state, actions };
}

View File

@ -0,0 +1,47 @@
import { FlatList, View, Alert, TouchableOpacity, Text } from 'react-native';
import { styles } from './BlockedTopics.styled';
import { useBlockedTopics } from './useBlockedTopics.hook';
import { Logo } from 'utils/Logo';
export function BlockedTopics() {
const { state, actions } = useBlockedTopics();
const unblock = (cardId, channelId) => {
Alert.alert(
'Unblocking Contact',
'Confirm?',
[
{ text: "Cancel", onPress: () => {}, },
{ text: "Unblock", onPress: () => actions.unblock(cardId, channelId) },
],
);
};
const BlockedItem = ({ item }) => {
return (
<TouchableOpacity style={styles.item} onPress={() => unblock(item.cardId, item.channelId)}>
<View style={styles.detail}>
<Text style={styles.name} numberOfLines={1} ellipsizeMode={'tail'}>{ item.name }</Text>
<Text style={styles.created} numberOfLines={1} ellipsizeMode={'tail'}>{ item.created }</Text>
</View>
</TouchableOpacity>
)
}
return (
<View style={styles.container}>
{ state.channels.length === 0 && (
<Text style={styles.default}>No Blocked Topics</Text>
)}
{ state.channels.length !== 0 && (
<FlatList
data={state.channels}
renderItem={({item}) => <BlockedItem item={item} />}
keyExtractor={item => item.id}
/>
)}
</View>
);
}

View File

@ -0,0 +1,46 @@
import { StyleSheet } from 'react-native';
import { Colors } from 'constants/Colors';
export const styles = StyleSheet.create({
container: {
backgroundColor: Colors.white,
display: 'flex',
width: '100%',
justifyContent: 'center',
fontSize: 14,
height: 200,
},
default: {
textAlign: 'center',
color: Colors.grey,
},
item: {
width: '100%',
display: 'flex',
flexDirection: 'row',
height: 32,
paddingLeft: 16,
alignItems: 'center',
borderBottomWidth: 1,
borderColor: Colors.itemDivider,
},
detail: {
paddingLeft: 12,
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
width: '100%',
},
name: {
color: Colors.text,
fontSize: 14,
flexGrow: 1,
flexShrink: 1,
minWidth: 0,
},
created: {
color: Colors.text,
fontSize: 12,
paddingRight: 16,
},
});

View File

@ -0,0 +1,122 @@
import { useState, useEffect, useContext } from 'react';
import { CardContext } from 'context/CardContext';
import { ChannelContext } from 'context/ChannelContext';
import { ProfileContext } from 'context/ProfileContext';
import moment from 'moment';
export function useBlockedTopics() {
const [state, setState] = useState({
channels: []
});
const profile = useContext(ProfileContext);
const card = useContext(CardContext);
const channel = useContext(ChannelContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const getCard = (guid) => {
let contact = null
card.state.cards.forEach((card, cardId, map) => {
if (card?.profile?.guid === guid) {
contact = card;
}
});
return contact;
}
const setChannelItem = (item) => {
let timestamp;
const date = new Date(item.detail.created * 1000);
const now = new Date();
const offset = now.getTime() - date.getTime();
if(offset < 86400000) {
timestamp = moment(date).format('h:mma');
}
else if (offset < 31449600000) {
timestamp = moment(date).format('M/DD');
}
else {
timestamp = moment(date).format('M/DD/YYYY');
}
let contacts = [];
if (item.cardId) {
contacts.push(card.state.cards.get(item.cardId));
}
if (item?.detail?.members) {
const profileGuid = profile.state.identity.guid;
item.detail.members.forEach(guid => {
if (profileGuid !== guid) {
const contact = getCard(guid);
contacts.push(contact);
}
})
}
let subject;
if (item?.detail?.data) {
try {
topic = JSON.parse(item?.detail?.data).subject;
subject = topic;
}
catch (err) {
console.log(err);
}
}
if (!subject) {
if (contacts.length) {
let names = [];
for (let contact of contacts) {
if (contact?.profile?.name) {
names.push(contact.profile.name);
}
else if (contact?.profile?.handle) {
names.push(contact?.profile?.handle);
}
}
subject = names.join(', ');
}
else {
subject = "Notes";
}
}
return {
id: `${item.cardId}:${item.channelId}`,
cardId: item.cardId,
channelId: item.channelId,
name: subject,
blocked: item.blocked,
created: timestamp,
}
};
useEffect(() => {
let merged = [];
card.state.cards.forEach((card, cardId, map) => {
merged.push(...Array.from(card.channels.values()));
});
merged.push(...Array.from(channel.state.channels.values()));
const items = merged.map(setChannelItem);
const filtered = items.filter(item => item.blocked);
updateState({ channels: filtered });
}, [card, channel]);
const actions = {
unblock: async (cardId, channelId) => {
if (cardId) {
await card.actions.clearChannelBlocked(cardId, channelId);
}
else {
await channel.actions.clearBlocked(channelId);
}
}
};
return { state, actions };
}

View File

@ -0,0 +1,118 @@
import { useState, useEffect, useRef, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { ProfileContext } from 'context/ProfileContext';
import { AppContext } from 'context/AppContext';
export function useProfile() {
const [state, setState] = useState({
name: null,
handle: null,
editHandle: null,
location: null,
editLocation: null,
description: null,
editDescritpion: null,
node: null,
showDelete: false,
editDetails: false,
editLogin: false,
editSeal: false,
confirmDelete: null,
blockedChannels: false,
blockedCards: false,
blockedMessages: false,
logginOut: false,
disconnected: false,
});
const app = useContext(AppContext);
const profile = useContext(ProfileContext);
const navigate = useNavigate();
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
const { name, handle, node, location, description, image } = profile.state.identity;
const imageSource = image ? profile.state.imageUrl : 'avatar';
updateState({ name, handle, node, location, description, imageSource, editHandle: handle,
editName: name, editLocation: location, editDescription: description });
}, [profile]);
useEffect(() => {
const { loggingOut, status } = app.state;
updateState({ loggingOut, disconnected: status === 'disconnected' });
}, [app.state]);
const actions = {
logout: async () => {
await app.actions.logout();
navigate('/');
},
remove: async () => {
await app.actions.remove();
updateState({ showDelete: false });
navigate('/');
},
showDelete: () => {
updateState({ showDelete: true, confirmDelete: null });
},
hideDelete: () => {
updateState({ showDelete: false });
},
setConfirmDelete: (confirmDelete) => {
updateState({ confirmDelete });
},
showEditDetails: () => {
updateState({ editDetails: true });
},
hideEditDetails: () => {
updateState({ editDetails: false });
},
showEditLogin: () => {
updateState({ editLogin: true });
},
hideEditLogin: () => {
updateState({ editLogin: false });
},
showEditSeal: () => {
updateState({ editSeal: true });
},
hideEditSeal: () => {
updateState({ editSeal: false });
},
showBlockedChannels: () => {
updateState({ blockedChannels: true });
},
hideBlockedChannels: () => {
updateState({ blockedChannels: false });
},
showBlockedCards: () => {
updateState({ blockedCards: true });
},
hideBlockedCards: () => {
updateState({ blockedCards: false });
},
showBlockedMessages: () => {
updateState({ blockedMessages: true });
},
hideBlockedMessages: () => {
updateState({ blockedMessages: false });
},
setEditName: (editName) => {
updateState({ editName });
},
setEditLocation: (editLocation) => {
updateState({ editLocation });
},
setEditDescription: (editDescription) => {
updateState({ editDescription });
},
};
return { state, actions };
}

View File

@ -5597,6 +5597,11 @@ mkdirp@^0.5.1:
dependencies:
minimist "^1.2.6"
moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@ -6228,6 +6233,11 @@ react-native-gradle-plugin@^0.71.15:
resolved "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.15.tgz"
integrity sha512-7S3pAuPaQJlhax6EZ4JMsDNpj05TfuzX9gPgWLrFfAIWIFLuJ6aDQYAZy2TEI9QJALPoWrj8LWaqP/DGYh14pw==
react-native-image-crop-picker@^0.39.0:
version "0.39.0"
resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.39.0.tgz#9cb8e8ffb0e8ab06f7b3227cadf077169e225eba"
integrity sha512-4aANbQMrmU6zN/4b0rVBA7SbaZ3aa5JESm3Xk751sINybZMt1yz/9h95LkO7U0pbslHDo3ofXjG75PmQRP6a/w==
react-native-reanimated@^2.14.4:
version "2.14.4"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836"