Use `ky` instead of `fetch`

This commit is contained in:
Zhiyuan Zheng 2020-10-31 02:22:08 +01:00
parent aa53533534
commit 698b54868e
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
13 changed files with 384 additions and 180 deletions

67
package-lock.json generated
View File

@ -4631,6 +4631,11 @@
"graceful-fs": "^4.1.9"
}
},
"ky": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/ky/-/ky-0.24.0.tgz",
"integrity": "sha512-/vpuQguwV30jErrqLpoaU/YJAFALrUkqqWLILnSoBOj5/O/LKzro/pPNtxbLgY6m4w5XNM6YZ3v7/or8qLlFuw=="
},
"leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -6020,6 +6025,21 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
}
}
},
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@ -6717,6 +6737,14 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-3.1.4.tgz",
"integrity": "sha512-bXx3hqz4LovFoMnJIRGIWL2oJ/PHadXviBKvgZV9yNErtURQLJSn0yfQytVtiqslhaBMZOJwH4R6HiClyofvBg=="
},
"react-native-safe-area-view": {
"version": "0.14.9",
"resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.14.9.tgz",
"integrity": "sha512-WII/ulhpVyL/qbYb7vydq7dJAfZRBcEhg4/UWt6F6nAKpLa3gAceMOxBxI914ppwSP/TdUsandFy6lkJQE0z4A==",
"requires": {
"hoist-non-react-statics": "^2.3.1"
}
},
"react-native-screens": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-2.10.1.tgz",
@ -6754,6 +6782,45 @@
}
}
},
"react-navigation": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-4.4.3.tgz",
"integrity": "sha512-tNBQQzbw0PVo9FLypQUUCISMcXW0wCW8oQeHtY0spWf35KC3IZHq/WcBm4E956wFsaqrDMGCUnyaVrxZNSuUGg==",
"requires": {
"@react-navigation/core": "^3.7.9",
"@react-navigation/native": "^3.8.3"
},
"dependencies": {
"@react-navigation/core": {
"version": "3.7.9",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.7.9.tgz",
"integrity": "sha512-EknbzM8OI9A5alRxXtQRV5Awle68B+z1QAxNty5DxmlS3BNfmduWNGnim159ROyqxkuDffK9L/U/Tbd45mx+Jg==",
"requires": {
"hoist-non-react-statics": "^3.3.2",
"path-to-regexp": "^1.8.0",
"query-string": "^6.13.6",
"react-is": "^16.13.0"
}
},
"@react-navigation/native": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.8.3.tgz",
"integrity": "sha512-1yLd2pi8SK3wPC58mWZ5fjW5uYr1gmMN8YwjkA2qVjyVYfzzctRkoFDu8poO5UzxEIgf/4ns6ezBtKY1Q601UQ==",
"requires": {
"hoist-non-react-statics": "^3.3.2",
"react-native-safe-area-view": "^0.14.9"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
}
}
},
"react-redux": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",

View File

@ -12,13 +12,18 @@
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/segmented-control": "2.1.1",
"@react-native-community/viewpager": "4.1.6",
"@react-navigation/bottom-tabs": "^5.9.2",
"@react-navigation/native": "^5.7.6",
"@react-navigation/stack": "^5.9.3",
"@reduxjs/toolkit": "^1.4.0",
"expo": "~39.0.2",
"expo-app-auth": "~9.2.0",
"expo-av": "~8.6.0",
"expo-secure-store": "~9.2.0",
"expo-splash-screen": "~0.6.1",
"expo-status-bar": "~1.0.2",
"ky": "^0.24.0",
"prop-types": "^15.7.2",
"react": "16.13.1",
"react-dom": "16.13.1",
@ -33,11 +38,8 @@
"react-native-screens": "~2.10.1",
"react-native-web": "~0.13.7",
"react-native-webview": "10.7.0",
"react-redux": "^7.2.1",
"expo-av": "~8.6.0",
"expo-secure-store": "~9.2.0",
"expo-splash-screen": "~0.6.1",
"@react-native-community/viewpager": "4.1.6"
"react-navigation": "^4.4.3",
"react-redux": "^7.2.1"
},
"devDependencies": {
"@babel/core": "~7.9.0",

View File

@ -1,43 +1,40 @@
export async function client (url, query, { body, ...customConfig } = {}) {
if (!url) {
return Promise.reject('Missing URL.')
}
const headers = { 'Content-Type': 'application/json' }
import store from 'src/stacks/common/store'
import ky from 'ky'
const config = {
method: body ? 'POST' : 'GET',
...customConfig,
headers: {
...headers,
...customConfig.headers
}
}
export default async function client ({
method, // * get / post
instance, // * local / remote
endpoint, // * if url is empty
query, // object
body // object
}) {
const state = store.getState().instanceInfo
const queryString = query
? `?${query.map(({ key, value }) => `${key}=${value}`).join('&')}`
: ''
if (body) {
config.body = JSON.stringify(body)
}
let data
let response
try {
const response = await fetch(`${url}${queryString}`, config)
data = await response.json()
if (response.ok) {
return { headers: response.headers, body: data }
}
throw new Error(response.statusText)
} catch (err) {
return Promise.reject(err.message ? err.message : data)
response = await ky(endpoint, {
method: method,
prefixUrl: `https://${state[instance]}/api/v1`,
searchParams: query,
headers: {
'Content-Type': 'application/json',
...(instance === 'local' && {
Authorization: `Bearer ${state.localToken}`
})
},
...(body && { json: body })
})
} catch {
return Promise.reject('ky error')
}
if (response.ok) {
return Promise.resolve({
headers: response.headers,
body: await response.json()
})
} else {
console.error(response.error)
return Promise.reject({ body: response.error_message })
}
}
client.get = function (instance, endpoint, query, customConfig = {}) {
return client(instance, endpoint, query, { ...customConfig, method: 'GET' })
}
client.post = function (instance, endpoint, query, body, customConfig = {}) {
return client(instance, endpoint, query, { ...customConfig, body })
}

61
src/api/client.js.backup Normal file
View File

@ -0,0 +1,61 @@
import store from 'src/stacks/common/store'
export async function client (instance, query, { body, ...customConfig } = {}) {
const state = store.getState().instanceInfo
let url
let authHeader
switch (instance.type) {
case 'local':
url = `https://${state.local}/${instance.endpoint}`
authHeader = {
Authorization: `Bearer ${state.localToken}`
}
break
case 'remote':
url = `https://${state.remote}/${instance.endpoint}`
authHeader = {}
break
default:
return Promise.reject('Instance type is not defined.')
}
const headers = { 'Content-Type': 'application/json', ...authHeader }
const config = {
method: body ? 'POST' : 'GET',
...customConfig,
headers: {
...headers,
...customConfig.headers
}
}
const queryString = query
? `?${query.map(({ key, value }) => `${key}=${value}`).join('&')}`
: ''
if (body) {
config.body = JSON.stringify(body)
}
let data
try {
const response = await fetch(`${url}${queryString}`, config)
data = await response.json()
if (response.ok) {
return { headers: response.headers, body: data }
}
throw new Error(response.statusText)
} catch (err) {
return Promise.reject(err.message ? err.message : data)
}
}
client.get = function (instance, endpoint, query, customConfig = {}) {
return client(instance, endpoint, query, { ...customConfig, method: 'GET' })
}
client.post = function (instance, endpoint, query, body, customConfig = {}) {
return client(instance, endpoint, query, { ...customConfig, body })
}

View File

@ -3,7 +3,10 @@ import PropTypes from 'prop-types'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import action from 'src/components/action'
export default function Actions ({
id,
replies_count,
reblogs_count,
reblogged,
@ -20,7 +23,7 @@ export default function Actions ({
<Feather name='repeat' />
<Text>{reblogs_count}</Text>
</Pressable>
<Pressable style={styles.action}>
<Pressable style={styles.action} onPress={() => action('favourite', id)}>
<Feather name='heart' />
<Text>{favourites_count}</Text>
</Pressable>
@ -34,15 +37,23 @@ export default function Actions ({
const styles = StyleSheet.create({
actions: {
flex: 1,
flexDirection: 'row'
flexDirection: 'row',
marginTop: 4
},
action: {
width: '25%',
flexDirection: 'row',
justifyContent: 'center'
justifyContent: 'center',
paddingTop: 8,
paddingBottom: 8
}
})
// Actions.propTypes = {
// uri: PropTypes.string
// }
Actions.propTypes = {
id: PropTypes.string.isRequired,
replies_count: PropTypes.number.isRequired,
reblogs_count: PropTypes.number.isRequired,
reblogged: PropTypes.bool.isRequired,
favourites_count: PropTypes.number.isRequired,
favourited: PropTypes.bool.isRequired
}

View File

@ -35,7 +35,6 @@ export default function TootNotification ({ toot }) {
account={actualAccount.acct}
created_at={toot.created_at}
/>
{/* Can pass toot info to next page to speed up performance */}
<Pressable
onPress={() => navigation.navigate('Toot', { toot: toot.id })}
>
@ -52,7 +51,7 @@ export default function TootNotification ({ toot }) {
/>
)}
{toot.status.poll && <Poll poll={toot.status.poll} />}
{toot.status.media_attachments && (
{toot.status.media_attachments.length > 0 && (
<Attachment
media_attachments={toot.status.media_attachments}
sensitive={toot.status.sensitive}
@ -67,6 +66,7 @@ export default function TootNotification ({ toot }) {
</Pressable>
{toot.status && (
<Actions
id={toot.status.id}
replies_count={toot.status.replies_count}
reblogs_count={toot.status.reblogs_count}
reblogged={toot.status.reblogged}
@ -78,7 +78,7 @@ export default function TootNotification ({ toot }) {
</View>
</View>
)
})
}, [toot])
return tootView
}

View File

@ -50,9 +50,9 @@ export default function TootTimeline ({ toot }) {
/>
{/* Can pass toot info to next page to speed up performance */}
<Pressable
// onPress={() =>
// navigation.navigate('Toot', { toot: actualContent.id })
// }
onPress={() =>
navigation.navigate('Toot', { toot: actualContent.id })
}
>
{actualContent.content ? (
<Content
@ -77,6 +77,7 @@ export default function TootTimeline ({ toot }) {
{actualContent.card && <Card card={actualContent.card} />}
</Pressable>
<Actions
id={actualContent.id}
replies_count={actualContent.replies_count}
reblogs_count={actualContent.reblogs_count}
reblogged={actualContent.reblogged}

63
src/components/action.js Normal file
View File

@ -0,0 +1,63 @@
import { Alert } from 'react-native'
import { useSelector } from 'react-redux'
import { client } from 'src/api/client'
export default async function action (type, id) {
// If header if needed for remote server
const header = {
headers: {
Authorization: `Bearer ${useSelector(
state => state.instanceInfo.localToken
)}`
}
}
const instance = `https://${useSelector(
state => state.instanceInfo.local
)}/api/v1/`
let endpoint
switch (type) {
case 'favourite':
endpoint = `${instance}statuses/${id}/favourite`
break
case 'unfavourite':
endpoint = `${instance}statuses/${id}/unfavourite`
break
case 'reblog':
endpoint = `${instance}statuses/${id}/reblog`
break
case 'unreblog':
endpoint = `${instance}statuses/${id}/unreblog`
break
case 'bookmark':
endpoint = `${instance}statuses/${id}/bookmark`
break
case 'unbookmark':
endpoint = `${instance}statuses/${id}/unbookmark`
break
case 'mute':
endpoint = `${instance}statuses/${id}/mute`
break
case 'unmute':
endpoint = `${instance}statuses/${id}/unmute`
break
case 'pin':
endpoint = `${instance}statuses/${id}/pin`
break
case 'unpin':
endpoint = `${instance}statuses/${id}/unpin`
break
}
const res = await client.post(endpoint, [], header)
console.log(res)
const alert = {
title: 'This is a title',
message: 'This is a message'
}
Alert.alert(alert.title, alert.message, [
{ text: 'OK', onPress: () => console.log('OK Pressed') }
])
}

View File

@ -6,7 +6,7 @@ const propTypesAttachment = PropTypes.shape({
type: PropTypes.oneOf(['unknown', 'image', 'gifv', 'video', 'audio'])
.isRequired,
url: PropTypes.string.isRequired,
preview_url: PropTypes.string.isRequired,
preview_url: PropTypes.string,
// Others
remote_url: PropTypes.string,

View File

@ -15,7 +15,7 @@ export default function Timeline ({
list,
toot,
account,
disableRefresh
disableRefresh = false
}) {
const dispatch = useDispatch()
const state = useSelector(state => state.timelines[page])
@ -50,11 +50,25 @@ export default function Timeline ({
{...(state.pointer && { initialScrollIndex: state.pointer })}
{...(!disableRefresh && {
onRefresh: () =>
dispatch(fetch({ page, paginationDirection: 'prev' })),
dispatch(
fetch({
page,
hashtag,
list,
paginationDirection: 'prev'
})
),
refreshing: state.status === 'loading',
onEndReached: () => {
if (!timelineReady) {
dispatch(fetch({ page, paginationDirection: 'next' }))
dispatch(
fetch({
page,
hashtag,
list,
paginationDirection: 'next'
})
)
setTimelineReady(true)
}
},

View File

@ -101,7 +101,8 @@ TimelinesCombined.propTypes = {
content: PropTypes.arrayOf(
PropTypes.exact({
title: PropTypes.string.isRequired,
page: Timeline.propTypes.page
page: Timeline.propTypes.page,
instance: PropTypes.oneOf(['local', 'remote'])
})
).isRequired
}

View File

@ -1,12 +1,15 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { client } from 'src/api/client'
import client from 'src/api/client'
export const fetch = createAsyncThunk(
'account/fetch',
async ({ id }, { getState }) => {
const instanceLocal = `https://${getState().instanceInfo.local}/api/v1/`
const res = await client.get(`${instanceLocal}accounts/${id}`)
const res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${id}`
})
return res.body
}
)

View File

@ -1,6 +1,6 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { client } from 'src/api/client'
import client from 'src/api/client'
// Naming convention
// Following: timelines/home
@ -11,170 +11,161 @@ import { client } from 'src/api/client'
// Hashtag: hastag
// List: list
function getPagination (headers, direction) {
if (!headers) console.error('Missing pagination headers')
const paginationLinks = headers.get('Link')
if (paginationLinks) {
if (direction) {
return {
[direction]: paginationLinks.split(
new RegExp(`<([^>]+)>; rel="${direction}"`)
)[1]
}
} else {
return {
prev: paginationLinks.split(new RegExp(/<([^>]+)>; rel="prev"/))[1],
next: paginationLinks.split(new RegExp(/<([^>]+)>; rel="next"/))[1]
}
}
} else {
return
}
}
export const fetch = createAsyncThunk(
'timeline/fetch',
async (
{ page, paginationDirection, query = [], account, hashtag, list, toot },
{ page, paginationDirection, query = {}, account, hashtag, list, toot },
{ getState }
) => {
const instanceLocal = `https://${getState().instanceInfo.local}/api/v1/`
const instanceRemote = `https://${getState().instanceInfo.remote}/api/v1/`
// If header if needed for remote server
const header = {
headers: {
Authorization: `Bearer ${getState().instanceInfo.localToken}`
}
}
let res
// For same page, but only pagination
if (paginationDirection) {
res = await client.get(
getState().timelines[page].pagination[paginationDirection],
query,
header
)
return {
toots: res.body,
pagination: getPagination(res.headers, paginationDirection)
const allToots = getState().timelines[page].toots
switch (paginationDirection) {
case 'prev':
query.min_id = allToots[0].id
break
case 'next':
query.max_id = allToots[allToots.length - 1].id
break
}
}
// For each page's first query
switch (page) {
case 'Following':
res = await client.get(`${instanceLocal}timelines/home`, query, header)
res = await client({
method: 'get',
instance: 'local',
endpoint: 'timelines/home',
query
})
return {
toots: res.body,
pagination: getPagination(res.headers)
toots: res.body
}
case 'Local':
query.push({ key: 'local', value: 'true' })
res = await client.get(
`${instanceLocal}timelines/public`,
query,
header
)
query.local = 'true'
res = await client({
method: 'get',
instance: 'local',
endpoint: 'timelines/public',
query
})
return {
toots: res.body,
pagination: getPagination(res.headers)
toots: res.body
}
case 'LocalPublic':
res = await client.get(
`${instanceLocal}timelines/public`,
query,
header
)
res = await client({
method: 'get',
instance: 'local',
endpoint: 'timelines/public',
query
})
return {
toots: res.body,
pagination: getPagination(res.headers)
toots: res.body
}
case 'RemotePublic':
res = await client.get(`${instanceRemote}timelines/public`, query)
res = await client({
method: 'get',
instance: 'remote',
endpoint: 'timelines/public',
query
})
return {
toots: res.body,
pagination: getPagination(res.headers)
toots: res.body
}
case 'Notifications':
res = await client.get(`${instanceLocal}notifications`, query, header)
res = await client({
method: 'get',
instance: 'local',
endpoint: 'notifications',
query
})
return {
toots: res.body,
pagination: getPagination(res.headers)
toots: res.body
}
case 'Account_Default':
res = await client.get(
`${instanceLocal}accounts/${account}/statuses`,
[{ key: 'pinned', value: 'true' }],
header
)
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query: {
pinned: 'true'
}
})
const toots = res.body
res = await client.get(
`${instanceLocal}accounts/${account}/statuses`,
[{ key: 'exclude_replies', value: 'true' }],
header
)
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query: {
exclude_replies: 'true'
}
})
toots.push(...res.body)
return { toots: toots }
case 'Account_All':
res = await client.get(
`${instanceLocal}accounts/${account}/statuses`,
query,
header
)
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query
})
return {
toots: res.body
}
case 'Account_Media':
res = await client.get(
`${instanceLocal}accounts/${account}/statuses`,
[{ key: 'only_media', value: 'true' }],
header
)
res = await client({
method: 'get',
instance: 'local',
endpoint: `accounts/${account}/statuses`,
query: {
only_media: 'true'
}
})
return {
toots: res.body
}
case 'Hashtag':
res = await client.get(
`${instanceLocal}timelines/tag/${hashtag}`,
query,
header
)
res = await client({
method: 'get',
instance: 'local',
endpoint: `timelines/tag/${hashtag}`,
query
})
return {
toots: res.body,
pagination: getPagination(res.headers)
toots: res.body
}
case 'List':
res = await client.get(
`${instanceLocal}timelines/list/${list}`,
query,
header
)
res = await client({
method: 'get',
instance: 'local',
endpoint: `timelines/list/${list}`,
query
})
return {
toots: res.body,
pagination: getPagination(res.headers)
toots: res.body
}
case 'Toot':
const current = await client.get(
`${instanceLocal}statuses/${toot}`,
[],
header
)
const context = await client.get(
`${instanceLocal}statuses/${toot}/context`,
[],
header
)
const current = await client({
method: 'get',
instance: 'local',
endpoint: `statuses/${toot}`
})
const context = await client({
method: 'get',
instance: 'local',
endpoint: `statuses/${toot}/context`
})
return {
toots: [...context.ancestors, current, ...context.descendants],
pointer: context.ancestors.length
@ -188,7 +179,6 @@ export const fetch = createAsyncThunk(
const timelineInitState = {
toots: [],
pagination: { prev: undefined, next: undefined },
pointer: undefined,
status: 'idle'
}
@ -226,12 +216,6 @@ export const timelineSlice = createSlice({
state[action.meta.arg.page].toots.push(...action.payload.toots)
}
if (action.payload.pagination) {
state[action.meta.arg.page].pagination = {
...state[action.meta.arg.page].pagination,
...action.payload.pagination
}
}
if (action.payload.pointer) {
state[action.meta.arg.page].pointer = action.payload.pointer
}