Upload images (#215)

This commit is contained in:
KoalaSat 2023-01-30 17:58:18 +00:00 committed by GitHub
commit 945446b5f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 289 additions and 20 deletions

View File

@ -0,0 +1,25 @@
import { nostrBuildUpload } from '../../Functions/ServicesFunctions/NostrBuildUpload'
import { voidCatUpload } from '../../Functions/ServicesFunctions/VoidCatUpload'
export const imageHostingServices: Record<
string,
{
uri: string
uploadUrl: string
donation: string
sendFunction: (fileUri: string, fileType: string, filename: string) => Promise<string | null>
}
> = {
voidCat: {
uri: 'https://void.cat',
uploadUrl: 'https://void.cat/upload',
donation: 'https://void.cat/donate',
sendFunction: voidCatUpload,
},
nostrBuild: {
uri: 'https://nostr.build',
uploadUrl: 'https://nostr.build/upload.php',
donation: 'https://nostr.build',
sendFunction: nostrBuildUpload,
},
}

View File

@ -4,6 +4,8 @@ import { initDatabase } from '../Functions/DatabaseFunctions'
import SInfo from 'react-native-sensitive-info'
import { Linking, StyleSheet } from 'react-native'
import { Text } from 'react-native-paper'
import { Config } from '../Pages/ConfigPage'
import { imageHostingServices } from '../Constants/Services'
export interface AppContextProps {
init: () => void
@ -16,6 +18,8 @@ export interface AppContextProps {
showSensitive: boolean
setShowSensitive: (showPublicImages: boolean) => void
satoshi: 'kebab' | 'sats'
imageHostingService: string
setImageHostingService: (imageHostingService: string) => void
setSatoshi: (showPublicImages: 'kebab' | 'sats') => void
getSatoshiSymbol: (fontSize?: number) => JSX.Element
}
@ -36,6 +40,8 @@ export const initialAppContext: AppContextProps = {
setShowSensitive: () => {},
satoshi: 'kebab',
setSatoshi: () => {},
imageHostingService: Object.keys(imageHostingServices)[0],
setImageHostingService: () => {},
getSatoshiSymbol: () => <></>,
}
@ -44,6 +50,9 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
initialAppContext.showPublicImages,
)
const [showSensitive, setShowSensitive] = React.useState<boolean>(initialAppContext.showSensitive)
const [imageHostingService, setImageHostingService] = React.useState<string>(
initialAppContext.imageHostingService,
)
const [notificationSeenAt, setNotificationSeenAt] = React.useState<number>(0)
const [satoshi, setSatoshi] = React.useState<'kebab' | 'sats'>(initialAppContext.satoshi)
const [database, setDatabase] = useState<QuickSQLiteConnection | null>(null)
@ -64,10 +73,14 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
setShowSensitive(config.show_sensitive ?? initialAppContext.showSensitive)
setSatoshi(config.satoshi)
setNotificationSeenAt(config.last_notification_seen_at ?? 0)
setImageHostingService(
config.image_hosting_service ?? initialAppContext.imageHostingService,
)
} else {
const config: Config = {
show_public_images: initialAppContext.showPublicImages,
show_sensitive: initialAppContext.showSensitive,
image_hosting_service: initialAppContext.imageHostingService,
satoshi: initialAppContext.satoshi,
last_notification_seen_at: 0,
last_pets_at: 0,
@ -93,6 +106,8 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
return (
<AppContext.Provider
value={{
imageHostingService,
setImageHostingService,
init,
loadingDb,
database,

View File

@ -0,0 +1,33 @@
import { imageHostingServices } from '../../../Constants/Services'
import axios from 'axios'
export const nostrBuildUpload: (
fileUri: string,
fileType: string,
fileName: string,
) => Promise<string | null> = async (fileUri, fileType, fileName) => {
return await new Promise<string | null>((resolve, reject) => {
const formdata = new FormData()
formdata.append('fileToUpload', {
uri: fileUri,
name: fileName,
type: fileType,
})
formdata.append('submit', 'Upload Image')
const headers = {
'Content-Type': 'multipart/form-data',
}
axios
.post(imageHostingServices.nostrBuild.uploadUrl, formdata, {
headers,
})
.then((response) => {
const regExp = /(https:\/\/nostr.build\/i\/nostr.build.*)<\/b>/
const imageUrl: string = response.data.match(regExp)[0].slice(0, -4)
resolve(imageUrl)
})
.catch(() => {
reject(new Error('Error uploading image'))
})
})
}

View File

@ -0,0 +1,39 @@
// Thanks to v0l/snort for the nice code!
// https://github.com/v0l/snort/blob/39fbe3b10f94b7542df01fb085e4f164aab15fca/src/Feed/VoidUpload.ts
import { imageHostingServices } from '../../../Constants/Services'
import ReactNativeBlobUtil from 'react-native-blob-util'
export const voidCatUpload: (
fileUri: string,
fileType: string,
fileName: string,
) => Promise<string | null> = async (fileUri, fileType, fileName) => {
const digest = await ReactNativeBlobUtil.fs.hash(fileUri, 'sha256')
return await new Promise<string | null>((resolve, reject) => {
ReactNativeBlobUtil.fetch(
'POST',
imageHostingServices.voidCat.uploadUrl,
{
'Content-Type': 'application/octet-stream',
'V-Content-Type': fileType,
'V-Filename': fileName,
'V-Full-Digest': digest,
'V-Description': 'Uploaded from Nostros https://github.com/KoalaSat/nostros',
'V-Strip-Metadata': 'true',
},
ReactNativeBlobUtil.wrap(fileUri),
)
.then((repsp) => JSON.parse(repsp.data))
.then((data) => {
if (data.ok) {
resolve(`${imageHostingServices.voidCat.uri}/d/${data.file.id}.png`)
} else {
reject(new Error('Error uploading image'))
}
})
.catch(() => {
reject(new Error('Error uploading image'))
})
})
}

View File

@ -46,7 +46,9 @@
"isContact": "Following",
"isNotContact": "Not following",
"contentWarning": "Sensitive content",
"send": "Send"
"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"
},
"menuItems": {
"relays": "Relays",
@ -61,7 +63,8 @@
"configPage": {
"showPublicImages": "Show images on public feed",
"showSensitive": "Show sensitive notes",
"satoshi": "Satoshi symbol"
"satoshi": "Satoshi symbol",
"imageHostingService": "Image hosting service"
},
"noteCard": {
"answering": "Answer to {{pubkey}}",

View File

@ -61,7 +61,8 @@
"configPage": {
"showPublicImages": "Mostrar imágenes en feed global",
"showSensitive": "Mostrar notas sensibles",
"satoshi": "Símbolo de satoshi"
"satoshi": "Símbolo de satoshi",
"imageHostingService": "Servicio de subida de imágenes"
},
"noteCard": {
"answering": "Responder a {{pubkey}}",

View File

@ -61,7 +61,8 @@
"configPage": {
"showPublicImages": "Afficher les images dans le flux global",
"showSensitive": "Montrer les notes sensibles",
"satoshi": "Symbole de satoshi"
"satoshi": "Symbole de satoshi",
"imageHostingService": "Image hosting service"
},
"noteCard": {
"answering": "Répondre à {{pubkey}}",

View File

@ -56,7 +56,8 @@
"followers": "{{followers}} followers",
"configuration": "Настройки",
"about": "Подроблее",
"logout": "Выйти"
"logout": "Выйти",
"imageHostingService": "Хостинг изображений"
},
"configPage": {
"showPublicImages": "Show images on public feed",

View File

@ -5,7 +5,16 @@ import { Divider, List, Switch, useTheme } from 'react-native-paper'
import SInfo from 'react-native-sensitive-info'
import RBSheet from 'react-native-raw-bottom-sheet'
import { AppContext } from '../../Contexts/AppContext'
import { Config } from '../../Functions/DatabaseFunctions/Config'
import { imageHostingServices } from '../../Constants/Services'
export interface Config {
satoshi: 'kebab' | 'sats'
show_public_images: boolean
show_sensitive: boolean
last_notification_seen_at: number
last_pets_at: number
image_hosting_service: string
}
export const ConfigPage: React.FC = () => {
const theme = useTheme()
@ -18,12 +27,15 @@ export const ConfigPage: React.FC = () => {
setShowSensitive,
satoshi,
setSatoshi,
imageHostingService,
setImageHostingService,
} = React.useContext(AppContext)
const bottomSheetRef = React.useRef<RBSheet>(null)
const bottomSheetSatoshiRef = React.useRef<RBSheet>(null)
const bottomSheetImageHostingRef = React.useRef<RBSheet>(null)
React.useEffect(() => {}, [showPublicImages, showSensitive, satoshi])
const createOptions = React.useMemo(() => {
const satoshiOptions = React.useMemo(() => {
return [
{
key: 1,
@ -35,7 +47,7 @@ export const ConfigPage: React.FC = () => {
config.satoshi = 'kebab'
SInfo.setItem('config', JSON.stringify(config), {})
})
bottomSheetRef.current?.close()
bottomSheetSatoshiRef.current?.close()
},
},
{
@ -48,12 +60,30 @@ export const ConfigPage: React.FC = () => {
config.satoshi = 'sats'
SInfo.setItem('config', JSON.stringify(config), {})
})
bottomSheetRef.current?.close()
bottomSheetSatoshiRef.current?.close()
},
},
]
}, [])
const imageHostingOptions = React.useMemo(() => {
return Object.keys(imageHostingServices).map((service, index) => {
return {
key: index,
title: <Text>{imageHostingServices[service].uri}</Text>,
onPress: () => {
setImageHostingService(service)
SInfo.getItem('config', {}).then((result) => {
const config: Config = JSON.parse(result)
config.image_hosting_service = service
SInfo.setItem('config', JSON.stringify(config), {})
})
bottomSheetImageHostingRef.current?.close()
},
}
})
}, [])
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
@ -105,12 +135,30 @@ export const ConfigPage: React.FC = () => {
/>
<List.Item
title={t('configPage.satoshi')}
onPress={() => bottomSheetRef.current?.open()}
onPress={() => bottomSheetSatoshiRef.current?.open()}
right={() => getSatoshiSymbol(25)}
/>
<RBSheet ref={bottomSheetRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<List.Item
title={t('configPage.imageHostingService')}
onPress={() => bottomSheetImageHostingRef.current?.open()}
right={() => <Text>{imageHostingServices[imageHostingService].uri}</Text>}
/>
<RBSheet ref={bottomSheetSatoshiRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<FlatList
data={createOptions}
data={satoshiOptions}
renderItem={({ item }) => {
return <List.Item key={item.key} title={item.title} onPress={item.onPress} />
}}
ItemSeparatorComponent={Divider}
/>
</RBSheet>
<RBSheet
ref={bottomSheetImageHostingRef}
closeOnDragDown={true}
customStyles={bottomSheetStyles}
>
<FlatList
data={imageHostingOptions}
renderItem={({ item }) => {
return <List.Item key={item.key} title={item.title} onPress={item.onPress} />
}}

View File

@ -6,6 +6,7 @@ import { UserContext } from '../../Contexts/UserContext'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { useTranslation } from 'react-i18next'
import getUnixTime from 'date-fns/getUnixTime'
import debounce from 'lodash.debounce'
import { StyleSheet, View } from 'react-native'
import Logo from '../../Components/Logo'
import { Button, Text, useTheme } from 'react-native-paper'
@ -23,14 +24,17 @@ export const ProfileLoadPage: React.FC = () => {
useFocusEffect(
React.useCallback(() => {
debounce(() => {
loadMeta()
loadPets()
}, 500)
return () => relayPool?.unsubscribe(['profile-load-notes', 'profile-load-meta-pets'])
}, []),
)
useEffect(() => {
loadMeta()
loadPets()
reloadUser()
if (name) {

View File

@ -9,23 +9,35 @@ 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 { Button, Switch, Text, TextInput, TouchableRipple } from 'react-native-paper'
import { launchImageLibrary } from 'react-native-image-picker'
import {
Button,
IconButton,
Snackbar,
Switch,
Text,
TextInput,
TouchableRipple,
} from 'react-native-paper'
import { UserContext } from '../../Contexts/UserContext'
import { goBack } from '../../lib/Navigation'
import { Kind } from 'nostr-tools'
import ProfileData from '../../Components/ProfileData'
import NoteCard from '../../Components/NoteCard'
import { imageHostingServices } from '../../Constants/Services'
interface SendPageProps {
route: { params: { note: Note; type?: 'reply' | 'repost' } | undefined }
}
export const SendPage: React.FC<SendPageProps> = ({ route }) => {
const { database } = useContext(AppContext)
const { database, imageHostingService } = useContext(AppContext)
const { publicKey } = useContext(UserContext)
const { relayPool, lastConfirmationtId } = useContext(RelayPoolContext)
const { t } = useTranslation('common')
// state
const [showNotification, setShowNotification] = useState<undefined | string>()
const [uploadingFile, setUploadingFile] = useState<boolean>(false)
const [content, setContent] = useState<string>('')
const [contentWarning, setContentWarning] = useState<boolean>(false)
const [userSuggestions, setUserSuggestions] = useState<User[]>([])
@ -64,6 +76,32 @@ 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 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)
})
} else {
setUploadingFile(false)
}
} else {
setUploadingFile(false)
}
})
}
const onPressSend: () => void = () => {
if (database && publicKey) {
setIsSending(true)
@ -178,15 +216,22 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
// FIXME: can't find this color
<View style={{ backgroundColor: '#001C37' }}>
<View style={styles.contentWarning}>
<Text>{t('sendPage.contentWarning')}</Text>
<Text style={styles.contentWarningText}>{t('sendPage.contentWarning')}</Text>
<Switch value={contentWarning} onValueChange={setContentWarning} />
<IconButton
icon='image-outline'
size={25}
style={styles.imageButton}
onPress={onUploadImage}
disabled={uploadingFile}
/>
</View>
<View style={styles.send}>
<Button
mode='contained'
onPress={onPressSend}
disabled={route.params?.type !== 'repost' && (!content || content === '')}
loading={isSending}
loading={isSending || uploadingFile}
>
{t('sendPage.send')}
</Button>
@ -194,17 +239,41 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
</View>
)}
</View>
{showNotification && (
<Snackbar
style={styles.snackbar}
visible={showNotification !== undefined}
duration={Snackbar.DURATION_SHORT}
onIconPress={() => setShowNotification(undefined)}
onDismiss={() => setShowNotification(undefined)}
>
{t(`sendPage.${showNotification}`, {
uri: imageHostingServices[imageHostingService].donation,
})}
</Snackbar>
)}
</>
)
}
const styles = StyleSheet.create({
contentWarningText: {
marginTop: 3,
},
snackbar: {
margin: 16,
bottom: 100,
},
textInputContainer: {
flex: 1,
},
textInput: {
paddingBottom: 0,
},
imageButton: {
marginBottom: -13,
marginTop: -8,
},
noteCard: {
flexDirection: 'column-reverse',
paddingLeft: 16,

View File

@ -19,6 +19,7 @@
"@react-navigation/stack": "^6.3.11",
"@scure/base": "^1.1.1",
"assert": "^2.0.0",
"axios": "^1.2.6",
"bip-schnorr": "^0.6.6",
"buffer": "^6.0.3",
"cipher-base": "https://github.com/KoalaSat/cipher-base",
@ -37,7 +38,9 @@
"react-native": "0.70.6",
"react-native-action-button": "^2.8.5",
"react-native-bidirectional-infinite-scroll": "^0.3.3",
"react-native-blob-util": "^0.17.1",
"react-native-gesture-handler": "^2.8.0",
"react-native-image-picker": "^5.0.1",
"react-native-multithreading": "^1.1.1",
"react-native-pager-view": "^6.1.2",
"react-native-paper": "^5.1.3",

View File

@ -2464,6 +2464,15 @@ axios@^1.2.2:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axios@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.6.tgz#eacb6d065baa11bad5959e7ffa0cb6745c65f392"
integrity sha512-rC/7F08XxZwjMV4iuWv+JpD3E0Ksqg9nac4IIg6RwNuF0JTeWoCo/mBNG54+tNhhI11G3/VDRbdDQTs9hGp4pQ==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-core@^7.0.0-bridge.0:
version "7.0.0-bridge.0"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
@ -2596,6 +2605,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-64@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
base-x@^3.0.2:
version "3.0.9"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
@ -4487,7 +4501,7 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
glob@^7.1.3, glob@^7.1.4:
glob@^7.1.3, glob@^7.1.4, glob@^7.2.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -7152,6 +7166,14 @@ react-native-bidirectional-infinite-scroll@^0.3.3:
resolved "https://registry.yarnpkg.com/react-native-bidirectional-infinite-scroll/-/react-native-bidirectional-infinite-scroll-0.3.3.tgz#31e83e30514be2eaaa889b97d01149c8a08576ec"
integrity sha512-zxYJDjrxTkGqg83WH3fSdufg79XZ7xDDn9HdHlKo9avAcz92Rf28/ivDeUM2aOUmmboqJK8BqtVByT6cF/taYg==
react-native-blob-util@^0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/react-native-blob-util/-/react-native-blob-util-0.17.1.tgz#d9d6caaa79cd44622ff68c566ab9b448c97003b6"
integrity sha512-sel2PvprG3Y5XK89mIuezB/ROTDkr9cz9nJXxfXil12GGZpGRsOsB+e919ZEGWV/BfPFx5AVjOE67XOJ7FNLMQ==
dependencies:
base-64 "0.1.0"
glob "^7.2.3"
react-native-codegen@^0.70.6:
version "0.70.6"
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.70.6.tgz#2ce17d1faad02ad4562345f8ee7cbe6397eda5cb"
@ -7178,6 +7200,11 @@ react-native-gradle-plugin@^0.70.3:
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8"
integrity sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A==
react-native-image-picker@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-5.0.1.tgz#c9e99217396bc82a977785e39e14afb4819e8448"
integrity sha512-+poQTHOnEGrbxJnut591XA9006svFOyfPg/i5bv+fLuwoSHh5HW0E/PVhvT8lbX0Z5C108vh3DAsnrfFFnPBGw==
react-native-multithreading@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/react-native-multithreading/-/react-native-multithreading-1.1.1.tgz#e1522ecd56115993d444a69c21bca49ca123bf4e"