710 lines
27 KiB
TypeScript
710 lines
27 KiB
TypeScript
import * as React from 'react';
|
|
import {
|
|
Text,
|
|
View,
|
|
Image,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
ScrollView
|
|
} from 'react-native';
|
|
import { inject, observer } from 'mobx-react';
|
|
import {
|
|
SharedText,
|
|
sharedTransitionEntering
|
|
} from '../components/SharedTransition';
|
|
import { Route } from '@react-navigation/native';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
import Screen from '../components/Screen';
|
|
import Button from '../components/Button';
|
|
import LoadingIndicator from '../components/LoadingIndicator';
|
|
import Header from '../components/Header';
|
|
import { Row } from '../components/layout/Row';
|
|
import { ContactAvatar } from '../components/ContactAvatar';
|
|
|
|
import ContactStore, { CONTACTS_KEY } from '../stores/ContactStore';
|
|
|
|
import LightningBolt from '../assets/images/SVG/Lightning Bolt.svg';
|
|
import BitcoinIcon from '../assets/images/SVG/BitcoinIcon.svg';
|
|
import KeySecurity from '../assets/images/SVG/Key Security.svg';
|
|
import VerifiedAccount from '../assets/images/SVG/Verified Account.svg';
|
|
import EditContact from '../assets/images/SVG/Pen.svg';
|
|
import Star from '../assets/images/SVG/Star.svg';
|
|
import QR from '../assets/images/SVG/QR.svg';
|
|
import Ecash from '../assets/images/SVG/Ecash.svg';
|
|
|
|
import { themeColor } from '../utils/ThemeUtils';
|
|
import LinkingUtils from '../utils/LinkingUtils';
|
|
import { localeString } from '../utils/LocaleUtils';
|
|
|
|
import Storage from '../storage';
|
|
|
|
import Contact from '../models/Contact';
|
|
|
|
const AddressRow: React.FC<{
|
|
address: string;
|
|
onPress: () => void;
|
|
icon: React.ReactNode;
|
|
}> = ({ address, onPress, icon }) => (
|
|
<TouchableOpacity onPress={onPress}>
|
|
<View style={styles.contactRow}>
|
|
{icon}
|
|
<Text
|
|
numberOfLines={1}
|
|
ellipsizeMode="middle"
|
|
style={{
|
|
...styles.contactFields,
|
|
color: themeColor('chain')
|
|
}}
|
|
>
|
|
{address}
|
|
</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
interface ContactDetailsProps {
|
|
navigation: NativeStackNavigationProp<any, any>;
|
|
route: Route<
|
|
'ContactDetails',
|
|
{
|
|
isNostrContact: boolean;
|
|
contactId: string;
|
|
contactName?: string;
|
|
contactPhoto?: string;
|
|
contactHasOnlyCashuPubkey?: boolean;
|
|
nostrContact: any;
|
|
cashuLockData?: any;
|
|
}
|
|
>;
|
|
ContactStore: ContactStore;
|
|
}
|
|
|
|
interface ContactDetailsState {
|
|
contact: Contact | any;
|
|
isLoading: boolean;
|
|
isNostrContact: boolean;
|
|
}
|
|
|
|
@inject('ContactStore')
|
|
@observer
|
|
export default class ContactDetails extends React.Component<
|
|
ContactDetailsProps,
|
|
ContactDetailsState
|
|
> {
|
|
private focusListener?: () => void;
|
|
constructor(props: ContactDetailsProps) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
contact: {
|
|
lnAddress: [''],
|
|
bolt12Address: [''],
|
|
bolt12Offer: [''],
|
|
onchainAddress: [''],
|
|
pubkey: [''],
|
|
cashuPubkey: [''],
|
|
nip05: [''],
|
|
nostrNpub: [''],
|
|
name: '',
|
|
description: '',
|
|
photo: null,
|
|
isFavourite: false,
|
|
contactId: '',
|
|
banner: '',
|
|
id: ''
|
|
},
|
|
isLoading: true,
|
|
isNostrContact: false
|
|
};
|
|
}
|
|
|
|
async componentDidMount() {
|
|
try {
|
|
const isNostrContact = this.props.route.params?.isNostrContact;
|
|
this.setState({ isNostrContact });
|
|
|
|
await this.fetchContact();
|
|
|
|
this.focusListener = this.props.navigation.addListener(
|
|
'focus',
|
|
async () => {
|
|
await this.fetchContact();
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.focusListener) {
|
|
this.focusListener();
|
|
}
|
|
}
|
|
|
|
fetchContact = async () => {
|
|
try {
|
|
const { contactId, nostrContact, isNostrContact } =
|
|
this.props.route.params ?? {};
|
|
const contactsString: any = await Storage.getItem(CONTACTS_KEY);
|
|
|
|
const storedContact =
|
|
contactsString && contactId
|
|
? JSON.parse(contactsString).find(
|
|
(c: Contact) =>
|
|
c.contactId === contactId || c.id === contactId
|
|
)
|
|
: undefined;
|
|
|
|
this.setState({
|
|
contact: storedContact ?? nostrContact,
|
|
isNostrContact,
|
|
isLoading: false
|
|
});
|
|
} catch (error) {
|
|
console.log('Error fetching contact:', error);
|
|
this.setState({ isLoading: false });
|
|
}
|
|
};
|
|
|
|
sendAddress = (address: string) => {
|
|
const { navigation } = this.props;
|
|
const { contact } = this.state;
|
|
navigation.navigate('Send', {
|
|
destination: address,
|
|
contactName: contact.name
|
|
});
|
|
};
|
|
|
|
selectCashuPubkeyForLocking = (cashuPubkey: string) => {
|
|
const { navigation, route } = this.props;
|
|
const { contact } = this.state;
|
|
const { cashuLockData } = route.params || {};
|
|
|
|
navigation.navigate('CashuLockSettings', {
|
|
destination: cashuPubkey,
|
|
contactName: contact.name,
|
|
...cashuLockData
|
|
});
|
|
};
|
|
|
|
saveUpdatedContact = async (updatedContact: Contact) => {
|
|
const { ContactStore } = this.props;
|
|
try {
|
|
const contactsString: any = await Storage.getItem(CONTACTS_KEY);
|
|
|
|
if (contactsString) {
|
|
const existingContacts: Contact[] = JSON.parse(contactsString);
|
|
|
|
// Find the index of the contact with the same name
|
|
const contactIndex = existingContacts.findIndex(
|
|
(contact) => contact.contactId === updatedContact.contactId
|
|
);
|
|
|
|
if (contactIndex !== -1) {
|
|
// Update the contact in the array
|
|
existingContacts[contactIndex] = updatedContact;
|
|
|
|
// Save the updated contacts back to storage
|
|
await Storage.setItem(CONTACTS_KEY, existingContacts);
|
|
|
|
console.log('Contact updated successfully!');
|
|
ContactStore?.loadContacts();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Error updating contact:', error);
|
|
}
|
|
};
|
|
|
|
importToContacts = async () => {
|
|
const { contact } = this.state;
|
|
|
|
const newContact = {
|
|
...contact,
|
|
contactId: uuidv4()
|
|
};
|
|
|
|
const contactsString: any = await Storage.getItem(CONTACTS_KEY);
|
|
|
|
const existingContacts: Contact[] = contactsString
|
|
? JSON.parse(contactsString)
|
|
: [];
|
|
|
|
const updatedContacts = [...existingContacts, newContact].sort((a, b) =>
|
|
a.name.localeCompare(b.name)
|
|
);
|
|
|
|
await Storage.setItem(CONTACTS_KEY, updatedContacts);
|
|
|
|
console.log('Contact imported successfully!');
|
|
this.props.navigation.popTo('Contacts');
|
|
};
|
|
|
|
toggleFavorite = () => {
|
|
const { contact } = this.state;
|
|
|
|
// Toggle the isFavourite field
|
|
const updatedContact = {
|
|
...contact,
|
|
isFavourite: !contact.isFavourite
|
|
};
|
|
|
|
// Save the updated contact
|
|
this.saveUpdatedContact(updatedContact);
|
|
|
|
// Update the state to reflect the changes
|
|
this.setState({ contact: updatedContact });
|
|
};
|
|
|
|
handleNostr = (value: string) => {
|
|
const deepLink = `nostr:${value}`;
|
|
LinkingUtils.handleDeepLink(deepLink, this.props.navigation);
|
|
};
|
|
|
|
render() {
|
|
const { isLoading, isNostrContact } = this.state;
|
|
const { navigation, ContactStore, route } = this.props;
|
|
const { setPrefillContact } = ContactStore;
|
|
const { cashuLockData } = route.params || {};
|
|
const fromCashuLockSettings = cashuLockData?.fromCashuLockSettings;
|
|
|
|
const contact = new Contact(this.state.contact);
|
|
const nostrContact = this.props.route.params?.nostrContact;
|
|
const StarButton = () => (
|
|
<TouchableOpacity onPress={this.toggleFavorite}>
|
|
<Star
|
|
fill={contact.isFavourite ? themeColor('text') : 'none'}
|
|
stroke={contact.isFavourite ? 'none' : themeColor('text')}
|
|
strokeWidth={2}
|
|
style={{ alignSelf: 'center', marginRight: 16 }}
|
|
/>
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
const EditContactButton = () => (
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setPrefillContact(contact);
|
|
navigation.navigate('AddContact', {
|
|
isEdit: true
|
|
});
|
|
}}
|
|
>
|
|
<EditContact
|
|
fill={themeColor('text')}
|
|
style={{ alignSelf: 'center' }}
|
|
/>
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
// Function to add prefixes to addresses based on their types
|
|
const addPrefixToAddresses = (
|
|
addresses: string[] | undefined,
|
|
prefix: string
|
|
) =>
|
|
(addresses || [])
|
|
.filter(Boolean)
|
|
.map((address) => `${prefix}${address}`);
|
|
|
|
const QRButton = () => {
|
|
const {
|
|
lnAddress,
|
|
onchainAddress,
|
|
pubkey,
|
|
nostrNpub,
|
|
nip05,
|
|
cashuPubkey
|
|
} = contact;
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
const contactDataWithoutPhoto = {
|
|
...this.state.contact
|
|
};
|
|
|
|
// Check if 'photo' exists and doesn't start with 'http'
|
|
if (
|
|
contactDataWithoutPhoto.photo &&
|
|
!contactDataWithoutPhoto.photo.startsWith('http')
|
|
) {
|
|
delete contactDataWithoutPhoto.photo;
|
|
}
|
|
|
|
// Add the 'zeuscontact:' prefix to the contactData parameter
|
|
const zeusContactData = `zeuscontact:${JSON.stringify(
|
|
contactDataWithoutPhoto
|
|
)}`;
|
|
navigation.navigate('MultiQR', {
|
|
fromContactDetailsView: true,
|
|
contactData: zeusContactData,
|
|
addressData: [
|
|
...addPrefixToAddresses(
|
|
lnAddress,
|
|
'lightning:'
|
|
),
|
|
...addPrefixToAddresses(pubkey, 'lightning:'),
|
|
...addPrefixToAddresses(
|
|
cashuPubkey,
|
|
'lightning:'
|
|
),
|
|
...addPrefixToAddresses(
|
|
onchainAddress,
|
|
'bitcoin:'
|
|
),
|
|
...addPrefixToAddresses(nostrNpub, 'nostr:'),
|
|
...addPrefixToAddresses(nip05, 'nostr:')
|
|
]
|
|
});
|
|
}}
|
|
>
|
|
<QR
|
|
fill={themeColor('text')}
|
|
style={{ alignSelf: 'center' }}
|
|
/>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
// Get params for shared element transition during loading
|
|
const contactId = this.props.route.params?.contactId;
|
|
const contactName = this.props.route.params?.contactName;
|
|
const contactPhoto = this.props.route.params?.contactPhoto;
|
|
const contactHasOnlyCashuPubkey =
|
|
this.props.route.params?.contactHasOnlyCashuPubkey;
|
|
|
|
return (
|
|
<>
|
|
{isLoading ? (
|
|
<Screen>
|
|
<Header
|
|
leftComponent="Back"
|
|
containerStyle={{
|
|
borderBottomWidth: 0
|
|
}}
|
|
navigation={navigation}
|
|
/>
|
|
<View style={{ alignItems: 'center', marginTop: 20 }}>
|
|
<ContactAvatar
|
|
contactId={contactId}
|
|
imageUrl={contactPhoto}
|
|
name={contactName}
|
|
size="large"
|
|
contactHasOnlyCashuPubkey={
|
|
contactHasOnlyCashuPubkey
|
|
}
|
|
/>
|
|
{contactName && (
|
|
<SharedText
|
|
tag={`contact-name-${contactId}`}
|
|
style={{
|
|
fontSize: 40,
|
|
fontWeight: 'bold',
|
|
marginBottom: 10,
|
|
color: 'white'
|
|
}}
|
|
>
|
|
{contactName}
|
|
</SharedText>
|
|
)}
|
|
<View style={{ marginTop: 40 }}>
|
|
<LoadingIndicator />
|
|
</View>
|
|
</View>
|
|
</Screen>
|
|
) : (
|
|
<Screen>
|
|
<Header
|
|
leftComponent="Back"
|
|
centerComponent={
|
|
isNostrContact ? <></> : <EditContactButton />
|
|
}
|
|
rightComponent={
|
|
<Row>
|
|
{!isNostrContact && <StarButton />}
|
|
<QRButton />
|
|
</Row>
|
|
}
|
|
placement="right"
|
|
containerStyle={{
|
|
borderBottomWidth: 0
|
|
}}
|
|
navigation={navigation}
|
|
/>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
>
|
|
{contact.banner && (
|
|
<Image
|
|
source={{ uri: contact.getBanner }}
|
|
style={{
|
|
width: '100%',
|
|
height: 150,
|
|
marginBottom: 20
|
|
}}
|
|
/>
|
|
)}
|
|
<ContactAvatar
|
|
size="large"
|
|
sharedTransitionEntering={
|
|
sharedTransitionEntering
|
|
}
|
|
style={{ marginTop: contact.banner ? -100 : 0 }}
|
|
contactId={contactId}
|
|
contactHasOnlyCashuPubkey={
|
|
contactHasOnlyCashuPubkey
|
|
}
|
|
name={contactName || contact.name}
|
|
imageUrl={contactPhoto || contact.getPhoto}
|
|
/>
|
|
<SharedText
|
|
tag={`contact-name-${
|
|
contactId || contact.contactId || contact.id
|
|
}`}
|
|
style={{
|
|
fontSize: 40,
|
|
fontWeight: 'bold',
|
|
marginBottom: 10,
|
|
color: themeColor('text')
|
|
}}
|
|
entering={sharedTransitionEntering}
|
|
>
|
|
{contact.name}
|
|
</SharedText>
|
|
<Text
|
|
style={{
|
|
...styles.contactDescription,
|
|
color: themeColor('secondaryText')
|
|
}}
|
|
>
|
|
{contact.description
|
|
.trim()
|
|
.replace(/\s+/g, ' ')}
|
|
</Text>
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasLnAddress &&
|
|
contact.lnAddress.map(
|
|
(address: string, index: number) => (
|
|
<AddressRow
|
|
key={`ln-${index}`}
|
|
address={address}
|
|
onPress={() =>
|
|
this.sendAddress(address)
|
|
}
|
|
icon={<LightningBolt />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasBolt12Address &&
|
|
contact.bolt12Address.map(
|
|
(address: string, index: number) => (
|
|
<AddressRow
|
|
key={`bolt12addr-${index}`}
|
|
address={address}
|
|
onPress={() =>
|
|
this.sendAddress(address)
|
|
}
|
|
icon={<LightningBolt />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasBolt12Offer &&
|
|
contact.bolt12Offer.map(
|
|
(address: string, index: number) => (
|
|
<AddressRow
|
|
key={`bolt12offer-${index}`}
|
|
address={address}
|
|
onPress={() =>
|
|
this.sendAddress(address)
|
|
}
|
|
icon={<LightningBolt />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasNoffer &&
|
|
contact.noffer.map(
|
|
(address: string, index: number) => (
|
|
<AddressRow
|
|
key={`noffer-${index}`}
|
|
address={address}
|
|
onPress={() =>
|
|
this.sendAddress(address)
|
|
}
|
|
icon={<LightningBolt />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasPubkey &&
|
|
contact.pubkey.map(
|
|
(address: string, index: number) => (
|
|
<AddressRow
|
|
key={`pubkey-${index}`}
|
|
address={address}
|
|
onPress={() =>
|
|
this.sendAddress(address)
|
|
}
|
|
icon={<LightningBolt />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{contact.hasCashuPubkey &&
|
|
contact.cashuPubkey.map(
|
|
(address: string, index: number) => (
|
|
<AddressRow
|
|
key={`cashu-${index}`}
|
|
address={address}
|
|
onPress={() =>
|
|
fromCashuLockSettings
|
|
? this.selectCashuPubkeyForLocking(
|
|
address
|
|
)
|
|
: this.sendAddress(address)
|
|
}
|
|
icon={<Ecash fill={'#FACC15'} />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasOnchainAddress &&
|
|
contact.onchainAddress.map(
|
|
(address: string, index: number) => (
|
|
<AddressRow
|
|
key={`onchain-${index}`}
|
|
address={address}
|
|
onPress={() =>
|
|
this.sendAddress(address)
|
|
}
|
|
icon={<BitcoinIcon />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasNip05 &&
|
|
contact.nip05.map(
|
|
(value: string, index: number) => (
|
|
<AddressRow
|
|
key={`nip05-${index}`}
|
|
address={value}
|
|
onPress={() =>
|
|
this.handleNostr(value)
|
|
}
|
|
icon={<VerifiedAccount />}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{!fromCashuLockSettings &&
|
|
contact.hasNpub &&
|
|
contact.nostrNpub.map(
|
|
(value: string, index: number) => (
|
|
<AddressRow
|
|
key={`npub-${index}`}
|
|
address={value}
|
|
onPress={() =>
|
|
this.handleNostr(value)
|
|
}
|
|
icon={<KeySecurity />}
|
|
/>
|
|
)
|
|
)}
|
|
</ScrollView>
|
|
{isNostrContact && (
|
|
<>
|
|
<Button
|
|
onPress={() => this.importToContacts()}
|
|
title={localeString(
|
|
'views.ContactDetails.saveToContacts'
|
|
)}
|
|
containerStyle={{ paddingBottom: 12 }}
|
|
/>
|
|
<Button
|
|
onPress={() => {
|
|
navigation.goBack();
|
|
setPrefillContact(nostrContact);
|
|
navigation.navigate('AddContact', {
|
|
isEdit: true,
|
|
isNostrContact
|
|
});
|
|
}}
|
|
title={localeString(
|
|
'views.ContactDetails.editAndSaveContact'
|
|
)}
|
|
containerStyle={{ paddingBottom: 12 }}
|
|
secondary
|
|
/>
|
|
</>
|
|
)}
|
|
</Screen>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
contactRow: {
|
|
flexDirection: 'row',
|
|
marginLeft: 20,
|
|
marginRight: 20,
|
|
alignItems: 'center'
|
|
},
|
|
contactFields: {
|
|
fontSize: 24,
|
|
marginBottom: 4,
|
|
marginLeft: 5,
|
|
marginRight: 5,
|
|
flexShrink: 1
|
|
},
|
|
avatarContainer: {
|
|
width: 150,
|
|
height: 150,
|
|
borderRadius: 75
|
|
},
|
|
avatarView: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
},
|
|
avatarInitials: {
|
|
fontSize: 48,
|
|
fontWeight: 'bold'
|
|
},
|
|
scrollContent: {
|
|
backgroundColor: 'none',
|
|
alignItems: 'center',
|
|
paddingBottom: 10
|
|
},
|
|
bannerContainer: {
|
|
width: '100%',
|
|
height: 150,
|
|
marginBottom: 95
|
|
},
|
|
bannerImage: {
|
|
width: '100%',
|
|
height: 150
|
|
},
|
|
bannerAvatarOverlay: {
|
|
position: 'absolute',
|
|
top: 75,
|
|
alignSelf: 'center'
|
|
},
|
|
contactDescription: {
|
|
fontSize: 20,
|
|
marginBottom: 6,
|
|
marginHorizontal: 20
|
|
}
|
|
});
|