@applicaster/zapp-react-native-ui-components 15.1.0-rc.1 → 15.1.0-rc.11
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.
- package/Components/CellRendererResolver/index.ts +1 -1
- package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
- package/Components/Layout/TV/ScreenContainer.tsx +5 -0
- package/Components/PlayerContainer/PlayerContainer.tsx +7 -9
- package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
- package/Components/Screen/TV/hooks/index.ts +2 -0
- package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
- package/Components/Screen/TV/index.web.tsx +16 -7
- package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
- package/Components/VideoLive/LiveImageManager.ts +56 -45
- package/Components/VideoLive/PlayerLiveImageComponent.tsx +4 -2
- package/Components/VideoModal/utils.ts +7 -1
- package/package.json +5 -5
|
@@ -83,7 +83,7 @@ export function CellRendererResolver({
|
|
|
83
83
|
|
|
84
84
|
if (!cellRendererPlugin && !isGroup(component)) {
|
|
85
85
|
logger.warning({
|
|
86
|
-
message:
|
|
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,
|
|
265
|
+
}, [isModal, state.playerId, showNavBar, navigator]);
|
|
266
266
|
|
|
267
|
-
const
|
|
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
|
+
});
|
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
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.
|
|
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 = (
|
|
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,
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "15.1.0-rc.11",
|
|
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.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.1.0-rc.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.1.0-rc.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.1.0-rc.
|
|
31
|
+
"@applicaster/applicaster-types": "15.1.0-rc.11",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.1.0-rc.11",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.1.0-rc.11",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.1.0-rc.11",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|