diff --git a/.gitignore b/.gitignore
index eb0c8957..6dacb2f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ web-build/
# macOS
.DS_Store
+
+.env
\ No newline at end of file
diff --git a/App.jsx b/App.jsx
index f46bb2bb..fdd93fb5 100644
--- a/App.jsx
+++ b/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 store from './app/store'
-import { Provider } from 'react-redux'
-import { StatusBar } from 'expo-status-bar'
+import Main from './src/Main'
-import ScreenTimeline from './screens/Timeline'
+const App = () =>
-const Stack = createStackNavigator()
-
-export default function App () {
- return (
-
-
-
-
-
-
-
-
- )
-}
+export default App
diff --git a/api/client.js b/api/client.js
deleted file mode 100644
index bc2b7318..00000000
--- a/api/client.js
+++ /dev/null
@@ -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 })
-}
diff --git a/app.json b/app.json
index d6edcc16..7c197d2a 100644
--- a/app.json
+++ b/app.json
@@ -2,6 +2,7 @@
"expo": {
"name": "mastodon-app",
"slug": "mastodon-app",
+ "scheme": "mastodonct",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
@@ -17,10 +18,7 @@
"**/*"
],
"ios": {
- "supportsTablet": true
- },
- "web": {
- "favicon": "./assets/favicon.png"
+ "supportsTablet": false
}
}
-}
+}
\ No newline at end of file
diff --git a/babel.config.js b/babel.config.js
index 2900afe9..5cc1c280 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,6 +1,16 @@
-module.exports = function(api) {
- api.cache(true);
+module.exports = function (api) {
+ api.cache(true)
return {
presets: ['babel-preset-expo'],
- };
-};
+ plugins: [
+ [
+ 'module-resolver',
+ {
+ alias: {
+ src: './src'
+ }
+ }
+ ]
+ ]
+ }
+}
diff --git a/components/TootTimeline.jsx b/components/TootTimeline.jsx
deleted file mode 100644
index 8e644993..00000000
--- a/components/TootTimeline.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
- {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 710173de..f637934a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
"version": "4.9.0",
"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",
"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": {
"version": "5.12.5",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-5.12.5.tgz",
@@ -2021,15 +2038,16 @@
}
},
"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==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.0.0.tgz",
+ "integrity": "sha512-3pdEq3PXALilSJ6dnC4wMWr0AZixHRM4utpdpBR9g5QG7B7JwWyukQv7a9hVxkbGFl+nQbrHDqqQOIBtTXTP/Q==",
+ "dev": true,
"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-babel-config": "^1.2.0",
+ "glob": "^7.1.6",
+ "pkg-up": "^3.1.0",
+ "reselect": "^4.0.0",
+ "resolve": "^1.13.1"
}
},
"babel-plugin-react-native-web": {
@@ -2052,6 +2070,71 @@
"babel-plugin-module-resolver": "^3.2.0",
"babel-plugin-react-native-web": "~0.13.6",
"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": {
@@ -3128,6 +3211,14 @@
"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": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-8.2.0.tgz",
@@ -3536,11 +3627,12 @@
}
},
"find-up": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
- "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
"requires": {
- "locate-path": "^2.0.0"
+ "locate-path": "^3.0.0"
}
},
"fontfaceobserver": {
@@ -4494,11 +4586,12 @@
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
},
"locate-path": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
- "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
"requires": {
- "p-locate": "^2.0.0",
+ "p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
@@ -5556,9 +5649,9 @@
"optional": true
},
"nanoid": {
- "version": "3.1.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz",
- "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A=="
+ "version": "3.1.13",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.13.tgz",
+ "integrity": "sha512-oYL7jWZUdScASxYOrcwE8EvISFGzO3/1g+t56vCyR0s2nrpmBcOc7hTAFJaVf6HMyEPJrnNelnjRnMN6KZnCPA=="
},
"nanomatch": {
"version": "1.2.13",
@@ -5798,25 +5891,28 @@
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-limit": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
- "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
"requires": {
- "p-try": "^1.0.0"
+ "p-try": "^2.0.0"
}
},
"p-locate": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
- "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
"requires": {
- "p-limit": "^1.1.0"
+ "p-limit": "^2.0.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="
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
},
"parse-json": {
"version": "4.0.0",
@@ -5929,11 +6025,12 @@
}
},
"pkg-up": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
- "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
+ "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
+ "dev": true,
"requires": {
- "find-up": "^2.1.0"
+ "find-up": "^3.0.0"
}
},
"plist": {
@@ -6773,9 +6870,10 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"reselect": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
- "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
+ "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==",
+ "dev": true
},
"resolve": {
"version": "1.18.1",
diff --git a/package.json b/package.json
index d434c108..83bc88a3 100644
--- a/package.json
+++ b/package.json
@@ -8,12 +8,16 @@
"eject": "expo eject"
},
"dependencies": {
+ "@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
+ "@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-status-bar": "~1.0.2",
+ "prop-types": "^15.7.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",
@@ -27,7 +31,8 @@
"react-redux": "^7.2.1"
},
"devDependencies": {
- "@babel/core": "~7.9.0"
+ "@babel/core": "~7.9.0",
+ "babel-plugin-module-resolver": "^4.0.0"
},
"private": true
}
diff --git a/screens/Timeline.jsx b/screens/Timeline.jsx
deleted file mode 100644
index 683edaff..00000000
--- a/screens/Timeline.jsx
+++ /dev/null
@@ -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 = {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
deleted file mode 100644
index db5646a6..00000000
--- a/screens/timelineSlice.js
+++ /dev/null
@@ -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
diff --git a/src/Main.jsx b/src/Main.jsx
new file mode 100644
index 00000000..0fba7890
--- /dev/null
+++ b/src/Main.jsx
@@ -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 (
+ <>
+
+
+
+
+
+
+ {/* */}
+
+
+
+
+ >
+ )
+}
diff --git a/src/api/client.js b/src/api/client.js
new file mode 100644
index 00000000..bd744518
--- /dev/null
+++ b/src/api/client.js
@@ -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 })
+}
diff --git a/app/store.js b/src/app/store.js
similarity index 51%
rename from app/store.js
rename to src/app/store.js
index f163eb98..c5335808 100644
--- a/app/store.js
+++ b/src/app/store.js
@@ -1,10 +1,12 @@
import { configureStore } from '@reduxjs/toolkit'
-import timelineReducer from '../screens/timelineSlice'
+import genericTimelineSlice from 'src/stacks/common/timelineSlice'
export default configureStore({
reducer: {
- timeline: timelineReducer
+ 'social.xmflsct.com': genericTimelineSlice('social.xmflsct.com').slice
+ .reducer,
+ 'm.cmx.im': genericTimelineSlice('m.cmx.im').slice.reducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
diff --git a/src/components/TootTimeline.jsx b/src/components/TootTimeline.jsx
new file mode 100644
index 00000000..4e15c8e5
--- /dev/null
+++ b/src/components/TootTimeline.jsx
@@ -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 (
+
+
+
+
+
+
+ {item.reblog
+ ? item.reblog.account.display_name
+ : item.account.display_name}
+
+
+ {item.reblog ? item.reblog.account.acct : item.account.acct}
+
+
+
+ {relativeTime(item.created_at)}
+ {item.application && item.application.name !== 'Web' && (
+ Linking.openURL(item.application.website)}>
+ {item.application.name}
+
+ )}
+
+
+
+ {item.content ? : <>>}
+
+ )
+}
+
+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'
+ }
+})
diff --git a/src/stacks/MainTimeline.jsx b/src/stacks/MainTimeline.jsx
new file mode 100644
index 00000000..f35015a6
--- /dev/null
+++ b/src/stacks/MainTimeline.jsx
@@ -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
+}
+
+export default function MainTimeline () {
+ return (
+
+
+
+ )
+}
diff --git a/src/stacks/Me.jsx b/src/stacks/Me.jsx
new file mode 100644
index 00000000..63a2ddab
--- /dev/null
+++ b/src/stacks/Me.jsx
@@ -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 (
+
+
+
+
+ )
+}
diff --git a/src/stacks/Me/Authentication.jsx b/src/stacks/Me/Authentication.jsx
new file mode 100644
index 00000000..3685ae11
--- /dev/null
+++ b/src/stacks/Me/Authentication.jsx
@@ -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 (
+
+
+
+
+ )
+}
diff --git a/src/stacks/Me/Authentication/Instance.jsx b/src/stacks/Me/Authentication/Instance.jsx
new file mode 100644
index 00000000..e4c653a3
--- /dev/null
+++ b/src/stacks/Me/Authentication/Instance.jsx
@@ -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 (
+
+ onChangeInstance(text)}
+ value={instance}
+ autoCapitalize='none'
+ autoCorrect={false}
+ clearButtonMode='unless-editing'
+ keyboardType='url'
+ textContentType='URL'
+ placeholder='输入服务器'
+ placeholderTextColor='#888888'
+ />
+
+ )
+}
diff --git a/src/stacks/Me/Authentication/Webview.jsx b/src/stacks/Me/Authentication/Webview.jsx
new file mode 100644
index 00000000..2a15b270
--- /dev/null
+++ b/src/stacks/Me/Authentication/Webview.jsx
@@ -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
+}
diff --git a/src/stacks/Me/Base.jsx b/src/stacks/Me/Base.jsx
new file mode 100644
index 00000000..a483da6c
--- /dev/null
+++ b/src/stacks/Me/Base.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import { Button, View } from 'react-native'
+
+export default function Base ({ navigation: { navigate } }) {
+ return (
+
+
+ )
+}
diff --git a/src/stacks/Notifications.jsx b/src/stacks/Notifications.jsx
new file mode 100644
index 00000000..e0ee503d
--- /dev/null
+++ b/src/stacks/Notifications.jsx
@@ -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
+}
diff --git a/src/stacks/PublicTimeline.jsx b/src/stacks/PublicTimeline.jsx
new file mode 100644
index 00000000..38b3e463
--- /dev/null
+++ b/src/stacks/PublicTimeline.jsx
@@ -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
+}
+
+export default function PublicTimeline () {
+ return (
+
+
+
+ )
+}
+
+// store by page maybe
diff --git a/src/stacks/common/Timeline.jsx b/src/stacks/common/Timeline.jsx
new file mode 100644
index 00000000..2b4675c9
--- /dev/null
+++ b/src/stacks/common/Timeline.jsx
@@ -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 = Error message
+ } else {
+ content = (
+ <>
+ 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' && }
+ >
+ )
+ }
+
+ return {content}
+}
+
+Timeline.propTypes = {
+ instance: PropTypes.string.isRequired,
+ public: PropTypes.bool
+}
diff --git a/src/stacks/common/timelineSlice.js b/src/stacks/common/timelineSlice.js
new file mode 100644
index 00000000..506ae9fa
--- /dev/null
+++ b/src/stacks/common/timelineSlice.js
@@ -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
diff --git a/src/utils/localStorage.js b/src/utils/localStorage.js
new file mode 100644
index 00000000..c0195a3e
--- /dev/null
+++ b/src/utils/localStorage.js
@@ -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')
+ }
+}
diff --git a/utils/relative-time.js b/src/utils/relativeTime.js
similarity index 77%
rename from utils/relative-time.js
rename to src/utils/relativeTime.js
index b658ada4..5429307c 100644
--- a/utils/relative-time.js
+++ b/src/utils/relativeTime.js
@@ -1,4 +1,6 @@
-export default function relative_time (date) {
+import PropTypes from 'prop-types'
+
+export default function relativeTime (date) {
var units = {
year: 24 * 60 * 60 * 1000 * 365,
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')
return rtf.format(Math.round(elapsed / units[u]), u)
}
+
+relativeTime.propTypes = {
+ date: PropTypes.string.isRequired
+}