@applicaster/zapp-react-native-ui-components 16.0.0-alpha.1535029240 → 16.0.0-alpha.1701179921

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 (92) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  7. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  8. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +64 -0
  9. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  10. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +18 -0
  11. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  12. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  13. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  14. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  15. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  16. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  17. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  18. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  19. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  20. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +2 -3
  21. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  22. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  23. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  24. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
  25. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  26. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  27. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  28. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  29. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  30. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +87 -97
  31. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +87 -126
  32. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -83
  33. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +252 -123
  34. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  35. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  36. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  37. package/Components/ModalComponent/AudioPlayer/Components/index.ts +3 -3
  38. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +356 -0
  39. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +521 -0
  40. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  41. package/Components/ModalComponent/BottomSheetModalContent.tsx +505 -66
  42. package/Components/ModalComponent/Button/index.tsx +5 -3
  43. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  44. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  45. package/Components/ModalComponent/Header/index.tsx +20 -7
  46. package/Components/ModalComponent/Header/utils.ts +1 -1
  47. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  48. package/Components/ModalComponent/SortableList.tsx +156 -0
  49. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  50. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  51. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  52. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  53. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  54. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  55. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  56. package/Components/ModalComponent/index.tsx +18 -9
  57. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  58. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  59. package/Components/ModalComponent/presets/index.ts +27 -0
  60. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  61. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  62. package/Components/ModalComponent/presets/types.ts +35 -0
  63. package/Components/ModalComponent/showTextInput.ts +52 -0
  64. package/Components/ModalComponent/utils.ts +82 -21
  65. package/Components/OfflineHandler/hooks.ts +0 -12
  66. package/Components/PlayerContainer/PlayerContainer.tsx +19 -8
  67. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  68. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
  69. package/Components/River/ComponentsMap/ComponentsMap.tsx +85 -59
  70. package/Components/River/River.tsx +4 -0
  71. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +138 -75
  72. package/Components/River/__tests__/componentsMap.test.js +8 -3
  73. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  74. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  75. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  76. package/Components/Screen/index.tsx +5 -0
  77. package/Components/Screen/navigationHandler.ts +8 -8
  78. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  79. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  80. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  81. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  82. package/Components/Transitioner/Transitioner.tsx +2 -3
  83. package/Components/Transitioner/__tests__/transitioner.test.js +83 -2
  84. package/Components/VideoLive/LiveImageManager.ts +9 -3
  85. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  86. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  87. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  88. package/Hooks/useEntryActionsExpander.ts +63 -0
  89. package/assets/empty-Info.png +0 -0
  90. package/package.json +6 -5
  91. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -332
  92. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -141,6 +141,10 @@ const {
141
141
  useGetScreenOrientation,
142
142
  } = require("@applicaster/zapp-react-native-utils/appUtils/orientationHelper");
143
143
 
144
+ const {
145
+ useIsScreenActive,
146
+ } = require("@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive");
147
+
144
148
  const navigator = {};
145
149
  const navBarProps = { nav_items: [] };
146
150
  const screenData = {};
@@ -195,4 +199,24 @@ describe("<Screen Component />", () => {
195
199
  expect(toJSON()).toMatchSnapshot();
196
200
  });
197
201
  });
202
+
203
+ describe("when the screen is inactive", () => {
204
+ afterEach(() => {
205
+ useIsScreenActive.mockReturnValue(true);
206
+ });
207
+
208
+ it("hides the screen from VoiceOver (iOS) and TalkBack (Android)", () => {
209
+ useIsScreenActive.mockReturnValue(false);
210
+
211
+ const { UNSAFE_root } = renderWithProviders(
212
+ <Screen {...screenProps} />,
213
+ store
214
+ );
215
+
216
+ expect(UNSAFE_root.findByType(View).props).toMatchObject({
217
+ accessibilityElementsHidden: true,
218
+ importantForAccessibility: "no-hide-descendants",
219
+ });
220
+ });
221
+ });
198
222
  });
@@ -13,6 +13,7 @@ exports[`<Screen Component /> when the navbar should be hidden renders correctly
13
13
  }
14
14
  >
15
15
  <View
16
+ accessibilityElementsHidden={false}
16
17
  importantForAccessibility="yes"
17
18
  style={
18
19
  {
@@ -58,6 +59,7 @@ exports[`<Screen Component /> when the navbar should show renders correctly 1`]
58
59
  }
59
60
  >
60
61
  <View
62
+ accessibilityElementsHidden={false}
61
63
  importantForAccessibility="yes"
62
64
  style={
63
65
  {
@@ -121,6 +121,11 @@ export function Screen(_props: Props) {
121
121
  ref={ref}
122
122
  style={style}
123
123
  importantForAccessibility={!isActive ? "no-hide-descendants" : "yes"}
124
+ // `importantForAccessibility` only affects Android (see RN docs).
125
+ // Without the iOS counterpart below, VoiceOver keeps traversing and
126
+ // focusing elements on an inactive/underlying screen (e.g. Albums In
127
+ // Order) even after a new screen (e.g. an Album) is pushed on top.
128
+ accessibilityElementsHidden={!isActive}
124
129
  >
125
130
  {isReady ? (
126
131
  <>
@@ -18,10 +18,9 @@ function shouldShowNavigation(route, screenData) {
18
18
  return false;
19
19
  }
20
20
 
21
- if (
22
- screenData.type === "qb_search_screen" ||
23
- screenData?.targetScreen?.type === "qb_search_screen"
24
- ) {
21
+ const currentScreenData = screenData?.targetScreen || screenData;
22
+
23
+ if (currentScreenData.type === "qb_search_screen") {
25
24
  return false;
26
25
  }
27
26
 
@@ -31,10 +30,11 @@ function shouldShowNavigation(route, screenData) {
31
30
  return hookPlugin?.showNavBar || hookPlugin?.presentFullScreen !== true;
32
31
  }
33
32
 
34
- if (
35
- screenData?.general?.allow_screen_plugin_presentation ||
36
- screenData?.targetScreen?.general?.allow_screen_plugin_presentation
37
- ) {
33
+ if (currentScreenData?.general?.hide_menu) {
34
+ return false;
35
+ }
36
+
37
+ if (currentScreenData?.general?.allow_screen_plugin_presentation) {
38
38
  return false;
39
39
  }
40
40
 
@@ -42,9 +42,25 @@ export class ScreenRevealManager {
42
42
  private subject$ = new Subject<void>();
43
43
  private subscription: Subscription;
44
44
 
45
- constructor(componentsToRender: ZappUIComponent[], callback: Callback) {
45
+ /**
46
+ * @param initialNumberToLoad how many components to wait for before revealing
47
+ * the screen. Staggering exists so a screen full of network-backed components
48
+ * does not load them all at once. A caller that has nothing to stagger - see
49
+ * `disableIncrementalLoading` on the HOC - passes the full count here so the
50
+ * reveal waits for exactly what gets rendered. Omitted, the count is derived
51
+ * from the components themselves.
52
+ */
53
+ constructor(
54
+ componentsToRender: ZappUIComponent[],
55
+ callback: Callback,
56
+ initialNumberToLoad?: number
57
+ ) {
46
58
  this.numberOfComponentsWaitToLoadBeforePresent =
47
- getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender);
59
+ initialNumberToLoad == null
60
+ ? getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender)
61
+ : // Clamped like the computed path: waiting for components that do not
62
+ // exist would hold the screen behind its overlay until the timeout.
63
+ Math.min(initialNumberToLoad, componentsToRender.length);
48
64
 
49
65
  this.renderingState = makeListOf<ComponentLoadingState>(
50
66
  COMPONENT_LOADING_STATE.UNKNOWN,
@@ -22,6 +22,42 @@ import { makeListOf } from "@applicaster/zapp-react-native-utils/arrayUtils";
22
22
  import { isFirstComponentGallery } from "@applicaster/zapp-react-native-utils/componentsUtils";
23
23
  import { withTimeout$ } from "@applicaster/zapp-react-native-utils/idleUtils";
24
24
 
25
+ describe("ScreenRevealManager explicit component count", () => {
26
+ beforeEach(() => {
27
+ (isFirstComponentGallery as jest.Mock).mockReturnValue(false);
28
+
29
+ (makeListOf as jest.Mock).mockImplementation((value, length) =>
30
+ Array(length).fill(value)
31
+ );
32
+
33
+ (withTimeout$ as jest.Mock).mockReturnValue(new Subject());
34
+ });
35
+
36
+ it("waits for the number of components it is given, not the computed default", () => {
37
+ const components = Array(6).fill({}) as any;
38
+
39
+ const manager = new ScreenRevealManager(components, jest.fn(), 6);
40
+
41
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(6);
42
+ });
43
+
44
+ it("never waits for more components than it was handed", () => {
45
+ const components = Array(3).fill({}) as any;
46
+
47
+ const manager = new ScreenRevealManager(components, jest.fn(), 10);
48
+
49
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
50
+ });
51
+
52
+ it("falls back to the computed default when given no explicit count", () => {
53
+ const components = Array(6).fill({}) as any;
54
+
55
+ const manager = new ScreenRevealManager(components, jest.fn());
56
+
57
+ expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
58
+ });
59
+ });
60
+
25
61
  describe("ScreenRevealManager", () => {
26
62
  let mockCallback: jest.Mock;
27
63
  let timeout$: Subject<void>;
@@ -9,6 +9,12 @@ import {
9
9
  TIMEOUT,
10
10
  } from "../withScreenRevealManager";
11
11
 
12
+ jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
13
+ useTheme: () => ({ app_background_color: "#000000" }),
14
+ }));
15
+
16
+ jest.mock("../Overlay", () => ({ Overlay: () => null }));
17
+
12
18
  // jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
13
19
 
14
20
  const MockComponent = ({
@@ -37,6 +43,56 @@ const MockComponent = ({
37
43
 
38
44
  const WrappedComponent = withScreenRevealManager(MockComponent);
39
45
 
46
+ describe("withScreenRevealManager disableIncrementalLoading", () => {
47
+ const components = (count: number) =>
48
+ Array(count).fill({ component_type: "grid-qb" });
49
+
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ it("asks for every component at once when incremental loading is disabled", () => {
55
+ render(
56
+ <WrappedComponent
57
+ componentsToRender={components(6)}
58
+ disableIncrementalLoading
59
+ />
60
+ );
61
+
62
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
63
+ 6
64
+ );
65
+ });
66
+
67
+ it("still tracks the component count after it changes", () => {
68
+ const { rerender } = render(
69
+ <WrappedComponent
70
+ componentsToRender={components(6)}
71
+ disableIncrementalLoading
72
+ />
73
+ );
74
+
75
+ rerender(
76
+ <WrappedComponent
77
+ componentsToRender={components(9)}
78
+ disableIncrementalLoading
79
+ />
80
+ );
81
+
82
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
83
+ 9
84
+ );
85
+ });
86
+
87
+ it("loads incrementally when the flag is not set", () => {
88
+ render(<WrappedComponent componentsToRender={components(6)} />);
89
+
90
+ expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
91
+ 3
92
+ );
93
+ });
94
+ });
95
+
40
96
  describe.skip("withScreenRevealManager", () => {
41
97
  beforeEach(() => {
42
98
  jest.clearAllMocks();
@@ -21,11 +21,23 @@ export const SHOWN = 1; // opacity = 1
21
21
  type Props = {
22
22
  componentsToRender: ZappUIComponent[];
23
23
  backgroundColor?: string;
24
+ /**
25
+ * Renders every component at once instead of revealing them a few at a time.
26
+ *
27
+ * For a screen that builds its own components and holds all of them already -
28
+ * a form, say - staggering buys nothing and costs: each rebuild of the list
29
+ * tears it back down to the first few and lets the rest crawl in again.
30
+ *
31
+ * This is a flag rather than a count on purpose. A count would have to be
32
+ * re-read whenever the number of components changes, while "do not stagger"
33
+ * stays true whatever the screen ends up holding.
34
+ */
35
+ disableIncrementalLoading?: boolean;
24
36
  };
25
37
 
26
38
  export const withScreenRevealManager = (Component) => {
27
39
  return function WithScreenRevealManager(props: Props) {
28
- const { componentsToRender } = props;
40
+ const { componentsToRender, disableIncrementalLoading } = props;
29
41
 
30
42
  const [isContentReadyToBeShown, setIsContentReadyToBeShown] =
31
43
  React.useState(false);
@@ -42,7 +54,8 @@ export const withScreenRevealManager = (Component) => {
42
54
  () =>
43
55
  new ScreenRevealManager(
44
56
  componentsToRender,
45
- handleSetIsContentReadyToBeShown
57
+ handleSetIsContentReadyToBeShown,
58
+ disableIncrementalLoading ? componentsToRender.length : undefined
46
59
  )
47
60
  );
48
61
 
@@ -72,7 +85,7 @@ export const withScreenRevealManager = (Component) => {
72
85
 
73
86
  if (isFirstComponentScreenPicker(componentsToRender)) {
74
87
  // for screen-picker with have additional internal ComponentsMap, no need to add this wrapper
75
- return <Component {...props} />;
88
+ return <Component {...props} initialNumberToLoad={1} />;
76
89
  }
77
90
 
78
91
  return (
@@ -80,7 +93,11 @@ export const withScreenRevealManager = (Component) => {
80
93
  <Component
81
94
  {...props}
82
95
  initialNumberToLoad={
83
- managerRef.current.numberOfComponentsWaitToLoadBeforePresent
96
+ // Recomputed on every render, so the count follows the components
97
+ // even though the manager settled its own expectation at mount.
98
+ disableIncrementalLoading
99
+ ? componentsToRender.length
100
+ : managerRef.current.numberOfComponentsWaitToLoadBeforePresent
84
101
  }
85
102
  onLoadFinishedFromScreenRevealManager={
86
103
  managerRef.current.onLoadFinished
@@ -129,8 +129,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
129
129
  * The last scene in the array is removed in the "backTransitionEnd" callback,
130
130
  * only after the animation is complete.
131
131
  */
132
- backTransitionStart() {
133
- const previousRoute = this.props.navigator?.currentRoute;
132
+ backTransitionStart(previousRoute: string) {
134
133
  const currentRoute = this.props.navigator?.currentRoute;
135
134
 
136
135
  const diffLengthRoutes =
@@ -242,7 +241,7 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
242
241
  break;
243
242
 
244
243
  case NAV_ACTION_BACK: {
245
- this.backTransitionStart();
244
+ this.backTransitionStart(previousRoute);
246
245
  break;
247
246
  }
248
247
 
@@ -1,8 +1,13 @@
1
1
  import React from "react";
2
2
  import { Text, View } from "react-native";
3
3
 
4
- import { TransitionerComponent } from "../Transitioner";
5
- import { render } from "@testing-library/react-native";
4
+ import { AnimationManager } from "../AnimationManager";
5
+ import {
6
+ NAV_ACTION_BACK,
7
+ NAV_ACTION_PUSH,
8
+ TransitionerComponent,
9
+ } from "../Transitioner";
10
+ import { act, render } from "@testing-library/react-native";
6
11
 
7
12
  jest.mock("../Scene", () => {
8
13
  const View = jest.requireActual("react-native").View;
@@ -40,6 +45,27 @@ const transitionerComponentProps = {
40
45
 
41
46
  jest.mock("uuid");
42
47
 
48
+ const sceneTransitionConfig = () => ({
49
+ push: {
50
+ duration: 0,
51
+ easing: jest.fn(),
52
+ from: { style: {} },
53
+ to: { style: {} },
54
+ },
55
+ back: {
56
+ duration: 0,
57
+ easing: jest.fn(),
58
+ from: { style: {} },
59
+ to: { style: {} },
60
+ },
61
+ });
62
+
63
+ const sceneFor = (route) => (
64
+ <View pathname={route}>
65
+ <Text>{route}</Text>
66
+ </View>
67
+ );
68
+
43
69
  describe("<TransitionerComponent />", () => {
44
70
  it("renders correctly", () => {
45
71
  const wrapper = render(
@@ -50,4 +76,59 @@ describe("<TransitionerComponent />", () => {
50
76
 
51
77
  expect(wrapper.toJSON()).toMatchSnapshot();
52
78
  });
79
+
80
+ it("removes both intermediate scenes when backing out to the tab root", () => {
81
+ jest
82
+ .spyOn(AnimationManager.prototype, "animate")
83
+ .mockImplementation((_, done) => done());
84
+
85
+ const ref = React.createRef();
86
+ const root = "/river/root";
87
+ const child = `${root}/river/child`;
88
+ const grandchild = `${child}/river/grandchild`;
89
+
90
+ const propsFor = (route, previousAction) => ({
91
+ navigator: {
92
+ previousAction,
93
+ currentRoute: route,
94
+ screenData: {},
95
+ },
96
+ transitionConfig: sceneTransitionConfig,
97
+ layoutData: {
98
+ isTablet: false,
99
+ isTabletPortrait: false,
100
+ },
101
+ children: sceneFor(route),
102
+ });
103
+
104
+ const { rerender } = render(
105
+ <TransitionerComponent ref={ref} {...propsFor(root, "REPLACE")} />
106
+ );
107
+
108
+ act(() => {
109
+ rerender(
110
+ <TransitionerComponent
111
+ ref={ref}
112
+ {...propsFor(child, NAV_ACTION_PUSH)}
113
+ />
114
+ );
115
+ });
116
+
117
+ act(() => {
118
+ rerender(
119
+ <TransitionerComponent
120
+ ref={ref}
121
+ {...propsFor(grandchild, NAV_ACTION_PUSH)}
122
+ />
123
+ );
124
+ });
125
+
126
+ act(() => {
127
+ rerender(
128
+ <TransitionerComponent ref={ref} {...propsFor(root, NAV_ACTION_BACK)} />
129
+ );
130
+ });
131
+
132
+ expect(ref.current.state.scenes.map((scene) => scene.key)).toEqual([root]);
133
+ });
53
134
  });
@@ -5,7 +5,7 @@ import {
5
5
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
6
6
  import { setUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
7
7
  import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
8
- import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/conts";
8
+ import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
9
9
  import { loggerLiveImageManager } from "./loggerHelper";
10
10
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
11
11
  import {
@@ -213,10 +213,16 @@ export class LiveImageManager implements PlayerLifecycleListener {
213
213
  log_debug(
214
214
  `pauseItem: live-image not ready, ${item.playerId} - ${item.tag}`
215
215
  );
216
- } else {
217
- item.player.pause();
218
216
  }
219
217
 
218
+ // Pause even when the player is not ready yet: the underlying player may
219
+ // still be loading and would otherwise start playing on its own once it
220
+ // has buffered, leaving the live-image audible underneath the primary
221
+ // player. Note this is best effort - the web controller's pause() only
222
+ // logs while the player component is unmounted - so the player itself also
223
+ // refuses buffer-driven starts when it was never asked to play.
224
+ item.player.pause();
225
+
220
226
  // Fake close event, because we unmount native view
221
227
  item.player.onPlayerClose();
222
228
  item.setMode?.(LiveImageType.Image);
@@ -1,6 +1,8 @@
1
1
  const { LiveImageManager } = require("../LiveImageManager");
2
2
 
3
3
  describe("LiveImageManager", () => {
4
+ const flushPromises = () => new Promise(setImmediate);
5
+
4
6
  it("should subscribe to LiveImageManager events", () => {
5
7
  const sampleListener = {
6
8
  id: "1",
@@ -78,4 +80,61 @@ describe("LiveImageManager", () => {
78
80
  expect(mockOnEnd1).toHaveBeenCalledWith(eventBody);
79
81
  expect(mockOnEnd2).toHaveBeenCalledWith(eventBody);
80
82
  });
83
+
84
+ it("should start a prepared live image when the mounted player loads", async () => {
85
+ const manager = new LiveImageManager();
86
+
87
+ const item = {
88
+ playerId: "live-image",
89
+ tag: "Live image",
90
+ player: {
91
+ playerId: "live-image",
92
+ playerState: { isReadyToPlay: false },
93
+ getEntry: jest.fn(),
94
+ play: jest.fn(),
95
+ },
96
+ getPlayer: jest.fn(),
97
+ prepareForPlayback: jest.fn().mockResolvedValue(true),
98
+ setMode: jest.fn(),
99
+ };
100
+
101
+ manager.playLiveImage(item);
102
+ await flushPromises();
103
+
104
+ expect(item.setMode).toHaveBeenCalledWith("video");
105
+ expect(item.player.play).not.toHaveBeenCalled();
106
+
107
+ manager.onLiveImageVideoLoaded(item);
108
+
109
+ expect(item.player.play).toHaveBeenCalledTimes(1);
110
+ });
111
+
112
+ it("should not start a live image that was paused before the player loaded", async () => {
113
+ const manager = new LiveImageManager();
114
+
115
+ const item = {
116
+ playerId: "live-image",
117
+ tag: "Live image",
118
+ player: {
119
+ playerId: "live-image",
120
+ playerState: { isReadyToPlay: false },
121
+ getEntry: jest.fn(),
122
+ play: jest.fn(),
123
+ pause: jest.fn(),
124
+ onPlayerClose: jest.fn(),
125
+ },
126
+ getPlayer: jest.fn(),
127
+ prepareForPlayback: jest.fn().mockResolvedValue(true),
128
+ setMode: jest.fn(),
129
+ };
130
+
131
+ manager.playLiveImage(item);
132
+ await flushPromises();
133
+ manager.pauseLiveImage(item);
134
+ manager.onLiveImageVideoLoaded(item);
135
+
136
+ expect(item.player.pause).toHaveBeenCalledTimes(1);
137
+ expect(item.player.play).not.toHaveBeenCalled();
138
+ expect(item.setMode).toHaveBeenLastCalledWith("image");
139
+ });
81
140
  });
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { withModalNavigationContextProvider } from "../";
4
+
5
+ describe("withModalNavigationContextProvider", () => {
6
+ it("does not re-render the wrapped component when props are unchanged", () => {
7
+ const Component = jest.fn(() => null);
8
+ const WrappedComponent = withModalNavigationContextProvider(Component);
9
+ const { rerender } = render(<WrappedComponent title="Modal" />);
10
+
11
+ rerender(<WrappedComponent title="Modal" />);
12
+
13
+ expect(Component).toHaveBeenCalledTimes(1);
14
+ });
15
+ });
@@ -6,11 +6,11 @@ export function useModalNavigationContext() {
6
6
  }
7
7
 
8
8
  export function withModalNavigationContextProvider(Component) {
9
- return function WrappedWithModalNavigationContextProvider(props) {
9
+ return React.memo(function WrappedWithModalNavigationContextProvider(props) {
10
10
  return (
11
11
  <ModalNavigationContext.Provider value={true}>
12
12
  <Component {...props} />
13
13
  </ModalNavigationContext.Provider>
14
14
  );
15
- };
15
+ });
16
16
  }
@@ -0,0 +1,63 @@
1
+ import * as React from "react";
2
+
3
+ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutorContext";
4
+ import {
5
+ buildEntryActions,
6
+ EntryActionDeps,
7
+ } from "@applicaster/zapp-react-native-utils/entryActions";
8
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
9
+ import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
10
+ import {
11
+ useCurrentScreenData,
12
+ useScreenContext,
13
+ } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
14
+ import { RegisteredAction } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
15
+
16
+ /**
17
+ * Expands `entry.extensions.entry_action` into invokable actions, against the
18
+ * screen this hook is called from.
19
+ *
20
+ * Entry actions are data on the entry, not an installed capability, so there is
21
+ * nothing to look up by identifier: the surface showing the entry expands them.
22
+ * That also decides which screen they execute against — the one whose tree this
23
+ * hook runs in, rather than whichever screen happened to be current when a
24
+ * global provider was registered.
25
+ *
26
+ * Pass the result to `resolveActionIdentifiers` / `resolvePlayerActions` as
27
+ * `expandEntryActions`.
28
+ */
29
+ export function useEntryActionsExpander(): (
30
+ entry: ZappEntry | ZappFeed
31
+ ) => RegisteredAction[] {
32
+ const { pathname } = useRoute();
33
+
34
+ const screenContext = useScreenContext();
35
+ const screenData = useCurrentScreenData();
36
+ const screenState = screenContext?.options;
37
+ const screenStateStore = useScreenStateStore();
38
+ const actionExecutor = React.useContext(ActionExecutorContext);
39
+
40
+ // Read through a ref so the returned expander keeps a stable identity: it
41
+ // feeds `useMemo` dependency lists on surfaces that re-resolve their actions
42
+ // on every registry change.
43
+ const depsRef = React.useRef<EntryActionDeps>();
44
+
45
+ depsRef.current = {
46
+ actionExecutor,
47
+ actionContext: {
48
+ screenData,
49
+ screenState,
50
+ screenRoute: pathname,
51
+ screenStateStore,
52
+ // From the screen context, the way `useCellClick` builds the same bag.
53
+ // The ZappPipes entry context is a route-keyed map, so reading `.data`
54
+ // off it was always undefined.
55
+ screenEntry: screenContext?.entry,
56
+ },
57
+ };
58
+
59
+ return React.useCallback(
60
+ (entry: ZappEntry | ZappFeed) => buildEntryActions(entry, depsRef.current),
61
+ []
62
+ );
63
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "16.0.0-alpha.1535029240",
3
+ "version": "16.0.0-alpha.1701179921",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,12 +28,13 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-alpha.1535029240",
32
- "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.1535029240",
33
- "@applicaster/zapp-react-native-redux": "16.0.0-alpha.1535029240",
34
- "@applicaster/zapp-react-native-utils": "16.0.0-alpha.1535029240",
31
+ "@applicaster/applicaster-types": "16.0.0-alpha.1701179921",
32
+ "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.1701179921",
33
+ "@applicaster/zapp-react-native-redux": "16.0.0-alpha.1701179921",
34
+ "@applicaster/zapp-react-native-utils": "16.0.0-alpha.1701179921",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
+ "react-native-sortables": "1.7.1",
37
38
  "url": "^0.11.0",
38
39
  "uuid": "^3.3.2"
39
40
  },