mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Second commit
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,3 +12,5 @@ web-build/
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
.env
|
25
App.jsx
25
App.jsx
@ -1,25 +1,6 @@
|
|||||||
import 'react-native-gesture-handler'
|
|
||||||
import { NavigationContainer } from '@react-navigation/native'
|
|
||||||
import { createStackNavigator } from '@react-navigation/stack'
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import store from './app/store'
|
import Main from './src/Main'
|
||||||
import { Provider } from 'react-redux'
|
|
||||||
import { StatusBar } from 'expo-status-bar'
|
|
||||||
|
|
||||||
import ScreenTimeline from './screens/Timeline'
|
const App = () => <Main />
|
||||||
|
|
||||||
const Stack = createStackNavigator()
|
export default App
|
||||||
|
|
||||||
export default function App () {
|
|
||||||
return (
|
|
||||||
<NavigationContainer>
|
|
||||||
<StatusBar style='auto' />
|
|
||||||
<Provider store={store}>
|
|
||||||
<Stack.Navigator>
|
|
||||||
<Stack.Screen name='Timeline' component={ScreenTimeline} />
|
|
||||||
</Stack.Navigator>
|
|
||||||
</Provider>
|
|
||||||
</NavigationContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
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 })
|
|
||||||
}
|
|
6
app.json
6
app.json
@ -2,6 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "mastodon-app",
|
"name": "mastodon-app",
|
||||||
"slug": "mastodon-app",
|
"slug": "mastodon-app",
|
||||||
|
"scheme": "mastodonct",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
@ -17,10 +18,7 @@
|
|||||||
"**/*"
|
"**/*"
|
||||||
],
|
],
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": false
|
||||||
},
|
|
||||||
"web": {
|
|
||||||
"favicon": "./assets/favicon.png"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,16 @@
|
|||||||
module.exports = function(api) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true)
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo'],
|
presets: ['babel-preset-expo'],
|
||||||
};
|
plugins: [
|
||||||
};
|
[
|
||||||
|
'module-resolver',
|
||||||
|
{
|
||||||
|
alias: {
|
||||||
|
src: './src'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
})
|
|
172
package-lock.json
generated
172
package-lock.json
generated
@ -1347,6 +1347,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@react-native-async-storage/async-storage": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-c+pKuUe54sysxnqsfG17kaAcd9xJQyTNYDQhZhYf3Ej5khAQPPM85eN2nc1sj1qEnxDde4mcfi3slrOd/KtoSw==",
|
||||||
|
"requires": {
|
||||||
|
"deep-assign": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@react-native-community/cli-debugger-ui": {
|
"@react-native-community/cli-debugger-ui": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-4.9.0.tgz",
|
||||||
@ -1679,6 +1687,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.10.tgz",
|
||||||
"integrity": "sha512-rk4sWFsmtOw8oyx8SD3KSvawwaK7gRBSEIy2TAwURyGt+3TizssXP1r8nx3zY+R7v2vYYHXZ+k2/GULAT/bcaQ=="
|
"integrity": "sha512-rk4sWFsmtOw8oyx8SD3KSvawwaK7gRBSEIy2TAwURyGt+3TizssXP1r8nx3zY+R7v2vYYHXZ+k2/GULAT/bcaQ=="
|
||||||
},
|
},
|
||||||
|
"@react-navigation/bottom-tabs": {
|
||||||
|
"version": "5.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-5.9.2.tgz",
|
||||||
|
"integrity": "sha512-nSaAXFNbRukhMGq4U3m1rqbFZEPT64/gPTwUwQ5YQom5Q0a6xZ0AnOmtFIXvDBbK5VJiokGdBLJ/TMDchqcujQ==",
|
||||||
|
"requires": {
|
||||||
|
"color": "^3.1.2",
|
||||||
|
"react-native-iphone-x-helper": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@react-navigation/core": {
|
"@react-navigation/core": {
|
||||||
"version": "5.12.5",
|
"version": "5.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-5.12.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-5.12.5.tgz",
|
||||||
@ -2021,15 +2038,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-module-resolver": {
|
"babel-plugin-module-resolver": {
|
||||||
"version": "3.2.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.0.0.tgz",
|
||||||
"integrity": "sha512-tjR0GvSndzPew/Iayf4uICWZqjBwnlMWjSx6brryfQ81F9rxBVqwDJtFCV8oOs0+vJeefK9TmdZtkIFdFe1UnA==",
|
"integrity": "sha512-3pdEq3PXALilSJ6dnC4wMWr0AZixHRM4utpdpBR9g5QG7B7JwWyukQv7a9hVxkbGFl+nQbrHDqqQOIBtTXTP/Q==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"find-babel-config": "^1.1.0",
|
"find-babel-config": "^1.2.0",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.6",
|
||||||
"pkg-up": "^2.0.0",
|
"pkg-up": "^3.1.0",
|
||||||
"reselect": "^3.0.1",
|
"reselect": "^4.0.0",
|
||||||
"resolve": "^1.4.0"
|
"resolve": "^1.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-react-native-web": {
|
"babel-plugin-react-native-web": {
|
||||||
@ -2052,6 +2070,71 @@
|
|||||||
"babel-plugin-module-resolver": "^3.2.0",
|
"babel-plugin-module-resolver": "^3.2.0",
|
||||||
"babel-plugin-react-native-web": "~0.13.6",
|
"babel-plugin-react-native-web": "~0.13.6",
|
||||||
"metro-react-native-babel-preset": "~0.59.0"
|
"metro-react-native-babel-preset": "~0.59.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"babel-plugin-module-resolver": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-tjR0GvSndzPew/Iayf4uICWZqjBwnlMWjSx6brryfQ81F9rxBVqwDJtFCV8oOs0+vJeefK9TmdZtkIFdFe1UnA==",
|
||||||
|
"requires": {
|
||||||
|
"find-babel-config": "^1.1.0",
|
||||||
|
"glob": "^7.1.2",
|
||||||
|
"pkg-up": "^2.0.0",
|
||||||
|
"reselect": "^3.0.1",
|
||||||
|
"resolve": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"find-up": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
|
||||||
|
"requires": {
|
||||||
|
"locate-path": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locate-path": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
|
||||||
|
"requires": {
|
||||||
|
"p-locate": "^2.0.0",
|
||||||
|
"path-exists": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p-limit": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
|
||||||
|
"requires": {
|
||||||
|
"p-try": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p-locate": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
||||||
|
"requires": {
|
||||||
|
"p-limit": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p-try": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||||
|
},
|
||||||
|
"pkg-up": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
|
||||||
|
"requires": {
|
||||||
|
"find-up": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reselect": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-preset-fbjs": {
|
"babel-preset-fbjs": {
|
||||||
@ -3128,6 +3211,14 @@
|
|||||||
"uuid": "^3.4.0"
|
"uuid": "^3.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"expo-app-auth": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-app-auth/-/expo-app-auth-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-aRsRTfCfpuR8n8nO22WwwwTB7JhhJv7T9ZB5f68u6NN/FTF5CnhI3m5t0lawmirNSdmK+mGL0L+gvRyVyt1WLQ==",
|
||||||
|
"requires": {
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"expo-asset": {
|
"expo-asset": {
|
||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-8.2.0.tgz",
|
||||||
@ -3536,11 +3627,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"find-up": {
|
"find-up": {
|
||||||
"version": "2.1.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
|
||||||
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
|
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"locate-path": "^2.0.0"
|
"locate-path": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fontfaceobserver": {
|
"fontfaceobserver": {
|
||||||
@ -4494,11 +4586,12 @@
|
|||||||
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
|
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
|
||||||
},
|
},
|
||||||
"locate-path": {
|
"locate-path": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||||
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
|
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"p-locate": "^2.0.0",
|
"p-locate": "^3.0.0",
|
||||||
"path-exists": "^3.0.0"
|
"path-exists": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5556,9 +5649,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.1.12",
|
"version": "3.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.13.tgz",
|
||||||
"integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A=="
|
"integrity": "sha512-oYL7jWZUdScASxYOrcwE8EvISFGzO3/1g+t56vCyR0s2nrpmBcOc7hTAFJaVf6HMyEPJrnNelnjRnMN6KZnCPA=="
|
||||||
},
|
},
|
||||||
"nanomatch": {
|
"nanomatch": {
|
||||||
"version": "1.2.13",
|
"version": "1.2.13",
|
||||||
@ -5798,25 +5891,28 @@
|
|||||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||||
},
|
},
|
||||||
"p-limit": {
|
"p-limit": {
|
||||||
"version": "1.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"p-try": "^1.0.0"
|
"p-try": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"p-locate": {
|
"p-locate": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
|
||||||
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"p-limit": "^1.1.0"
|
"p-limit": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"p-try": {
|
"p-try": {
|
||||||
"version": "1.0.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"parse-json": {
|
"parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@ -5929,11 +6025,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pkg-up": {
|
"pkg-up": {
|
||||||
"version": "2.0.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
|
||||||
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
|
"integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"find-up": "^2.1.0"
|
"find-up": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plist": {
|
"plist": {
|
||||||
@ -6773,9 +6870,10 @@
|
|||||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||||
},
|
},
|
||||||
"reselect": {
|
"reselect": {
|
||||||
"version": "3.0.1",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
|
||||||
"integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
|
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.18.1",
|
"version": "1.18.1",
|
||||||
|
@ -8,12 +8,16 @@
|
|||||||
"eject": "expo eject"
|
"eject": "expo eject"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||||
"@react-native-community/masked-view": "0.1.10",
|
"@react-native-community/masked-view": "0.1.10",
|
||||||
|
"@react-navigation/bottom-tabs": "^5.9.2",
|
||||||
"@react-navigation/native": "^5.7.6",
|
"@react-navigation/native": "^5.7.6",
|
||||||
"@react-navigation/stack": "^5.9.3",
|
"@react-navigation/stack": "^5.9.3",
|
||||||
"@reduxjs/toolkit": "^1.4.0",
|
"@reduxjs/toolkit": "^1.4.0",
|
||||||
"expo": "~39.0.2",
|
"expo": "~39.0.2",
|
||||||
|
"expo-app-auth": "~9.2.0",
|
||||||
"expo-status-bar": "~1.0.2",
|
"expo-status-bar": "~1.0.2",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-dom": "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": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz",
|
||||||
@ -27,7 +31,8 @@
|
|||||||
"react-redux": "^7.2.1"
|
"react-redux": "^7.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "~7.9.0"
|
"@babel/core": "~7.9.0",
|
||||||
|
"babel-plugin-module-resolver": "^4.0.0"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
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
|
|
35
src/Main.jsx
Normal file
35
src/Main.jsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'react-native-gesture-handler'
|
||||||
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||||
|
import { NavigationContainer } from '@react-navigation/native'
|
||||||
|
import { enableScreens } from 'react-native-screens'
|
||||||
|
enableScreens()
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import store from './app/store'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { StatusBar } from 'expo-status-bar'
|
||||||
|
|
||||||
|
import MainTimeline from 'src/stacks/MainTimeline'
|
||||||
|
import PublicTimeline from 'src/stacks/PublicTimeline'
|
||||||
|
import Notifications from 'src/stacks/Notifications'
|
||||||
|
import Me from 'src/stacks/Me'
|
||||||
|
|
||||||
|
const Tab = createBottomTabNavigator()
|
||||||
|
|
||||||
|
export default function Main () {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Provider store={store}>
|
||||||
|
<StatusBar style='auto' />
|
||||||
|
<NavigationContainer>
|
||||||
|
<Tab.Navigator>
|
||||||
|
<Tab.Screen name='MainTimeline' component={MainTimeline} />
|
||||||
|
<Tab.Screen name='PublicTimeline' component={PublicTimeline} />
|
||||||
|
{/* <Tab.Screen name='Notifications' component={Notifications} /> */}
|
||||||
|
<Tab.Screen name='Me' component={Me} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
</Provider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
53
src/api/client.js
Normal file
53
src/api/client.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
export async function client (
|
||||||
|
instance,
|
||||||
|
endpoint,
|
||||||
|
query,
|
||||||
|
{ body, ...customConfig } = {}
|
||||||
|
) {
|
||||||
|
if (!instance || !endpoint) {
|
||||||
|
console.error('Missing instance or endpoint.')
|
||||||
|
return Promise.reject('Missing instance or endpoint.')
|
||||||
|
}
|
||||||
|
const headers = { 'Content-Type': 'application/json' }
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
method: body ? 'POST' : 'GET',
|
||||||
|
...customConfig,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...customConfig.headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
config.body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://${instance}/api/v1/${endpoint}${
|
||||||
|
query
|
||||||
|
? `?${Object.keys(query)
|
||||||
|
.map(key => `${key}=${query[key]}`)
|
||||||
|
.join('&')}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
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 (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 })
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit'
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
import timelineReducer from '../screens/timelineSlice'
|
import genericTimelineSlice from 'src/stacks/common/timelineSlice'
|
||||||
|
|
||||||
export default configureStore({
|
export default configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
timeline: timelineReducer
|
'social.xmflsct.com': genericTimelineSlice('social.xmflsct.com').slice
|
||||||
|
.reducer,
|
||||||
|
'm.cmx.im': genericTimelineSlice('m.cmx.im').slice.reducer
|
||||||
},
|
},
|
||||||
middleware: getDefaultMiddleware =>
|
middleware: getDefaultMiddleware =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
75
src/components/TootTimeline.jsx
Normal file
75
src/components/TootTimeline.jsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import { Image, StyleSheet, Text, View } from 'react-native'
|
||||||
|
import HTML from 'react-native-render-html'
|
||||||
|
|
||||||
|
import relativeTime from 'src/utils/relativeTime'
|
||||||
|
|
||||||
|
export default function TootTimeline ({ item }) {
|
||||||
|
return (
|
||||||
|
<View style={styles.tootTimeline}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: item.reblog ? item.reblog.account.avatar : item.account.avatar
|
||||||
|
}}
|
||||||
|
style={styles.avatar}
|
||||||
|
/>
|
||||||
|
<View>
|
||||||
|
<View style={styles.name}>
|
||||||
|
<Text>
|
||||||
|
{item.reblog
|
||||||
|
? item.reblog.account.display_name
|
||||||
|
: item.account.display_name}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
{item.reblog ? item.reblog.account.acct : item.account.acct}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text>{relativeTime(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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TootTimeline.propTypes = {
|
||||||
|
item: PropTypes.shape({
|
||||||
|
account: PropTypes.shape({
|
||||||
|
avatar: PropTypes.string.isRequired,
|
||||||
|
display_name: PropTypes.string.isRequired,
|
||||||
|
acct: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
created_at: PropTypes.string.isRequired,
|
||||||
|
application: PropTypes.exact({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
website: PropTypes.string
|
||||||
|
}),
|
||||||
|
content: PropTypes.string
|
||||||
|
}).isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
tootTimeline: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 15
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
width: 40,
|
||||||
|
height: 40
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
flexDirection: 'row'
|
||||||
|
}
|
||||||
|
})
|
18
src/stacks/MainTimeline.jsx
Normal file
18
src/stacks/MainTimeline.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
|
||||||
|
import Timeline from 'src/stacks/common/Timeline'
|
||||||
|
|
||||||
|
const MainTimelineStack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
function Base () {
|
||||||
|
return <Timeline instance='social.xmflsct.com' endpoint='home' />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MainTimeline () {
|
||||||
|
return (
|
||||||
|
<MainTimelineStack.Navigator>
|
||||||
|
<MainTimelineStack.Screen name='Base' component={Base} />
|
||||||
|
</MainTimelineStack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
22
src/stacks/Me.jsx
Normal file
22
src/stacks/Me.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
|
||||||
|
import Base from './Me/Base'
|
||||||
|
import Authentication from 'src/stacks/Me/Authentication'
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
export default function Me () {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen name='Me-Base' component={Base} />
|
||||||
|
<Stack.Screen
|
||||||
|
name='Me-Authentication'
|
||||||
|
component={Authentication}
|
||||||
|
options={{
|
||||||
|
stackPresentation: 'modal'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
16
src/stacks/Me/Authentication.jsx
Normal file
16
src/stacks/Me/Authentication.jsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
|
||||||
|
import Instance from './Authentication/Instance'
|
||||||
|
import Webview from './Authentication/Webview'
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
export default function Base () {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen name='Me-Authentication-Instance' component={Instance} />
|
||||||
|
<Stack.Screen name='Me-Authentication-Webview' component={Webview} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
31
src/stacks/Me/Authentication/Instance.jsx
Normal file
31
src/stacks/Me/Authentication/Instance.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Button, TextInput, View } from 'react-native'
|
||||||
|
|
||||||
|
export default function Instance ({ navigation }) {
|
||||||
|
const [instance, onChangeInstance] = useState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<TextInput
|
||||||
|
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
|
||||||
|
onChangeText={text => onChangeInstance(text)}
|
||||||
|
value={instance}
|
||||||
|
autoCapitalize='none'
|
||||||
|
autoCorrect={false}
|
||||||
|
clearButtonMode='unless-editing'
|
||||||
|
keyboardType='url'
|
||||||
|
textContentType='URL'
|
||||||
|
placeholder='输入服务器'
|
||||||
|
placeholderTextColor='#888888'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title='登录'
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate('Me-Authentication-Webview', {
|
||||||
|
instance: instance
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
9
src/stacks/Me/Authentication/Webview.jsx
Normal file
9
src/stacks/Me/Authentication/Webview.jsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Button, View } from 'react-native'
|
||||||
|
import * as AppAuth from 'expo-app-auth'
|
||||||
|
|
||||||
|
export default function Webview ({ navigation, route }) {
|
||||||
|
const { instance } = route.params
|
||||||
|
|
||||||
|
return <View></View>
|
||||||
|
}
|
10
src/stacks/Me/Base.jsx
Normal file
10
src/stacks/Me/Base.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Button, View } from 'react-native'
|
||||||
|
|
||||||
|
export default function Base ({ navigation: { navigate } }) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Button title='登录' onPress={() => navigate('Me-Authentication')} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
9
src/stacks/Notifications.jsx
Normal file
9
src/stacks/Notifications.jsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { ActivityIndicator, FlatList, View } from 'react-native'
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
import TootTimeline from 'src/components/TootTimeline'
|
||||||
|
|
||||||
|
export default function Notifications () {
|
||||||
|
return <View></View>
|
||||||
|
}
|
20
src/stacks/PublicTimeline.jsx
Normal file
20
src/stacks/PublicTimeline.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
|
||||||
|
import Timeline from 'src/stacks/common/Timeline'
|
||||||
|
|
||||||
|
const PublicTimelineStack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
function Base () {
|
||||||
|
return <Timeline instance='social.xmflsct.com' endpoint='public' local />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PublicTimeline () {
|
||||||
|
return (
|
||||||
|
<PublicTimelineStack.Navigator>
|
||||||
|
<PublicTimelineStack.Screen name='Base' component={Base} />
|
||||||
|
</PublicTimelineStack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store by page maybe
|
66
src/stacks/common/Timeline.jsx
Normal file
66
src/stacks/common/Timeline.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { ActivityIndicator, FlatList, View } from 'react-native'
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
import TootTimeline from 'src/components/TootTimeline'
|
||||||
|
|
||||||
|
import genericTimelineSlice from './timelineSlice'
|
||||||
|
|
||||||
|
export default function Timeline ({ instance, endpoint, local }) {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const toots = useSelector(genericTimelineSlice(instance).getToots)
|
||||||
|
const status = useSelector(genericTimelineSlice(instance).getStatus)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'idle') {
|
||||||
|
dispatch(genericTimelineSlice(instance).fetch({ endpoint, local }))
|
||||||
|
}
|
||||||
|
}, [status, dispatch])
|
||||||
|
|
||||||
|
let content
|
||||||
|
|
||||||
|
if (status === 'error') {
|
||||||
|
content = <Text>Error message</Text>
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
<FlatList
|
||||||
|
data={toots}
|
||||||
|
keyExtractor={({ id }) => id}
|
||||||
|
renderItem={TootTimeline}
|
||||||
|
onRefresh={() =>
|
||||||
|
dispatch(
|
||||||
|
genericTimelineSlice(instance).fetch({
|
||||||
|
endpoint,
|
||||||
|
local,
|
||||||
|
id: toots[0].id,
|
||||||
|
newer: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
refreshing={status === 'loading'}
|
||||||
|
onEndReached={() =>
|
||||||
|
dispatch(
|
||||||
|
genericTimelineSlice(instance).fetch({
|
||||||
|
endpoint,
|
||||||
|
local,
|
||||||
|
id: toots[toots.length - 1].id
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
/>
|
||||||
|
{status === 'loading' && <ActivityIndicator />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <View>{content}</View>
|
||||||
|
}
|
||||||
|
|
||||||
|
Timeline.propTypes = {
|
||||||
|
instance: PropTypes.string.isRequired,
|
||||||
|
public: PropTypes.bool
|
||||||
|
}
|
77
src/stacks/common/timelineSlice.js
Normal file
77
src/stacks/common/timelineSlice.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
import { client } from 'src/api/client'
|
||||||
|
import * as localStorage from 'src/utils/localStorage'
|
||||||
|
|
||||||
|
export default function genericTimelineSlice (instance) {
|
||||||
|
const fetch = createAsyncThunk(
|
||||||
|
'timeline/fetch',
|
||||||
|
async ({ endpoint, local, id, newer }) => {
|
||||||
|
if (!instance || !endpoint) console.error('Missing instance or endpoint.')
|
||||||
|
|
||||||
|
let query = {}
|
||||||
|
if (local) query.local = 'true'
|
||||||
|
if (newer) {
|
||||||
|
query.since_id = id
|
||||||
|
} else {
|
||||||
|
if (id) {
|
||||||
|
query.max_id = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let header
|
||||||
|
const instanceToken = await localStorage.getItem()
|
||||||
|
if (instanceToken) {
|
||||||
|
header = { headers: { Authorization: `Bearer ${instanceToken}` } }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: await client.get(
|
||||||
|
instance,
|
||||||
|
`timelines/${endpoint}`,
|
||||||
|
query,
|
||||||
|
header
|
||||||
|
),
|
||||||
|
newer: newer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const slice = createSlice({
|
||||||
|
name: instance,
|
||||||
|
initialState: {
|
||||||
|
toots: [],
|
||||||
|
status: 'idle',
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
extraReducers: {
|
||||||
|
[fetch.pending]: state => {
|
||||||
|
state.status = 'loading'
|
||||||
|
},
|
||||||
|
[fetch.fulfilled]: (state, action) => {
|
||||||
|
state.status = 'succeeded'
|
||||||
|
action.payload.newer
|
||||||
|
? state.toots.unshift(...action.payload.data)
|
||||||
|
: state.toots.push(...action.payload.data)
|
||||||
|
},
|
||||||
|
[fetch.rejected]: (state, action) => {
|
||||||
|
state.status = 'failed'
|
||||||
|
state.error = action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
getToots = state => state[instance].toots
|
||||||
|
getStatus = state => state[instance].status
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetch,
|
||||||
|
slice,
|
||||||
|
getToots,
|
||||||
|
getStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const timelineSlice = genericTimelineSlice(data)
|
||||||
|
|
||||||
|
// export default timelineSlice.reducer
|
24
src/utils/localStorage.js
Normal file
24
src/utils/localStorage.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
|
|
||||||
|
export async function getItem () {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem('@social.xmflsct.com')
|
||||||
|
if (!value) {
|
||||||
|
await AsyncStorage.setItem(
|
||||||
|
'@social.xmflsct.com',
|
||||||
|
'qjzJ0IjvZ1apsn0_wBkGcdjKgX7Dao9KEPhGwggPwAo'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Get token error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllKeys () {
|
||||||
|
try {
|
||||||
|
return await AsyncStorage.getAllKeys()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Get all keys error')
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
export default function relative_time (date) {
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function relativeTime (date) {
|
||||||
var units = {
|
var units = {
|
||||||
year: 24 * 60 * 60 * 1000 * 365,
|
year: 24 * 60 * 60 * 1000 * 365,
|
||||||
month: (24 * 60 * 60 * 1000 * 365) / 12,
|
month: (24 * 60 * 60 * 1000 * 365) / 12,
|
||||||
@ -17,3 +19,7 @@ export default function relative_time (date) {
|
|||||||
if (Math.abs(elapsed) > units[u] || u == 'second')
|
if (Math.abs(elapsed) > units[u] || u == 'second')
|
||||||
return rtf.format(Math.round(elapsed / units[u]), u)
|
return rtf.format(Math.round(elapsed / units[u]), u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relativeTime.propTypes = {
|
||||||
|
date: PropTypes.string.isRequired
|
||||||
|
}
|
Reference in New Issue
Block a user