New UI navigation bar and profile config (#120)

This commit is contained in:
KoalaSat 2023-01-14 20:36:14 +00:00 committed by GitHub
commit 28f404983f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1591 additions and 1128 deletions

View File

@ -1,48 +1,20 @@
import React from 'react'
import { Layout, useTheme } from '@ui-kitten/components'
import { Image, StyleSheet, Text } from 'react-native'
import { stringToColour } from '../../Functions/NativeFunctions'
import { StyleSheet } from 'react-native'
import { Avatar as PaperAvatar, useTheme } from 'react-native-paper'
interface AvatarProps {
pubKey: string
src?: string
name?: string
pubKey: string
size?: number
lud06?: string
}
export const Avatar: React.FC<AvatarProps> = ({ src, name, pubKey, size = 50 }) => {
export const NostrosAvatar: React.FC<AvatarProps> = ({ src, name, pubKey, size = 40, lud06 }) => {
const theme = useTheme()
const displayName = name && name !== '' ? name : pubKey
const styles = StyleSheet.create({
layout: {
flexDirection: 'row',
alignContent: 'center',
width: size,
height: size,
backgroundColor: 'transparent',
},
image: {
width: size,
height: size,
borderRadius: 100,
},
textAvatarLayout: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
width: size,
height: size,
borderRadius: 100,
backgroundColor: stringToColour(pubKey),
},
textAvatar: {
fontSize: size / 2,
alignContent: 'center',
color: theme['text-basic-color'],
textTransform: 'uppercase',
},
})
const hasLud06 = lud06 && lud06 !== ''
const lud06IconSize = size / 2.85
const validImage: () => boolean = () => {
if (src) {
const regexp = /^(https?:\/\/.*\.(?:png|jpg|jpeg))$/
@ -53,16 +25,30 @@ export const Avatar: React.FC<AvatarProps> = ({ src, name, pubKey, size = 50 })
}
return (
<Layout style={styles.layout}>
<>
{validImage() ? (
<Image style={styles.image} source={{ uri: src }} />
<PaperAvatar.Image size={size} source={{ uri: src }} />
) : (
<Layout style={styles.textAvatarLayout}>
<Text style={styles.textAvatar}>{displayName.substring(0, 2)}</Text>
</Layout>
<PaperAvatar.Text size={size} label={displayName} />
)}
</Layout>
{hasLud06 && (
<PaperAvatar.Icon
size={lud06IconSize}
icon='lightning-bolt'
style={[
styles.iconLightning,
{ right: -(size - lud06IconSize), backgroundColor: theme.colors.secondaryContainer, top: lud06IconSize * -1 },
]}
color='#F5D112'
/>
)}
</>
)
}
export default Avatar
const styles = StyleSheet.create({
iconLightning: {
},
})
export default NostrosAvatar

View File

@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'
import { requestInvoice } from 'lnurl-pay'
import { Button, Card, Input, Layout, Modal, Text } from '@ui-kitten/components'
import QRCode from 'react-native-qrcode-svg'
import { Event } from '../../lib/nostr/Events'
import { User } from '../../Functions/DatabaseFunctions/Users'
import { Clipboard, Linking, StyleSheet } from 'react-native'
import { Clipboard, Linking, StyleSheet, View } from 'react-native'
import { useTranslation } from 'react-i18next'
import { showMessage } from 'react-native-flash-message'
import RBSheet from 'react-native-raw-bottom-sheet'
import { Button, Card, IconButton, Text, TextInput, useTheme } from 'react-native-paper'
interface TextContentProps {
open: boolean
@ -15,73 +16,41 @@ interface TextContentProps {
}
export const LnPayment: React.FC<TextContentProps> = ({ open, setOpen, event, user }) => {
const theme = useTheme()
const { t } = useTranslation('common')
const bottomSheetLnPaymentRef = React.useRef<RBSheet>(null)
const bottomSheetInvoiceRef = React.useRef<RBSheet>(null)
const [monto, setMonto] = useState<string>('')
const defaultComment = event?.id ? `Tip for Nostr event ${event?.id}` : ''
const [comment, setComment] = useState<string>(defaultComment)
const [invoice, setInvoice] = useState<string>()
const [loading, setLoading] = useState<boolean>(false)
useEffect(() => {
setMonto('')
setInvoice(undefined)
if (open) {
bottomSheetLnPaymentRef.current?.open()
} else {
bottomSheetLnPaymentRef.current?.close()
bottomSheetInvoiceRef.current?.close()
}
}, [open])
useEffect(() => {
setComment(defaultComment)
}, [event, open])
const styles = StyleSheet.create({
modal: {
paddingLeft: 32,
paddingRight: 32,
width: '100%',
},
backdrop: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
input: {
marginTop: 31,
},
modalContainer: {
marginBottom: 15,
},
buttonsContainer: {
flexDirection: 'row',
marginTop: 31,
},
buttonLeft: {
flex: 3,
paddingRight: 16,
},
buttonRight: {
flex: 3,
paddingLeft: 16,
},
buttonMonto: {
flex: 2,
},
buttonMontoMiddle: {
flex: 2,
marginLeft: 10,
marginRight: 10,
},
satoshi: {
fontFamily: 'Satoshi-Symbol',
},
})
const copyInvoice: (invoice: string) => void = (invoice) => {
Clipboard.setString(invoice)
showMessage({
message: t('alerts.invoiceCopied'),
type: 'success',
})
const copyInvoice: () => void = () => {
console.log(invoice)
Clipboard.setString(invoice ?? '')
}
const openApp: (invoice: string) => void = (invoice) => {
const openApp: () => void = () => {
Linking.openURL(`lightning:${invoice}`)
}
const generateInvoice: (copy: boolean) => void = async (copy) => {
const generateInvoice: () => void = async () => {
if (user?.lnurl && monto !== '') {
setLoading(true)
requestInvoice({
@ -91,98 +60,161 @@ export const LnPayment: React.FC<TextContentProps> = ({ open, setOpen, event, us
})
.then((action) => {
if (action.hasValidAmount && action.invoice) {
copy ? copyInvoice(action.invoice) : openApp(action.invoice)
} else {
showMessage({
message: t('alerts.invoiceError'),
type: 'danger',
})
setInvoice(action.invoice)
bottomSheetInvoiceRef.current?.open()
}
setLoading(false)
setOpen(false)
setMonto('')
setComment('')
})
.catch(() => setLoading(false))
}
}
const rbSheetCustomStyles = React.useMemo(() => {
return {
container: {
...styles.rbsheetContainer,
backgroundColor: theme.colors.background,
},
draggableIcon: styles.rbsheetDraggableIcon,
}
}, [])
return user?.lnurl ? (
<Modal
style={styles.modal}
visible={open}
backdropStyle={styles.backdrop}
onBackdropPress={() => setOpen(false)}
>
<Card disabled={true}>
<Layout style={styles.modalContainer}>
<Layout style={styles.buttonsContainer}>
<Button style={styles.buttonMonto} onPress={() => setMonto('1000')}>
<>
<RBSheet
ref={bottomSheetLnPaymentRef}
closeOnDragDown={true}
height={330}
customStyles={rbSheetCustomStyles}
onClose={() => setOpen(false)}
>
<View>
<View style={styles.montoSelection}>
<Button style={styles.montoButton} mode='outlined' onPress={() => setMonto('1000')}>
<>
<Text style={styles.satoshi}>s</Text>
<Text> 1k</Text>
</>
</Button>
<Button style={styles.buttonMontoMiddle} onPress={() => setMonto('5000')}>
<Button style={styles.montoButton} mode='outlined' onPress={() => setMonto('5000')}>
<>
<Text style={styles.satoshi}>s</Text>
<Text> 5k</Text>
</>
</Button>
<Button style={styles.buttonMonto} onPress={() => setMonto('10000')}>
<Button style={styles.montoButton} mode='outlined' onPress={() => setMonto('10000')}>
<>
<Text style={styles.satoshi}>s</Text>
<Text> 10k</Text>
</>
</Button>
</Layout>
<Layout style={styles.input}>
<Input
value={monto}
onChangeText={(text) => {
if (/^\d+$/.test(text)) {
setMonto(text)
}
}}
size='large'
placeholder={t('lnPayment.monto')}
accessoryLeft={() => <Text style={styles.satoshi}>s</Text>}
/>
</Layout>
<Layout style={styles.input}>
<Input
value={comment}
onChangeText={setComment}
placeholder={t('lnPayment.comment')}
size='large'
/>
</Layout>
<Layout style={styles.buttonsContainer}>
<Layout style={styles.buttonLeft}>
<Button
onPress={() => generateInvoice(true)}
appearance='ghost'
disabled={loading || monto === ''}
>
{t('lnPayment.copy')}
</Button>
</Layout>
<Layout style={styles.buttonRight}>
<Button
onPress={() => generateInvoice(false)}
status='warning'
disabled={loading || monto === ''}
>
{t('lnPayment.openApp')}
</Button>
</Layout>
</Layout>
</Layout>
</Card>
</Modal>
</View>
<TextInput
mode='outlined'
label={t('lnPayment.monto') ?? ''}
onChangeText={setMonto}
value={monto}
/>
<TextInput
mode='outlined'
label={t('lnPayment.comment') ?? ''}
onChangeText={setComment}
value={comment}
/>
<Button
mode='contained'
disabled={loading || monto === ''}
onPress={() => generateInvoice()}
>
{t('lnPayment.generateInvoice')}
</Button>
<Button mode='outlined' onPress={() => setOpen(false)}>
{t('lnPayment.cancel')}
</Button>
</View>
</RBSheet>
<RBSheet
ref={bottomSheetInvoiceRef}
closeOnDragDown={true}
height={630}
customStyles={rbSheetCustomStyles}
onClose={() => setOpen(false)}
>
<Card style={styles.qrContainer}>
<Card.Content>
<View>
<QRCode value={invoice} size={350} />
</View>
<View style={styles.qrText}>
<Text variant='titleMedium' style={styles.satoshi}>
s
</Text>
<Text variant='titleMedium'>{monto}</Text>
</View>
{comment && (
<View style={styles.qrText}>
<Text>{comment}</Text>
</View>
)}
</Card.Content>
</Card>
<View style={styles.cardActions}>
<View style={styles.actionButton}>
<IconButton icon='content-copy' size={28} onPress={copyInvoice} />
<Text>{t('profileConfigPage.copyNPub')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton icon='wallet' size={28} onPress={openApp} />
<Text>{t('profileConfigPage.invoice')}</Text>
</View>
<View style={styles.actionButton}></View>
<View style={styles.actionButton}></View>
</View>
</RBSheet>
</>
) : (
<></>
)
}
const styles = StyleSheet.create({
qrContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
qrText: {
marginTop: 20,
flexDirection: 'row',
justifyContent: 'center',
},
rbsheetDraggableIcon: {
backgroundColor: '#000',
},
rbsheetContainer: {
padding: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
},
satoshi: {
fontFamily: 'Satoshi-Symbol',
fontSize: 20,
},
montoSelection: {
flexDirection: 'row',
},
montoButton: {
flex: 2,
},
actionButton: {
justifyContent: 'center',
alignItems: 'center',
width: 80,
},
cardActions: {
flexDirection: 'row',
justifyContent: 'space-around',
},
})
export default LnPayment

View File

@ -1,84 +1,52 @@
import * as React from 'react'
import { StyleSheet } from 'react-native'
import { StyleSheet, View } from 'react-native'
import { DrawerContentScrollView } from '@react-navigation/drawer'
import { Button, Drawer, Text, useTheme } from 'react-native-paper'
import {
Button,
Card,
Chip,
Drawer,
IconButton,
Text,
TouchableRipple,
useTheme,
} from 'react-native-paper'
import Logo from '../Logo'
import { useTranslation } from 'react-i18next'
import SInfo from 'react-native-sensitive-info'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { dropTables } from '../../Functions/DatabaseFunctions'
import { AppContext } from '../../Contexts/AppContext'
import { UserContext } from '../../Contexts/UserContext'
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types'
interface ItemList {
label: string
icon: string
key: number
right?: () => JSX.Element
}
import { navigate } from '../../lib/Navigation'
import NostrosAvatar from '../Avatar'
import { formatPubKey } from '../../Functions/RelayFunctions/Users'
interface MenuItemsProps {
navigation: DrawerNavigationHelpers;
navigation: DrawerNavigationHelpers
}
export const MenuItems: React.FC<MenuItemsProps> = ({ navigation }) => {
const [drawerItemIndex, setDrawerItemIndex] = React.useState<number>(-1)
const { goToPage, database, init } = React.useContext(AppContext)
const { setPrivateKey, setPublicKey, relayPool, publicKey } = React.useContext(RelayPoolContext)
const { relays } = React.useContext(RelayPoolContext)
const { nPub, publicKey, user, contactsCount, followersCount, logout } =
React.useContext(UserContext)
const { t } = useTranslation('common')
const theme = useTheme()
const onPressLogout: () => void = () => {
if (database) {
relayPool?.unsubscribeAll()
setPrivateKey(undefined)
setPublicKey(undefined)
dropTables(database).then(() => {
SInfo.deleteItem('privateKey', {}).then(() => {
SInfo.deleteItem('publicKey', {}).then(() => {
init()
goToPage('landing', true)
})
})
})
}
logout()
}
const onPressItem: (index:number) => void = (index) => {
const onPressItem: (key: string, index: number) => void = (key, index) => {
setDrawerItemIndex(index)
const pagesIndex = [
'Relays',
'Config',
'About'
]
navigation.navigate(pagesIndex[index])
}
const relaysRightButton: () => JSX.Element = () => {
if (!relayPool || relayPool?.relays.length < 1) {
return <Text style={{color: theme.colors.error}}>{t('menuItems.notConnected')}</Text>
if (key === 'relays') {
navigate('Relays')
} else if (key === 'config') {
navigate('Feed', { page: 'Config' })
} else if (key === 'about') {
navigate('About')
}
return <Text style={{color: theme.colors.inversePrimary}}>{t('menuItems.connectedRelays', { number: relayPool?.relays.length.toString()})}</Text>
}
const DrawerItemsData = React.useMemo(
() => {
if (!publicKey) return []
const defaultList: ItemList[] = [
{ label: t('menuItems.relays'), icon: 'message-question-outline', key: 0, right: relaysRightButton},
{ label: t('menuItems.configuration'), icon: 'cog-outline', key: 1 }
]
return defaultList
},
[publicKey],
)
const DrawerBottomItemsData = React.useMemo(
() => [{ label: t('menuItems.about'), icon: 'message-question-outline', key: 2 }],
[],
)
return (
<>
<DrawerContentScrollView
@ -94,30 +62,85 @@ export const MenuItems: React.FC<MenuItemsProps> = ({ navigation }) => {
<Drawer.Section showDivider={false}>
<Logo />
</Drawer.Section>
<Drawer.Section showDivider={publicKey !== undefined}>
{DrawerItemsData.map((props, index) => (
{nPub && (
<Card style={styles.cardContainer}>
<Card.Content style={styles.cardContent}>
<TouchableRipple onPress={() => navigate('Profile')}>
<View style={styles.cardContent}>
<View style={styles.cardAvatar}>
<NostrosAvatar
name={user?.name}
pubKey={nPub}
src={user?.picture}
lud06={user?.lnurl}
/>
</View>
<View>
<Text variant='titleMedium'>{user?.name}</Text>
<Text>{formatPubKey(nPub)}</Text>
</View>
</View>
</TouchableRipple>
<View style={styles.cardEdit}>
<IconButton icon='pencil' size={20} onPress={() => navigate('ProfileConfig')} />
</View>
</Card.Content>
<Card.Content style={styles.cardActions}>
<Chip
compact={true}
style={styles.cardActionsChip}
onPress={() => console.log('Pressed')}
>
{t('menuItems.following', { following: contactsCount })}
</Chip>
<Chip
compact={true}
style={styles.cardActionsChip}
onPress={() => console.log('Pressed')}
>
{t('menuItems.followers', { followers: followersCount })}
</Chip>
</Card.Content>
</Card>
)}
{publicKey && (
<Drawer.Section>
<Drawer.Item
label={props.label}
icon={props.icon}
key={props.key}
active={drawerItemIndex === index}
onPress={() => onPressItem(index)}
label={t('menuItems.relays')}
icon='message-question-outline'
key='relays'
active={drawerItemIndex === 0}
onPress={() => onPressItem('relays', 0)}
onTouchEnd={() => setDrawerItemIndex(-1)}
right={props.right}
right={() =>
relays.length < 1 ? (
<Text style={{ color: theme.colors.error }}>{t('menuItems.notConnected')}</Text>
) : (
<Text style={{ color: theme.colors.inversePrimary }}>
{t('menuItems.connectedRelays', { number: relays.length })}
</Text>
)
}
/>
))}
</Drawer.Section>
{/* <Drawer.Item
label={t('menuItems.configuration')}
icon='cog-outline'
key='config'
active={drawerItemIndex === 1}
onPress={() => onPressItem('config', 1)}
onTouchEnd={() => setDrawerItemIndex(-1)}
/> */}
</Drawer.Section>
)}
<Drawer.Section showDivider={false}>
{DrawerBottomItemsData.map((props, index) => (
<Drawer.Item
label={props.label}
icon={props.icon}
key={props.key}
active={drawerItemIndex === DrawerItemsData.length + index}
onPress={() => onPressItem(DrawerItemsData.length + index)}
onTouchEnd={() => setDrawerItemIndex(-1)}
/>
))}
<Drawer.Item
label={t('menuItems.about')}
icon='message-question-outline'
key='about'
active={drawerItemIndex === 2}
onPress={() => onPressItem('about', 2)}
onTouchEnd={() => setDrawerItemIndex(-1)}
/>
</Drawer.Section>
</DrawerContentScrollView>
{publicKey && (
@ -142,12 +165,34 @@ export const MenuItems: React.FC<MenuItemsProps> = ({ navigation }) => {
const styles = StyleSheet.create({
drawerContent: {
flex: 1,
borderTopRightRadius: 28
borderTopRightRadius: 28,
},
cardContainer: {
margin: 12,
},
cardActions: {
flexDirection: 'row',
justifyContent: 'space-between',
},
cardActionsChip: {
width: '47%',
},
cardAvatar: {
marginRight: 14,
},
cardContent: {
width: '100%',
flexDirection: 'row',
},
cardEdit: {
flexDirection: 'row',
justifyContent: 'flex-end',
flex: 1,
},
bottomSection: {
padding: 24,
marginBottom: 0,
borderBottomRightRadius: 28
borderBottomRightRadius: 28,
padding: 24,
},
})

View File

@ -3,10 +3,11 @@ import { BottomNavigation, BottomNavigationTab, useTheme } from '@ui-kitten/comp
import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { UserContext } from '../../Contexts/UserContext'
export const NavigationBar: React.FC = () => {
const { goToPage, getActualPage, page } = useContext(AppContext)
const { publicKey, privateKey } = useContext(RelayPoolContext)
const { publicKey, privateKey } = React.useContext(UserContext)
const theme = useTheme()
const profilePage = `profile#${publicKey ?? ''}`

View File

@ -35,7 +35,8 @@ export const NoteCard: React.FC<NoteCardProps> = ({
onlyContactsReplies = false,
}) => {
const theme = useTheme()
const { relayPool, publicKey, privateKey, lastEventId } = useContext(RelayPoolContext)
const { publicKey, privateKey } = React.useContext(UserContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
const { database, goToPage } = useContext(AppContext)
const [relayAdded, setRelayAdded] = useState<boolean>(false)
const [replies, setReplies] = useState<Note[]>([])

View File

@ -2,13 +2,8 @@ import React, { useEffect, useState } from 'react'
import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { initDatabase } from '../Functions/DatabaseFunctions'
import SInfo from 'react-native-sensitive-info'
import { BackHandler } from 'react-native'
export interface AppContextProps {
page: string
goToPage: (path: string, root?: boolean) => void
goBack: () => void
getActualPage: () => string
init: () => void
loadingDb: boolean
database: QuickSQLiteConnection | null
@ -19,65 +14,29 @@ export interface AppContextProviderProps {
}
export const initialAppContext: AppContextProps = {
page: '',
init: () => {},
goToPage: () => {},
getActualPage: () => '',
goBack: () => {},
loadingDb: true,
database: null,
}
export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.Element => {
const [page, setPage] = useState<string>(initialAppContext.page)
const [database, setDatabase] = useState<QuickSQLiteConnection | null>(null)
const [loadingDb, setLoadingDb] = useState<boolean>(initialAppContext.loadingDb)
const init: () => void = () => {
const db = initDatabase()
setDatabase(db)
SInfo.getItem('privateKey', {}).then(() => {
SInfo.getItem('publicKey', {}).then((value) => {
setLoadingDb(false)
})
}
useEffect(init, [])
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', () => {
goBack()
return true
})
}, [page])
const goToPage: (path: string, root?: boolean) => void = (path, root) => {
if (page !== '' && !root) {
setPage(`${page}%${path}`)
} else {
setPage(path)
}
}
const goBack: () => void = () => {
const breadcrump = page.split('%')
if (breadcrump.length > 1) {
setPage(breadcrump.slice(0, -1).join('%'))
}
}
const getActualPage: () => string = () => {
const breadcrump = page.split('%')
return breadcrump[breadcrump.length - 1]
}
return (
<AppContext.Provider
value={{
page,
init,
goToPage,
goBack,
getActualPage,
loadingDb,
database,
}}

View File

@ -1,21 +1,20 @@
import React, { useContext, useEffect, useMemo, useState } from 'react'
import RelayPool from '../lib/nostr/RelayPool/intex'
import { AppContext } from './AppContext'
import SInfo from 'react-native-sensitive-info'
import { getPublickey } from '../lib/nostr/Bip'
import { DeviceEventEmitter } from 'react-native'
import debounce from 'lodash.debounce'
import { getRelays, Relay } from '../Functions/DatabaseFunctions/Relays'
import { UserContext } from './UserContext'
export interface RelayPoolContextProps {
loadingRelayPool: boolean
relayPool?: RelayPool
setRelayPool: (relayPool: RelayPool) => void
publicKey?: string
setPublicKey: (privateKey: string | undefined) => void
privateKey?: string
setPrivateKey: (privateKey: string | undefined) => void
lastEventId?: string
lastConfirmationtId?: string
relays: Relay[]
addRelayItem: (relay: Relay) => Promise<void>
removeRelayItem: (relay: Relay) => Promise<void>
}
export interface WebsocketEvent {
@ -29,26 +28,26 @@ export interface RelayPoolContextProviderProps {
export const initialRelayPoolContext: RelayPoolContextProps = {
loadingRelayPool: true,
setPublicKey: () => {},
setPrivateKey: () => {},
setRelayPool: () => {},
addRelayItem: async () => await new Promise(() => {}),
removeRelayItem: async () => await new Promise(() => {}),
relays: []
}
export const RelayPoolContextProvider = ({
children,
images,
}: RelayPoolContextProviderProps): JSX.Element => {
const { database, loadingDb, goToPage, page } = useContext(AppContext)
const { database } = useContext(AppContext)
const { publicKey, privateKey } = React.useContext(UserContext)
const [publicKey, setPublicKey] = useState<string>()
const [privateKey, setPrivateKey] = useState<string>()
const [relayPool, setRelayPool] = useState<RelayPool>()
const [loadingRelayPool, setLoadingRelayPool] = useState<boolean>(
initialRelayPoolContext.loadingRelayPool,
)
const [lastEventId, setLastEventId] = useState<string>('')
const [lastConfirmationtId, setLastConfirmationId] = useState<string>('')
const [lastPage, setLastPage] = useState<string>(page)
const [relays, setRelays] = React.useState<Relay[]>([])
const changeEventIdHandler: (event: WebsocketEvent) => void = (event) => {
setLastEventId(event.eventId)
@ -74,52 +73,44 @@ export const RelayPoolContextProvider = ({
initRelayPool.connect(publicKey, (eventId: string) => setLastEventId(eventId))
setRelayPool(initRelayPool)
setLoadingRelayPool(false)
loadRelays()
}
}
useEffect(() => {
if (relayPool && lastPage !== page) {
setLastPage(page)
const loadRelays: () => void = () => {
if (database) {
getRelays(database).then((results) => setRelays(results))
}
}, [page])
}
const addRelayItem: (relay: Relay) => Promise<void> = async (relay) => {
return await new Promise((resolve, _reject) => {
if (relayPool && database && publicKey) {
relayPool.add(relay.url, () => {
setRelays((prev) => [...prev, relay])
resolve()
})
}
})
}
const removeRelayItem: (relay: Relay) => Promise<void> = async (relay) => {
return await new Promise((resolve, _reject) => {
if (relayPool && database && publicKey) {
relayPool.remove(relay.url, () => {
setRelays((prev) => prev.filter((item) => item.url !== relay.url))
resolve()
})
}
})
}
useEffect(() => {
if (publicKey && publicKey !== '') {
SInfo.setItem('publicKey', publicKey, {})
if (!loadingRelayPool && page !== 'landing') {
goToPage('home', true)
} else {
loadRelayPool()
}
loadRelayPool()
}
}, [publicKey, loadingRelayPool])
useEffect(() => {
if (privateKey && privateKey !== '') {
SInfo.setItem('privateKey', privateKey, {})
const publicKey: string = getPublickey(privateKey)
setPublicKey(publicKey)
}
}, [privateKey])
useEffect(() => {
if (!loadingDb) {
SInfo.getItem('privateKey', {}).then((privateResult) => {
if (privateResult && privateResult !== '') {
setPrivateKey(privateResult)
setPublicKey(getPublickey(privateResult))
} else {
SInfo.getItem('publicKey', {}).then((publicResult) => {
if (publicResult && publicResult !== '') {
setPublicKey(publicResult)
} else {
goToPage('landing', true)
}
})
}
})
}
}, [loadingDb])
}, [publicKey])
return (
<RelayPoolContext.Provider
@ -127,12 +118,11 @@ export const RelayPoolContextProvider = ({
loadingRelayPool,
relayPool,
setRelayPool,
publicKey,
setPublicKey,
privateKey,
setPrivateKey,
lastEventId,
lastConfirmationtId,
relays,
addRelayItem,
removeRelayItem
}}
>
{children}

View File

@ -0,0 +1,143 @@
import React, { useContext, useEffect, useState } from 'react'
import { getPublickey } from '../lib/nostr/Bip'
import SInfo from 'react-native-sensitive-info'
import { RelayPoolContext } from './RelayPoolContext'
import { AppContext } from './AppContext'
import { getContactsCount, getFollowersCount, getUser, User } from '../Functions/DatabaseFunctions/Users'
import { dropTables } from '../Functions/DatabaseFunctions'
import { navigate, jumpTo } from '../lib/Navigation'
import { npubEncode, nsecEncode } from 'nostr-tools/nip19'
export interface UserContextProps {
nPub?: string
nSec?: string
publicKey?: string
setPublicKey: (privateKey: string | undefined) => void
privateKey?: string
setPrivateKey: (privateKey: string | undefined) => void
setUser: (user: User) => void
user?: User,
contactsCount: number,
followersCount: number,
reloadUser: () => void
logout: () => void
}
export interface UserContextProviderProps {
children: React.ReactNode
}
export const initialUserContext: UserContextProps = {
setPublicKey: () => {},
setPrivateKey: () => {},
setUser: () => {},
reloadUser: () => {},
logout: () => {},
contactsCount: 0,
followersCount: 0
}
export const UserContextProvider = ({ children }: UserContextProviderProps): JSX.Element => {
const { database, loadingDb, init } = useContext(AppContext)
const { relayPool } = React.useContext(RelayPoolContext)
const [publicKey, setPublicKey] = useState<string>()
const [nPub, setNpub] = useState<string>()
const [nSec, setNsec] = useState<string>()
const [privateKey, setPrivateKey] = useState<string>()
const [user, setUser] = React.useState<User>()
const [contactsCount, setContantsCount] = React.useState<number>(0)
const [followersCount, setFollowersCount] = React.useState<number>(0)
const reloadUser: () => void = () => {
if (database && publicKey) {
getUser(publicKey, database).then((result) => {
if (result) setUser(result)
})
getContactsCount(database).then(setContantsCount)
getFollowersCount(database).then(setFollowersCount)
}
}
const logout: () => void = () => {
if (database) {
relayPool?.unsubscribeAll()
setPrivateKey(undefined)
setPublicKey(undefined)
setNpub(undefined)
setNsec(undefined)
setUser(undefined)
dropTables(database).then(() => {
SInfo.deleteItem('privateKey', {}).then(() => {
SInfo.deleteItem('publicKey', {}).then(() => {
init()
navigate('Home', { screen: 'ProfileConnect' })
})
})
})
}
}
useEffect(() => {
if (privateKey && privateKey !== '') {
SInfo.setItem('privateKey', privateKey, {})
setNsec(nsecEncode(privateKey))
const publicKey: string = getPublickey(privateKey)
setPublicKey(publicKey)
}
}, [privateKey])
useEffect(() => {
if (publicKey && publicKey !== '') {
SInfo.setItem('publicKey', publicKey, {})
setNpub(npubEncode(publicKey))
reloadUser()
}
}, [publicKey])
useEffect(() => {
if (user) {
navigate('Feed')
}
}, [user])
useEffect(() => {
if (!loadingDb ) {
SInfo.getItem('privateKey', {}).then((privateResult) => {
if (privateResult && privateResult !== '') {
setPrivateKey(privateResult)
setPublicKey(getPublickey(privateResult))
} else {
SInfo.getItem('publicKey', {}).then((publicResult) => {
if (publicResult && publicResult !== '') {
setPublicKey(publicResult)
jumpTo('Feed')
}
})
}
})
}
}, [loadingDb])
return (
<UserContext.Provider
value={{
nSec,
nPub,
setUser,
publicKey,
setPublicKey,
privateKey,
setPrivateKey,
user,
contactsCount,
followersCount,
reloadUser,
logout
}}
>
{children}
</UserContext.Provider>
)
}
export const UserContext = React.createContext(initialUserContext)

View File

@ -37,7 +37,7 @@ export const getReactionsCount: (
const resultSet = await db.execute(notesQuery)
const item: { 'COUNT(*)': number } = resultSet?.rows?.item(0)
return item['COUNT(*)']
return item['COUNT(*)'] ?? 0
}
export const getUserReaction: (

View File

@ -28,3 +28,4 @@ export const getRelays: (db: QuickSQLiteConnection) => Promise<Relay[]> = async
const relays: Relay[] = items.map((object) => databaseToEntity(object))
return relays
}

View File

@ -54,6 +54,22 @@ export const addUser: (pubKey: string, db: QuickSQLiteConnection) => Promise<Que
return db.execute(query, [pubKey])
}
export const getContactsCount: (db: QuickSQLiteConnection) => Promise<number> = async (db) => {
const countQuery = 'SELECT COUNT(*) FROM nostros_users WHERE contact = 1'
const resultSet = db.execute(countQuery)
const item: { 'COUNT(*)': number } = resultSet?.rows?.item(0)
return item['COUNT(*)'] ?? 0
}
export const getFollowersCount: (db: QuickSQLiteConnection) => Promise<number> = async (db) => {
const countQuery = 'SELECT COUNT(*) FROM nostros_users WHERE follower = 1'
const resultSet = db.execute(countQuery)
const item: { 'COUNT(*)': number } = resultSet?.rows?.item(0)
return item['COUNT(*)'] ?? 0
}
export const getUsers: (
db: QuickSQLiteConnection,
options: {

View File

@ -1,6 +1,6 @@
{
"common": {
"loggerPage": {
"homeNavigator": {
"ProfileConnect": "",
"ProfileLoad": ""
},
@ -10,7 +10,12 @@
"menuItems": {
"relays": "Relays",
"notConnected": "Not connected",
"connectedRelays": "{{number}} connected"
"connectedRelays": "{{number}} connected",
"following": "{{following}} following",
"followers": "{{followers}} followers",
"configuration": "Configuration",
"about": "About",
"logout": "Logout"
}
}
}

View File

@ -1,244 +0,0 @@
import { Divider, Input, Layout, TopNavigation, useTheme } from '@ui-kitten/components'
import React, { useContext, useEffect, useState } from 'react'
import { Clipboard, ScrollView, StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { useTranslation } from 'react-i18next'
import { dropTables } from '../../Functions/DatabaseFunctions'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import SInfo from 'react-native-sensitive-info'
import { getUser } from '../../Functions/DatabaseFunctions/Users'
import { EventKind } from '../../lib/nostr/Events'
import moment from 'moment'
import { showMessage } from 'react-native-flash-message'
import { Button } from '../../Components'
export const ConfigPage: React.FC = () => {
const theme = useTheme()
const { goToPage, goBack, database, init } = useContext(AppContext)
const { setPrivateKey, setPublicKey, relayPool, publicKey, privateKey } =
useContext(RelayPoolContext)
// State
const [name, setName] = useState<string>()
const [picture, setPicture] = useState<string>()
const [about, setAbout] = useState<string>()
const [lnurl, setLnurl] = useState<string>()
const [isPublishingProfile, setIsPublishingProfile] = useState<boolean>(false)
const [nip05, setNip05] = useState<string>()
const { t } = useTranslation('common')
useEffect(() => {
relayPool?.unsubscribeAll()
if (database && publicKey) {
getUser(publicKey, database).then((user) => {
if (user) {
setName(user.name)
setPicture(user.picture)
setAbout(user.about)
setLnurl(user.lnurl)
setNip05(user.nip05)
}
})
}
}, [])
const onPressBack: () => void = () => {
relayPool?.unsubscribeAll()
goBack()
}
const onPressLogout: () => void = () => {
if (database) {
relayPool?.unsubscribeAll()
setPrivateKey(undefined)
setPublicKey(undefined)
dropTables(database).then(() => {
SInfo.deleteItem('privateKey', {}).then(() => {
SInfo.deleteItem('publicKey', {}).then(() => {
init()
goToPage('landing', true)
})
})
})
}
}
const onPushPublishProfile: () => void = () => {
if (publicKey) {
setIsPublishingProfile(true)
relayPool
?.sendEvent({
content: JSON.stringify({
name,
about,
picture,
lud06: lnurl,
nip05,
}),
created_at: moment().unix(),
kind: EventKind.meta,
pubkey: publicKey,
tags: [],
})
.then(() => {
showMessage({
message: t('alerts.profilePublished'),
duration: 4000,
type: 'success',
})
setIsPublishingProfile(false) // restore sending status
})
.catch((err) => {
showMessage({
message: t('alerts.profilePublishError'),
description: err.message,
type: 'danger',
})
setIsPublishingProfile(false) // restore sending status
})
}
}
const renderBackAction = (): JSX.Element => (
<Button
accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack}
appearance='ghost'
/>
)
const copyToClipboard: (value: string) => JSX.Element = (value) => {
const copy: () => void = () => Clipboard.setString(value)
return <Icon name={'copy'} size={16} color={theme['text-basic-color']} solid onPress={copy} />
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
actionContainer: {
marginTop: 30,
paddingLeft: 32,
paddingRight: 32,
paddingBottom: 32,
},
action: {
backgroundColor: 'transparent',
marginTop: 30,
},
})
return (
<>
<Layout style={styles.container} level='2'>
<TopNavigation
alignment='center'
title={t('configPage.title')}
accessoryLeft={renderBackAction}
/>
<ScrollView horizontal={false}>
<Layout style={styles.actionContainer} level='2'>
<Layout style={styles.action}>
<Button
onPress={() => goToPage('relays')}
status='warning'
accessoryLeft={
<Icon name='server' size={16} color={theme['text-basic-color']} solid />
}
>
{t('configPage.relays')}
</Button>
</Layout>
<Layout style={styles.action}>
<Divider />
</Layout>
<Layout style={styles.action}>
<Input
placeholder={t('configPage.username')}
value={name}
onChangeText={setName}
label={t('configPage.username')}
/>
</Layout>
<Layout style={styles.action}>
<Input
placeholder={t('configPage.picture')}
value={picture}
onChangeText={setPicture}
label={t('configPage.picture')}
/>
</Layout>
<Layout style={styles.action}>
<Input
placeholder={t('configPage.lnurl')}
value={lnurl}
onChangeText={setLnurl}
label={t('configPage.lnurl')}
/>
</Layout>
<Layout style={styles.action}>
<Input
placeholder={t('configPage.nip05')}
value={nip05}
onChangeText={setNip05}
label={t('configPage.nip05')}
/>
</Layout>
<Layout style={styles.action}>
<Input
placeholder={t('configPage.about')}
multiline={true}
textStyle={{ minHeight: 64 }}
value={about}
onChangeText={setAbout}
label={t('configPage.about')}
/>
</Layout>
<Layout style={styles.action}>
<Button
onPress={onPushPublishProfile}
status='success'
loading={isPublishingProfile}
accessoryLeft={
<Icon name='paper-plane' size={16} color={theme['text-basic-color']} solid />
}
>
{t('configPage.publish')}
</Button>
</Layout>
<Layout style={styles.action}>
<Divider />
</Layout>
<Layout style={styles.action}>
<Input
disabled={true}
placeholder={t('configPage.publicKey')}
accessoryRight={() => copyToClipboard(publicKey ?? '')}
value={publicKey}
label={t('configPage.publicKey')}
/>
</Layout>
<Layout style={styles.action}>
<Input
disabled={true}
placeholder={t('configPage.privateKey')}
accessoryRight={() => copyToClipboard(privateKey ?? '')}
value={privateKey}
secureTextEntry={true}
label={t('configPage.privateKey')}
/>
</Layout>
<Layout style={styles.action}>
<Button onPress={onPressLogout} status='danger'>
{t('configPage.logout')}
</Button>
</Layout>
</Layout>
</ScrollView>
</Layout>
</>
)
}
export default ConfigPage

View File

@ -20,10 +20,12 @@ import { Button, UserCard } from '../../Components'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { populatePets } from '../../Functions/RelayFunctions/Users'
import { getNip19Key } from '../../lib/nostr/Nip19'
import { UserContext } from '../../Contexts/UserContext'
export const ContactsPage: React.FC = () => {
const { database, goBack } = useContext(AppContext)
const { relayPool, publicKey, privateKey, lastEventId } = useContext(RelayPoolContext)
const { database } = useContext(AppContext)
const { publicKey, privateKey } = React.useContext(UserContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
const theme = useTheme()
// State
const [users, setUsers] = useState<User[]>()

View File

@ -6,6 +6,10 @@ import { Appbar, Snackbar, Text, useTheme } from 'react-native-paper'
import RBSheet from "react-native-raw-bottom-sheet"
import { useTranslation } from 'react-i18next'
import HomePage from '../HomePage'
import RelaysPage from '../RelaysPage'
import AboutPage from '../AboutPage'
import ProfileConfigPage from '../ProfileConfigPage'
import ProfilePage from '../ProfilePage'
export const HomeNavigator: React.FC = () => {
const theme = useTheme()
@ -20,10 +24,6 @@ export const HomeNavigator: React.FC = () => {
[],
)
const onPressQuestion: () => void = () => {
bottomSheetRef.current?.open()
}
return (
<>
<Stack.Navigator
@ -43,8 +43,7 @@ export const HomeNavigator: React.FC = () => {
onPress={() => (navigation as any as DrawerNavigationProp<{}>).openDrawer()}
/>
) : null}
<Appbar.Content title={t(`loggerPage.${route.name}`)} />
<Appbar.Action icon='help-circle-outline' isLeading onPress={onPressQuestion} />
<Appbar.Content title={t(`homeNavigator.${route.name}`)} />
</Appbar.Header>
)
},
@ -52,7 +51,13 @@ export const HomeNavigator: React.FC = () => {
}}
>
<Stack.Group>
<Stack.Screen name='Feed' component={HomePage} />
<Stack.Screen name='Landing' component={HomePage} />
</Stack.Group>
<Stack.Group>
<Stack.Screen name='Relays' component={RelaysPage} />
<Stack.Screen name='About' component={AboutPage} />
<Stack.Screen name='ProfileConfig' component={ProfileConfigPage} />
<Stack.Screen name='Profile' component={ProfilePage} />
</Stack.Group>
</Stack.Navigator>
<RBSheet

View File

@ -107,7 +107,7 @@ export const HomeNavigator: React.FC = () => {
return (
<Appbar.Header>
{leftAction()}
<Appbar.Content title={t(`loggerPage.${route.name}`)} />
<Appbar.Content title={t(`homeNavigator.${route.name}`)} />
<Appbar.Action icon='help-circle-outline' isLeading onPress={() => onPressQuestion(route.name)} />
</Appbar.Header>
)

View File

@ -11,13 +11,13 @@ import {
import { AppContext } from '../../Contexts/AppContext'
import { getMainNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../../Components/NoteCard'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { useTheme } from 'react-native-paper'
export const HomePage: React.FC = () => {
const { database, goToPage } = useContext(AppContext)

View File

@ -0,0 +1,537 @@
import React, { useContext, useEffect, useState } from 'react'
import { Clipboard, Linking, ScrollView, StyleSheet, View } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { useTranslation } from 'react-i18next'
import { UserContext } from '../../Contexts/UserContext'
import { getUser } from '../../Functions/DatabaseFunctions/Users'
import { EventKind } from '../../lib/nostr/Events'
import moment from 'moment'
import {
Avatar,
Button,
Card,
useTheme,
IconButton,
Text,
TouchableRipple,
TextInput,
Snackbar,
} from 'react-native-paper'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import RBSheet from 'react-native-raw-bottom-sheet'
export const ProfileConfigPage: React.FC = () => {
const theme = useTheme()
const bottomSheetPictureRef = React.useRef<RBSheet>(null)
const bottomSheetDirectoryRef = React.useRef<RBSheet>(null)
const bottomSheetNip05Ref = React.useRef<RBSheet>(null)
const bottomSheetLud06Ref = React.useRef<RBSheet>(null)
const { database } = useContext(AppContext)
const { relayPool } = useContext(RelayPoolContext)
const { user, publicKey, nPub, nSec, contactsCount, followersCount, setUser } =
useContext(UserContext)
// State
const [name, setName] = useState<string>()
const [picture, setPicture] = useState<string>()
const [about, setAbout] = useState<string>()
const [lnurl, setLnurl] = useState<string>()
const [isPublishingProfile, setIsPublishingProfile] = useState<boolean>(false)
const [nip05, setNip05] = useState<string>()
const [showNotification, setShowNotification] = useState<
| 'npubCopied'
| 'picturePublished'
| 'connectionError'
| 'nsecCopied'
| 'profilePublished'
| 'nip05Published'
| 'lud06Published'
>()
const { t } = useTranslation('common')
useEffect(() => {
relayPool?.unsubscribeAll()
if (database && publicKey) {
if (user) {
setName(user.name)
setPicture(user.picture)
setAbout(user.about)
setLnurl(user.lnurl)
setNip05(user.nip05)
}
}
}, [user])
const onPressSavePicture: () => void = () => {
if (publicKey && database) {
getUser(publicKey, database).then((user) => {
if (user) {
relayPool
?.sendEvent({
content: JSON.stringify({
name: user.name,
about: user.about,
picture,
lud06: user.lnurl,
nip05: user.nip05,
}),
created_at: moment().unix(),
kind: EventKind.meta,
pubkey: publicKey,
tags: [],
})
.then(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('picturePublished')
setUser({
...user,
picture,
})
bottomSheetPictureRef.current?.close()
})
.catch(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('connectionError')
})
}
})
}
}
const onPressSaveNip05: () => void = () => {
if (publicKey && database) {
getUser(publicKey, database).then((user) => {
if (user) {
relayPool
?.sendEvent({
content: JSON.stringify({
name: user.name,
about: user.about,
picture: user.picture,
lud06: user.lnurl,
nip05,
}),
created_at: moment().unix(),
kind: EventKind.meta,
pubkey: publicKey,
tags: [],
})
.then(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('nip05Published')
setUser({
...user,
nip05,
})
bottomSheetNip05Ref.current?.close()
})
.catch(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('connectionError')
})
}
})
}
}
const onPressSaveLnurl: () => void = () => {
if (publicKey && database) {
getUser(publicKey, database).then((user) => {
if (user) {
relayPool
?.sendEvent({
content: JSON.stringify({
name: user.name,
about: user.about,
picture: user.picture,
lnurl,
nip05: user.nip05,
}),
created_at: moment().unix(),
kind: EventKind.meta,
pubkey: publicKey,
tags: [],
})
.then(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('lud06Published')
setUser({
...user,
lnurl,
})
bottomSheetLud06Ref.current?.close()
})
.catch(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('connectionError')
})
}
})
}
}
const onPressSaveProfile: () => void = () => {
if (publicKey && database) {
getUser(publicKey, database).then((user) => {
if (user) {
relayPool
?.sendEvent({
content: JSON.stringify({
name,
about,
picture: user.picture,
lud06: lnurl,
nip05: user.nip05,
}),
created_at: moment().unix(),
kind: EventKind.meta,
pubkey: publicKey,
tags: [],
})
.then(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('profilePublished')
bottomSheetPictureRef.current?.close()
})
.catch(() => {
setIsPublishingProfile(false) // restore sending status
setShowNotification('connectionError')
})
setUser({
...user,
name,
about,
picture,
lnurl,
nip05,
})
}
})
}
}
const rbSheetCustomStyles = React.useMemo(() => {
return {
container: {
...styles.rbsheetContainer,
backgroundColor: theme.colors.background,
},
draggableIcon: styles.rbsheetDraggableIcon,
}
}, [])
const pastePicture: () => void = () => {
Clipboard.getString().then((value) => {
setPicture(value ?? '')
})
}
const pasteNip05: () => void = () => {
Clipboard.getString().then((value) => {
setNip05(value ?? '')
})
}
const pasteLud06: () => void = () => {
Clipboard.getString().then((value) => {
setLnurl(value ?? '')
})
}
return (
<View style={styles.container}>
<ScrollView horizontal={false}>
<Card style={styles.cardContainer}>
<Card.Content>
<View style={styles.cardPicture}>
<TouchableRipple onPress={() => bottomSheetPictureRef.current?.open()}>
{user?.picture ? (
<Avatar.Image size={100} source={{ uri: user.picture }} />
) : (
<Avatar.Icon
size={100}
icon='image-plus'
style={{ backgroundColor: theme.colors.primaryContainer }}
/>
)}
</TouchableRipple>
</View>
<View style={styles.cardActions}>
<Button mode='elevated'>
{t('menuItems.following', { following: contactsCount })}
</Button>
<Button mode='elevated'>
{t('menuItems.followers', { followers: followersCount })}
</Button>
</View>
<View style={styles.cardActions}>
<View style={styles.actionButton}>
<IconButton
icon='content-copy'
size={28}
onPress={() => {
setShowNotification('picturePublished')
Clipboard.setString(nPub ?? '')
}}
/>
<Text>{t('profileConfigPage.copyNPub')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='twitter'
size={28}
onPress={() => bottomSheetDirectoryRef.current?.open()}
/>
<Text>{t('profileConfigPage.directory')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='lightning-bolt'
size={28}
iconColor='#F5D112'
onPress={() => bottomSheetLud06Ref.current?.open()}
/>
<Text>{t('profileConfigPage.invoice')}</Text>
</View>
</View>
<View style={styles.cardActions}>
<View style={styles.actionButton}>
<IconButton
icon='check-circle-outline'
size={28}
onPress={() => bottomSheetNip05Ref.current?.open()}
/>
<Text>{t('profileConfigPage.nip05')}</Text>
</View>
<View style={styles.actionButton}></View>
<View style={styles.actionButton}></View>
</View>
</Card.Content>
</Card>
<TextInput
mode='outlined'
label={t('profileConfigPage.name') ?? ''}
onChangeText={setName}
value={name}
/>
<TextInput
mode='outlined'
label={t('profileConfigPage.about') ?? ''}
onChangeText={setAbout}
value={about}
/>
<TextInput
mode='outlined'
label={t('profileConfigPage.lud06') ?? ''}
onChangeText={setLnurl}
value={lnurl}
/>
<TextInput
mode='outlined'
label={t('profileConfigPage.npub') ?? ''}
value={nPub}
right={
<TextInput.Icon
icon='content-paste'
onPress={() => {
setShowNotification('npubCopied')
Clipboard.setString(nPub ?? '')
}}
forceTextInputFocus={false}
/>
}
/>
<TextInput
mode='outlined'
label={t('profileConfigPage.nsec') ?? ''}
value={nSec}
secureTextEntry={true}
right={
<TextInput.Icon
icon='content-paste'
onPress={() => {
setShowNotification('nsecCopied')
Clipboard.setString(nSec ?? '')
}}
forceTextInputFocus={false}
/>
}
/>
<Button
mode='contained'
disabled={!picture || picture === ''}
onPress={onPressSaveProfile}
loading={isPublishingProfile}
>
{t('profileConfigPage.publish')}
</Button>
</ScrollView>
<RBSheet
ref={bottomSheetPictureRef}
closeOnDragDown={true}
height={230}
customStyles={rbSheetCustomStyles}
>
<View>
<Text variant='titleLarge'>{t('profileConfigPage.pictureTitle')}</Text>
<Text variant='bodyMedium'>{t('profileConfigPage.pictureDescription')}</Text>
<TextInput
mode='outlined'
label={t('profileConfigPage.pictureUrl') ?? ''}
onChangeText={setPicture}
value={picture}
right={
<TextInput.Icon
icon='content-paste'
onPress={pastePicture}
forceTextInputFocus={false}
/>
}
/>
<Button
mode='contained'
disabled={!picture || picture === ''}
onPress={onPressSavePicture}
loading={isPublishingProfile}
>
{t('profileConfigPage.publishPicture')}
</Button>
</View>
</RBSheet>
<RBSheet
ref={bottomSheetDirectoryRef}
closeOnDragDown={true}
height={230}
customStyles={rbSheetCustomStyles}
>
<View>
<Text variant='titleLarge'>{t('profileConfigPage.directoryTitle')}</Text>
<Text variant='bodyMedium'>{t('profileConfigPage.directoryDescription')}</Text>
<Button
mode='contained'
onPress={async () => await Linking.openURL('https://www.nostr.directory')}
loading={isPublishingProfile}
>
{t('profileConfigPage.continue')}
</Button>
<Button mode='outlined' onPress={() => bottomSheetDirectoryRef.current?.close()}>
{t('profileConfigPage.cancell')}
</Button>
</View>
</RBSheet>
<RBSheet
ref={bottomSheetNip05Ref}
closeOnDragDown={true}
height={230}
customStyles={rbSheetCustomStyles}
>
<View>
<Text variant='titleLarge'>{t('profileConfigPage.pictureTitle')}</Text>
<Text variant='bodyMedium'>{t('profileConfigPage.pictureDescription')}</Text>
<TextInput
mode='outlined'
label={t('profileConfigPage.nip05') ?? ''}
onChangeText={setNip05}
value={nip05}
right={
<TextInput.Icon
icon='content-paste'
onPress={pasteNip05}
forceTextInputFocus={false}
/>
}
/>
<Button
mode='contained'
disabled={!nip05 || nip05 === ''}
onPress={onPressSaveNip05}
loading={isPublishingProfile}
>
{t('profileConfigPage.publishPicture')}
</Button>
</View>
</RBSheet>
<RBSheet
ref={bottomSheetLud06Ref}
closeOnDragDown={true}
height={230}
customStyles={rbSheetCustomStyles}
>
<View>
<Text variant='titleLarge'>{t('profileConfigPage.lud06Title')}</Text>
<Text variant='bodyMedium'>{t('profileConfigPage.lud06Description')}</Text>
<TextInput
mode='outlined'
label={t('profileConfigPage.lud06') ?? ''}
onChangeText={setLnurl}
value={lnurl}
right={
<TextInput.Icon
icon='content-paste'
onPress={pasteLud06}
forceTextInputFocus={false}
/>
}
/>
<Button
mode='contained'
disabled={!lnurl || lnurl === ''}
onPress={onPressSaveLnurl}
loading={isPublishingProfile}
>
{t('profileConfigPage.publishPicture')}
</Button>
</View>
</RBSheet>
<Snackbar
style={styles.snackbar}
visible={showNotification !== undefined}
duration={Snackbar.DURATION_SHORT}
onIconPress={() => setShowNotification(undefined)}
onDismiss={() => setShowNotification(undefined)}
>
{t(`profileConfigPage.${showNotification}`)}
</Snackbar>
</View>
)
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
cardContainer: {
width: '100%',
justifyContent: 'center',
alignContent: 'center',
},
cardActions: {
flexDirection: 'row',
justifyContent: 'space-around',
},
cardPicture: {
flexDirection: 'row',
justifyContent: 'center',
alignContent: 'center',
marginBottom: 32,
},
actionButton: {
marginTop: 32,
justifyContent: 'center',
alignItems: 'center',
width: 80,
},
rbsheetDraggableIcon: {
backgroundColor: '#000',
},
rbsheetContainer: {
padding: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
},
snackbar: {
margin: 16,
bottom: 70,
},
})
export default ProfileConfigPage

View File

@ -1,28 +1,20 @@
import React, { useContext, useEffect, useState } from 'react'
import { Clipboard, StyleSheet, View } from 'react-native'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { UserContext } from '../../Contexts/UserContext'
import { useTranslation } from 'react-i18next'
import { getNip19Key, isPrivateKey, isPublicKey } from '../../lib/nostr/Nip19'
import { Button, Switch, Text, TextInput } from 'react-native-paper'
import Logo from '../../Components/Logo'
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types'
import { navigate } from '../../lib/Navigation'
interface ProfileConnectPageProps {
navigation: DrawerNavigationHelpers;
}
export const ProfileConnectPage: React.FC<ProfileConnectPageProps> = ({navigation}) => {
const { setPrivateKey, setPublicKey } = useContext(RelayPoolContext)
export const ProfileConnectPage: React.FC = () => {
const { setPrivateKey, setPublicKey } = useContext(UserContext)
const { t } = useTranslation('common')
const [isNip19, setIsNip19] = useState<boolean>(false)
const [isPublic, setIsPublic] = useState<boolean>(false)
const [inputValue, setInputValue] = useState<string>('')
useEffect(() => checkKey(), [inputValue])
useEffect(() => {
setPrivateKey(undefined)
setPublicKey(undefined)
}, [])
const checkKey: () => void = () => {
if (inputValue && inputValue !== '') {
@ -43,7 +35,7 @@ export const ProfileConnectPage: React.FC<ProfileConnectPageProps> = ({navigatio
setPrivateKey(key)
}
navigation.navigate('ProfileLoad')
navigate('ProfileLoad')
}
}
@ -52,7 +44,6 @@ export const ProfileConnectPage: React.FC<ProfileConnectPageProps> = ({navigatio
Clipboard.getString().then((value) => {
setInputValue(value ?? '')
})
}
const label: string = React.useMemo(() => isPublic ? t('loggerPage.publicKey') : t('loggerPage.privateKey'), [isPublic])
@ -93,7 +84,7 @@ export const ProfileConnectPage: React.FC<ProfileConnectPageProps> = ({navigatio
</View>
<View style={styles.row}>
<Text>{t('loggerPage.notKeys')}</Text>
<Button mode='text' onPress={() => navigation.navigate('ProfileCreate')}>
<Button mode='text' onPress={() => navigate('ProfileCreate')}>
{t('loggerPage.createButton')}
</Button>
</View>

View File

@ -4,8 +4,8 @@ import { Clipboard, StyleSheet, View } from 'react-native'
import { Button, Snackbar, TextInput } from 'react-native-paper'
import { useTranslation } from 'react-i18next'
import { nsecEncode } from 'nostr-tools/nip19'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types'
import { UserContext } from '../../Contexts/UserContext'
interface ProfileCreatePageProps {
navigation: DrawerNavigationHelpers;
@ -13,7 +13,7 @@ interface ProfileCreatePageProps {
export const ProfileCreatePage: React.FC<ProfileCreatePageProps> = ({navigation}) => {
const { t } = useTranslation('common')
const { setPrivateKey } = useContext(RelayPoolContext)
const { setPrivateKey } = useContext(UserContext)
const [inputValue, setInputValue] = useState<string>()
const [copied, setCopied] = useState<boolean>(false)

View File

@ -2,31 +2,39 @@ import React, { useContext, useEffect, useState } from 'react'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events'
import { AppContext } from '../../Contexts/AppContext'
import { getUser, getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { UserContext } from '../../Contexts/UserContext'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { useTranslation } from 'react-i18next'
import moment from 'moment'
import { StyleSheet, View } from 'react-native'
import Logo from '../../Components/Logo'
import { Button, Snackbar, Text } from 'react-native-paper'
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types'
import { navigate } from '../../lib/Navigation'
interface ProfileLoadPageProps {
navigation: DrawerNavigationHelpers;
}
export const ProfileLoadPage: React.FC<ProfileLoadPageProps> = ({navigation}) => {
export const ProfileLoadPage: React.FC = () => {
const { loadingDb, database } = useContext(AppContext)
const { publicKey, relayPool, lastEventId, loadingRelayPool } = useContext(RelayPoolContext)
const { relayPool, lastEventId, loadingRelayPool } = useContext(RelayPoolContext)
const { publicKey, reloadUser, user } = useContext(UserContext)
const { t } = useTranslation('common')
const [profileFound, setProfileFound] = useState<boolean>(false)
const [contactsCount, setContactsCount] = useState<number>()
const [contactsCount, setContactsCount] = useState<number>(0)
useEffect(() => {
if (!loadingRelayPool && !loadingDb && publicKey) {
relayPool?.subscribe('loading-meta', [
{
kinds: [EventKind.petNames, EventKind.meta],
kinds: [EventKind.meta],
authors: [publicKey],
}
])
relayPool?.subscribe('loading-pets', [
{
kinds: [EventKind.petNames],
authors: [publicKey],
},
{
kinds: [EventKind.petNames],
'#p': [publicKey],
},
])
}
@ -34,14 +42,19 @@ export const ProfileLoadPage: React.FC<ProfileLoadPageProps> = ({navigation}) =>
useEffect(() => {
loadPets()
loadProfile()
reloadUser()
}, [lastEventId])
useEffect(() => {
if (user) setProfileFound(true)
}, [user])
const loadPets: () => void = () => {
if (database) {
if (database && publicKey) {
getUsers(database, { contacts: true }).then((results) => {
setContactsCount(results.length)
if (publicKey && results && results.length > 0) {
if (results && results.length > 0) {
reloadUser()
setContactsCount(results.length)
const authors = [...results.map((user: User) => user.id), publicKey]
relayPool?.subscribe('loading-notes', [
{
@ -55,16 +68,6 @@ export const ProfileLoadPage: React.FC<ProfileLoadPageProps> = ({navigation}) =>
}
}
const loadProfile: () => void = () => {
if (database && publicKey) {
getUser(publicKey, database).then((result) => {
if (result) {
setProfileFound(true)
}
})
}
}
return (
<View style={styles.container}>
<Logo onlyIcon size='medium'/>
@ -74,15 +77,14 @@ export const ProfileLoadPage: React.FC<ProfileLoadPageProps> = ({navigation}) =>
<Text variant='titleMedium'>
{t('profileLoadPage.foundContacts', { contactsCount })}
</Text>
<Button mode='contained' onPress={() => navigation}>
<Button mode='contained' onPress={() => navigate('Feed')}>
{t('profileLoadPage.home')}
</Button>
<Snackbar
style={styles.snackbar}
visible
onDismiss={() => {}}
action={{label: t('profileLoadPage.relays') ?? '', onPress: () => navigation.navigate('Relays')}}
action={{label: t('profileLoadPage.relays') ?? '', onPress: () => navigate('Relays')}}
>
Conéctate a otros relays si tienes problemas encontrando tus datos.
</Snackbar>

View File

@ -1,391 +1,17 @@
import { Button, Card, Layout, Spinner, Text, TopNavigation, useTheme } from '@ui-kitten/components'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import {
Clipboard,
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
TouchableOpacity,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../../Components/NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getUser, User, updateUserContact } from '../../Functions/DatabaseFunctions/Users'
import { EventKind } from '../../lib/nostr/Events'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { formatPubKey, populatePets } from '../../Functions/RelayFunctions/Users'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import Loading from '../../Components/Loading'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import Avatar from '../../Components/Avatar'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { t } from 'i18next'
import TextContent from '../../Components/TextContent'
import LnPayment from '../../Components/LnPayment'
import React from 'react'
import { StyleSheet, View } from 'react-native'
export const ProfilePage: React.FC = () => {
const { database, page, goToPage, goBack } = useContext(AppContext)
const { publicKey, lastEventId, relayPool } = useContext(RelayPoolContext)
const theme = useTheme()
const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>()
const [user, setUser] = useState<User>()
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [isContact, setIsContact] = useState<boolean>()
const [refreshing, setRefreshing] = useState(false)
const [openPayment, setOpenPayment] = useState<boolean>(false)
const [firstLoad, setFirstLoad] = useState(true)
const breadcrump = page.split('%')
const userId = breadcrump[breadcrump.length - 1].split('#')[1] ?? publicKey
const username = user?.name === '' ? formatPubKey(user.id) : user?.name
useEffect(() => {
setRefreshing(true)
setNotes(undefined)
setUser(undefined)
loadUser()
loadNotes()
subscribeProfile()
subscribeNotes()
setFirstLoad(false)
}, [page])
useEffect(() => {
if (notes && !firstLoad) {
loadUser()
loadNotes()
}
}, [lastEventId])
useEffect(() => {
if (pageSize > initialPageSize && !firstLoad) {
loadUser()
loadNotes()
subscribeNotes(true)
}
}, [pageSize])
const loadUser: () => void = () => {
if (database) {
getUser(userId, database).then((result) => {
if (result) {
setUser(result)
setIsContact(result?.contact)
}
})
}
}
const loadNotes: (past?: boolean) => void = () => {
if (database) {
getNotes(database, { filters: { pubkey: userId }, limit: pageSize }).then((results) => {
setNotes(results)
setRefreshing(false)
relayPool?.subscribe('answers-profile', [
{
kinds: [EventKind.reaction],
'#e': results.map((note) => note.id ?? ''),
},
])
})
}
}
const subscribeNotes: (past?: boolean) => void = (past) => {
if (!database) return
const message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [userId],
limit: pageSize,
}
relayPool?.subscribe('main-profile', [message])
}
const subscribeProfile: () => Promise<void> = async () => {
relayPool?.subscribe('user-profile', [
{
kinds: [EventKind.meta, EventKind.petNames],
authors: [userId],
},
])
}
const onRefresh = useCallback(() => {
setRefreshing(true)
relayPool?.unsubscribeAll()
loadUser()
loadNotes()
subscribeProfile()
subscribeNotes()
}, [])
const removeAuthor: () => void = () => {
if (relayPool && database && publicKey) {
updateUserContact(userId, database, false).then(() => {
populatePets(relayPool, database, publicKey)
setIsContact(false)
})
}
}
const addAuthor: () => void = () => {
if (relayPool && database && publicKey) {
updateUserContact(userId, database, true).then(() => {
populatePets(relayPool, database, publicKey)
setIsContact(true)
})
}
}
const renderOptions: () => JSX.Element = () => {
const payment = user?.lnurl ? (
<Button appearance='ghost' onPress={() => setOpenPayment(true)} status='warning'>
<Icon name='bolt' size={16} color={theme['text-basic-color']} solid />
</Button>
) : (
<></>
)
if (publicKey === userId) {
return (
<>
{payment}
<Button
accessoryRight={<Icon name='cog' size={16} color={theme['text-basic-color']} solid />}
onPress={() => goToPage('config')}
appearance='ghost'
/>
</>
)
} else {
if (user) {
const contact = isContact ? (
<Button
accessoryRight={
<Icon name='user-minus' size={16} color={theme['color-danger-500']} solid />
}
onPress={removeAuthor}
appearance='ghost'
/>
) : (
<Button
accessoryRight={
<Icon name='user-plus' size={16} color={theme['color-success-500']} solid />
}
onPress={addAuthor}
appearance='ghost'
/>
)
return (
<>
{payment}
{contact}
</>
)
} else {
return <Spinner size='small' />
}
}
}
const onPressBack: () => void = () => {
relayPool?.unsubscribeAll()
goBack()
}
const renderBackAction = (): JSX.Element => {
if (publicKey === userId) {
return <></>
} else {
return (
<Button
accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack}
appearance='ghost'
/>
)
}
}
const styles = StyleSheet.create({
list: {
flex: 1,
},
icon: {
width: 32,
height: 32,
},
settingsIcon: {
width: 48,
height: 48,
},
avatar: {
width: 130,
marginBottom: 16,
},
profile: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 2,
paddingLeft: 32,
paddingRight: 32,
},
loading: {
maxHeight: 160,
},
about: {
flex: 4,
maxHeight: 200,
},
stats: {
flex: 1,
},
statsItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 5,
},
description: {
marginTop: 16,
flexDirection: 'row',
},
notCreated: {
height: 64,
justifyContent: 'center',
alignItems: 'center',
},
spinner: {
justifyContent: 'center',
alignItems: 'center',
height: 64,
},
})
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
<Card onPress={() => onPressNote(note)} key={note.id ?? ''}>
<NoteCard note={note} onlyContactsReplies={true} />
</Card>
)
}
const onPressNote: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const mainEventId = getReplyEventId(note)
if (mainEventId) {
goToPage(`note#${mainEventId}`)
} else if (note.id) {
goToPage(`note#${note.id}`)
}
}
}
const onPressId: () => void = () => {
Clipboard.setString(user?.id ?? '')
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const profile: JSX.Element = (
<Layout style={styles.profile} level='3'>
<Layout style={styles.avatar} level='3'>
{user ? (
<>
<Avatar src={user?.picture} name={username} size={130} pubKey={user.id} />
</>
) : (
<></>
)}
</Layout>
<TouchableOpacity onPress={onPressId}>
<Text appearance='hint'>{user?.id}</Text>
</TouchableOpacity>
<Layout style={styles.description} level='3'>
{user && (
<>
<Layout style={styles.about} level='3'>
<TextContent content={user?.about} preview={false} />
</Layout>
</>
)}
</Layout>
</Layout>
)
const createProfile: JSX.Element = (
<Layout style={styles.profile} level='3'>
<Layout style={styles.notCreated} level='3'>
<Text>{t('profilePage.profileNotCreated')}</Text>
</Layout>
<Button
onPress={() => goToPage('config')}
status='warning'
accessoryLeft={<Icon name='cog' size={16} color={theme['text-basic-color']} solid />}
>
{t('profilePage.createProfile')}
</Button>
</Layout>
)
return (
<>
<TopNavigation
alignment='center'
title={username}
accessoryLeft={renderBackAction}
accessoryRight={renderOptions}
/>
{!user && userId === publicKey ? createProfile : profile}
<Layout style={styles.list} level='3'>
{notes && notes.length > 0 ? (
<ScrollView
onScroll={onScroll}
horizontal={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{notes.map((note) => itemCard(note))}
{notes.length >= 10 && (
<Layout style={styles.spinner}>
<Spinner size='small' />
</Layout>
)}
</ScrollView>
) : (
<Loading />
)}
<LnPayment user={user} open={openPayment} setOpen={setOpenPayment} />
</Layout>
{publicKey === userId && (
<TouchableOpacity
style={{
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.2)',
alignItems: 'center',
justifyContent: 'center',
width: 65,
position: 'absolute',
bottom: 20,
right: 20,
height: 65,
backgroundColor: theme['color-warning-500'],
borderRadius: 100,
}}
onPress={() => goToPage('contacts')}
>
<Icon name='address-book' size={30} color={theme['text-basic-color']} solid />
</TouchableOpacity>
)}
</>
<View style={styles.container}>
</View>
)
}
const styles = StyleSheet.create({
container: {
padding: 16,
},
})
export default ProfilePage

View File

@ -0,0 +1,342 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import {
Clipboard,
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
TouchableOpacity,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../../Components/NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getUser, User, updateUserContact } from '../../Functions/DatabaseFunctions/Users'
import { EventKind } from '../../lib/nostr/Events'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { formatPubKey, populatePets } from '../../Functions/RelayFunctions/Users'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import Avatar from '../../Components/Avatar'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { t } from 'i18next'
import TextContent from '../../Components/TextContent'
import LnPayment from '../../Components/LnPayment'
export const ProfilePage: React.FC = () => {
const { database, page, goToPage, goBack } = useContext(AppContext)
const { publicKey, lastEventId, relayPool } = useContext(RelayPoolContext)
const theme = useTheme()
const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>()
const [user, setUser] = useState<User>()
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [isContact, setIsContact] = useState<boolean>()
const [refreshing, setRefreshing] = useState(false)
const [openPayment, setOpenPayment] = useState<boolean>(false)
const [firstLoad, setFirstLoad] = useState(true)
const breadcrump = page.split('%')
const userId = breadcrump[breadcrump.length - 1].split('#')[1] ?? publicKey
const username = user?.name === '' ? formatPubKey(user.id) : user?.name
useEffect(() => {
setRefreshing(true)
setNotes(undefined)
setUser(undefined)
loadUser()
loadNotes()
subscribeProfile()
subscribeNotes()
setFirstLoad(false)
}, [page])
useEffect(() => {
if (notes && !firstLoad) {
loadUser()
loadNotes()
}
}, [lastEventId])
useEffect(() => {
if (pageSize > initialPageSize && !firstLoad) {
loadUser()
loadNotes()
subscribeNotes(true)
}
}, [pageSize])
const loadUser: () => void = () => {
if (database) {
getUser(userId, database).then((result) => {
if (result) {
setUser(result)
setIsContact(result?.contact)
}
})
}
}
const loadNotes: (past?: boolean) => void = () => {
if (database) {
getNotes(database, { filters: { pubkey: userId }, limit: pageSize }).then((results) => {
setNotes(results)
setRefreshing(false)
relayPool?.subscribe('answers-profile', [
{
kinds: [EventKind.reaction],
'#e': results.map((note) => note.id ?? ''),
},
])
})
}
}
const subscribeNotes: (past?: boolean) => void = (past) => {
if (!database) return
const message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [userId],
limit: pageSize,
}
relayPool?.subscribe('main-profile', [message])
}
const subscribeProfile: () => Promise<void> = async () => {
relayPool?.subscribe('user-profile', [
{
kinds: [EventKind.meta, EventKind.petNames],
authors: [userId],
},
])
}
const onRefresh = useCallback(() => {
setRefreshing(true)
relayPool?.unsubscribeAll()
loadUser()
loadNotes()
subscribeProfile()
subscribeNotes()
}, [])
const removeAuthor: () => void = () => {
if (relayPool && database && publicKey) {
updateUserContact(userId, database, false).then(() => {
populatePets(relayPool, database, publicKey)
setIsContact(false)
})
}
}
const addAuthor: () => void = () => {
if (relayPool && database && publicKey) {
updateUserContact(userId, database, true).then(() => {
populatePets(relayPool, database, publicKey)
setIsContact(true)
})
}
}
const renderOptions: () => JSX.Element = () => {
const payment = user?.lnurl ? (
// <Button appearance='ghost' onPress={() => setOpenPayment(true)} status='warning'>
// <Icon name='bolt' size={16} color={theme['text-basic-color']} solid />
// </Button>
<></>
) : (
<></>
)
if (publicKey === userId) {
return (
<>
{payment}
{/* <Button
accessoryRight={<Icon name='cog' size={16} color={theme['text-basic-color']} solid />}
onPress={() => goToPage('config')}
appearance='ghost'
/> */}
<></>
</>
)
} else {
if (user) {
const contact = isContact ? (
// <Button
// accessoryRight={
// <Icon name='user-minus' size={16} color={theme['color-danger-500']} solid />
// }
// onPress={removeAuthor}
// appearance='ghost'
// />
<></>
) : (
// <Button
// accessoryRight={
// <Icon name='user-plus' size={16} color={theme['color-success-500']} solid />
// }
// onPress={addAuthor}
// appearance='ghost'
// />
<></>
)
return (
<>
{payment}
{contact}
</>
)
} else {
return <Spinner size='small' />
}
}
}
const onPressBack: () => void = () => {
relayPool?.unsubscribeAll()
goBack()
}
const renderBackAction = (): JSX.Element => {
if (publicKey === userId) {
return <></>
} else {
return (
// <Button
// accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
// onPress={onPressBack}
// appearance='ghost'
// />
<></>
)
}
}
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
// <Card onPress={() => onPressNote(note)} key={note.id ?? ''}>
// <NoteCard note={note} onlyContactsReplies={true} />
// </Card>
<></>
)
}
const onPressNote: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const mainEventId = getReplyEventId(note)
if (mainEventId) {
goToPage(`note#${mainEventId}`)
} else if (note.id) {
goToPage(`note#${note.id}`)
}
}
}
const onPressId: () => void = () => {
Clipboard.setString(user?.id ?? '')
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const profile: JSX.Element = (
// <Layout style={styles.profile} level='3'>
// <Layout style={styles.avatar} level='3'>
// {user ? (
// <>
// <Avatar src={user?.picture} name={username} size={130} pubKey={user.id} />
// </>
// ) : (
// <></>
// )}
// </Layout>
// <TouchableOpacity onPress={onPressId}>
// <Text appearance='hint'>{user?.id}</Text>
// </TouchableOpacity>
// <Layout style={styles.description} level='3'>
// {user && (
// <>
// <Layout style={styles.about} level='3'>
// <TextContent content={user?.about} preview={false} />
// </Layout>
// </>
// )}
// </Layout>
// </Layout>
<></>
)
const createProfile: JSX.Element = (
// <Layout style={styles.profile} level='3'>
// <Layout style={styles.notCreated} level='3'>
// <Text>{t('profilePage.profileNotCreated')}</Text>
// </Layout>
// <Button
// onPress={() => goToPage('config')}
// status='warning'
// accessoryLeft={<Icon name='cog' size={16} color={theme['text-basic-color']} solid />}
// >
// {t('profilePage.createProfile')}
// </Button>
// </Layout>
<></>
)
return (
<>
{/* <TopNavigation
alignment='center'
title={username}
accessoryLeft={renderBackAction}
accessoryRight={renderOptions}
/>
{!user && userId === publicKey ? createProfile : profile}
<Layout style={styles.list} level='3'>
{notes && notes.length > 0 ? (
<ScrollView
onScroll={onScroll}
horizontal={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{notes.map((note) => itemCard(note))}
{notes.length >= 10 && (
<Layout style={styles.spinner}>
<Spinner size='small' />
</Layout>
)}
</ScrollView>
) : (
<Loading />
)}
<LnPayment user={user} open={openPayment} setOpen={setOpenPayment} />
</Layout>
{publicKey === userId && (
<TouchableOpacity
style={{
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.2)',
alignItems: 'center',
justifyContent: 'center',
width: 65,
position: 'absolute',
bottom: 20,
right: 20,
height: 65,
backgroundColor: theme['color-warning-500'],
borderRadius: 100,
}}
onPress={() => goToPage('contacts')}
>
<Icon name='address-book' size={30} color={theme['text-basic-color']} solid />
</TouchableOpacity>
)} */}
</>
)
}
export default ProfilePage

View File

@ -1,9 +1,8 @@
import React, { useContext, useEffect, useState } from 'react'
import React, { useContext, useState } from 'react'
import { Clipboard, FlatList, ListRenderItem, StyleSheet, View } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { useTranslation } from 'react-i18next'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getRelays, Relay } from '../../Functions/DatabaseFunctions/Relays'
import { Relay } from '../../Functions/DatabaseFunctions/Relays'
import { defaultRelays, REGEX_SOCKET_LINK } from '../../Constants/Relay'
import {
Snackbar,
@ -21,55 +20,35 @@ import RBSheet from 'react-native-raw-bottom-sheet'
export const RelaysPage: React.FC = () => {
const defaultRelayInput = React.useMemo(() => 'wss://', [])
const { database } = useContext(AppContext)
const { relayPool, publicKey } = useContext(RelayPoolContext)
const { addRelayItem, removeRelayItem, relays } = useContext(RelayPoolContext)
const { t } = useTranslation('common')
const theme = useTheme()
const bottomSheetAddRef = React.useRef<RBSheet>(null)
const bottomSheetEditRef = React.useRef<RBSheet>(null)
const [relays, setRelays] = useState<Relay[]>([])
const [selectedRelay, setSelectedRelay] = useState<Relay>()
const [addRelayInput, setAddRelayInput] = useState<string>(defaultRelayInput)
const [showNotification, setShowNotification] = useState<'remove' | 'add' | 'badFormat'>()
const loadRelays: () => void = () => {
if (database) {
getRelays(database).then((results) => {
if (results) {
setRelays(results)
}
})
}
const addRelay: (url: string) => void = (url) => {
addRelayItem({
url,
}).then(() => {
setShowNotification('add')
})
}
useEffect(loadRelays, [])
const addRelayItem: (relay: Relay) => void = async (relay) => {
if (relayPool && database && publicKey) {
setRelays((prev) => [...prev, relay])
relayPool.add(relay.url, () => {
setShowNotification('add')
loadRelays()
})
}
}
const removeRelayItem: (relay: Relay) => void = async (relay) => {
if (relayPool && database && publicKey) {
setRelays((prev) => prev.filter((item) => item.url !== relay.url))
relayPool.remove(relay.url, () => {
setShowNotification('remove')
loadRelays()
})
}
const removeRelay: (url: string) => void = (url) => {
removeRelayItem({
url,
}).then(() => {
setShowNotification('remove')
})
}
const onPressAddRelay: () => void = () => {
if (REGEX_SOCKET_LINK.test(addRelayInput)) {
bottomSheetAddRef.current?.close()
addRelayItem({
url: addRelayInput,
})
setAddRelayInput(defaultRelayInput)
} else {
bottomSheetAddRef.current?.close()
@ -91,7 +70,7 @@ export const RelaysPage: React.FC = () => {
const active = relays?.some((item) => item.url === relay.url)
const onValueChange: () => void = () => {
active ? removeRelayItem(relay) : addRelayItem(relay)
active ? removeRelay(relay.url) : addRelay(relay.url)
}
return <Switch value={active} onValueChange={onValueChange} />
@ -109,6 +88,16 @@ export const RelaysPage: React.FC = () => {
/>
)
const rbSheetCustomStyles = React.useMemo(() => {
return {
container: {
...styles.rbsheetContainer,
backgroundColor: theme.colors.background,
},
draggableIcon: styles.rbsheetDraggableIcon,
}
}, [])
return (
<View style={styles.container}>
<FlatList style={styles.list} data={[...relays, ...defaultList()]} renderItem={renderItem} />
@ -130,18 +119,7 @@ export const RelaysPage: React.FC = () => {
>
{t(`relaysPage.${showNotification}`)}
</Snackbar>
<RBSheet
ref={bottomSheetAddRef}
closeOnDragDown={true}
height={260}
customStyles={{
container: {
...styles.rbsheetContainer,
backgroundColor: theme.colors.background,
},
draggableIcon: styles.rbsheetDraggableIcon,
}}
>
<RBSheet ref={bottomSheetAddRef} closeOnDragDown={true} height={260} customStyles={rbSheetCustomStyles}>
<View>
<TextInput
mode='outlined'
@ -168,13 +146,7 @@ export const RelaysPage: React.FC = () => {
ref={bottomSheetEditRef}
closeOnDragDown={true}
height={260}
customStyles={{
container: {
...styles.rbsheetContainer,
backgroundColor: theme.colors.background,
},
draggableIcon: styles.rbsheetDraggableIcon,
}}
customStyles={rbSheetCustomStyles}
>
<View>
<View style={styles.relayActions}>
@ -183,7 +155,7 @@ export const RelaysPage: React.FC = () => {
icon='trash-can-outline'
size={28}
onPress={() => {
if (selectedRelay) removeRelayItem(selectedRelay)
if (selectedRelay) removeRelay(selectedRelay.url)
bottomSheetEditRef.current?.close()
}}
/>
@ -200,7 +172,7 @@ export const RelaysPage: React.FC = () => {
<Text>{t('relaysPage.copyRelay')}</Text>
</View>
</View>
<Divider style={styles.divider}/>
<Divider style={styles.divider} />
<Text variant='titleLarge'>{selectedRelay?.url.split('wss://')[1]?.split('/')[0]}</Text>
</View>
</RBSheet>
@ -238,12 +210,12 @@ const styles = StyleSheet.create({
actionButton: {
justifyContent: 'center',
alignItems: 'center',
width: 80
width: 80,
},
divider: {
marginBottom: 26,
marginTop: 26
}
marginTop: 26,
},
})
export default RelaysPage

View File

@ -1,7 +1,6 @@
import React from 'react'
import { AppContextProvider } from './Contexts/AppContext'
import {
InitialState,
NavigationContainer,
DefaultTheme as NavigationDefaultTheme,
DarkTheme as NavigationDarkTheme,
@ -13,15 +12,15 @@ import { adaptNavigationTheme, Provider as PaperProvider } from 'react-native-pa
import { SafeAreaProvider, SafeAreaInsetsContext } from 'react-native-safe-area-context'
import i18n from './i18n.config'
import nostrosDarkTheme from './Constants/Theme/theme-dark.json'
import { navigationRef } from './lib/Navigation'
import HomeNavigator from './Pages/HomeNavigator'
import MenuItems from './Components/MenuItems'
import FeedNavigator from './Pages/FeedNavigator'
import { UserContextProvider } from './Contexts/UserContext'
const DrawerNavigator = createDrawerNavigator()
export const Frontend: React.FC = () => {
const [initialState] = React.useState<InitialState | undefined>()
const { DarkTheme } = adaptNavigationTheme({
reactNavigationLight: NavigationDefaultTheme,
reactNavigationDark: NavigationDarkTheme,
@ -41,39 +40,42 @@ export const Frontend: React.FC = () => {
<PaperProvider theme={nostrosDarkTheme}>
<SafeAreaProvider>
<I18nextProvider i18n={i18n}>
<AppContextProvider>
<RelayPoolContextProvider>
<React.Fragment>
<NavigationContainer theme={CombinedDefaultTheme} initialState={initialState}>
<SafeAreaInsetsContext.Consumer>
{() => {
return (
<DrawerNavigator.Navigator
drawerContent={({navigation}) => <MenuItems navigation={navigation}/>}
screenOptions={{
drawerStyle: {
borderRadius: 28
},
}}
>
<DrawerNavigator.Screen
name='Home'
component={HomeNavigator}
options={{ headerShown: false }}
/>
<DrawerNavigator.Screen
name='Feed'
component={FeedNavigator}
options={{ headerShown: false }}
/>
</DrawerNavigator.Navigator>
)
}}
</SafeAreaInsetsContext.Consumer>
</NavigationContainer>
</React.Fragment>
</RelayPoolContextProvider>
</AppContextProvider>
<NavigationContainer theme={CombinedDefaultTheme} ref={navigationRef}>
<AppContextProvider>
<UserContextProvider>
<RelayPoolContextProvider>
<React.Fragment>
<SafeAreaInsetsContext.Consumer>
{() => {
return (
<DrawerNavigator.Navigator
drawerContent={({ navigation }) => <MenuItems navigation={navigation} />}
screenOptions={{
drawerStyle: {
borderRadius: 28,
width: 296
},
}}
>
<DrawerNavigator.Screen
name='Home'
component={HomeNavigator}
options={{ headerShown: false }}
/>
<DrawerNavigator.Screen
name='Feed'
component={FeedNavigator}
options={{ headerShown: false }}
/>
</DrawerNavigator.Navigator>
)
}}
</SafeAreaInsetsContext.Consumer>
</React.Fragment>
</RelayPoolContextProvider>
</UserContextProvider>
</AppContextProvider>
</NavigationContainer>
</I18nextProvider>
</SafeAreaProvider>
</PaperProvider>

View File

@ -0,0 +1,15 @@
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef()
export const navigate: (name: string, params?: any) => void = (name, params ={}) => {
if (navigationRef.isReady()) {
navigationRef.navigate(name as never, params as never);
}
}
export const jumpTo: (name: string, params?: any) => void = (name, params ={}) => {
if (navigationRef.isReady()) {
navigationRef.jumpTo(name as never, params as never);
}
}

View File

@ -39,6 +39,7 @@
"react-native-multithreading": "^1.1.1",
"react-native-paper": "^5.1.3",
"react-native-parsed-text": "^0.0.22",
"react-native-qrcode-svg": "^6.1.2",
"react-native-quick-sqlite": "^6.1.1",
"react-native-raw-bottom-sheet": "^2.2.0",
"react-native-reanimated": "^2.14.0",
@ -46,7 +47,7 @@
"react-native-screens": "^3.18.2",
"react-native-securerandom": "^1.0.1",
"react-native-sensitive-info": "^5.5.8",
"react-native-svg": "^13.5.0",
"react-native-svg": "^13.7.0",
"react-native-vector-icons": "^9.2.0",
"react-native-webp-format": "^1.1.2",
"readable-stream": "^4.3.0",

View File

@ -3452,6 +3452,11 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dijkstrajs@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@ -3549,6 +3554,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encode-utf8@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@ -6894,6 +6904,11 @@ pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -7013,6 +7028,16 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qrcode@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.1.tgz#0103f97317409f7bc91772ef30793a54cd59f0cb"
integrity sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==
dependencies:
dijkstrajs "^1.0.1"
encode-utf8 "^1.0.3"
pngjs "^5.0.0"
yargs "^15.3.1"
query-string@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328"
@ -7148,6 +7173,14 @@ react-native-parsed-text@^0.0.22:
dependencies:
prop-types "^15.7.x"
react-native-qrcode-svg@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/react-native-qrcode-svg/-/react-native-qrcode-svg-6.1.2.tgz#a7cb6c10199ab01418a7f7700ce17a6a014f544e"
integrity sha512-lMbbxoPVybXCp9SYm73Aj/0iZ9OlSZl2u+zpdbjgC4DYHBm9m9tDQxISNg1OPeR7AAzmyx8IV4JTFmk8G5R22g==
dependencies:
prop-types "^15.7.2"
qrcode "^1.5.0"
react-native-quick-sqlite@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/react-native-quick-sqlite/-/react-native-quick-sqlite-6.1.1.tgz#ba35d0a4a919a5a0962306c3fee4dc46983b0839"
@ -7196,10 +7229,10 @@ react-native-sensitive-info@^5.5.8:
resolved "https://registry.yarnpkg.com/react-native-sensitive-info/-/react-native-sensitive-info-5.5.8.tgz#6ebb67eed83d1c2867bd435630ef2c41eef204ed"
integrity sha512-p99oaEW4QG1RdUNrkvd/c6Qdm856dQw/Rk81f9fA6Y3DlPs6ADNdU+jbPuTz3CcOUJwuKBDNenX6LR9KfmGFEg==
react-native-svg@^13.5.0:
version "13.6.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-13.6.0.tgz#46e95a44aabbd778db7c46d8a1047da376b28058"
integrity sha512-1wjHCMJ8siyZbDZ0MX5wM+Jr7YOkb6GADn4/Z+/u1UwJX8WfjarypxDF3UO1ugMHa+7qor39oY+URMcrgPpiww==
react-native-svg@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-13.7.0.tgz#be2ffb935e996762543dd7376bdc910722f7a43c"
integrity sha512-WR5CIURvee5cAfvMhmdoeOjh1SC8KdLq5u5eFsz4pbYzCtIFClGSkLnNgkMSDMVV5LV0qQa4jeIk75ieIBzaDA==
dependencies:
css-select "^5.1.0"
css-tree "^1.1.3"