From adc1be2d2461eaac6047f4a9c9133fff2d0d4d6e Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Mon, 27 Nov 2023 18:27:31 +0100 Subject: [PATCH] Exports and NIP-88 --- android/app/build.gradle | 2 + .../java/com/nostros/classes/Websocket.java | 16 +- android/settings.gradle | 2 + frontend/Components/LnPreview/index.tsx | 88 ++++++-- frontend/Components/MenuItems/index.tsx | 16 +- frontend/Components/NoteCard/index.tsx | 9 +- .../TextContent/LinksPreview/index.tsx | 7 +- frontend/Components/TextContent/index.tsx | 42 ++-- frontend/Components/UploadImage/index.tsx | 5 +- frontend/Contexts/AppContext.tsx | 4 +- frontend/Contexts/RelayPoolContext.tsx | 36 ++- frontend/Locales/de.json | 24 +- frontend/Locales/en.json | 23 +- frontend/Locales/es.json | 23 +- frontend/Locales/fr.json | 20 +- frontend/Locales/ru.json | 23 +- frontend/Locales/zhCn.json | 23 +- frontend/Pages/ExportsPage/index.tsx | 212 ++++++++++++++++++ frontend/Pages/FeedNavigator/index.tsx | 2 + .../HomePage/NotificationsFeed/index.tsx | 3 +- frontend/Pages/HomePage/index.tsx | 4 +- frontend/Pages/ProfileActionsPage/index.tsx | 7 +- frontend/Pages/SplitZapPage/index.tsx | 4 +- frontend/Pages/ZapPage/index.tsx | 10 +- package.json | 1 + yarn.lock | 15 +- 26 files changed, 535 insertions(+), 86 deletions(-) create mode 100644 frontend/Pages/ExportsPage/index.tsx diff --git a/android/app/build.gradle b/android/app/build.gradle index 871c10c..6eb6912 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -170,6 +170,8 @@ dependencies { implementation 'com.facebook.fresco:animated-gif:2.6.0' + implementation project(':react-native-fs') + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") diff --git a/android/app/src/main/java/com/nostros/classes/Websocket.java b/android/app/src/main/java/com/nostros/classes/Websocket.java index 933560e..18fd373 100644 --- a/android/app/src/main/java/com/nostros/classes/Websocket.java +++ b/android/app/src/main/java/com/nostros/classes/Websocket.java @@ -89,6 +89,9 @@ public class Websocket { } else if (messageType.equals("AUTH")) { Log.d("Websocket", "RECEIVE AUTH:" + url + message); reactNativeAuth(jsonArray.get(1).toString()); + } else if (messageType.equals("PAY")) { + Log.d("Websocket", "RECEIVE PAY:" + url + message); + reactNativePay(jsonArray.get(1).toString(), jsonArray.get(2).toString(), jsonArray.get(3).toString()); } } @Override @@ -136,10 +139,21 @@ public class Websocket { public void reactNativeAuth(String challenge) { Log.d("Websocket", "reactNativeNotification"); WritableMap payload = Arguments.createMap(); - payload.putString("challenge", challenge); + payload.putString("description", challenge); payload.putString("url", url); context .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("WebsocketAuth", payload); } + + public void reactNativePay(String invoice, String description, String url) { + Log.d("Websocket", "reactNativeNotification"); + WritableMap payload = Arguments.createMap(); + payload.putString("invoice", invoice); + payload.putString("description", description); + payload.putString("url", url); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("WebsocketPay", payload); + } } diff --git a/android/settings.gradle b/android/settings.gradle index 885f09a..a0dc87a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -2,3 +2,5 @@ rootProject.name = 'Nostros' apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/react-native-gradle-plugin') +include ':react-native-fs' +project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android') diff --git a/frontend/Components/LnPreview/index.tsx b/frontend/Components/LnPreview/index.tsx index 22aec86..7497202 100644 --- a/frontend/Components/LnPreview/index.tsx +++ b/frontend/Components/LnPreview/index.tsx @@ -9,15 +9,16 @@ import { AppContext } from '../../Contexts/AppContext' import { decode, type PaymentRequestObject, type TagsObject } from 'bolt11' import { WalletContext } from '../../Contexts/WalletContext' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' +import { type PayEvent } from '../../Contexts/RelayPoolContext' interface LnPreviewProps { - invoices: string[] - setInvoices: (invoices: string[]) => void + invoices: PayEvent[] + setInvoices: (invoices: PayEvent[]) => void } export const LnPreview: React.FC = ({ invoices, - setInvoices + setInvoices, }) => { const theme = useTheme() const { t } = useTranslation('common') @@ -27,13 +28,12 @@ export const LnPreview: React.FC = ({ const [decodedLnUrl, setDecodedLnUrl] = useState< PaymentRequestObject & { tagsObject: TagsObject } >() - const [invoice, setInvoice] = useState(invoices[0]) + const [invoice, setInvoice] = useState(invoices[0].invoice) const [index, setIndex] = useState(0) - const [paymentDone, setPaymentDone] = useState() + const [paymentDone, setPaymentDone] = useState([]) useEffect(() => { - setPaymentDone(false) - setInvoice(invoices[index]) + setInvoice(invoices[index].invoice) }, [index]) useEffect(() => { @@ -54,7 +54,9 @@ export const LnPreview: React.FC = ({ const payWithWallet: () => void = () => { if (invoice) { - payInvoice(invoice).then(setPaymentDone) + payInvoice(invoice).then((done) => { + if (done) setPaymentDone(prev => [...prev, invoice]) + }) } } @@ -88,39 +90,70 @@ export const LnPreview: React.FC = ({ - {!paymentDone ? ( - - ) : ( + {paymentDone.includes(invoice) ? ( <> - {index < invoices.length - 1 && ( - setIndex(prev => prev + 1)} - > - {t('lnPayment.nextInvoice')} - - )} + ) : ( + )} {decodedLnUrl?.satoshis} {getSatoshiSymbol(23)} + {invoices[index].description ? ( + + {invoices[index].description} + + ) : <>} + {invoices[index].url ? ( + + + await Linking.openURL(invoices[index]?.url ?? '') + } + > + {invoices[index].url} + + + ) : <>} {invoices.length > 1 && ( + setIndex(prev => prev - 1)} + > + {t('lnPayment.prevInvoice')} + {`${t('lnPayment.invoice')}: ${index + 1} / ${invoices.length}`} + = invoices.length} + mode='outlined' + onPress={() => setIndex(prev => prev + 1)} + > + {t('lnPayment.nextInvoice')} + )} @@ -159,6 +192,9 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'center', }, + link: { + textDecorationLine: 'underline' + }, satoshi: { fontFamily: 'Satoshi-Symbol', fontSize: 20, @@ -171,7 +207,10 @@ const styles = StyleSheet.create({ }, chip: { height: 40, - marginTop: 16 + width: 100, + marginTop: 16, + justifyContent: 'center', + alignItems: 'center', }, actionButton: { justifyContent: 'center', @@ -184,9 +223,10 @@ const styles = StyleSheet.create({ }, counter: { flexDirection: 'row', - justifyContent: 'center', + justifyContent: 'space-between', alignItems: 'center', - margin: 16 + marginTop: 12, + marginBottom: 12 }, qr: { justifyContent: 'center', diff --git a/frontend/Components/MenuItems/index.tsx b/frontend/Components/MenuItems/index.tsx index 5444107..99c9748 100644 --- a/frontend/Components/MenuItems/index.tsx +++ b/frontend/Components/MenuItems/index.tsx @@ -67,6 +67,8 @@ export const MenuItems: React.FC = () => { navigate('Contacts') } else if (key === 'wallet') { navigate('Wallet') + } else if (key === 'exports') { + navigate('Exports') } } @@ -173,12 +175,20 @@ export const MenuItems: React.FC = () => { onTouchEnd={() => setDrawerItemIndex(-1)} /> )} + onPressItem('exports', 2)} + onTouchEnd={() => setDrawerItemIndex(-1)} + /> onPressItem('config', 2)} + onPress={() => onPressItem('config', 3)} onTouchEnd={() => setDrawerItemIndex(-1)} /> @@ -188,7 +198,7 @@ export const MenuItems: React.FC = () => { icon='information-outline' key='about' active={drawerItemIndex === 3} - onPress={() => onPressItem('about', 3)} + onPress={() => onPressItem('about', 4)} onTouchEnd={() => setDrawerItemIndex(-1)} /> { icon='comment-question-outline' key='faq' active={drawerItemIndex === 4} - onPress={() => onPressItem('faq', 4)} + onPress={() => onPressItem('faq', 5)} onTouchEnd={() => setDrawerItemIndex(-1)} /> = ({ > {note.zap_pubkey?.length > 0 ? formatBigNumber(zapsAmount) : ''} - {zapInvoices.length > 0 && } + {zapInvoices.length > 0 && + { + return { invoice: e } + })} + setInvoices={(invoices) => setZapInvoices(invoices.map(i => i.invoice))} + /> + } )} diff --git a/frontend/Components/TextContent/LinksPreview/index.tsx b/frontend/Components/TextContent/LinksPreview/index.tsx index 1adccc3..dece857 100644 --- a/frontend/Components/TextContent/LinksPreview/index.tsx +++ b/frontend/Components/TextContent/LinksPreview/index.tsx @@ -176,7 +176,12 @@ export const LinksPreview: React.FC = ({ urls, lnUrl }) => { {decodedLnUrl && lnUrlPreview} {Object.keys(urls).length > 0 && {preview()}} - {invoice && setInvoice(arr[0])} />} + {invoice && ( + setInvoice(arr[0]?.invoice)} + /> + )} ) } diff --git a/frontend/Components/TextContent/index.tsx b/frontend/Components/TextContent/index.tsx index c61cf60..6b8af9b 100644 --- a/frontend/Components/TextContent/index.tsx +++ b/frontend/Components/TextContent/index.tsx @@ -138,27 +138,31 @@ export const TextContent: React.FC = ({ const renderProfile: (matchingString: string, matches: string[]) => string = ( matchingString, ) => { - const decoded = nip19.decode(matchingString.replace('nostr:', '')) + try { + const decoded = nip19.decode(matchingString.replace('nostr:', '')) + + let pubKey = decoded.data as string - let pubKey = decoded.data as string - - if (decoded.type === 'nprofile') { - pubKey = (decoded.data as nip19.ProfilePointer).pubkey - } - - if (userNames[matchingString]) { - return `@${userNames[matchingString]}` - } else { - if (database) { - getUser(pubKey, database).then((user) => { - setLoadedUsers(getUnixTime(new Date())) - setUserNames((prev) => { - if (user?.name) prev[matchingString] = user.name - return prev - }) - }) + if (decoded.type === 'nprofile') { + pubKey = (decoded.data as nip19.ProfilePointer).pubkey } - return `@${formatPubKey(pubKey)}` + + if (userNames[matchingString]) { + return `@${userNames[matchingString]}` + } else { + if (database) { + getUser(pubKey, database).then((user) => { + setLoadedUsers(getUnixTime(new Date())) + setUserNames((prev) => { + if (user?.name) prev[matchingString] = user.name + return prev + }) + }) + } + return `@${formatPubKey(pubKey)}` + } + } catch { + return '@[invalid nip19]' } } diff --git a/frontend/Components/UploadImage/index.tsx b/frontend/Components/UploadImage/index.tsx index fbc1857..1c1648f 100644 --- a/frontend/Components/UploadImage/index.tsx +++ b/frontend/Components/UploadImage/index.tsx @@ -30,7 +30,7 @@ export const UploadImage: React.FC = ({ const [showNotification, setShowNotification] = useState() const [imageUpload, setImageUpload] = useState() const bottomSheetImageRef = React.useRef(null) - const [imageHostingService] = useState(getImageHostingService()) + const [imageHostingService, setImageHostingService] = useState(getImageHostingService()) useEffect(() => { if (startUpload && !uploadingFile) { @@ -77,6 +77,9 @@ export const UploadImage: React.FC = ({ setShowNotification('imageUploadErro') onError() }) + .finally(() => { + setImageHostingService(getImageHostingService()) + }) } } diff --git a/frontend/Contexts/AppContext.tsx b/frontend/Contexts/AppContext.tsx index 8553a30..5598811 100644 --- a/frontend/Contexts/AppContext.tsx +++ b/frontend/Contexts/AppContext.tsx @@ -99,7 +99,7 @@ export const initialAppContext: AppContextProps = { longPressZap: undefined, setLongPressZap: () => {}, signHeight: false, - setSignWithHeight: () => {}, + setSignWithHeight: () => {} } export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.Element => { @@ -264,7 +264,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E qrReader, setQrReader, signHeight, - setSignWithHeight, + setSignWithHeight }} > {children} diff --git a/frontend/Contexts/RelayPoolContext.tsx b/frontend/Contexts/RelayPoolContext.tsx index 4f10236..6bc3033 100644 --- a/frontend/Contexts/RelayPoolContext.tsx +++ b/frontend/Contexts/RelayPoolContext.tsx @@ -32,13 +32,21 @@ export interface RelayPoolContextProps { setNewDirectMessages: (newDirectMessages: number) => void newGroupMessages: number setNewGroupMessages: (newGroupMessages: number) => void + relayPay: PayEvent[] + setRelayPay: (invoices: PayEvent[]) => void } export interface WebsocketEvent { eventId?: string kind?: string url?: string - challenge?: string + description?: string + invoice?: string +} +export interface PayEvent { + invoice: string + url?: string + description?: string } export interface RelayPoolContextProviderProps { @@ -64,6 +72,8 @@ export const initialRelayPoolContext: RelayPoolContextProps = { setNewDirectMessages: () => {}, newGroupMessages: 0, setNewGroupMessages: () => {}, + relayPay: [], + setRelayPay: () => {} } export const RelayPoolContextProvider = ({ @@ -82,6 +92,8 @@ export const RelayPoolContextProvider = ({ const [newDirectMessages, setNewDirectMessages] = useState(0) const [newGroupMessages, setNewGroupMessages] = useState(0) const [chalenges, setChallenges] = useState([]) + const [relayPay, setRelayPay] = React.useState([]) + const [receivedInvoices, setReceivedInvoices] = React.useState([]) const sendEvent: (event: Event, relayUrl?: string) => Promise = async ( event, @@ -110,8 +122,8 @@ export const RelayPoolContextProvider = ({ return await relayPool?.sendEvent(event, relayUrl) } - const sendAuth: (challenge: string, url: string) => Promise = async ( - challenge, + const sendAuth: (description: string, url: string) => Promise = async ( + description, url, ) => { if (publicKey && privateKey) { @@ -122,7 +134,7 @@ export const RelayPoolContextProvider = ({ pubkey: publicKey, tags: [ ["relay", url], - ["challenge", challenge] + ["challenge", description] ], } nostrEvent = await signEvent(nostrEvent, privateKey) @@ -157,7 +169,7 @@ export const RelayPoolContextProvider = ({ } const authHandler: (event: WebsocketEvent) => Promise = async (event) => { - if (event.url && event.challenge) { + if (event.url && event.description) { setChallenges((prev) => { prev.push(event) return prev @@ -175,6 +187,13 @@ export const RelayPoolContextProvider = ({ } } + const payHandler: (event: WebsocketEvent) => void = (event) => { + if (event.invoice && !receivedInvoices.includes(event.invoice)) { + setReceivedInvoices(prev => [...prev, event.invoice as string]) + setRelayPay(prev => [...prev, event as PayEvent]) + } + } + const debouncedAuthdHandler = useMemo( () => debounce(authHandler, 250), [relayPool], @@ -279,6 +298,7 @@ export const RelayPoolContextProvider = ({ DeviceEventEmitter.addListener('WebsocketConfirmation', debouncedConfirmationHandler) DeviceEventEmitter.addListener('WebsocketAuth', debouncedAuthdHandler) DeviceEventEmitter.addListener('WebsocketNotification', changeNotificationHandler) + DeviceEventEmitter.addListener('WebsocketPay', payHandler) loadRelayPool() } }, [publicKey]) @@ -293,7 +313,7 @@ export const RelayPoolContextProvider = ({ if (relayPoolReady) { setChallenges((prev) => { prev.forEach((event) => { - if (event.challenge && event.url) sendAuth(event.challenge, event.url) + if (event.description && event.url) sendAuth(event.description, event.url) }) return [] @@ -324,7 +344,9 @@ export const RelayPoolContextProvider = ({ newDirectMessages, setNewDirectMessages, newGroupMessages, - setNewGroupMessages + setNewGroupMessages, + relayPay, + setRelayPay }} > {children} diff --git a/frontend/Locales/de.json b/frontend/Locales/de.json index 9a445ad..cfb78a4 100644 --- a/frontend/Locales/de.json +++ b/frontend/Locales/de.json @@ -73,14 +73,28 @@ "ProfileConfig": "Mein Profil", "markAllRead": "Alle als gelesen markieren", "ProfileActions": "Actions", - "SplitZap": "Split Zaps" + "SplitZap": "Split Zaps", + "Exports": "Export" + }, + "exportsPage": { + "notifications": { + "notesExport": "All your notes has been downloaded.", + "petsExport": "All your contacts has been downloaded.", + "relaysExport": "All your relays has been downloaded.", + "converationsExport": "All your direct messages has been downloaded.", + "exportError": "There was an error creating the export file." + }, + "myNotes": "My notes", + "myContacts": "My contacts", + "myDirectMessages": "My direct messages", + "myRelays": "My relays", + "download": "Download" }, "searchPage": { "placeholder": "Suche nach öffentlichen Schlüsseln, Notizen, hashtags...", "emptyTitle": "Tip", "emptyDescription": "Tippe @ um jemanden zu finden\n\nTippe # um nach Themen zu suchen" }, - "conversationPage": { "unableDecypt": "{{username}} hat dich erwähnt", "typeMessage": "Nachricht schreiben", @@ -142,7 +156,8 @@ "about": "Über Nostros", "faq": "FAQ", "reportBug": "Bug melden", - "logout": "Abmelden" + "logout": "Abmelden", + "exports": "Export" }, "configPage": { "signHeight": "Sign events with latest Bitcoin block", @@ -203,7 +218,8 @@ "zap": "Zap", "pay": "Pay", "invoice": "Invoice", - "nextInvoice": "Next invoice", + "nextInvoice": "Next", + "prevInvoice": "Previous", "split": "Splits between {{count}}", "zappers": "Zappers" }, diff --git a/frontend/Locales/en.json b/frontend/Locales/en.json index 04665ed..65f9e2a 100644 --- a/frontend/Locales/en.json +++ b/frontend/Locales/en.json @@ -64,7 +64,22 @@ "ProfileConfig": "My profile", "markAllRead": "Mark all as read", "ProfileActions": "Actions", - "SplitZap": "Split Zaps" + "SplitZap": "Split Zaps", + "Exports": "Export" + }, + "exportsPage": { + "notifications": { + "notesExport": "All your notes has been downloaded.", + "petsExport": "All your contacts has been downloaded.", + "relaysExport": "All your relays has been downloaded.", + "converationsExport": "All your direct messages has been downloaded.", + "exportError": "There was an error creating the export file." + }, + "myNotes": "My notes", + "myContacts": "My contacts", + "myDirectMessages": "My direct messages", + "myRelays": "My relays", + "download": "Download" }, "searchPage": { "placeholder": "Look for public keys, notes, hashtags...", @@ -136,7 +151,8 @@ "faq": "FAQ", "contacts": "Contacts", "reportBug": "Report a bug", - "logout": "Logout" + "logout": "Logout", + "exports": "Export" }, "configPage": { "showPublicImages": "Show images on public feed", @@ -198,7 +214,8 @@ "zap": "Zap", "pay": "Pay", "invoice": "Invoice", - "nextInvoice": "Next invoice", + "nextInvoice": "Next", + "prevInvoice": "Previous", "split": "Splits between {{count}}", "zappers": "Zappers" }, diff --git a/frontend/Locales/es.json b/frontend/Locales/es.json index 5a9250a..6632589 100644 --- a/frontend/Locales/es.json +++ b/frontend/Locales/es.json @@ -83,7 +83,22 @@ "ProfileConfig": "Mi perfil", "markAllRead": "Marcar todo como leído", "ProfileActions": "Acciones", - "SplitZap": "Repartir Zaps" + "SplitZap": "Repartir Zaps", + "Exports": "Export" + }, + "exportsPage": { + "notifications": { + "notesExport": "Todas tus notas se han descargado.", + "petsExport": "Todos tus contactos se han descargado.", + "relaysExport": "Todos tus relays se han descargado.", + "converationsExport": "Todas tus mensages directos se han descargado.", + "exportError": "Se ha producido un error creato tu archivo." + }, + "myNotes": "Mis notas", + "myContacts": "Mis contactos", + "myDirectMessages": "Mis mensages directos", + "myRelays": "Mis relays", + "download": "Download" }, "conversationPage": { "unableDecypt": "{{username}} está hablando con otros sobre ti", @@ -162,7 +177,8 @@ "about": "Sobre Nostros", "faq": "Preguntas frecuentes", "reportBug": "Reportar un bug", - "logout": "Salir" + "logout": "Salir", + "exports": "Exportar/Importar" }, "configPage": { "signHeight": "Firmar con el último bloque bitcoin", @@ -219,7 +235,8 @@ "zap": "Zap", "pay": "Pagar", "invoice": "Factura", - "nextInvoice": "Siguiente factura", + "nextInvoice": "Siguiente", + "prevInvoice": "Anterior", "split": "Se reparte entre {{count}}", "zappers": "Zappers" }, diff --git a/frontend/Locales/fr.json b/frontend/Locales/fr.json index 42775b9..0964eac 100644 --- a/frontend/Locales/fr.json +++ b/frontend/Locales/fr.json @@ -83,7 +83,22 @@ "ProfileConfig": "Mon profil", "markAllRead": "Mark all as read", "ProfileActions": "Actions", - "SplitZap": "Split Zaps" + "SplitZap": "Split Zaps", + "Exports": "Export" + }, + "exportsPage": { + "notifications": { + "notesExport": "All your notes has been downloaded.", + "petsExport": "All your contacts has been downloaded.", + "relaysExport": "All your relays has been downloaded.", + "converationsExport": "All your direct messages has been downloaded.", + "exportError": "There was an error creating the export file." + }, + "myNotes": "My notes", + "myContacts": "My contacts", + "myDirectMessages": "My direct messages", + "myRelays": "My relays", + "download": "Download" }, "groupHeaderIcon": { "delete": "Leave group", @@ -162,7 +177,8 @@ "about": "À propos de Nostros", "faq": "FAQ", "reportBug": "Report a bug", - "logout": "Sortir" + "logout": "Sortir", + "exports": "Export" }, "language": { "en": "English", diff --git a/frontend/Locales/ru.json b/frontend/Locales/ru.json index a11f641..07b4346 100644 --- a/frontend/Locales/ru.json +++ b/frontend/Locales/ru.json @@ -83,7 +83,22 @@ "ProfileConfig": "Мой профиль", "markAllRead": "Mark all as read", "ProfileActions": "Actions", - "SplitZap": "Split Zaps" + "SplitZap": "Split Zaps", + "Exports": "Export" + }, + "exportsPage": { + "notifications": { + "notesExport": "All your notes has been downloaded.", + "petsExport": "All your contacts has been downloaded.", + "relaysExport": "All your relays has been downloaded.", + "converationsExport": "All your direct messages has been downloaded.", + "exportError": "There was an error creating the export file." + }, + "myNotes": "My notes", + "myContacts": "My contacts", + "myDirectMessages": "My direct messages", + "myRelays": "My relays", + "download": "Download" }, "groupHeaderIcon": { "delete": "Leave group", @@ -163,7 +178,8 @@ "faq": "", "logout": "Выйти", "reportBug": "Report a bug", - "imageHostingService": "Хостинг изображений" + "imageHostingService": "Хостинг изображений", + "exports": "Export" }, "configPage": { "signHeight": "Sign events with latest Bitcoin block", @@ -220,7 +236,8 @@ "zap": "Zap", "pay": "Pay", "invoice": "Invoice", - "nextInvoice": "Next invoice", + "nextInvoice": "Next", + "prevInvoice": "Previous", "split": "Splits between {{count}}", "zappers": "Zappers" }, diff --git a/frontend/Locales/zhCn.json b/frontend/Locales/zhCn.json index 49848c0..d91da1c 100644 --- a/frontend/Locales/zhCn.json +++ b/frontend/Locales/zhCn.json @@ -82,7 +82,22 @@ "Relays": "中继", "ProfileConfig": "简介设置", "ProfileActions": "Actions", - "SplitZap": "Split Zaps" + "SplitZap": "Split Zaps", + "Exports": "Export" + }, + "exportsPage": { + "notifications": { + "notesExport": "All your notes has been downloaded.", + "petsExport": "All your contacts has been downloaded.", + "relaysExport": "All your relays has been downloaded.", + "converationsExport": "All your direct messages has been downloaded.", + "exportError": "There was an error creating the export file." + }, + "myNotes": "My notes", + "myContacts": "My contacts", + "myDirectMessages": "My direct messages", + "myRelays": "My relays", + "download": "Download" }, "groupHeaderIcon": { "delete": "退出", @@ -161,7 +176,8 @@ "about": "关于", "faq": "常见问题", "reportBug": "反馈问题", - "logout": "退出" + "logout": "退出", + "exports": "Export" }, "configPage": { "signHeight": "用最新的比特币区块签署事件", @@ -218,7 +234,8 @@ "zap": "赞赏", "pay": "支付", "invoice": "Invoice", - "nextInvoice": "Next invoice", + "nextInvoice": "Next", + "prevInvoice": "Previous", "split": "Splits between {{count}}", "zappers": "Zappers" }, diff --git a/frontend/Pages/ExportsPage/index.tsx b/frontend/Pages/ExportsPage/index.tsx new file mode 100644 index 0000000..7f71320 --- /dev/null +++ b/frontend/Pages/ExportsPage/index.tsx @@ -0,0 +1,212 @@ +import React, { } from 'react' +import { useTranslation } from 'react-i18next' +import { + StyleSheet, + View, +} from 'react-native' +import { + Button, + List, + Snackbar, + useTheme +} from 'react-native-paper' +import RNFS from 'react-native-fs'; +import { type Event } from '../../lib/nostr/Events' +import { getRawUserNotes } from '../../Functions/DatabaseFunctions/Notes' +import { AppContext } from '../../Contexts/AppContext' +import { UserContext } from '../../Contexts/UserContext' +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import { getUnixTime } from 'date-fns'; +import { usersToTags } from '../../Functions/RelayFunctions/Users'; +import { Kind } from 'nostr-tools'; +import { getUsers } from '../../Functions/DatabaseFunctions/Users'; +import { getRawRelayMetadata } from '../../Functions/DatabaseFunctions/RelayMetadatas'; +import { getRawUserConversation } from '../../Functions/DatabaseFunctions/DirectMessages'; + + +export const ExportsPage: React.FC = () => { + const { t } = useTranslation('common') + const theme = useTheme() + const { database } = React.useContext(AppContext) + const { publicKey } = React.useContext(UserContext) + const [showNotification, setShowNotification] = React.useState() + const [loading, setLoading] = React.useState(false) + + const downloadNotes = (): void => { + if (database && publicKey) { + setLoading(true) + getRawUserNotes(database, publicKey).then(async (resultNotes) => { + const exportNotes: Event[] = resultNotes.map((note) => { + note.content = note.content.replace("''", "'") + return note + }) + + const jsonString = JSON.stringify(exportNotes, null, 2); + const filePath = `${RNFS.DownloadDirectoryPath}/nostr_notes_${getUnixTime(new Date())}.json`; + await RNFS.writeFile(filePath, jsonString, 'utf8') + .catch((e) => { + console.log(e) + setShowNotification('exportError') + }) + setShowNotification('notesExport') + setLoading(false) + }) + .catch(() => setLoading(false)) + } + } + + const downloadPets = (): void => { + if (database && publicKey) { + setLoading(true) + getUsers(database, { exludeIds: [publicKey], contacts: true }).then(async (users) => { + if (users) { + const event: Event = { + content: '', + created_at: getUnixTime(new Date()), + kind: Kind.Contacts, + pubkey: publicKey, + tags: usersToTags(users), + } + const jsonString = JSON.stringify([event], null, 2); + const filePath = `${RNFS.DownloadDirectoryPath}/nostr_pets_${getUnixTime(new Date())}.json`; + await RNFS.writeFile(filePath, jsonString, 'utf8') + .catch((e) => { + console.log(e) + setShowNotification('exportError') + }) + setShowNotification('petsExport') + setLoading(false) + } + }) + .catch(() => setLoading(false)) + } + } + + const downloadRelays = (): void => { + if (database && publicKey) { + setLoading(true) + + getRawRelayMetadata(database, publicKey).then(async (lists) => { + const jsonString = JSON.stringify(lists, null, 2); + const filePath = `${RNFS.DownloadDirectoryPath}/nostr_relays_${getUnixTime(new Date())}.json`; + await RNFS.writeFile(filePath, jsonString, 'utf8') + .catch((e) => { + console.log(e) + setShowNotification('exportError') + }) + setShowNotification('relaysExport') + setLoading(false) + }) + .catch(() => setLoading(false)) + } + } + + const downloadConversations = (): void => { + if (database && publicKey) { + setLoading(true) + + getRawUserConversation(database, publicKey).then(async (resultConversations) => { + const exportNotes: Event[] = resultConversations.map((conversation) => { + conversation.content = conversation.content.replace("''", "'") + return conversation + }) + const jsonString = JSON.stringify(exportNotes, null, 2); + const filePath = `${RNFS.DownloadDirectoryPath}/nostr_direct_messages_${getUnixTime(new Date())}.json`; + await RNFS.writeFile(filePath, jsonString, 'utf8') + .catch((e) => { + console.log(e) + setShowNotification('exportError') + }) + setShowNotification('converationsExport') + setLoading(false) + }) + .catch(() => setLoading(false)) + } + } + + return ( + <> + + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + {showNotification && ( + setShowNotification(undefined)} + onDismiss={() => setShowNotification(undefined)} + > + {t(`exportsPage.notifications.${showNotification}`)} + + )} + + ) +} + +const styles = StyleSheet.create({ + main: { + flex: 1, + padding: 16, + }, + snackbar: { + flex: 1, + }, +}) + +export default ExportsPage diff --git a/frontend/Pages/FeedNavigator/index.tsx b/frontend/Pages/FeedNavigator/index.tsx index 0bd43e2..f03aaf4 100644 --- a/frontend/Pages/FeedNavigator/index.tsx +++ b/frontend/Pages/FeedNavigator/index.tsx @@ -34,6 +34,7 @@ import NoteActionsPage from '../NoteActionsPage' import ProfileActionsPage from '../ProfileActionsPage' import ZapPage from '../ZapPage' import SplitZapPage from '../SplitZapPage' +import ExportsPage from '../ExportsPage' export const HomeNavigator: React.FC = () => { const theme = useTheme() @@ -204,6 +205,7 @@ export const HomeNavigator: React.FC = () => { + diff --git a/frontend/Pages/HomePage/NotificationsFeed/index.tsx b/frontend/Pages/HomePage/NotificationsFeed/index.tsx index ad62838..b1281f1 100644 --- a/frontend/Pages/HomePage/NotificationsFeed/index.tsx +++ b/frontend/Pages/HomePage/NotificationsFeed/index.tsx @@ -291,7 +291,8 @@ export const NotificationsFeed: React.FC = () => { ]} childrenProps={{ allowFontScaling: false }} > - {content} + {content.substring(0, 300)} + {content.length > 300 && '...'} )} diff --git a/frontend/Pages/HomePage/index.tsx b/frontend/Pages/HomePage/index.tsx index 38cc79d..1ab0384 100644 --- a/frontend/Pages/HomePage/index.tsx +++ b/frontend/Pages/HomePage/index.tsx @@ -14,11 +14,12 @@ import { useTranslation } from 'react-i18next' import { navigate } from '../../lib/Navigation' import GroupsFeed from './GroupsFeed' import { RelayPoolContext } from '../../Contexts/RelayPoolContext' +import LnPreview from '../../Components/LnPreview' export const HomePage: React.FC = () => { const theme = useTheme() const { t } = useTranslation('common') - const { relayPool, newNotifications, setNewNotifications, newDirectMessages, setNewDirectMessages, newGroupMessages, setNewGroupMessages } = useContext(RelayPoolContext) + const { relayPool, newNotifications, setNewNotifications, newDirectMessages, setNewDirectMessages, newGroupMessages, setNewGroupMessages, relayPay, setRelayPay } = useContext(RelayPoolContext) const { setPushedTab } = React.useContext(AppContext) const { privateKey, publicKey } = React.useContext(UserContext) const { clipboardNip21, setClipboardNip21 } = useContext(AppContext) @@ -212,6 +213,7 @@ export const HomePage: React.FC = () => { }} /> + {relayPay.length > 0 ? : <>} = ({ route: { par - {zapInvoice && setZapInvoice(arr[0])} />} + {zapInvoice && ( + setZapInvoice(arr[0]?.invoice)} + /> + )} {showNotification && ( = ({ u.contact)} + data={users?.filter((u) => { + return (u.contact ?? false) || (u.id === publicKey) + })} renderItem={renderUserItem} ItemSeparatorComponent={Divider} horizontal={false} diff --git a/frontend/Pages/ZapPage/index.tsx b/frontend/Pages/ZapPage/index.tsx index 2144a15..d38585b 100644 --- a/frontend/Pages/ZapPage/index.tsx +++ b/frontend/Pages/ZapPage/index.tsx @@ -297,10 +297,12 @@ export const ZapPage: React.FC = ({ route: { params: { note, user {invoices.length > 0 && ( - + { + return { invoice: e } + })} + setInvoices={(invoices) => setInvoices(invoices.map(i => i.invoice))} + /> )} diff --git a/package.json b/package.json index b1eed24..cc410f0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react-native-bidirectional-infinite-scroll": "^0.3.3", "react-native-blob-util": "^0.17.3", "react-native-fast-image": "^8.6.3", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.8.0", "react-native-image-picker": "^5.1.0", "react-native-pager-view": "^6.1.4", diff --git a/yarn.lock b/yarn.lock index 4ec5eba..083539b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2389,7 +2389,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@0.1.0: +base-64@0.1.0, base-64@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== @@ -6976,6 +6976,14 @@ react-native-fast-image@^8.6.3: resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255" integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg== +react-native-fs@^2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" + integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== + dependencies: + base-64 "^0.1.0" + utf8 "^3.0.0" + react-native-gesture-handler@^2.8.0: version "2.9.0" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241" @@ -8269,6 +8277,11 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf8@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"