diff --git a/frontend/Components/TextContent/index.tsx b/frontend/Components/TextContent/index.tsx index b1c2042..712afc1 100644 --- a/frontend/Components/TextContent/index.tsx +++ b/frontend/Components/TextContent/index.tsx @@ -185,7 +185,7 @@ export const TextContent: React.FC = ({ pattern: /#\[(\d+)\]/, }, { pattern: /#(\w+)/, style: styles.hashTag }, - { pattern: /(note1)\S*/, style: styles.nip19, onPress: handleNip05NotePress }, + { pattern: /(nevent1)\S*/, style: styles.nip19, onPress: handleNip05NotePress }, { pattern: /(npub1|nprofile1)\S*/, style: styles.nip19, diff --git a/frontend/Contexts/AppContext.tsx b/frontend/Contexts/AppContext.tsx index 93aa921..917576b 100644 --- a/frontend/Contexts/AppContext.tsx +++ b/frontend/Contexts/AppContext.tsx @@ -1,12 +1,13 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { QuickSQLiteConnection } from 'react-native-quick-sqlite' import { initDatabase } from '../Functions/DatabaseFunctions' import SInfo from 'react-native-sensitive-info' -import { Linking, StyleSheet } from 'react-native' +import { AppState, Linking, StyleSheet } from 'react-native' import { Text } from 'react-native-paper' import { Config } from '../Pages/ConfigPage' import { imageHostingServices } from '../Constants/Services' -import { randomInt } from '../Functions/NativeFunctions' +import { randomInt, validNip21 } from '../Functions/NativeFunctions' +import Clipboard from '@react-native-clipboard/clipboard' export interface AppContextProps { init: () => void @@ -24,6 +25,9 @@ export interface AppContextProps { setSatoshi: (showPublicImages: 'kebab' | 'sats') => void getSatoshiSymbol: (fontSize?: number) => JSX.Element getImageHostingService: () => string + clipboardNip21?: string + setClipboardNip21: (clipboardNip21: string | undefined) => void + checkClipboard: () => void } export interface AppContextProviderProps { @@ -42,13 +46,17 @@ export const initialAppContext: AppContextProps = { setShowSensitive: () => {}, satoshi: 'kebab', setSatoshi: () => {}, + checkClipboard: () => {}, imageHostingService: Object.keys(imageHostingServices)[0], setImageHostingService: () => {}, - getImageHostingService: () => "", + getImageHostingService: () => '', getSatoshiSymbol: () => <>, + setClipboardNip21: () => {}, } export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.Element => { + const currentState = useRef(AppState.currentState) + const [appState, setAppState] = useState(currentState.current) const [showPublicImages, setShowPublicImages] = React.useState( initialAppContext.showPublicImages, ) @@ -60,6 +68,25 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E const [satoshi, setSatoshi] = React.useState<'kebab' | 'sats'>(initialAppContext.satoshi) const [database, setDatabase] = useState(null) const [loadingDb, setLoadingDb] = useState(initialAppContext.loadingDb) + const [clipboardLoads, setClipboardLoads] = React.useState([]) + const [clipboardNip21, setClipboardNip21] = React.useState() + + useEffect(() => { + const handleChange = AppState.addEventListener('change', (changedState) => { + currentState.current = changedState + setAppState(currentState.current) + }) + + return () => { + handleChange.remove() + } + }, []) + + useEffect(() => { + if (appState === 'active') { + checkClipboard() + } + }, [appState]) const init: () => void = () => { const db = initDatabase() @@ -112,11 +139,25 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E return Object.keys(imageHostingServices)[randomIndex - 1] } + const checkClipboard: () => void = () => { + if (Clipboard.hasString()) { + Clipboard.getString().then((clipboardContent) => { + if (validNip21(clipboardContent) && !clipboardLoads.includes(clipboardContent)) { + setClipboardLoads((prev) => [...prev, clipboardContent]) + setClipboardNip21(clipboardContent) + } + }) + } + } + useEffect(init, []) return ( () const [nip05, setNip05] = useState() const [validNip05, setValidNip05] = useState() - // const [clipboardLoads, setClipboardLoads] = React.useState([]) const reloadUser: () => void = () => { if (database && publicKey) { @@ -82,16 +81,6 @@ export const UserContextProvider = ({ children }: UserContextProviderProps): JSX } } - // const checkClipboard: () => void = () => { - // if (Clipboard.hasString()) { - // Clipboard.getString().then((clipboardContent) => { - // if (validNip21(clipboardContent) && !clipboardLoads.includes(clipboardContent)) { - // setClipboardLoads((prev) => [...prev, clipboardContent]) - // } - // }) - // } - // } - const logout: () => void = () => { if (database) { relayPool?.unsubscribeAll() diff --git a/frontend/Functions/NativeFunctions/index.ts b/frontend/Functions/NativeFunctions/index.ts index c188b0d..b194bd3 100644 --- a/frontend/Functions/NativeFunctions/index.ts +++ b/frontend/Functions/NativeFunctions/index.ts @@ -55,7 +55,7 @@ export const validBlueBirdUrl: (url: string | undefined) => boolean = (url) => { export const validNip21: (string: string | undefined) => boolean = (string) => { if (string) { - const regexp = /^nostr:(npub1|nprofile1|note1|nevent1)S*$/ + const regexp = /^(nostr:)?(npub1|nprofile1|nevent1|nrelay1)\S*$/ return regexp.test(string) } else { return false diff --git a/frontend/Locales/en.json b/frontend/Locales/en.json index c310ec9..fcbde4b 100644 --- a/frontend/Locales/en.json +++ b/frontend/Locales/en.json @@ -225,6 +225,11 @@ "unfollow": "Following", "copyNPub": "Copy key" }, + "homePage": { + "clipboardTitle": "Nostr key detected on your clipboard", + "goToEvent": "Open", + "cancel": "Cancel" + }, "profileCard": { "notifications": { "contactAdded": "Profile followed", diff --git a/frontend/Locales/es.json b/frontend/Locales/es.json index 19d5f5d..807e310 100644 --- a/frontend/Locales/es.json +++ b/frontend/Locales/es.json @@ -213,6 +213,11 @@ "unfollow": "Siguiendo", "copyNPub": "Copiar clave" }, + "homePage": { + "clipboardTitle": "Se ha detectado una clave Nostr en el portapapeles", + "goToEvent": "Abrir", + "cancel": "Cancelar" + }, "profileCard": { "notifications": { "contactAdded": "Siguiendo", diff --git a/frontend/Locales/ru.json b/frontend/Locales/ru.json index 88fe799..e4f3221 100644 --- a/frontend/Locales/ru.json +++ b/frontend/Locales/ru.json @@ -212,6 +212,11 @@ "unfollow": "Following", "copyNPub": "Copy key" }, + "homePage": { + "clipboardTitle": "Se ha detectado una clave Nostr en el portapapeles", + "goToEvent": "Abrir", + "cancel": "Отменить" + }, "profileCard": { "notifications": { "contactAdded": "Profile followed", diff --git a/frontend/Pages/HomePage/index.tsx b/frontend/Pages/HomePage/index.tsx index 8d59634..b71bb39 100644 --- a/frontend/Pages/HomePage/index.tsx +++ b/frontend/Pages/HomePage/index.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from 'react' -import { Badge, TouchableRipple, useTheme } from 'react-native-paper' +import { Badge, Button, Text, TouchableRipple, useTheme } from 'react-native-paper' import ContactsFeed from '../ContactsFeed' import ConversationsFeed from '../ConversationsFeed' import HomeFeed from '../HomeFeed' @@ -12,13 +12,26 @@ import { Kind } from 'nostr-tools' import { getMentionNotes, getNotificationsCount } from '../../Functions/DatabaseFunctions/Notes' import { AppContext } from '../../Contexts/AppContext' import { StyleSheet } from 'react-native' +import RBSheet from 'react-native-raw-bottom-sheet' +import { useTranslation } from 'react-i18next' +import { navigate } from '../../lib/Navigation' +import { decode } from 'nostr-tools/nip19' export const HomePage: React.FC = () => { const theme = useTheme() + const { t } = useTranslation('common') const { privateKey, publicKey } = React.useContext(UserContext) - const { database, notificationSeenAt } = useContext(AppContext) + const { database, notificationSeenAt, clipboardNip21, setClipboardNip21 } = useContext(AppContext) const { relayPool, lastEventId } = useContext(RelayPoolContext) const [newNotifications, setNewNotifications] = useState(0) + const bottomSheetClipboardRef = React.useRef(null) + + useEffect(() => { + if (clipboardNip21) { + console.log('clipboardNip21', clipboardNip21) + bottomSheetClipboardRef.current?.open() + } + }, [clipboardNip21]) useEffect(() => { if (publicKey && database) { @@ -45,87 +58,139 @@ export const HomePage: React.FC = () => { } }, [publicKey]) + const goToEvent: () => void = () => { + if (clipboardNip21) { + const key = decode(clipboardNip21.replace('nostr:', '')) + if (key) { + if (key.type === 'nevent') { + navigate('Note', { noteId: key.data }) + } else if (key.type === 'nprofile' || key.type === 'npub') { + navigate('Profile', { pubKey: key.data }) + } + } + } + bottomSheetClipboardRef.current?.close() + } + const Tab = createBottomTabNavigator() + const bottomSheetStyles = React.useMemo(() => { + return { + container: { + backgroundColor: theme.colors.background, + paddingTop: 16, + paddingRight: 16, + paddingBottom: 32, + paddingLeft: 16, + borderTopRightRadius: 28, + borderTopLeftRadius: 28, + height: 'auto', + }, + } + }, []) + return ( - , - }} - > - ( - - ), + <> + , }} - /> - {privateKey && ( + > ( ), }} /> - )} - ( - - ), - }} - /> - ( - <> - {newNotifications > 0 && ( - {newNotifications} - )} + {privateKey && ( + ( + + ), + }} + /> + )} + ( - - ), - }} - /> - + ), + }} + /> + ( + <> + {newNotifications > 0 && ( + {newNotifications} + )} + + + ), + }} + /> + + + + {t('homePage.clipboardTitle')} + {clipboardNip21} + + + + ) } @@ -135,6 +200,10 @@ const styles = StyleSheet.create({ right: 25, top: 10, }, + buttonSpacer: { + marginTop: 16, + marginBottom: 16, + }, }) export default HomePage diff --git a/frontend/Pages/SendPage/index.tsx b/frontend/Pages/SendPage/index.tsx index bcbde5d..4001c68 100644 --- a/frontend/Pages/SendPage/index.tsx +++ b/frontend/Pages/SendPage/index.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useMemo, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react' import { StyleSheet, View } from 'react-native' import { AppContext } from '../../Contexts/AppContext' import { Event } from '../../lib/nostr/Events' diff --git a/package.json b/package.json index bc15255..2ed1474 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "i18next": "^22.4.9", "lnurl-pay": "^2.1.1", "lodash.debounce": "^4.0.8", - "nostr-tools": "^1.1.1", + "nostr-tools": "^1.2.1", "react": "18.1.0", "react-content-loader": "^6.2.0", "react-i18next": "^12.1.4", diff --git a/yarn.lock b/yarn.lock index d7cc133..2d03040 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6514,10 +6514,10 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -nostr-tools@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.1.1.tgz#2be4cd650bc0a4d20650b6cf46fee451c9f565b8" - integrity sha512-mxgjbHR6nx2ACBNa2tBpeM/glsPWqxHPT1Kszx/XfzL+kUdi1Gm3Xz1UcaODQ2F84IFtCKNLO+aF31ZfTAhSYQ== +nostr-tools@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.2.1.tgz#0871572254679744539846602ef2cd2f204d82c4" + integrity sha512-SL0sst29mrQ7oUPGQn+NMH4yuTe69a8S4bliNpYB2IG0fDl3Cx+xSLnuCTb4nZiNalatYsA5l+751wQiDGA3+A== dependencies: "@noble/hashes" "^0.5.7" "@noble/secp256k1" "^1.7.0"