Basic attachment done

Switch from ky to axios
This commit is contained in:
Zhiyuan Zheng 2020-12-05 01:55:53 +01:00
parent 82d9cdf702
commit b274aef31a
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
21 changed files with 284 additions and 213 deletions

View File

@ -14,6 +14,7 @@
"@react-navigation/bottom-tabs": "^5.10.6",
"@react-navigation/native": "^5.8.6",
"@reduxjs/toolkit": "^1.4.0",
"axios": "^0.21.0",
"expo": "~39.0.4",
"expo-auth-session": "~2.0.0",
"expo-av": "~8.6.0",
@ -25,7 +26,6 @@
"expo-splash-screen": "~0.6.1",
"expo-status-bar": "~1.0.2",
"i18next": "^19.8.4",
"ky": "^0.24.0",
"lodash": "^4.17.20",
"react": "16.13.1",
"react-dom": "16.13.1",

View File

@ -1,37 +1,38 @@
import axios from 'axios'
import { store, RootState } from 'src/store'
import ky from 'ky'
const client = async ({
version = 'v1',
method,
instance,
instanceUrl,
endpoint,
instanceDomain,
version = 'v1',
url,
params,
headers,
query,
body
body,
onUploadProgress
}: {
version?: 'v1' | 'v2'
method: 'get' | 'post' | 'put' | 'delete'
instance: 'local' | 'remote'
instanceUrl?: string
endpoint: string
headers?: { [key: string]: string }
query?: {
instanceDomain?: string
version?: 'v1' | 'v2'
url: string
params?: {
[key: string]: string | number | boolean
}
headers?: { [key: string]: string }
body?: FormData
onUploadProgress?: (progressEvent: any) => void
}): Promise<any> => {
const state: RootState['instances'] = store.getState().instances
const url =
instance === 'remote' ? instanceUrl || state.remote.url : state.local.url
const domain =
instance === 'remote' ? instanceDomain || state.remote.url : state.local.url
let response
// try {
response = await ky(endpoint, {
method: method,
prefixUrl: `https://${url}/api/${version}`,
searchParams: query,
return axios({
method,
baseURL: `https://${domain}/api/${version}/`,
url,
params,
headers: {
'Content-Type': 'application/json',
...headers,
@ -39,28 +40,35 @@ const client = async ({
Authorization: `Bearer ${state.local.token}`
})
},
...(body && { body: body }),
throwHttpErrors: false
...(body && { data: body }),
...(onUploadProgress && { onUploadProgress: onUploadProgress })
})
// } catch (error) {
// return Promise.reject('ky error: ' + error.json())
// }
console.log('Query: /' + endpoint)
if (response.ok) {
return Promise.resolve({
headers: response.headers,
body: await response.json()
.then(response =>
Promise.resolve({
headers: response.headers,
body: response.data
})
)
.catch(error => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error('axios error', error.response)
return Promise.reject({
headers: error.response.headers,
body: error.response.data.error
})
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error('axios error', error)
return Promise.reject()
} else {
console.error('axios error', error.message)
return Promise.reject({ body: error.message })
}
})
} else {
let errorResponse
try {
errorResponse = await response.json()
} catch (error) {
return Promise.reject({ body: 'Nothing found' })
}
console.error(response.status + ': ' + errorResponse.error)
return Promise.reject({ body: errorResponse.error })
}
}
export default client

View File

@ -27,7 +27,7 @@ const fireMutation = async ({
res = await client({
method: 'post',
instance: 'local',
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
url: `statuses/${id}/${prevState ? 'un' : ''}${type}`
}) // bug in response from Mastodon
if (!res.body[stateKey] === prevState) {

View File

@ -20,7 +20,7 @@ const fireMutation = async ({
res = await client({
method: 'post',
instance: 'local',
endpoint: `accounts/${id}/${type}`
url: `accounts/${id}/${type}`
})
if (res.body[stateKey!] === true) {
@ -35,8 +35,8 @@ const fireMutation = async ({
res = await client({
method: 'post',
instance: 'local',
endpoint: `reports`,
query: {
url: `reports`,
params: {
account_id: id!
}
})

View File

@ -10,8 +10,8 @@ const fireMutation = async ({ domain }: { domain: string }) => {
const res = await client({
method: 'post',
instance: 'local',
endpoint: `domain_blocks`,
query: {
url: `domain_blocks`,
params: {
domain: domain!
}
})

View File

@ -22,7 +22,7 @@ const fireMutation = async ({
res = await client({
method: 'post',
instance: 'local',
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
url: `statuses/${id}/${prevState ? 'un' : ''}${type}`
}) // bug in response from Mastodon
if (!res.body[stateKey] === prevState) {
@ -37,7 +37,7 @@ const fireMutation = async ({
res = await client({
method: 'delete',
instance: 'local',
endpoint: `statuses/${id}`
url: `statuses/${id}`
})
if (res.body[stateKey] === id) {

View File

@ -29,7 +29,7 @@ const fireMutation = async ({
const res = await client({
method: 'post',
instance: 'local',
endpoint: `polls/${id}/votes`,
url: `polls/${id}/votes`,
body: formData
})

View File

@ -62,8 +62,8 @@ const Login: React.FC = () => {
const res = await client({
method: 'post',
instance: 'remote',
instanceUrl: instance,
endpoint: `apps`,
instanceDomain: instance,
url: `apps`,
body: formData
})
if (res.body?.client_id.length > 0) {

View File

@ -34,11 +34,10 @@ export type PostState = {
active: boolean
total: number
options: {
'1': string
'2': string
'3': string
'4': string
[key: string]: string
'1': string | undefined
'2': string | undefined
'3': string | undefined
'4': string | undefined
}
multiple: boolean
expire:
@ -51,12 +50,8 @@ export type PostState = {
| '604800'
| string
}
attachments: {
id: string
url: string
preview_url: string
description: string
}[]
attachments: Mastodon.Attachment[]
attachmentUploadProgress: { progress: number; aspect: number } | undefined
visibility: 'public' | 'unlisted' | 'private' | 'direct'
}
@ -82,19 +77,12 @@ export type PostAction =
payload: PostState['poll']
}
| {
type: 'attachments/add'
payload: {
id: string
url: string
preview_url: string
description: string
}
type: 'attachments'
payload: PostState['attachments']
}
| {
type: 'attachments/remove'
payload: {
id: string
}
type: 'attachmentUploadProgress'
payload: PostState['attachmentUploadProgress']
}
| {
type: 'visibility'
@ -114,15 +102,16 @@ const postInitialState: PostState = {
active: false,
total: 2,
options: {
'1': '',
'2': '',
'3': '',
'4': ''
'1': undefined,
'2': undefined,
'3': undefined,
'4': undefined
},
multiple: false,
expire: '86400'
},
attachments: [],
attachmentUploadProgress: undefined,
visibility:
getLocalAccountPreferences(store.getState())[
'posting:default:visibility'
@ -140,13 +129,10 @@ const postReducer = (state: PostState, action: PostAction): PostState => {
return { ...state, emoji: action.payload }
case 'poll':
return { ...state, poll: action.payload }
case 'attachments/add':
return { ...state, attachments: state.attachments.concat(action.payload) }
case 'attachments/remove':
return {
...state,
attachments: state.attachments.filter(a => a.id !== action.payload.id)
}
case 'attachments':
return { ...state, attachments: action.payload }
case 'attachmentUploadProgress':
return { ...state, attachmentUploadProgress: action.payload }
case 'visibility':
return { ...state, visibility: action.payload }
default:
@ -186,27 +172,29 @@ const Compose: React.FC = () => {
])
} else {
const formData = new FormData()
formData.append('status', postState.text.raw)
if (postState.poll.active) {
Object.values(postState.poll.options)
.filter(e => e.length)
.forEach(e => {
formData.append('poll[options][]', e)
})
.filter(e => e?.length)
.forEach(e => formData.append('poll[options][]', e!))
formData.append('poll[expires_in]', postState.poll.expire)
formData.append('poll[multiple]', postState.poll.multiple.toString())
}
if (postState.attachments.length > 0) {
postState.attachments.forEach(attachment =>
formData.append('media_ids[]', attachment.id)
if (postState.attachments.length) {
postState.attachments.forEach(e =>
formData.append('media_ids[]', e!.id)
)
}
formData.append('visibility', postState.visibility)
client({
method: 'post',
instance: 'local',
endpoint: 'statuses',
url: 'statuses',
headers: {
'Idempotency-Key': Date.now().toString() + Math.random().toString()
},

View File

@ -8,6 +8,8 @@ import {
Text,
TextInput
} from 'react-native'
import { useSelector } from 'react-redux'
import { getLocalToken, getLocalUrl } from 'src/utils/slices/instancesSlice'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, PostState } from '../Compose'
@ -25,6 +27,8 @@ const ComposeActions: React.FC<Props> = ({
postDispatch
}) => {
const { theme } = useTheme()
const localUrl = useSelector(getLocalUrl)
const localToken = useSelector(getLocalToken)
const getVisibilityIcon = () => {
switch (postState.visibility) {
@ -50,20 +54,30 @@ const ComposeActions: React.FC<Props> = ({
<Feather
name='aperture'
size={24}
color={postState.poll.active ? theme.secondary : theme.primary}
onPress={async () =>
!postState.poll.active &&
(await addAttachments({ postState, postDispatch }))
color={
postState.poll.active || postState.attachments.length >= 4
? theme.secondary
: theme.primary
}
onPress={async () => {
if (!postState.poll.active && postState.attachments.length < 4) {
await addAttachments({ postState, postDispatch })
}
}}
/>
<Feather
name='bar-chart-2'
size={24}
color={
postState.attachments.length > 0 ? theme.secondary : theme.primary
postState.attachments.length || postState.attachmentUploadProgress
? theme.secondary
: theme.primary
}
onPress={() => {
if (postState.attachments.length === 0) {
if (
!postState.attachments.length &&
!postState.attachmentUploadProgress
) {
postDispatch({
type: 'poll',
payload: { ...postState.poll, active: !postState.poll.active }

View File

@ -1,8 +1,15 @@
import React, { Dispatch } from 'react'
import { Image, StyleSheet, View } from 'react-native'
import React, { Dispatch, useCallback } from 'react'
import {
ActivityIndicator,
FlatList,
Image,
StyleSheet,
View
} from 'react-native'
import { Feather } from '@expo/vector-icons'
import { PostAction, PostState } from '../Compose'
import { StyleConstants } from 'src/utils/styles/constants'
export interface Props {
postState: PostState
@ -10,31 +17,66 @@ export interface Props {
}
const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
const renderImage = useCallback(({ item, index }) => {
return (
<View key={index}>
<Image
style={[
styles.image,
{
width: (item.meta?.original?.aspect || 1) * 200
}
]}
source={{ uri: item!.preview_url }}
/>
<Feather
name='edit'
size={24}
color='white'
style={styles.buttonEdit}
/>
<Feather
name='trash-2'
size={24}
color='white'
style={styles.buttonRemove}
onPress={() =>
postDispatch({
type: 'attachments',
payload: postState.attachments.filter(e => e.id !== item.id)
})
}
/>
</View>
)
}, [])
const listFooter = useCallback(() => {
return postState.attachmentUploadProgress ? (
<View
style={{
width: postState.attachmentUploadProgress.aspect * 200,
height: 200,
flex: 1,
backgroundColor: 'gray',
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}}
>
<ActivityIndicator />
</View>
) : null
}, [postState.attachmentUploadProgress])
return (
<View style={styles.base}>
{postState.attachments.map((attachment, index) => (
<View key={index} style={styles.imageContainer}>
<Image
style={styles.image}
source={{ uri: attachment.preview_url }}
/>
<Feather
name='edit'
size={24}
color='white'
style={styles.buttonEdit}
/>
<Feather
name='trash-2'
size={24}
color='white'
style={styles.buttonRemove}
onPress={() =>
postDispatch({ type: 'attachments/remove', payload: attachment })
}
/>
</View>
))}
<FlatList
horizontal
data={postState.attachments}
renderItem={renderImage}
ListFooterComponent={listFooter}
/>
</View>
)
}
@ -43,13 +85,14 @@ const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row',
backgroundColor: 'lightgreen'
},
imageContainer: {
flexBasis: 100
marginRight: StyleConstants.Spacing.Global.PagePadding,
height: 200
},
image: {
flex: 1
flex: 1,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
},
buttonEdit: {
position: 'absolute',

View File

@ -73,7 +73,7 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
const textInputRef = useRef<TextInput>(null)
const listFooter = useMemo(() => {
const listFooter = () => {
return (
<>
{postState.emoji.active && (
@ -86,7 +86,8 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
</View>
)}
{postState.attachments.length > 0 && (
{(postState.attachments.length > 0 ||
postState.attachmentUploadProgress) && (
<View style={styles.attachments}>
<ComposeAttachments
postState={postState}
@ -101,11 +102,7 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
)}
</>
)
}, [
postState.emoji.active,
postState.attachments.length,
postState.poll.active
])
}
const listEmpty = useMemo(() => {
if (isFetching) {
@ -215,8 +212,7 @@ const styles = StyleSheet.create({
contentView: { flex: 1 },
attachments: {
flex: 1,
height: 100
flex: 1
},
poll: {
flex: 1,

View File

@ -1,35 +1,55 @@
import { Dispatch } from 'react'
import { ActionSheetIOS, Alert } from 'react-native'
import * as ImagePicker from 'expo-image-picker'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import { PostAction, PostState } from '../Compose'
import client from 'src/api/client'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
const uploadAttachment = async (uri: ImageInfo['uri']) => {
const filename = uri.split('/').pop()
const uploadAttachment = async ({
result,
postState,
postDispatch
}: {
result: ImageInfo
postState: PostState
postDispatch: Dispatch<PostAction>
}) => {
const filename = result.uri.split('/').pop()
const match = /\.(\w+)$/.exec(filename!)
const type = match ? `image/${match[1]}` : `image`
const formData = new FormData()
formData.append('file', { uri: uri, name: filename, type: type })
// @ts-ignore
formData.append('file', { uri: result.uri, name: filename, type: type })
return client({
client({
method: 'post',
instance: 'local',
endpoint: 'media',
body: formData
url: 'media',
body: formData,
onUploadProgress: p => {
postDispatch({
type: 'attachmentUploadProgress',
payload: {
progress: p.loaded / p.total,
aspect: result.width / result.height
}
})
}
})
.then(res => {
if (res.body.id && res.body.type !== 'unknown') {
console.log('url: ' + res.body.preview_url)
return Promise.resolve({
id: res.body.id,
url: res.body.url,
preview_url: res.body.preview_url,
description: res.body.description
.then(({ body }: { body: Mastodon.Attachment }) => {
postDispatch({
type: 'attachmentUploadProgress',
payload: undefined
})
if (body.id) {
postDispatch({
type: 'attachments',
payload: postState.attachments.concat([body])
})
return Promise.resolve()
} else {
Alert.alert('上传失败', '', [
{
@ -50,12 +70,11 @@ const uploadAttachment = async (uri: ImageInfo['uri']) => {
}
const addAttachments = async ({
postState,
postDispatch
...params
}: {
postState: PostState
postDispatch: Dispatch<PostAction>
}) => {
}): Promise<any> => {
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['从相册选取', '现照', '取消'],
@ -63,19 +82,13 @@ const addAttachments = async ({
},
async buttonIndex => {
if (buttonIndex === 0) {
let result = await ImagePicker.launchImageLibraryAsync({
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
exif: false
})
if (!result.cancelled) {
const response = await uploadAttachment(result.uri)
if (response.id) {
postDispatch({
type: 'attachments/add',
payload: response
})
}
await uploadAttachment({ result, ...params })
}
} else if (buttonIndex === 1) {
// setResult(Math.floor(Math.random() * 100) + 1)

View File

@ -4,7 +4,7 @@ export const accountFetch = async (key: string, { id }: { id: string }) => {
const res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${id}`
url: `accounts/${id}`
})
return Promise.resolve(res.body)
}

View File

@ -4,7 +4,7 @@ export const emojisFetch = async () => {
const res = await client({
method: 'get',
instance: 'local',
endpoint: 'custom_emojis'
url: 'custom_emojis'
})
return Promise.resolve(res.body)
}

View File

@ -7,8 +7,8 @@ export const instanceFetch = async (
const res = await client({
method: 'get',
instance: 'remote',
instanceUrl: instance,
endpoint: `instance`
instanceDomain: instance,
url: `instance`
})
return Promise.resolve(res.body)
}

View File

@ -4,7 +4,7 @@ export const listsFetch = async () => {
const res = await client({
method: 'get',
instance: 'local',
endpoint: 'lists'
url: 'lists'
})
return Promise.resolve(res.body)
}

View File

@ -16,8 +16,8 @@ export const searchFetch = async (
version: 'v2',
method: 'get',
instance: 'local',
endpoint: 'search',
query: { type, q: term, limit }
url: 'search',
params: { type, q: term, limit }
})
return Promise.resolve(res.body)
}

View File

@ -6,14 +6,14 @@ export const timelineFetch = async (
key: string,
{
page,
query = {},
params = {},
account,
hashtag,
list,
toot
}: {
page: string
query?: {
params?: {
[key: string]: string | number | boolean
}
account?: string
@ -31,10 +31,10 @@ export const timelineFetch = async (
if (pagination && pagination.id) {
switch (pagination.direction) {
case 'prev':
query.min_id = pagination.id
params.min_id = pagination.id
break
case 'next':
query.max_id = pagination.id
params.max_id = pagination.id
break
}
}
@ -44,18 +44,18 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: 'timelines/home',
query
url: 'timelines/home',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
case 'Local':
query.local = 'true'
params.local = 'true'
res = await client({
method: 'get',
instance: 'local',
endpoint: 'timelines/public',
query
url: 'timelines/public',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -63,8 +63,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: 'timelines/public',
query
url: 'timelines/public',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -72,8 +72,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'remote',
endpoint: 'timelines/public',
query
url: 'timelines/public',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -81,8 +81,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: 'notifications',
query
url: 'notifications',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -90,8 +90,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query: {
url: `accounts/${account}/statuses`,
params: {
pinned: 'true'
}
})
@ -99,8 +99,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query: {
url: `accounts/${account}/statuses`,
params: {
exclude_replies: 'true'
}
})
@ -111,8 +111,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query
url: `accounts/${account}/statuses`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -120,8 +120,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query: {
url: `accounts/${account}/statuses`,
params: {
only_media: 'true'
}
})
@ -131,8 +131,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `timelines/tag/${hashtag}`,
query
url: `timelines/tag/${hashtag}`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -140,8 +140,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `conversations`,
query
url: `conversations`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -149,8 +149,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `bookmarks`,
query
url: `bookmarks`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -158,8 +158,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `favourites`,
query
url: `favourites`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -167,8 +167,8 @@ export const timelineFetch = async (
res = await client({
method: 'get',
instance: 'local',
endpoint: `timelines/list/${list}`,
query
url: `timelines/list/${list}`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
@ -176,12 +176,12 @@ export const timelineFetch = async (
const current = await client({
method: 'get',
instance: 'local',
endpoint: `statuses/${toot}`
url: `statuses/${toot}`
})
const context = await client({
method: 'get',
instance: 'local',
endpoint: `statuses/${toot}/context`
url: `statuses/${toot}/context`
})
return Promise.resolve({
toots: [

View File

@ -50,16 +50,16 @@ export const updateLocal = createAsyncThunk(
} = await client({
method: 'get',
instance: 'remote',
instanceUrl: url,
endpoint: `accounts/verify_credentials`,
instanceDomain: url,
url: `accounts/verify_credentials`,
headers: { Authorization: `Bearer ${token}` }
})
const { body: preferences } = await client({
method: 'get',
instance: 'remote',
instanceUrl: url,
endpoint: `preferences`,
instanceDomain: url,
url: `preferences`,
headers: { Authorization: `Bearer ${token}` }
})
@ -91,14 +91,11 @@ const instancesSlice = createSlice({
})
export const getLocalUrl = (state: RootState) => state.instances.local.url
export const getLocalToken = (state: RootState) => state.instances.local.token
export const getRemoteUrl = (state: RootState) => state.instances.remote.url
export const getLocalAccountId = (state: RootState) =>
state.instances.local.account.id
export const getLocalAccountPreferences = (state: RootState) =>
state.instances.local.account.preferences
// export const {
// updateLocalInstance,
// updateLocalAccount
// } = instancesSlice.actions
export default instancesSlice.reducer

View File

@ -1740,6 +1740,13 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
dependencies:
array-filter "^1.0.0"
axios@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
dependencies:
follow-redirects "^1.10.0"
babel-plugin-dynamic-import-node@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
@ -3060,6 +3067,11 @@ find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
follow-redirects@^1.10.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
fontfaceobserver@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fontfaceobserver/-/fontfaceobserver-2.1.0.tgz#e2705d293e2c585a6531c2a722905657317a2991"