From 698b54868e8fb5adb1fba722bb50876abfce2978 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sat, 31 Oct 2020 02:22:08 +0100 Subject: [PATCH] Use `ky` instead of `fetch` --- package-lock.json | 67 ++++++++ package.json | 12 +- src/api/client.js | 73 ++++---- src/api/client.js.backup | 61 +++++++ src/components/Toot/Actions.jsx | 23 ++- src/components/TootNotification.jsx | 6 +- src/components/TootTimeline.jsx | 7 +- src/components/action.js | 63 +++++++ src/prop-types/attachment.js | 2 +- src/stacks/common/Timeline.jsx | 20 ++- src/stacks/common/TimelinesCombined.jsx | 3 +- src/stacks/common/accountSlice.js | 9 +- src/stacks/common/timelineSlice.js | 218 +++++++++++------------- 13 files changed, 384 insertions(+), 180 deletions(-) create mode 100644 src/api/client.js.backup create mode 100644 src/components/action.js diff --git a/package-lock.json b/package-lock.json index 6cc1af47..158ab384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 58e7faee..4fa26a0f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/client.js b/src/api/client.js index 0f9a34ac..1d22de22 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -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 }) -} diff --git a/src/api/client.js.backup b/src/api/client.js.backup new file mode 100644 index 00000000..b6e07dd2 --- /dev/null +++ b/src/api/client.js.backup @@ -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 }) +} diff --git a/src/components/Toot/Actions.jsx b/src/components/Toot/Actions.jsx index 64f279b0..c1f997ee 100644 --- a/src/components/Toot/Actions.jsx +++ b/src/components/Toot/Actions.jsx @@ -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 ({ {reblogs_count} - + action('favourite', id)}> {favourites_count} @@ -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 +} diff --git a/src/components/TootNotification.jsx b/src/components/TootNotification.jsx index 93771b3c..f1a991e8 100644 --- a/src/components/TootNotification.jsx +++ b/src/components/TootNotification.jsx @@ -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 */} navigation.navigate('Toot', { toot: toot.id })} > @@ -52,7 +51,7 @@ export default function TootNotification ({ toot }) { /> )} {toot.status.poll && } - {toot.status.media_attachments && ( + {toot.status.media_attachments.length > 0 && ( {toot.status && ( ) - }) + }, [toot]) return tootView } diff --git a/src/components/TootTimeline.jsx b/src/components/TootTimeline.jsx index db6f781d..8f17825a 100644 --- a/src/components/TootTimeline.jsx +++ b/src/components/TootTimeline.jsx @@ -50,9 +50,9 @@ export default function TootTimeline ({ toot }) { /> {/* Can pass toot info to next page to speed up performance */} - // navigation.navigate('Toot', { toot: actualContent.id }) - // } + onPress={() => + navigation.navigate('Toot', { toot: actualContent.id }) + } > {actualContent.content ? ( } 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') } + ]) +} diff --git a/src/prop-types/attachment.js b/src/prop-types/attachment.js index c800a0ac..6b7065a3 100644 --- a/src/prop-types/attachment.js +++ b/src/prop-types/attachment.js @@ -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, diff --git a/src/stacks/common/Timeline.jsx b/src/stacks/common/Timeline.jsx index b63216f5..76b6ad3b 100644 --- a/src/stacks/common/Timeline.jsx +++ b/src/stacks/common/Timeline.jsx @@ -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) } }, diff --git a/src/stacks/common/TimelinesCombined.jsx b/src/stacks/common/TimelinesCombined.jsx index 2cd0e2d1..4cf390f4 100644 --- a/src/stacks/common/TimelinesCombined.jsx +++ b/src/stacks/common/TimelinesCombined.jsx @@ -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 } diff --git a/src/stacks/common/accountSlice.js b/src/stacks/common/accountSlice.js index 22b7b6ee..1a3e57a0 100644 --- a/src/stacks/common/accountSlice.js +++ b/src/stacks/common/accountSlice.js @@ -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 } ) diff --git a/src/stacks/common/timelineSlice.js b/src/stacks/common/timelineSlice.js index 409fcb0d..6b3295a4 100644 --- a/src/stacks/common/timelineSlice.js +++ b/src/stacks/common/timelineSlice.js @@ -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 }