From 4af19d058861b4db030aec4302ae1f1ba1235e4b Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Thu, 22 Oct 2020 00:47:02 +0200 Subject: [PATCH] First commit Public timeline working, with refreshing and load more --- App.js | 21 --- App.jsx | 25 +++ api/client.js | 21 +++ app/store.js | 14 ++ components/TootTimeline.jsx | 47 +++++ package-lock.json | 330 ++++++++++++++++++++++++++++++++++++ package.json | 13 +- screens/Timeline.jsx | 47 +++++ screens/timelineSlice.js | 72 ++++++++ utils/relative-time.js | 19 +++ 10 files changed, 587 insertions(+), 22 deletions(-) delete mode 100644 App.js create mode 100644 App.jsx create mode 100644 api/client.js create mode 100644 app/store.js create mode 100644 components/TootTimeline.jsx create mode 100644 screens/Timeline.jsx create mode 100644 screens/timelineSlice.js create mode 100644 utils/relative-time.js diff --git a/App.js b/App.js deleted file mode 100644 index 181f3cee..00000000 --- a/App.js +++ /dev/null @@ -1,21 +0,0 @@ -import { StatusBar } from 'expo-status-bar'; -import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; - -export default function App() { - return ( - - Open up App.js to start working on your app! - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/App.jsx b/App.jsx new file mode 100644 index 00000000..f46bb2bb --- /dev/null +++ b/App.jsx @@ -0,0 +1,25 @@ +import 'react-native-gesture-handler' +import { NavigationContainer } from '@react-navigation/native' +import { createStackNavigator } from '@react-navigation/stack' + +import React from 'react' +import store from './app/store' +import { Provider } from 'react-redux' +import { StatusBar } from 'expo-status-bar' + +import ScreenTimeline from './screens/Timeline' + +const Stack = createStackNavigator() + +export default function App () { + return ( + + + + + + + + + ) +} diff --git a/api/client.js b/api/client.js new file mode 100644 index 00000000..bc2b7318 --- /dev/null +++ b/api/client.js @@ -0,0 +1,21 @@ +export async function client(endpoint, { body, ...customConfig } = {}) { + let data + try { + const response = await window.fetch(endpoint, config) + data = await response.json() + if (response.ok) { + return data + } + throw new Error(response.statusText) + } catch (err) { + return Promise.reject(err.message ? err.message : data) + } +} + +client.get = function (endpoint, customConfig = {}) { + return client(endpoint, { ...customConfig, method: 'GET' }) +} + +client.post = function (endpoint, body, customConfig = {}) { + return client(endpoint, { ...customConfig, body }) +} diff --git a/app/store.js b/app/store.js new file mode 100644 index 00000000..f163eb98 --- /dev/null +++ b/app/store.js @@ -0,0 +1,14 @@ +import { configureStore } from '@reduxjs/toolkit' + +import timelineReducer from '../screens/timelineSlice' + +export default configureStore({ + reducer: { + timeline: timelineReducer + }, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + immutableCheck: false, + serializableCheck: false + }) +}) diff --git a/components/TootTimeline.jsx b/components/TootTimeline.jsx new file mode 100644 index 00000000..8e644993 --- /dev/null +++ b/components/TootTimeline.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import { Image, StyleSheet, Text, View } from 'react-native' +import HTML from 'react-native-render-html' + +import relative_time from '../utils/relative-time' + +export default function TootTimeline ({ item }) { + return ( + + + + + + {item.account.display_name} + {item.account.acct} + + + {relative_time(item.created_at)} + {item.application && item.application.name !== 'Web' && ( + Linking.openURL(item.application.website)}> + {item.application.name} + + )} + + + + {item.content ? : <>} + + ) +} + +const styles = StyleSheet.create({ + tootTimeline: { + flex: 1, + padding: 15 + }, + header: { + flexDirection: 'row' + }, + avatar: { + width: 40, + height: 40 + }, + name: { + flexDirection: 'row' + } +}) diff --git a/package-lock.json b/package-lock.json index 59bc5770..710173de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1105,6 +1105,14 @@ "minimist": "^1.2.0" } }, + "@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "requires": { + "@types/hammerjs": "^2.0.36" + } + }, "@expo/configure-splash-screen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@expo/configure-splash-screen/-/configure-splash-screen-0.2.0.tgz", @@ -1666,6 +1674,80 @@ "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-4.10.1.tgz", "integrity": "sha512-ael2f1onoPF3vF7YqHGWy7NnafzGu+yp88BbFbP0ydoCP2xGSUzmZVw0zakPTC040Id+JQ9WeFczujMkDy6jYQ==" }, + "@react-native-community/masked-view": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.10.tgz", + "integrity": "sha512-rk4sWFsmtOw8oyx8SD3KSvawwaK7gRBSEIy2TAwURyGt+3TizssXP1r8nx3zY+R7v2vYYHXZ+k2/GULAT/bcaQ==" + }, + "@react-navigation/core": { + "version": "5.12.5", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-5.12.5.tgz", + "integrity": "sha512-+QdQDtC75K1sBfACwCUNFlrCOYf36GYGr9YURHugm3LDEHUwAj7HPJA8FFLw1Rmp5N/P5q9Uct2B/XxJhYzKqA==", + "requires": { + "@react-navigation/routers": "^5.4.12", + "escape-string-regexp": "^4.0.0", + "nanoid": "^3.1.12", + "query-string": "^6.13.5", + "react-is": "^16.13.0", + "use-subscription": "^1.4.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } + } + }, + "@react-navigation/native": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-5.7.6.tgz", + "integrity": "sha512-u+o9Ifs4//Ah6UczXiaAV+hiWPL0NyTbErj5WayeQUd6K5IIbA1UwumRVWs2Xp8q/4Q9h6FpPHUcKOyiTxhaqA==", + "requires": { + "@react-navigation/core": "^5.12.5", + "nanoid": "^3.1.12" + } + }, + "@react-navigation/routers": { + "version": "5.4.12", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-5.4.12.tgz", + "integrity": "sha512-IwMmxeb5e6LboljhakmhtrHBXLYFrFDr2c1GjAG538e4MjT4QGi/ZYckAxCh/NqKI0knnzqKppPl2NsOMv/NoQ==", + "requires": { + "nanoid": "^3.1.12" + } + }, + "@react-navigation/stack": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-5.9.3.tgz", + "integrity": "sha512-/CJ5Rsrc9bMI20dD8oC/QSZHARMFuUv8DeoiYE7R2N0M44cFupF1CrzaZBBC/S4Zi1ahZ0A+Hj/gAzAEQrNTvA==", + "requires": { + "color": "^3.1.2", + "react-native-iphone-x-helper": "^1.2.1" + } + }, + "@reduxjs/toolkit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.4.0.tgz", + "integrity": "sha512-hkxQwVx4BNVRsYdxjNF6cAseRmtrkpSlcgJRr3kLUcHPIAMZAmMJkXmHh/eUEGTMqPzsYpJLM7NN2w9fxQDuGw==", + "requires": { + "immer": "^7.0.3", + "redux": "^4.0.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "dependencies": { + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + } + } + }, + "@types/hammerjs": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz", + "integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -2161,6 +2243,23 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -2348,6 +2447,15 @@ "object-visit": "^1.0.0" } }, + "color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2689,6 +2797,49 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", + "integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==" + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2725,6 +2876,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "envinfo": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.7.3.tgz", @@ -2857,6 +3013,11 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "exec-sh": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", @@ -3570,6 +3731,41 @@ } } }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, + "html-entities": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "http-errors": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", @@ -3595,6 +3791,11 @@ "safer-buffer": ">= 2.1.2 < 3.0.0" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "image-size": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", @@ -3605,6 +3806,11 @@ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" }, + "immer": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.14.tgz", + "integrity": "sha512-BxCs6pJwhgSEUEOZjywW7OA8DXVzfHjkBelSEl0A+nEu0+zS4cFVdNOONvt55N4WOm8Pu4xqSPYxhm1Lv2iBBA==" + }, "import-fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", @@ -5349,6 +5555,11 @@ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "optional": true }, + "nanoid": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -5849,6 +6060,16 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" }, + "query-string": { + "version": "6.13.6", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.6.tgz", + "integrity": "sha512-/WWZ7d9na6s2wMEGdVCVgKWE9Rt7nYyNIf7k8xmHXcesPMlEzicWo3lbYwHyA4wBktI2KrXxxZeACLbE84hvSQ==", + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -6273,11 +6494,51 @@ } } }, + "react-native-gesture-handler": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.7.0.tgz", + "integrity": "sha512-1CrjJf8Z6Iz2XWzfZknYtsm2sud5Lu/pLhhokkgBIKttxqGDtetDEVFDJOTJWJyKCrUPk0X5tnWi/diSF4q++w==", + "requires": { + "@egjs/hammerjs": "^2.0.17", + "hoist-non-react-statics": "^2.3.1", + "invariant": "^2.2.4", + "prop-types": "^15.7.2" + } + }, + "react-native-iphone-x-helper": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.0.tgz", + "integrity": "sha512-+/bcZWFeZt0xSS/+3CHM5K7qPL4vDO/3ARLIowzFpUPGZiPsv9+NET+XNqqseRYwFJwYMmtX+Q4TZKxAVy09ew==" + }, + "react-native-reanimated": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-1.13.1.tgz", + "integrity": "sha512-3sF46jts9MbktgIasf0sTM8uhOYO5a5Q3YyQ4X1jjSE82n/fY2nW3XTFsLGfLEpK2ir4XSDhQWVgFHazaXZTww==", + "requires": { + "fbjs": "^1.0.0" + } + }, + "react-native-render-html": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-4.2.4.tgz", + "integrity": "sha512-OiLItEzKgS7dzD9XI5bHhjcUEfpWdzH1FgexzjbBdICPfYjmmcefpcRmLZY1+HMfxJ7wL8iF1PzTF48LchGTBA==", + "requires": { + "buffer": "^4.5.1", + "events": "^1.1.0", + "html-entities": "^1.2.0", + "htmlparser2": "3.10.1" + } + }, "react-native-safe-area-context": { "version": "3.1.4", "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-screens": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-2.10.1.tgz", + "integrity": "sha512-Z2kKSk4AwWRQNCBmTjViuBQK0/Lx0jc25TZptn/2gKYUCOuVRvCekoA26u0Tsb3BIQ8tWDsZW14OwDlFUXW1aw==" + }, "react-native-web": { "version": "0.13.18", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.13.18.tgz", @@ -6294,6 +6555,44 @@ "react-timer-mixin": "^0.13.4" } }, + "react-native-webview": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-10.7.0.tgz", + "integrity": "sha512-4TSYwJqMBUTKB9+xqGbPwx+eLXbp6RRD7lQ2BumT8eSTfuuqr2rXNqcrlKU1VRla7QGGYowmYmxl2aXIx5k9wA==", + "requires": { + "escape-string-regexp": "2.0.0", + "invariant": "2.2.4" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, + "react-redux": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.1.tgz", + "integrity": "sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + }, + "dependencies": { + "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-refresh": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", @@ -6325,6 +6624,27 @@ } } }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + } + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", @@ -6905,6 +7225,11 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -6960,6 +7285,11 @@ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=" }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", diff --git a/package.json b/package.json index 3ec2c28a..d434c108 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,23 @@ "eject": "expo eject" }, "dependencies": { + "@react-native-community/masked-view": "0.1.10", + "@react-navigation/native": "^5.7.6", + "@react-navigation/stack": "^5.9.3", + "@reduxjs/toolkit": "^1.4.0", "expo": "~39.0.2", "expo-status-bar": "~1.0.2", "react": "16.13.1", "react-dom": "16.13.1", "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz", - "react-native-web": "~0.13.12" + "react-native-gesture-handler": "~1.7.0", + "react-native-reanimated": "~1.13.0", + "react-native-render-html": "^4.2.4", + "react-native-safe-area-context": "3.1.4", + "react-native-screens": "~2.10.1", + "react-native-web": "~0.13.12", + "react-native-webview": "10.7.0", + "react-redux": "^7.2.1" }, "devDependencies": { "@babel/core": "~7.9.0" diff --git a/screens/Timeline.jsx b/screens/Timeline.jsx new file mode 100644 index 00000000..683edaff --- /dev/null +++ b/screens/Timeline.jsx @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react' +import { ActivityIndicator, FlatList, View } from 'react-native' +import { useSelector, useDispatch } from 'react-redux' + +import TootTimeline from '../components/TootTimeline' + +import { allToots, fetchNewer, fetchOlder } from './timelineSlice' + +export default function ScreenTimeline () { + const dispatch = useDispatch() + const toots = useSelector(allToots) + + const fetchNewerStatus = useSelector(state => state.timeline.status) + const fetchNewerError = useSelector(state => state.timeline.error) + const fetchOlderStatus = useSelector(state => state.timeline.status) + const fetchOlderError = useSelector(state => state.timeline.error) + + useEffect(() => { + if (fetchOlderStatus === 'idle') { + dispatch(fetchOlder()) + } + }, [fetchOlderStatus, dispatch]) + + let content + + if (fetchOlderStatus === 'error') { + content = {fetchOlderError} + } else { + content = ( + <> + id} + renderItem={TootTimeline} + onRefresh={() => dispatch(fetchNewer(toots[0].id))} + refreshing={fetchNewerStatus === 'loading'} + onEndReached={() => dispatch(fetchOlder(toots[toots.length - 1].id))} + onEndReachedThreshold={0.2} + style={{ height: '100%', width: '100%' }} + /> + {fetchOlderStatus === 'loading' && } + + ) + } + + return {content} +} diff --git a/screens/timelineSlice.js b/screens/timelineSlice.js new file mode 100644 index 00000000..db5646a6 --- /dev/null +++ b/screens/timelineSlice.js @@ -0,0 +1,72 @@ +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' + +export const fetchNewer = createAsyncThunk('timeline/fetchNewer', async id => { + let data + try { + const response = await fetch( + `https://m.cmx.im/api/v1/timelines/public?since_id=${id}` + ) + data = await response.json() + if (response.ok) { + return data + } + throw new Error(response.statusText) + } catch (err) { + return Promise.reject(err.message ? err.message : data) + } +}) + +export const fetchOlder = createAsyncThunk('timeline/fetchOlder', async id => { + let data + try { + const response = await fetch( + `https://m.cmx.im/api/v1/timelines/public${id ? `?max_id=${id}` : ''}` + ) + data = await response.json() + if (response.ok) { + return data + } + throw new Error(response.statusText) + } catch (err) { + return Promise.reject(err.message ? err.message : data) + } +}) + +export const timelineSlice = createSlice({ + name: 'timeline', + initialState: { + toots: [], + status: 'idle', + error: null + }, + extraReducers: { + [fetchNewer.pending]: (state, action) => { + state.status = 'loading' + }, + [fetchNewer.fulfilled]: (state, action) => { + state.status = 'succeeded' + state.toots.unshift(...action.payload) + }, + [fetchNewer.rejected]: (state, action) => { + state.status = 'failed' + state.error = action.payload + }, + [fetchOlder.pending]: (state, action) => { + state.status = 'loading' + }, + [fetchOlder.fulfilled]: (state, action) => { + state.status = 'succeeded' + state.toots.push(...action.payload) + }, + [fetchOlder.rejected]: (state, action) => { + state.status = 'failed' + state.error = action.payload + } + } +}) + +// export const { update } = timelineSlice.actions + +export const allToots = state => state.timeline.toots + +export default timelineSlice.reducer diff --git a/utils/relative-time.js b/utils/relative-time.js new file mode 100644 index 00000000..b658ada4 --- /dev/null +++ b/utils/relative-time.js @@ -0,0 +1,19 @@ +export default function relative_time (date) { + var units = { + year: 24 * 60 * 60 * 1000 * 365, + month: (24 * 60 * 60 * 1000 * 365) / 12, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + second: 1000 + } + + var rtf = new Intl.RelativeTimeFormat('zh', { numeric: 'auto' }) + + var elapsed = new Date(date) - new Date() + + // "Math.abs" accounts for both "past" & "future" scenarios + for (var u in units) + if (Math.abs(elapsed) > units[u] || u == 'second') + return rtf.format(Math.round(elapsed / units[u]), u) +}