From d863faa55d849c42d74474e318a015fbda80dde0 Mon Sep 17 00:00:00 2001 From: abhay-raizada Date: Tue, 14 Jan 2025 20:05:25 +0530 Subject: [PATCH 1/8] Add Contacts component --- app/screens/nip17-chat/NIP17Chat.tsx | 133 +++++++++++++--------- app/screens/nip17-chat/contacts.tsx | 87 ++++++++++++++ app/screens/nip17-chat/searchListItem.tsx | 14 ++- app/utils/nostr.ts | 10 ++ 4 files changed, 188 insertions(+), 56 deletions(-) create mode 100644 app/screens/nip17-chat/contacts.tsx diff --git a/app/screens/nip17-chat/NIP17Chat.tsx b/app/screens/nip17-chat/NIP17Chat.tsx index 32845cfba2..73d5da1ef8 100644 --- a/app/screens/nip17-chat/NIP17Chat.tsx +++ b/app/screens/nip17-chat/NIP17Chat.tsx @@ -7,6 +7,7 @@ import { FlatList } from "react-native-gesture-handler" import Icon from "react-native-vector-icons/Ionicons" import { Screen } from "../../components/screen" +import { bytesToHex } from "@noble/hashes/utils" import { testProps } from "../../utils/testProps" import { useI18nContext } from "@app/i18n/i18n-react" @@ -27,6 +28,10 @@ import { useAppSelector } from "@app/store/redux" import { ImportNsecModal } from "./import-nsec" import { useIsAuthed } from "@app/graphql/is-authed-context" import { useHomeAuthedQuery } from "@app/graphql/generated" +import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs" +import Contacts from "./contacts" + +const Tab = createMaterialTopTabNavigator() export const NIP17Chat: React.FC = () => { const styles = useStyles() @@ -232,63 +237,81 @@ export const NIP17Chat: React.FC = () => { return ( {privateKey && !showImportModal ? ( - - {SearchBarContent} + + + {() => ( + + {SearchBarContent} - {searchText ? ( - ( - - )} - keyExtractor={(item) => item.id} - /> - ) : ( - - - Chats - - - signed in as:{" "} - - {userData?.username || nip19.npubEncode(getPublicKey(privateKey))} - - - { - return ( - ( + + )} + keyExtractor={(item) => item.id} + /> + ) : ( + + + Chats + + + signed in as:{" "} + + {userData?.username || nip19.npubEncode(getPublicKey(privateKey))} + + + { + return ( + + ) + }} + keyExtractor={(item) => item} /> - ) - }} - keyExtractor={(item) => item} - /> - - )} - + + )} + + )} + + + {() => ( + + + + )} + + ) : ( Loading your nostr keys... )} diff --git a/app/screens/nip17-chat/contacts.tsx b/app/screens/nip17-chat/contacts.tsx new file mode 100644 index 0000000000..f50c808182 --- /dev/null +++ b/app/screens/nip17-chat/contacts.tsx @@ -0,0 +1,87 @@ +// Contacts.tsx +import React, { useState } from "react" +import { Alert, FlatList, Text, View } from "react-native" +import { useStyles } from "./style" // Adjust the path as needed +import { bytesToHex } from "@noble/hashes/utils" +import { fetchContactList, fetchSecretFromLocalStorage } from "@app/utils/nostr" +import { useChatContext } from "./chatContext" +import { getPublicKey, nip19 } from "nostr-tools" +import { useFocusEffect, useNavigation } from "@react-navigation/native" +import { ListItem } from "@rneui/themed" +import { StackNavigationProp } from "@react-navigation/stack" +import { ChatStackParamList } from "@app/navigation/stack-param-lists" + +interface ContactsProps { + userPrivateKey: string +} + +const Contacts: React.FC = ({ userPrivateKey }) => { + const styles = useStyles() + const [contacts, setContacts] = useState([]) + const [initialized, setInitialized] = useState(false) + const { poolRef } = useChatContext() + const navigation = useNavigation>() + useFocusEffect( + React.useCallback(() => { + async function initialize() { + console.log("Initializing contacts") + let secretKeyString = await fetchSecretFromLocalStorage() + if (!secretKeyString) { + Alert.alert("Secret Key Not Found in Storage") + return + } + let secret = nip19.decode(secretKeyString).data as Uint8Array + let newContacts = + (await fetchContactList(getPublicKey(secret), poolRef!.current))?.tags + .filter((t: string[]) => t[0] === "p") + .map((t: string[]) => { + return { pubkey: t[1] } + }) || [] + console.log("contacts are", newContacts) + setContacts([...contacts, ...newContacts]) + setInitialized(true) + } + if (poolRef && !initialized) { + initialize() + } + }, []), + ) + return ( + + Contacts: {contacts.join(", ")} + No Contacts Available} + renderItem={({ item }) => ( + + navigation.navigate("messages", { + groupId: item.pubkey!, + userPrivateKey, + }) + } + > + + + + {" "} + {item.username || nip19.npubEncode(item.pubkey!)} + + + + + )} + keyExtractor={(item) => item.pubkey!} + /> + + ) +} + +export default Contacts diff --git a/app/screens/nip17-chat/searchListItem.tsx b/app/screens/nip17-chat/searchListItem.tsx index daa4066974..b71ab5c2a0 100644 --- a/app/screens/nip17-chat/searchListItem.tsx +++ b/app/screens/nip17-chat/searchListItem.tsx @@ -1,4 +1,4 @@ -import { ListItem, useTheme } from "@rneui/themed" +import { Icon, ListItem, useTheme } from "@rneui/themed" import { useStyles } from "./style" import { Image } from "react-native" import { useNavigation } from "@react-navigation/native" @@ -6,6 +6,7 @@ import { StackNavigationProp } from "@react-navigation/stack" import { ChatStackParamList } from "@app/navigation/stack-param-lists" import { nip19 } from "nostr-tools" import { bytesToHex } from "@noble/hashes/utils" +import { useChatContext } from "./chatContext" interface SearchListItemProps { item: Chat @@ -15,6 +16,7 @@ export const SearchListItem: React.FC = ({ item, userPrivateKey, }) => { + const { poolRef } = useChatContext() const styles = useStyles() const { theme: { colors }, @@ -49,6 +51,16 @@ export const SearchListItem: React.FC = ({ nip19.npubEncode(item.id)} + { + if (!poolRef) return + addToContactList(userPrivateKey, item.id, poolRef.current) + console.log("Add contact pressed for", item) + }} + /> ) } diff --git a/app/utils/nostr.ts b/app/utils/nostr.ts index 9daa37a0cd..632f623670 100644 --- a/app/utils/nostr.ts +++ b/app/utils/nostr.ts @@ -220,6 +220,16 @@ export const sendNIP4Message = async (message: string, recipient: string) => { let NIP4Messages = {} } +export const fetchContactList = async (userPubkey: string, pool: SimplePool) => { + let filter = { + kinds: [3], + authors: [userPubkey], + limit: 1, + } + let contactListEvent = await pool.querySync(["wss://relay.damus.io"], filter) + return contactListEvent[0] +} + export const setPreferredRelay = async (flashRelay: string, secretKey?: Uint8Array) => { let pool = new SimplePool() console.log("inside setpreferredRelay") From 94e2e4cd3e5cefe6dccec4870ca5d7831d1cf2c7 Mon Sep 17 00:00:00 2001 From: abhay-raizada Date: Tue, 14 Jan 2025 20:36:55 +0530 Subject: [PATCH 2/8] Working Contacts --- app/screens/nip17-chat/searchListItem.tsx | 1 + app/utils/nostr.ts | 25 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/app/screens/nip17-chat/searchListItem.tsx b/app/screens/nip17-chat/searchListItem.tsx index b71ab5c2a0..46b6b0a800 100644 --- a/app/screens/nip17-chat/searchListItem.tsx +++ b/app/screens/nip17-chat/searchListItem.tsx @@ -7,6 +7,7 @@ import { ChatStackParamList } from "@app/navigation/stack-param-lists" import { nip19 } from "nostr-tools" import { bytesToHex } from "@noble/hashes/utils" import { useChatContext } from "./chatContext" +import { addToContactList } from "@app/utils/nostr" interface SearchListItemProps { item: Chat diff --git a/app/utils/nostr.ts b/app/utils/nostr.ts index 632f623670..fb83751056 100644 --- a/app/utils/nostr.ts +++ b/app/utils/nostr.ts @@ -263,6 +263,31 @@ export const setPreferredRelay = async (flashRelay: string, secretKey?: Uint8Arr }, 5000) } +export const addToContactList = async ( + userPrivateKey: Uint8Array, + pubKeyToAdd: string, + pool: SimplePool, +) => { + console.log("adding contact") + const userPubkey = getPublicKey(userPrivateKey) + let contactListEvent = await fetchContactList(userPubkey, pool) + let tags = contactListEvent?.tags || [] + console.log("existing event? ", contactListEvent) + if (tags.map((t) => t[1]).includes(pubKeyToAdd)) return + tags.push(["p", pubKeyToAdd]) + let newEvent: UnsignedEvent = { + kind: 3, + pubkey: userPubkey, + content: contactListEvent?.content || "", + created_at: Math.floor(Date.now() / 1000), + tags: tags, + } + const finalNewEvent = finalizeEvent(newEvent, userPrivateKey) + console.log("final contact event is", finalNewEvent) + pool.publish(["wss://relay.damus.io"], finalNewEvent) + console.log("List Published!!") +} + export async function sendNip17Message( recipients: string[], message: string, From e99130124212a2552aec63511766ff523a485a49 Mon Sep 17 00:00:00 2001 From: abhay-raizada Date: Tue, 21 Jan 2025 18:32:44 +0530 Subject: [PATCH 3/8] Fix Styles --- app/screens/nip17-chat/NIP17Chat.tsx | 4 +- app/screens/nip17-chat/contacts.tsx | 73 +++++++++++++++++++++------- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/app/screens/nip17-chat/NIP17Chat.tsx b/app/screens/nip17-chat/NIP17Chat.tsx index 73d5da1ef8..21f714ac1e 100644 --- a/app/screens/nip17-chat/NIP17Chat.tsx +++ b/app/screens/nip17-chat/NIP17Chat.tsx @@ -245,7 +245,7 @@ export const NIP17Chat: React.FC = () => { > {() => ( - + {SearchBarContent} {searchText ? ( @@ -306,7 +306,7 @@ export const NIP17Chat: React.FC = () => { {() => ( - + )} diff --git a/app/screens/nip17-chat/contacts.tsx b/app/screens/nip17-chat/contacts.tsx index f50c808182..aab8711b54 100644 --- a/app/screens/nip17-chat/contacts.tsx +++ b/app/screens/nip17-chat/contacts.tsx @@ -1,15 +1,17 @@ // Contacts.tsx import React, { useState } from "react" -import { Alert, FlatList, Text, View } from "react-native" +import { Alert, FlatList, Text, View, Image } from "react-native" import { useStyles } from "./style" // Adjust the path as needed import { bytesToHex } from "@noble/hashes/utils" import { fetchContactList, fetchSecretFromLocalStorage } from "@app/utils/nostr" import { useChatContext } from "./chatContext" import { getPublicKey, nip19 } from "nostr-tools" import { useFocusEffect, useNavigation } from "@react-navigation/native" -import { ListItem } from "@rneui/themed" +import { ListItem, useTheme } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" import { ChatStackParamList } from "@app/navigation/stack-param-lists" +import ChatIcon from "@app/assets/icons/chat.svg" +import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" interface ContactsProps { userPrivateKey: string @@ -19,8 +21,10 @@ const Contacts: React.FC = ({ userPrivateKey }) => { const styles = useStyles() const [contacts, setContacts] = useState([]) const [initialized, setInitialized] = useState(false) - const { poolRef } = useChatContext() + const { poolRef, profileMap, addEventToProfiles } = useChatContext() const navigation = useNavigation>() + const { theme } = useTheme() + const colors = theme.colors useFocusEffect( React.useCallback(() => { async function initialize() { @@ -46,36 +50,71 @@ const Contacts: React.FC = ({ userPrivateKey }) => { } }, []), ) + const getContactMetadata = (contact: NostrProfile) => { + let profile = profileMap?.get(contact.pubkey || "") + return ( + profile?.nip05 || + profile?.name || + profile?.username || + nip19.npubEncode(contact.pubkey!).slice(0, 9) + ".." + ) + } return ( - Contacts: {contacts.join(", ")} No Contacts Available} renderItem={({ item }) => ( - - navigation.navigate("messages", { - groupId: item.pubkey!, - userPrivateKey, - }) - } - > - + + + {" "} - {item.username || nip19.npubEncode(item.pubkey!)} + {getContactMetadata(item)} + + navigation.navigate("messages", { + groupId: item.pubkey!, + userPrivateKey, + }) + } + /> + { + navigation.navigate("sendBitcoinDestination", { + username: profileMap?.get(item.pubkey!)?.lud16 || "", + }) + }} + key="lightning-button" + /> )} keyExtractor={(item) => item.pubkey!} From a1248b980ee368a3cbf5062459ef400fb222963f Mon Sep 17 00:00:00 2001 From: abhay-raizada Date: Thu, 6 Feb 2025 19:04:22 +0530 Subject: [PATCH 4/8] Add Search Bar Component --- app/screens/nip17-chat/NIP17Chat.tsx | 110 ++++------------ app/screens/nip17-chat/UserSearchBar.tsx | 131 +++++++++++++++++++ app/screens/nip17-chat/chatContext.tsx | 7 + app/screens/nip17-chat/contacts.tsx | 151 ++++++++++++---------- app/screens/nip17-chat/searchListItem.tsx | 26 +++- 5 files changed, 271 insertions(+), 154 deletions(-) create mode 100644 app/screens/nip17-chat/UserSearchBar.tsx diff --git a/app/screens/nip17-chat/NIP17Chat.tsx b/app/screens/nip17-chat/NIP17Chat.tsx index 21f714ac1e..6b8123492d 100644 --- a/app/screens/nip17-chat/NIP17Chat.tsx +++ b/app/screens/nip17-chat/NIP17Chat.tsx @@ -30,6 +30,7 @@ import { useIsAuthed } from "@app/graphql/is-authed-context" import { useHomeAuthedQuery } from "@app/graphql/generated" import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs" import Contacts from "./contacts" +import { UserSearchBar } from "./UserSearchBar" const Tab = createMaterialTopTabNavigator() @@ -47,8 +48,6 @@ export const NIP17Chat: React.FC = () => { errorPolicy: "all", }) const { rumors, poolRef, addEventToProfiles, profileMap, resetChat } = useChatContext() - const [searchText, setSearchText] = useState("") - const [refreshing, setRefreshing] = useState(false) const [initialized, setInitialized] = useState(false) const [searchedUsers, setSearchedUsers] = useState([]) const [privateKey, setPrivateKey] = useState() @@ -58,9 +57,7 @@ export const NIP17Chat: React.FC = () => { const { userData } = useAppSelector((state) => state.user) const reset = useCallback(() => { - setSearchText("") setSearchedUsers([]) - setRefreshing(false) setskipMismatchCheck(true) }, []) @@ -119,93 +116,36 @@ export const NIP17Chat: React.FC = () => { } } if (initialized) { - setSearchText("") setSearchedUsers([]) checkSecretKey() } - }, [setSearchText, setSearchedUsers, dataAuthed, isAuthed, skipMismatchCheck]), - ) - - const updateSearchResults = useCallback( - async (newSearchText: string) => { - const nip05Matching = async (alias: string) => { - let nostrUser = await nip05.queryProfile(alias.toLocaleLowerCase()) - console.log("nostr user for", alias, nostrUser) - if (nostrUser) { - let nostrProfile = profileMap?.get(nostrUser.pubkey) - let userPubkey = getPublicKey(privateKey!) - let participants = [nostrUser.pubkey, userPubkey] - setSearchedUsers([ - { - id: nostrUser.pubkey, - username: alias, - ...nostrProfile, - groupId: getGroupId(participants), - }, - ]) - if (!nostrProfile) - fetchNostrUsers([nostrUser.pubkey], poolRef!.current, searchedUsersHandler) - return true - } - return false - } - const aliasPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/ - if (!privateKey) { - Alert.alert("User Profile not yet loaded") - return - } - if (!newSearchText) { - setRefreshing(false) - } - setRefreshing(true) - setSearchText(newSearchText) - if (newSearchText.startsWith("npub1") && newSearchText.length == 63) { - let hexPubkey = nip19.decode(newSearchText).data as string - let userPubkey = getPublicKey(privateKey) - let participants = [hexPubkey, userPubkey] - setSearchedUsers([{ id: hexPubkey, groupId: getGroupId(participants) }]) - fetchNostrUsers([hexPubkey], poolRef!.current, searchedUsersHandler) - setRefreshing(false) - return - } else if (newSearchText.match(aliasPattern)) { - if (await nip05Matching(newSearchText)) { - setRefreshing(false) - return - } - } else if (!newSearchText.includes("@")) { - let modifiedSearchText = - newSearchText + "@" + appConfig.galoyInstance.lnAddressHostname - console.log("Searching for", modifiedSearchText) - if (await nip05Matching(modifiedSearchText)) { - setRefreshing(false) - return - } - } - }, - [privateKey], + }, [setSearchedUsers, dataAuthed, isAuthed, skipMismatchCheck]), ) let SearchBarContent: React.ReactNode let ListEmptyContent: React.ReactNode SearchBarContent = ( - } - clearIcon={ - - } - /> + <> + {/* } + clearIcon={ + + } + /> */} + + ) if (!initialized) { @@ -248,7 +188,7 @@ export const NIP17Chat: React.FC = () => { {SearchBarContent} - {searchText ? ( + {searchedUsers.length !== 0 ? ( { /> ) : ( - { }} > Chats - + */} void +} + +export const UserSearchBar: React.FC = ({ setSearchedUsers }) => { + const [searchText, setSearchText] = useState("") + const { rumors, poolRef, addEventToProfiles, profileMap, resetChat } = useChatContext() + const [refreshing, setRefreshing] = useState(false) + const [privateKey, setPrivateKey] = useState(null) + const styles = useStyles() + const { appConfig } = useAppConfig() + + const reset = useCallback(() => { + setSearchText("") + setSearchedUsers([]) + setRefreshing(false) + }, []) + + useEffect(() => { + const initialize = async () => { + let secretKeyString = await fetchSecretFromLocalStorage() + if (secretKeyString) setPrivateKey(nip19.decode(secretKeyString).data as Uint8Array) + } + initialize() + }, []) + + const { LL } = useI18nContext() + + const searchedUsersHandler = (event: Event, closer: SubCloser) => { + let nostrProfile = JSON.parse(event.content) + addEventToProfiles(event) + let userPubkey = getPublicKey(privateKey!) + let participants = [event.pubkey, userPubkey] + setSearchedUsers([ + { ...nostrProfile, id: event.pubkey, groupId: getGroupId(participants) }, + ]) + closer.close() + } + + const updateSearchResults = useCallback( + async (newSearchText: string) => { + const nip05Matching = async (alias: string) => { + let nostrUser = await nip05.queryProfile(alias.toLocaleLowerCase()) + console.log("nostr user for", alias, nostrUser) + if (nostrUser) { + let nostrProfile = profileMap?.get(nostrUser.pubkey) + let userPubkey = getPublicKey(privateKey!) + let participants = [nostrUser.pubkey, userPubkey] + setSearchedUsers([ + { + id: nostrUser.pubkey, + username: alias, + ...nostrProfile, + groupId: getGroupId(participants), + }, + ]) + if (!nostrProfile) + fetchNostrUsers([nostrUser.pubkey], poolRef!.current, searchedUsersHandler) + return true + } + return false + } + const aliasPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/ + if (!newSearchText) { + setRefreshing(false) + } + setRefreshing(true) + setSearchText(newSearchText) + if (newSearchText.startsWith("npub1") && newSearchText.length == 63) { + let hexPubkey = nip19.decode(newSearchText).data as string + let userPubkey = getPublicKey(privateKey!) + let participants = [hexPubkey, userPubkey] + setSearchedUsers([{ id: hexPubkey, groupId: getGroupId(participants) }]) + fetchNostrUsers([hexPubkey], poolRef!.current, searchedUsersHandler) + setRefreshing(false) + return + } else if (newSearchText.match(aliasPattern)) { + if (await nip05Matching(newSearchText)) { + setRefreshing(false) + return + } + } else if (!newSearchText.includes("@")) { + let modifiedSearchText = + newSearchText + "@" + appConfig.galoyInstance.lnAddressHostname + console.log("Searching for", modifiedSearchText) + if (await nip05Matching(modifiedSearchText)) { + setRefreshing(false) + return + } + } + }, + [privateKey], + ) + + return privateKey ? ( + } + clearIcon={ + + } + /> + ) : null +} diff --git a/app/screens/nip17-chat/chatContext.tsx b/app/screens/nip17-chat/chatContext.tsx index 6ad55eac3a..370421fb04 100644 --- a/app/screens/nip17-chat/chatContext.tsx +++ b/app/screens/nip17-chat/chatContext.tsx @@ -20,6 +20,8 @@ type ChatContextType = { profileMap: Map | undefined addEventToProfiles: (event: Event) => void resetChat: () => void + contacts: NostrProfile[] + setContacts: (c: NostrProfile[]) => void } const publicRelays = [ @@ -39,6 +41,8 @@ const ChatContext = createContext({ profileMap: undefined, addEventToProfiles: (event: Event) => {}, resetChat: () => {}, + contacts: [], + setContacts: (contacts) => [], }) export const useChatContext = () => useContext(ChatContext) @@ -51,6 +55,7 @@ export const ChatContextProvider: React.FC = ({ children }) = const profileMap = useRef>(new Map()) const poolRef = useRef(new SimplePool()) const processedEventIds = useRef(new Set()) + const [contacts, setContacts] = useState([]) const { appConfig: { galoyInstance: { relayUrl }, @@ -176,6 +181,8 @@ export const ChatContextProvider: React.FC = ({ children }) = profileMap: profileMap.current, addEventToProfiles, resetChat, + contacts, + setContacts, }} > {children} diff --git a/app/screens/nip17-chat/contacts.tsx b/app/screens/nip17-chat/contacts.tsx index aab8711b54..5e8c812b18 100644 --- a/app/screens/nip17-chat/contacts.tsx +++ b/app/screens/nip17-chat/contacts.tsx @@ -1,8 +1,7 @@ // Contacts.tsx import React, { useState } from "react" -import { Alert, FlatList, Text, View, Image } from "react-native" +import { Alert, FlatList, Text, View, Image, ActivityIndicator } from "react-native" import { useStyles } from "./style" // Adjust the path as needed -import { bytesToHex } from "@noble/hashes/utils" import { fetchContactList, fetchSecretFromLocalStorage } from "@app/utils/nostr" import { useChatContext } from "./chatContext" import { getPublicKey, nip19 } from "nostr-tools" @@ -12,6 +11,9 @@ import { StackNavigationProp } from "@react-navigation/stack" import { ChatStackParamList } from "@app/navigation/stack-param-lists" import ChatIcon from "@app/assets/icons/chat.svg" import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" +import { UserSearchBar } from "./UserSearchBar" +import { SearchListItem } from "./searchListItem" +import { hexToBytes } from "@noble/curves/abstract/utils" interface ContactsProps { userPrivateKey: string @@ -19,9 +21,8 @@ interface ContactsProps { const Contacts: React.FC = ({ userPrivateKey }) => { const styles = useStyles() - const [contacts, setContacts] = useState([]) - const [initialized, setInitialized] = useState(false) - const { poolRef, profileMap, addEventToProfiles } = useChatContext() + const [searchedUsers, setSearchedUsers] = useState([]) + const { poolRef, profileMap, contacts, setContacts } = useChatContext() const navigation = useNavigation>() const { theme } = useTheme() const colors = theme.colors @@ -42,10 +43,9 @@ const Contacts: React.FC = ({ userPrivateKey }) => { return { pubkey: t[1] } }) || [] console.log("contacts are", newContacts) - setContacts([...contacts, ...newContacts]) - setInitialized(true) + setContacts(newContacts) } - if (poolRef && !initialized) { + if (poolRef) { initialize() } }, []), @@ -59,66 +59,87 @@ const Contacts: React.FC = ({ userPrivateKey }) => { nip19.npubEncode(contact.pubkey!).slice(0, 9) + ".." ) } + let ListEmptyContent = ( + + + + ) return ( - No Contacts Available} - renderItem={({ item }) => ( - - - - - - {" "} - {getContactMetadata(item)} - - - - - navigation.navigate("messages", { - groupId: item.pubkey!, - userPrivateKey, - }) - } - /> - { - navigation.navigate("sendBitcoinDestination", { - username: profileMap?.get(item.pubkey!)?.lud16 || "", - }) - }} - key="lightning-button" + + {searchedUsers.length !== 0 ? ( + ( + - - )} - keyExtractor={(item) => item.pubkey!} - /> + )} + keyExtractor={(item) => item.id} + /> + ) : ( + No Contacts Available} + renderItem={({ item }) => ( + + + + + + {" "} + {getContactMetadata(item)} + + + + + navigation.navigate("messages", { + groupId: item.pubkey!, + userPrivateKey, + }) + } + /> + { + navigation.navigate("sendBitcoinDestination", { + username: profileMap?.get(item.pubkey!)?.lud16 || "", + }) + }} + key="lightning-button" + /> + + )} + keyExtractor={(item) => item.pubkey!} + /> + )} ) } diff --git a/app/screens/nip17-chat/searchListItem.tsx b/app/screens/nip17-chat/searchListItem.tsx index 46b6b0a800..5828edca7a 100644 --- a/app/screens/nip17-chat/searchListItem.tsx +++ b/app/screens/nip17-chat/searchListItem.tsx @@ -1,13 +1,14 @@ -import { Icon, ListItem, useTheme } from "@rneui/themed" +import { ListItem, useTheme } from "@rneui/themed" import { useStyles } from "./style" import { Image } from "react-native" import { useNavigation } from "@react-navigation/native" import { StackNavigationProp } from "@react-navigation/stack" import { ChatStackParamList } from "@app/navigation/stack-param-lists" -import { nip19 } from "nostr-tools" +import { getPublicKey, nip19 } from "nostr-tools" import { bytesToHex } from "@noble/hashes/utils" import { useChatContext } from "./chatContext" import { addToContactList } from "@app/utils/nostr" +import Icon from "react-native-vector-icons/Ionicons" interface SearchListItemProps { item: Chat @@ -17,12 +18,28 @@ export const SearchListItem: React.FC = ({ item, userPrivateKey, }) => { - const { poolRef } = useChatContext() + const { poolRef, contacts } = useChatContext() const styles = useStyles() const { theme: { colors }, } = useTheme() const navigation = useNavigation>() + const tabNavigation = useNavigation() + + const getIcon = () => { + let itemPubkey = item.groupId + .split(",") + .filter((p) => p !== getPublicKey(userPrivateKey))[0] + console.log( + "item pubkey is", + itemPubkey, + contacts.filter((c) => c.pubkey! === itemPubkey).length, + contacts, + ) + return contacts.filter((c) => c.pubkey! === itemPubkey).length === 0 + ? "person-add" + : "checkmark-outline" + } return ( = ({ { if (!poolRef) return addToContactList(userPrivateKey, item.id, poolRef.current) console.log("Add contact pressed for", item) + setTimeout(() => tabNavigation.navigate("Contacts"), 500) }} /> From 74e084b5d6f4d60d960d8006c81321dd1ee11207 Mon Sep 17 00:00:00 2001 From: abhay-raizada Date: Tue, 11 Feb 2025 18:02:52 +0530 Subject: [PATCH 5/8] Working Search List Item --- app/screens/nip17-chat/NIP17Chat.tsx | 67 ++++++----------------- app/screens/nip17-chat/UserSearchBar.tsx | 3 +- app/screens/nip17-chat/chatContext.tsx | 20 ++++--- app/screens/nip17-chat/contacts.tsx | 41 ++++---------- app/screens/nip17-chat/searchListItem.tsx | 50 ++++++++++------- app/screens/nip17-chat/style.ts | 2 +- app/screens/nip17-chat/utils.ts | 9 +++ app/utils/nostr.ts | 27 +++++---- 8 files changed, 98 insertions(+), 121 deletions(-) diff --git a/app/screens/nip17-chat/NIP17Chat.tsx b/app/screens/nip17-chat/NIP17Chat.tsx index 6b8123492d..66b73e4443 100644 --- a/app/screens/nip17-chat/NIP17Chat.tsx +++ b/app/screens/nip17-chat/NIP17Chat.tsx @@ -1,4 +1,3 @@ -import { SearchBar } from "@rneui/base" import { useTheme } from "@rneui/themed" import * as React from "react" import { useCallback, useState } from "react" @@ -11,12 +10,11 @@ import { bytesToHex } from "@noble/hashes/utils" import { testProps } from "../../utils/testProps" import { useI18nContext } from "@app/i18n/i18n-react" -import { Event, SubCloser, getPublicKey, nip05, nip19 } from "nostr-tools" +import { getPublicKey, nip19 } from "nostr-tools" import { convertRumorsToGroups, fetchNostrUsers, fetchSecretFromLocalStorage, - getGroupId, } from "@app/utils/nostr" import { useStyles } from "./style" import { SearchListItem } from "./searchListItem" @@ -36,7 +34,6 @@ const Tab = createMaterialTopTabNavigator() export const NIP17Chat: React.FC = () => { const styles = useStyles() - const { appConfig } = useAppConfig() const { theme: { colors }, } = useTheme() @@ -56,22 +53,6 @@ export const NIP17Chat: React.FC = () => { const { LL } = useI18nContext() const { userData } = useAppSelector((state) => state.user) - const reset = useCallback(() => { - setSearchedUsers([]) - setskipMismatchCheck(true) - }, []) - - const searchedUsersHandler = (event: Event, closer: SubCloser) => { - let nostrProfile = JSON.parse(event.content) - addEventToProfiles(event) - let userPubkey = getPublicKey(privateKey!) - let participants = [event.pubkey, userPubkey] - setSearchedUsers([ - { ...nostrProfile, id: event.pubkey, groupId: getGroupId(participants) }, - ]) - closer.close() - } - React.useEffect(() => { const unsubscribe = () => { console.log("unsubscribing") @@ -127,23 +108,6 @@ export const NIP17Chat: React.FC = () => { SearchBarContent = ( <> - {/* } - clearIcon={ - - } - /> */} ) @@ -178,10 +142,23 @@ export const NIP17Chat: React.FC = () => { {privateKey && !showImportModal ? ( ({ + // tabBarLabelStyle: { fontSize: 18, fontWeight: "600" }, + // tabBarIndicatorStyle: { backgroundColor: "#60aa55" }, tabBarIndicatorStyle: { backgroundColor: "#60aa55" }, - }} + tabBarIcon: ({ color }) => { + let iconName: string + if (route.name === "Chats") { + iconName = "chatbubble-ellipses-outline" // Chat icon + } else if (route.name === "Contacts") { + iconName = "people-outline" // Contacts icon + } else { + iconName = "" + } + return + }, + tabBarShowLabel: false, // Hide text labels + })} > {() => ( @@ -200,16 +177,6 @@ export const NIP17Chat: React.FC = () => { /> ) : ( - {/* - Chats - */} = ({ setSearchedUsers }) => { const [searchText, setSearchText] = useState("") - const { rumors, poolRef, addEventToProfiles, profileMap, resetChat } = useChatContext() + const { rumors, poolRef, addEventToProfiles, profileMap } = useChatContext() const [refreshing, setRefreshing] = useState(false) const [privateKey, setPrivateKey] = useState(null) const styles = useStyles() @@ -56,6 +56,7 @@ export const UserSearchBar: React.FC = ({ setSearchedUsers } const updateSearchResults = useCallback( async (newSearchText: string) => { + if (newSearchText === "") reset() const nip05Matching = async (alias: string) => { let nostrUser = await nip05.queryProfile(alias.toLocaleLowerCase()) console.log("nostr user for", alias, nostrUser) diff --git a/app/screens/nip17-chat/chatContext.tsx b/app/screens/nip17-chat/chatContext.tsx index 370421fb04..05bf11ef6c 100644 --- a/app/screens/nip17-chat/chatContext.tsx +++ b/app/screens/nip17-chat/chatContext.tsx @@ -1,6 +1,7 @@ import { useAppConfig } from "@app/hooks" import { Rumor, + fetchContactList, fetchGiftWrapsForPublicKey, fetchSecretFromLocalStorage, getRumorFromWrap, @@ -20,8 +21,8 @@ type ChatContextType = { profileMap: Map | undefined addEventToProfiles: (event: Event) => void resetChat: () => void - contacts: NostrProfile[] - setContacts: (c: NostrProfile[]) => void + contactsEvent: Event | undefined + setContactsEvent: (e: Event) => void } const publicRelays = [ @@ -41,8 +42,8 @@ const ChatContext = createContext({ profileMap: undefined, addEventToProfiles: (event: Event) => {}, resetChat: () => {}, - contacts: [], - setContacts: (contacts) => [], + contactsEvent: undefined, + setContactsEvent: (event: Event) => {}, }) export const useChatContext = () => useContext(ChatContext) @@ -55,7 +56,7 @@ export const ChatContextProvider: React.FC = ({ children }) = const profileMap = useRef>(new Map()) const poolRef = useRef(new SimplePool()) const processedEventIds = useRef(new Set()) - const [contacts, setContacts] = useState([]) + const [contactsEvent, setContactsEvent] = useState() const { appConfig: { galoyInstance: { relayUrl }, @@ -95,6 +96,11 @@ export const ChatContextProvider: React.FC = ({ children }) = let cachedRumors = cachedGiftwraps.map((wrap) => getRumorFromWrap(wrap, secret)) setRumors(cachedRumors) let closer = await fetchNewGiftwraps(cachedGiftwraps, publicKey) + + fetchContactList(getPublicKey(secret), poolRef!.current, (event: Event) => { + console.log("NEW CONTACTS EVENT IS", event) + setContactsEvent(event) + }) setCloser(closer) } if (poolRef && !closer) initialize() @@ -181,8 +187,8 @@ export const ChatContextProvider: React.FC = ({ children }) = profileMap: profileMap.current, addEventToProfiles, resetChat, - contacts, - setContacts, + contactsEvent, + setContactsEvent, }} > {children} diff --git a/app/screens/nip17-chat/contacts.tsx b/app/screens/nip17-chat/contacts.tsx index 5e8c812b18..399b6ef795 100644 --- a/app/screens/nip17-chat/contacts.tsx +++ b/app/screens/nip17-chat/contacts.tsx @@ -1,11 +1,10 @@ // Contacts.tsx import React, { useState } from "react" -import { Alert, FlatList, Text, View, Image, ActivityIndicator } from "react-native" +import { FlatList, Text, View, Image, ActivityIndicator } from "react-native" import { useStyles } from "./style" // Adjust the path as needed -import { fetchContactList, fetchSecretFromLocalStorage } from "@app/utils/nostr" import { useChatContext } from "./chatContext" -import { getPublicKey, nip19 } from "nostr-tools" -import { useFocusEffect, useNavigation } from "@react-navigation/native" +import { nip19 } from "nostr-tools" +import { useNavigation } from "@react-navigation/native" import { ListItem, useTheme } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" import { ChatStackParamList } from "@app/navigation/stack-param-lists" @@ -14,6 +13,7 @@ import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" import { UserSearchBar } from "./UserSearchBar" import { SearchListItem } from "./searchListItem" import { hexToBytes } from "@noble/curves/abstract/utils" +import { getContactsFromEvent } from "./utils" interface ContactsProps { userPrivateKey: string @@ -22,34 +22,11 @@ interface ContactsProps { const Contacts: React.FC = ({ userPrivateKey }) => { const styles = useStyles() const [searchedUsers, setSearchedUsers] = useState([]) - const { poolRef, profileMap, contacts, setContacts } = useChatContext() + const { poolRef, profileMap, contactsEvent } = useChatContext() const navigation = useNavigation>() const { theme } = useTheme() const colors = theme.colors - useFocusEffect( - React.useCallback(() => { - async function initialize() { - console.log("Initializing contacts") - let secretKeyString = await fetchSecretFromLocalStorage() - if (!secretKeyString) { - Alert.alert("Secret Key Not Found in Storage") - return - } - let secret = nip19.decode(secretKeyString).data as Uint8Array - let newContacts = - (await fetchContactList(getPublicKey(secret), poolRef!.current))?.tags - .filter((t: string[]) => t[0] === "p") - .map((t: string[]) => { - return { pubkey: t[1] } - }) || [] - console.log("contacts are", newContacts) - setContacts(newContacts) - } - if (poolRef) { - initialize() - } - }, []), - ) + const getContactMetadata = (contact: NostrProfile) => { let profile = profileMap?.get(contact.pubkey || "") return ( @@ -80,10 +57,10 @@ const Contacts: React.FC = ({ userPrivateKey }) => { )} keyExtractor={(item) => item.id} /> - ) : ( + ) : contactsEvent ? ( No Contacts Available} renderItem={({ item }) => ( @@ -139,6 +116,8 @@ const Contacts: React.FC = ({ userPrivateKey }) => { )} keyExtractor={(item) => item.pubkey!} /> + ) : ( + Loading... )} ) diff --git a/app/screens/nip17-chat/searchListItem.tsx b/app/screens/nip17-chat/searchListItem.tsx index 5828edca7a..f06ddbafba 100644 --- a/app/screens/nip17-chat/searchListItem.tsx +++ b/app/screens/nip17-chat/searchListItem.tsx @@ -9,6 +9,8 @@ import { bytesToHex } from "@noble/hashes/utils" import { useChatContext } from "./chatContext" import { addToContactList } from "@app/utils/nostr" import Icon from "react-native-vector-icons/Ionicons" +import { getContactsFromEvent } from "./utils" +import { useEffect } from "react" interface SearchListItemProps { item: Chat @@ -18,7 +20,14 @@ export const SearchListItem: React.FC = ({ item, userPrivateKey, }) => { - const { poolRef, contacts } = useChatContext() + const { poolRef, contactsEvent } = useChatContext() + + const isUserAdded = () => { + if (!contactsEvent) return false + let existingContacts = getContactsFromEvent(contactsEvent) + return existingContacts.map((p: NostrProfile) => p.pubkey).includes(item.id) + } + const styles = useStyles() const { theme: { colors }, @@ -30,13 +39,9 @@ export const SearchListItem: React.FC = ({ let itemPubkey = item.groupId .split(",") .filter((p) => p !== getPublicKey(userPrivateKey))[0] - console.log( - "item pubkey is", - itemPubkey, - contacts.filter((c) => c.pubkey! === itemPubkey).length, - contacts, - ) - return contacts.filter((c) => c.pubkey! === itemPubkey).length === 0 + if (!contactsEvent) return "alert-circle-outline" + return getContactsFromEvent(contactsEvent).filter((c) => c.pubkey! === itemPubkey) + .length === 0 ? "person-add" : "checkmark-outline" } @@ -69,17 +74,24 @@ export const SearchListItem: React.FC = ({ nip19.npubEncode(item.id)} - { - if (!poolRef) return - addToContactList(userPrivateKey, item.id, poolRef.current) - console.log("Add contact pressed for", item) - setTimeout(() => tabNavigation.navigate("Contacts"), 500) - }} - /> + {contactsEvent ? ( + { + if (!isUserAdded()) return false + if (!poolRef) return + await addToContactList( + userPrivateKey, + item.id, + contactsEvent, + poolRef.current, + ) + }} + /> + ) : null} ) } diff --git a/app/screens/nip17-chat/style.ts b/app/screens/nip17-chat/style.ts index 14fb0931a9..5f7e68b106 100644 --- a/app/screens/nip17-chat/style.ts +++ b/app/screens/nip17-chat/style.ts @@ -65,7 +65,7 @@ export const useStyles = makeStyles(({ colors }) => ({ backgroundColor: colors.white, borderBottomColor: colors.white, borderTopColor: colors.white, - marginHorizontal: 26, + marginHorizontal: 12, marginVertical: 8, }, diff --git a/app/screens/nip17-chat/utils.ts b/app/screens/nip17-chat/utils.ts index 608a103c66..904460e50c 100644 --- a/app/screens/nip17-chat/utils.ts +++ b/app/screens/nip17-chat/utils.ts @@ -1,5 +1,6 @@ import { Rumor } from "@app/utils/nostr" import AsyncStorage from "@react-native-async-storage/async-storage" +import { Event } from "nostr-tools" export const updateLastSeen = async (groupId: string, timestamp: number) => { try { @@ -19,6 +20,14 @@ export const getLastSeen = async (groupId: string) => { } } +export const getContactsFromEvent = (event: Event) => { + return event.tags + .filter((t) => t[0] === "p") + .map((t) => { + return { pubkey: t[1] } + }) +} + export const getAllLastSeen = async () => { try { const keys = await AsyncStorage.getAllKeys() diff --git a/app/utils/nostr.ts b/app/utils/nostr.ts index fb83751056..4badb7b163 100644 --- a/app/utils/nostr.ts +++ b/app/utils/nostr.ts @@ -1,4 +1,5 @@ import { useAppConfig } from "@app/hooks" +import { getContactsFromEvent } from "@app/screens/nip17-chat/utils" import { bytesToHex } from "@noble/curves/abstract/utils" import AsyncStorage from "@react-native-async-storage/async-storage" import { @@ -16,6 +17,7 @@ import { SubCloser, AbstractRelay, } from "nostr-tools" +import { VoidFunctionComponent } from "react" import { Alert } from "react-native" import * as Keychain from "react-native-keychain" @@ -220,14 +222,18 @@ export const sendNIP4Message = async (message: string, recipient: string) => { let NIP4Messages = {} } -export const fetchContactList = async (userPubkey: string, pool: SimplePool) => { +export const fetchContactList = async ( + userPubkey: string, + pool: SimplePool, + onEvent: (event: Event) => void, +) => { let filter = { kinds: [3], authors: [userPubkey], - limit: 1, } - let contactListEvent = await pool.querySync(["wss://relay.damus.io"], filter) - return contactListEvent[0] + pool.subscribeMany(["wss://relay.damus.io"], [filter], { + onevent: onEvent, + }) } export const setPreferredRelay = async (flashRelay: string, secretKey?: Uint8Array) => { @@ -266,26 +272,23 @@ export const setPreferredRelay = async (flashRelay: string, secretKey?: Uint8Arr export const addToContactList = async ( userPrivateKey: Uint8Array, pubKeyToAdd: string, + contactsEvent: Event, pool: SimplePool, ) => { - console.log("adding contact") const userPubkey = getPublicKey(userPrivateKey) - let contactListEvent = await fetchContactList(userPubkey, pool) - let tags = contactListEvent?.tags || [] - console.log("existing event? ", contactListEvent) - if (tags.map((t) => t[1]).includes(pubKeyToAdd)) return + let existingContacts = getContactsFromEvent(contactsEvent) + let tags = contactsEvent.tags + if (existingContacts.map((p: NostrProfile) => p.pubkey).includes(pubKeyToAdd)) return tags.push(["p", pubKeyToAdd]) let newEvent: UnsignedEvent = { kind: 3, pubkey: userPubkey, - content: contactListEvent?.content || "", + content: contactsEvent.content || "", created_at: Math.floor(Date.now() / 1000), tags: tags, } const finalNewEvent = finalizeEvent(newEvent, userPrivateKey) - console.log("final contact event is", finalNewEvent) pool.publish(["wss://relay.damus.io"], finalNewEvent) - console.log("List Published!!") } export async function sendNip17Message( From e0d9e38498070e4a37e5a6632faf1d464fe96639 Mon Sep 17 00:00:00 2001 From: Abhay Date: Tue, 18 Feb 2025 20:06:43 +0530 Subject: [PATCH 6/8] Work for new users --- app/screens/nip17-chat/searchListItem.tsx | 45 ++++++++++------------- app/utils/nostr.ts | 10 +++-- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/app/screens/nip17-chat/searchListItem.tsx b/app/screens/nip17-chat/searchListItem.tsx index f06ddbafba..0c8506f4a6 100644 --- a/app/screens/nip17-chat/searchListItem.tsx +++ b/app/screens/nip17-chat/searchListItem.tsx @@ -10,7 +10,6 @@ import { useChatContext } from "./chatContext" import { addToContactList } from "@app/utils/nostr" import Icon from "react-native-vector-icons/Ionicons" import { getContactsFromEvent } from "./utils" -import { useEffect } from "react" interface SearchListItemProps { item: Chat @@ -33,17 +32,17 @@ export const SearchListItem: React.FC = ({ theme: { colors }, } = useTheme() const navigation = useNavigation>() - const tabNavigation = useNavigation() const getIcon = () => { let itemPubkey = item.groupId .split(",") .filter((p) => p !== getPublicKey(userPrivateKey))[0] - if (!contactsEvent) return "alert-circle-outline" - return getContactsFromEvent(contactsEvent).filter((c) => c.pubkey! === itemPubkey) - .length === 0 - ? "person-add" - : "checkmark-outline" + if (contactsEvent) + return getContactsFromEvent(contactsEvent).filter((c) => c.pubkey! === itemPubkey) + .length === 0 + ? "person-add" + : "checkmark-outline" + else return "person-add" } return ( = ({ nip19.npubEncode(item.id)} - {contactsEvent ? ( - { - if (!isUserAdded()) return false - if (!poolRef) return - await addToContactList( - userPrivateKey, - item.id, - contactsEvent, - poolRef.current, - ) - }} - /> - ) : null} + + { + console.log("Trying to add user to contact list") + if (isUserAdded()) return false + if (!poolRef) return + await addToContactList(userPrivateKey, item.id, poolRef.current, contactsEvent) + console.log("probably added user to contact list") + }} + /> ) } diff --git a/app/utils/nostr.ts b/app/utils/nostr.ts index 4badb7b163..484b970d53 100644 --- a/app/utils/nostr.ts +++ b/app/utils/nostr.ts @@ -272,18 +272,20 @@ export const setPreferredRelay = async (flashRelay: string, secretKey?: Uint8Arr export const addToContactList = async ( userPrivateKey: Uint8Array, pubKeyToAdd: string, - contactsEvent: Event, pool: SimplePool, + contactsEvent?: Event, ) => { const userPubkey = getPublicKey(userPrivateKey) - let existingContacts = getContactsFromEvent(contactsEvent) - let tags = contactsEvent.tags + let existingContacts: NostrProfile[] + if (contactsEvent) existingContacts = getContactsFromEvent(contactsEvent) + else existingContacts = [] + let tags = contactsEvent?.tags || [] if (existingContacts.map((p: NostrProfile) => p.pubkey).includes(pubKeyToAdd)) return tags.push(["p", pubKeyToAdd]) let newEvent: UnsignedEvent = { kind: 3, pubkey: userPubkey, - content: contactsEvent.content || "", + content: contactsEvent?.content || "", created_at: Math.floor(Date.now() / 1000), tags: tags, } From f39bcc67a363b18f19baa144d6f5ab39c79d037a Mon Sep 17 00:00:00 2001 From: Abhay Date: Tue, 25 Feb 2025 18:24:44 +0530 Subject: [PATCH 7/8] Move Contact Card out of Contacts --- app/screens/nip17-chat/contactCard.tsx | 61 +++++++++++ app/screens/nip17-chat/contacts.tsx | 145 ++++++++++++++++--------- app/screens/nip17-chat/style.ts | 2 + app/utils/nostr.ts | 3 +- 4 files changed, 156 insertions(+), 55 deletions(-) create mode 100644 app/screens/nip17-chat/contactCard.tsx diff --git a/app/screens/nip17-chat/contactCard.tsx b/app/screens/nip17-chat/contactCard.tsx new file mode 100644 index 0000000000..d73db5ff66 --- /dev/null +++ b/app/screens/nip17-chat/contactCard.tsx @@ -0,0 +1,61 @@ +// ContactCard.tsx +import React from "react" +import { Image, Text, View } from "react-native" +import { ListItem } from "@rneui/themed" +import { useTheme } from "@rneui/themed" +import { nip19 } from "nostr-tools" +import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" +import ChatIcon from "@app/assets/icons/chat.svg" +import { hexToBytes } from "@noble/curves/abstract/utils" + +interface ContactCardProps { + item: any + profileMap?: Map + style?: Object + containerStyle?: Object +} + +const ContactCard: React.FC = ({ + item, + profileMap, + style, + containerStyle, +}) => { + const { theme } = useTheme() + const colors = theme.colors + + const getContactMetadata = (contact: any) => { + let profile = profileMap?.get(contact.pubkey || "") + return ( + profile?.nip05 || + profile?.name || + profile?.username || + nip19.npubEncode(contact.pubkey!).slice(0, 9) + ".." + ) + } + + return ( + + + + + {getContactMetadata(item)} + + + + ) +} + +export default ContactCard diff --git a/app/screens/nip17-chat/contacts.tsx b/app/screens/nip17-chat/contacts.tsx index 399b6ef795..a2fdb0979c 100644 --- a/app/screens/nip17-chat/contacts.tsx +++ b/app/screens/nip17-chat/contacts.tsx @@ -1,9 +1,9 @@ // Contacts.tsx -import React, { useState } from "react" +import React, { useEffect, useState } from "react" import { FlatList, Text, View, Image, ActivityIndicator } from "react-native" import { useStyles } from "./style" // Adjust the path as needed import { useChatContext } from "./chatContext" -import { nip19 } from "nostr-tools" +import { nip19, Event } from "nostr-tools" import { useNavigation } from "@react-navigation/native" import { ListItem, useTheme } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" @@ -14,6 +14,11 @@ import { UserSearchBar } from "./UserSearchBar" import { SearchListItem } from "./searchListItem" import { hexToBytes } from "@noble/curves/abstract/utils" import { getContactsFromEvent } from "./utils" +import ContactCard from "./contactCard" +import { Swipeable } from "react-native-gesture-handler" +import { fetchNostrUsers, publicRelays } from "@app/utils/nostr" +import Icon from "react-native-vector-icons/Ionicons" +import { bytesToHex } from "@noble/hashes/utils" interface ContactsProps { userPrivateKey: string @@ -22,7 +27,7 @@ interface ContactsProps { const Contacts: React.FC = ({ userPrivateKey }) => { const styles = useStyles() const [searchedUsers, setSearchedUsers] = useState([]) - const { poolRef, profileMap, contactsEvent } = useChatContext() + const { poolRef, profileMap, contactsEvent, addEventToProfiles } = useChatContext() const navigation = useNavigation>() const { theme } = useTheme() const colors = theme.colors @@ -36,11 +41,84 @@ const Contacts: React.FC = ({ userPrivateKey }) => { nip19.npubEncode(contact.pubkey!).slice(0, 9) + ".." ) } + + const handleUnfollow = (contactPubkey: string) => { + if (!poolRef || !contactsEvent) return + console.log("unfollowing pubkey", contactPubkey) + let profiles = contactsEvent.tags.filter((p) => p[0] === "p").map((p) => p[1]) + let tagsWithoutProfiles = contactsEvent.tags.filter((p) => p[0] !== "p") + let newProfiles = profiles.filter((p) => p[1] !== contactPubkey) + let newContactsEvent: Event = { + ...contactsEvent, + tags: [...tagsWithoutProfiles, newProfiles], + } + console.log( + "Old Contacts event vs NewContacts Event", + contactPubkey, + contactsEvent, + newContactsEvent, + ) + poolRef.current.publish(publicRelays, newContactsEvent) + } + + useEffect(() => { + if (!poolRef) return + let contactPubkeys = + contactsEvent?.tags.filter((p) => p[0] === "p").map((p) => p[1]) || [] + let closer = fetchNostrUsers(contactPubkeys, poolRef.current, (event: Event) => { + addEventToProfiles(event) + }) + return () => { + if (closer) closer.close() + } + }, [poolRef, contactsEvent]) + let ListEmptyContent = ( ) + + const renderRightActions = (item: { pubkey: string }) => { + return ( + + { + handleUnfollow(item.pubkey) + }} + /> + { + navigation.navigate("sendBitcoinDestination", { + username: profileMap?.get(item.pubkey)?.lud16 || "", + }) + }} + /> + { + navigation.navigate("messages", { + groupId: item.pubkey, + userPrivateKey: userPrivateKey, + }) + }} + style={{ margin: 10 }} + fontSize={20} + /> + + ) + } return ( @@ -59,60 +137,21 @@ const Contacts: React.FC = ({ userPrivateKey }) => { /> ) : contactsEvent ? ( No Contacts Available} renderItem={({ item }) => ( - - - - - - {" "} - {getContactMetadata(item)} - - - - - navigation.navigate("messages", { - groupId: item.pubkey!, - userPrivateKey, - }) - } - /> - { - navigation.navigate("sendBitcoinDestination", { - username: profileMap?.get(item.pubkey!)?.lud16 || "", - }) - }} - key="lightning-button" + renderRightActions(item)} + containerStyle={styles.itemContainer} + > + - + )} keyExtractor={(item) => item.pubkey!} /> diff --git a/app/screens/nip17-chat/style.ts b/app/screens/nip17-chat/style.ts index 5f7e68b106..9e5f13c44b 100644 --- a/app/screens/nip17-chat/style.ts +++ b/app/screens/nip17-chat/style.ts @@ -57,6 +57,8 @@ export const useStyles = makeStyles(({ colors }) => ({ itemContainer: { borderRadius: 8, backgroundColor: colors.grey5, + minHeight: 50, + margin: 7, }, listContainer: { flexGrow: 1 }, diff --git a/app/utils/nostr.ts b/app/utils/nostr.ts index 484b970d53..d0544d5b4f 100644 --- a/app/utils/nostr.ts +++ b/app/utils/nostr.ts @@ -17,12 +17,11 @@ import { SubCloser, AbstractRelay, } from "nostr-tools" -import { VoidFunctionComponent } from "react" import { Alert } from "react-native" import * as Keychain from "react-native-keychain" -let publicRelays = [ +export const publicRelays = [ "wss://relay.damus.io", "wss://relay.primal.net", "wss://relay.snort.social", From d4d7abad759c501a7b717339677e1765741c9567 Mon Sep 17 00:00:00 2001 From: abhay-raizada Date: Tue, 25 Feb 2025 19:18:54 +0530 Subject: [PATCH 8/8] Complete Contacts --- app/screens/nip17-chat/contacts.tsx | 20 +++++++++++++------- app/screens/nip17-chat/utils.ts | 9 +++++++++ app/utils/nostr.ts | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/screens/nip17-chat/contacts.tsx b/app/screens/nip17-chat/contacts.tsx index a2fdb0979c..a48037725e 100644 --- a/app/screens/nip17-chat/contacts.tsx +++ b/app/screens/nip17-chat/contacts.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react" import { FlatList, Text, View, Image, ActivityIndicator } from "react-native" import { useStyles } from "./style" // Adjust the path as needed import { useChatContext } from "./chatContext" -import { nip19, Event } from "nostr-tools" +import { nip19, Event, finalizeEvent } from "nostr-tools" import { useNavigation } from "@react-navigation/native" import { ListItem, useTheme } from "@rneui/themed" import { StackNavigationProp } from "@react-navigation/stack" @@ -16,7 +16,7 @@ import { hexToBytes } from "@noble/curves/abstract/utils" import { getContactsFromEvent } from "./utils" import ContactCard from "./contactCard" import { Swipeable } from "react-native-gesture-handler" -import { fetchNostrUsers, publicRelays } from "@app/utils/nostr" +import { customPublish, fetchNostrUsers, publicRelays } from "@app/utils/nostr" import Icon from "react-native-vector-icons/Ionicons" import { bytesToHex } from "@noble/hashes/utils" @@ -45,20 +45,26 @@ const Contacts: React.FC = ({ userPrivateKey }) => { const handleUnfollow = (contactPubkey: string) => { if (!poolRef || !contactsEvent) return console.log("unfollowing pubkey", contactPubkey) - let profiles = contactsEvent.tags.filter((p) => p[0] === "p").map((p) => p[1]) - let tagsWithoutProfiles = contactsEvent.tags.filter((p) => p[0] !== "p") - let newProfiles = profiles.filter((p) => p[1] !== contactPubkey) + let tagsWithoutProfile = contactsEvent.tags.filter( + (p) => p[0] === "p" && p[1] !== contactPubkey, + ) + let newCreatedAt = Math.floor(new Date().getTime() / 1000) let newContactsEvent: Event = { ...contactsEvent, - tags: [...tagsWithoutProfiles, newProfiles], + id: "", + sig: "", + created_at: newCreatedAt, + tags: [...tagsWithoutProfile], } + let finalEvent = finalizeEvent(newContactsEvent, hexToBytes(userPrivateKey)) console.log( "Old Contacts event vs NewContacts Event", contactPubkey, contactsEvent, newContactsEvent, ) - poolRef.current.publish(publicRelays, newContactsEvent) + customPublish(publicRelays, finalEvent) + console.log("Published!") } useEffect(() => { diff --git a/app/screens/nip17-chat/utils.ts b/app/screens/nip17-chat/utils.ts index 904460e50c..83cfe3fe9c 100644 --- a/app/screens/nip17-chat/utils.ts +++ b/app/screens/nip17-chat/utils.ts @@ -21,6 +21,15 @@ export const getLastSeen = async (groupId: string) => { } export const getContactsFromEvent = (event: Event) => { + console.log("CALLED GET CONTACTS ON", event) + console.log( + "Contacts ARE", + event.tags + .filter((t) => t[0] === "p") + .map((t) => { + return { pubkey: t[1] } + }), + ) return event.tags .filter((t) => t[0] === "p") .map((t) => { diff --git a/app/utils/nostr.ts b/app/utils/nostr.ts index d0544d5b4f..5de6c9b840 100644 --- a/app/utils/nostr.ts +++ b/app/utils/nostr.ts @@ -385,7 +385,7 @@ export const customPublish = ( return value }, (reason: string) => { - console.log("Rejected on", url) + console.log("Rejected on", url, reason) onRejectedRelays?.(url) return reason },