Split image cropping into secondary step (#473)
* Split image cropping into secondary step * Use ImageModel and GalleryModel * Add fix for pasting image URLs * Move models to state folder * Fix things that broke after rebase * Latest -- has image display bug * Remove contentFit * Fix iOS display in gallery * Tuneup the api signatures and implement compress/resize on web * Fix await * Lint fix and remove unused function * Fix android image pathing * Fix external embed x button on android * Remove min-height from composer (no longer useful and was mispositioning the composer on android) * Fix e2e picker --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
91fadadb58
commit
2509290fdd
30 changed files with 875 additions and 833 deletions
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
|
@ -30,47 +30,42 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|||
import {cleanError} from 'lib/strings/errors'
|
||||
import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
|
||||
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
||||
import {SelectedPhotos} from './photos/SelectedPhotos'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
|
||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {Gallery} from './photos/Gallery'
|
||||
|
||||
const MAX_GRAPHEME_LENGTH = 300
|
||||
|
||||
type Props = ComposerOpts & {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const ComposePost = observer(function ComposePost({
|
||||
replyTo,
|
||||
onPost,
|
||||
onClose,
|
||||
quote: initQuote,
|
||||
}: {
|
||||
replyTo?: ComposerOpts['replyTo']
|
||||
onPost?: ComposerOpts['onPost']
|
||||
onClose: () => void
|
||||
quote?: ComposerOpts['quote']
|
||||
}) {
|
||||
}: Props) {
|
||||
const {track} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const textInput = React.useRef<TextInputRef>(null)
|
||||
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||
const [processingState, setProcessingState] = React.useState('')
|
||||
const [error, setError] = React.useState('')
|
||||
const [richtext, setRichText] = React.useState(new RichText({text: ''}))
|
||||
const graphemeLength = React.useMemo(
|
||||
() => richtext.graphemeLength,
|
||||
[richtext],
|
||||
)
|
||||
const [quote, setQuote] = React.useState<ComposerOpts['quote'] | undefined>(
|
||||
const textInput = useRef<TextInputRef>(null)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [processingState, setProcessingState] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [richtext, setRichText] = useState(new RichText({text: ''}))
|
||||
const graphemeLength = useMemo(() => richtext.graphemeLength, [richtext])
|
||||
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
||||
initQuote,
|
||||
)
|
||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||
const [suggestedLinks, setSuggestedLinks] = React.useState<Set<string>>(
|
||||
new Set(),
|
||||
)
|
||||
const [selectedPhotos, setSelectedPhotos] = React.useState<string[]>([])
|
||||
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
|
||||
const gallery = useMemo(() => new GalleryModel(store), [store])
|
||||
|
||||
const autocompleteView = React.useMemo<UserAutocompleteModel>(
|
||||
const autocompleteView = useMemo<UserAutocompleteModel>(
|
||||
() => new UserAutocompleteModel(store),
|
||||
[store],
|
||||
)
|
||||
|
@ -82,17 +77,17 @@ export const ComposePost = observer(function ComposePost({
|
|||
// is focused during unmount, an exception will throw (seems that a blur method isnt implemented)
|
||||
// manually blurring before closing gets around that
|
||||
// -prf
|
||||
const hackfixOnClose = React.useCallback(() => {
|
||||
const hackfixOnClose = useCallback(() => {
|
||||
textInput.current?.blur()
|
||||
onClose()
|
||||
}, [textInput, onClose])
|
||||
|
||||
// initial setup
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
autocompleteView.setup()
|
||||
}, [autocompleteView])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
// HACK
|
||||
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
|
||||
// -prf
|
||||
|
@ -109,60 +104,51 @@ export const ComposePost = observer(function ComposePost({
|
|||
}
|
||||
}, [])
|
||||
|
||||
const onPressContainer = React.useCallback(() => {
|
||||
const onPressContainer = useCallback(() => {
|
||||
textInput.current?.focus()
|
||||
}, [textInput])
|
||||
|
||||
const onSelectPhotos = React.useCallback(
|
||||
(photos: string[]) => {
|
||||
track('Composer:SelectedPhotos')
|
||||
setSelectedPhotos(photos)
|
||||
},
|
||||
[track, setSelectedPhotos],
|
||||
)
|
||||
|
||||
const onPressAddLinkCard = React.useCallback(
|
||||
const onPressAddLinkCard = useCallback(
|
||||
(uri: string) => {
|
||||
setExtLink({uri, isLoading: true})
|
||||
},
|
||||
[setExtLink],
|
||||
)
|
||||
|
||||
const onPhotoPasted = React.useCallback(
|
||||
const onPhotoPasted = useCallback(
|
||||
async (uri: string) => {
|
||||
if (selectedPhotos.length >= 4) {
|
||||
return
|
||||
}
|
||||
onSelectPhotos([...selectedPhotos, uri])
|
||||
track('Composer:PastedPhotos')
|
||||
gallery.paste(uri)
|
||||
},
|
||||
[selectedPhotos, onSelectPhotos],
|
||||
[gallery, track],
|
||||
)
|
||||
|
||||
const onPressPublish = React.useCallback(async () => {
|
||||
if (isProcessing) {
|
||||
return
|
||||
}
|
||||
if (richtext.graphemeLength > MAX_GRAPHEME_LENGTH) {
|
||||
const onPressPublish = useCallback(async () => {
|
||||
if (isProcessing || richtext.graphemeLength > MAX_GRAPHEME_LENGTH) {
|
||||
return
|
||||
}
|
||||
|
||||
setError('')
|
||||
if (richtext.text.trim().length === 0 && selectedPhotos.length === 0) {
|
||||
|
||||
if (richtext.text.trim().length === 0 && gallery.isEmpty) {
|
||||
setError('Did you want to say anything?')
|
||||
return false
|
||||
}
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
await apilib.post(store, {
|
||||
rawText: richtext.text,
|
||||
replyTo: replyTo?.uri,
|
||||
images: selectedPhotos,
|
||||
images: gallery.paths,
|
||||
quote: quote,
|
||||
extLink: extLink,
|
||||
onStateChange: setProcessingState,
|
||||
knownHandles: autocompleteView.knownHandles,
|
||||
})
|
||||
track('Create Post', {
|
||||
imageCount: selectedPhotos.length,
|
||||
imageCount: gallery.size,
|
||||
})
|
||||
} catch (e: any) {
|
||||
if (extLink) {
|
||||
|
@ -191,34 +177,33 @@ export const ComposePost = observer(function ComposePost({
|
|||
hackfixOnClose,
|
||||
onPost,
|
||||
quote,
|
||||
selectedPhotos,
|
||||
setExtLink,
|
||||
store,
|
||||
track,
|
||||
gallery,
|
||||
])
|
||||
|
||||
const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH
|
||||
|
||||
const selectTextInputPlaceholder = replyTo
|
||||
? 'Write your reply'
|
||||
: selectedPhotos.length !== 0
|
||||
: gallery.isEmpty
|
||||
? 'Write a comment'
|
||||
: "What's up?"
|
||||
|
||||
const canSelectImages = gallery.size <= 4
|
||||
const viewStyles = {
|
||||
paddingBottom: Platform.OS === 'android' ? insets.bottom : 0,
|
||||
paddingTop: Platform.OS === 'android' ? insets.top : 15,
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
testID="composePostView"
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.outer}>
|
||||
<TouchableWithoutFeedback onPressIn={onPressContainer}>
|
||||
<View
|
||||
style={[
|
||||
s.flex1,
|
||||
{
|
||||
paddingBottom: Platform.OS === 'android' ? insets.bottom : 0,
|
||||
paddingTop: Platform.OS === 'android' ? insets.top : 15,
|
||||
},
|
||||
]}>
|
||||
<View style={[s.flex1, viewStyles]}>
|
||||
<View style={styles.topbar}>
|
||||
<TouchableOpacity
|
||||
testID="composerCancelButton"
|
||||
|
@ -301,11 +286,8 @@ export const ComposePost = observer(function ComposePost({
|
|||
/>
|
||||
</View>
|
||||
|
||||
<SelectedPhotos
|
||||
selectedPhotos={selectedPhotos}
|
||||
onSelectPhotos={onSelectPhotos}
|
||||
/>
|
||||
{selectedPhotos.length === 0 && extLink && (
|
||||
<Gallery gallery={gallery} />
|
||||
{gallery.isEmpty && extLink && (
|
||||
<ExternalEmbed
|
||||
link={extLink}
|
||||
onRemove={() => setExtLink(undefined)}
|
||||
|
@ -317,9 +299,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
</View>
|
||||
) : undefined}
|
||||
</ScrollView>
|
||||
{!extLink &&
|
||||
selectedPhotos.length === 0 &&
|
||||
suggestedLinks.size > 0 ? (
|
||||
{!extLink && suggestedLinks.size > 0 ? (
|
||||
<View style={s.mb5}>
|
||||
{Array.from(suggestedLinks).map(url => (
|
||||
<TouchableOpacity
|
||||
|
@ -335,16 +315,12 @@ export const ComposePost = observer(function ComposePost({
|
|||
</View>
|
||||
) : null}
|
||||
<View style={[pal.border, styles.bottomBar]}>
|
||||
<SelectPhotoBtn
|
||||
enabled={selectedPhotos.length < 4}
|
||||
selectedPhotos={selectedPhotos}
|
||||
onSelectPhotos={setSelectedPhotos}
|
||||
/>
|
||||
<OpenCameraBtn
|
||||
enabled={selectedPhotos.length < 4}
|
||||
selectedPhotos={selectedPhotos}
|
||||
onSelectPhotos={setSelectedPhotos}
|
||||
/>
|
||||
{canSelectImages ? (
|
||||
<>
|
||||
<SelectPhotoBtn gallery={gallery} />
|
||||
<OpenCameraBtn gallery={gallery} />
|
||||
</>
|
||||
) : null}
|
||||
<View style={s.flex1} />
|
||||
<CharProgress count={graphemeLength} />
|
||||
</View>
|
||||
|
|
|
@ -2,11 +2,10 @@ import React from 'react'
|
|||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {BlurView} from '../util/BlurView'
|
||||
import {AutoSizedImage} from '../util/images/AutoSizedImage'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {s} from 'lib/styles'
|
||||
|
@ -61,11 +60,9 @@ export const ExternalEmbed = ({
|
|||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableWithoutFeedback onPress={onRemove}>
|
||||
<BlurView style={styles.removeBtn} blurType="dark">
|
||||
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />
|
||||
</BlurView>
|
||||
</TouchableWithoutFeedback>
|
||||
<TouchableOpacity style={styles.removeBtn} onPress={onRemove}>
|
||||
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -92,6 +89,7 @@ const styles = StyleSheet.create({
|
|||
right: 10,
|
||||
width: 36,
|
||||
height: 36,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
|
130
src/view/com/composer/photos/Gallery.tsx
Normal file
130
src/view/com/composer/photos/Gallery.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {colors} from 'lib/styles'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {ImageModel} from 'state/models/media/image'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
interface Props {
|
||||
gallery: GalleryModel
|
||||
}
|
||||
|
||||
export const Gallery = observer(function ({gallery}: Props) {
|
||||
const getImageStyle = useCallback(() => {
|
||||
switch (gallery.size) {
|
||||
case 1:
|
||||
return styles.image250
|
||||
case 2:
|
||||
return styles.image175
|
||||
default:
|
||||
return styles.image85
|
||||
}
|
||||
}, [gallery])
|
||||
|
||||
const imageStyle = getImageStyle()
|
||||
const handleRemovePhoto = useCallback(
|
||||
(image: ImageModel) => {
|
||||
gallery.remove(image)
|
||||
},
|
||||
[gallery],
|
||||
)
|
||||
|
||||
const handleEditPhoto = useCallback(
|
||||
(image: ImageModel) => {
|
||||
gallery.crop(image)
|
||||
},
|
||||
[gallery],
|
||||
)
|
||||
|
||||
return !gallery.isEmpty ? (
|
||||
<View testID="selectedPhotosView" style={styles.gallery}>
|
||||
{gallery.images.map(image =>
|
||||
image.compressed !== undefined ? (
|
||||
<View
|
||||
key={`selected-image-${image.path}`}
|
||||
style={[styles.imageContainer, imageStyle]}>
|
||||
<View style={styles.imageControls}>
|
||||
<TouchableOpacity
|
||||
testID="cropPhotoButton"
|
||||
onPress={() => {
|
||||
handleEditPhoto(image)
|
||||
}}
|
||||
style={styles.imageControl}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={{color: colors.white}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="removePhotoButton"
|
||||
onPress={() => handleRemovePhoto(image)}
|
||||
style={styles.imageControl}>
|
||||
<FontAwesomeIcon
|
||||
icon="xmark"
|
||||
size={16}
|
||||
style={{color: colors.white}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Image
|
||||
testID="selectedPhotoImage"
|
||||
style={[styles.image, imageStyle]}
|
||||
source={{
|
||||
uri: image.compressed.path,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null,
|
||||
)}
|
||||
</View>
|
||||
) : null
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gallery: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 16,
|
||||
},
|
||||
imageContainer: {
|
||||
margin: 2,
|
||||
},
|
||||
image: {
|
||||
resizeMode: 'cover',
|
||||
borderRadius: 8,
|
||||
},
|
||||
image250: {
|
||||
width: 250,
|
||||
height: 250,
|
||||
},
|
||||
image175: {
|
||||
width: 175,
|
||||
height: 175,
|
||||
},
|
||||
image85: {
|
||||
width: 85,
|
||||
height: 85,
|
||||
},
|
||||
imageControls: {
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 4,
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 1,
|
||||
},
|
||||
imageControl: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderWidth: 0.5,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
|
@ -10,62 +10,44 @@ import {useStores} from 'state/index'
|
|||
import {s} from 'lib/styles'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {openCamera} from 'lib/media/picker'
|
||||
import {compressIfNeeded} from 'lib/media/manip'
|
||||
import {useCameraPermission} from 'lib/hooks/usePermissions'
|
||||
import {
|
||||
POST_IMG_MAX_WIDTH,
|
||||
POST_IMG_MAX_HEIGHT,
|
||||
POST_IMG_MAX_SIZE,
|
||||
} from 'lib/constants'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
|
||||
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
||||
|
||||
export function OpenCameraBtn({
|
||||
enabled,
|
||||
selectedPhotos,
|
||||
onSelectPhotos,
|
||||
}: {
|
||||
enabled: boolean
|
||||
selectedPhotos: string[]
|
||||
onSelectPhotos: (v: string[]) => void
|
||||
}) {
|
||||
type Props = {
|
||||
gallery: GalleryModel
|
||||
}
|
||||
|
||||
export function OpenCameraBtn({gallery}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const store = useStores()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
|
||||
const onPressTakePicture = React.useCallback(async () => {
|
||||
const onPressTakePicture = useCallback(async () => {
|
||||
track('Composer:CameraOpened')
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!(await requestCameraAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
const cameraRes = await openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
width: POST_IMG_MAX_WIDTH,
|
||||
height: POST_IMG_MAX_HEIGHT,
|
||||
|
||||
const img = await openCamera(store, {
|
||||
width: POST_IMG_MAX.width,
|
||||
height: POST_IMG_MAX.height,
|
||||
freeStyleCropEnabled: true,
|
||||
})
|
||||
const img = await compressIfNeeded(cameraRes, POST_IMG_MAX_SIZE)
|
||||
onSelectPhotos([...selectedPhotos, img.path])
|
||||
|
||||
gallery.add(img)
|
||||
} catch (err: any) {
|
||||
// ignore
|
||||
store.log.warn('Error using camera', err)
|
||||
}
|
||||
}, [
|
||||
track,
|
||||
store,
|
||||
onSelectPhotos,
|
||||
selectedPhotos,
|
||||
enabled,
|
||||
requestCameraAccessIfNeeded,
|
||||
])
|
||||
}, [gallery, track, store, requestCameraAccessIfNeeded])
|
||||
|
||||
if (isDesktopWeb) {
|
||||
return <></>
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -76,11 +58,7 @@ export function OpenCameraBtn({
|
|||
hitSlop={HITSLOP}>
|
||||
<FontAwesomeIcon
|
||||
icon="camera"
|
||||
style={
|
||||
(enabled
|
||||
? pal.link
|
||||
: [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
|
||||
}
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
|
3
src/view/com/composer/photos/OpenCameraBtn.web.tsx
Normal file
3
src/view/com/composer/photos/OpenCameraBtn.web.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function OpenCameraBtn() {
|
||||
return null
|
||||
}
|
|
@ -1,86 +1,36 @@
|
|||
import React from 'react'
|
||||
import {Platform, TouchableOpacity} from 'react-native'
|
||||
import React, {useCallback} from 'react'
|
||||
import {TouchableOpacity} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {useStores} from 'state/index'
|
||||
import {s} from 'lib/styles'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {openPicker, cropAndCompressFlow, pickImagesFlow} from 'lib/media/picker'
|
||||
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
|
||||
import {
|
||||
POST_IMG_MAX_WIDTH,
|
||||
POST_IMG_MAX_HEIGHT,
|
||||
POST_IMG_MAX_SIZE,
|
||||
} from 'lib/constants'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
|
||||
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
||||
|
||||
export function SelectPhotoBtn({
|
||||
enabled,
|
||||
selectedPhotos,
|
||||
onSelectPhotos,
|
||||
}: {
|
||||
enabled: boolean
|
||||
selectedPhotos: string[]
|
||||
onSelectPhotos: (v: string[]) => void
|
||||
}) {
|
||||
type Props = {
|
||||
gallery: GalleryModel
|
||||
}
|
||||
|
||||
export function SelectPhotoBtn({gallery}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const store = useStores()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
|
||||
const onPressSelectPhotos = React.useCallback(async () => {
|
||||
const onPressSelectPhotos = useCallback(async () => {
|
||||
track('Composer:GalleryOpened')
|
||||
if (!enabled) {
|
||||
|
||||
if (!isDesktopWeb && !(await requestPhotoAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
if (isDesktopWeb) {
|
||||
const images = await pickImagesFlow(
|
||||
store,
|
||||
4 - selectedPhotos.length,
|
||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||
POST_IMG_MAX_SIZE,
|
||||
)
|
||||
onSelectPhotos([...selectedPhotos, ...images])
|
||||
} else {
|
||||
if (!(await requestPhotoAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
const items = await openPicker(store, {
|
||||
multiple: true,
|
||||
maxFiles: 4 - selectedPhotos.length,
|
||||
mediaType: 'photo',
|
||||
})
|
||||
const result = []
|
||||
for (const image of items) {
|
||||
if (Platform.OS === 'android') {
|
||||
result.push(image.path)
|
||||
continue
|
||||
}
|
||||
result.push(
|
||||
await cropAndCompressFlow(
|
||||
store,
|
||||
image.path,
|
||||
image,
|
||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||
POST_IMG_MAX_SIZE,
|
||||
),
|
||||
)
|
||||
}
|
||||
onSelectPhotos([...selectedPhotos, ...result])
|
||||
}
|
||||
}, [
|
||||
track,
|
||||
store,
|
||||
onSelectPhotos,
|
||||
selectedPhotos,
|
||||
enabled,
|
||||
requestPhotoAccessIfNeeded,
|
||||
])
|
||||
|
||||
gallery.pick()
|
||||
}, [track, gallery, requestPhotoAccessIfNeeded])
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
@ -90,11 +40,7 @@ export function SelectPhotoBtn({
|
|||
hitSlop={HITSLOP}>
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'image']}
|
||||
style={
|
||||
(enabled
|
||||
? pal.link
|
||||
: [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
|
||||
}
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Image} from 'expo-image'
|
||||
import {colors} from 'lib/styles'
|
||||
|
||||
export const SelectedPhotos = ({
|
||||
selectedPhotos,
|
||||
onSelectPhotos,
|
||||
}: {
|
||||
selectedPhotos: string[]
|
||||
onSelectPhotos: (v: string[]) => void
|
||||
}) => {
|
||||
const imageStyle =
|
||||
selectedPhotos.length === 1
|
||||
? styles.image250
|
||||
: selectedPhotos.length === 2
|
||||
? styles.image175
|
||||
: styles.image85
|
||||
|
||||
const handleRemovePhoto = useCallback(
|
||||
item => {
|
||||
onSelectPhotos(selectedPhotos.filter(filterItem => filterItem !== item))
|
||||
},
|
||||
[selectedPhotos, onSelectPhotos],
|
||||
)
|
||||
|
||||
return selectedPhotos.length !== 0 ? (
|
||||
<View testID="selectedPhotosView" style={styles.gallery}>
|
||||
{selectedPhotos.length !== 0 &&
|
||||
selectedPhotos.map((item, index) => (
|
||||
<View
|
||||
key={`selected-image-${index}`}
|
||||
style={[styles.imageContainer, imageStyle]}>
|
||||
<TouchableOpacity
|
||||
testID="removePhotoButton"
|
||||
onPress={() => handleRemovePhoto(item)}
|
||||
style={styles.removePhotoButton}>
|
||||
<FontAwesomeIcon
|
||||
icon="xmark"
|
||||
size={16}
|
||||
style={{color: colors.white}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Image
|
||||
testID="selectedPhotoImage"
|
||||
style={[styles.image, imageStyle]}
|
||||
source={{uri: item}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gallery: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 16,
|
||||
},
|
||||
imageContainer: {
|
||||
margin: 2,
|
||||
},
|
||||
image: {
|
||||
resizeMode: 'cover',
|
||||
borderRadius: 8,
|
||||
},
|
||||
image250: {
|
||||
width: 250,
|
||||
height: 250,
|
||||
},
|
||||
image175: {
|
||||
width: 175,
|
||||
height: 175,
|
||||
},
|
||||
image85: {
|
||||
width: 85,
|
||||
height: 85,
|
||||
},
|
||||
removePhotoButton: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.black,
|
||||
zIndex: 1,
|
||||
borderColor: colors.gray4,
|
||||
borderWidth: 0.5,
|
||||
},
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, {forwardRef, useCallback, useEffect, useRef, useMemo} from 'react'
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
|
@ -14,18 +14,13 @@ import isEqual from 'lodash.isequal'
|
|||
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
|
||||
import {Autocomplete} from './mobile/Autocomplete'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {getImageDim} from 'lib/media/manip'
|
||||
import {cropAndCompressFlow} from 'lib/media/picker'
|
||||
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
||||
import {
|
||||
POST_IMG_MAX_WIDTH,
|
||||
POST_IMG_MAX_HEIGHT,
|
||||
POST_IMG_MAX_SIZE,
|
||||
} from 'lib/constants'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {isUriImage} from 'lib/media/util'
|
||||
import {downloadAndResize} from 'lib/media/manip'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
|
||||
export interface TextInputRef {
|
||||
focus: () => void
|
||||
|
@ -48,7 +43,7 @@ interface Selection {
|
|||
end: number
|
||||
}
|
||||
|
||||
export const TextInput = React.forwardRef(
|
||||
export const TextInput = forwardRef(
|
||||
(
|
||||
{
|
||||
richtext,
|
||||
|
@ -63,9 +58,8 @@ export const TextInput = React.forwardRef(
|
|||
ref,
|
||||
) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const textInput = React.useRef<PasteInputRef>(null)
|
||||
const textInputSelection = React.useRef<Selection>({start: 0, end: 0})
|
||||
const textInput = useRef<PasteInputRef>(null)
|
||||
const textInputSelection = useRef<Selection>({start: 0, end: 0})
|
||||
const theme = useTheme()
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
|
@ -73,7 +67,7 @@ export const TextInput = React.forwardRef(
|
|||
blur: () => textInput.current?.blur(),
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
// HACK
|
||||
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
|
||||
// -prf
|
||||
|
@ -90,8 +84,8 @@ export const TextInput = React.forwardRef(
|
|||
}
|
||||
}, [])
|
||||
|
||||
const onChangeText = React.useCallback(
|
||||
(newText: string) => {
|
||||
const onChangeText = useCallback(
|
||||
async (newText: string) => {
|
||||
const newRt = new RichText({text: newText})
|
||||
newRt.detectFacetsWithoutResolution()
|
||||
setRichText(newRt)
|
||||
|
@ -108,50 +102,62 @@ export const TextInput = React.forwardRef(
|
|||
}
|
||||
|
||||
const set: Set<string> = new Set()
|
||||
|
||||
if (newRt.facets) {
|
||||
for (const facet of newRt.facets) {
|
||||
for (const feature of facet.features) {
|
||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||
set.add(feature.uri)
|
||||
if (isUriImage(feature.uri)) {
|
||||
const res = await downloadAndResize({
|
||||
uri: feature.uri,
|
||||
width: POST_IMG_MAX.width,
|
||||
height: POST_IMG_MAX.height,
|
||||
mode: 'contain',
|
||||
maxSize: POST_IMG_MAX.size,
|
||||
timeout: 15e3,
|
||||
})
|
||||
|
||||
if (res !== undefined) {
|
||||
onPhotoPasted(res.path)
|
||||
}
|
||||
} else {
|
||||
set.add(feature.uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEqual(set, suggestedLinks)) {
|
||||
onSuggestedLinksChanged(set)
|
||||
}
|
||||
},
|
||||
[setRichText, autocompleteView, suggestedLinks, onSuggestedLinksChanged],
|
||||
[
|
||||
setRichText,
|
||||
autocompleteView,
|
||||
suggestedLinks,
|
||||
onSuggestedLinksChanged,
|
||||
onPhotoPasted,
|
||||
],
|
||||
)
|
||||
|
||||
const onPaste = React.useCallback(
|
||||
const onPaste = useCallback(
|
||||
async (err: string | undefined, files: PastedFile[]) => {
|
||||
if (err) {
|
||||
return onError(cleanError(err))
|
||||
}
|
||||
|
||||
const uris = files.map(f => f.uri)
|
||||
const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
|
||||
if (imgUri) {
|
||||
let imgDim
|
||||
try {
|
||||
imgDim = await getImageDim(imgUri)
|
||||
} catch (e) {
|
||||
imgDim = {width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT}
|
||||
}
|
||||
const finalImgPath = await cropAndCompressFlow(
|
||||
store,
|
||||
imgUri,
|
||||
imgDim,
|
||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||
POST_IMG_MAX_SIZE,
|
||||
)
|
||||
onPhotoPasted(finalImgPath)
|
||||
const uri = uris.find(isUriImage)
|
||||
|
||||
if (uri) {
|
||||
onPhotoPasted(uri)
|
||||
}
|
||||
},
|
||||
[store, onError, onPhotoPasted],
|
||||
[onError, onPhotoPasted],
|
||||
)
|
||||
|
||||
const onSelectionChange = React.useCallback(
|
||||
const onSelectionChange = useCallback(
|
||||
(evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
||||
// NOTE we track the input selection using a ref to avoid excessive renders -prf
|
||||
textInputSelection.current = evt.nativeEvent.selection
|
||||
|
@ -159,7 +165,7 @@ export const TextInput = React.forwardRef(
|
|||
[textInputSelection],
|
||||
)
|
||||
|
||||
const onSelectAutocompleteItem = React.useCallback(
|
||||
const onSelectAutocompleteItem = useCallback(
|
||||
(item: string) => {
|
||||
onChangeText(
|
||||
insertMentionAt(
|
||||
|
@ -173,23 +179,19 @@ export const TextInput = React.forwardRef(
|
|||
[onChangeText, richtext, autocompleteView],
|
||||
)
|
||||
|
||||
const textDecorated = React.useMemo(() => {
|
||||
const textDecorated = useMemo(() => {
|
||||
let i = 0
|
||||
return Array.from(richtext.segments()).map(segment => {
|
||||
if (!segment.facet) {
|
||||
return (
|
||||
<Text key={i++} style={[pal.text, styles.textInputFormatting]}>
|
||||
{segment.text}
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Text key={i++} style={[pal.link, styles.textInputFormatting]}>
|
||||
{segment.text}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(richtext.segments()).map(segment => (
|
||||
<Text
|
||||
key={i++}
|
||||
style={[
|
||||
!segment.facet ? pal.text : pal.link,
|
||||
styles.textInputFormatting,
|
||||
]}>
|
||||
{segment.text}
|
||||
</Text>
|
||||
))
|
||||
}, [richtext, pal.link, pal.text])
|
||||
|
||||
return (
|
||||
|
@ -223,7 +225,6 @@ const styles = StyleSheet.create({
|
|||
textInput: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
minHeight: 80,
|
||||
padding: 5,
|
||||
paddingBottom: 20,
|
||||
marginLeft: 8,
|
||||
|
|
|
@ -12,6 +12,7 @@ import isEqual from 'lodash.isequal'
|
|||
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
|
||||
import {createSuggestion} from './web/Autocomplete'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {isUriImage, blobToDataUri} from 'lib/media/util'
|
||||
|
||||
export interface TextInputRef {
|
||||
focus: () => void
|
||||
|
@ -37,7 +38,7 @@ export const TextInput = React.forwardRef(
|
|||
suggestedLinks,
|
||||
autocompleteView,
|
||||
setRichText,
|
||||
// onPhotoPasted, TODO
|
||||
onPhotoPasted,
|
||||
onSuggestedLinksChanged,
|
||||
}: // onError, TODO
|
||||
TextInputProps,
|
||||
|
@ -72,6 +73,15 @@ export const TextInput = React.forwardRef(
|
|||
attributes: {
|
||||
class: modeClass,
|
||||
},
|
||||
handlePaste: (_, event) => {
|
||||
const items = event.clipboardData?.items
|
||||
|
||||
if (items === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
getImageFromUri(items, onPhotoPasted)
|
||||
},
|
||||
},
|
||||
content: richtext.text.toString(),
|
||||
autofocus: true,
|
||||
|
@ -147,3 +157,33 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 10,
|
||||
},
|
||||
})
|
||||
|
||||
function getImageFromUri(
|
||||
items: DataTransferItemList,
|
||||
callback: (uri: string) => void,
|
||||
) {
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index]
|
||||
const {kind, type} = item
|
||||
|
||||
if (type === 'text/plain') {
|
||||
item.getAsString(async itemString => {
|
||||
if (isUriImage(itemString)) {
|
||||
const response = await fetch(itemString)
|
||||
const blob = await response.blob()
|
||||
blobToDataUri(blob).then(callback, err => console.error(err))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (kind === 'file') {
|
||||
const file = item.getAsFile()
|
||||
|
||||
if (file instanceof Blob) {
|
||||
blobToDataUri(new Blob([file], {type: item.type})).then(callback, err =>
|
||||
console.error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {getPostAsQuote} from 'lib/link-meta/bsky'
|
|||
import {downloadAndResize} from 'lib/media/manip'
|
||||
import {isBskyPostUrl} from 'lib/strings/url-helpers'
|
||||
import {ComposerOpts} from 'state/models/ui/shell'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
|
||||
export function useExternalLinkFetch({
|
||||
setQuote,
|
||||
|
@ -55,13 +56,12 @@ export function useExternalLinkFetch({
|
|||
return cleanup
|
||||
}
|
||||
if (extLink.isLoading && extLink.meta?.image && !extLink.localThumb) {
|
||||
console.log('attempting download')
|
||||
downloadAndResize({
|
||||
uri: extLink.meta.image,
|
||||
width: 2000,
|
||||
height: 2000,
|
||||
width: POST_IMG_MAX.width,
|
||||
height: POST_IMG_MAX.height,
|
||||
mode: 'contain',
|
||||
maxSize: 1000000,
|
||||
maxSize: POST_IMG_MAX.size,
|
||||
timeout: 15e3,
|
||||
})
|
||||
.catch(() => undefined)
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {PickedMedia} from '../../../lib/media/picker'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {useStores} from 'state/index'
|
||||
|
@ -53,15 +53,15 @@ export function Component({
|
|||
profileView.avatar,
|
||||
)
|
||||
const [newUserBanner, setNewUserBanner] = useState<
|
||||
PickedMedia | undefined | null
|
||||
RNImage | undefined | null
|
||||
>()
|
||||
const [newUserAvatar, setNewUserAvatar] = useState<
|
||||
PickedMedia | undefined | null
|
||||
RNImage | undefined | null
|
||||
>()
|
||||
const onPressCancel = () => {
|
||||
store.shell.closeModal()
|
||||
}
|
||||
const onSelectNewAvatar = async (img: PickedMedia | null) => {
|
||||
const onSelectNewAvatar = async (img: RNImage | null) => {
|
||||
track('EditProfile:AvatarSelected')
|
||||
try {
|
||||
// if img is null, user selected "remove avatar"
|
||||
|
@ -71,13 +71,13 @@ export function Component({
|
|||
return
|
||||
}
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserAvatar({mediaType: 'photo', ...finalImg})
|
||||
setNewUserAvatar(finalImg)
|
||||
setUserAvatar(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setError(cleanError(e))
|
||||
}
|
||||
}
|
||||
const onSelectNewBanner = async (img: PickedMedia | null) => {
|
||||
const onSelectNewBanner = async (img: RNImage | null) => {
|
||||
if (!img) {
|
||||
setNewUserBanner(null)
|
||||
setUserBanner(null)
|
||||
|
@ -86,7 +86,7 @@ export function Component({
|
|||
track('EditProfile:BannerSelected')
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserBanner({mediaType: 'photo', ...finalImg})
|
||||
setNewUserBanner(finalImg)
|
||||
setUserBanner(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setError(cleanError(e))
|
||||
|
|
|
@ -4,7 +4,7 @@ import ImageEditor from 'react-avatar-editor'
|
|||
import {Slider} from '@miblanchard/react-native-slider'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {PickedMedia} from 'lib/media/types'
|
||||
import {Dimensions, Image} from 'lib/media/types'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
import {s, gradients} from 'lib/styles'
|
||||
import {useStores} from 'state/index'
|
||||
|
@ -16,11 +16,8 @@ enum AspectRatio {
|
|||
Wide = 'wide',
|
||||
Tall = 'tall',
|
||||
}
|
||||
interface Dim {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
const DIMS: Record<string, Dim> = {
|
||||
|
||||
const DIMS: Record<string, Dimensions> = {
|
||||
[AspectRatio.Square]: {width: 1000, height: 1000},
|
||||
[AspectRatio.Wide]: {width: 1000, height: 750},
|
||||
[AspectRatio.Tall]: {width: 750, height: 1000},
|
||||
|
@ -33,7 +30,7 @@ export function Component({
|
|||
onSelect,
|
||||
}: {
|
||||
uri: string
|
||||
onSelect: (img?: PickedMedia) => void
|
||||
onSelect: (img?: Image) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
@ -52,7 +49,6 @@ export function Component({
|
|||
if (canvas) {
|
||||
const dataUri = canvas.toDataURL('image/jpeg')
|
||||
onSelect({
|
||||
mediaType: 'photo',
|
||||
path: dataUri,
|
||||
mime: 'image/jpeg',
|
||||
size: getDataUriSize(dataUri),
|
||||
|
|
|
@ -4,12 +4,7 @@ import Svg, {Circle, Path} from 'react-native-svg'
|
|||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {HighPriorityImage} from 'view/com/util/images/Image'
|
||||
import {
|
||||
openCamera,
|
||||
openCropper,
|
||||
openPicker,
|
||||
PickedMedia,
|
||||
} from '../../../lib/media/picker'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
import {
|
||||
usePhotoLibraryPermission,
|
||||
useCameraPermission,
|
||||
|
@ -19,6 +14,7 @@ import {colors} from 'lib/styles'
|
|||
import {DropdownButton} from './forms/DropdownButton'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
|
||||
function DefaultAvatar({size}: {size: number}) {
|
||||
return (
|
||||
|
@ -50,7 +46,7 @@ export function UserAvatar({
|
|||
size: number
|
||||
avatar?: string | null
|
||||
hasWarning?: boolean
|
||||
onSelectNewAvatar?: (img: PickedMedia | null) => void
|
||||
onSelectNewAvatar?: (img: RNImage | null) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
@ -68,7 +64,6 @@ export function UserAvatar({
|
|||
}
|
||||
onSelectNewAvatar?.(
|
||||
await openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
cropperCircleOverlay: true,
|
||||
|
@ -84,9 +79,8 @@ export function UserAvatar({
|
|||
if (!(await requestPhotoAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
const items = await openPicker(store, {
|
||||
mediaType: 'photo',
|
||||
})
|
||||
const items = await openPicker(store)
|
||||
|
||||
onSelectNewAvatar?.(
|
||||
await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
|
|
|
@ -4,12 +4,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {Image} from 'expo-image'
|
||||
import {colors} from 'lib/styles'
|
||||
import {
|
||||
openCamera,
|
||||
openCropper,
|
||||
openPicker,
|
||||
PickedMedia,
|
||||
} from '../../../lib/media/picker'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
import {Image as TImage} from 'lib/media/types'
|
||||
import {useStores} from 'state/index'
|
||||
import {
|
||||
usePhotoLibraryPermission,
|
||||
|
@ -24,7 +20,7 @@ export function UserBanner({
|
|||
onSelectNewBanner,
|
||||
}: {
|
||||
banner?: string | null
|
||||
onSelectNewBanner?: (img: PickedMedia | null) => void
|
||||
onSelectNewBanner?: (img: TImage | null) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
@ -42,7 +38,6 @@ export function UserBanner({
|
|||
}
|
||||
onSelectNewBanner?.(
|
||||
await openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
// compressImageMaxWidth: 3000, TODO needed?
|
||||
width: 3000,
|
||||
// compressImageMaxHeight: 1000, TODO needed?
|
||||
|
@ -59,9 +54,7 @@ export function UserBanner({
|
|||
if (!(await requestPhotoAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
const items = await openPicker(store, {
|
||||
mediaType: 'photo',
|
||||
})
|
||||
const items = await openPicker(store)
|
||||
onSelectNewBanner?.(
|
||||
await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react'
|
||||
import {Dimensions} from 'lib/media/types'
|
||||
import React, {useState} from 'react'
|
||||
import {
|
||||
LayoutChangeEvent,
|
||||
StyleProp,
|
||||
|
@ -11,11 +12,6 @@ import {Image, ImageStyle} from 'expo-image'
|
|||
|
||||
export const DELAY_PRESS_IN = 500
|
||||
|
||||
interface Dim {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export type ImageLayoutGridType = 'two' | 'three' | 'four'
|
||||
|
||||
export function ImageLayoutGrid({
|
||||
|
@ -33,7 +29,7 @@ export function ImageLayoutGrid({
|
|||
onPressIn?: (index: number) => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const [containerInfo, setContainerInfo] = React.useState<Dim | undefined>()
|
||||
const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>()
|
||||
|
||||
const onLayout = (evt: LayoutChangeEvent) => {
|
||||
setContainerInfo({
|
||||
|
@ -71,7 +67,7 @@ function ImageLayoutGridInner({
|
|||
onPress?: (index: number) => void
|
||||
onLongPress?: (index: number) => void
|
||||
onPressIn?: (index: number) => void
|
||||
containerInfo: Dim
|
||||
containerInfo: Dimensions
|
||||
}) {
|
||||
const size1 = React.useMemo<ImageStyle>(() => {
|
||||
if (type === 'three') {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue