Exports and NIP-88

This commit is contained in:
KoalaSat 2023-11-27 18:27:31 +01:00
parent c7c49b654e
commit adc1be2d24
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
26 changed files with 535 additions and 86 deletions

View File

@ -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}")

View File

@ -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);
}
}

View File

@ -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')

View File

@ -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<LnPreviewProps> = ({
invoices,
setInvoices
setInvoices,
}) => {
const theme = useTheme()
const { t } = useTranslation('common')
@ -27,13 +28,12 @@ export const LnPreview: React.FC<LnPreviewProps> = ({
const [decodedLnUrl, setDecodedLnUrl] = useState<
PaymentRequestObject & { tagsObject: TagsObject }
>()
const [invoice, setInvoice] = useState<string>(invoices[0])
const [invoice, setInvoice] = useState<string>(invoices[0].invoice)
const [index, setIndex] = useState<number>(0)
const [paymentDone, setPaymentDone] = useState<boolean>()
const [paymentDone, setPaymentDone] = useState<string[]>([])
useEffect(() => {
setPaymentDone(false)
setInvoice(invoices[index])
setInvoice(invoices[index].invoice)
}, [index])
useEffect(() => {
@ -54,7 +54,9 @@ export const LnPreview: React.FC<LnPreviewProps> = ({
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<LnPreviewProps> = ({
<Card style={styles.qrContainer}>
<Card.Content>
<View style={styles.qr}>
{!paymentDone ? (
<QRCode value={invoice} quietZone={8} size={Dimensions.get('window').width - 64} />
) : (
{paymentDone.includes(invoice) ? (
<>
<MaterialCommunityIcons
name={paymentDone ? 'check-circle-outline' : 'close-circle-outline'}
size={120}
color={paymentDone ? '#7ADC70' : theme.colors.error}
/>
{index < invoices.length - 1 && (
<Chip
compact
style={{ ...styles.chip, backgroundColor: theme.colors.secondaryContainer }}
mode='outlined'
onPress={() => setIndex(prev => prev + 1)}
>
{t('lnPayment.nextInvoice')}
</Chip>
)}
</>
) : (
<QRCode value={invoice} quietZone={8} size={Dimensions.get('window').width - 64} />
)}
</View>
<View style={styles.qrText}>
<Text>{decodedLnUrl?.satoshis} </Text>
{getSatoshiSymbol(23)}
</View>
{invoices[index].description ? (
<View style={styles.qrText}>
<Text>{invoices[index].description}</Text>
</View>
) : <></>}
{invoices[index].url ? (
<View style={styles.qrText}>
<Text
style={styles.link}
onPress={async () =>
await Linking.openURL(invoices[index]?.url ?? '')
}
>
{invoices[index].url}
</Text>
</View>
) : <></>}
</Card.Content>
</Card>
{invoices.length > 1 && (
<View style={styles.counter}>
<Chip
compact
style={[
styles.chip,
{ backgroundColor: theme.colors.secondaryContainer }
]}
disabled={index === 0}
mode='outlined'
onPress={() => setIndex(prev => prev - 1)}
>
{t('lnPayment.prevInvoice')}
</Chip>
<Text>
{`${t('lnPayment.invoice')}: ${index + 1} / ${invoices.length}`}
</Text>
<Chip
compact
style={[
styles.chip,
{ backgroundColor: theme.colors.secondaryContainer }
]}
disabled={index + 1 >= invoices.length}
mode='outlined'
onPress={() => setIndex(prev => prev + 1)}
>
{t('lnPayment.nextInvoice')}
</Chip>
</View>
)}
<View style={styles.cardActions}>
@ -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',

View File

@ -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)}
/>
)}
<Drawer.Item
label={t('menuItems.exports')}
icon='file-import-outline'
key='exports'
active={drawerItemIndex === 2}
onPress={() => onPressItem('exports', 2)}
onTouchEnd={() => setDrawerItemIndex(-1)}
/>
<Drawer.Item
label={t('menuItems.configuration')}
icon='cog'
key='configuration'
active={drawerItemIndex === 2}
onPress={() => onPressItem('config', 2)}
onPress={() => onPressItem('config', 3)}
onTouchEnd={() => setDrawerItemIndex(-1)}
/>
</Drawer.Section>
@ -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)}
/>
<Drawer.Item
@ -196,7 +206,7 @@ export const MenuItems: React.FC = () => {
icon='comment-question-outline'
key='faq'
active={drawerItemIndex === 4}
onPress={() => onPressItem('faq', 4)}
onPress={() => onPressItem('faq', 5)}
onTouchEnd={() => setDrawerItemIndex(-1)}
/>
<Drawer.Item

View File

@ -490,7 +490,14 @@ export const NoteCard: React.FC<NoteCardProps> = ({
>
{note.zap_pubkey?.length > 0 ? formatBigNumber(zapsAmount) : ''}
</Button>
{zapInvoices.length > 0 && <LnPreview invoices={zapInvoices} setInvoices={setZapInvoices} />}
{zapInvoices.length > 0 &&
<LnPreview
invoices={zapInvoices.map((e) => {
return { invoice: e }
})}
setInvoices={(invoices) => setZapInvoices(invoices.map(i => i.invoice))}
/>
}
</Card.Content>
)}
<Card.Content style={styles.relayList}>

View File

@ -176,7 +176,12 @@ export const LinksPreview: React.FC<TextContentProps> = ({ urls, lnUrl }) => {
<View>
{decodedLnUrl && lnUrlPreview}
{Object.keys(urls).length > 0 && <View style={styles.previewCard}>{preview()}</View>}
{invoice && <LnPreview invoices={[invoice]} setInvoices={(arr) => setInvoice(arr[0])} />}
{invoice && (
<LnPreview
invoices={[{ invoice }]}
setInvoices={(arr) => setInvoice(arr[0]?.invoice)}
/>
)}
</View>
)
}

View File

@ -138,27 +138,31 @@ export const TextContent: React.FC<TextContentProps> = ({
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]'
}
}

View File

@ -30,7 +30,7 @@ export const UploadImage: React.FC<UploadImageProps> = ({
const [showNotification, setShowNotification] = useState<undefined | string>()
const [imageUpload, setImageUpload] = useState<Asset>()
const bottomSheetImageRef = React.useRef<RBSheet>(null)
const [imageHostingService] = useState<string>(getImageHostingService())
const [imageHostingService, setImageHostingService] = useState<string>(getImageHostingService())
useEffect(() => {
if (startUpload && !uploadingFile) {
@ -77,6 +77,9 @@ export const UploadImage: React.FC<UploadImageProps> = ({
setShowNotification('imageUploadErro')
onError()
})
.finally(() => {
setImageHostingService(getImageHostingService())
})
}
}

View File

@ -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}

View File

@ -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<number>(0)
const [newGroupMessages, setNewGroupMessages] = useState<number>(0)
const [chalenges, setChallenges] = useState<WebsocketEvent[]>([])
const [relayPay, setRelayPay] = React.useState<PayEvent[]>([])
const [receivedInvoices, setReceivedInvoices] = React.useState<string[]>([])
const sendEvent: (event: Event, relayUrl?: string) => Promise<Event | null | undefined> = async (
event,
@ -110,8 +122,8 @@ export const RelayPoolContextProvider = ({
return await relayPool?.sendEvent(event, relayUrl)
}
const sendAuth: (challenge: string, url: string) => Promise<void> = async (
challenge,
const sendAuth: (description: string, url: string) => Promise<void> = 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<void> = 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}

View File

@ -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"
},

View File

@ -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"
},

View File

@ -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"
},

View File

@ -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",

View File

@ -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"
},

View File

@ -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"
},

View File

@ -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<string>()
const [loading, setLoading] = React.useState<boolean>(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 (
<>
<View style={styles.main}>
<List.Item
title={t('exportsPage.myNotes')}
right={() =>
<Button
loading={loading}
disabled={loading}
onPress={downloadNotes}
textColor={theme.colors.onSurface}
icon='file-import-outline'
>
{t('exportsPage.download')}
</Button>
}
/>
<List.Item
title={t('exportsPage.myContacts')}
right={() =>
<Button
loading={loading}
disabled={loading}
onPress={downloadPets}
textColor={theme.colors.onSurface}
icon='file-import-outline'
>
{t('exportsPage.download')}
</Button>
}
/>
<List.Item
title={t('exportsPage.myDirectMessages')}
right={() =>
<Button
loading={loading}
disabled={loading}
onPress={downloadConversations}
textColor={theme.colors.onSurface}
icon='file-import-outline'
>
{t('exportsPage.download')}
</Button>
}
/>
<List.Item
title={t('exportsPage.myRelays')}
right={() =>
<Button
loading={loading}
disabled={loading}
onPress={downloadRelays}
textColor={theme.colors.onSurface}
icon='file-import-outline'
>
{t('exportsPage.download')}
</Button>
}
/>
</View>
{showNotification && (
<Snackbar
style={styles.snackbar}
visible={showNotification !== undefined}
duration={Snackbar.DURATION_SHORT}
onIconPress={() => setShowNotification(undefined)}
onDismiss={() => setShowNotification(undefined)}
>
{t(`exportsPage.notifications.${showNotification}`)}
</Snackbar>
)}
</>
)
}
const styles = StyleSheet.create({
main: {
flex: 1,
padding: 16,
},
snackbar: {
flex: 1,
},
})
export default ExportsPage

View File

@ -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 = () => {
</Stack.Group>
<Stack.Group>
<Stack.Screen name='Wallet' component={WalletPage} />
<Stack.Screen name='Exports' component={ExportsPage} />
<Stack.Screen name='Contacts' component={ContactsPage} />
<Stack.Screen name='Relays' component={RelaysPage} />
<Stack.Screen name='About' component={AboutPage} />

View File

@ -291,7 +291,8 @@ export const NotificationsFeed: React.FC = () => {
]}
childrenProps={{ allowFontScaling: false }}
>
{content}
{content.substring(0, 300)}
{content.length > 300 && '...'}
</ParsedText>
)}
</View>

View File

@ -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 = () => {
}}
/>
</Tab.Navigator>
{relayPay.length > 0 ? <LnPreview invoices={relayPay} setInvoices={setRelayPay}/> : <></>}
<RBSheet
ref={bottomSheetClipboardRef}
closeOnDragDown={true}

View File

@ -381,7 +381,12 @@ export const ProfileActionsPage: React.FC<ProfileActionsProps> = ({ route: { par
</Button>
</View>
</RBSheet>
{zapInvoice && <LnPreview invoices={[zapInvoice]} setInvoices={(arr) => setZapInvoice(arr[0])} />}
{zapInvoice && (
<LnPreview
invoices={[{ invoice: zapInvoice }]}
setInvoices={(arr) => setZapInvoice(arr[0]?.invoice)}
/>
)}
{showNotification && (
<Snackbar
style={styles.snackbar}

View File

@ -250,7 +250,9 @@ export const SplitZapPage: React.FC<SplitZapPageProps> = ({
</RBSheet>
<RBSheet ref={bottomSheetUserListRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<FlatList
data={users?.filter(u => u.contact)}
data={users?.filter((u) => {
return (u.contact ?? false) || (u.id === publicKey)
})}
renderItem={renderUserItem}
ItemSeparatorComponent={Divider}
horizontal={false}

View File

@ -297,10 +297,12 @@ export const ZapPage: React.FC<ZapPageProps> = ({ route: { params: { note, user
</Button>
</View>
{invoices.length > 0 && (
<LnPreview
invoices={invoices}
setInvoices={setInvoices}
/>
<LnPreview
invoices={invoices.map((e) => {
return { invoice: e }
})}
setInvoices={(invoices) => setInvoices(invoices.map(i => i.invoice))}
/>
)}
</View>
</View>

View File

@ -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",

View File

@ -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"