1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Use ky instead of fetch

This commit is contained in:
Zhiyuan Zheng
2020-10-31 02:22:08 +01:00
parent aa53533534
commit 698b54868e
13 changed files with 384 additions and 180 deletions

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
}