@applicaster/zapp-react-native-ui-components 15.1.0-rc.1 → 15.1.0-rc.10

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.
@@ -83,7 +83,7 @@ export function CellRendererResolver({
83
83
 
84
84
  if (!cellRendererPlugin && !isGroup(component)) {
85
85
  logger.warning({
86
- message: "Could not resolve cell builder plugin",
86
+ message: `Could not resolve cell builder plugin: ${component?.component_type}`,
87
87
  data: { component },
88
88
  });
89
89
  }
@@ -107,7 +107,7 @@ describe("ComponentResolverComponent", () => {
107
107
  expect(mockLogger.warning).toHaveBeenNthCalledWith(
108
108
  1,
109
109
  expect.objectContaining({
110
- message: "Could not resolve cell builder plugin",
110
+ message: "Could not resolve cell builder plugin: foo",
111
111
  })
112
112
  );
113
113
 
@@ -133,6 +133,11 @@ export const ScreenContainer = React.memo(function ScreenContainer({
133
133
  []
134
134
  );
135
135
 
136
+ // We need to render menu first and then proceed with screen content,
137
+ // otherwise screen will stay black until everything is loaded and screen
138
+ // rendering put huge load the CPU pushing rendering even further.
139
+ // With this approach, menu will be rendered immediately and screen content
140
+ // will be rendered after paint, which makes it more responsive and prevents black screen.
136
141
  const [navBarReady, setNavBarReady] = React.useState(false);
137
142
 
138
143
  const navBarContainer = (
@@ -262,9 +262,14 @@ const PlayerContainerComponent = (props: Props) => {
262
262
 
263
263
  showNavBar(true);
264
264
  navigator.goBack();
265
- }, [isModal, navigator.goBack, state.playerId, showNavBar]);
265
+ }, [isModal, state.playerId, showNavBar, navigator]);
266
266
 
267
- const playEntry = (entry) => navigator.replaceTop(entry, { mode });
267
+ const pluginConfiguration = React.useMemo(
268
+ () => player?.getPluginConfiguration(),
269
+ [player]
270
+ );
271
+
272
+ const playEntry = (entry: ZappEntry) => navigator.replaceTop(entry, { mode });
268
273
 
269
274
  const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
270
275
  if (!playNextOverlayState.entry) {
@@ -448,13 +453,6 @@ const PlayerContainerComponent = (props: Props) => {
448
453
  }
449
454
  }, []);
450
455
 
451
- const pluginConfiguration = React.useMemo(() => {
452
- return (
453
- playerManager.getPluginConfiguration() ||
454
- R.prop("__plugin_configuration", Player)
455
- );
456
- }, [playerManager.isRegistered()]);
457
-
458
456
  const disableMiniPlayer = React.useMemo(() => {
459
457
  return pluginConfiguration?.disable_mini_player_when_inline;
460
458
  }, [pluginConfiguration]);
@@ -0,0 +1,60 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+
3
+ import { useAfterPaint } from "../useAfterPaint";
4
+
5
+ describe("useAfterPaint", () => {
6
+ let frameCallbacks: FrameRequestCallback[];
7
+ let originalRaf: typeof requestAnimationFrame;
8
+ let originalCancelRaf: typeof cancelAnimationFrame;
9
+
10
+ const flushFrame = () => {
11
+ const callbacks = frameCallbacks;
12
+ frameCallbacks = [];
13
+
14
+ act(() => {
15
+ callbacks.forEach((cb) => cb(0));
16
+ });
17
+ };
18
+
19
+ beforeEach(() => {
20
+ frameCallbacks = [];
21
+ originalRaf = global.requestAnimationFrame;
22
+ originalCancelRaf = global.cancelAnimationFrame;
23
+
24
+ global.requestAnimationFrame = jest.fn((cb: FrameRequestCallback) => {
25
+ frameCallbacks.push(cb);
26
+
27
+ return frameCallbacks.length;
28
+ }) as unknown as typeof requestAnimationFrame;
29
+
30
+ global.cancelAnimationFrame = jest.fn();
31
+ });
32
+
33
+ afterEach(() => {
34
+ global.requestAnimationFrame = originalRaf;
35
+ global.cancelAnimationFrame = originalCancelRaf;
36
+ });
37
+
38
+ it("returns false on the initial render", () => {
39
+ const { result } = renderHook(() => useAfterPaint());
40
+
41
+ expect(result.current).toBe(false);
42
+ });
43
+
44
+ it("stays false after only one animation frame (before paint completes)", () => {
45
+ const { result } = renderHook(() => useAfterPaint());
46
+
47
+ flushFrame();
48
+
49
+ expect(result.current).toBe(false);
50
+ });
51
+
52
+ it("returns true after two animation frames (a real post-paint boundary)", () => {
53
+ const { result } = renderHook(() => useAfterPaint());
54
+
55
+ flushFrame();
56
+ flushFrame();
57
+
58
+ expect(result.current).toBe(true);
59
+ });
60
+ });
@@ -1 +1,3 @@
1
1
  export { useInitialFocus } from "./useInitialFocus";
2
+
3
+ export { useAfterPaint } from "./useAfterPaint";
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+
3
+ export const useAfterPaint = (): boolean => {
4
+ const [painted, setPainted] = React.useState(false);
5
+
6
+ React.useEffect(() => {
7
+ let secondFrame: number | undefined;
8
+
9
+ const firstFrame = requestAnimationFrame(() => {
10
+ secondFrame = requestAnimationFrame(() => setPainted(true));
11
+ });
12
+
13
+ return () => {
14
+ cancelAnimationFrame(firstFrame);
15
+
16
+ if (secondFrame !== undefined) {
17
+ cancelAnimationFrame(secondFrame);
18
+ }
19
+ };
20
+ }, []);
21
+
22
+ return painted;
23
+ };
@@ -20,7 +20,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
20
20
 
21
21
  import { NavBarContainer } from "../../Layout/TV/NavBarContainer";
22
22
  import { ScreenResolver } from "../../ScreenResolver";
23
- import { useInitialFocus } from "./hooks";
23
+ import { useInitialFocus, useAfterPaint } from "./hooks";
24
24
  import { isPlayerPlugin } from "@applicaster/zapp-react-native-utils/pluginUtils";
25
25
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
26
26
  import { FreezeWithCallback } from "../../FreezeWithCallback";
@@ -155,6 +155,13 @@ export const Screen = ({ route, Components }: Props) => {
155
155
 
156
156
  const isScreenActive = useIsScreenActive();
157
157
 
158
+ // We need to render menu first and then proceed with screen content,
159
+ // otherwise screen will stay black until everything is loaded and screen
160
+ // rendering put huge load the CPU pushing rendering even further.
161
+ // With this approach, menu will be rendered immediately and screen content
162
+ // will be rendered after paint, which makes it more responsive and prevents black screen.
163
+ const isContentReady = useAfterPaint();
164
+
158
165
  return (
159
166
  <FreezeWithCallback freeze={!isScreenActive} onRelease={onRelease}>
160
167
  <View style={[styles.container, { backgroundColor }]}>
@@ -165,12 +172,14 @@ export const Screen = ({ route, Components }: Props) => {
165
172
  navigationProps={navigationProps}
166
173
  />
167
174
  </NavBarContainer>
168
- <ScreenResolver
169
- screenType={screenType}
170
- screenId={screenId}
171
- screenData={screenData}
172
- groupId={route}
173
- />
175
+ {isContentReady ? (
176
+ <ScreenResolver
177
+ screenType={screenType}
178
+ screenId={screenId}
179
+ screenData={screenData}
180
+ groupId={route}
181
+ />
182
+ ) : null}
174
183
  </View>
175
184
  </FreezeWithCallback>
176
185
  );
@@ -77,7 +77,7 @@ export const withScreenRevealManager = (Component) => {
77
77
 
78
78
  if (isFirstComponentScreenPicker(componentsToRender)) {
79
79
  // for screen-picker with have additional internal ComponentsMap, no need to add this wrapper
80
- return <Component {...props} />;
80
+ return <Component {...props} initialNumberToLoad={1} />;
81
81
  }
82
82
 
83
83
  return (
@@ -48,7 +48,6 @@ type Position = {
48
48
  type PlayerFactoryConfig = {
49
49
  player: any; // React ref for native view
50
50
  playerId: string;
51
- muted: boolean;
52
51
  playerPluginId: string;
53
52
  screenConfig: Record<string, any>;
54
53
  entry: ZappEntry; // original entry, used as fallback if no hooks
@@ -296,7 +295,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
296
295
 
297
296
  setUserCellPlayerMutedPreference(true);
298
297
 
299
- this.items.forEach((liveImage) => liveImage.player?.mute());
298
+ this.items.forEach((liveImage) => liveImage.mute());
300
299
  };
301
300
 
302
301
  public unmuteAll = () => {
@@ -304,7 +303,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
304
303
 
305
304
  setUserCellPlayerMutedPreference(false);
306
305
 
307
- this.items.forEach((liveImage) => liveImage.player?.unmute());
306
+ this.items.forEach((liveImage) => liveImage.unmute());
308
307
  };
309
308
 
310
309
  public onViewPositionChanged = (item: LiveImage) => {
@@ -574,6 +573,20 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
574
573
  left: 0,
575
574
  };
576
575
 
576
+ // Keeps muted state in sync: updates the initial value for deferred
577
+ // player creation and forwards to the player if it already exists.
578
+ public initiallyMuted: boolean = true;
579
+
580
+ mute = () => {
581
+ this.initiallyMuted = true;
582
+ this.player?.mute();
583
+ };
584
+
585
+ unmute = () => {
586
+ this.initiallyMuted = false;
587
+ this.player?.unmute();
588
+ };
589
+
577
590
  positionToString() {
578
591
  if (!this.position) {
579
592
  return "position not set";
@@ -611,48 +624,7 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
611
624
  return this._preparePromise;
612
625
  }
613
626
 
614
- this._preparePromise = (async (): Promise<boolean> => {
615
- // 1. Run hooks if configured
616
- let entry = this.factoryConfig.entry;
617
-
618
- if (this.preloadHooks?.length) {
619
- const result = await executePreloadHooks({
620
- preloadHooks: this.preloadHooks,
621
- entry,
622
- });
623
-
624
- if (result) {
625
- this.processedEntry = result;
626
- entry = result;
627
- } else {
628
- return false;
629
- }
630
- }
631
-
632
- // 2. Create the player with the correct entry
633
- const factoryItem = playerFactory({
634
- player: this.factoryConfig.player,
635
- playerId: this.factoryConfig.playerId,
636
- autoplay: false,
637
- entry,
638
- muted: this.factoryConfig.muted,
639
- playerPluginId: this.factoryConfig.playerPluginId,
640
- screenConfig: this.factoryConfig.screenConfig,
641
- playerRole: PlayerRole.Cell,
642
- });
643
-
644
- if (!factoryItem) {
645
- throw new Error("Player factory returned null");
646
- }
647
-
648
- this.player = factoryItem.controller;
649
- this.component = factoryItem.Component;
650
-
651
- // 3. Register callbacks — player now exists
652
- this.player.addListener({ id: "live-image", listener: this });
653
-
654
- return true;
655
- })()
627
+ this._preparePromise = this.createPreparedPlayer()
656
628
  .then((result) => {
657
629
  this._preparePromise = null;
658
630
 
@@ -672,6 +644,45 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
672
644
  return this._preparePromise;
673
645
  }
674
646
 
647
+ private async createPreparedPlayer(): Promise<boolean> {
648
+ let entry = this.factoryConfig.entry;
649
+
650
+ if (this.preloadHooks?.length) {
651
+ const result = await executePreloadHooks({
652
+ preloadHooks: this.preloadHooks,
653
+ entry,
654
+ });
655
+
656
+ if (!result) return false;
657
+
658
+ this.processedEntry = result;
659
+ entry = result;
660
+ }
661
+
662
+ const factoryItem = await playerFactory({
663
+ player: this.factoryConfig.player,
664
+ playerId: this.factoryConfig.playerId,
665
+ autoplay: false,
666
+ entry,
667
+ muted: this.initiallyMuted,
668
+ playerPluginId: this.factoryConfig.playerPluginId,
669
+ screenConfig: this.factoryConfig.screenConfig,
670
+ playerRole: PlayerRole.Cell,
671
+ });
672
+
673
+ if (!factoryItem) {
674
+ throw new Error(
675
+ `Player factory returned null (playerId: ${this.factoryConfig.playerId}, playerPluginId: ${this.factoryConfig.playerPluginId})`
676
+ );
677
+ }
678
+
679
+ this.player = factoryItem.controller;
680
+ this.component = factoryItem.Component;
681
+ this.player.addListener({ id: "live-image", listener: this });
682
+
683
+ return true;
684
+ }
685
+
675
686
  public getPlayer = (): Player | null => {
676
687
  return this.player;
677
688
  };
@@ -126,14 +126,16 @@ const PlayerLiveImageComponent = (props: Props) => {
126
126
  factoryConfig: {
127
127
  player: ref,
128
128
  playerId,
129
- muted,
130
129
  playerPluginId: playerPluginId,
131
130
  screenConfig: screenConfig,
132
131
  entry: item,
133
132
  },
134
133
  tag: item.title?.toString(),
135
134
  });
136
- }, [playerId, preloadHooks, muted, playerPluginId, screenConfig, item]);
135
+ }, [playerId, preloadHooks, playerPluginId, screenConfig, item]);
136
+
137
+ // Keep the muted state in sync with user preference and player state
138
+ liveImageItem.initiallyMuted = muted;
137
139
 
138
140
  React.useEffect(() => {
139
141
  liveImageItem.setMode = setModeDebounced;
@@ -5,6 +5,7 @@ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/n
5
5
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks/device/useIsTablet";
6
6
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
7
7
  import { create } from "zustand";
8
+ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
8
9
 
9
10
  export const useConfiguration = () => {
10
11
  const {
@@ -17,13 +18,18 @@ export const useConfiguration = () => {
17
18
  "pluginConfigurations",
18
19
  ]);
19
20
 
21
+ const player = usePlayer();
22
+
20
23
  const targetScreenId = contentTypes?.[item?.type?.value]?.screen_id;
21
24
  const targetScreenConfiguration = rivers?.[targetScreenId];
22
25
 
23
26
  const pluginConfiguration =
24
27
  pluginConfigurations[targetScreenConfiguration?.type]?.configuration_json;
25
28
 
26
- const playerPluginConfig = playerManager.getPluginConfiguration();
29
+ const playerPluginConfig =
30
+ player?.getPluginConfiguration() ??
31
+ playerManager.getInstanceController()?.getPluginConfiguration() ??
32
+ {};
27
33
 
28
34
  const config = mergeRight(playerPluginConfig, {
29
35
  ...pluginConfiguration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.1.0-rc.1",
3
+ "version": "15.1.0-rc.10",
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,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "15.1.0-rc.1",
32
- "@applicaster/zapp-react-native-bridge": "15.1.0-rc.1",
33
- "@applicaster/zapp-react-native-redux": "15.1.0-rc.1",
34
- "@applicaster/zapp-react-native-utils": "15.1.0-rc.1",
31
+ "@applicaster/applicaster-types": "15.1.0-rc.10",
32
+ "@applicaster/zapp-react-native-bridge": "15.1.0-rc.10",
33
+ "@applicaster/zapp-react-native-redux": "15.1.0-rc.10",
34
+ "@applicaster/zapp-react-native-utils": "15.1.0-rc.10",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",