From 2494b205e89a802776b77a8a537523319cbe1960 Mon Sep 17 00:00:00 2001 From: Nathan_akin <85641756+akintewe@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:44:25 +0100 Subject: [PATCH] contacts integration --- .../src/components/ContactsRow/index.tsx | 46 +++ .../src/components/ContactsRow/styles.ts | 50 +++ .../FormPrivateMessage/index.tsx | 94 +++-- .../FormPrivateMessage/styles.ts | 4 +- .../src/modules/Contacts/ContactList.tsx | 342 ++++++++++++++++++ apps/mobile/src/modules/Contacts/styles.ts | 165 +++++++++ .../src/modules/DirectMessages/index.tsx | 16 +- .../src/modules/DirectMessages/styles.ts | 2 +- apps/mobile/src/types/tab.ts | 8 +- 9 files changed, 672 insertions(+), 55 deletions(-) create mode 100644 apps/mobile/src/components/ContactsRow/index.tsx create mode 100644 apps/mobile/src/components/ContactsRow/styles.ts create mode 100644 apps/mobile/src/modules/Contacts/ContactList.tsx create mode 100644 apps/mobile/src/modules/Contacts/styles.ts diff --git a/apps/mobile/src/components/ContactsRow/index.tsx b/apps/mobile/src/components/ContactsRow/index.tsx new file mode 100644 index 00000000..77285907 --- /dev/null +++ b/apps/mobile/src/components/ContactsRow/index.tsx @@ -0,0 +1,46 @@ +import {Contact} from 'afk_nostr_sdk'; +import React from 'react'; +import {Image, ScrollView, Text, TouchableOpacity, View} from 'react-native'; + +import {useStyles} from '../../hooks'; +import stylesheet from './styles'; + +interface ContactsRowProps { + contacts: Contact[]; + onContactPress: (contact: Contact) => void; + onAddContact: () => void; +} + +export const ContactsRow: React.FC = ({ + contacts, + onContactPress, + onAddContact, +}) => { + const styles = useStyles(stylesheet); + return ( + + Contacts + + + + + + + {contacts.map((contact) => ( + + + + {contact.displayName || 'Unnamed'} + + + ))} + + + ); +}; \ No newline at end of file diff --git a/apps/mobile/src/components/ContactsRow/styles.ts b/apps/mobile/src/components/ContactsRow/styles.ts new file mode 100644 index 00000000..7b460540 --- /dev/null +++ b/apps/mobile/src/components/ContactsRow/styles.ts @@ -0,0 +1,50 @@ +import {ThemedStyleSheet} from '../../styles'; + +export default ThemedStyleSheet((theme) => ({ + contactsContainer: { + paddingHorizontal: 16, + marginBottom: 16, + }, + contactsTitle: { + fontSize: 16, + fontWeight: 'bold', + color: theme.colors.text, + marginBottom: 12, + }, + contactsScrollContent: { + flexDirection: 'row', + alignItems: 'center', + }, + addContactButton: { + width: 50, + height: 50, + borderRadius: 25, + backgroundColor: theme.colors.primary, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + plusSign: { + color: theme.colors.white, + fontSize: 24, + fontWeight: 'bold', + lineHeight: 24, + }, + contactAvatar: { + alignItems: 'center', + marginRight: 12, + width: 50, + }, + avatarImage: { + width: 50, + height: 50, + borderRadius: 25, + marginBottom: 4, + }, + contactName: { + fontSize: 12, + color: theme.colors.text, + textAlign: 'center', + width: '100%', + }, +})); \ No newline at end of file diff --git a/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/index.tsx b/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/index.tsx index dfb35874..7809c593 100644 --- a/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/index.tsx +++ b/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/index.tsx @@ -1,11 +1,12 @@ import {NDKUser} from '@nostr-dev-kit/ndk'; import {useQueryClient} from '@tanstack/react-query'; -import {useSendPrivateMessage} from 'afk_nostr_sdk'; -import React from 'react'; +import {Contact, getContacts, useSendPrivateMessage} from 'afk_nostr_sdk'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import {useStyles} from '../../../hooks'; import {useToast} from '../../../hooks/modals'; +import {ContactsRow} from '../../ContactsRow'; import {Divider} from '../../Divider'; import {IconButton} from '../../IconButton'; import {Input} from '../../Input'; @@ -18,6 +19,7 @@ interface IFormPrivateMessage { receiverPublicKeyProps?: string; handleClose?: () => void; } + export const FormPrivateMessage: React.FC = ({ user, publicKey, @@ -25,6 +27,7 @@ export const FormPrivateMessage: React.FC = ({ receiverPublicKeyProps, }) => { const styles = useStyles(stylesheet); + const [storedContacts, setStoredContacts] = useState([]); const avatar = user?.profile?.banner ?? require('../../../assets/pepe-logo.png'); const [receiverPublicKey, setReceiverPublicKey] = React.useState(receiverPublicKeyProps); @@ -33,15 +36,28 @@ export const FormPrivateMessage: React.FC = ({ const {showToast} = useToast(); const queryClient = useQueryClient(); + useEffect(() => { + const fetchContacts = () => { + const contactsData = getContacts(); + if (contactsData) { + setStoredContacts(JSON.parse(contactsData)); + } + }; + fetchContacts(); + }, []); + + const handleContactSelect = (contact: Contact) => { + if (contact.pubkey) { + setReceiverPublicKey(contact.pubkey); + } + }; + const sendMessage = async (message: string) => { if (!receiverPublicKey) { showToast({title: 'Please choose a Nostr public key', type: 'error'}); return; } - //todo: integrate hook here - //todo: encrypt message - //todo: send message await sendPrivateMessage.mutateAsync( {receiverPublicKeyProps: receiverPublicKey, content: message}, { @@ -59,50 +75,28 @@ export const FormPrivateMessage: React.FC = ({ ); }; - const handleSendMessage = () => { - if (!message) { - showToast({title: 'Please add a content', type: 'error'}); - return; - } - if (!receiverPublicKey) { - showToast({title: 'Please choose a Nostr public key', type: 'error'}); - return; - } - - sendMessage(message); - }; - return ( - <> - {/* - - - - {user.name} - - */} - - - - - - - - - - - - {/* */} - - + + { + // Handle add contact action + showToast({title: 'Add contact functionality to be implemented', type: 'info'}); + }} + /> + + + + + message && sendMessage(message)} /> + + + ); -}; +}; \ No newline at end of file diff --git a/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/styles.ts b/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/styles.ts index 8515a2a4..0af3973a 100644 --- a/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/styles.ts +++ b/apps/mobile/src/components/PrivateMessages/FormPrivateMessage/styles.ts @@ -3,7 +3,7 @@ import {Spacing, ThemedStyleSheet} from '../../../styles'; export default ThemedStyleSheet((theme) => ({ container: { flex: 1, - backgroundColor: theme.colors.background, + backgroundColor: theme.colors.surface, }, header: { padding: 10, @@ -48,4 +48,4 @@ export default ThemedStyleSheet((theme) => ({ flex: 1, width: 'auto', }, -})); +})); \ No newline at end of file diff --git a/apps/mobile/src/modules/Contacts/ContactList.tsx b/apps/mobile/src/modules/Contacts/ContactList.tsx new file mode 100644 index 00000000..f70208b3 --- /dev/null +++ b/apps/mobile/src/modules/Contacts/ContactList.tsx @@ -0,0 +1,342 @@ +import {useQueryClient} from '@tanstack/react-query'; +import {addContacts, Contact, getContacts, useEditContacts, useProfile} from 'afk_nostr_sdk'; +import React, {useEffect, useState} from 'react'; +import { + Dimensions, + Image, + Modal, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; + +import {useTheme} from '../../hooks'; +import {useToast} from '../../hooks/modals'; + +interface ContactListProps { + onClose: () => void; +} + +export const ContactList: React.FC = ({onClose}) => { + const [nostrAddress, setNostrAddress] = useState(''); + const [activeTab, setActiveTab] = useState('all'); // 'all' or 'add' + const [storedContacts, setStoredContacts] = useState([]); + const {theme} = useTheme(); + const {showToast} = useToast(); + const editContacts = useEditContacts(); + const queryClient = useQueryClient(); + + // Destructure refetch from useProfile hook + const {data: profile, refetch} = useProfile({publicKey: nostrAddress}); + + // Fetch contacts when component mounts + useEffect(() => { + const fetchContacts = () => { + const contactsData = getContacts(); + if (contactsData) { + setStoredContacts(JSON.parse(contactsData)); + } + }; + fetchContacts(); + }, []); + + const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', // semi-transparent background + }, + modalView: { + width: Dimensions.get('window').width * 0.85, // Reduced from 0.9 + maxHeight: Dimensions.get('window').height * 0.8, + backgroundColor: theme.colors.surface, + borderRadius: 12, + padding: 20, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + tabContainer: { + flexDirection: 'row', + marginBottom: 20, + backgroundColor: theme.colors.messageCard, + borderRadius: 8, + padding: 4, + }, + tab: { + flex: 1, + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 6, + marginHorizontal: 2, + }, + activeTab: { + backgroundColor: theme.colors.primary, + }, + inactiveTab: { + backgroundColor: 'transparent', + }, + tabText: { + color: theme.colors.textSecondary, + fontSize: 16, + textAlign: 'center', + }, + activeTabText: { + color: theme.colors.white, + fontWeight: '600', + }, + inputSection: { + marginBottom: 24, + }, + inputLabel: { + fontSize: 16, + color: theme.colors.text, + marginBottom: 8, + }, + input: { + backgroundColor: theme.colors.messageCard, + borderRadius: 8, + padding: 12, + color: theme.colors.text, + marginBottom: 8, // Reduced from 16 + borderWidth: 1, + borderColor: theme.colors.primary, // Add blue border like in the image + }, + actionButton: { + backgroundColor: theme.colors.primary, + padding: 12, + borderRadius: 8, + alignItems: 'center', + marginBottom: 12, + }, + actionButtonText: { + color: theme.colors.white, + fontSize: 16, + fontWeight: '500', + }, + closeButton: { + padding: 12, + borderRadius: 8, + alignItems: 'center', + backgroundColor: theme.colors.messageCard, + }, + closeButtonText: { + color: theme.colors.text, + fontSize: 16, + }, + profileInfo: { + marginVertical: 16, + }, + profileDetail: { + fontSize: 14, + color: theme.colors.textSecondary, // Using secondary text color for the gray appearance + marginBottom: 4, + lineHeight: 20, + }, + contactItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + marginBottom: 8, + }, + contactImage: { + width: 40, + height: 40, + borderRadius: 20, + marginRight: 12, + backgroundColor: theme.colors.messageCard, + }, + contactInfo: { + flex: 1, + }, + removeButton: { + backgroundColor: theme.colors.red, // Changed from 'error' to 'red' + padding: 8, + borderRadius: 6, + }, + removeButtonText: { + color: theme.colors.white, + fontSize: 12, + }, + separator: { + height: 1, + backgroundColor: theme.colors.background, + marginVertical: 8, + }, + }); + + // Add handler for Check address button + const handleCheckAddress = async () => { + if (nostrAddress) { + await refetch(); + } + }; + + const handleAddContact = () => { + if (!profile) return; + + console.log('Adding new contact with profile:', profile); + + const newContact: Contact = { + pubkey: nostrAddress, + displayName: profile.displayName || profile.name, + nip05: profile.nip05, + lud16: profile.lud16, + about: profile.about, + bio: profile.bio, + }; + + console.log('Created contact object:', newContact); + + try { + addContacts([newContact]); + console.log('Contact successfully added to storage'); + + showToast({ + type: 'success', + title: 'Contact added successfully', + }); + console.log('Toast shown, closing modal'); + + onClose(); + } catch (error) { + console.error('Error adding contact:', error); + showToast({ + type: 'error', + title: 'Failed to add contact', + }); + } + }; + + const renderProfileInfo = () => { + if (!nostrAddress || !profile) return null; + + return ( + + NIP05: {profile.nip05 || 'unrecognized'} + + Lightning address: {profile.lud16 || 'unrecognized'} + + + Name: {profile.displayName || profile.name || 'unrecognized'} + + About: {profile.about || 'unrecognized'} + Bio: {profile.bio || 'unrecognized'} + + ); + }; + + const renderAddNewContact = () => ( + + + Add a contact with Nostr address + + + + {renderProfileInfo()} + + + Check address + + + + Add contact + + + ); + + const handleRemoveContact = async (pubkey: string) => { + try { + await editContacts.mutateAsync( + {pubkey, type: 'remove'}, + { + onSuccess: () => { + queryClient.invalidateQueries({queryKey: ['contacts']}); + showToast({type: 'success', title: 'Contact removed successfully'}); + // Update local storage contacts + const updatedContacts = storedContacts.filter((c) => c.pubkey !== pubkey); + setStoredContacts(updatedContacts); + }, + }, + ); + } catch (error) { + showToast({type: 'error', title: 'Failed to remove contact'}); + } + }; + + const renderAllContacts = () => ( + + All contacts + {storedContacts.length === 0 ? ( + No contacts found + ) : ( + storedContacts.map((contact, index) => ( + + + + {contact.displayName || 'Unnamed Contact'} + + contact.pubkey && handleRemoveContact(contact.pubkey)} + > + Remove + + {index !== storedContacts.length - 1 && } + + )) + )} + + ); + + return ( + + + + + setActiveTab('all')} + > + + All contacts + + + setActiveTab('add')} + > + + Add new contact + + + + + {activeTab === 'all' ? renderAllContacts() : renderAddNewContact()} + + + Close + + + + + ); +}; \ No newline at end of file diff --git a/apps/mobile/src/modules/Contacts/styles.ts b/apps/mobile/src/modules/Contacts/styles.ts new file mode 100644 index 00000000..b50e2020 --- /dev/null +++ b/apps/mobile/src/modules/Contacts/styles.ts @@ -0,0 +1,165 @@ +import {Dimensions, StyleSheet} from 'react-native'; + +import {Theme} from '../../styles'; + +const {width} = Dimensions.get('window'); + +export default (theme: Theme) => + StyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, + contactItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + borderBottomWidth: 1, + borderBottomColor: theme.colors.divider, + }, + avatar: { + width: 40, + height: 40, + borderRadius: 20, + marginRight: 12, + }, + contactInfo: { + flex: 1, + marginVertical: 15, + }, + name: { + fontSize: 16, + fontWeight: 'bold', + color: theme.colors.text, + }, + pubkey: { + fontSize: 14, + color: theme.colors.textSecondary, + }, + menuContainer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: theme.colors.background, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + padding: 20, + maxHeight: '80%', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: -2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + menuOption: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + }, + menuText: { + marginLeft: 12, + fontSize: 16, + color: theme.colors.text, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + color: theme.colors.text, + marginBottom: 20, + }, + section: { + marginBottom: 20, + }, + sectionTitle: { + fontSize: 18, + color: theme.colors.text, + marginBottom: 10, + }, + buttonRow: { + flexDirection: 'row', + gap: 10, + marginBottom: 15, + }, + button: { + backgroundColor: theme.colors.surface, + padding: 10, + borderRadius: 8, + flex: 1, + }, + buttonText: { + color: theme.colors.text, + textAlign: 'center', + }, + input: { + backgroundColor: '#000', + borderRadius: 8, + padding: 12, + color: theme.colors.text, + marginBottom: 15, + }, + infoText: { + color: theme.colors.textSecondary, + marginBottom: 5, + }, + actionButton: { + backgroundColor: '#7C3AED', // Purple color from the screenshot + padding: 15, + borderRadius: 8, + marginBottom: 10, + alignItems: 'center', + }, + actionButtonText: { + color: '#FFFFFF', + fontWeight: '500', + }, + closeButton: { + backgroundColor: theme.colors.surface, + padding: 15, + borderRadius: 8, + alignItems: 'center', + marginTop: 5, + }, + closeButtonText: { + color: theme.colors.text, + }, + contactsList: { + maxHeight: 300, // Fixed height instead of percentage + marginBottom: 15, + }, + activeButton: { + backgroundColor: '#7C3AED', // Purple color for active tab + }, + activeButtonText: { + color: '#FFFFFF', + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + dialogContainer: { + width: width * 0.9, // 90% of screen width + maxHeight: '80%', + backgroundColor: theme.colors.background, + borderRadius: 16, + padding: 20, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + }); \ No newline at end of file diff --git a/apps/mobile/src/modules/DirectMessages/index.tsx b/apps/mobile/src/modules/DirectMessages/index.tsx index 2ddb9bf8..c07a00f4 100644 --- a/apps/mobile/src/modules/DirectMessages/index.tsx +++ b/apps/mobile/src/modules/DirectMessages/index.tsx @@ -3,10 +3,12 @@ import React, {useRef, useState} from 'react'; import {ActivityIndicator, FlatList, Pressable, Text, View} from 'react-native'; import {AddPostIcon} from '../../assets/icons'; +import {TabSelector} from '../../components'; import {Conversation as ConversationPreview, Modalize} from '../../components'; import {Chat} from '../../components/PrivateMessages/Chat'; import {FormPrivateMessage} from '../../components/PrivateMessages/FormPrivateMessage'; import {useStyles, useTheme} from '../../hooks'; +import {ContactList} from '../Contacts/ContactList'; import stylesheet from './styles'; export const DirectMessages: React.FC = () => { @@ -15,6 +17,7 @@ export const DirectMessages: React.FC = () => { const styles = useStyles(stylesheet); const [selectedConversation, setSelectedConversation] = useState(null); + const [activeTab, setActiveTab] = useState('messages'); const {data, isPending} = useIncomingMessageUsers(); @@ -76,6 +79,17 @@ export const DirectMessages: React.FC = () => { )} + + + + {activeTab === 'contacts' && setActiveTab('messages')} />} ); -}; +}; \ No newline at end of file diff --git a/apps/mobile/src/modules/DirectMessages/styles.ts b/apps/mobile/src/modules/DirectMessages/styles.ts index 21fd5de8..cc3f3c4c 100644 --- a/apps/mobile/src/modules/DirectMessages/styles.ts +++ b/apps/mobile/src/modules/DirectMessages/styles.ts @@ -19,4 +19,4 @@ export default ThemedStyleSheet((theme) => ({ bottom: Spacing.large, right: Spacing.pagePadding, }, -})); +})); \ No newline at end of file diff --git a/apps/mobile/src/types/tab.ts b/apps/mobile/src/types/tab.ts index 10d101ab..ee35203b 100644 --- a/apps/mobile/src/types/tab.ts +++ b/apps/mobile/src/types/tab.ts @@ -43,6 +43,7 @@ export enum SelectedTab { ONRAMP_OFFRAMP, PAY_WALLET, WALLET_INTERNAL, + CONTACTS = 'contacts', } export const TABS_TIP_LIST: {screen?: string; title: string; tab: SelectedTab}[] = [ @@ -288,6 +289,11 @@ export const TABS_CASHU: {screen?: string; title: string; tab: SelectedTab}[] = screen: 'History', tab: SelectedTab.CASHU_HISTORY, }, + { + title: 'Contacts', + screen: 'Contacts', + tab: SelectedTab.CONTACTS, + }, ]; export const TABS_WALLET: {screen?: string; title: string; tab: SelectedTab}[] = [ @@ -348,4 +354,4 @@ export const TABS_ONBOARDING_WALLET: {screen?: string; title: string; tab: Selec screen: 'Portfolio', tab: SelectedTab.PORTFOLIO, }, -]; +]; \ No newline at end of file