New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add the ability to "retain" a dismissed screen, to support iOS Picture In Picture overlays. #11097
base: 6.x
Are you sure you want to change the base?
Changes from all commits
24bc363
5a59a67
80f083a
fc6d2aa
7af6c50
05e057b
c185fd2
58bd2f8
338d1d0
2d093fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -40,6 +40,7 @@ | |||||
import useInvalidPreventRemoveError from '../utils/useInvalidPreventRemoveError'; | ||||||
import DebugContainer from './DebugContainer'; | ||||||
import HeaderConfig from './HeaderConfig'; | ||||||
import { RetainContext, useRetainContext } from './RetainContext'; | ||||||
|
||||||
const isAndroid = Platform.OS === 'android'; | ||||||
|
||||||
|
@@ -120,6 +121,7 @@ | |||||
descriptor: NativeStackDescriptor; | ||||||
previousDescriptor?: NativeStackDescriptor; | ||||||
nextDescriptor?: NativeStackDescriptor; | ||||||
hidden: boolean; | ||||||
onWillDisappear: () => void; | ||||||
onAppear: () => void; | ||||||
onDisappear: () => void; | ||||||
|
@@ -134,6 +136,7 @@ | |||||
descriptor, | ||||||
previousDescriptor, | ||||||
nextDescriptor, | ||||||
hidden, | ||||||
onWillDisappear, | ||||||
onAppear, | ||||||
onDisappear, | ||||||
|
@@ -252,6 +255,7 @@ | |||||
return ( | ||||||
<Screen | ||||||
key={route.key} | ||||||
hidden={hidden} | ||||||
Check failure on line 258 in packages/native-stack/src/views/NativeStackView.native.tsx GitHub Actions / lint
|
||||||
enabled | ||||||
style={StyleSheet.absoluteFill} | ||||||
customAnimationOnSwipe={customAnimationOnGesture} | ||||||
|
@@ -397,74 +401,86 @@ | |||||
|
||||||
useInvalidPreventRemoveError(descriptors); | ||||||
|
||||||
const { retainContext, hiddenRoutes, hiddenDescriptors, retainedKeys } = | ||||||
useRetainContext(state, navigation, descriptors); | ||||||
descriptors = { ...hiddenDescriptors, ...descriptors }; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd avoid overriding props, as ot may be confusing for someone reading the code. I guess it's a mental model that we don't mutate values as long as possible. How about It's a bit confusing (at least, I can imagine, it can be) that you use create an array and object without memorizing. Of course, it makes sense here as we don't pass them as props, but I'd add a comment, e.g., "no need to memorize as we don't pass them directly as props" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
imo the comment isn't necessary since memorizing depends on other factors and not about props. it'd be confusing for me to see such a comment. |
||||||
|
||||||
const routes = hiddenRoutes.concat(state.routes); | ||||||
|
||||||
return ( | ||||||
<ScreenStack style={styles.container}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. best viewed with whitespace ignored |
||||||
{state.routes.map((route, index) => { | ||||||
const descriptor = descriptors[route.key]; | ||||||
const isFocused = state.index === index; | ||||||
const previousKey = state.routes[index - 1]?.key; | ||||||
const nextKey = state.routes[index + 1]?.key; | ||||||
const previousDescriptor = previousKey | ||||||
? descriptors[previousKey] | ||||||
: undefined; | ||||||
const nextDescriptor = nextKey ? descriptors[nextKey] : undefined; | ||||||
|
||||||
return ( | ||||||
<SceneView | ||||||
key={route.key} | ||||||
index={index} | ||||||
focused={isFocused} | ||||||
descriptor={descriptor} | ||||||
previousDescriptor={previousDescriptor} | ||||||
nextDescriptor={nextDescriptor} | ||||||
onWillDisappear={() => { | ||||||
navigation.emit({ | ||||||
type: 'transitionStart', | ||||||
data: { closing: true }, | ||||||
target: route.key, | ||||||
}); | ||||||
}} | ||||||
onAppear={() => { | ||||||
navigation.emit({ | ||||||
type: 'transitionEnd', | ||||||
data: { closing: false }, | ||||||
target: route.key, | ||||||
}); | ||||||
}} | ||||||
onDisappear={() => { | ||||||
navigation.emit({ | ||||||
type: 'transitionEnd', | ||||||
data: { closing: true }, | ||||||
target: route.key, | ||||||
}); | ||||||
}} | ||||||
onDismissed={(event) => { | ||||||
navigation.dispatch({ | ||||||
...StackActions.pop(event.nativeEvent.dismissCount), | ||||||
source: route.key, | ||||||
target: state.key, | ||||||
}); | ||||||
|
||||||
setNextDismissedKey(route.key); | ||||||
}} | ||||||
onHeaderBackButtonClicked={() => { | ||||||
navigation.dispatch({ | ||||||
...StackActions.pop(), | ||||||
source: route.key, | ||||||
target: state.key, | ||||||
}); | ||||||
}} | ||||||
onNativeDismissCancelled={(event) => { | ||||||
navigation.dispatch({ | ||||||
...StackActions.pop(event.nativeEvent.dismissCount), | ||||||
source: route.key, | ||||||
target: state.key, | ||||||
}); | ||||||
}} | ||||||
/> | ||||||
); | ||||||
})} | ||||||
</ScreenStack> | ||||||
<RetainContext.Provider value={retainContext}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about |
||||||
<ScreenStack style={styles.container}> | ||||||
{routes.map((route, offsetIndex) => { | ||||||
const index = offsetIndex - hiddenRoutes.length; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's confusing. I don't think we have any offset here.
Suggested change
and then There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
const descriptor = descriptors[route.key]; | ||||||
const isFocused = state.index === index; | ||||||
const previousKey = state.routes[index - 1]?.key; | ||||||
const nextKey = state.routes[index + 1]?.key; | ||||||
const previousDescriptor = previousKey | ||||||
? descriptors[previousKey] | ||||||
: undefined; | ||||||
const nextDescriptor = nextKey ? descriptors[nextKey] : undefined; | ||||||
|
||||||
return ( | ||||||
<SceneView | ||||||
key={route.key} | ||||||
hidden={index < 0} | ||||||
index={index} | ||||||
focused={isFocused} | ||||||
descriptor={descriptor} | ||||||
previousDescriptor={previousDescriptor} | ||||||
nextDescriptor={nextDescriptor} | ||||||
onWillDisappear={() => { | ||||||
navigation.emit({ | ||||||
type: 'transitionStart', | ||||||
data: { closing: true }, | ||||||
target: route.key, | ||||||
}); | ||||||
}} | ||||||
onAppear={() => { | ||||||
navigation.emit({ | ||||||
type: 'transitionEnd', | ||||||
data: { closing: false }, | ||||||
target: route.key, | ||||||
}); | ||||||
}} | ||||||
onDisappear={() => { | ||||||
navigation.emit({ | ||||||
type: 'transitionEnd', | ||||||
data: { closing: true }, | ||||||
target: route.key, | ||||||
}); | ||||||
}} | ||||||
onDismissed={(event) => { | ||||||
navigation.dispatch({ | ||||||
...StackActions.pop(event.nativeEvent.dismissCount), | ||||||
source: route.key, | ||||||
target: state.key, | ||||||
}); | ||||||
|
||||||
if (!retainedKeys.includes(route.key)) { | ||||||
setNextDismissedKey(route.key); | ||||||
} | ||||||
Comment on lines
+461
to
+463
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this warning fires spuriously when restoring a previously dismissed screen There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jaredly yea this one has some issue, need to address it later |
||||||
}} | ||||||
onHeaderBackButtonClicked={() => { | ||||||
navigation.dispatch({ | ||||||
...StackActions.pop(), | ||||||
source: route.key, | ||||||
target: state.key, | ||||||
}); | ||||||
}} | ||||||
onNativeDismissCancelled={(event) => { | ||||||
navigation.dispatch({ | ||||||
...StackActions.pop(event.nativeEvent.dismissCount), | ||||||
source: route.key, | ||||||
target: state.key, | ||||||
}); | ||||||
}} | ||||||
/> | ||||||
); | ||||||
})} | ||||||
</ScreenStack> | ||||||
</RetainContext.Provider> | ||||||
); | ||||||
} | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,138 @@ | ||||||||||||||||||||||||||||||||||||||
import type { | ||||||||||||||||||||||||||||||||||||||
ParamListBase, | ||||||||||||||||||||||||||||||||||||||
Route, | ||||||||||||||||||||||||||||||||||||||
StackNavigationState, | ||||||||||||||||||||||||||||||||||||||
} from '@react-navigation/native'; | ||||||||||||||||||||||||||||||||||||||
import * as React from 'react'; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
import type { | ||||||||||||||||||||||||||||||||||||||
NativeStackDescriptorMap, | ||||||||||||||||||||||||||||||||||||||
NativeStackNavigationHelpers, | ||||||||||||||||||||||||||||||||||||||
} from '../types'; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
type RetainContextT = { | ||||||||||||||||||||||||||||||||||||||
retain(): string; | ||||||||||||||||||||||||||||||||||||||
release(key: string): boolean; | ||||||||||||||||||||||||||||||||||||||
restore(key: string): boolean; | ||||||||||||||||||||||||||||||||||||||
supported: boolean; | ||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||
* This context allows you to retain screens in memory, even after | ||||||||||||||||||||||||||||||||||||||
* they have been popped off the stack. This is important for | ||||||||||||||||||||||||||||||||||||||
* supporting native iOS "Picture in Picture" mode, as the | ||||||||||||||||||||||||||||||||||||||
* originating UIViewController must be retained in memory for the | ||||||||||||||||||||||||||||||||||||||
* PiP overlay to continue running. | ||||||||||||||||||||||||||||||||||||||
* Retained screens can also be "restored" to the top of the stack, | ||||||||||||||||||||||||||||||||||||||
* for example when the user taps the button in the PiP overlay | ||||||||||||||||||||||||||||||||||||||
* to return to the app. | ||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+21
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I'm not native so feel free to ignore. I believe there's no need for a quotation as the description is precise enough. |
||||||||||||||||||||||||||||||||||||||
export const RetainContext = React.createContext<RetainContextT>({ | ||||||||||||||||||||||||||||||||||||||
retain() { | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd do something even stronger.
Suggested change
This will throw an error on access, without waiting for invoking the function. |
||||||||||||||||||||||||||||||||||||||
throw new Error(`Not in a native screen stack`); | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
and then:
|
||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
release() { | ||||||||||||||||||||||||||||||||||||||
throw new Error(`Not in a native screen stack`); | ||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
restore() { | ||||||||||||||||||||||||||||||||||||||
throw new Error(`Not in a native screen stack`); | ||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
supported: false, | ||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
type RetainedScenes = { | ||||||||||||||||||||||||||||||||||||||
[key: string]: { | ||||||||||||||||||||||||||||||||||||||
route: Route<any>; | ||||||||||||||||||||||||||||||||||||||
descriptor: NativeStackDescriptorMap['key']; | ||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
export function useRetainContext( | ||||||||||||||||||||||||||||||||||||||
state: StackNavigationState<ParamListBase>, | ||||||||||||||||||||||||||||||||||||||
navigation: NativeStackNavigationHelpers, | ||||||||||||||||||||||||||||||||||||||
descriptors: NativeStackDescriptorMap | ||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||
const [retainedScenes, setRetainedScenes] = React.useState<RetainedScenes>( | ||||||||||||||||||||||||||||||||||||||
{} | ||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||
const latestValues = React.useRef({ state, retainedScenes, descriptors }); | ||||||||||||||||||||||||||||||||||||||
React.useEffect(() => { | ||||||||||||||||||||||||||||||||||||||
latestValues.current = { state, retainedScenes, descriptors }; | ||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+58
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm rather against patters like that. The new state should be the result of props and the previous state as react may batch those updates, we should not rely on what is currently in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is not very needed here and it's possible to refactor the code without that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume the intention is to memoize the With that it's unnecessary to do such a ref. |
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const retainContext = React.useMemo<RetainContextT>( | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I thnk TS should be smart enough. Maybe not :) |
||||||||||||||||||||||||||||||||||||||
() => ({ | ||||||||||||||||||||||||||||||||||||||
retain() { | ||||||||||||||||||||||||||||||||||||||
const { state, descriptors } = latestValues.current; | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not |
||||||||||||||||||||||||||||||||||||||
const route = state.routes[state.routes.length - 1]; | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of assuming the last screen, this hook should use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As currently written, the |
||||||||||||||||||||||||||||||||||||||
setRetainedScenes((screens) => ({ | ||||||||||||||||||||||||||||||||||||||
...screens, | ||||||||||||||||||||||||||||||||||||||
[route.key]: { | ||||||||||||||||||||||||||||||||||||||
route, | ||||||||||||||||||||||||||||||||||||||
descriptor: descriptors[route.key], | ||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+70
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should only store the route key. Duplicating the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm it can't be read from |
||||||||||||||||||||||||||||||||||||||
})); | ||||||||||||||||||||||||||||||||||||||
return route.key; | ||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
release(key) { | ||||||||||||||||||||||||||||||||||||||
setRetainedScenes((screens) => { | ||||||||||||||||||||||||||||||||||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||||||||||||||||||||||||||||||||||
const { [key]: _, ...rest } = screens; | ||||||||||||||||||||||||||||||||||||||
return rest; | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check and warn if not present |
||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||
return latestValues.current.retainedScenes[key] != null; | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you should not rely on the ref here as concurrent mode can make it flaky. Why can't you rely on the previous state? |
||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
restore(key) { | ||||||||||||||||||||||||||||||||||||||
const { retainedScenes, state } = latestValues.current; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const route = retainedScenes[key]?.route; | ||||||||||||||||||||||||||||||||||||||
if (!route) { | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. throw error |
||||||||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Remove from retained | ||||||||||||||||||||||||||||||||||||||
setRetainedScenes((screens) => { | ||||||||||||||||||||||||||||||||||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||||||||||||||||||||||||||||||||||
const { [key]: _, ...rest } = screens; | ||||||||||||||||||||||||||||||||||||||
return rest; | ||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const index = state.routes.findIndex((r) => r.key === key); | ||||||||||||||||||||||||||||||||||||||
const routes = | ||||||||||||||||||||||||||||||||||||||
index !== -1 ? state.routes.slice(0, index) : state.routes.slice(); | ||||||||||||||||||||||||||||||||||||||
routes.push(route); | ||||||||||||||||||||||||||||||||||||||
navigation.dispatch({ | ||||||||||||||||||||||||||||||||||||||
type: 'RESET', | ||||||||||||||||||||||||||||||||||||||
payload: { ...state, index: routes.length - 1, routes }, | ||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+100
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't refer to actions by string. Use the Also, I assume it's moving the screen to the end. Is this necessary/desirable? If the screen wasn't the last one, seems strange to change the order after pip restore. I'd remove this, consumers can do this themselves if they want to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I thought that in order for a screen to be visible on a screen stack, it needed to be the last one. Is that not the case? |
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
supported: true, | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This boolean seems unnecessary if it's always There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's true here, but the default value for the |
||||||||||||||||||||||||||||||||||||||
}), | ||||||||||||||||||||||||||||||||||||||
[navigation] | ||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const routeKeys = state.routes.reduce((map, route) => { | ||||||||||||||||||||||||||||||||||||||
map[route.key] = true; | ||||||||||||||||||||||||||||||||||||||
return map; | ||||||||||||||||||||||||||||||||||||||
}, {} as { [key: string]: true }); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Routes that were marked as 'retain' and have been removed from the stack, | ||||||||||||||||||||||||||||||||||||||
// for which we still want to keep the memory allocated. | ||||||||||||||||||||||||||||||||||||||
const hiddenRoutes: Route<any>[] = []; | ||||||||||||||||||||||||||||||||||||||
const hiddenDescriptors: NativeStackDescriptorMap = {}; | ||||||||||||||||||||||||||||||||||||||
Object.keys(retainedScenes).forEach((key) => { | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code will be much simpler if there was just a |
||||||||||||||||||||||||||||||||||||||
if (!routeKeys[key]) { | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just check the |
||||||||||||||||||||||||||||||||||||||
hiddenRoutes.push(retainedScenes[key].route); | ||||||||||||||||||||||||||||||||||||||
hiddenDescriptors[key] = retainedScenes[key].descriptor; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is that supposed to be a public API? |
||||||||||||||||||||||||||||||||||||||
retainContext, | ||||||||||||||||||||||||||||||||||||||
hiddenRoutes, | ||||||||||||||||||||||||||||||||||||||
hiddenDescriptors, | ||||||||||||||||||||||||||||||||||||||
retainedKeys: Object.keys(retainedScenes), | ||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let me know if you want this to go into a
/contexts/
directory, or somewhere else