Update composer to preview external link cards (#52)

* Fetch external link metadata during compose so the user can preview and remove the embed

* Add missing mocks

* Update tests to match recent changes
This commit is contained in:
Paul Frazee 2023-01-18 18:14:46 -06:00 committed by GitHub
parent 27ee550d15
commit 6588961d2e
9 changed files with 262 additions and 64 deletions

View file

@ -16,6 +16,7 @@ import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
import {Autocomplete} from './Autocomplete'
import {ExternalEmbed} from './ExternalEmbed'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
// @ts-ignore no type definition -prf
@ -28,7 +29,9 @@ import {useStores} from '../../../state'
import * as apilib from '../../../state/lib/api'
import {ComposerOpts} from '../../../state/models/shell-ui'
import {s, colors, gradients} from '../../lib/styles'
import {detectLinkables} from '../../../lib/strings'
import {detectLinkables, extractEntities} from '../../../lib/strings'
import {getLinkMeta} from '../../../lib/link-meta'
import {downloadAndResize} from '../../../lib/images'
import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
import {PhotoCarouselPicker} from './PhotoCarouselPicker'
import {SelectedPhoto} from './SelectedPhoto'
@ -56,6 +59,10 @@ export const ComposePost = observer(function ComposePost({
const [processingState, setProcessingState] = useState('')
const [error, setError] = useState('')
const [text, setText] = useState('')
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
undefined,
)
const [attemptedExtLinks, setAttemptedExtLinks] = useState<string[]>([])
const [isSelectingPhotos, setIsSelectingPhotos] = useState(
imagesOpen || false,
)
@ -71,11 +78,61 @@ export const ComposePost = observer(function ComposePost({
[store],
)
// initial setup
useEffect(() => {
autocompleteView.setup()
localPhotos.setup()
}, [autocompleteView, localPhotos])
// external link metadata-fetch flow
useEffect(() => {
let aborted = false
const cleanup = () => {
aborted = true
}
if (!extLink) {
return cleanup
}
if (!extLink.meta) {
getLinkMeta(extLink.uri).then(meta => {
if (aborted) {
return
}
setExtLink({
uri: extLink.uri,
isLoading: !!meta.image,
meta,
})
})
return cleanup
}
if (extLink.isLoading && extLink.meta?.image && !extLink.localThumb) {
downloadAndResize({
uri: extLink.meta.image,
width: 250,
height: 250,
mode: 'contain',
maxSize: 100000,
timeout: 15e3,
})
.catch(() => undefined)
.then(localThumb => {
setExtLink({
...extLink,
isLoading: false, // done
localThumb,
})
})
return cleanup
}
if (extLink.isLoading) {
setExtLink({
...extLink,
isLoading: false, // done
})
}
}, [extLink])
useEffect(() => {
// HACK
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
@ -119,6 +176,22 @@ export const ComposePost = observer(function ComposePost({
} else {
autocompleteView.setActive(false)
}
if (!extLink && /\s$/.test(newText)) {
const ents = extractEntities(newText)
const entLink = ents
?.filter(
ent => ent.type === 'link' && !attemptedExtLinks.includes(ent.value),
)
.pop() // use last
if (entLink) {
setExtLink({
uri: entLink.value,
isLoading: true,
})
setAttemptedExtLinks([...attemptedExtLinks, entLink.value])
}
}
}
const onPressCancel = () => {
onClose()
@ -141,6 +214,7 @@ export const ComposePost = observer(function ComposePost({
store,
text,
replyTo?.uri,
extLink,
selectedPhotos,
autocompleteView.knownHandles,
setProcessingState,
@ -297,6 +371,12 @@ export const ComposePost = observer(function ComposePost({
selectedPhotos={selectedPhotos}
onSelectPhotos={onSelectPhotos}
/>
{!selectedPhotos.length && extLink && (
<ExternalEmbed
link={extLink}
onRemove={() => setExtLink(undefined)}
/>
)}
</ScrollView>
{isSelectingPhotos &&
localPhotos.photos != null &&