Share contact and Upload image preview (#221)

This commit is contained in:
KoalaSat 2023-01-31 09:46:55 +00:00 committed by GitHub
commit 25de204b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 263 additions and 53 deletions

View File

@ -2,6 +2,8 @@
package="com.nostros">
<uses-permission android:name="android.permission.INTERNET" />
<!-- required for react-native-share base64 sharing -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".MainApplication"

View File

@ -156,8 +156,8 @@ export const LnPayment: React.FC<TextContentProps> = ({ open, setOpen, event, us
>
<Card style={styles.qrContainer}>
<Card.Content>
<View>
<QRCode value={invoice} size={350} />
<View style={styles.qr}>
<QRCode value={invoice} size={300} quietZone={8}/>
</View>
<View style={styles.qrText}>
<Text>{monto} </Text>
@ -222,6 +222,11 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-around',
},
qr: {
justifyContent: 'center',
alignItems: 'center',
padding: 16
},
})
export default LnPayment

View File

@ -2,7 +2,7 @@ import { t } from 'i18next'
import * as React from 'react'
import { StyleSheet, View } from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import { Card, IconButton, Snackbar, Text, useTheme } from 'react-native-paper'
import { Card, IconButton, Snackbar, Text, TouchableRipple, useTheme } from 'react-native-paper'
import { AppContext } from '../../Contexts/AppContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { UserContext } from '../../Contexts/UserContext'
@ -12,6 +12,7 @@ import {
updateUserContact,
User,
} from '../../Functions/DatabaseFunctions/Users'
import Share from 'react-native-share'
import { populatePets, usernamePubKey } from '../../Functions/RelayFunctions/Users'
import LnPayment from '../LnPayment'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
@ -19,6 +20,7 @@ import { navigate, push } from '../../lib/Navigation'
import RBSheet from 'react-native-raw-bottom-sheet'
import { getNpub } from '../../lib/nostr/Nip19'
import ProfileData from '../ProfileData'
import QRCode from 'react-native-qrcode-svg'
interface ProfileCardProps {
userPubKey: string
@ -32,6 +34,7 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({
showImages = true,
}) => {
const theme = useTheme()
const bottomSheetShareRef = React.useRef<RBSheet>(null)
const { database } = React.useContext(AppContext)
const { publicKey } = React.useContext(UserContext)
const { relayPool } = React.useContext(RelayPoolContext)
@ -40,6 +43,7 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({
const [openLn, setOpenLn] = React.useState<boolean>(false)
const [isContact, setIsContact] = React.useState<boolean>()
const [showNotification, setShowNotification] = React.useState<undefined | string>()
const [qrCode, setQrCode] = React.useState<any>()
const nPub = React.useMemo(() => getNpub(userPubKey), [userPubKey])
const username = React.useMemo(() => usernamePubKey(user?.name ?? '', nPub), [nPub, user])
@ -96,6 +100,21 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({
push('Profile', { pubKey: userPubKey, title: username })
}
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
backgroundColor: theme.colors.background,
paddingTop: 16,
paddingRight: 16,
paddingBottom: 32,
paddingLeft: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
height: 'auto',
},
}
}, [])
return (
<View>
<Card onPress={goToProfile}>
@ -144,14 +163,6 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({
<Text>{isContact ? t('profileCard.unfollow') : t('profileCard.follow')}</Text>
</View>
)}
<View style={styles.actionButton}>
<IconButton
icon={blocked ? 'account-cancel' : 'account-cancel-outline'}
size={28}
onPress={onChangeBlockUser}
/>
<Text>{t(blocked ? 'profileCard.unblock' : 'profileCard.block')}</Text>
</View>
<View style={styles.actionButton}>
<IconButton
icon='message-plus-outline'
@ -165,14 +176,13 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({
</View>
<View style={styles.actionButton}>
<IconButton
icon='content-copy'
icon='share-variant-outline'
size={28}
onPress={() => {
setShowNotification('npubCopied')
Clipboard.setString(nPub ?? '')
bottomSheetShareRef.current?.open()
}}
/>
<Text>{t('profileCard.copyNPub')}</Text>
<Text>{t('profileCard.share')}</Text>
</View>
{user?.lnurl && (
<View style={styles.actionButton}>
@ -187,6 +197,14 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({
</>
</View>
)}
<View style={styles.actionButton}>
<IconButton
icon={blocked ? 'account-cancel' : 'account-cancel-outline'}
size={28}
onPress={onChangeBlockUser}
/>
<Text>{t(blocked ? 'profileCard.unblock' : 'profileCard.block')}</Text>
</View>
</View>
{showNotification && (
<Snackbar
@ -200,6 +218,60 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({
</Snackbar>
)}
<LnPayment setOpen={setOpenLn} open={openLn} user={user} />
<RBSheet ref={bottomSheetShareRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<View style={styles.mainLayout}>
<View style={styles.qr}>
<TouchableRipple
onPress={() => {
if (qrCode) {
qrCode.toDataURL((base64: string) => {
Share.open({
url: `data:image/png;base64,${base64}`,
filename: user?.id ?? 'nostrosshare'
})
})
}
}}
>
<QRCode
quietZone={8}
value={`nostr:${nPub}`}
size={350}
logoBorderRadius={50}
logoSize={100}
logo={{ uri: user?.picture }}
getRef={setQrCode}
/>
</TouchableRipple>
</View>
<View style={styles.shareActionButton}>
<IconButton
icon='key-outline'
size={28}
onPress={() => {
setShowNotification('npubCopied')
Clipboard.setString(nPub ?? '')
bottomSheetShareRef.current?.close()
}}
/>
<Text>{t('profileCard.copyNPub')}</Text>
</View>
{user?.nip05 && (
<View style={styles.shareActionButton}>
<IconButton
icon='check-decagram-outline'
size={28}
onPress={() => {
setShowNotification('npubCopied')
Clipboard.setString(user?.nip05 ?? '')
bottomSheetShareRef.current?.close()
}}
/>
<Text>{t('profileCard.copyNip05')}</Text>
</View>
)}
</View>
</RBSheet>
</View>
)
}
@ -247,7 +319,18 @@ const styles = StyleSheet.create({
actionButton: {
justifyContent: 'center',
alignItems: 'center',
flexBasis: '33.333333%',
flexBasis: '25%',
marginBottom: 4,
},
qr: {
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
shareActionButton: {
justifyContent: 'center',
alignItems: 'center',
flexBasis: '50%',
marginBottom: 4,
},
list: {

View File

@ -6,6 +6,7 @@ import { Linking, StyleSheet } from 'react-native'
import { Text } from 'react-native-paper'
import { Config } from '../Pages/ConfigPage'
import { imageHostingServices } from '../Constants/Services'
import { randomInt } from '../Functions/NativeFunctions'
export interface AppContextProps {
init: () => void
@ -22,6 +23,7 @@ export interface AppContextProps {
setImageHostingService: (imageHostingService: string) => void
setSatoshi: (showPublicImages: 'kebab' | 'sats') => void
getSatoshiSymbol: (fontSize?: number) => JSX.Element
getImageHostingService: () => string
}
export interface AppContextProviderProps {
@ -42,6 +44,7 @@ export const initialAppContext: AppContextProps = {
setSatoshi: () => {},
imageHostingService: Object.keys(imageHostingServices)[0],
setImageHostingService: () => {},
getImageHostingService: () => "",
getSatoshiSymbol: () => <></>,
}
@ -101,6 +104,14 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
)
}
const getImageHostingService: () => string = () => {
if (imageHostingService !== 'random') return imageHostingService
const randomIndex = randomInt(1, Object.keys(imageHostingServices).length)
return Object.keys(imageHostingServices)[randomIndex - 1]
}
useEffect(init, [])
return (
@ -108,6 +119,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
value={{
imageHostingService,
setImageHostingService,
getImageHostingService,
init,
loadingDb,
database,

View File

@ -61,3 +61,5 @@ export const validNip21: (string: string | undefined) => boolean = (string) => {
return false
}
}
export const randomInt: (min: number, max: number) => number = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min

View File

@ -47,8 +47,11 @@
"isNotContact": "Not following",
"contentWarning": "Sensitive content",
"send": "Send",
"imageUploaded": "Your file has been uploaded.\nConsider donating to the service: {{uri}}",
"imageUploadErro": "There was an error while trying to upload your file"
"imageUploaded": "Your file has been uploaded.\nConsider donating to the service:\n{{uri}}",
"imageUploadErro": "There was an error while trying to upload your file",
"uploadImage": "Upload image now",
"cancel": "Cancel",
"poweredBy": "Powered by {{uri}}"
},
"menuItems": {
"relays": "Relays",
@ -64,7 +67,8 @@
"showPublicImages": "Show images on public feed",
"showSensitive": "Show sensitive notes",
"satoshi": "Satoshi symbol",
"imageHostingService": "Image hosting service"
"imageHostingService": "Image hosting service",
"random": "Random"
},
"noteCard": {
"answering": "Answer to {{pubkey}}",
@ -233,6 +237,8 @@
"unfollow": "Following",
"block": "Block",
"unblock": "Unblock",
"share": "Share",
"copyNip05": "Copy NIP-05",
"copyNPub": "Copy key"
},
"conversationsFeed": {

View File

@ -46,7 +46,12 @@
"isContact": "Siguiendo",
"isNotContact": "Sin seguir",
"contentWarning": "Contenido sensible",
"send": "Enviar"
"send": "Enviar",
"imageUploaded": "Tu archivo se ha subido.\nConsidera donar al servicio:\n{{uri}}",
"imageUploadErro": "Se ha producido un error al subir la imagen.",
"uploadImage": "Subir imagen ahora",
"cancel": "Cancelar",
"poweredBy": "Servido por {{uri}}"
},
"menuItems": {
"relays": "Relays",
@ -62,7 +67,8 @@
"showPublicImages": "Mostrar imágenes en feed global",
"showSensitive": "Mostrar notas sensibles",
"satoshi": "Símbolo de satoshi",
"imageHostingService": "Servicio de subida de imágenes"
"imageHostingService": "Servicio de subida de imágenes",
"random": "Aleatorio"
},
"noteCard": {
"answering": "Responder a {{pubkey}}",
@ -217,8 +223,10 @@
"message": "Mensaje",
"follow": "Seguir",
"block": "Bloquear",
"share": "Share",
"unblock": "Desbloquear",
"unfollow": "Siguiendo",
"copyNip05": "Copiar NIP-05",
"copyNPub": "Copiar clave"
},
"conversationsFeed": {

View File

@ -46,7 +46,12 @@
"isContact": "Following",
"isNotContact": "Not following",
"contentWarning": "Делекатный контент",
"send": "Send"
"send": "Send",
"imageUploaded": "Tu archivo se ha subido.\nConsidera donar al servicio:\n{{uri}}",
"imageUploadErro": "Se ha producido un error al subir la imagen.",
"uploadImage": "Subir imagen ahora",
"cancel": "Отменить",
"poweredBy": "Powered by {{uri}}"
},
"menuItems": {
"relays": "Реле",
@ -62,7 +67,8 @@
"configPage": {
"showPublicImages": "Show images on public feed",
"showSensitive": "Show sensitive notes",
"satoshi": "Satoshi symbol"
"satoshi": "Satoshi symbol",
"random": "Random"
},
"noteCard": {
"answering": "Ответить {{pubkey}}",
@ -218,6 +224,8 @@
"block": "Block",
"unblock": "Desbloquear",
"unfollow": "Following",
"share": "Share",
"copyNip05": "Copy NIP-05",
"copyNPub": "Copy key"
},
"conversationsFeed": {

View File

@ -67,10 +67,10 @@ export const ConfigPage: React.FC = () => {
}, [])
const imageHostingOptions = React.useMemo(() => {
return Object.keys(imageHostingServices).map((service, index) => {
return ['random', ...Object.keys(imageHostingServices)].map((service, index) => {
return {
key: index,
title: <Text>{imageHostingServices[service].uri}</Text>,
title: <Text>{imageHostingServices[service]?.uri ?? t(`configPage.${service}`)}</Text>,
onPress: () => {
setImageHostingService(service)
SInfo.getItem('config', {}).then((result) => {
@ -141,7 +141,12 @@ export const ConfigPage: React.FC = () => {
<List.Item
title={t('configPage.imageHostingService')}
onPress={() => bottomSheetImageHostingRef.current?.open()}
right={() => <Text>{imageHostingServices[imageHostingService].uri}</Text>}
right={() => (
<Text>
{imageHostingServices[imageHostingService]?.uri ??
t(`configPage.${imageHostingService}`)}
</Text>
)}
/>
<RBSheet ref={bottomSheetSatoshiRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<FlatList

View File

@ -167,15 +167,19 @@ export const RelaysPage: React.FC = () => {
)}
<RBSheet ref={bottomSheetAddRef} closeOnDragDown={true} customStyles={rbSheetCustomStyles}>
<View style={styles.addRelay}>
<TextInput
mode='outlined'
label={t('relaysPage.labelAdd') ?? ''}
onChangeText={setAddRelayInput}
value={addRelayInput}
/>
<Button mode='contained' onPress={onPressAddRelay}>
{t('relaysPage.add')}
</Button>
<View style={styles.bottomDrawerButton}>
<TextInput
mode='outlined'
label={t('relaysPage.labelAdd') ?? ''}
onChangeText={setAddRelayInput}
value={addRelayInput}
/>
</View>
<View style={styles.bottomDrawerButton}>
<Button mode='contained' onPress={onPressAddRelay}>
{t('relaysPage.add')}
</Button>
</View>
<Button
mode='outlined'
onPress={() => {
@ -224,6 +228,9 @@ const styles = StyleSheet.create({
title: {
paddingLeft: 16,
},
bottomDrawerButton: {
paddingBottom: 16,
},
container: {
padding: 0,
paddingBottom: 32,
@ -243,7 +250,6 @@ const styles = StyleSheet.create({
},
addRelay: {
alignContent: 'center',
height: '80%',
justifyContent: 'space-between',
},
relayActions: {

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'
import React, { useContext, useEffect, useMemo, useState } from 'react'
import { StyleSheet, View } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { Event } from '../../lib/nostr/Events'
@ -9,15 +9,17 @@ import { Note } from '../../Functions/DatabaseFunctions/Notes'
import { getETags, getTaggedPubKeys } from '../../Functions/RelayFunctions/Events'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { formatPubKey } from '../../Functions/RelayFunctions/Users'
import { launchImageLibrary } from 'react-native-image-picker'
import { Asset, launchImageLibrary } from 'react-native-image-picker'
import {
Button,
Card,
IconButton,
Snackbar,
Switch,
Text,
TextInput,
TouchableRipple,
useTheme,
} from 'react-native-paper'
import { UserContext } from '../../Contexts/UserContext'
import { goBack } from '../../lib/Navigation'
@ -25,13 +27,15 @@ import { Kind } from 'nostr-tools'
import ProfileData from '../../Components/ProfileData'
import NoteCard from '../../Components/NoteCard'
import { imageHostingServices } from '../../Constants/Services'
import RBSheet from 'react-native-raw-bottom-sheet'
interface SendPageProps {
route: { params: { note: Note; type?: 'reply' | 'repost' } | undefined }
}
export const SendPage: React.FC<SendPageProps> = ({ route }) => {
const { database, imageHostingService } = useContext(AppContext)
const theme = useTheme()
const { database, getImageHostingService } = useContext(AppContext)
const { publicKey } = useContext(UserContext)
const { relayPool, lastConfirmationtId } = useContext(RelayPoolContext)
const { t } = useTranslation('common')
@ -43,7 +47,10 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
const [userSuggestions, setUserSuggestions] = useState<User[]>([])
const [userMentions, setUserMentions] = useState<User[]>([])
const [isSending, setIsSending] = useState<boolean>(false)
const [imageUpload, setImageUpload] = useState<Asset>()
const note = React.useMemo(() => route.params?.note, [])
const [imageHostingService] = useState<string>(getImageHostingService())
const bottomSheetImageRef = React.useRef<RBSheet>(null)
useEffect(() => {
if (isSending) goBack()
@ -76,32 +83,44 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
return `@${user.name ?? formatPubKey(user.id)}`
}
const onUploadImage: () => void = async () => {
launchImageLibrary({ selectionLimit: 1, quality: 0, mediaType: 'photo' }, async (result) => {
const getImage: () => void = () => {
launchImageLibrary({ selectionLimit: 1, mediaType: 'photo' }, async (result) => {
const assets = result?.assets
if (assets && assets.length > 0) {
const file = assets[0]
if (file.uri && file.type && file.fileName) {
imageHostingServices[imageHostingService]
.sendFunction(file.uri, file.type, file.fileName)
.then((imageUri) => {
setShowNotification('imageUploaded')
setUploadingFile(false)
setContent((prev) => `${prev}\n\n${imageUri}`)
})
.catch(() => {
setShowNotification('imageUploadErro')
setUploadingFile(false)
})
setImageUpload(file)
bottomSheetImageRef.current?.open()
} else {
setUploadingFile(false)
setShowNotification('imageUploadErro')
}
} else {
setUploadingFile(false)
setShowNotification('imageUploadErro')
}
})
}
const uploadImage: () => void = async () => {
if (imageUpload?.uri && imageUpload.type && imageUpload.fileName) {
imageHostingServices[imageHostingService]
.sendFunction(imageUpload.uri, imageUpload.type, imageUpload.fileName)
.then((imageUri) => {
bottomSheetImageRef.current?.close()
setUploadingFile(false)
setContent((prev) => `${prev}\n\n${imageUri}`)
setImageUpload(undefined)
setShowNotification('imageUploaded')
})
.catch(() => {
bottomSheetImageRef.current?.close()
setUploadingFile(false)
setShowNotification('imageUploadErro')
})
}
}
const onPressSend: () => void = () => {
if (database && publicKey) {
setIsSending(true)
@ -179,6 +198,21 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
</TouchableRipple>
)
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
backgroundColor: theme.colors.background,
paddingTop: 16,
paddingRight: 16,
paddingBottom: 32,
paddingLeft: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
height: 'auto',
},
}
}, [])
return (
<>
<View style={[styles.textInputContainer, { paddingBottom: note ? 200 : 10 }]}>
@ -222,7 +256,7 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
icon='image-outline'
size={25}
style={styles.imageButton}
onPress={onUploadImage}
onPress={getImage}
disabled={uploadingFile}
/>
</View>
@ -239,6 +273,31 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
</View>
)}
</View>
<RBSheet ref={bottomSheetImageRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<Card style={styles.imageUploadPreview}>
{imageUpload && (
<Card.Cover source={{ uri: imageUpload?.uri ?? '' }} resizeMode='contain' />
)}
</Card>
<Text>{t('sendPage.poweredBy', { uri: imageHostingServices[imageHostingService].uri })}</Text>
<Button
style={styles.buttonSpacer}
mode='contained'
onPress={uploadImage}
loading={uploadingFile}
>
{t('sendPage.uploadImage')}
</Button>
<Button
mode='outlined'
onPress={() => {
bottomSheetImageRef.current?.close()
setImageUpload(undefined)
}}
>
{t('sendPage.cancel')}
</Button>
</RBSheet>
{showNotification && (
<Snackbar
style={styles.snackbar}
@ -324,6 +383,14 @@ const styles = StyleSheet.create({
paddingTop: 4,
paddingLeft: 5,
},
imageUploadPreview: {
marginTop: 16,
marginBottom: 16,
},
buttonSpacer: {
marginTop: 16,
marginBottom: 16,
},
})
export default SendPage

View File

@ -53,6 +53,7 @@
"react-native-screens": "^3.19.0",
"react-native-securerandom": "^1.0.1",
"react-native-sensitive-info": "^5.5.8",
"react-native-share": "^8.1.0",
"react-native-svg": "^13.7.0",
"react-native-tab-view": "^3.3.4",
"react-native-vector-icons": "^9.2.0",

View File

@ -7287,6 +7287,11 @@ 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-share@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/react-native-share/-/react-native-share-8.1.0.tgz#34c9977e5aa49254b191f19d779bb4cff43fe2da"
integrity sha512-gME+6+FkQQ5/Ss4ulPjxwtgyZsF/YqBvG3qIVWN1urUhFFG2m2kycrNB0fPLLZy517/G6aDyUMioVZtPQArRHQ==
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"