@applicaster/zapp-react-native-ui-components 16.0.0-alpha.5652127751 → 16.0.0-alpha.5803191443
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/MasterCell/DefaultComponents/ActionButton.tsx +20 -9
- package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
- package/Components/MasterCell/DefaultComponents/__tests__/ActionButton.test.tsx +156 -0
- package/Components/VideoLive/LiveImageManager.ts +42 -40
- package/package.json +5 -5
|
@@ -4,6 +4,7 @@ import { TouchableOpacity, ViewStyle } from "react-native";
|
|
|
4
4
|
import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
5
5
|
|
|
6
6
|
import Image from "./Image";
|
|
7
|
+
|
|
7
8
|
type Props = {
|
|
8
9
|
item: ZappEntry | ZappFeed;
|
|
9
10
|
flavour?: "flavour_1" | "flavour_2";
|
|
@@ -12,7 +13,7 @@ type Props = {
|
|
|
12
13
|
src: {
|
|
13
14
|
active: string;
|
|
14
15
|
inactive: string;
|
|
15
|
-
}
|
|
16
|
+
};
|
|
16
17
|
style: {};
|
|
17
18
|
type: "Image" | "Svg" | "Lottie";
|
|
18
19
|
};
|
|
@@ -32,6 +33,9 @@ function isStringAsset(asset) {
|
|
|
32
33
|
return typeof asset === "string" || Array.isArray(asset);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/** return asset based on the asset, flavour, and fallbackAsset
|
|
37
|
+
* usully fallback asset is set on cell configuration, while the asset is configured on action plugin configuration
|
|
38
|
+
*/
|
|
35
39
|
function getAssetValue(asset, flavour, fallbackAsset = null) {
|
|
36
40
|
if (!asset) {
|
|
37
41
|
return null;
|
|
@@ -60,26 +64,33 @@ export const ActionButton = React.memo(function ActionButtonComponent(
|
|
|
60
64
|
typeof actionContext?.isActionAvailable === "function" &&
|
|
61
65
|
!actionContext.isActionAvailable(item);
|
|
62
66
|
|
|
67
|
+
const getActionEntryState = useCallback(() => {
|
|
68
|
+
return actionContext.initialEntryState(item, {
|
|
69
|
+
fallbackAssetSrc: asset?.src,
|
|
70
|
+
});
|
|
71
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
72
|
+
}, [actionContext, item?.id, asset?.src]);
|
|
73
|
+
|
|
63
74
|
// Note: in theory initialization is not needed anymore, we are using useEffect
|
|
64
75
|
const [actionState, setActionState] = React.useState(
|
|
65
|
-
actionDisabled || !actionContext
|
|
66
|
-
? null
|
|
67
|
-
: actionContext.initialEntryState(item)
|
|
76
|
+
actionDisabled || !actionContext ? null : getActionEntryState()
|
|
68
77
|
);
|
|
69
78
|
|
|
70
79
|
useEffect(() => {
|
|
71
80
|
if (!((actionDisabled || !actionContext) && actionState !== null)) {
|
|
72
|
-
setActionState(
|
|
81
|
+
setActionState(getActionEntryState());
|
|
73
82
|
}
|
|
74
|
-
|
|
83
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
84
|
+
}, [actionDisabled, item?.id, actionContext, getActionEntryState]);
|
|
75
85
|
|
|
76
86
|
const onPress = useCallback(() => {
|
|
77
87
|
actionContext.invokeAction(item, {
|
|
78
|
-
updateState: (
|
|
79
|
-
setActionState(
|
|
88
|
+
updateState: () => {
|
|
89
|
+
setActionState(getActionEntryState());
|
|
80
90
|
},
|
|
81
91
|
});
|
|
82
|
-
|
|
92
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
93
|
+
}, [actionContext, item?.id, getActionEntryState]);
|
|
83
94
|
|
|
84
95
|
useEffect(() => {
|
|
85
96
|
if (typeof actionContext?.addListener === "function") {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { act, fireEvent, render } from "@testing-library/react-native";
|
|
4
|
+
import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
5
|
+
|
|
6
|
+
import { ActionButton } from "../ActionButton";
|
|
7
|
+
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
|
|
9
|
+
useActions: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock("../Image", () => {
|
|
13
|
+
const React = require("react");
|
|
14
|
+
const { Text } = require("react-native");
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: ({ uri }: { uri: string }) =>
|
|
19
|
+
React.createElement(Text, { testID: "action-button-image" }, uri),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const mockUseActions = useActions as jest.Mock;
|
|
24
|
+
|
|
25
|
+
const item = { id: "entry-1" } as ZappEntry;
|
|
26
|
+
|
|
27
|
+
const fallbackAsset = {
|
|
28
|
+
props: {},
|
|
29
|
+
src: {
|
|
30
|
+
active: "cell-active.png",
|
|
31
|
+
inactive: "cell-inactive.png",
|
|
32
|
+
},
|
|
33
|
+
style: {},
|
|
34
|
+
type: "Image" as const,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function renderActionButton(actionContext: any, props = {}) {
|
|
38
|
+
mockUseActions.mockReturnValue(actionContext);
|
|
39
|
+
|
|
40
|
+
return render(
|
|
41
|
+
<ActionButton
|
|
42
|
+
item={item}
|
|
43
|
+
action={{ identifier: "test-action" }}
|
|
44
|
+
asset={fallbackAsset}
|
|
45
|
+
flavour="flavour_1"
|
|
46
|
+
style={{ width: 40, height: 40 }}
|
|
47
|
+
testID="action-button"
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("ActionButton", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
jest.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("passes cell fallback assets to initialEntryState", () => {
|
|
59
|
+
const actionContext = {
|
|
60
|
+
initialEntryState: jest.fn(() => ({ asset: ["action-1", "action-2"] })),
|
|
61
|
+
invokeAction: jest.fn(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
renderActionButton(actionContext);
|
|
65
|
+
|
|
66
|
+
expect(actionContext.initialEntryState).toHaveBeenCalledWith(item, {
|
|
67
|
+
fallbackAssetSrc: fallbackAsset.src,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("refreshes entry state when invokeAction calls updateState", () => {
|
|
72
|
+
let stateAsset = ["initial-1.png", "initial-2.png"];
|
|
73
|
+
|
|
74
|
+
const actionContext = {
|
|
75
|
+
initialEntryState: jest.fn(() => ({ asset: stateAsset })),
|
|
76
|
+
invokeAction: jest.fn((_entry, options) => {
|
|
77
|
+
stateAsset = ["recomputed-1.png", "recomputed-2.png"];
|
|
78
|
+
options.updateState({ asset: ["ignored-1.png", "ignored-2.png"] });
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const { getByTestId, getByText, queryByText } =
|
|
83
|
+
renderActionButton(actionContext);
|
|
84
|
+
|
|
85
|
+
fireEvent.press(getByTestId("action-button"));
|
|
86
|
+
|
|
87
|
+
expect(actionContext.invokeAction).toHaveBeenCalledWith(
|
|
88
|
+
item,
|
|
89
|
+
expect.objectContaining({ updateState: expect.any(Function) })
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(getByText("recomputed-1.png")).toBeTruthy();
|
|
93
|
+
expect(queryByText("ignored-1.png")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("updates entry state from addListener", () => {
|
|
97
|
+
let listener: (state: unknown) => void;
|
|
98
|
+
|
|
99
|
+
const actionContext = {
|
|
100
|
+
initialEntryState: jest.fn(() => ({ asset: ["initial-1.png"] })),
|
|
101
|
+
invokeAction: jest.fn(),
|
|
102
|
+
addListener: jest.fn((_entryId, callback) => {
|
|
103
|
+
listener = callback;
|
|
104
|
+
|
|
105
|
+
return jest.fn();
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const { getByText } = renderActionButton(actionContext);
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
listener({ asset: ["listener-1.png"] });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(actionContext.addListener).toHaveBeenCalledWith(
|
|
116
|
+
String(item.id),
|
|
117
|
+
expect.any(Function)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(getByText("listener-1.png")).toBeTruthy();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("renders selected flavour from action state asset arrays", () => {
|
|
124
|
+
const actionContext = {
|
|
125
|
+
initialEntryState: jest.fn(() => ({
|
|
126
|
+
asset: ["flavour-1.png", "flavour-2.png"],
|
|
127
|
+
})),
|
|
128
|
+
invokeAction: jest.fn(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const { getByText } = renderActionButton(actionContext, {
|
|
132
|
+
flavour: "flavour_2",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(getByText("flavour-2.png")).toBeTruthy();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("passes fallback cell asset to component assets", () => {
|
|
139
|
+
const AssetComponent = jest.fn(() => <View testID="component-asset" />);
|
|
140
|
+
|
|
141
|
+
const actionContext = {
|
|
142
|
+
initialEntryState: jest.fn(() => ({ asset: AssetComponent })),
|
|
143
|
+
invokeAction: jest.fn(),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
renderActionButton(actionContext);
|
|
147
|
+
|
|
148
|
+
expect(AssetComponent).toHaveBeenCalledWith(
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
asset: fallbackAsset.src,
|
|
151
|
+
flavour: "flavour_1",
|
|
152
|
+
}),
|
|
153
|
+
expect.anything()
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -624,7 +624,48 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
|
|
|
624
624
|
return this._preparePromise;
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
-
this._preparePromise =
|
|
627
|
+
this._preparePromise = (async (): Promise<boolean> => {
|
|
628
|
+
// 1. Run hooks if configured
|
|
629
|
+
let entry = this.factoryConfig.entry;
|
|
630
|
+
|
|
631
|
+
if (this.preloadHooks?.length) {
|
|
632
|
+
const result = await executePreloadHooks({
|
|
633
|
+
preloadHooks: this.preloadHooks,
|
|
634
|
+
entry,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if (result) {
|
|
638
|
+
this.processedEntry = result;
|
|
639
|
+
entry = result;
|
|
640
|
+
} else {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 2. Create the player with the correct entry
|
|
646
|
+
const factoryItem = playerFactory({
|
|
647
|
+
player: this.factoryConfig.player,
|
|
648
|
+
playerId: this.factoryConfig.playerId,
|
|
649
|
+
autoplay: false,
|
|
650
|
+
entry,
|
|
651
|
+
muted: this.initiallyMuted,
|
|
652
|
+
playerPluginId: this.factoryConfig.playerPluginId,
|
|
653
|
+
screenConfig: this.factoryConfig.screenConfig,
|
|
654
|
+
playerRole: PlayerRole.Cell,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
if (!factoryItem) {
|
|
658
|
+
throw new Error("Player factory returned null");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
this.player = factoryItem.controller;
|
|
662
|
+
this.component = factoryItem.Component;
|
|
663
|
+
|
|
664
|
+
// 3. Register callbacks — player now exists
|
|
665
|
+
this.player.addListener({ id: "live-image", listener: this });
|
|
666
|
+
|
|
667
|
+
return true;
|
|
668
|
+
})()
|
|
628
669
|
.then((result) => {
|
|
629
670
|
this._preparePromise = null;
|
|
630
671
|
|
|
@@ -644,45 +685,6 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
|
|
|
644
685
|
return this._preparePromise;
|
|
645
686
|
}
|
|
646
687
|
|
|
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
|
-
|
|
686
688
|
public getPlayer = (): Player | null => {
|
|
687
689
|
return this.player;
|
|
688
690
|
};
|
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.
|
|
3
|
+
"version": "16.0.0-alpha.5803191443",
|
|
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": "16.0.0-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "16.0.0-alpha.5803191443",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.5803191443",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.5803191443",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.5803191443",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|