Commit 86784835 authored by Cyrille Bollu's avatar Cyrille Bollu
Browse files

Merge branch 'enhancement/32/remember-last-visited-board-and-stack' into 'master'

Remember last visited board and stack upon app's opening

See merge request !9
parents 448fc9b3 98a6c72e
Pipeline #456377 passed with stage
in 52 seconds
......@@ -35,14 +35,17 @@ class App extends React.Component {
}
constructor(props) {
console.log('initialising app')
super(props)
this.state = {
fontsLoaded: false,
colorScheme: 'light',
navigation: {},
}
// Force portrait mode on iPhones
if (Device.modelId && Device.modelId.startsWith('iPhone')) {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP)
......@@ -53,15 +56,26 @@ class App extends React.Component {
}
componentDidMount() {
this.loadFonts()
// get initial theme
this.setState({ colorScheme: Appearance.getColorScheme()})
this.props.setTheme(Appearance.getColorScheme());
// register theme change subscription
this._schemeSubscription = Appearance.addChangeListener(({ colorScheme }) => {
this.setState({ colorScheme })
this.props.setTheme(colorScheme);
});
// Retrieves last viewed board and stack if available
AsyncStorage.getItem('navigation').then(navigation => {
navigation = navigation != null ? JSON.parse(navigation) : { boardId: null, stackId: null }
console.log('Retrieved last navigated board+stack')
this.setState({navigation})
})
// Retrieve token from storage if available
if (!env.expoDebug) {
AsyncStorage.getItem('NCtoken').then(token => {
......@@ -83,6 +97,7 @@ class App extends React.Component {
this.props.setToken(env.token)
this.props.setServer(env.server)
}
}
// Function to retrieve the device's token and save it after user logged in
......@@ -101,7 +116,7 @@ class App extends React.Component {
}
render() {
if(this.state.fontsLoaded) {
if(this.state.fontsLoaded && Object.keys(this.state.navigation).length !== 0) {
if (this.props.token.value === null || this.props.server.value === null) {
// No token is stored yet, we need to get one
return (
......@@ -116,8 +131,11 @@ class App extends React.Component {
return (
<KeyboardAvoidingView style={{ flex: 1 }} behavior='padding'>
<NavigationContainer theme={this.state.colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack.Navigator screenOptions={({navigation}) => {return {detachPreviousScreen: !navigation.isFocused()}}}>
<Stack.Screen name="AllBoards" component={AllBoards} />
<Stack.Navigator
screenOptions={({navigation}) => {return {detachPreviousScreen: !navigation.isFocused()}}}
initialRouteName='AllBoards'
>
<Stack.Screen name="AllBoards" component={AllBoards} initialParams={{navigation: this.state.navigation}}/>
<Stack.Screen name="BoardDetails" component={BoardDetails} />
<Stack.Screen name="CardDetails" component={Card} />
<Stack.Screen name="NewCard" component={Card} />
......@@ -135,9 +153,9 @@ class App extends React.Component {
// Connect to store
const mapStateToProps = state => ({
token: state.token,
server: state.server,
theme: state.theme,
token: state.token,
})
const mapDispatchToProps = dispatch => (
bindActionCreators( {
......
......@@ -16,28 +16,40 @@ import Board from './Board';
class AllBoards extends React.Component {
constructor(props) {
super(props)
this.state = {
creatingBoard: false,
lastRefresh: new Date(0),
refreshing: false,
newBoardName: '',
}
this.loadBoards = this.loadBoards.bind(this);
this.insets = initialWindowMetrics?.insets ?? {
left: 0,
right: 0,
bottom: 0,
top: 0,
}
}
super(props)
this.state = {
creatingBoard: false,
lastRefresh: new Date(0),
refreshing: false,
newBoardName: '',
}
this.loadBoards = this.loadBoards.bind(this);
this.insets = initialWindowMetrics?.insets ?? {
left: 0,
right: 0,
bottom: 0,
top: 0,
}
}
componentDidMount() {
this.loadBoards()
async componentDidMount() {
this.props.navigation.setOptions({
headerTitle: 'All Boards',
headerRight: () => (<AppMenu navigation={this.props.navigation} setServer={this.props.setServer} setToken={this.props.setToken} />)
}, [this.props.navigation, this.props.setServer, this.props.setToken])
await this.loadBoards()
// Navigate to last viewed board+stack on initial app load
if (this.props.route.params.navigation.boardId !== null) {
console.log('Initial app loading, navigating to last viewed board+stack')
this.props.navigation.navigate('BoardDetails', {
boardId: this.props.route.params.navigation.boardId,
stackId: this.props.route.params.navigation.stackId,
})
}
}
render() {
......@@ -122,12 +134,13 @@ class AllBoards extends React.Component {
})
}
loadBoards() {
// Get all user boards
// Gets all user boards
async loadBoards() {
console.log('Retrieving boards from server')
this.setState({
refreshing: true
})
axios.get(this.props.server.value + '/index.php/apps/deck/api/v1.0/boards', {
await axios.get(this.props.server.value + '/index.php/apps/deck/api/v1.0/boards', {
headers: {
'Content-Type': 'application/json',
'Authorization': this.props.token.value,
......
import React from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ActionSheetIOS, Pressable, View, Text, TextInput } from 'react-native';
import { initialWindowMetrics } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
......@@ -30,9 +31,15 @@ class Board extends React.Component {
<Pressable
onPress={() => {
// Opens board's details page
console.log(`navigating to board ${this.props.board.id}`)
this.props.navigation.navigate('BoardDetails',{
boardId: this.props.board.id
boardId: this.props.board.id,
stackId: null,
})
AsyncStorage.setItem('navigation', JSON.stringify({
boardId: this.props.board.id,
stackId: null,
}));
}}
onLongPress={() => {
// Context menu
......
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { addStack, deleteCard, moveCard } from '../store/boardSlice';
import { setServer } from '../store/serverSlice';
import { setToken } from '../store/tokenSlice';
......@@ -9,6 +10,7 @@ import { ActionSheetIOS, Pressable, RefreshControl, Text, TextInput, View } from
import { DraxProvider, DraxScrollView, DraxView } from 'react-native-drax';
import axios from 'axios';
import { initialWindowMetrics } from 'react-native-safe-area-context';
import { HeaderBackButton } from '@react-navigation/elements';
import { i18n } from '../i18n/i18n.js';
// Component that display a board's cards, grouped by stack
......@@ -33,11 +35,6 @@ class BoardDetails extends React.Component {
}
}
// Function to change the displayed stack
_handleIndexChange(index) {
this.setState({ index })
}
// Function to detect long press on card and open a context menu
cardPressedDown(id) {
this.setState({cardPressed: id})
......@@ -66,19 +63,34 @@ class BoardDetails extends React.Component {
}, 500)
}
// Gets the board's details from the server and setup the page's header bar
async componentDidMount() {
// Setup page's header bar
this.props.navigation.setOptions({
headerTitle: 'Board details',
headerRight: () => (<AppMenu/>)
headerRight: () => (<AppMenu/>),
headerLeft: () => (
<HeaderBackButton
label = 'All boards'
labelVisible = {true}
onPress = {() => {
AsyncStorage.removeItem('navigation')
this.props.navigation.navigate('AllBoards')
}}
/>
)
})
await this.loadBoard()
// Shows stack with order === 0, if stacks are available
if (this.props.boards.value[this.props.route.params.boardId].stacks?.length) {
// Gets board details if not yet done
if (this.props.boards.value[this.props.route.params.boardId].stacks.length === 0) {
await this.loadBoard()
} else {
// Navigates to stack with order === 0
this.setState({
index: this.props.boards.value[this.props.route.params.boardId].stacks[0].id,
index: this.props.boards.value[this.props.route.params.boardId].stacks[0].id,
})
}
}
render() {
......@@ -139,7 +151,7 @@ class BoardDetails extends React.Component {
onReceiveDragDrop={({ dragged: { payload } }) => {
// Don't try to move card when the drop stack is the same
if (stack.id !== payload.stackId) {
console.log(`moving card ${payload.id}`);
console.log(`moving card ${payload.id}`)
this.moveCard(payload.id, stack.id)
}
}}
......@@ -147,10 +159,15 @@ class BoardDetails extends React.Component {
<Pressable
key={stack.id}
onPress={() => {
// Switches to selected stack
console.log(`Navigating to stack ${stack.id}`)
// Switches to selected stack and remember navigation
this.setState({
index: stack.id
index: stack.id,
})
AsyncStorage.setItem('navigation', JSON.stringify({
boardId: this.props.route.params.boardId,
stackId: stack.id,
}))
}}
>
<Text style={[this.props.theme.stackTabText, this.state.index === stack.id ? this.props.theme.stackTabTextSelected : this.props.theme.stackTabTextNormal]}>
......@@ -256,29 +273,44 @@ class BoardDetails extends React.Component {
})
}
loadBoard() {
async loadBoard() {
console.log('Retrieving board details from server')
this.setState({
refreshing: true
})
axios.get(this.props.server.value + `/index.php/apps/deck/api/v1.0/boards/${this.props.route.params.boardId}/stacks`, {
await axios.get(this.props.server.value + `/index.php/apps/deck/api/v1.0/boards/${this.props.route.params.boardId}/stacks`, {
headers: {
'Content-Type': 'application/json',
'Authorization': this.props.token.value
}
})
.then((resp) => {
}).then((resp) => {
// TODO check for error
console.log('board details retrieved from server')
this.setState({
refreshing: false
})
console.log('cards retrieved from server')
// TODO check for error
// Update board's details in store
resp.data.forEach(stack => {
this.props.addStack({
boardId: this.props.route.params.boardId,
stack
})
})
// Shows last visited stack or stack with order === 0 (assumes server's answer is ordered)
// TODO: handle case where the remembered stackId has been deleted
if (resp.data.length > 0) {
this.setState({
index: this.props.route.params.stackId !== null ? parseInt(this.props.route.params.stackId) : resp.data[0].id,
})
}
})
}
moveCard(cardId, stackId) {
......@@ -348,7 +380,7 @@ const mapStateToProps = state => ({
boards: state.boards,
server: state.server,
theme: state.theme,
token: state.token
token: state.token,
})
const mapDispatchToProps = dispatch => (
......@@ -357,11 +389,11 @@ const mapDispatchToProps = dispatch => (
deleteCard,
moveCard,
setServer,
setToken
setToken,
}, dispatch)
)
export default connect(
mapStateToProps,
mapDispatchToProps
mapDispatchToProps,
)(BoardDetails)
\ No newline at end of file
This diff is collapsed.
......@@ -37,7 +37,6 @@ export const boardSlice = createSlice({
},
deleteAllBoards: (state) => {
state.value = {}
console.log('All boards deleted from store')
},
deleteBoard: (state, action) => {
delete state.value[action.payload.boardId]
......@@ -45,7 +44,6 @@ export const boardSlice = createSlice({
},
deleteCard: (state, action) => {
delete state.value[action.payload.boardId].stacks.find(oneStack => oneStack.id === action.payload.stackId).cards[action.payload.cardId]
console.log('card ' + action.payload.cardId + ' deleted from store')
},
moveCard: (state, action) => {
const card = state.value[action.payload.boardId].stacks.find(oneStack => oneStack.id === action.payload.oldStackId)?.cards[action.payload.cardId]
......
import { createSlice } from '@reduxjs/toolkit'
export const preferencesSlice = createSlice({
name: 'preferences',
initialState: {
theme: null,
},
reducers: {
setTheme: (state, action) => {
state.theme = action.payload
},
}
})
export const { setTheme } = preferencesSlice.actions
export default preferencesSlice.reducer
\ No newline at end of file
import { configureStore } from '@reduxjs/toolkit'
import boardReducer from './boardSlice'
import serverReducer from './serverSlice'
import preferencesReducer from './preferencesSlice'
import themeReducer from './themeSlice'
import tokenReducer from './tokenSlice'
export default configureStore({
reducer: {
boards: boardReducer,
preferences: preferencesReducer,
server: serverReducer,
theme: themeReducer,
token: tokenReducer
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment