@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/android/CLAUDE.md +26 -9
  3. package/android/CORE.md +32 -17
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +25 -9
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  8. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  9. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  12. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  13. package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
  14. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  15. package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
  16. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +6 -6
  18. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +29 -36
  19. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  20. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +33 -22
  21. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  22. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  23. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
  24. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -4
  25. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +6 -6
  26. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +10 -5
  27. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +47 -46
  28. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
  29. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
  30. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -4
  31. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  32. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
  33. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  34. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  35. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  36. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  37. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  38. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  39. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +3 -2
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +25 -17
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +6 -2
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +2 -2
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -8
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +8 -5
  53. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
  54. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  55. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
  56. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  57. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +61 -15
  58. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  59. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +25 -12
  60. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
  61. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
  62. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
  63. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  64. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
  65. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +5 -8
  66. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
  67. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  68. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +14 -11
  69. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
  70. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  71. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
  72. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  73. package/cli/doctor/checks/app.js +61 -13
  74. package/cli/doctor/checks/splash.js +4 -22
  75. package/cli/migrate/native.js +18 -0
  76. package/cli/setup/source.js +2 -1
  77. package/cli/setup/steps/mainApplication.js +134 -19
  78. package/cli/splash/index.js +3 -1
  79. package/docs-md/architecture/transitions.md +6 -7
  80. package/docs-md/containers/modals.md +2 -2
  81. package/docs-md/guides/cli.md +1 -2
  82. package/docs-md/help/glossary.md +1 -1
  83. package/docs-md/help/troubleshooting.md +22 -5
  84. package/docs-md/index.json +4 -0
  85. package/docs-md/migration/from-react-native-navigation.md +8 -2
  86. package/docs-md/screen-ui/back-and-gestures.md +24 -2
  87. package/docs-md/start/installation.md +4 -2
  88. package/ios/CLAUDE.md +17 -5
  89. package/ios/CORE.md +18 -13
  90. package/ios/Contract/ANNCommandAnswer.h +9 -2
  91. package/ios/Contract/ANNCommandAnswer.mm +28 -30
  92. package/ios/Contract/ANNCommandContract.h +15 -0
  93. package/ios/Contract/ANNCommandContract.mm +78 -0
  94. package/ios/Core/ANNAppearanceDispatcher.h +5 -4
  95. package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
  96. package/ios/Core/ANNErrorHandler.h +1 -1
  97. package/ios/Core/ANNErrorHandler.mm +3 -1
  98. package/ios/Core/ANNEventEmitter.h +1 -1
  99. package/ios/Core/ANNEventEmitter.mm +2 -2
  100. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  101. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  102. package/ios/Core/ANNNavigationState.h +7 -5
  103. package/ios/Core/ANNNavigationState.mm +8 -7
  104. package/ios/Core/ANNNodeHelper.h +32 -12
  105. package/ios/Core/ANNNodeHelper.mm +100 -3
  106. package/ios/Core/ANNRenderWait.h +15 -1
  107. package/ios/Core/ANNRenderWait.mm +119 -17
  108. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  109. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  110. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  111. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  112. package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
  113. package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
  114. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  115. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  116. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  117. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  118. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  119. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  120. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  121. package/ios/Layouts/Stack/ANNPreviewPush.h +5 -5
  122. package/ios/Layouts/Stack/ANNPreviewPush.mm +85 -22
  123. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  124. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  125. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  126. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  127. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  128. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  129. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  130. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  131. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  132. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  133. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  134. package/ios/Options/ANNOptionsCache.h +7 -6
  135. package/ios/Options/ANNSearchBarOptions.h +4 -3
  136. package/ios/Options/ANNUserChoice.h +57 -0
  137. package/ios/Options/ANNUserChoice.mm +44 -0
  138. package/ios/Options/Params/ANNImageSource.h +61 -0
  139. package/ios/Options/Params/ANNImageSource.mm +101 -0
  140. package/ios/Options/Params/ANNOptionImages.h +37 -0
  141. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  142. package/ios/Options/Params/Image.h +11 -2
  143. package/ios/Options/Params/Image.mm +15 -2
  144. package/ios/Options/Params/ImageParser.h +5 -5
  145. package/ios/Options/Params/ImageParser.mm +28 -46
  146. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  147. package/ios/RECIPES.md +11 -5
  148. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  149. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  150. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  151. package/ios/TopBar/ANNBarButtons.h +9 -5
  152. package/ios/TopBar/ANNBarButtons.mm +211 -63
  153. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  154. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  155. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  156. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  157. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  158. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  159. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  160. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  161. package/lib/module/ARCHITECTURE.md +18 -18
  162. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  163. package/lib/module/api/commands/commands.js +54 -6
  164. package/lib/module/api/commands/commands.js.map +1 -1
  165. package/lib/module/api/navigation.js.map +1 -1
  166. package/lib/module/engine/commands.js +9 -7
  167. package/lib/module/engine/commands.js.map +1 -1
  168. package/lib/module/engine/layout.js +13 -9
  169. package/lib/module/engine/layout.js.map +1 -1
  170. package/lib/module/engine/options.js +44 -11
  171. package/lib/module/engine/options.js.map +1 -1
  172. package/lib/module/engine/places.js +52 -7
  173. package/lib/module/engine/places.js.map +1 -1
  174. package/lib/module/engine/props.js +30 -1
  175. package/lib/module/engine/props.js.map +1 -1
  176. package/lib/module/engine/rememberedOptions.js +1 -1
  177. package/lib/module/engine/rememberedOptions.js.map +1 -1
  178. package/lib/module/engine/screens.js +22 -12
  179. package/lib/module/engine/screens.js.map +1 -1
  180. package/lib/module/interfaces/contract/commands.json +101 -0
  181. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  182. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  183. package/lib/typescript/api/commands/commands.d.ts.map +1 -1
  184. package/lib/typescript/api/commands/presentation.d.ts +4 -3
  185. package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
  186. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  187. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  188. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  189. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  190. package/lib/typescript/api/navigation.d.ts +4 -1
  191. package/lib/typescript/api/navigation.d.ts.map +1 -1
  192. package/lib/typescript/engine/commands.d.ts.map +1 -1
  193. package/lib/typescript/engine/options.d.ts +5 -2
  194. package/lib/typescript/engine/options.d.ts.map +1 -1
  195. package/lib/typescript/engine/places.d.ts +7 -2
  196. package/lib/typescript/engine/places.d.ts.map +1 -1
  197. package/lib/typescript/engine/props.d.ts +8 -1
  198. package/lib/typescript/engine/props.d.ts.map +1 -1
  199. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  200. package/lib/typescript/engine/screens.d.ts.map +1 -1
  201. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  202. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  203. package/lib/typescript/interfaces/Events.d.ts +19 -10
  204. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  205. package/lib/typescript/interfaces/Options.d.ts +1 -1
  206. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  207. package/lib/typescript/interfaces/options/animations.d.ts +23 -4
  208. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  209. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  210. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  211. package/lib/typescript/interfaces/options/stack.d.ts +9 -0
  212. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  213. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  214. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  215. package/package.json +1 -1
  216. package/src/ARCHITECTURE.md +18 -18
  217. package/src/adapters/NativeANNTurboModule.ts +5 -2
  218. package/src/api/commands/commands.ts +48 -7
  219. package/src/api/commands/presentation.ts +4 -3
  220. package/src/api/events/navigationEvents.ts +1 -1
  221. package/src/api/hooks/useNavigation.ts +1 -1
  222. package/src/api/navigation.ts +4 -1
  223. package/src/engine/commands.ts +19 -7
  224. package/src/engine/layout.ts +16 -9
  225. package/src/engine/options.ts +39 -11
  226. package/src/engine/places.ts +65 -7
  227. package/src/engine/props.ts +30 -1
  228. package/src/engine/rememberedOptions.ts +4 -1
  229. package/src/engine/screens.tsx +23 -13
  230. package/src/interfaces/ComponentEvents.ts +2 -2
  231. package/src/interfaces/Events.ts +19 -10
  232. package/src/interfaces/Options.ts +1 -0
  233. package/src/interfaces/contract/commands.json +101 -0
  234. package/src/interfaces/options/animations.ts +24 -4
  235. package/src/interfaces/options/buttons.ts +13 -5
  236. package/src/interfaces/options/stack.ts +9 -0
  237. package/src/interfaces/options/topBar.ts +22 -9
  238. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
  239. package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
@@ -99,8 +99,8 @@ class BottomTabsEnterExitAnimationOptions {
99
99
  }
100
100
 
101
101
  /**
102
- * `StackAnimationOptions`: the animation of `pop`, and the base of `push` and `setStackRoot`; over the shared elements
103
- * and element transitions of [LayoutAnimation].
102
+ * `StackAnimationOptions`: the base of the animations of `push`, `pop` and `setStackRoot`; over the shared elements and
103
+ * element transitions of [LayoutAnimation].
104
104
  */
105
105
  open class StackAnimationOptions : LayoutAnimation() {
106
106
  @JvmField var enabled: Bool = NullBool()
@@ -188,6 +188,44 @@ class StackPushAnimationOptions : StackAnimationOptions() {
188
188
  }
189
189
  }
190
190
 
191
+ /**
192
+ * `StackPopAnimationOptions`: the animation of `pop`, and `preview`: whether a back gesture over the stack previews the
193
+ * pop of its top (Android 14+); `false` pops it when the gesture is released, as on Android 13.
194
+ */
195
+ class StackPopAnimationOptions : StackAnimationOptions() {
196
+ @JvmField var preview: Bool = NullBool()
197
+
198
+ override fun hasValue(): Boolean = super.hasValue() || preview.hasValue()
199
+
200
+ fun mergeWith(other: StackPopAnimationOptions) {
201
+ super.mergeWith(other)
202
+ if (other.preview.hasValue()) preview = other.preview
203
+ }
204
+
205
+ fun mergeWithDefault(defaults: StackPopAnimationOptions) {
206
+ super.mergeWithDefault(defaults)
207
+ if (!preview.hasValue()) preview = defaults.preview
208
+ }
209
+
210
+ override fun copy(): StackPopAnimationOptions = StackPopAnimationOptions().also { it.mergeWith(this) }
211
+
212
+ // Reads the fields of this class from [map], after those of its parent.
213
+ private fun readStackPopAnimationOptions(map: ReadableMap) {
214
+ readStackAnimationOptions(map)
215
+ preview = BoolParser.parse(map, "preview")
216
+ }
217
+
218
+ companion object {
219
+ @JvmStatic
220
+ fun parse(map: ReadableMap?): StackPopAnimationOptions {
221
+ val options = StackPopAnimationOptions()
222
+ if (map == null) return options
223
+ options.readStackPopAnimationOptions(map)
224
+ return options
225
+ }
226
+ }
227
+ }
228
+
191
229
  /** `SetStackRootAnimationOptions`: the animation of `setStackRoot`. */
192
230
  class SetStackRootAnimationOptions : StackAnimationOptions() {
193
231
  @JvmField var asPush: Bool = NullBool()
@@ -0,0 +1,159 @@
1
+ package dev.apexnative.navigation.options
2
+
3
+ import dev.apexnative.navigation.options.params.Text
4
+
5
+ /**
6
+ * The user layer of a node: what the user chose in the native UI, kept apart from the node's own options so that the
7
+ * app's next `mergeOptions` or `setDefaultOptions` doesn't wipe it. The choices are the check of a single-selection
8
+ * menu (a bar button's `menu`, by the button's id; `topBar.overflowMenu`) and the search scope
9
+ * (`topBar.searchBar.selectedScope`).
10
+ *
11
+ * It goes over everything else as the node's options resolve (defaults < parents < own < user), by identity rather
12
+ * than as a tree merged over: a menu's check by the button's id and the item's id, the scope by its id. A choice whose
13
+ * button, item or scope is gone is not applied, and leaves at the node's next merge; a merge that gives a chosen field
14
+ * a value other than the one the app had there before takes the choice away ([yieldTo]). `setDefaultOptions` never
15
+ * clears it. The node owns it and writes it through `ViewController.changeChoices`, which moves [OptionsCache]. Main
16
+ * thread only.
17
+ */
18
+ internal class UserChoices {
19
+ // The menus' checks: per menu (a button's id, OVERFLOW for `overflowMenu`) the item picked in each of its levels,
20
+ // in the order of the picks.
21
+ private val checks = LinkedHashMap<String, MutableList<String>>()
22
+ private var scope: String? = null
23
+
24
+ /** The user chose nothing that stays. */
25
+ val isEmpty: Boolean
26
+ get() = checks.isEmpty() && scope == null
27
+
28
+ /**
29
+ * The user picked [itemId] in [menu], the menu of the button [buttonId] or `overflowMenu` (null), as it showed: it
30
+ * takes the place of an earlier pick in the same level.
31
+ */
32
+ fun check(buttonId: String?, menu: ButtonMenuOptions, itemId: String) {
33
+ val picks = checks.getOrPut(keyOf(buttonId)) { mutableListOf() }
34
+ val level = levelItems(menu.items, itemId)?.map { it.id }.orEmpty()
35
+ picks.removeAll { it == itemId || it in level }
36
+ picks += itemId
37
+ }
38
+
39
+ /** The user picked the search scope [scopeId]. */
40
+ fun pickScope(scopeId: String) {
41
+ scope = scopeId
42
+ }
43
+
44
+ /**
45
+ * Writes the choices into [resolved], a result of resolution nobody else holds yet; the menus it changes are copied
46
+ * first, as a resolved result shares its menus with the layers they come from. Returns [resolved].
47
+ */
48
+ fun applyTo(resolved: Options): Options {
49
+ if (isEmpty) return resolved
50
+ val topBar = resolved.topBar
51
+ for ((key, picks) in checks) {
52
+ if (key == OVERFLOW) {
53
+ val menu = topBar.overflowMenu ?: continue
54
+ topBar.overflowMenu = menu.copyMenu().also { copy -> picks.forEach { mark(copy, it) } }
55
+ continue
56
+ }
57
+ val button = buttonIn(topBar.buttons, key) ?: continue
58
+ val menu = button.menu ?: continue
59
+ button.menu = menu.copyMenu().also { copy -> picks.forEach { mark(copy, it) } }
60
+ }
61
+ scope?.let { picked ->
62
+ val searchBar = topBar.searchBar
63
+ if (searchBar.scopes?.any { it.id == picked } == true) searchBar.selectedScope = Text(picked)
64
+ }
65
+ return resolved
66
+ }
67
+
68
+ /**
69
+ * The app merged [delta] into the node. A choice leaves when what it was made in is gone from [after] (the node's
70
+ * options as they resolve after the merge, without this layer), or when [delta] gives its field a value other than
71
+ * the one the node's own options had before the merge ([own]; where they had none, the app's resolution before the
72
+ * merge, [before]). The same value sent again (a re-render) keeps the choice. Returns whether a choice left.
73
+ */
74
+ fun yieldTo(delta: Options, own: Options, before: Options, after: Options): Boolean {
75
+ var changed = false
76
+ val entries = checks.entries.iterator()
77
+ while (entries.hasNext()) {
78
+ val (key, picks) = entries.next()
79
+ val now = menuIn(after, key)
80
+ val given = menuIn(delta, key)
81
+ val was = menuIn(own, key) ?: menuIn(before, key)
82
+ val left = picks.removeAll { itemId ->
83
+ val gone = now == null || levelItems(now.items, itemId) == null
84
+ gone || (given != null && checkedInLevel(given, itemId) != was?.let { checkedInLevel(it, itemId) })
85
+ }
86
+ if (left) changed = true
87
+ if (picks.isEmpty()) entries.remove()
88
+ }
89
+ scope?.let { picked ->
90
+ val given = delta.topBar.searchBar.selectedScope
91
+ val ownScope = own.topBar.searchBar.selectedScope
92
+ val was = (if (ownScope.hasValue()) ownScope else before.topBar.searchBar.selectedScope).get(null)
93
+ val changedByApp = given.hasValue() && given.get(null) != was
94
+ val gone = after.topBar.searchBar.scopes?.any { it.id == picked } != true
95
+ if (changedByApp || gone) {
96
+ scope = null
97
+ changed = true
98
+ }
99
+ }
100
+ return changed
101
+ }
102
+
103
+ companion object {
104
+ // The key of `overflowMenu`, which has no button; no placed button has an empty id.
105
+ private const val OVERFLOW = ""
106
+
107
+ private fun keyOf(buttonId: String?): String = buttonId ?: OVERFLOW
108
+
109
+ /** Whether picking [itemId] in [menu] moves a check: its level has `singleSelection`. */
110
+ fun checks(menu: ButtonMenuOptions, itemId: String): Boolean =
111
+ levelOf(menu.items, menu.singleSelection, itemId) == true
112
+
113
+ /** Moves the check of [itemId]'s level to it, in [menu] itself; nothing in a level without single selection. */
114
+ fun mark(menu: ButtonMenuOptions, itemId: String) {
115
+ mark(menu.items, menu.singleSelection, itemId)
116
+ }
117
+
118
+ private fun buttonIn(buttons: TopBarButtons, id: String): ButtonOptions? =
119
+ buttons.left?.firstOrNull { it.id.get(null) == id } ?: buttons.right?.firstOrNull { it.id.get(null) == id }
120
+
121
+ // The menu the key names in [options]: `overflowMenu`, or the menu of the button with that id.
122
+ private fun menuIn(options: Options, key: String): ButtonMenuOptions? =
123
+ if (key == OVERFLOW) options.topBar.overflowMenu else buttonIn(options.topBar.buttons, key)?.menu
124
+
125
+ // The checked items of the level that holds [itemId] in [menu]: the field a merge gives that level.
126
+ private fun checkedInLevel(menu: ButtonMenuOptions, itemId: String): Set<String>? =
127
+ levelItems(menu.items, itemId)?.filter { it.checked }?.map { it.id }?.toSet()
128
+
129
+ // The items of the level that holds [itemId]; null when it isn't in [elements].
130
+ private fun levelItems(
131
+ elements: List<ButtonMenuOptions.Element>,
132
+ itemId: String,
133
+ ): List<ButtonMenuOptions.Item>? {
134
+ val items = elements.filterIsInstance<ButtonMenuOptions.Item>()
135
+ if (items.any { it.id == itemId }) return items
136
+ return elements.filterIsInstance<ButtonMenuOptions.Submenu>().firstNotNullOfOrNull {
137
+ levelItems(it.items, itemId)
138
+ }
139
+ }
140
+
141
+ // Whether the level holding [itemId] has single selection; null when [itemId] isn't in [elements].
142
+ private fun levelOf(elements: List<ButtonMenuOptions.Element>, single: Boolean, itemId: String): Boolean? {
143
+ if (elements.any { it is ButtonMenuOptions.Item && it.id == itemId }) return single
144
+ for (element in elements) {
145
+ if (element !is ButtonMenuOptions.Submenu) continue
146
+ levelOf(element.items, element.singleSelection, itemId)?.let { return it }
147
+ }
148
+ return null
149
+ }
150
+
151
+ private fun mark(elements: List<ButtonMenuOptions.Element>, single: Boolean, itemId: String): Boolean {
152
+ if (elements.any { it is ButtonMenuOptions.Item && it.id == itemId }) {
153
+ if (single) elements.forEach { if (it is ButtonMenuOptions.Item) it.checked = it.id == itemId }
154
+ return true
155
+ }
156
+ return elements.any { it is ButtonMenuOptions.Submenu && mark(it.items, it.singleSelection, itemId) }
157
+ }
158
+ }
159
+ }
@@ -1,7 +1,5 @@
1
1
  package dev.apexnative.navigation.react
2
2
 
3
- import android.content.Context
4
- import com.facebook.react.ReactApplication
5
3
  import com.facebook.react.bridge.Arguments
6
4
  import com.facebook.react.bridge.WritableMap
7
5
  import dev.apexnative.navigation.react.events.EventContract
@@ -19,11 +17,13 @@ internal object ArrangementStates {
19
17
 
20
18
  private val states = ConcurrentHashMap<String, State>()
21
19
 
22
- /** Keeps the screen's state, and tells JavaScript when it changed and [notify] holds. Main thread only. */
23
- fun set(context: Context, screenId: String, state: State, notify: Boolean) {
20
+ /**
21
+ * Keeps the screen's state, and tells JavaScript when it changed and [notify] holds, through [emitter], the one of
22
+ * the JavaScript the screen was made for: silent once it went. Main thread only.
23
+ */
24
+ fun set(screenId: String, state: State, notify: Boolean, emitter: EventEmitter) {
24
25
  if (states.put(screenId, state) == state || !notify) return
25
- val reactContext = (context.applicationContext as? ReactApplication)?.reactHost?.currentReactContext ?: return
26
- EventEmitter(reactContext).emitArrangementChanged(screenId, toMap(state))
26
+ emitter.emitArrangementChanged(screenId, toMap(state))
27
27
  }
28
28
 
29
29
  /** `{ placement, layout, front, hidden }`, empty for a screen outside a pair. Any thread. */
@@ -3,13 +3,14 @@ package dev.apexnative.navigation.react
3
3
  import android.util.Log
4
4
  import com.facebook.react.bridge.Promise
5
5
  import dev.apexnative.navigation.react.events.EventEmitter
6
- import dev.apexnative.navigation.react.events.NavigationStateReporter
6
+ import dev.apexnative.navigation.react.events.NavigationState
7
7
  import dev.apexnative.navigation.utils.NavigationLog
8
8
 
9
9
  /**
10
10
  * The one answer of a command with a promise: [resolve] or [reject], once; after that, and once the JavaScript it came
11
- * from went ([generation]), nothing. [CommandRunner] makes it on the main thread for each command; area code that takes
12
- * a [CommandListener] answers through [asListener]. Main thread only.
11
+ * from went ([generation]), nothing. It owns the command's one state step ([reportStateNow]). [CommandRunner] makes it
12
+ * on the main thread for each command; area code that takes a [CommandListener] answers through [asListener]. Main
13
+ * thread only.
13
14
  */
14
15
  internal class CommandAnswer(
15
16
  /** The method of the command: `push`, `setRoot`, … */
@@ -27,22 +28,39 @@ internal class CommandAnswer(
27
28
  var isAnswered = false
28
29
  private set
29
30
 
31
+ // The state step of this answer ran ([reportStateNow]).
32
+ private var stateReported = false
33
+
30
34
  /** Whether an answer now would reach the JavaScript the command came from. */
31
35
  val canAnswer: Boolean
32
36
  get() = !isAnswered && ReloadCoordinator.isCurrent(generation)
33
37
 
34
38
  /**
35
- * Resolves the promise with [value]. For the commands that change what shows, the navigation state goes first,
36
- * when it changed, so JavaScript has it when its `await` goes on; `ANN.CommandCompleted` follows the promise, for
37
- * the commands that show or dismiss. A failure of either is logged; the promise goes all the same.
39
+ * The navigation state now, sent when it changed, once per answer: the end of the command's transition asks for it
40
+ * after the DidAppear it sends and before the transition's own events (`AppearanceDispatcher.endTransition`);
41
+ * otherwise [resolve] asks for it before the promise. A later call sends nothing, and nothing goes for a JavaScript
42
+ * that went. A failure of the step is logged and costs only the state.
43
+ */
44
+ fun reportStateNow() {
45
+ if (stateReported) return
46
+ stateReported = true
47
+ if (!ReloadCoordinator.isCurrent(generation)) return
48
+ guarded("report the navigation state") { events.stateNow() }
49
+ }
50
+
51
+ /**
52
+ * Resolves the promise with [value]. For the commands that change what shows (`CommandContract.STATE`), the
53
+ * navigation state goes first, when it changed and the command's transition didn't send it already
54
+ * ([reportStateNow]), so JavaScript has it when its `await` goes on; `ANN.CommandCompleted` follows the promise,
55
+ * for the commands that show or dismiss. A failure of either is logged; the promise goes all the same.
38
56
  */
39
57
  fun resolve(value: Any?) {
40
58
  if (!canAnswer) return
41
59
  isAnswered = true
42
- if (commandName in STATE_COMMANDS) guarded("report the navigation state") { events.beforeAnswer() }
60
+ if (commandName in CommandContract.STATE) reportStateNow()
43
61
  promise.resolve(value)
44
62
  val commandId = commandId
45
- if (commandName in COMPLETED_COMMANDS && commandId != null) {
63
+ if (commandName in CommandContract.COMPLETED && commandId != null) {
46
64
  guarded("send ANN.CommandCompleted") { events.commandCompleted(commandName, commandId, clock()) }
47
65
  }
48
66
  }
@@ -65,44 +83,19 @@ internal class CommandAnswer(
65
83
  log("[ApexNavigation] $commandName: ${ErrorTexts.couldNot(what, ErrorTexts.reasonOf(e))}")
66
84
  }
67
85
  }
68
-
69
- companion object {
70
- // The commands that send `ANN.CommandCompleted` (EVT-CommandCompleted).
71
- val COMPLETED_COMMANDS = setOf(
72
- "setRoot",
73
- "push",
74
- "pop",
75
- "popTo",
76
- "popToRoot",
77
- "setStackRoot",
78
- "showInColumn",
79
- "showModal",
80
- "dismissModal",
81
- "dismissAllModals",
82
- "showOverlay",
83
- "dismissOverlay",
84
- "dismissAllOverlays",
85
- )
86
-
87
- // The commands whose answer comes after the navigation state: those and `setColumnShown`.
88
- val STATE_COMMANDS = COMPLETED_COMMANDS + "setColumnShown"
89
- }
90
86
  }
91
87
 
92
88
  /** What goes to JavaScript around the answer of a command. Main thread only. */
93
89
  internal interface AnswerEvents {
94
- /** Right before the promise resolves: the navigation state, when it changed. */
95
- fun beforeAnswer()
90
+ /** The answer's one state step ([CommandAnswer.reportStateNow]): the navigation state now, when it changed. */
91
+ fun stateNow()
96
92
 
97
93
  /** After the promise resolved: `ANN.CommandCompleted`. */
98
94
  fun commandCompleted(commandName: String, commandId: String, completionTime: Long)
99
95
 
100
96
  /** Through the sender of the JavaScript that runs now. */
101
97
  object Current : AnswerEvents {
102
- override fun beforeAnswer() {
103
- val emitter = EventEmitter.current()
104
- if (emitter.canSend) NavigationStateReporter.updateNow(emitter)
105
- }
98
+ override fun stateNow() = NavigationState.updateNow()
106
99
 
107
100
  override fun commandCompleted(commandName: String, commandId: String, completionTime: Long) {
108
101
  EventEmitter.current().emitCommandCompleted(commandName, commandId, completionTime)
@@ -0,0 +1,59 @@
1
+ // Made by scripts/contract.mjs from src/interfaces/contract/commands.json: change that file and run the script again.
2
+
3
+ package dev.apexnative.navigation.react
4
+
5
+ /**
6
+ * What the commands with a promise answer, the same table as iOS. What each resolves with:
7
+ * - `setRoot`: the id of the root, once the root and the layout's modals and overlays show.
8
+ * - `push`: the id of the screen that shows; of a pair, the pair's id.
9
+ * - `preparePush`: the frame `{ width, height, padding }` of the prepared screen, or null; always null on iOS.
10
+ * - `pop`: the id of the screen that went.
11
+ * - `popTo`: the id of the screen it went back to.
12
+ * - `popToRoot`: the id of the first screen of the stack.
13
+ * - `setStackRoot`: the id of the new top screen.
14
+ * - `showModal`: the id of the modal's root.
15
+ * - `dismissModal`: the id of the modal's root.
16
+ * - `dismissAllModals`: an empty string.
17
+ * - `showOverlay`: the id of the overlay's root.
18
+ * - `dismissOverlay`: the id of the overlay's root.
19
+ * - `dismissAllOverlays`: an empty string.
20
+ * - `showInColumn`: the id of the screen that shows.
21
+ * - `setColumnShown`: nothing once the columns stopped moving (`undefined` on iOS, `null` on Android).
22
+ * - `getLaunchLink`: the link that launched the app, or null.
23
+ */
24
+ internal object CommandContract {
25
+ /** The commands whose answer comes after the navigation state: every one that may change what shows. */
26
+ val STATE: Set<String> = setOf(
27
+ "setRoot",
28
+ "push",
29
+ "pop",
30
+ "popTo",
31
+ "popToRoot",
32
+ "setStackRoot",
33
+ "showModal",
34
+ "dismissModal",
35
+ "dismissAllModals",
36
+ "showOverlay",
37
+ "dismissOverlay",
38
+ "dismissAllOverlays",
39
+ "showInColumn",
40
+ "setColumnShown",
41
+ )
42
+
43
+ /** The commands that send `ANN.CommandCompleted` after their answer: every one that shows or dismisses. */
44
+ val COMPLETED: Set<String> = setOf(
45
+ "setRoot",
46
+ "push",
47
+ "pop",
48
+ "popTo",
49
+ "popToRoot",
50
+ "setStackRoot",
51
+ "showModal",
52
+ "dismissModal",
53
+ "dismissAllModals",
54
+ "showOverlay",
55
+ "dismissOverlay",
56
+ "dismissAllOverlays",
57
+ "showInColumn",
58
+ )
59
+ }
@@ -9,7 +9,6 @@ import android.os.Build
9
9
  import androidx.lifecycle.DefaultLifecycleObserver
10
10
  import androidx.lifecycle.Lifecycle
11
11
  import androidx.lifecycle.LifecycleOwner
12
- import com.facebook.react.ReactApplication
13
12
  import com.facebook.react.bridge.Arguments
14
13
  import com.facebook.react.bridge.WritableMap
15
14
  import dev.apexnative.navigation.react.events.EventContract
@@ -19,17 +18,23 @@ import java.lang.ref.WeakReference
19
18
 
20
19
  /**
21
20
  * The device's hinge (`useHinge`): its state from the fold in the activity's window, its angle from the hinge angle
22
- * sensor (Android 11+). Android has no public state of a closed device: closed is a window without a fold and an
23
- * angle under 90°. No sensor and no fold: no hinge, `{}`. It runs while JavaScript has a hook mounted
24
- * (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value is not sent twice. Main
25
- * thread only, but [get].
21
+ * sensor (Android 11+), in degrees to a tenth as on iOS. Android has no public state of a closed device: closed is a
22
+ * window without a fold, or an angle under 90° ([valueOf]). No sensor and no fold: no hinge, `{}`. It runs while
23
+ * JavaScript has a hook mounted (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value
24
+ * is not sent twice. The values go to the JavaScript that turned the updates on (`EventEmitter.current` then): after a
25
+ * reload, until the updates stop, they go nowhere. Main thread only, but [get].
26
26
  */
27
27
  internal object Hinge {
28
- data class Value(val state: String, val angle: Float?)
28
+ /** A value of the hinge: its state, and its angle in degrees to a tenth; none while the state is unknown. */
29
+ data class Value(val state: String, val angle: Double?)
30
+
31
+ private const val UNKNOWN = "unknown"
29
32
 
30
33
  private const val CLOSED_BELOW = 90f
31
34
 
32
35
  private var enabled = false
36
+ // The sender of the JavaScript that turned the updates on; none while they are off.
37
+ private var emitter: EventEmitter? = null
33
38
  private var activity: WeakReference<Activity>? = null
34
39
  private var folds: WindowFolds? = null
35
40
  private var sensorManager: SensorManager? = null
@@ -60,9 +65,11 @@ internal object Hinge {
60
65
  fun setEnabled(activity: Activity?, enabled: Boolean) {
61
66
  this.enabled = enabled
62
67
  if (!enabled) {
68
+ emitter = null
63
69
  detach()
64
70
  return
65
71
  }
72
+ emitter = EventEmitter.current()
66
73
  activity?.let(::attachTo)
67
74
  update()
68
75
  }
@@ -128,28 +135,32 @@ internal object Hinge {
128
135
  if (!enabled) return
129
136
  val folds = folds ?: return
130
137
  if (!folds.hasInfo || (listening && angle == null)) return
131
- val fold = folds.folds.firstOrNull()
132
- val angle = angle
133
- val value = if (fold == null && sensor == null) {
134
- null
135
- } else {
136
- val state = when {
137
- fold?.halfOpened == true -> "partiallyOpen"
138
- fold != null -> "fullyOpen"
139
- angle != null && angle < CLOSED_BELOW -> "closed"
140
- else -> "unknown"
141
- }
142
- Value(state, angle)
143
- }
138
+ val value = valueOf(folds.folds.firstOrNull(), sensor != null, angle)
144
139
  if (value == last) return
145
140
  last = value
146
- val reactContext = (activity?.get()?.application as? ReactApplication)?.reactHost?.currentReactContext ?: return
147
- EventEmitter(reactContext).emitHingeChanged(toMap(value))
141
+ emitter?.emitHingeChanged(toMap(value))
142
+ }
143
+
144
+ /**
145
+ * The value of the window's first [fold] and the sensor's [angle]: none without a fold and without a sensor
146
+ * ([hasSensor]). `closed` without a fold or under 90°, else `partiallyOpen` for a half-opened fold and `fullyOpen`
147
+ * for a flat one. The angle goes to a tenth, as iOS gives it: the sensor's noise doesn't make a new value, and it
148
+ * is left out while the state is unknown.
149
+ */
150
+ fun valueOf(fold: WindowFolds.Fold?, hasSensor: Boolean, angle: Float?): Value? {
151
+ if (fold == null && !hasSensor) return null
152
+ val state = when {
153
+ fold == null || (angle != null && angle < CLOSED_BELOW) -> "closed"
154
+ fold.halfOpened -> "partiallyOpen"
155
+ else -> "fullyOpen"
156
+ }
157
+ val tenths = angle?.let { Math.round(it * 10.0) / 10.0 }
158
+ return Value(state, tenths.takeIf { state != UNKNOWN })
148
159
  }
149
160
 
150
161
  private fun toMap(value: Value?): WritableMap = Arguments.createMap().apply {
151
162
  if (value == null) return@apply
152
163
  putString(EventContract.HingeChanged.STATE, value.state)
153
- value.angle?.let { putDouble(EventContract.HingeChanged.ANGLE, it.toDouble()) }
164
+ if (value.state != UNKNOWN) value.angle?.let { putDouble(EventContract.HingeChanged.ANGLE, it) }
154
165
  }
155
166
  }
@@ -211,7 +211,7 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
211
211
  layout: ReadableMap,
212
212
  promise: Promise,
213
213
  ) {
214
- // Android has one column to show screens in: the secondary pane (`showInColumn`).
214
+ // Android's split view has the primary and the secondary pane; another column rejects (`showInColumn`).
215
215
  val what = "show the screen '${LayoutReader.idOf(layout)}' in the '$column' column"
216
216
  runner.run(
217
217
  name = "showInColumn",
@@ -219,7 +219,7 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
219
219
  what = what,
220
220
  promise = promise,
221
221
  check = { LayoutReader.node(layout, context = appContext()) },
222
- ) { node, answer -> navigator().stackCommands.showInColumn(componentId, node, answer, what) }
222
+ ) { node, answer -> navigator().stackCommands.showInColumn(componentId, column, node, answer, what) }
223
223
  }
224
224
 
225
225
  override fun setColumnShown(
@@ -295,9 +295,10 @@ class NavigationTurboModule(reactContext: ReactApplicationContext) : NativeANNTu
295
295
  })
296
296
  }
297
297
 
298
- // The navigation of the activity the command runs in; the runner checked there is one.
298
+ // The navigation of the activity the command runs in; the runner checked there is an activity. One without its
299
+ // navigation yet (or any more) has nowhere to show screens either: the command rejects with the runner's own text.
299
300
  private fun navigator(): Navigator =
300
- requireNotNull(Navigators.current()?.navigator) { "the activity has no navigation" }
301
+ Navigators.current()?.navigator ?: throw LayoutException(ErrorTexts.nowhereToShow())
301
302
 
302
303
  // The app's context, which the colors of the options resolve platform colors with.
303
304
  private fun appContext(): Context = reactApplicationContext.applicationContext
@@ -0,0 +1,48 @@
1
+ package dev.apexnative.navigation.react
2
+
3
+ import dev.apexnative.navigation.NavigationApplication
4
+ import dev.apexnative.navigation.utils.UiThread
5
+
6
+ /**
7
+ * The calls JavaScript made to the module that may not have reached the main thread yet: [after] runs a task on the
8
+ * main thread behind every one of them. A screen's first mount reaches the main thread from Fabric while a
9
+ * `mergeOptions` of the same React commit (its layout effects: `useScreenOptions`) may still be on its way: an
10
+ * asynchronous method goes from the JavaScript thread to the module's thread (`BridgelessNativeMethodCallInvoker`),
11
+ * which reads its options there and posts its body to the main thread ([CommandRunner]). The task goes the same way: a
12
+ * turn of the JavaScript thread, which comes after the task of the commit with its layout effects; then the module's
13
+ * thread, behind the calls that task made; then the main thread, behind their bodies. Each of the three runs its queue
14
+ * in order, so every call JavaScript made before the mount has applied when the task runs.
15
+ *
16
+ * Without JavaScript (none yet, a reload, a queue that ended) the task is posted to the main thread at once: nothing is
17
+ * on its way. A queue that ends while the task is on it drops it, and a wait with no limit of its own (a push with
18
+ * `waitForRender: true` whose content has mounted: the task is its only end) would not end; a queue ends only as the
19
+ * JavaScript goes (a reload, the host destroyed), and then the layouts go with it (`ReloadCoordinator`), the stack
20
+ * and its waiting push too. Main thread.
21
+ */
22
+ internal object PendingCalls {
23
+ /** A queue the task goes through: it runs what it takes in order, and says false when it ended and took nothing. */
24
+ fun interface Queue {
25
+ fun run(task: Runnable): Boolean
26
+ }
27
+
28
+ /** Runs [task] on the main thread, on a later turn, after every call JavaScript made so far has applied there. */
29
+ fun after(task: () -> Unit) {
30
+ val context = NavigationApplication.instanceOrNull?.reactGatewayOrNull?.currentReactContext
31
+ ?.takeIf { it.hasActiveReactInstance() }
32
+ val js = context?.jsMessageQueueThread?.let { Queue(it::runOnQueue) }
33
+ val module = context?.nativeModulesMessageQueueThread?.let { Queue(it::runOnQueue) }
34
+ after(js, module, UiThread::post, task)
35
+ }
36
+
37
+ /**
38
+ * The way of [after] over the queues given: [js], then [module], then [main]; straight to [main] without one of
39
+ * them, or from where a queue refused it. The unit tests run it over their own queues.
40
+ */
41
+ fun after(js: Queue?, module: Queue?, main: (Runnable) -> Unit, task: () -> Unit) {
42
+ val onMain = Runnable(task)
43
+ if (js == null || module == null) return main(onMain)
44
+ val toMain = Runnable { main(onMain) }
45
+ val toModule = Runnable { if (!module.run(toMain)) main(onMain) }
46
+ if (!js.run(toModule)) main(onMain)
47
+ }
48
+ }