diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d19f1cf5e..09e30fa63 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -475,6 +475,8 @@ PODS: - React - RNCPicker (2.4.8): - React-Core + - RNFS (2.20.0): + - React-Core - RNGestureHandler (2.12.1): - React-Core - RNKeychain (8.1.1): @@ -589,6 +591,7 @@ DEPENDENCIES: - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" + - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNKeychain (from `../node_modules/react-native-keychain`) - RNPermissions (from `../node_modules/react-native-permissions`) @@ -734,6 +737,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/masked-view" RNCPicker: :path: "../node_modules/@react-native-picker/picker" + RNFS: + :path: "../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNKeychain: @@ -756,7 +761,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 57d2868c099736d80fcd648bf211b4431e51a558 + boost: a7c83b31436843459a1961bfd74b96033dc77234 BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 @@ -768,7 +773,7 @@ SPEC CHECKSUMS: libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 lottie-ios: 8f97d3271e155c2d688875c29cd3c74908aef5f8 lottie-react-native: 8f9d4be452e23f6e5ca0fdc11669dc99ab52be81 - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: df81ab637d35fac9e6eb94611cfd20f0feb05455 RCTTypeSafety: 4636e4a36c7c2df332bda6d59b19b41c443d4287 React: e0cc5197a804031a6c53fb38483c3485fcb9d6f3 @@ -821,6 +826,7 @@ SPEC CHECKSUMS: RNCClipboard: 99fc8ad669a376b756fbc8098ae2fd05c0ed0668 RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNCPicker: 0bf8ef8f7800524f32d2bb2a8bcadd53eda0ecd1 + RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: c0d04458598fcb26052494ae23dda8f8f5162b13 RNKeychain: ff836453cba46938e0e9e4c22e43d43fa2c90333 RNPermissions: 06845210af313594dc3fe5e8ae6afc820d791409 diff --git a/package.json b/package.json index ca15294c7..81c7e2a9d 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "react-native-draglist": "3.5.1", "react-native-elements": "3.4.3", "react-native-encrypted-storage": "4.0.3", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "2.12.1", "react-native-get-random-values": "1.9.0", "react-native-hce": "0.1.2", diff --git a/views/NostrContacts.tsx b/views/NostrContacts.tsx index 796f1eddf..ca545964f 100644 --- a/views/NostrContacts.tsx +++ b/views/NostrContacts.tsx @@ -13,6 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; import { CheckBox, Icon } from 'react-native-elements'; import EncryptedStorage from 'react-native-encrypted-storage'; import { relayInit, nip05, nip19 } from 'nostr-tools'; +import RNFS from 'react-native-fs'; import Button from '../components/Button'; import Header from '../components/Header'; @@ -196,31 +197,84 @@ export default class NostrContacts extends React.Component< }); } - transformContactData = (contact: any) => { - return { - photo: contact?.picture, - name: contact?.display_name || contact?.name, - description: contact?.about, - lnAddress: contact?.lud16 - ? [contact?.lud16] - : contact?.lud06 - ? [contact?.lud06] - : contact?.lud16 && contact?.lud06 - ? [contact?.lud06, contact?.lud16] - : [], - onchainAddress: [''], - pubkey: [''], - nip05: contact?.nip05 ? [contact?.nip05] : [], - nostrNpub: contact?.npub - ? [contact?.npub] - : contact?.pubkey - ? [nip19.npubEncode(contact.pubkey)] - : [], - contactId: uuidv4(), - isFavourite: false, - banner: contact?.banner, - isSelected: false - }; + transformContactData = async (contact: any) => { + try { + const name = contact?.display_name || contact?.name || ''; + const transformedContact = { + photo: '', + name, + description: contact?.about || '', + lnAddress: contact?.lud16 + ? [contact?.lud16] + : contact?.lud06 + ? [contact?.lud06] + : contact?.lud16 && contact?.lud06 + ? [contact?.lud06, contact?.lud16] + : [], + onchainAddress: [''], + pubkey: [''], + nip05: contact?.nip05 ? [contact?.nip05] : [], + nostrNpub: contact?.npub + ? [contact?.npub] + : contact?.pubkey + ? [nip19.npubEncode(contact.pubkey)] + : [], + contactId: uuidv4(), + isFavourite: false, + banner: '', + isSelected: false + }; + + if (contact?.banner) { + console.log('Downloading banner...'); + const bannerFileName = + 'nostrContactBanner_' + transformedContact.name + '.png'; + const bannerFilePath = + RNFS.DocumentDirectoryPath + '/' + bannerFileName; + + try { + // Download the banner and save it locally + await RNFS.downloadFile({ + fromUrl: contact?.banner, + toFile: bannerFilePath + }).promise; + + console.log('Banner download successful!'); + transformedContact.banner = 'file://' + bannerFilePath; + } catch (bannerError) { + console.error('Error downloading banner:', bannerError); + } + } + + console.log('Transformed contact:', transformedContact); + + if (contact?.picture) { + console.log('Downloading image...'); + const fileName = + 'nostrContactPhoto_' + + transformedContact.contactId + + '.png'; + const filePath = RNFS.DocumentDirectoryPath + '/' + fileName; + + try { + // Download the image and save it locally + await RNFS.downloadFile({ + fromUrl: contact?.picture, + toFile: filePath + }).promise; + + console.log('Download successful!'); + transformedContact.photo = 'file://' + filePath; + } catch (photoError) { + console.error('Error downloading photo:', photoError); + } + } + + return transformedContact; + } catch (error) { + console.error('Error transforming contact:', error); + return null; + } }; toggleContactSelection = (contact: any) => { @@ -267,7 +321,7 @@ export default class NostrContacts extends React.Component< return `${str.substring(0, 6)}...${str.substring(str.length - 6)}`; }; - if (!item.name && !item.display_name) { + if (!item?.name && !item?.display_name) { return null; } @@ -286,14 +340,12 @@ export default class NostrContacts extends React.Component< return ( { + onPress={async () => { if (isSelectionMode) { - this.toggleContactSelection( - this.transformContactData(item) - ); + this.toggleContactSelection(item); } else { navigation.navigate('ContactDetails', { - nostrContact: this.transformContactData(item), + nostrContact: await this.transformContactData(item), isNostrContact: true }); } @@ -314,9 +366,7 @@ export default class NostrContacts extends React.Component< { - this.toggleContactSelection( - this.transformContactData(item) - ); + this.toggleContactSelection(item); }} /> @@ -349,7 +399,7 @@ export default class NostrContacts extends React.Component< color: themeColor('text') }} > - {item.display_name || item.name} + {item?.display_name || item?.name} {item.lud06 && ( { + importContacts = async () => { try { - const { selectedContacts } = this.state; - - // Retrieve existing contacts from Encrypted storage - const contactsString = await EncryptedStorage.getItem( - 'zeus-contacts' - ); - const existingContacts: any = contactsString - ? JSON.parse(contactsString) - : []; + let contactsToImport = []; - // Merge existing contacts with the selected contacts - const updatedContacts = [ - ...existingContacts, - ...selectedContacts - ].sort((a, b) => a.name.localeCompare(b.name)); + // Check if selectedContacts is not empty, use it; otherwise, use contactsData + if (this.state.selectedContacts.length > 0) { + contactsToImport = this.state.selectedContacts; + } else { + contactsToImport = this.state.contactsData; + } - // Save the updated contacts to encrypted storage - await EncryptedStorage.setItem( - 'zeus-contacts', - JSON.stringify(updatedContacts) + // Remove null values and contacts without name or display_name + contactsToImport = contactsToImport.filter( + (contact) => + contact !== null && + (contact?.name || contact?.display_name) && + contact?.picture && + (contact?.lud06 || contact.lud16) ); - console.log('Contacts imported successfully!'); - } catch (error) { - console.log('Error importing contacts:', error); - Alert.alert( - localeString('general.error'), - localeString('views.NostrContacts.importContactsError') + // Transform Nostr contacts data to match the format in AddContact + const transformedContactsPromises = contactsToImport.map( + (contact) => this.transformContactData(contact) ); - } - }; - importAllContacts = async () => { - try { - const contactsToImport = this.state.contactsData; - - // Transform Nostr contacts data to match the format in AddContact - const transformedContacts = contactsToImport.map((contact) => - this.transformContactData(contact) + const transformedContacts = await Promise.all( + transformedContactsPromises ); // Retrieve existing contacts from Encrypted storage @@ -623,7 +659,7 @@ export default class NostrContacts extends React.Component< 'views.NostrContacts.importAllContacts' )} onPress={() => { - this.importAllContacts(); + this.importContacts(); navigation.navigate('Contacts'); }} containerStyle={{ @@ -643,7 +679,7 @@ export default class NostrContacts extends React.Component< : '' }`} onPress={() => { - this.importSelectedContacts(); + this.importContacts(); navigation.navigate('Contacts'); }} containerStyle={{ paddingBottom: 12, paddingTop: 8 }} diff --git a/views/Settings/AddContact.tsx b/views/Settings/AddContact.tsx index 251ca1903..f599e4c7e 100644 --- a/views/Settings/AddContact.tsx +++ b/views/Settings/AddContact.tsx @@ -15,6 +15,7 @@ import { v4 as uuidv4 } from 'uuid'; import EncryptedStorage from 'react-native-encrypted-storage'; import { Icon, Divider } from 'react-native-elements'; import { launchImageLibrary } from 'react-native-image-picker'; +import RNFS from 'react-native-fs'; import LightningBolt from '../../assets/images/SVG/Lightning Bolt.svg'; import BitcoinIcon from '../../assets/images/SVG/BitcoinIcon.svg'; @@ -260,13 +261,33 @@ export default class AddContact extends React.Component< maxHeight: 500, includeBase64: true }, - (response: any) => { + async (response: any) => { if (!response.didCancel) { const asset = response?.assets[0]; if (asset.base64) { - this.setState({ - photo: `data:image/png;base64,${asset.base64}` - }); + // Generate a unique name for the image + const timestamp = new Date().getTime(); // Timestamp + const fileName = `photo_${timestamp}.png`; + + const filePath = + RNFS.DocumentDirectoryPath + '/' + fileName; + + try { + // Write the base64 data to the file + await RNFS.writeFile( + filePath, + asset.base64, + 'base64' + ); + console.log('File saved to ', filePath); + + // Set the local file path in the state + this.setState({ + photo: 'file://' + filePath + }); + } catch (error) { + console.error('Error saving file: ', error); + } } } } diff --git a/yarn.lock b/yarn.lock index 93c268824..1e721d3f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3198,7 +3198,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@0.1.0: +base-64@0.1.0, base-64@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== @@ -8098,6 +8098,14 @@ react-native-encrypted-storage@4.0.3: resolved "https://registry.yarnpkg.com/react-native-encrypted-storage/-/react-native-encrypted-storage-4.0.3.tgz#2a4d65459870511e8f4ccd22f02433dab7fa5e91" integrity sha512-0pJA4Aj2S1PIJEbU7pN/Qvf7JIJx3hFywx+i+bLHtgK0/6Zryf1V2xVsWcrD50dfiu3jY1eN2gesQ5osGxE7jA== +react-native-fs@^2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" + integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== + dependencies: + base-64 "^0.1.0" + utf8 "^3.0.0" + react-native-gesture-handler@2.12.1: version "2.12.1" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.12.1.tgz#f11a99fb95169810c6886fad5efa01a17fd81660" @@ -9545,7 +9553,7 @@ use-sync-external-store@^1.0.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== -utf8@3.0.0: +utf8@3.0.0, utf8@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==