diff --git a/app/screens/nip17-chat/NIP17Chat.tsx b/app/screens/nip17-chat/NIP17Chat.tsx index 32845cfba2..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" @@ -7,15 +6,15 @@ 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" -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" @@ -27,10 +26,14 @@ 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" +import { UserSearchBar } from "./UserSearchBar" + +const Tab = createMaterialTopTabNavigator() export const NIP17Chat: React.FC = () => { const styles = useStyles() - const { appConfig } = useAppConfig() const { theme: { colors }, } = useTheme() @@ -42,8 +45,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() @@ -52,24 +53,6 @@ export const NIP17Chat: React.FC = () => { const { LL } = useI18nContext() const { userData } = useAppSelector((state) => state.user) - const reset = useCallback(() => { - setSearchText("") - setSearchedUsers([]) - setRefreshing(false) - 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") @@ -114,93 +97,19 @@ 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={ - - } - /> + <> + + ) if (!initialized) { @@ -232,63 +141,84 @@ export const NIP17Chat: React.FC = () => { return ( {privateKey && !showImportModal ? ( - - {SearchBarContent} + ({ + // 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 + })} + > + + {() => ( + + {SearchBarContent} - {searchText ? ( - ( - - )} - keyExtractor={(item) => item.id} - /> - ) : ( - - - Chats - - - signed in as:{" "} - - {userData?.username || nip19.npubEncode(getPublicKey(privateKey))} - - - { - return ( - ( + + )} + keyExtractor={(item) => item.id} + /> + ) : ( + + + 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/UserSearchBar.tsx b/app/screens/nip17-chat/UserSearchBar.tsx new file mode 100644 index 0000000000..e2df99c7cf --- /dev/null +++ b/app/screens/nip17-chat/UserSearchBar.tsx @@ -0,0 +1,132 @@ +import { useI18nContext } from "@app/i18n/i18n-react" +import { SearchBar } from "@rneui/themed" +import { Event, getPublicKey, nip05, nip19, SubCloser } from "nostr-tools" +import { useCallback, useEffect, useState } from "react" +import { useChatContext } from "./chatContext" +import { + fetchNostrUsers, + fetchSecretFromLocalStorage, + getGroupId, +} from "@app/utils/nostr" +import { hexToBytes } from "@noble/curves/abstract/utils" +import { useStyles } from "./style" +import { Alert } from "react-native" +import { useAppConfig } from "@app/hooks" +import { testProps } from "@app/utils/testProps" +import Icon from "react-native-vector-icons/Ionicons" + +interface UserSearchBarProps { + setSearchedUsers: (q: Chat[]) => void +} + +export const UserSearchBar: React.FC = ({ setSearchedUsers }) => { + const [searchText, setSearchText] = useState("") + const { rumors, poolRef, addEventToProfiles, profileMap } = 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) => { + if (newSearchText === "") reset() + 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..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,6 +21,8 @@ type ChatContextType = { profileMap: Map | undefined addEventToProfiles: (event: Event) => void resetChat: () => void + contactsEvent: Event | undefined + setContactsEvent: (e: Event) => void } const publicRelays = [ @@ -39,6 +42,8 @@ const ChatContext = createContext({ profileMap: undefined, addEventToProfiles: (event: Event) => {}, resetChat: () => {}, + contactsEvent: undefined, + setContactsEvent: (event: Event) => {}, }) export const useChatContext = () => useContext(ChatContext) @@ -51,6 +56,7 @@ export const ChatContextProvider: React.FC = ({ children }) = const profileMap = useRef>(new Map()) const poolRef = useRef(new SimplePool()) const processedEventIds = useRef(new Set()) + const [contactsEvent, setContactsEvent] = useState() const { appConfig: { galoyInstance: { relayUrl }, @@ -90,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() @@ -176,6 +187,8 @@ export const ChatContextProvider: React.FC = ({ children }) = profileMap: profileMap.current, addEventToProfiles, resetChat, + contactsEvent, + setContactsEvent, }} > {children} 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 new file mode 100644 index 0000000000..a48037725e --- /dev/null +++ b/app/screens/nip17-chat/contacts.tsx @@ -0,0 +1,171 @@ +// Contacts.tsx +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, finalizeEvent } 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" +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" +import { getContactsFromEvent } from "./utils" +import ContactCard from "./contactCard" +import { Swipeable } from "react-native-gesture-handler" +import { customPublish, fetchNostrUsers, publicRelays } from "@app/utils/nostr" +import Icon from "react-native-vector-icons/Ionicons" +import { bytesToHex } from "@noble/hashes/utils" + +interface ContactsProps { + userPrivateKey: string +} + +const Contacts: React.FC = ({ userPrivateKey }) => { + const styles = useStyles() + const [searchedUsers, setSearchedUsers] = useState([]) + const { poolRef, profileMap, contactsEvent, addEventToProfiles } = useChatContext() + const navigation = useNavigation>() + const { theme } = useTheme() + const colors = theme.colors + + const getContactMetadata = (contact: NostrProfile) => { + let profile = profileMap?.get(contact.pubkey || "") + return ( + profile?.nip05 || + profile?.name || + profile?.username || + nip19.npubEncode(contact.pubkey!).slice(0, 9) + ".." + ) + } + + const handleUnfollow = (contactPubkey: string) => { + if (!poolRef || !contactsEvent) return + console.log("unfollowing pubkey", 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, + id: "", + sig: "", + created_at: newCreatedAt, + tags: [...tagsWithoutProfile], + } + let finalEvent = finalizeEvent(newContactsEvent, hexToBytes(userPrivateKey)) + console.log( + "Old Contacts event vs NewContacts Event", + contactPubkey, + contactsEvent, + newContactsEvent, + ) + customPublish(publicRelays, finalEvent) + console.log("Published!") + } + + 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 ( + + + {searchedUsers.length !== 0 ? ( + ( + + )} + keyExtractor={(item) => item.id} + /> + ) : contactsEvent ? ( + No Contacts Available} + renderItem={({ item }) => ( + renderRightActions(item)} + containerStyle={styles.itemContainer} + > + + + )} + keyExtractor={(item) => item.pubkey!} + /> + ) : ( + Loading... + )} + + ) +} + +export default Contacts diff --git a/app/screens/nip17-chat/searchListItem.tsx b/app/screens/nip17-chat/searchListItem.tsx index daa4066974..0c8506f4a6 100644 --- a/app/screens/nip17-chat/searchListItem.tsx +++ b/app/screens/nip17-chat/searchListItem.tsx @@ -4,8 +4,12 @@ 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" +import { getContactsFromEvent } from "./utils" interface SearchListItemProps { item: Chat @@ -15,11 +19,31 @@ export const SearchListItem: React.FC = ({ item, userPrivateKey, }) => { + 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 }, } = useTheme() const navigation = useNavigation>() + + const getIcon = () => { + let itemPubkey = item.groupId + .split(",") + .filter((p) => p !== getPublicKey(userPrivateKey))[0] + 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)} + + { + 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/screens/nip17-chat/style.ts b/app/screens/nip17-chat/style.ts index 14fb0931a9..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 }, @@ -65,7 +67,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..83cfe3fe9c 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,23 @@ 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) => { + 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 9daa37a0cd..5de6c9b840 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 { @@ -20,7 +21,7 @@ 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", @@ -220,6 +221,20 @@ export const sendNIP4Message = async (message: string, recipient: string) => { let NIP4Messages = {} } +export const fetchContactList = async ( + userPubkey: string, + pool: SimplePool, + onEvent: (event: Event) => void, +) => { + let filter = { + kinds: [3], + authors: [userPubkey], + } + pool.subscribeMany(["wss://relay.damus.io"], [filter], { + onevent: onEvent, + }) +} + export const setPreferredRelay = async (flashRelay: string, secretKey?: Uint8Array) => { let pool = new SimplePool() console.log("inside setpreferredRelay") @@ -253,6 +268,30 @@ export const setPreferredRelay = async (flashRelay: string, secretKey?: Uint8Arr }, 5000) } +export const addToContactList = async ( + userPrivateKey: Uint8Array, + pubKeyToAdd: string, + pool: SimplePool, + contactsEvent?: Event, +) => { + const userPubkey = getPublicKey(userPrivateKey) + 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 || "", + created_at: Math.floor(Date.now() / 1000), + tags: tags, + } + const finalNewEvent = finalizeEvent(newEvent, userPrivateKey) + pool.publish(["wss://relay.damus.io"], finalNewEvent) +} + export async function sendNip17Message( recipients: string[], message: string, @@ -346,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 },