@applicaster/zapp-react-native-utils 16.0.0-rc.113 → 16.0.0-rc.114
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/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
- package/actionsExecutor/actions/openBottomSheet.ts +1 -0
- package/appUtils/playerManager/__tests__/playerToolbox.test.ts +3 -3
- package/appUtils/playerManager/index.ts +5 -0
- package/appUtils/playerManager/playerToolbox.ts +4 -39
- package/audioPlayerUtils/__tests__/getArtworkImage.test.ts +10 -0
- package/audioPlayerUtils/index.ts +7 -2
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +60 -0
- package/logger/__tests__/xrayToolbox.test.ts +69 -0
- package/logger/xrayToolbox.ts +67 -0
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/package.json +2 -2
- package/riverComponetsMeasurementProvider/index.tsx +3 -1
|
@@ -60,6 +60,30 @@ describe("openBottomSheetAction custom content", () => {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("passes themePluginId through to the name sheet", async () => {
|
|
64
|
+
const CustomContent = function CustomContent() {
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await openBottomSheetAction({
|
|
69
|
+
type: "openBottomSheet",
|
|
70
|
+
options: {
|
|
71
|
+
header: { title: "Create New Playlist" },
|
|
72
|
+
themePluginId: "playlist-experience",
|
|
73
|
+
content: {
|
|
74
|
+
component: CustomContent,
|
|
75
|
+
props: { buttonLabel: "Create" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} as any);
|
|
79
|
+
|
|
80
|
+
const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
|
|
81
|
+
|
|
82
|
+
expect(args.modalBottomSheetContentProps.themePluginId).toBe(
|
|
83
|
+
"playlist-experience"
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
63
87
|
it("returns Error when neither list data nor content.component is provided", async () => {
|
|
64
88
|
const result = await openBottomSheetAction({
|
|
65
89
|
type: "openBottomSheet",
|
|
@@ -45,7 +45,7 @@ describe("playerToolbox", () => {
|
|
|
45
45
|
|
|
46
46
|
await expect(
|
|
47
47
|
loadToolbox().registerPlayerToolbox(manager(null))
|
|
48
|
-
).resolves.
|
|
48
|
+
).resolves.toBe(false);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it("does nothing when getXray returns null", async () => {
|
|
@@ -53,7 +53,7 @@ describe("playerToolbox", () => {
|
|
|
53
53
|
|
|
54
54
|
await expect(
|
|
55
55
|
loadToolbox().registerPlayerToolbox(manager(null))
|
|
56
|
-
).resolves.
|
|
56
|
+
).resolves.toBe(false);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it("swallows a failed registration", async () => {
|
|
@@ -62,7 +62,7 @@ describe("playerToolbox", () => {
|
|
|
62
62
|
|
|
63
63
|
await expect(
|
|
64
64
|
loadToolbox().registerPlayerToolbox(manager(null))
|
|
65
|
-
).resolves.
|
|
65
|
+
).resolves.toBe(false);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it("seeks to an absolute position, from the end when negative, clamped", async () => {
|
|
@@ -397,6 +397,11 @@ export class PlayerManager {
|
|
|
397
397
|
return this.manager.removeHandler(event, handler);
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
/** Whether anything handles `event` now, e.g. a mounted PlayerContainer for "toggleFullscreen". */
|
|
401
|
+
public hasHandler(event: string) {
|
|
402
|
+
return this.manager.handlers[event]?.length > 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
400
405
|
// TODO: should not be
|
|
401
406
|
public invokeHandler(event, ...args) {
|
|
402
407
|
return this.manager.invokeHandler(event, ...args);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerXrayToolbox, XrayToolSpec } from "../../logger/xrayToolbox";
|
|
2
2
|
import type { PlayerManager } from "./index";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -9,25 +9,6 @@ import type { PlayerManager } from "./index";
|
|
|
9
9
|
* How the player is shown (video modal mode, mini player) is navigation state: see `nav.*`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
type ToolSpec = {
|
|
13
|
-
name: string;
|
|
14
|
-
description: string;
|
|
15
|
-
parameters?: Array<{
|
|
16
|
-
name: string;
|
|
17
|
-
type: "number";
|
|
18
|
-
description: string;
|
|
19
|
-
}>;
|
|
20
|
-
handler: (args: Record<string, unknown>) => unknown;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type RegisterReactToolbox = (toolbox: {
|
|
24
|
-
name: string;
|
|
25
|
-
description: string;
|
|
26
|
-
tools: ToolSpec[];
|
|
27
|
-
}) => Promise<void>;
|
|
28
|
-
|
|
29
|
-
let registration: Promise<void> | undefined;
|
|
30
|
-
|
|
31
12
|
const toNumber = (value: unknown) =>
|
|
32
13
|
typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
33
14
|
|
|
@@ -44,7 +25,7 @@ const requireSeekable = (player: {
|
|
|
44
25
|
return seekable;
|
|
45
26
|
};
|
|
46
27
|
|
|
47
|
-
const tools = (manager: PlayerManager):
|
|
28
|
+
const tools = (manager: PlayerManager): XrayToolSpec[] => {
|
|
48
29
|
const requirePlayer = () => {
|
|
49
30
|
const player = manager.getActivePlayer();
|
|
50
31
|
|
|
@@ -168,25 +149,9 @@ const tools = (manager: PlayerManager): ToolSpec[] => {
|
|
|
168
149
|
];
|
|
169
150
|
};
|
|
170
151
|
|
|
171
|
-
export const registerPlayerToolbox = (manager: PlayerManager) =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const register = (
|
|
175
|
-
getXray() as
|
|
176
|
-
| { registerReactToolbox?: RegisterReactToolbox }
|
|
177
|
-
| null
|
|
178
|
-
| undefined
|
|
179
|
-
)?.registerReactToolbox;
|
|
180
|
-
|
|
181
|
-
if (!register) return Promise.resolve();
|
|
182
|
-
|
|
183
|
-
registration = register({
|
|
152
|
+
export const registerPlayerToolbox = (manager: PlayerManager) =>
|
|
153
|
+
registerXrayToolbox({
|
|
184
154
|
name: "player",
|
|
185
155
|
description: "Inspect and control the active player.",
|
|
186
156
|
tools: tools(manager),
|
|
187
|
-
}).catch(() => {
|
|
188
|
-
registration = undefined;
|
|
189
157
|
});
|
|
190
|
-
|
|
191
|
-
return registration;
|
|
192
|
-
};
|
|
@@ -141,4 +141,14 @@ describe("getArtworkImage", () => {
|
|
|
141
141
|
|
|
142
142
|
expect(result).toBe(DEFAULT_IMAGE);
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
it("returns undefined when entry explicitly sets an empty key", () => {
|
|
146
|
+
const result = getArtworkImage({
|
|
147
|
+
key: "artwork_key",
|
|
148
|
+
entry: { ...entryWithImage, extensions: { artwork_key: "" } },
|
|
149
|
+
plugin_configuration: pluginConfigWithImage,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(result).toBeUndefined();
|
|
153
|
+
});
|
|
144
154
|
});
|
|
@@ -110,10 +110,15 @@ export const getArtworkImage = ({
|
|
|
110
110
|
key,
|
|
111
111
|
entry,
|
|
112
112
|
plugin_configuration,
|
|
113
|
-
}: GetArtworkImageParams): string => {
|
|
113
|
+
}: GetArtworkImageParams): Option<string> => {
|
|
114
114
|
// 1) image_key from extensions
|
|
115
115
|
const imageKeyFromExtensions = getPropertyFromExtensions(key, entry);
|
|
116
116
|
|
|
117
|
+
// An explicitly empty key means the feed wants no artwork (background only)
|
|
118
|
+
if (imageKeyFromExtensions === "") {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
const imageFromExtensions = getImageFromEntry({
|
|
118
123
|
entry,
|
|
119
124
|
imageKey: imageKeyFromExtensions,
|
|
@@ -165,7 +170,7 @@ const AUDIO_PLAYER_ARTWORK_IMAGE_KEY = "audio_player_artwork_image_key";
|
|
|
165
170
|
const audioPlayerArtworkImageKeySelector = (config) =>
|
|
166
171
|
config?.[AUDIO_PLAYER_ARTWORK_IMAGE_KEY];
|
|
167
172
|
|
|
168
|
-
export const useArtworkImage = (entry: ZappEntry): string => {
|
|
173
|
+
export const useArtworkImage = (entry: ZappEntry): Option<string> => {
|
|
169
174
|
const configuration = useZStore("playerConfiguration");
|
|
170
175
|
|
|
171
176
|
const audio_player_artwork_image_key_value = configuration(
|
|
@@ -329,4 +329,64 @@ describe("getEmptyModalBlocksStructure", () => {
|
|
|
329
329
|
expect(result.sectionHeader.title).toEqual({ default: {} });
|
|
330
330
|
expect(result.sectionHeader.actionButtons).toEqual({ default: {} });
|
|
331
331
|
});
|
|
332
|
+
|
|
333
|
+
it("maps playlist button font family and the new green style keys", () => {
|
|
334
|
+
const result = parseModalBlocksConfiguration(
|
|
335
|
+
{
|
|
336
|
+
"button.title.ios.font_family": "GreycliffCF-Bold",
|
|
337
|
+
"button.title.inactive_font_color": "#111111",
|
|
338
|
+
"button.title.focused_font_color": "#222222",
|
|
339
|
+
"button.title.text_transform": "uppercase",
|
|
340
|
+
"button.background.inactive_color": "#999999",
|
|
341
|
+
"button.background.horizontal_gutter": 6,
|
|
342
|
+
"button.icons.width": 24,
|
|
343
|
+
"button.icons.height": 24,
|
|
344
|
+
},
|
|
345
|
+
"playlist-experience"
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(result.button.title.default).toEqual(
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
fontFamily: "GreycliffCF-Bold",
|
|
351
|
+
inactiveFontColor: "#111111",
|
|
352
|
+
textTransform: "uppercase",
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// "focused_" is a state prefix, so this key lands on the focused bucket.
|
|
357
|
+
expect(result.button.title.focused).toEqual(
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
fontColor: "#222222",
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
expect(result.button.background.default).toEqual(
|
|
364
|
+
expect.objectContaining({
|
|
365
|
+
inactiveColor: "#999999",
|
|
366
|
+
horizontalGutter: 6,
|
|
367
|
+
})
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(result.button.icons.default).toEqual(
|
|
371
|
+
expect.objectContaining({
|
|
372
|
+
width: 24,
|
|
373
|
+
height: 24,
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("keeps a font-selector object on the button title", () => {
|
|
379
|
+
const result = parseModalBlocksConfiguration(
|
|
380
|
+
{
|
|
381
|
+
"button.title.ios.font_family": { fontFamily: "GreycliffCF-Bold" },
|
|
382
|
+
},
|
|
383
|
+
"playlist-experience"
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
expect(result.button.title.default).toEqual(
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
fontFamily: { fontFamily: "GreycliffCF-Bold" },
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
});
|
|
332
392
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const xray = require("@applicaster/quick-brick-xray");
|
|
2
|
+
|
|
3
|
+
const toolbox = { name: "test", description: "", tools: [] };
|
|
4
|
+
|
|
5
|
+
const load = (): typeof import("../xrayToolbox") => {
|
|
6
|
+
let module;
|
|
7
|
+
|
|
8
|
+
jest.isolateModules(() => {
|
|
9
|
+
module = require("../xrayToolbox");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return module;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("registerXrayToolbox", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
delete xray.registerReactToolbox;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("registers once per name", async () => {
|
|
21
|
+
xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
|
|
22
|
+
const { registerXrayToolbox } = load();
|
|
23
|
+
|
|
24
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
|
|
25
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
|
|
26
|
+
|
|
27
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("resolves false without a toolbox API", async () => {
|
|
31
|
+
await expect(load().registerXrayToolbox(toolbox)).resolves.toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps a failed registration instead of retrying it", async () => {
|
|
35
|
+
xray.registerReactToolbox = jest
|
|
36
|
+
.fn()
|
|
37
|
+
.mockRejectedValue(new Error("XRayToolboxBridge is not available"));
|
|
38
|
+
|
|
39
|
+
const { registerXrayToolbox } = load();
|
|
40
|
+
|
|
41
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
|
|
42
|
+
await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
|
|
43
|
+
|
|
44
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("registers new tools under the same name again (Fast Refresh)", async () => {
|
|
48
|
+
xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
|
|
49
|
+
const { registerXrayToolbox } = load();
|
|
50
|
+
|
|
51
|
+
await registerXrayToolbox(toolbox);
|
|
52
|
+
await registerXrayToolbox({ ...toolbox, tools: [] });
|
|
53
|
+
|
|
54
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("stops trying every toolbox once the bridge is missing", async () => {
|
|
58
|
+
xray.registerReactToolbox = jest.fn().mockRejectedValue(new Error("no"));
|
|
59
|
+
const { registerXrayToolbox } = load();
|
|
60
|
+
|
|
61
|
+
await registerXrayToolbox(toolbox);
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
registerXrayToolbox({ ...toolbox, name: "other" })
|
|
65
|
+
).resolves.toBe(false);
|
|
66
|
+
|
|
67
|
+
expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getXray } from ".";
|
|
2
|
+
|
|
3
|
+
/** One tool of an X-Ray React toolbox: an agent calls it by name with JSON arguments. */
|
|
4
|
+
export type XrayToolSpec = {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
parameters?: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
type: "number" | "string";
|
|
10
|
+
description: string;
|
|
11
|
+
}>;
|
|
12
|
+
handler: (args: Record<string, unknown>) => unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type XrayToolbox = {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
tools: XrayToolSpec[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type RegisterReactToolbox = (toolbox: XrayToolbox) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
const registered = new Map<
|
|
24
|
+
string,
|
|
25
|
+
{ tools: XrayToolSpec[]; registration: Promise<boolean> }
|
|
26
|
+
>();
|
|
27
|
+
|
|
28
|
+
/** Set once a registration fails: the build has no X-Ray bridge, which cannot appear later. */
|
|
29
|
+
let bridgeMissing = false;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Registers an X-Ray React toolbox and resolves whether it is registered. The same tools are
|
|
33
|
+
* registered once; new tools under the same name (Fast Refresh re-ran the module) replace them.
|
|
34
|
+
* A build without the X-Ray native bridge (release builds) rejects the first attempt and every
|
|
35
|
+
* later call resolves false at once, so callers on hot paths pay for one attempt only.
|
|
36
|
+
*/
|
|
37
|
+
export const registerXrayToolbox = (toolbox: XrayToolbox): Promise<boolean> => {
|
|
38
|
+
const cached = registered.get(toolbox.name);
|
|
39
|
+
|
|
40
|
+
if (cached?.tools === toolbox.tools) return cached.registration;
|
|
41
|
+
|
|
42
|
+
if (bridgeMissing) return Promise.resolve(false);
|
|
43
|
+
|
|
44
|
+
const register = (
|
|
45
|
+
getXray() as
|
|
46
|
+
| { registerReactToolbox?: RegisterReactToolbox }
|
|
47
|
+
| null
|
|
48
|
+
| undefined
|
|
49
|
+
)?.registerReactToolbox;
|
|
50
|
+
|
|
51
|
+
const registration = register
|
|
52
|
+
? register(toolbox).then(
|
|
53
|
+
() => true,
|
|
54
|
+
() => {
|
|
55
|
+
bridgeMissing = true;
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
: Promise.resolve(false);
|
|
61
|
+
|
|
62
|
+
if (!register) bridgeMissing = true;
|
|
63
|
+
|
|
64
|
+
registered.set(toolbox.name, { tools: toolbox.tools, registration });
|
|
65
|
+
|
|
66
|
+
return registration;
|
|
67
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app navigator for the X-Ray toolboxes (nav, screen), which run outside React. The
|
|
3
|
+
* NavigationProvider sets it after each commit; it is a plain module variable, so updating it
|
|
4
|
+
* never renders anything.
|
|
5
|
+
*/
|
|
6
|
+
let navigator: QuickBrickAppNavigator | undefined;
|
|
7
|
+
|
|
8
|
+
export const setXrayNavigator = (value: QuickBrickAppNavigator | undefined) => {
|
|
9
|
+
navigator = value;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getXrayNavigator = () => navigator;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.114",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "16.0.0-rc.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-rc.114",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -34,6 +34,7 @@ type Props = {
|
|
|
34
34
|
type ItemProps = {
|
|
35
35
|
children: React.ReactNode;
|
|
36
36
|
index: number;
|
|
37
|
+
testID?: string;
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
type MeasurementContext = {
|
|
@@ -153,11 +154,12 @@ export const ScreenLoadingMeasurements = ({
|
|
|
153
154
|
export const ScreenLoadingMeasurementsListItemWrapper = ({
|
|
154
155
|
children,
|
|
155
156
|
index,
|
|
157
|
+
testID,
|
|
156
158
|
}: ItemProps) => {
|
|
157
159
|
const { onLayout } = React.useContext(MeasurementsSettersContext);
|
|
158
160
|
|
|
159
161
|
return (
|
|
160
|
-
<View style={styles.container} onLayout={onLayout?.(index)}>
|
|
162
|
+
<View style={styles.container} onLayout={onLayout?.(index)} testID={testID}>
|
|
161
163
|
{children}
|
|
162
164
|
</View>
|
|
163
165
|
);
|