First commit

Public timeline working, with refreshing and load more
This commit is contained in:
Zhiyuan Zheng 2020-10-22 00:47:02 +02:00
parent fb152fece9
commit 4af19d0588
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
10 changed files with 587 additions and 22 deletions

21
App.js
View File

@ -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 (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

25
App.jsx Normal file
View File

@ -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 (
<NavigationContainer>
<StatusBar style='auto' />
<Provider store={store}>
<Stack.Navigator>
<Stack.Screen name='Timeline' component={ScreenTimeline} />
</Stack.Navigator>
</Provider>
</NavigationContainer>
)
}

21
api/client.js Normal file
View File

@ -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 })
}

14
app/store.js Normal file
View File

@ -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
})
})

View File

@ -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 (
<View style={styles.tootTimeline}>
<View style={styles.header}>
<Image source={{ uri: item.account.avatar }} style={styles.avatar} />
<View>
<View style={styles.name}>
<Text>{item.account.display_name}</Text>
<Text>{item.account.acct}</Text>
</View>
<View>
<Text>{relative_time(item.created_at)}</Text>
{item.application && item.application.name !== 'Web' && (
<Text onPress={() => Linking.openURL(item.application.website)}>
{item.application.name}
</Text>
)}
</View>
</View>
</View>
{item.content ? <HTML html={item.content} /> : <></>}
</View>
)
}
const styles = StyleSheet.create({
tootTimeline: {
flex: 1,
padding: 15
},
header: {
flexDirection: 'row'
},
avatar: {
width: 40,
height: 40
},
name: {
flexDirection: 'row'
}
})

330
package-lock.json generated
View File

@ -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",

View File

@ -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"

47
screens/Timeline.jsx Normal file
View File

@ -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 = <Text>{fetchOlderError}</Text>
} else {
content = (
<>
<FlatList
data={toots}
keyExtractor={({ id }) => 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' && <ActivityIndicator />}
</>
)
}
return <View>{content}</View>
}

72
screens/timelineSlice.js Normal file
View File

@ -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

19
utils/relative-time.js Normal file
View File

@ -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)
}