Files
com.blitzwallet/app/components/admin/homeComponents/contacts/editMyProfilePage.js
T

994 lines
30 KiB
JavaScript

import { StyleSheet, View, TouchableOpacity, ScrollView } from 'react-native';
import {
CENTER,
COLORS,
CONTENT_KEYBOARD_OFFSET,
EMAIL_REGEX,
SIZES,
VALID_NAME_BIO_REGEX,
} from '../../../../constants';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { useEffect, useState, useRef, useCallback } from 'react';
import { encriptMessage } from '../../../../functions/messaging/encodingAndDecodingMessages';
import {
CustomKeyboardAvoidingView,
ThemeText,
} from '../../../../functions/CustomElements';
import { isValidUniqueName } from '../../../../../db';
import CustomButton from '../../../../functions/CustomElements/button';
import { useGlobalContactsInfo } from '../../../../../context-store/globalContacts';
import GetThemeColors from '../../../../hooks/themeColors';
import { useGlobalThemeContext } from '../../../../../context-store/theme';
import { useKeysContext } from '../../../../../context-store/keys';
import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar';
import {
INSET_WINDOW_WIDTH,
MAX_CONTENT_WIDTH,
} from '../../../../constants/theme';
import { keyboardGoBack } from '../../../../functions/customNavigation';
import { useTranslation } from 'react-i18next';
import ContactProfileImage from './internalComponents/profileImage';
import FullLoadingScreen from '../../../../functions/CustomElements/loadingScreen';
import { useImageCache } from '../../../../../context-store/imageCache';
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
import { useProfileImage } from './hooks/useProfileImage';
import EditProfileTextInput from './internalComponents/editProfileTextItems';
import { areImagesSame } from './utils/imageComparison';
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
import { useGlobalContextProvider } from '../../../../../context-store/context';
import { KeyboardController } from 'react-native-keyboard-controller';
export default function EditMyProfilePage(props) {
const navigate = useNavigation();
const {
decodedAddedContacts,
toggleGlobalContactsInformation,
globalContactsInformation,
deleteContact,
} = useGlobalContactsInfo();
const { t } = useTranslation();
const pageType = props?.pageType || props.route?.params?.pageType;
const fromSettings = props.fromSettings || props.route?.params?.fromSettings;
const hideProfileImage = props?.hideProfileImage;
const isEditingMyProfile = pageType.toLowerCase() === 'myprofile';
const providedContact =
!isEditingMyProfile &&
(props?.selectedAddedContact || props.route?.params?.selectedAddedContact);
const myContact = globalContactsInformation.myProfile;
const isFirstTimeEditing = myContact.didEditProfile;
useFocusEffect(
useCallback(() => {
return () => {
if (isFirstTimeEditing || !isEditingMyProfile) return;
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
didEditProfile: true,
},
addedContacts: globalContactsInformation.addedContacts,
},
true,
);
};
}, [
isFirstTimeEditing,
globalContactsInformation,
toggleGlobalContactsInformation,
isEditingMyProfile,
]),
);
const selectedAddedContact = props.fromInitialAdd
? providedContact
: decodedAddedContacts.find(
contact => contact.uuid === providedContact?.uuid,
);
const deleteUser = shouldDelete => {
if (shouldDelete) {
deleteContact(selectedAddedContact);
navigate.popTo('HomeAdmin');
}
};
if (hideProfileImage) {
return (
<View style={{ flex: 1, width: INSET_WINDOW_WIDTH, ...CENTER }}>
<InnerContent
isEditingMyProfile={isEditingMyProfile}
selectedAddedContact={selectedAddedContact}
fromInitialAdd={props.fromInitialAdd}
fromSettings={fromSettings}
hideProfileImage={true}
/>
</View>
);
}
return (
<CustomKeyboardAvoidingView
useTouchableWithoutFeedback={true}
useStandardWidth={true}
>
<CustomSettingsTopBar
shouldDismissKeyboard={true}
label={fromSettings ? t('contacts.editMyProfilePage.navTitle') : ''}
customBackFunction={() => keyboardGoBack(navigate)}
iconNew="Trash2"
leftImageFunction={() =>
navigate.navigate('ConfirmActionPage', {
confirmMessage: t('contacts.editMyProfilePage.deleteWarning'),
confirmFunction: () => deleteUser(true),
cancelFunction: () => deleteUser(false),
})
}
showLeftImage={!isEditingMyProfile}
/>
<View
style={{
flex: 1,
width: fromSettings ? '100%' : INSET_WINDOW_WIDTH,
...CENTER,
}}
>
<InnerContent
isEditingMyProfile={isEditingMyProfile}
selectedAddedContact={selectedAddedContact}
fromInitialAdd={props.fromInitialAdd}
fromSettings={fromSettings}
hideProfileImage={false}
/>
</View>
</CustomKeyboardAvoidingView>
);
}
// ─── Contact-mode input fields ───────────────────────────────────
function ProfileInputFields({
inputs,
changeInputText,
setIsKeyboardActive,
nameRef,
bioRef,
receiveAddressRef,
isEditingMyProfile,
selectedAddedContact,
theme,
darkModeType,
textInputColor,
textInputBackground,
textColor,
navigate,
t,
}) {
const hasLNURL = !isEditingMyProfile && selectedAddedContact?.isLNURL;
const bioIsLast = !isEditingMyProfile;
return (
<>
<EditProfileTextInput
label={t('contacts.editMyProfilePage.nameInputDesc')}
placeholder={t('contacts.editMyProfilePage.nameInputPlaceholder')}
value={inputs.name}
onChangeText={text => changeInputText(text, 'name')}
onFocus={() => setIsKeyboardActive(true)}
onBlur={() => setIsKeyboardActive(false)}
inputRef={nameRef}
maxLength={30}
theme={theme}
darkModeType={darkModeType}
textInputColor={textInputColor}
textInputBackground={textInputBackground}
textColor={textColor}
showDivider={true}
/>
{hasLNURL && (
<EditProfileTextInput
label={t('contacts.editMyProfilePage.lnurlInputDesc')}
placeholder={t('contacts.editMyProfilePage.lnurlInputPlaceholder')}
value={inputs.receiveAddress}
onChangeText={text => changeInputText(text, 'receiveAddress')}
onFocus={() => setIsKeyboardActive(true)}
onBlur={() => setIsKeyboardActive(false)}
inputRef={receiveAddressRef}
maxLength={200}
multiline={false}
minHeight={60}
theme={theme}
darkModeType={darkModeType}
textInputColor={textInputColor}
textInputBackground={textInputBackground}
textColor={textColor}
showDivider={true}
/>
)}
<EditProfileTextInput
label={t('contacts.editMyProfilePage.bioInputDesc')}
placeholder={t('contacts.editMyProfilePage.bioInputPlaceholder')}
value={inputs.bio}
onChangeText={text => changeInputText(text, 'bio')}
onFocus={() => setIsKeyboardActive(true)}
onBlur={() => setIsKeyboardActive(false)}
inputRef={bioRef}
maxLength={150}
multiline={true}
minHeight={60}
maxHeight={100}
theme={theme}
darkModeType={darkModeType}
textInputColor={textInputColor}
textInputBackground={textInputBackground}
textColor={textColor}
showDivider={!bioIsLast}
/>
</>
);
}
// ─── My profile nav rows ──────────────────────────────────────────────────────
function MyProfileRows({
myContact,
backgroundOffset,
textColor,
navigate,
masterInfoObject,
t,
}) {
const receiveCurrencyValue =
masterInfoObject.lnurlReceiveCurrency === 'usd'
? t('constants.dollars_upper')
: t('constants.bitcoin_upper');
return (
<View
style={[
styles.card,
{ backgroundColor: backgroundOffset, marginTop: 12 },
]}
>
{/* Name */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('EditProfileFieldPage', { fieldKey: 'name' })
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.editMyProfilePage.nameInputDesc')}
/>
<ThemeText
styles={styles.navRowLabel}
content={myContact?.name || t('contacts.splitBill.noName')}
CustomNumberOfLines={1}
CustomEllipsizeMode="tail"
/>
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
<View style={[styles.divider, { backgroundColor: textColor }]} />
{/* Username */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('EditProfileFieldPage', { fieldKey: 'uniquename' })
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.editMyProfilePage.uniqueNameInputDesc')}
/>
<ThemeText
styles={styles.navRowLabel}
content={myContact?.uniqueName || ''}
CustomNumberOfLines={1}
CustomEllipsizeMode="tail"
/>
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
<View style={[styles.divider, { backgroundColor: textColor }]} />
{/* Bio */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('EditProfileFieldPage', { fieldKey: 'bio' })
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.editMyProfilePage.bioInputDesc')}
/>
<ThemeText
styles={styles.navRowLabel}
content={myContact?.bio || t('constants.noBioSet')}
CustomNumberOfLines={1}
CustomEllipsizeMode="tail"
/>
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
<View style={[styles.divider, { backgroundColor: textColor }]} />
{/* Lightning Address */}
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigate.navigate('CustomHalfModal', {
wantedContent: 'lnurlReceiveCurrencySelect',
})
}
style={styles.navRow}
>
<ThemeText
CustomNumberOfLines={1}
styles={styles.navRowValue}
content={t('contacts.remotePaymentCurrencySelect.title')}
/>
<ThemeText styles={styles.navRowLabel} content={receiveCurrencyValue} />
<ThemeIcon iconName="ChevronRight" size={16} />
</TouchableOpacity>
</View>
);
}
// ─── InnerContent ─────────────────────────────────────────────────────────────
function InnerContent({
isEditingMyProfile,
selectedAddedContact,
fromInitialAdd,
fromSettings,
hideProfileImage = false,
}) {
const { contactsPrivateKey, publicKey } = useKeysContext();
const { theme, darkModeType } = useGlobalThemeContext();
const { cache, refreshCacheObject } = useImageCache();
const { backgroundOffset, textInputColor, textInputBackground, textColor } =
GetThemeColors();
const { masterInfoObject } = useGlobalContextProvider();
const {
decodedAddedContacts,
globalContactsInformation,
toggleGlobalContactsInformation,
} = useGlobalContactsInfo();
const { t } = useTranslation();
const {
isAddingImage,
deleteProfilePicture,
getProfileImage,
saveProfileImage,
} = useProfileImage();
// Contact-mode refs (unused in myProfile path but kept for contact path)
const nameRef = useRef(null);
const bioRef = useRef(null);
const receiveAddressRef = useRef(null);
const didCallImagePicker = useRef(null);
const myContact = globalContactsInformation.myProfile;
const selectedAddedContactName = selectedAddedContact?.name || '';
const selectedAddedContactBio = selectedAddedContact?.bio || '';
const selectedAddedContactUniqueName = selectedAddedContact?.uniqueName || '';
const selectedAddedContactReceiveAddress =
selectedAddedContact?.receiveAddress || '';
const [isSaving, setIsSaving] = useState(false);
const [inputs, setInputs] = useState(() => ({
name: selectedAddedContactName || '',
bio: selectedAddedContactBio || '',
uniquename: selectedAddedContactUniqueName || '',
receiveAddress: selectedAddedContactReceiveAddress || '',
}));
const [tempImage, setTempImage] = useState({
uri: null,
comparison: null,
updated: 0,
shouldDelete: false,
});
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
const { bottomPadding } = useGlobalInsets();
const navigate = useNavigation();
function changeInputText(text, type) {
setInputs(prev => {
return { ...prev, [type]: text };
});
}
const myProfileImage = cache[myContact?.uuid];
const selectedAddedContactImage = cache[selectedAddedContact?.uuid];
const hasImage = tempImage.shouldDelete
? false
: tempImage.uri
? true
: isEditingMyProfile
? !!myProfileImage?.localUri
: !!selectedAddedContactImage?.localUri;
// For contact path: full change detection. For myProfile: image only.
const hasChangedInfo = isEditingMyProfile
? tempImage.uri || tempImage.shouldDelete
: selectedAddedContactName !== inputs.name ||
selectedAddedContactBio !== inputs.bio ||
selectedAddedContactUniqueName !== inputs.uniquename ||
selectedAddedContactReceiveAddress !== inputs.receiveAddress ||
fromInitialAdd ||
tempImage.uri ||
tempImage.shouldDelete;
const handleDeleteProfilePicture = () => {
setTempImage({
uri: null,
comparison: null,
updated: 0,
shouldDelete: true,
});
};
const addProfilePicture = async () => {
if (didCallImagePicker.current) return;
didCallImagePicker.current = true;
const response = await getProfileImage();
if (response?.imgURL && response?.comparison) {
setTempImage({
comparison: response?.comparison,
uri: response?.imgURL,
updated: Date.now(),
});
}
didCallImagePicker.current = false;
};
useEffect(() => {
if (!fromInitialAdd) return;
if (hasImage) return;
// Making sure to update UI for new contacts image
refreshCacheObject();
}, []);
// Shared props for ProfileInputFields
const inputFieldsProps = {
inputs,
changeInputText,
setIsKeyboardActive,
nameRef,
bioRef,
receiveAddressRef,
isEditingMyProfile,
selectedAddedContact,
theme,
darkModeType,
textInputColor,
textInputBackground,
textColor,
navigate,
t,
};
if (hideProfileImage) {
return (
<>
<View style={styles.hideProfileContainer}>
<View style={[styles.card, { backgroundColor: backgroundOffset }]}>
<ProfileInputFields {...inputFieldsProps} />
</View>
</View>
<CustomButton
buttonStyles={{
width: 'auto',
...CENTER,
marginTop: 10,
marginBottom: isKeyboardActive
? CONTENT_KEYBOARD_OFFSET
: bottomPadding,
}}
useLoading={isSaving}
actionFunction={saveChanges}
textContent={
hasChangedInfo
? fromInitialAdd
? t('contacts.editMyProfilePage.addContactBTN')
: t('constants.save')
: t('constants.back')
}
/>
</>
);
}
// ── myProfile path ──────────────────────────────────────────────────────────
if (isEditingMyProfile) {
return (
<View
style={[
styles.innerContainer,
fromSettings && { maxWidth: MAX_CONTENT_WIDTH, width: '100%' },
]}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
flexGrow: 1,
alignItems: 'center',
width: fromSettings ? INSET_WINDOW_WIDTH : '100%',
...CENTER,
}}
keyboardShouldPersistTaps="handled"
>
{/* Profile image */}
<TouchableOpacity
activeOpacity={!isAddingImage ? 0.2 : 1}
onPress={async () => {
if (isAddingImage) return;
if (KeyboardController.isVisible()) {
await KeyboardController.dismiss();
await new Promise(resolve => setTimeout(resolve, 250));
}
if (!hasImage) {
addProfilePicture();
return;
}
navigate.navigate('AddOrDeleteContactImage', {
addPhoto: () => addProfilePicture(),
deletePhoto: handleDeleteProfilePicture,
hasImage: hasImage,
});
}}
>
<View
style={[
styles.profileImage,
{ backgroundColor: backgroundOffset },
]}
>
{isAddingImage ? (
<FullLoadingScreen showText={false} />
) : (
<ContactProfileImage
updated={
tempImage.shouldDelete
? null
: tempImage.uri
? tempImage.comparison?.updated
: myProfileImage?.updated
}
uri={
tempImage.shouldDelete
? null
: tempImage.uri
? tempImage.comparison?.uri
: myProfileImage?.localUri
}
darkModeType={darkModeType}
theme={theme}
/>
)}
</View>
<View style={styles.selectFromPhotos}>
<ThemeIcon
colorOverride={COLORS.lightModeText}
size={20}
iconName={hasImage ? 'X' : 'Image'}
/>
</View>
</TouchableOpacity>
<ThemeText
styles={styles.sectionHeader}
content={t('contacts.editMyProfilePage.aboutYou')}
/>
<MyProfileRows
myContact={myContact}
backgroundOffset={backgroundOffset}
textColor={textColor}
navigate={navigate}
masterInfoObject={masterInfoObject}
t={t}
/>
<CustomButton
buttonStyles={{
width: '100%',
...CENTER,
marginTop: 'auto',
marginBottom: bottomPadding,
}}
actionFunction={saveChanges}
useLoading={isSaving}
textContent={
hasChangedInfo ? t('constants.save') : t('constants.back')
}
/>
</ScrollView>
</View>
);
}
// ── Contact path (unchanged) ────────────────────────────────────────────────
return (
<View
style={[
styles.innerContainer,
fromSettings && { maxWidth: MAX_CONTENT_WIDTH, width: '100%' },
]}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
flexGrow: 1,
alignItems: 'center',
width: fromSettings ? INSET_WINDOW_WIDTH : '100%',
...CENTER,
}}
keyboardShouldPersistTaps="handled"
>
{/* Profile image */}
<TouchableOpacity
activeOpacity={
(isEditingMyProfile || selectedAddedContact.isLNURL) &&
!isAddingImage
? 0.2
: 1
}
onPress={async () => {
if (!isEditingMyProfile && !selectedAddedContact.isLNURL) return;
if (isAddingImage) return;
if (KeyboardController.isVisible()) {
await KeyboardController.dismiss();
await new Promise(resolve => setTimeout(resolve, 250));
}
if (!hasImage) {
addProfilePicture(isEditingMyProfile, selectedAddedContact);
return;
}
navigate.navigate('AddOrDeleteContactImage', {
addPhoto: () =>
addProfilePicture(isEditingMyProfile, selectedAddedContact),
deletePhoto: handleDeleteProfilePicture,
hasImage: hasImage,
});
}}
>
<View
style={[
styles.profileImage,
{
backgroundColor: backgroundOffset,
},
]}
>
{isAddingImage ? (
<FullLoadingScreen showText={false} />
) : (
<ContactProfileImage
updated={
tempImage.shouldDelete
? null
: tempImage.uri
? tempImage.comparison?.updated
: isEditingMyProfile
? myProfileImage?.updated
: selectedAddedContactImage?.updated
}
uri={
tempImage.shouldDelete
? null
: tempImage.uri
? tempImage.comparison?.uri
: isEditingMyProfile
? myProfileImage?.localUri
: selectedAddedContactImage?.localUri
}
darkModeType={darkModeType}
theme={theme}
/>
)}
</View>
{(isEditingMyProfile || selectedAddedContact.isLNURL) && (
<View style={styles.selectFromPhotos}>
<ThemeIcon
colorOverride={COLORS.lightModeText}
size={20}
iconName={hasImage ? 'X' : 'Image'}
/>
</View>
)}
</TouchableOpacity>
{/* Unified settings card */}
<View style={[styles.card, { backgroundColor: backgroundOffset }]}>
<ProfileInputFields {...inputFieldsProps} />
</View>
<CustomButton
buttonStyles={{
width: '100%',
...CENTER,
marginTop: 'auto',
marginBottom: isKeyboardActive
? CONTENT_KEYBOARD_OFFSET
: bottomPadding,
}}
actionFunction={saveChanges}
useLoading={isSaving}
textContent={
hasChangedInfo
? fromInitialAdd
? t('contacts.editMyProfilePage.addContactBTN')
: t('constants.save')
: t('constants.back')
}
/>
</ScrollView>
</View>
);
async function saveChanges() {
try {
if (isAddingImage) return;
setIsSaving(true);
// ── myProfile: image only ─────────────────────────────────────────────
if (isEditingMyProfile) {
if (tempImage.shouldDelete) {
await deleteProfilePicture(true, null);
} else if (tempImage.uri && tempImage.comparison) {
const areImagesTheSame = await areImagesSame(
tempImage.comparison?.uri,
myProfileImage?.localUri,
);
if (!areImagesTheSame) {
await saveProfileImage(tempImage, true, null);
}
}
keyboardGoBack(navigate);
return;
}
// ── Contact path (unchanged) ──────────────────────────────────────────
if (
inputs.name.length >= 30 ||
inputs.bio.length >= 150 ||
inputs.uniquename.length >= 30 ||
(selectedAddedContact?.isLNURL && inputs.receiveAddress.length >= 200)
)
return;
if (
!VALID_NAME_BIO_REGEX.test(inputs.name) ||
!VALID_NAME_BIO_REGEX.test(inputs.bio)
) {
navigate.navigate('ErrorScreen', {
errorMessage: t('contacts.editMyProfilePage.invalidCharactersError'),
});
return;
}
if (
selectedAddedContact?.isLNURL &&
!EMAIL_REGEX.test(inputs.receiveAddress.trim())
) {
navigate.navigate('ErrorScreen', {
errorMessage: t(
'contacts.editMyProfilePage.invalidReceiveAddressError',
),
});
return;
}
if (tempImage.shouldDelete) {
await deleteProfilePicture(false, selectedAddedContact);
} else if (tempImage.uri && tempImage.comparison) {
const areImagesTheSame = await areImagesSame(
tempImage.comparison?.uri,
selectedAddedContactImage?.localUri,
);
if (!areImagesTheSame) {
await saveProfileImage(tempImage, false, selectedAddedContact);
}
}
if (fromInitialAdd) {
let tempContact = JSON.parse(JSON.stringify(selectedAddedContact));
tempContact.name = inputs.name.trim();
tempContact.nameLower = inputs.name.trim().toLowerCase();
tempContact.bio = inputs.bio;
tempContact.isAdded = true;
tempContact.unlookedTransactions = 0;
if (selectedAddedContact.isLNURL) {
tempContact.receiveAddress = inputs.receiveAddress;
}
let newAddedContacts = JSON.parse(JSON.stringify(decodedAddedContacts));
const isContactInAddedContacts = newAddedContacts.filter(
addedContact => addedContact.uuid === tempContact.uuid,
).length;
if (isContactInAddedContacts) {
newAddedContacts = newAddedContacts.map(addedContact => {
if (addedContact.uuid === tempContact.uuid) {
return {
...addedContact,
name: tempContact.name,
nameLower: tempContact.nameLower,
bio: tempContact.bio,
unlookedTransactions: 0,
isAdded: true,
};
} else return addedContact;
});
} else newAddedContacts.push(tempContact);
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
},
addedContacts: encriptMessage(
contactsPrivateKey,
publicKey,
JSON.stringify(newAddedContacts),
),
},
true,
);
return;
}
if (
selectedAddedContact?.bio === inputs.bio &&
selectedAddedContact?.name === inputs.name &&
selectedAddedContact?.receiveAddress === inputs.receiveAddress
) {
keyboardGoBack(navigate);
} else {
let newAddedContacts = [...decodedAddedContacts];
const indexOfContact = decodedAddedContacts.findIndex(
obj => obj.uuid === selectedAddedContact.uuid,
);
let contact = newAddedContacts[indexOfContact];
contact['name'] = inputs.name.trim();
contact['nameLower'] = inputs.name.trim().toLowerCase();
contact['bio'] = inputs.bio.trim();
if (
selectedAddedContact.isLNURL &&
selectedAddedContact?.receiveAddress !== inputs.receiveAddress
) {
contact['receiveAddress'] = inputs.receiveAddress.trim();
}
toggleGlobalContactsInformation(
{
myProfile: {
...globalContactsInformation.myProfile,
},
addedContacts: encriptMessage(
contactsPrivateKey,
publicKey,
JSON.stringify(newAddedContacts),
),
},
true,
);
keyboardGoBack(navigate);
}
} catch (err) {
console.log(err);
} finally {
setIsSaving(false);
}
}
}
const styles = StyleSheet.create({
hideProfileContainer: {
flex: 1,
alignItems: 'center',
width: INSET_WINDOW_WIDTH,
...CENTER,
},
innerContainer: {
flex: 1,
},
card: {
width: '100%',
borderRadius: 16,
overflow: 'hidden',
marginTop: 16,
marginBottom: 20,
},
sectionHeader: {
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
alignSelf: 'flex-start',
marginTop: 24,
},
selectFromPhotos: {
width: 30,
height: 30,
borderRadius: 20,
backgroundColor: COLORS.darkModeText,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
right: 12.5,
bottom: 12.5,
zIndex: 2,
},
profileImage: {
width: 150,
height: 150,
borderRadius: 125,
...CENTER,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10,
overflow: 'hidden',
},
navRow: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: 'row',
alignItems: 'center',
gap: 5,
minHeight: 60,
},
navRowLabelLine: {
marginRight: 'auto',
flexShrink: 1,
},
navRowLabel: {
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
flexShrink: 1,
marginLeft: 10,
},
navRowValue: {
fontSize: SIZES.medium,
includeFontPadding: false,
marginRight: 'auto',
},
copyLinkRow: {
paddingVertical: 8,
},
copyLinkText: {
fontSize: SIZES.small,
includeFontPadding: false,
opacity: 0.55,
flex: 1,
marginRight: 4,
},
divider: {
height: StyleSheet.hairlineWidth,
marginHorizontal: 16,
opacity: 0.15,
},
});