nostros/frontend/Pages/ProfileConfigPage/index.tsx
2023-01-28 21:22:04 +01:00

455 lines
14 KiB
TypeScript

import React, { useContext, useEffect, useState } from 'react'
import { Linking, ScrollView, StyleSheet, View } from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import { AppContext } from '../../Contexts/AppContext'
import { useTranslation } from 'react-i18next'
import { UserContext } from '../../Contexts/UserContext'
import { Kind } from 'nostr-tools'
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'
import NostrosAvatar from '../../Components/NostrosAvatar'
import { getUnixTime } from 'date-fns'
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, lastEventId, lastConfirmationtId } = useContext(RelayPoolContext)
const {
publicKey,
nPub,
nSec,
name,
setName,
picture,
setPicture,
about,
setAbout,
lnurl,
setLnurl,
nip05,
setNip05,
reloadUser,
} = useContext(UserContext)
// State
const [showNotification, setShowNotification] = useState<undefined | string>()
const [isPublishingProfile, setIsPublishingProfile] = useState<string>()
const { t } = useTranslation('common')
useEffect(() => {
if (database && publicKey) {
relayPool?.unsubscribeAll()
relayPool?.subscribe('loading-meta', [
{
kinds: [Kind.Metadata],
authors: [publicKey],
},
])
}
}, [])
useEffect(() => {
if (isPublishingProfile) {
reloadUser()
setIsPublishingProfile(undefined)
setShowNotification(isPublishingProfile)
bottomSheetPictureRef.current?.close()
bottomSheetNip05Ref.current?.close()
bottomSheetLud06Ref.current?.close()
}
}, [lastEventId, lastConfirmationtId])
const publishUser: () => Promise<void> = async () => {
return await new Promise<void>((resolve) => {
if (publicKey && relayPool) {
relayPool
?.sendEvent({
content: JSON.stringify({
name,
about,
lud06: lnurl,
nip05,
picture,
}),
created_at: getUnixTime(new Date()),
kind: Kind.Metadata,
pubkey: publicKey,
tags: [],
})
.then(() => resolve())
} else {
resolve()
}
})
}
const onPublishUser: (notification: string) => void = (notification) => {
if (publicKey && database) {
setIsPublishingProfile(notification)
publishUser().catch(() => {
setIsPublishingProfile(undefined) // restore sending status
setShowNotification('connectionError')
})
}
}
const rbSheetCustomStyles = React.useMemo(() => {
return {
container: {
backgroundColor: theme.colors.background,
paddingTop: 16,
paddingRight: 16,
paddingBottom: 32,
paddingLeft: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
height: 'auto',
},
}
}, [])
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} showsVerticalScrollIndicator={false}>
<Card style={styles.cardContainer}>
<Card.Content>
<View style={styles.cardPicture}>
<TouchableRipple onPress={() => bottomSheetPictureRef.current?.open()}>
<View style={{ borderRadius: 50, overflow: 'hidden' }}>
{picture ? (
<Avatar.Image size={100} source={{ uri: picture }} />
) : (
<NostrosAvatar
name={name}
pubKey={nPub ?? ''}
src={picture}
lud06={lnurl}
size={100}
/>
)}
</View>
</TouchableRipple>
</View>
<View style={styles.cardActions}>
<View style={styles.actionButton}>
<IconButton
icon='content-copy'
size={28}
onPress={() => {
setShowNotification('npubCopied')
Clipboard.setString(nPub ?? '')
}}
/>
<Text>{t('profileConfigPage.copyNPub')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='folder-check-outline'
size={28}
onPress={() => bottomSheetDirectoryRef.current?.open()}
/>
<Text>{t('profileConfigPage.directory')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='check-decagram-outline'
size={28}
onPress={() => bottomSheetNip05Ref.current?.open()}
/>
<Text>{t('profileConfigPage.nip05')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='lightning-bolt'
size={28}
iconColor='#F5D112'
onPress={() => bottomSheetLud06Ref.current?.open()}
/>
<Text>{t('profileConfigPage.lud06')}</Text>
</View>
</View>
</Card.Content>
</Card>
<View style={styles.inputContainer}>
<TextInput
mode='outlined'
label={t('profileConfigPage.name') ?? ''}
onChangeText={setName}
value={name}
style={styles.input}
/>
<TextInput
mode='outlined'
label={t('profileConfigPage.about') ?? ''}
onChangeText={setAbout}
value={about}
multiline
numberOfLines={5}
style={styles.input}
/>
<TextInput
mode='outlined'
label={t('profileConfigPage.npub') ?? ''}
value={nPub}
selectTextOnFocus={true}
editable={false}
right={
<TextInput.Icon
icon='content-copy'
onPress={() => {
setShowNotification('npubCopied')
Clipboard.setString(nPub ?? '')
}}
forceTextInputFocus={false}
/>
}
style={styles.input}
/>
<TextInput
mode='outlined'
label={t('profileConfigPage.nsec') ?? ''}
value={nSec}
secureTextEntry={true}
editable={false}
selectTextOnFocus={true}
right={
<TextInput.Icon
icon='content-copy'
onPress={() => {
setShowNotification('nsecCopied')
Clipboard.setString(nSec ?? '')
}}
forceTextInputFocus={false}
/>
}
style={styles.input}
/>
<Button
mode='contained'
onPress={() => onPublishUser('profilePublished')}
loading={isPublishingProfile !== undefined}
>
{t('profileConfigPage.publish')}
</Button>
</View>
</ScrollView>
<RBSheet
ref={bottomSheetPictureRef}
closeOnDragDown={true}
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}
style={styles.imageInput}
right={
<TextInput.Icon
icon='content-paste'
onPress={pastePicture}
forceTextInputFocus={false}
/>
}
/>
<Button
mode='contained'
disabled={!picture || picture === ''}
onPress={() => onPublishUser('picturePublished')}
loading={isPublishingProfile !== undefined}
>
{t('profileConfigPage.publishPicture')}
</Button>
</View>
</RBSheet>
<RBSheet
ref={bottomSheetDirectoryRef}
closeOnDragDown={true}
customStyles={rbSheetCustomStyles}
>
<View>
<Text variant='titleLarge'>{t('profileConfigPage.directoryTitle')}</Text>
<Text style={styles.spacer} variant='bodyMedium'>
{t('profileConfigPage.directoryDescription')}
</Text>
<Button
style={styles.spacer}
mode='contained'
onPress={async () => await Linking.openURL('https://www.nostr.directory')}
loading={isPublishingProfile !== undefined}
>
{t('profileConfigPage.directoryContinue')}
</Button>
<Button mode='outlined' onPress={() => bottomSheetDirectoryRef.current?.close()}>
{t('profileConfigPage.directoryCancell')}
</Button>
</View>
</RBSheet>
<RBSheet ref={bottomSheetNip05Ref} closeOnDragDown={true} customStyles={rbSheetCustomStyles}>
<View>
<Text variant='titleLarge'>{t('profileConfigPage.nip05Title')}</Text>
<Text style={styles.spacer} variant='bodyMedium'>
{t('profileConfigPage.nip05Description')}
<Text
style={styles.link}
onPress={async () =>
await Linking.openURL('https://github.com/nostr-protocol/nips/blob/master/05.md')
}
>
{t('profileConfigPage.nip05Link')}
</Text>
</Text>
<TextInput
style={styles.spacer}
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={() => onPublishUser('nip05Published')}
loading={isPublishingProfile !== undefined}
>
{t('profileConfigPage.publishNip05')}
</Button>
</View>
</RBSheet>
<RBSheet ref={bottomSheetLud06Ref} closeOnDragDown={true} customStyles={rbSheetCustomStyles}>
<View>
<Text variant='titleLarge'>{t('profileConfigPage.lud06Title')}</Text>
<Text style={styles.spacer} variant='bodyMedium'>
{t('profileConfigPage.lud06Description')}
</Text>
<TextInput
style={styles.spacer}
mode='outlined'
multiline
label={t('profileConfigPage.lud06Label') ?? ''}
onChangeText={setLnurl}
value={lnurl}
right={
<TextInput.Icon
icon='content-paste'
onPress={pasteLud06}
forceTextInputFocus={false}
/>
}
/>
<Button
mode='contained'
disabled={!lnurl || lnurl === ''}
onPress={() => onPublishUser('lud06Published')}
loading={isPublishingProfile !== undefined}
>
{t('profileConfigPage.publishLud06')}
</Button>
</View>
</RBSheet>
{showNotification && (
<Snackbar
style={styles.snackbar}
visible={showNotification !== undefined}
duration={Snackbar.DURATION_SHORT}
onIconPress={() => setShowNotification(undefined)}
onDismiss={() => setShowNotification(undefined)}
>
{t(`profileConfigPage.notifications.${showNotification}`, { nip05, lud06: lnurl })}
</Snackbar>
)}
</View>
)
}
const styles = StyleSheet.create({
spacer: {
marginBottom: 16,
},
container: {
padding: 16,
},
inputContainer: {
justifyContent: 'space-between',
paddingTop: 16,
},
input: {
marginBottom: 16,
},
cardContainer: {
width: '100%',
justifyContent: 'center',
alignContent: 'center',
},
cardActions: {
flexDirection: 'row',
justifyContent: 'space-around',
},
cardPicture: {
flexDirection: 'row',
justifyContent: 'center',
alignContent: 'center',
},
actionButton: {
marginTop: 32,
justifyContent: 'center',
alignItems: 'center',
},
snackbar: {
marginLeft: 16,
bottom: 24,
width: '100%',
},
link: {
textDecorationLine: 'underline',
},
imageInput: {
marginTop: 16,
marginBottom: 16,
},
})
export default ProfileConfigPage