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:
Ollie Hsieh 2023-04-17 15:41:44 -07:00 committed by GitHub
parent 91fadadb58
commit 2509290fdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 875 additions and 833 deletions

View file

@ -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,

View file

@ -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),
)
}
}
}
}