Long pres zap (#355)

This commit is contained in:
KoalaSat 2023-02-20 17:56:43 +00:00 committed by GitHub
commit 5b6d255ee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 413 additions and 110 deletions

View File

@ -16,11 +16,8 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { Kind } from 'nostr-tools' import { Kind } from 'nostr-tools'
import { getUnixTime } from 'date-fns' import { getUnixTime } from 'date-fns'
import { Event, signEvent } from '../../lib/nostr/Events'
import { getRelays, Relay } from '../../Functions/DatabaseFunctions/Relays'
import { UserContext } from '../../Contexts/UserContext' import { UserContext } from '../../Contexts/UserContext'
import { requestInvoiceWithServiceParams, requestPayServiceParams } from 'lnurl-pay' import { lightningInvoice } from '../../Functions/ServicesFunctions/ZapInvoice'
import axios from 'axios'
interface LnPaymentProps { interface LnPaymentProps {
open: boolean open: boolean
@ -86,57 +83,26 @@ export const LnPayment: React.FC<LnPaymentProps> = ({ open, setOpen, note, user
setIsZap(zap) setIsZap(zap)
const lud = lnAddress && lnAddress !== '' ? lnAddress : lnurl const lud = lnAddress && lnAddress !== '' ? lnAddress : lnurl
if (lud && lud !== '' && monto !== '') { if (lud && lud !== '' && monto !== '' && database && privateKey && publicKey && userId) {
setLoading(true) setLoading(true)
const tokens: number = parseInt(monto, 10) ?? 0 lightningInvoice(
let nostr: string database,
lud,
if (zap && database && privateKey && publicKey && zapPubkey && userId) { parseInt(monto, 10),
const relays: Relay[] = await getRelays(database) privateKey,
const tags = [ publicKey,
['p', userId], userId,
['amount', (tokens * 1000).toString()], zap,
['relays', ...relays.map((relay) => relay.url)], zapPubkey,
]
if (note?.id) tags.push(['e', note.id])
const event: Event = {
content: comment,
created_at: getUnixTime(new Date()),
kind: 9734,
pubkey: publicKey,
tags,
}
const signedEvent = await signEvent(event, privateKey)
nostr = JSON.stringify(signedEvent)
}
const serviceParams = await requestPayServiceParams({ lnUrlOrAddress: lud })
requestInvoiceWithServiceParams({
params: serviceParams,
lnUrlOrAddress: lud,
tokens,
comment, comment,
fetchGet: async ({ url, params }) => { note?.id,
if (params && nostr && serviceParams.rawData.allowsNostr) { )
params.nostr = nostr .then((invoice) => {
} if (invoice) setInvoice(invoice)
const response = await axios.get(url, {
params,
})
console.log(response)
return response.data
},
})
.then((action) => {
if (action.hasValidAmount && action.invoice) {
setInvoice(action.invoice)
}
setLoading(false) setLoading(false)
}) })
.catch((e) => { .catch(() => {
setLoading(false) setLoading(false)
}) })
} }

View File

@ -41,6 +41,9 @@ import { SvgXml } from 'react-native-svg'
import { reactionIcon } from '../../Constants/Theme' import { reactionIcon } from '../../Constants/Theme'
import LnPayment from '../LnPayment' import LnPayment from '../LnPayment'
import { getZapsAmount } from '../../Functions/DatabaseFunctions/Zaps' import { getZapsAmount } from '../../Functions/DatabaseFunctions/Zaps'
import { lightningInvoice } from '../../Functions/ServicesFunctions/ZapInvoice'
import LnPreview from '../LnPreview'
import { getNpub } from '../../lib/nostr/Nip19'
interface NoteCardProps { interface NoteCardProps {
note?: Note note?: Note
@ -72,7 +75,8 @@ export const NoteCard: React.FC<NoteCardProps> = ({
const theme = useTheme() const theme = useTheme()
const { publicKey, privateKey } = React.useContext(UserContext) const { publicKey, privateKey } = React.useContext(UserContext)
const { relayPool, lastEventId, setDisplayrelayDrawer } = useContext(RelayPoolContext) const { relayPool, lastEventId, setDisplayrelayDrawer } = useContext(RelayPoolContext)
const { database, showSensitive, setDisplayUserDrawer, relayColouring } = useContext(AppContext) const { database, showSensitive, setDisplayUserDrawer, relayColouring, longPressZap } =
useContext(AppContext)
const [relayAdded, setRelayAdded] = useState<boolean>(false) const [relayAdded, setRelayAdded] = useState<boolean>(false)
const [positiveReactions, setPositiveReactions] = useState<number>(0) const [positiveReactions, setPositiveReactions] = useState<number>(0)
const [negativeReactions, setNegativeReactions] = useState<number>(0) const [negativeReactions, setNegativeReactions] = useState<number>(0)
@ -87,6 +91,8 @@ export const NoteCard: React.FC<NoteCardProps> = ({
const [repost, setRepost] = useState<Note>() const [repost, setRepost] = useState<Note>()
const [openLn, setOpenLn] = React.useState<boolean>(false) const [openLn, setOpenLn] = React.useState<boolean>(false)
const [showReactions, setShowReactions] = React.useState<boolean>(false) const [showReactions, setShowReactions] = React.useState<boolean>(false)
const [loadingZap, setLoadingZap] = React.useState<boolean>(false)
const [zapInvoice, setZapInvoice] = React.useState<string>()
useEffect(() => { useEffect(() => {
if (database && publicKey && note?.id) { if (database && publicKey && note?.id) {
@ -340,6 +346,33 @@ export const NoteCard: React.FC<NoteCardProps> = ({
</Card> </Card>
) )
const generateZapInvoice: () => void = () => {
const lud = note?.ln_address && note?.ln_address !== '' ? note?.ln_address : note?.lnurl
if (lud && lud !== '' && longPressZap && database && privateKey && publicKey && note?.pubkey) {
setLoadingZap(true)
lightningInvoice(
database,
lud,
longPressZap,
privateKey,
publicKey,
note?.pubkey,
true,
note?.zap_pubkey,
`Nostr: ${formatPubKey(getNpub(note?.id))}`,
note?.id,
)
.then((invoice) => {
if (invoice) setZapInvoice(invoice)
setLoadingZap(false)
})
.catch(() => {
setLoadingZap(false)
})
}
}
const reactionsCount: () => number = () => { const reactionsCount: () => number = () => {
if (userDownvoted) return negativeReactions if (userDownvoted) return negativeReactions
if (userUpvoted) return positiveReactions if (userUpvoted) return positiveReactions
@ -432,10 +465,13 @@ export const NoteCard: React.FC<NoteCardProps> = ({
/> />
)} )}
onPress={() => setOpenLn(true)} onPress={() => setOpenLn(true)}
onLongPress={longPressZap ? generateZapInvoice : undefined}
loading={loadingZap}
> >
{note.zap_pubkey?.length > 0 ? formatBigNumber(zapsAmount) : ''} {note.zap_pubkey?.length > 0 ? formatBigNumber(zapsAmount) : ''}
</Button> </Button>
{openLn && <LnPayment open={openLn} setOpen={setOpenLn} note={note} />} {openLn && <LnPayment open={openLn} setOpen={setOpenLn} note={note} />}
{zapInvoice && <LnPreview invoice={zapInvoice} setInvoice={setZapInvoice} />}
</Card.Content> </Card.Content>
)} )}
<Card.Content style={styles.relayList}> <Card.Content style={styles.relayList}>

View File

@ -4,8 +4,8 @@ import { Text, useTheme } from 'react-native-paper'
import { getNip05Domain, usernamePubKey } from '../../Functions/RelayFunctions/Users' import { getNip05Domain, usernamePubKey } from '../../Functions/RelayFunctions/Users'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import NostrosAvatar from '../NostrosAvatar' import NostrosAvatar from '../NostrosAvatar'
import { fromUnixTime, formatDistance } from 'date-fns'
import { getNpub } from '../../lib/nostr/Nip19' import { getNpub } from '../../lib/nostr/Nip19'
import { formatDate } from '../../Functions/NativeFunctions'
interface ProfileCardProps { interface ProfileCardProps {
username?: string username?: string
@ -32,10 +32,7 @@ export const ProfileData: React.FC<ProfileCardProps> = ({
}) => { }) => {
const theme = useTheme() const theme = useTheme()
const nPub = React.useMemo(() => (publicKey ? getNpub(publicKey) : ''), [publicKey]) const nPub = React.useMemo(() => (publicKey ? getNpub(publicKey) : ''), [publicKey])
const date = React.useMemo( const date = React.useMemo(() => formatDate(timestamp), [timestamp])
() => (timestamp ? formatDistance(fromUnixTime(timestamp), new Date()) : null),
[timestamp],
)
return ( return (
<View style={styles.container}> <View style={styles.container}>

View File

@ -6,18 +6,31 @@ import { IconButton, Snackbar, Text, TouchableRipple } from 'react-native-paper'
import { User } from '../../Functions/DatabaseFunctions/Users' import { User } from '../../Functions/DatabaseFunctions/Users'
import Share from 'react-native-share' import Share from 'react-native-share'
import RBSheet from 'react-native-raw-bottom-sheet' import RBSheet from 'react-native-raw-bottom-sheet'
import { getNpub } from '../../lib/nostr/Nip19' import { getNprofile } from '../../lib/nostr/Nip19'
import QRCode from 'react-native-qrcode-svg' import QRCode from 'react-native-qrcode-svg'
import { useContext } from 'react'
import { AppContext } from '../../Contexts/AppContext'
import { getUserRelays } from '../../Functions/DatabaseFunctions/NotesRelays'
interface ProfileShareProps { interface ProfileShareProps {
user: User user: User
} }
export const ProfileShare: React.FC<ProfileShareProps> = ({ user }) => { export const ProfileShare: React.FC<ProfileShareProps> = ({ user }) => {
const { database } = useContext(AppContext)
const bottomSheetShareRef = React.useRef<RBSheet>(null) const bottomSheetShareRef = React.useRef<RBSheet>(null)
const [qrCode, setQrCode] = React.useState<any>() const [qrCode, setQrCode] = React.useState<any>()
const [showNotification, setShowNotification] = React.useState<undefined | string>() const [showNotification, setShowNotification] = React.useState<undefined | string>()
const nPub = React.useMemo(() => getNpub(user.id), [user]) const [nProfile, setNProfile] = React.useState<string>()
React.useEffect(() => {
if (database && user.id) {
getUserRelays(database, user.id).then((results) => {
const urls = results.map((relay) => relay.relay_url)
setNProfile(getNprofile(user.id, urls))
})
}
}, [user])
return ( return (
<View style={styles.mainLayout}> <View style={styles.mainLayout}>
@ -36,7 +49,7 @@ export const ProfileShare: React.FC<ProfileShareProps> = ({ user }) => {
> >
<QRCode <QRCode
quietZone={8} quietZone={8}
value={`nostr:${nPub}`} value={`nostr:${nProfile}`}
size={Dimensions.get('window').width - 64} size={Dimensions.get('window').width - 64}
logoBorderRadius={50} logoBorderRadius={50}
logoSize={100} logoSize={100}
@ -51,7 +64,7 @@ export const ProfileShare: React.FC<ProfileShareProps> = ({ user }) => {
size={28} size={28}
onPress={() => { onPress={() => {
setShowNotification('npubCopied') setShowNotification('npubCopied')
Clipboard.setString(nPub ?? '') Clipboard.setString(nProfile ?? '')
bottomSheetShareRef.current?.close() bottomSheetShareRef.current?.close()
}} }}
/> />

View File

@ -13,6 +13,7 @@ interface UploadImageProps {
setImageUri: (uri: string) => void setImageUri: (uri: string) => void
uploadingFile: boolean uploadingFile: boolean
setUploadingFile: (uploading: boolean) => void setUploadingFile: (uploading: boolean) => void
onError: () => void
} }
export const UploadImage: React.FC<UploadImageProps> = ({ export const UploadImage: React.FC<UploadImageProps> = ({
@ -21,6 +22,7 @@ export const UploadImage: React.FC<UploadImageProps> = ({
setImageUri, setImageUri,
uploadingFile, uploadingFile,
setUploadingFile, setUploadingFile,
onError,
}) => { }) => {
const { getImageHostingService } = useContext(AppContext) const { getImageHostingService } = useContext(AppContext)
const theme = useTheme() const theme = useTheme()
@ -46,10 +48,12 @@ export const UploadImage: React.FC<UploadImageProps> = ({
setUploadingFile(false) setUploadingFile(false)
bottomSheetImageRef.current?.open() bottomSheetImageRef.current?.open()
} else { } else {
onError()
setUploadingFile(false) setUploadingFile(false)
setShowNotification('imageUploadErro') setShowNotification('imageUploadErro')
} }
} else { } else {
onError()
setUploadingFile(false) setUploadingFile(false)
setShowNotification('imageUploadErro') setShowNotification('imageUploadErro')
} }
@ -72,6 +76,7 @@ export const UploadImage: React.FC<UploadImageProps> = ({
bottomSheetImageRef.current?.close() bottomSheetImageRef.current?.close()
setUploadingFile(false) setUploadingFile(false)
setShowNotification('imageUploadErro') setShowNotification('imageUploadErro')
onError()
}) })
} }
} }
@ -122,6 +127,7 @@ export const UploadImage: React.FC<UploadImageProps> = ({
onPress={() => { onPress={() => {
bottomSheetImageRef.current?.close() bottomSheetImageRef.current?.close()
setImageUpload(undefined) setImageUpload(undefined)
onError()
}} }}
> >
{t('uploadImage.cancel')} {t('uploadImage.cancel')}

View File

@ -37,6 +37,8 @@ export interface AppContextProps {
setDisplayUserDrawer: (displayUserDrawer: string | undefined) => void setDisplayUserDrawer: (displayUserDrawer: string | undefined) => void
refreshBottomBarAt?: number refreshBottomBarAt?: number
setRefreshBottomBarAt: (refreshBottomBarAt: number) => void setRefreshBottomBarAt: (refreshBottomBarAt: number) => void
longPressZap?: number | undefined
setLongPressZap: (longPressZap: number | undefined) => void
pushedTab?: string pushedTab?: string
setPushedTab: (pushedTab: string) => void setPushedTab: (pushedTab: string) => void
} }
@ -76,6 +78,8 @@ export const initialAppContext: AppContextProps = {
getSatoshiSymbol: () => <></>, getSatoshiSymbol: () => <></>,
setClipboardNip21: () => {}, setClipboardNip21: () => {},
setDisplayUserDrawer: () => {}, setDisplayUserDrawer: () => {},
longPressZap: undefined,
setLongPressZap: () => {},
} }
export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.Element => { export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.Element => {
@ -92,6 +96,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
const [imageHostingService, setImageHostingService] = React.useState<string>( const [imageHostingService, setImageHostingService] = React.useState<string>(
initialAppContext.imageHostingService, initialAppContext.imageHostingService,
) )
const [longPressZap, setLongPressZap] = React.useState<number>()
const [notificationSeenAt, setNotificationSeenAt] = React.useState<number>(0) const [notificationSeenAt, setNotificationSeenAt] = React.useState<number>(0)
const [refreshBottomBarAt, setRefreshBottomBarAt] = React.useState<number>(0) const [refreshBottomBarAt, setRefreshBottomBarAt] = React.useState<number>(0)
const [satoshi, setSatoshi] = React.useState<'kebab' | 'sats'>(initialAppContext.satoshi) const [satoshi, setSatoshi] = React.useState<'kebab' | 'sats'>(initialAppContext.satoshi)
@ -148,6 +153,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
config.image_hosting_service ?? initialAppContext.imageHostingService, config.image_hosting_service ?? initialAppContext.imageHostingService,
) )
setLanguage(config.language ?? initialAppContext.language) setLanguage(config.language ?? initialAppContext.language)
setLongPressZap(config.long_press_zap ?? initialAppContext.longPressZap)
} else { } else {
const config: Config = { const config: Config = {
show_public_images: initialAppContext.showPublicImages, show_public_images: initialAppContext.showPublicImages,
@ -158,6 +164,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
last_pets_at: 0, last_pets_at: 0,
language: initialAppContext.language, language: initialAppContext.language,
relay_coloruring: initialAppContext.relayColouring, relay_coloruring: initialAppContext.relayColouring,
long_press_zap: initialAppContext.longPressZap,
} }
SInfo.setItem('config', JSON.stringify(config), {}) SInfo.setItem('config', JSON.stringify(config), {})
} }
@ -229,6 +236,8 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
setRefreshBottomBarAt, setRefreshBottomBarAt,
pushedTab, pushedTab,
setPushedTab, setPushedTab,
longPressZap,
setLongPressZap,
}} }}
> >
{children} {children}

View File

@ -177,7 +177,7 @@ export const getUserGroupMessagesCount: (
SELECT SELECT
COUNT(*) COUNT(*)
FROM nostros_group_messages FROM nostros_group_messages
WHERE (user_mentioned != NULL AND user_mentioned = 1) WHERE user_mentioned = 1
AND (read = NULL OR read = 0) AND (read = NULL OR read = 0)
` `

View File

@ -1,3 +1,5 @@
import { endOfYesterday, format, formatDistanceToNow, fromUnixTime, isBefore } from 'date-fns'
export const handleInfinityScroll: (event: any) => boolean = (event) => { export const handleInfinityScroll: (event: any) => boolean = (event) => {
const mHeight = event.nativeEvent.layoutMeasurement.height const mHeight = event.nativeEvent.layoutMeasurement.height
const cSize = event.nativeEvent.contentSize.height const cSize = event.nativeEvent.contentSize.height
@ -81,6 +83,17 @@ export const validNip21: (string: string | undefined) => boolean = (string) => {
} }
} }
export const formatDate: (unix: number | undefined) => string = (unix) => {
if (!unix) return ''
const date = fromUnixTime(unix)
if (isBefore(date, endOfYesterday())) {
return formatDistanceToNow(fromUnixTime(unix), { addSuffix: true })
} else {
return format(date, 'HH:mm')
}
}
export const formatBigNumber: (num: number | undefined) => string = (num) => { export const formatBigNumber: (num: number | undefined) => string = (num) => {
if (num === undefined) return '' if (num === undefined) return ''

View File

@ -0,0 +1,83 @@
// Thanks to v0l/snort for the nice code!
// https://github.com/v0l/snort/blob/39fbe3b10f94b7542df01fb085e4f164aab15fca/src/Feed/VoidUpload.ts
import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getRelays, Relay } from '../../DatabaseFunctions/Relays'
import { getUnixTime } from 'date-fns'
import { Event, signEvent } from '../../../lib/nostr/Events'
import { requestInvoiceWithServiceParams, requestPayServiceParams } from 'lnurl-pay'
import axios from 'axios'
export const lightningInvoice: (
database: QuickSQLiteConnection,
lud: string,
tokens: number,
privateKey: string,
publicKey: string,
userId: string,
zap?: boolean,
zapPubkey?: string,
comment?: string,
noteId?: string,
) => Promise<string | null> = async (
database,
lud,
tokens,
privateKey,
publicKey,
userId,
zap,
zapPubkey,
comment,
noteId,
) => {
let nostr: string
if (zap && database && privateKey && publicKey && zapPubkey && userId) {
const relays: Relay[] = await getRelays(database)
const tags = [
['p', userId],
['amount', (tokens * 1000).toString()],
['relays', ...relays.map((relay) => relay.url)],
]
if (noteId) tags.push(['e', noteId])
const event: Event = {
content: comment,
created_at: getUnixTime(new Date()),
kind: 9734,
pubkey: publicKey,
tags,
}
const signedEvent = await signEvent(event, privateKey)
nostr = JSON.stringify(signedEvent)
}
const serviceParams = await requestPayServiceParams({ lnUrlOrAddress: lud })
return await new Promise<string>((resolve, reject) => {
requestInvoiceWithServiceParams({
params: serviceParams,
lnUrlOrAddress: lud,
tokens,
comment,
fetchGet: async ({ url, params }) => {
if (params && nostr && serviceParams.rawData.allowsNostr) {
params.nostr = nostr
}
const response = await axios.get(url, {
params,
})
return response.data
},
})
.then((action) => {
if (action.hasValidAmount && action.invoice) {
resolve(action.invoice)
}
})
.catch((e) => {
reject(new Error())
})
})
}

View File

@ -82,7 +82,16 @@
"imageHostingService": "Bilder Hosting Service", "imageHostingService": "Bilder Hosting Service",
"random": "zufällig", "random": "zufällig",
"language": "Sprache", "language": "Sprache",
"relayColoruring": "Relays farblich darstellen" "relayColoruring": "Relays farblich darstellen",
"app": "App",
"feed": "Feed",
"zaps": "Zaps",
"disabled": "Disabled",
"longPressZap": "Long press Zaps",
"longPressZapDescription": "Define a default amount to automatically generate invoices after a long press on the Zap button.",
"defaultZapAmount": "Defaul Zap amount",
"update": "Update",
"disable": "Disable"
}, },
"noteCard": { "noteCard": {
"answering": "{{pubkey}} antworten", "answering": "{{pubkey}} antworten",

View File

@ -82,7 +82,16 @@
"imageHostingService": "Image hosting service", "imageHostingService": "Image hosting service",
"random": "Random", "random": "Random",
"language": "Language", "language": "Language",
"relayColoruring": "Relay colouring" "relayColoruring": "Relay colouring",
"app": "App",
"feed": "Feed",
"zaps": "Zaps",
"disabled": "Disabled",
"longPressZap": "Long press Zaps",
"longPressZapDescription": "Define a default amount to automatically generate invoices after a long press on the Zap button.",
"defaultZapAmount": "Defaul Zap amount",
"update": "Update",
"disable": "Disable"
}, },
"noteCard": { "noteCard": {
"answering": "Answer to {{pubkey}}", "answering": "Answer to {{pubkey}}",

View File

@ -103,7 +103,16 @@
"imageHostingService": "Servicio de subida de imágenes", "imageHostingService": "Servicio de subida de imágenes",
"random": "Aleatorio", "random": "Aleatorio",
"language": "Idioma", "language": "Idioma",
"relayColoruring": "Coloreado de relays" "relayColoruring": "Coloreado de relays",
"app": "Aplicación",
"feed": "Feed",
"zaps": "Zaps",
"disabled": "Desabilitado",
"longPressZap": "Mantener pulsado para Zap",
"longPressZapDescription": "Define una cantidad por defecto para generar facturas LN tras mantener pulsado el botón de Zap.",
"defaultZapAmount": "Cantidad por defecto",
"update": "Actualizar",
"disable": "Desabilitar"
}, },
"noteCard": { "noteCard": {
"answering": "Responder a {{pubkey}}", "answering": "Responder a {{pubkey}}",

View File

@ -110,7 +110,16 @@
"satoshi": "Symbole de satoshi", "satoshi": "Symbole de satoshi",
"imageHostingService": "Service d'hébergement d'images", "imageHostingService": "Service d'hébergement d'images",
"language": "Langue", "language": "Langue",
"relayColoruring": "Relay coloruring" "relayColoruring": "Relay coloruring",
"app": "App",
"feed": "Feed",
"zaps": "Zaps",
"disabled": "Disabled",
"longPressZap": "Long press Zaps",
"longPressZapDescription": "Define a default amount to automatically generate invoices after a long press on the Zap button.",
"defaultZapAmount": "Defaul Zap amount",
"update": "Update",
"disable": "Disable"
}, },
"noteCard": { "noteCard": {
"answering": "Répondre à {{pubkey}}", "answering": "Répondre à {{pubkey}}",

View File

@ -103,7 +103,16 @@
"satoshi": "Satoshi symbol", "satoshi": "Satoshi symbol",
"random": "Random", "random": "Random",
"language": "Язык", "language": "Язык",
"relayColoruring": "Relay coloruring" "relayColoruring": "Relay coloruring",
"app": "App",
"feed": "Feed",
"zaps": "Zaps",
"disabled": "Disabled",
"longPressZap": "Long press Zaps",
"longPressZapDescription": "Define a default amount to automatically generate invoices after a long press on the Zap button.",
"defaultZapAmount": "Defaul Zap amount",
"update": "Update",
"disable": "Disable"
}, },
"noteCard": { "noteCard": {
"answering": "Ответить {{pubkey}}", "answering": "Ответить {{pubkey}}",

View File

@ -102,7 +102,16 @@
"imageHostingService": "图片托管服务", "imageHostingService": "图片托管服务",
"random": "随机", "random": "随机",
"language": "语言", "language": "语言",
"relayColoruring": "颜色标示中继状态" "relayColoruring": "颜色标示中继状态",
"app": "App",
"feed": "Feed",
"zaps": "Zaps",
"disabled": "Disabled",
"longPressZap": "Long press Zaps",
"longPressZapDescription": "Define a default amount to automatically generate invoices after a long press on the Zap button.",
"defaultZapAmount": "Defaul Zap amount",
"update": "Update",
"disable": "Disable"
}, },
"noteCard": { "noteCard": {
"answering": "回复 {{pubkey}}", "answering": "回复 {{pubkey}}",

View File

@ -1,7 +1,7 @@
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList, StyleSheet, Text } from 'react-native' import { FlatList, StyleSheet } from 'react-native'
import { Divider, List, Switch, useTheme } from 'react-native-paper' import { Button, Divider, List, Switch, Text, TextInput, useTheme } from 'react-native-paper'
import SInfo from 'react-native-sensitive-info' import SInfo from 'react-native-sensitive-info'
import RBSheet from 'react-native-raw-bottom-sheet' import RBSheet from 'react-native-raw-bottom-sheet'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
@ -16,6 +16,7 @@ export interface Config {
image_hosting_service: string image_hosting_service: string
language: string language: string
relay_coloruring: boolean relay_coloruring: boolean
long_press_zap: number | undefined
} }
export const ConfigPage: React.FC = () => { export const ConfigPage: React.FC = () => {
@ -35,10 +36,14 @@ export const ConfigPage: React.FC = () => {
setLanguage, setLanguage,
relayColouring, relayColouring,
setRelayColouring, setRelayColouring,
longPressZap,
setLongPressZap,
} = React.useContext(AppContext) } = React.useContext(AppContext)
const bottomSheetSatoshiRef = React.useRef<RBSheet>(null) const bottomSheetSatoshiRef = React.useRef<RBSheet>(null)
const bottomSheetImageHostingRef = React.useRef<RBSheet>(null) const bottomSheetImageHostingRef = React.useRef<RBSheet>(null)
const bottomSheetLanguageRef = React.useRef<RBSheet>(null) const bottomSheetLanguageRef = React.useRef<RBSheet>(null)
const bottomSheetLongPressZapRef = React.useRef<RBSheet>(null)
const [zapAmount, setZapAmount] = React.useState<string | undefined>(longPressZap?.toString())
React.useEffect(() => {}, [showPublicImages, showSensitive, satoshi]) React.useEffect(() => {}, [showPublicImages, showSensitive, satoshi])
@ -126,6 +131,25 @@ export const ConfigPage: React.FC = () => {
return ( return (
<> <>
<List.Item title={t('configPage.app')} />
<Divider />
<List.Item
title={t('configPage.language')}
onPress={() => bottomSheetLanguageRef.current?.open()}
right={() => <Text>{t(`language.${language}`)}</Text>}
/>
<List.Item
title={t('configPage.imageHostingService')}
onPress={() => bottomSheetImageHostingRef.current?.open()}
right={() => (
<Text style={{ color: theme.colors.onSurfaceVariant }}>
{imageHostingServices[imageHostingService]?.uri ??
t(`configPage.${imageHostingService}`)}
</Text>
)}
/>
<List.Item title={t('configPage.feed')} />
<Divider />
<List.Item <List.Item
title={t('configPage.showPublicImages')} title={t('configPage.showPublicImages')}
right={() => ( right={() => (
@ -174,23 +198,19 @@ export const ConfigPage: React.FC = () => {
/> />
)} )}
/> />
<List.Item <List.Item title={t('configPage.zaps')} />
title={t('configPage.language')} <Divider />
onPress={() => bottomSheetLanguageRef.current?.open()}
right={() => <Text>{t(`language.${language}`)}</Text>}
/>
<List.Item <List.Item
title={t('configPage.satoshi')} title={t('configPage.satoshi')}
onPress={() => bottomSheetSatoshiRef.current?.open()} onPress={() => bottomSheetSatoshiRef.current?.open()}
right={() => getSatoshiSymbol(25)} right={() => getSatoshiSymbol(25)}
/> />
<List.Item <List.Item
title={t('configPage.imageHostingService')} title={t('configPage.longPressZap')}
onPress={() => bottomSheetImageHostingRef.current?.open()} onPress={() => bottomSheetLongPressZapRef.current?.open()}
right={() => ( right={() => (
<Text style={{ color: theme.colors.onSurfaceVariant }}> <Text style={{ color: theme.colors.onSurfaceVariant }}>
{imageHostingServices[imageHostingService]?.uri ?? {longPressZap ?? t('configPage.disabled')}
t(`configPage.${imageHostingService}`)}
</Text> </Text>
)} )}
/> />
@ -225,6 +245,56 @@ export const ConfigPage: React.FC = () => {
ItemSeparatorComponent={Divider} ItemSeparatorComponent={Divider}
/> />
</RBSheet> </RBSheet>
<RBSheet
ref={bottomSheetLongPressZapRef}
closeOnDragDown={true}
customStyles={bottomSheetStyles}
>
<Text variant='titleLarge'>{t('configPage.longPressZap')}</Text>
<Text style={styles.input} variant='bodyMedium'>
{t('configPage.longPressZapDescription')}
</Text>
<TextInput
style={styles.input}
mode='outlined'
label={t('configPage.defaultZapAmount') ?? ''}
onChangeText={setZapAmount}
value={zapAmount}
/>
<Button
mode='contained'
style={styles.input}
onPress={() => {
if (zapAmount) {
SInfo.getItem('config', {}).then((result) => {
const config: Config = JSON.parse(result)
config.long_press_zap = parseInt(zapAmount, 10)
SInfo.setItem('config', JSON.stringify(config), {})
setLongPressZap(parseInt(zapAmount, 10))
})
}
bottomSheetLongPressZapRef.current?.close()
}}
>
{t('configPage.update')}
</Button>
<Button
mode='outlined'
style={styles.input}
onPress={() => {
SInfo.getItem('config', {}).then((result) => {
const config: Config = JSON.parse(result)
config.long_press_zap = undefined
SInfo.setItem('config', JSON.stringify(config), {})
setLongPressZap(undefined)
setZapAmount(undefined)
})
bottomSheetLongPressZapRef.current?.close()
}}
>
{t('configPage.disable')}
</Button>
</RBSheet>
</> </>
) )
} }
@ -234,6 +304,9 @@ const styles = StyleSheet.create({
fontFamily: 'Satoshi-Symbol', fontFamily: 'Satoshi-Symbol',
fontSize: 25, fontSize: 25,
}, },
input: {
marginTop: 16,
},
}) })
export default ConfigPage export default ConfigPage

View File

@ -20,7 +20,7 @@ import {
import { getUser, User } from '../../Functions/DatabaseFunctions/Users' import { getUser, User } from '../../Functions/DatabaseFunctions/Users'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { username, usernamePubKey, usersToTags } from '../../Functions/RelayFunctions/Users' import { username, usernamePubKey, usersToTags } from '../../Functions/RelayFunctions/Users'
import { getUnixTime, formatDistance, fromUnixTime } from 'date-fns' import { getUnixTime } from 'date-fns'
import TextContent from '../../Components/TextContent' import TextContent from '../../Components/TextContent'
import { encrypt, decrypt } from '../../lib/nostr/Nip04' import { encrypt, decrypt } from '../../lib/nostr/Nip04'
import { import {
@ -36,7 +36,7 @@ import { UserContext } from '../../Contexts/UserContext'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from '@react-navigation/native'
import { Kind } from 'nostr-tools' import { Kind } from 'nostr-tools'
import { handleInfinityScroll } from '../../Functions/NativeFunctions' import { formatDate, handleInfinityScroll } from '../../Functions/NativeFunctions'
import NostrosAvatar from '../../Components/NostrosAvatar' import NostrosAvatar from '../../Components/NostrosAvatar'
import UploadImage from '../../Components/UploadImage' import UploadImage from '../../Components/UploadImage'
import { Swipeable } from 'react-native-gesture-handler' import { Swipeable } from 'react-native-gesture-handler'
@ -246,9 +246,7 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
/> />
</View> </View>
)} )}
<Text> <Text>{formatDate(message?.created_at)}</Text>
{message?.created_at && formatDistance(fromUnixTime(message.created_at), new Date())}
</Text>
</View> </View>
</View> </View>
{message ? ( {message ? (
@ -319,9 +317,8 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
<Card <Card
style={[ style={[
styles.card, styles.card,
// FIXME: can't find this color
{ {
backgroundColor: '#001C37', backgroundColor: theme.colors.elevation.level2,
}, },
]} ]}
> >
@ -456,6 +453,7 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
setInput((prev) => `${prev} ${imageUri}`) setInput((prev) => `${prev} ${imageUri}`)
setStartUpload(false) setStartUpload(false)
}} }}
onError={() => setStartUpload(false)}
uploadingFile={uploadingFile} uploadingFile={uploadingFile}
setUploadingFile={setUploadingFile} setUploadingFile={setUploadingFile}
/> />

View File

@ -38,8 +38,7 @@ import { useTranslation } from 'react-i18next'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from '@react-navigation/native'
import ProfileData from '../../Components/ProfileData' import ProfileData from '../../Components/ProfileData'
import { fromUnixTime, formatDistance } from 'date-fns' import { formatDate, handleInfinityScroll } from '../../Functions/NativeFunctions'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
export const ConversationsFeed: React.FC = () => { export const ConversationsFeed: React.FC = () => {
const initialPageSize = 14 const initialPageSize = 14
@ -148,7 +147,7 @@ export const ConversationsFeed: React.FC = () => {
</View> </View>
<View style={styles.contactInfo}> <View style={styles.contactInfo}>
<View style={styles.contactDate}> <View style={styles.contactDate}>
<Text>{formatDistance(fromUnixTime(item.created_at), new Date())}</Text> <Text>{formatDate(item?.created_at)}</Text>
{item.pubkey !== publicKey && !item.read && <Badge size={16}></Badge>} {item.pubkey !== publicKey && !item.read && <Badge size={16}></Badge>}
</View> </View>
</View> </View>

View File

@ -13,7 +13,7 @@ import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { Event } from '../../lib/nostr/Events' import { Event } from '../../lib/nostr/Events'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatPubKey, username, usernamePubKey } from '../../Functions/RelayFunctions/Users' import { formatPubKey, username, usernamePubKey } from '../../Functions/RelayFunctions/Users'
import { getUnixTime, formatDistance, fromUnixTime } from 'date-fns' import { getUnixTime } from 'date-fns'
import TextContent from '../../Components/TextContent' import TextContent from '../../Components/TextContent'
import { import {
Card, Card,
@ -28,7 +28,7 @@ import { UserContext } from '../../Contexts/UserContext'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from '@react-navigation/native'
import { Kind } from 'nostr-tools' import { Kind } from 'nostr-tools'
import { handleInfinityScroll } from '../../Functions/NativeFunctions' import { formatDate, handleInfinityScroll } from '../../Functions/NativeFunctions'
import NostrosAvatar from '../../Components/NostrosAvatar' import NostrosAvatar from '../../Components/NostrosAvatar'
import UploadImage from '../../Components/UploadImage' import UploadImage from '../../Components/UploadImage'
import { import {
@ -304,10 +304,7 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
/> />
</View> </View>
)} )}
<Text> <Text>{formatDate(message?.created_at)}</Text>
{message?.created_at &&
formatDistance(fromUnixTime(message.created_at), new Date())}
</Text>
</View> </View>
</View> </View>
{message ? ( {message ? (
@ -367,9 +364,8 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
<Card <Card
style={[ style={[
styles.card, styles.card,
// FIXME: can't find this color
{ {
backgroundColor: '#001C37', backgroundColor: theme.colors.elevation.level2,
}, },
]} ]}
> >
@ -429,7 +425,7 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
<></> <></>
)} )}
{reply ? ( {reply ? (
<View style={[styles.reply, { backgroundColor: theme.colors.backdrop }]}> <View style={[styles.reply, { backgroundColor: theme.colors.elevation.level2 }]}>
<MaterialCommunityIcons <MaterialCommunityIcons
name='reply' name='reply'
size={25} size={25}
@ -502,6 +498,7 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
setInput((prev) => `${prev} ${imageUri}`) setInput((prev) => `${prev} ${imageUri}`)
setStartUpload(false) setStartUpload(false)
}} }}
onError={() => setStartUpload(false)}
uploadingFile={uploadingFile} uploadingFile={uploadingFile}
setUploadingFile={setUploadingFile} setUploadingFile={setUploadingFile}
/> />
@ -542,6 +539,7 @@ const styles = StyleSheet.create({
scaleY: -1, scaleY: -1,
paddingLeft: 16, paddingLeft: 16,
paddingRight: 16, paddingRight: 16,
paddingBottom: 3,
}, },
cardContentDate: { cardContentDate: {
flexDirection: 'row', flexDirection: 'row',

View File

@ -88,11 +88,13 @@ export const HomePage: React.FC = () => {
const goToEvent: () => void = () => { const goToEvent: () => void = () => {
if (clipboardNip21) { if (clipboardNip21) {
const key = decode(clipboardNip21.replace('nostr:', '')) const key = decode(clipboardNip21.replace('nostr:', ''))
if (key) { if (key?.data) {
if (key.type === 'nevent') { if (key.type === 'nevent') {
navigate('Note', { noteId: key.data }) navigate('Note', { noteId: key.data })
} else if (key.type === 'nprofile' || key.type === 'npub') { } else if (key.type === 'npub') {
navigate('Profile', { pubKey: key.data }) navigate('Profile', { pubKey: key.data })
} else if (key.type === 'nprofile' && key.data.pubkey) {
navigate('Profile', { pubKey: key.data.pubkey })
} }
} }
} }

View File

@ -22,6 +22,7 @@ import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from '@react-navigation/native'
import ProfileData from '../../Components/ProfileData' import ProfileData from '../../Components/ProfileData'
import ProfileActions from '../../Components/ProfileActions' import ProfileActions from '../../Components/ProfileActions'
import TextContent from '../../Components/TextContent'
interface ProfilePageProps { interface ProfilePageProps {
route: { params: { pubKey: string } } route: { params: { pubKey: string } }
@ -170,7 +171,7 @@ export const ProfilePage: React.FC<ProfilePageProps> = ({ route }) => {
</View> </View>
</View> </View>
<View> <View>
<Text>{user?.about}</Text> <TextContent content={user?.about} showPreview={false} numberOfLines={10} />
</View> </View>
<Divider style={styles.divider} /> <Divider style={styles.divider} />
<View style={styles.profileActions}> <View style={styles.profileActions}>

View File

@ -219,6 +219,7 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
setContent((prev) => `${prev}\n\n${imageUri}`) setContent((prev) => `${prev}\n\n${imageUri}`)
setStartUpload(false) setStartUpload(false)
}} }}
onError={() => setStartUpload(false)}
uploadingFile={uploadingFile} uploadingFile={uploadingFile}
setUploadingFile={setUploadingFile} setUploadingFile={setUploadingFile}
/> />

View File

@ -1,4 +1,11 @@
import { decode, EventPointer, neventEncode, npubEncode, ProfilePointer } from 'nostr-tools/nip19' import {
decode,
EventPointer,
neventEncode,
nprofileEncode,
npubEncode,
ProfilePointer,
} from 'nostr-tools/nip19'
export function getNpub(key: string | undefined): string { export function getNpub(key: string | undefined): string {
if (!key) return '' if (!key) return ''
@ -13,6 +20,21 @@ export function getNpub(key: string | undefined): string {
return key return key
} }
export function getNprofile(key: string, relays: string[]): string {
if (!key) return ''
try {
return nprofileEncode({
pubkey: key,
relays,
})
} catch {
console.log('Error encoding')
}
return key
}
export function getNevent(key: string | undefined): string { export function getNevent(key: string | undefined): string {
if (!key) return '' if (!key) return ''
if (isPublicKey(key)) return key if (isPublicKey(key)) return key

View File

@ -34,17 +34,49 @@ export interface ResilientAssignation {
export const fallbackRelays = [ export const fallbackRelays = [
'wss://brb.io', 'wss://brb.io',
'wss://damus.io',
'wss://nostr-pub.wellorder.net', 'wss://nostr-pub.wellorder.net',
'wss://nostr.swiss-enigma.ch',
'wss://nostr.onsats.org',
'wss://nostr-pub.semisol.dev',
'wss://nostr.openchain.fr',
'wss://relay.nostr.info',
'wss://nostr.oxtr.dev',
'wss://nostr.ono.re',
'wss://relay.grunch.dev',
'wss://nostr.developer.li', 'wss://nostr.developer.li',
'wss://nostr.oxtr.dev',
'wss://nostr.swiss-enigma.ch',
'wss://relay.nostr.snblago.com',
'wss://nos.lol',
'wss://relay.austrich.net',
'wss://nostr.cro.social',
'wss://relay.koreus.social',
'wss://spore.ws',
'wss://nostr.web3infra.xyz',
'wss://nostr.snblago.com',
'wss://relay.nostrified.org',
'wss://relay.ryzizub.com',
'wss://relay.wellorder.net',
'wss://nostr.btcmp.com',
'wss://relay.nostromo.social',
'wss://relay.stoner.com',
'wss://nostr.massmux.com',
'wss://nostr.robotesc.ro',
'wss://relay.humanumest.social',
'wss://relay-local.cowdle.gg',
'wss://nostr-2.afarazit.eu',
'wss://nostr.data.haus',
'wss://nostr-pub.wellorder.net',
'wss://nostr.thank.eu',
'wss://relay-dev.cowdle.gg',
'wss://nostrsxz4lbwe-nostr.functions.fnc.fr-par.scw.cloud',
'wss://relay.nostrcheck.me',
'wss://relay.nostrich.de',
'wss://nostr.com.de',
'wss://relay.nostr.scot',
'wss://nostr.8e23.net',
'wss://nostr.mouton.dev',
'wss://nostr.l00p.org',
'wss://nostr.island.network',
'wss://nostr.handyjunky.com',
'wss://relay.valera.co',
'wss://relay.nostr.vet',
'wss://tmp-relay.cesc.trade',
'wss://relay.dwadziesciajeden.pl',
'wss://nostr-1.afarazit.eu',
'wss://lbrygen.xyz',
] ]
class RelayPool { class RelayPool {