1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Setup of single store works for multiple timeline

This commit is contained in:
Zhiyuan Zheng
2020-10-24 02:47:57 +02:00
parent 83f6039ade
commit cda67d23f6
16 changed files with 334 additions and 330 deletions

View File

@ -5,31 +5,29 @@ import { enableScreens } from 'react-native-screens'
enableScreens()
import React from 'react'
import store from './app/store'
import store from 'src/stacks/common/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 Main from 'src/stacks/Main'
import Public from 'src/stacks/Public'
import Notifications from 'src/stacks/Notifications'
import Me from 'src/stacks/Me'
const Tab = createBottomTabNavigator()
export default function Main () {
export default function Index () {
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='Main' component={Main} />
<Tab.Screen name='Public' component={Public} />
{/* <Tab.Screen name='Notifications' component={Notifications} /> */}
<Tab.Screen name='Me' component={Me} />
</Tab.Navigator>
</NavigationContainer>
</Provider>
</>
)
}

View File

@ -1,12 +1,6 @@
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.')
export async function client (url, query, { body, ...customConfig } = {}) {
if (!url) {
return Promise.reject('Missing URL.')
}
const headers = { 'Content-Type': 'application/json' }
@ -25,8 +19,8 @@ export async function client (
let data
try {
const response = await fetch(
`https://${instance}/api/v1/${endpoint}${
query
`https://${url}${
Object.keys(query).length
? `?${Object.keys(query)
.map(key => `${key}=${query[key]}`)
.join('&')}`

View File

@ -1,16 +0,0 @@
import { configureStore } from '@reduxjs/toolkit'
import genericTimelineSlice from 'src/stacks/common/timelineSlice'
export default configureStore({
reducer: {
'social.xmflsct.com': genericTimelineSlice('social.xmflsct.com').slice
.reducer,
'm.cmx.im': genericTimelineSlice('m.cmx.im').slice.reducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false
})
})

14
src/stacks/Main.jsx Normal file
View File

@ -0,0 +1,14 @@
import React from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import Following from 'src/stacks/Main/Following'
const MainStack = createNativeStackNavigator()
export default function Main () {
return (
<MainStack.Navigator>
<MainStack.Screen name='Following' component={Following} />
</MainStack.Navigator>
)
}

View File

@ -0,0 +1,7 @@
import React from 'react'
import Timeline from 'src/stacks/common/Timeline'
export default function Following () {
return <Timeline endpoint='home' />
}

View File

@ -1,18 +0,0 @@
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>
)
}

14
src/stacks/Public.jsx Normal file
View File

@ -0,0 +1,14 @@
import React from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import CurrentPublic from 'src/stacks/Public/CurrentPublic'
const PublicStack = createNativeStackNavigator()
export default function PublicTimeline () {
return (
<PublicStack.Navigator>
<PublicStack.Screen name='CurrentPublic' component={CurrentPublic} />
</PublicStack.Navigator>
)
}

View File

@ -0,0 +1,7 @@
import React from 'react'
import Timeline from 'src/stacks/common/Timeline'
export default function CurrentPublic () {
return <Timeline endpoint='public' local />
}

View File

@ -1,20 +0,0 @@
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

View File

@ -1,23 +1,25 @@
import PropTypes from 'prop-types'
import React, { useEffect } from 'react'
import { ActivityIndicator, FlatList, View } from 'react-native'
import { useSelector, useDispatch } from 'react-redux'
import { connect, useSelector, useDispatch } from 'react-redux'
import TootTimeline from 'src/components/TootTimeline'
import { fetch, getToots, getStatus } from './timelineSlice'
import genericTimelineSlice from './timelineSlice'
export default function Timeline ({ instance, endpoint, local }) {
export default function Timeline ({ remote, endpoint, local }) {
const dispatch = useDispatch()
const toots = useSelector(genericTimelineSlice(instance).getToots)
const status = useSelector(genericTimelineSlice(instance).getStatus)
const toots = useSelector(state =>
getToots(state)({ remote, endpoint, local })
)
const status = useSelector(state =>
getStatus(state)({ remote, endpoint, local })
)
useEffect(() => {
if (status === 'idle') {
dispatch(genericTimelineSlice(instance).fetch({ endpoint, local }))
dispatch(fetch({ remote, endpoint, local }))
}
}, [status, dispatch])
let content
if (status === 'error') {
@ -31,22 +33,13 @@ export default function Timeline ({ instance, endpoint, local }) {
renderItem={TootTimeline}
onRefresh={() =>
dispatch(
genericTimelineSlice(instance).fetch({
endpoint,
local,
id: toots[0].id,
newer: true
})
fetch({ remote, endpoint, local, id: toots[0].id, newer: true })
)
}
refreshing={status === 'loading'}
onEndReached={() =>
dispatch(
genericTimelineSlice(instance).fetch({
endpoint,
local,
id: toots[toots.length - 1].id
})
fetch({ remote, endpoint, local, id: toots[toots.length - 1].id })
)
}
onEndReachedThreshold={0.5}
@ -61,6 +54,7 @@ export default function Timeline ({ instance, endpoint, local }) {
}
Timeline.propTypes = {
instance: PropTypes.string.isRequired,
public: PropTypes.bool
remote: PropTypes.bool,
endpoint: PropTypes.string.isRequired,
local: PropTypes.bool
}

View File

@ -0,0 +1,24 @@
import { createSlice } from '@reduxjs/toolkit'
const instanceInfoSlice = createSlice({
name: 'instanceInfo',
initialState: {},
reducers: {
// increment (state) {
// state.value++
// },
// decrement (state) {
// state.value--
// },
// incrementByAmount (state, action) {
// state.value += action.payload
// }
}
})
export const getCurrent = state => state.current
export const getCurrentToken = state => state.currentToken
export const getRemote = state => state.remote
// export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default instanceInfoSlice.reducer

View File

@ -0,0 +1,30 @@
import { configureStore } from '@reduxjs/toolkit'
import instanceInfoSlice from 'src/stacks/common/instanceInfoSlice'
import timelineSlice from 'src/stacks/common/timelineSlice'
// get site information from local storage and pass to reducers
const preloadedState = {
instanceInfo: {
current: 'social.xmflsct.com',
currentToken: 'qjzJ0IjvZ1apsn0_wBkGcdjKgX7Dao9KEPhGwggPwAo',
remote: 'm.cmx.im'
}
}
const reducer = {
instanceInfo: instanceInfoSlice,
timelines: timelineSlice
}
// const middleware = getDefaultMiddleware =>
// getDefaultMiddleware({
// immutableCheck: false,
// serializableCheck: false
// })
export default configureStore({
preloadedState,
reducer,
// middleware
})

View File

@ -1,77 +1,82 @@
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.')
// Naming convention
// Following: home
// Local: home/local
// CurrentPublic: public/local
// RemotePublic: remote
let query = {}
if (local) query.local = 'true'
if (newer) {
query.since_id = id
} else {
if (id) {
query.max_id = id
const checkInstance = ({ remote, endpoint, local }) =>
remote ? 'remote' : `${endpoint}${local ? '/local' : ''}`
export const fetch = createAsyncThunk(
'timeline/fetch',
async ({ remote, endpoint, local, id, newer }, { getState }) => {
if (!endpoint) console.error('Missing endpoint')
const instance = remote
? `${getState().instanceInfo.remote}/api/v1/timelines/public`
: `${getState().instanceInfo.current}/api/v1/timelines/${endpoint}`
const query = {
...(local && { local: 'true' }),
...(newer ? { since_id: id } : id && { max_id: id })
}
const header = {
...(getState().instanceInfo.currentToken && {
headers: {
Authorization: `Bearer ${getState().instanceInfo.currentToken}`
}
}
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
return await client.get(instance, query, header)
}
}
)
// export const timelineSlice = genericTimelineSlice(data)
export const timelineSlice = createSlice({
name: 'timeline',
initialState: {
home: {
toots: [],
status: 'idle'
},
'home/local': {
toots: [],
status: 'idle'
},
'public/local': {
toots: [],
status: 'idle'
},
remote: {
toots: [],
status: 'idle'
}
},
extraReducers: {
[fetch.pending]: (state, action) => {
state[checkInstance(action.meta.arg)].status = 'loading'
},
[fetch.fulfilled]: (state, action) => {
state[checkInstance(action.meta.arg)].status = 'succeeded'
action.meta.arg.newer
? state[checkInstance(action.meta.arg)].toots.unshift(...action.payload)
: state[checkInstance(action.meta.arg)].toots.push(...action.payload)
},
[fetch.rejected]: (state, action) => {
console.error(action.error.message)
state[checkInstance(action.meta.arg)].status = 'failed'
}
}
})
// export default timelineSlice.reducer
export const getToots = state => instance =>
state.timelines[checkInstance(instance)].toots
export const getStatus = state => instance =>
state.timelines[checkInstance(instance)].status
export default timelineSlice.reducer