{"action":"created","issue":{"url":"https://api.github.com/repos/keybase/client/issues/4681","repository_url":"https://api.github.com/repos/keybase/client","labels_url":"https://api.github.com/repos/keybase/client/issues/4681/labels{/name}","comments_url":"https://api.github.com/repos/keybase/client/issues/4681/comments","events_url":"https://api.github.com/repos/keybase/client/issues/4681/events","html_url":"https://github.com/keybase/client/pull/4681","id":184365212,"number":4681,"title":"[WIP] New router","user":{"login":"chromakode","id":16893,"avatar_url":"https://avatars.githubusercontent.com/u/16893?v=3","gravatar_id":"","url":"https://api.github.com/users/chromakode","html_url":"https://github.com/chromakode","followers_url":"https://api.github.com/users/chromakode/followers","following_url":"https://api.github.com/users/chromakode/following{/other_user}","gists_url":"https://api.github.com/users/chromakode/gists{/gist_id}","starred_url":"https://api.github.com/users/chromakode/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chromakode/subscriptions","organizations_url":"https://api.github.com/users/chromakode/orgs","repos_url":"https://api.github.com/users/chromakode/repos","events_url":"https://api.github.com/users/chromakode/events{/privacy}","received_events_url":"https://api.github.com/users/chromakode/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":6,"created_at":"2016-10-20T23:26:09Z","updated_at":"2016-10-25T18:45:31Z","closed_at":null,"pull_request":{"url":"https://api.github.com/repos/keybase/client/pulls/4681","html_url":"https://github.com/keybase/client/pull/4681","diff_url":"https://github.com/keybase/client/pull/4681.diff","patch_url":"https://github.com/keybase/client/pull/4681.patch"},"body":":warning: WIP, please do not merge!\r\n\r\nHere's the current progress on my router branch.\r\n\r\n:eyeglasses: @keybase/react-hackers \r\n\r\n## Design Notes\r\n\r\n### Routes are now a tree:\r\n\r\nEvery route in the application is defined in a big tree. In the code, this is broken down into `routes.js` files for each subfeature.\r\n\r\nsettings/routes.js:\r\n```js\r\nconst routeTree = Routes({\r\n  selected: landingTab,\r\n  wrapComponent: Settings,\r\n  children: {\r\n    [landingTab]: {\r\n      component: LandingContainer,\r\n    },\r\n    [updatePaymentTab]: {\r\n      component: UpdatePayment,\r\n    },\r\n    [invitationsTab]: {\r\n      component: InvitationsContainer,\r\n      children: {\r\n        inviteSent: {\r\n          component: InviteGenerated,\r\n        },\r\n      },\r\n    },\r\n    ...\r\n```\r\n\r\n`wrapComponent` allows parent routes to render nav/tabs UI. When a route is rendered, `wrapComponents` in the tree are rendered with sub-route info and their `children` prop contains the next child `wrapComponent` or `component`.\r\n\r\n### Routes can have props and state:\r\n\r\nProps can be thought of like query parameters. You specify them when navigating and they don't change after that.\r\n\r\nState allows temporary data to be attached to a route, such as scroll position or expando state. We can use this to persist field values across tab changes (retaining the value even when the tab is inactive). This allows us to localize UI state that was previously in global stores to the routes they are intended for.\r\n\r\nFor example:\r\n\r\nThe folders view has a \"private\" and \"public\" tab, and expandos on each of these tabs. Previously we had to manage this state in the store. It can now all be handled in the routes. This is a good place to put state for which the lifecycle matches an app screen -- when the user returns to the tab, it will be persisted, whereas internal component state will be lost when the component is unmounted.\r\n\r\n\r\nfolders/routes.js:\r\n```js\r\nconst routeTree = Routes({\r\n  selected: 'private',\r\n  children: {\r\n    private: {\r\n      component: Folders,\r\n      staticProps: {showingPrivate: true},\r\n      initialState: {showingIgnored: false},\r\n    },\r\n    public: {\r\n      component: Folders,\r\n      staticProps: {showingPrivate: false},\r\n      initialState: {showingIgnored: false},\r\n    },\r\n  }\r\n})\r\n```\r\n\r\nRoute state is manipulated via props passed to the smart components:\r\n\r\nfolders/index.js:\r\n```js\r\nexport default connect(\r\n  (state: TypedState, {routeProps, routeState}) => ({\r\n    username: state.config.username,\r\n    folderState: state.favorite ? state.favorite.folderState : null,\r\n    showingPrivate: !!state.favorite && routeProps.showingPrivate,\r\n    showingIgnored: !!state.favorite && routeState.showingIgnored,\r\n  }),\r\n  (dispatch: any, {routePath, routeState, setRouteState}) => ({\r\n    favoriteList: () => { dispatch(favoriteList()) },\r\n    navigateAppend: path => { dispatch(navigateAppend(path)) },\r\n    openInKBFS: path => { dispatch(openInKBFS(path)) },\r\n    // NOTE: this code is a little tricky due to the way the folder tab routes are structured. This is essentially an explicit way to switch to \"../private\" or \"../public\". Happy to discuss.\r\n    switchTab: showingPrivate => { dispatch(switchTo(...routePath.slice(0, -1), showingPrivate ? 'private' : 'public')) },\r\n    onToggleShowIgnored: () => { setRouteState({showingIgnored: !routeState.showingIgnored}) }\r\n  })\r\n)(Folders)\r\n```\r\n\r\n### \"Switching\" vs \"navigating\"\r\n\r\nOne detail of working with tabbed routes is that sometimes you want to navigate to `/foo/bar/<whatever the user was on before>`. This is a different operation from navigation, because `navigateTo(/foo/bar)` specifies the absolute route we should end up on as `/foo/bar`. We want to change only the parent path, and retain whatever existing state was in the child routes. I've named this operation `switchTo`, which is similar to the nomenclature we used to use for tabs.\r\n\r\n### Leaf tags and modal views\r\n\r\nSometimes we have views that we don't want wrapped by a nav of their parent route. For instance, in the settings routes, confirmation screens for deleting an account don't include the settings nav. To implement this in a general fashion, leaf routes can specify tags which get passed to their wrapper components:\r\n\r\nsettings/routes.js:\r\n```js\r\n    [deleteMeTab]: {\r\n      component: DeleteContainer,\r\n      children: {\r\n        deleteConfirm: {\r\n          component: DeleteConfirm,\r\n          tags: {modal: true},\r\n        },\r\n        removeDevice: {\r\n          component: RemoveDevice,\r\n          tags: {modal: true},\r\n        },\r\n      },\r\n    },\r\n```\r\n\r\nThe settings wrapper component, `Settings`, will be passed a prop containing the tags for whatever component is the currently rendered leaf route. We can use this to hide the nav UI when a modal sub-route is being displayed:\r\n\r\nsettings/index.js:\r\n```js\r\nexport default connect(\r\n  (state, {routeSelected, routeLeafTags}) => ({\r\n    showComingSoon: !flags.tabSettingsEnabled,\r\n    selectedTab: routeSelected,\r\n    isModal: routeLeafTags.modal,\r\n  }),\r\n  ...\r\n```\r\n\r\nsettings/render.desktop.js:\r\n```js\r\n      {!props.isModal && <SettingsNav selectedTab={props.selectedTab} onTabChange={props.onTabChange} />}\r\n\r\n```\r\n\r\n## Todo:\r\n\r\n- [ ] finish updating settings pages\r\n- [x] finish folders routes\r\n- [ ] profile pages (recursive routes!)\r\n- [ ] device kex routes\r\n- [x] search routes\r\n- [ ] update any routeAppend in sagas/reducers\r\n- [ ] ~~clean up login route incoming call map code~~ defer if possible\r\n- [ ] hmr fixes\r\n- [ ] mobile\r\n- [ ] FIXMEs & TODOs\r\n- [ ] pass flow\r\n- [ ] pass eslint\r\n- [ ] qa"},"comment":{"url":"https://api.github.com/repos/keybase/client/issues/comments/256135452","html_url":"https://github.com/keybase/client/pull/4681#issuecomment-256135452","issue_url":"https://api.github.com/repos/keybase/client/issues/4681","id":256135452,"user":{"login":"gabriel","id":2669,"avatar_url":"https://avatars.githubusercontent.com/u/2669?v=3","gravatar_id":"","url":"https://api.github.com/users/gabriel","html_url":"https://github.com/gabriel","followers_url":"https://api.github.com/users/gabriel/followers","following_url":"https://api.github.com/users/gabriel/following{/other_user}","gists_url":"https://api.github.com/users/gabriel/gists{/gist_id}","starred_url":"https://api.github.com/users/gabriel/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gabriel/subscriptions","organizations_url":"https://api.github.com/users/gabriel/orgs","repos_url":"https://api.github.com/users/gabriel/repos","events_url":"https://api.github.com/users/gabriel/events{/privacy}","received_events_url":"https://api.github.com/users/gabriel/received_events","type":"User","site_admin":false},"created_at":"2016-10-25T18:45:31Z","updated_at":"2016-10-25T18:45:31Z","body":"And I'm cool with punting on a lot of these issues so that we have routing with our stuff without a massive migration, and then do migration later, or whatever is easiest to get this in master :)"}}
{"actor":{"display_login":"gabriel"}}