|
import React from 'react'; |
|
import PropTypes from 'prop-types'; |
|
|
|
import { NavigationActions, addNavigationHelpers } from 'react-navigation'; |
|
|
|
const publicPath = '/m/'; |
|
|
|
// currentAction turns the URL pathname and url parameters like this |
|
// /chat/photo?0.chatId=1&1.photoId=2 |
|
// into a react-navigation action to reset the chat parameters |
|
// [ |
|
// { routeName: 'chat', params: { chatId: '1' } }, |
|
// { routeName: 'photo', params: { photoId: '2' } }, |
|
// ] |
|
function currentAction(router) { |
|
if (typeof window === 'undefined') { |
|
return router.getActionForPathAndParams('', {}); |
|
} |
|
|
|
let { pathname } = window.location; |
|
const { search } = window.location; |
|
if (pathname.indexOf(publicPath) === 0) { |
|
pathname = pathname.substr(publicPath.length); |
|
} else { |
|
pathname = pathname.substr(1); |
|
} |
|
|
|
let routes = null; |
|
pathname = pathname.replace(/\/+/g, '/'); |
|
if (pathname === '/') { |
|
routes = [{ routeName: 'landing', params: {} }]; |
|
} else { |
|
routes = pathname.split('/').map((routeName) => { |
|
return { routeName, params: {} }; |
|
}); |
|
|
|
if (pathname.endsWith('/')) { |
|
routes = routes.slice(0, -1); |
|
} |
|
} |
|
|
|
// Parse the URL parameters into key value pairs |
|
const params = (search || '?') |
|
.substr(1) |
|
.split('&') |
|
.map((pair) => { |
|
const eqIdx = pair.indexOf('='); |
|
let key; |
|
let value; |
|
if (eqIdx >= 0) { |
|
key = decodeURIComponent(pair.substr(0, eqIdx)); |
|
value = decodeURIComponent(pair.substr(eqIdx + 1)); |
|
} else { |
|
key = decodeURIComponent(pair); |
|
value = null; |
|
} |
|
|
|
return { key, value }; |
|
}); |
|
|
|
// Distribute the url parmeters into routes represented |
|
// Example: |
|
// /a/b/c?0.id=23&1.name=Steven |
|
// becomes |
|
// StackRouter: [ |
|
// { routeName: 'a', params: { id: '23' } }, |
|
// { routeName: 'b', params: { name: 'Steven' } }, |
|
// { routeName: 'c', params: {} }, |
|
// ] |
|
for (const p of params) { |
|
// If this is an indexed parameter |
|
const pIdx = p.key.indexOf('.'); |
|
if (pIdx > -1) { |
|
const routeIndex = parseInt(p.key.substr(0, pIdx), 10); |
|
if (routes[routeIndex] && routes[routeIndex].params) { |
|
const realKey = p.key.substr(pIdx + 1); |
|
routes[routeIndex].params[realKey] = p.value; |
|
} |
|
} |
|
} |
|
|
|
// Special case for only 1 route, no nesting needed |
|
if (routes.length === 1) { |
|
return router.getActionForPathAndParams(routes[0].routeName, routes[0].params); |
|
} |
|
|
|
return NavigationActions.reset({ |
|
index: routes.length - 1, |
|
actions: routes.map(r => NavigationActions.navigate(r)), |
|
}); |
|
} |
|
|
|
function makeUri(state) { |
|
let path = state.routes.map(r => r.routeName).join('/'); |
|
const params = []; |
|
// We only use a stack router, this is a convention for mapping url paths back into a route stack |
|
// |
|
// StackRouter: [ |
|
// { routeName: 'a', params: { id: '23' } }, |
|
// { routeName: 'b', params: { name: 'Steven' } }, |
|
// { routeName: 'c', params: {} }, |
|
// ] |
|
// becomes |
|
// /a/b/c?0.id=23&1.name=Steven |
|
state.routes.forEach((r, i) => { |
|
Object.keys(r.params || {}).sort().forEach((name) => { |
|
const key = `${i}.${name}`; |
|
const value = r.params[name]; |
|
params.push({ key, value }); |
|
}); |
|
}); |
|
|
|
let qs = params.map(p => `${encodeURIComponent(p.key)}=${encodeURIComponent(p.value)}`).join('&'); |
|
if (qs) { |
|
qs = `?${qs}`; |
|
} |
|
|
|
if (path === 'landing') { |
|
path = ''; |
|
} |
|
|
|
return `${publicPath}${path}${qs}`; |
|
} |
|
|
|
export default (NavigationAwareView) => { |
|
const initialAction = currentAction(NavigationAwareView.router); |
|
const initialState = NavigationAwareView.router.getStateForAction(initialAction); |
|
const initialUri = makeUri(initialState); |
|
|
|
if (typeof window !== 'undefined' && window.location.pathname + window.location.search !== initialUri) { |
|
window.history.replaceState({}, initialState.title, initialUri); |
|
} |
|
|
|
console.log({ initialAction, initialState }); // eslint-disable-line no-console |
|
|
|
class NavigationContainer extends React.Component { |
|
state = initialState; |
|
|
|
_actionEventSubscribers = new Set(); |
|
|
|
componentDidMount() { |
|
const navigation = addNavigationHelpers({ |
|
state: this.state.routes[this.state.index], |
|
dispatch: this.dispatch, |
|
}); |
|
|
|
document.title = NavigationAwareView.router.getScreenOptions( |
|
navigation, |
|
).title; |
|
|
|
window.onpopstate = e => { |
|
e.preventDefault(); |
|
const action = NavigationActions.back(); |
|
if (action) { |
|
this.dispatch(action); |
|
} |
|
}; |
|
} |
|
|
|
componentWillUpdate(props, state) { |
|
const uri = makeUri(state); |
|
if (window.location.pathname + window.location.search !== uri) { |
|
window.history.pushState({}, state.title, uri); |
|
} |
|
|
|
const navigation = addNavigationHelpers({ |
|
state: state.routes[state.index], |
|
dispatch: this.dispatch, |
|
}); |
|
|
|
document.title = NavigationAwareView.router.getScreenOptions(navigation).title; |
|
} |
|
|
|
dispatch = action => { |
|
const state = NavigationAwareView.router.getStateForAction(action, this.state); |
|
|
|
/* eslint-disable no-console */ |
|
if (!state) { |
|
console.log('Dispatched action did not change state: ', { action }); |
|
} else if (console.group) { |
|
console.group('Navigation Dispatch: '); |
|
console.log('Action: ', action); |
|
console.log('New State: ', state); |
|
console.log('Last State: ', this.state); |
|
console.groupEnd(); |
|
} else { |
|
console.log('Navigation Dispatch: ', { |
|
action, |
|
newState: state, |
|
lastState: this.state, |
|
}); |
|
} |
|
/* eslint-enable no-console */ |
|
|
|
if (!state) { |
|
return true; |
|
} |
|
|
|
if (state !== this.state) { |
|
this.setState(state); |
|
return true; |
|
} |
|
|
|
return false; |
|
}; |
|
|
|
render() { |
|
const navigation = addNavigationHelpers({ |
|
state: this.state, |
|
dispatch: this.dispatch, |
|
addListener: (eventName, handler) => { |
|
if (eventName !== 'action') { |
|
return { remove: () => {} }; |
|
} |
|
this._actionEventSubscribers.add(handler); |
|
return { |
|
remove: () => { |
|
this._actionEventSubscribers.delete(handler); |
|
}, |
|
}; |
|
}, |
|
}); |
|
|
|
return ( |
|
<NavigationAwareView {...this.props} navigation={navigation} /> |
|
); |
|
} |
|
|
|
getURIForAction = action => { |
|
console.warn('getURIForAction: Dont really expect this to happen'); // eslint-disable-line no-console |
|
const state = |
|
NavigationAwareView.router.getStateForAction(action, this.state) || |
|
this.state; |
|
const { path } = NavigationAwareView.router.getPathAndParamsForState(state); |
|
return `${publicPath}${path}`; |
|
}; |
|
|
|
getActionForPathAndParams = (path, params) => { |
|
console.warn('getActionForPathAndParams: Dont really expect this to happen'); // eslint-disable-line no-console |
|
return NavigationAwareView.router.getActionForPathAndParams(path, params); |
|
}; |
|
|
|
getChildContext() { |
|
return { |
|
getActionForPathAndParams: this.getActionForPathAndParams, |
|
getURIForAction: this.getURIForAction, |
|
dispatch: this.dispatch, |
|
}; |
|
} |
|
} |
|
|
|
NavigationContainer.childContextTypes = { |
|
getActionForPathAndParams: PropTypes.func.isRequired, |
|
getURIForAction: PropTypes.func.isRequired, |
|
dispatch: PropTypes.func.isRequired, |
|
}; |
|
|
|
NavigationContainer.propTypes = { |
|
uriPrefix: PropTypes.string, |
|
onNavigationStateChange: PropTypes.func, |
|
}; |
|
|
|
return NavigationContainer; |
|
}; |