@applicaster/quick-brick-core 16.0.0-rc.99 → 17.0.0-rc.1
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/App/ActionSetters/index.ts +2 -2
- package/App/ActionsProvider/ActionsProvider.tsx +1 -1
- package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +17 -1
- package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
- package/App/AssetCache/logger.ts +1 -1
- package/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/usePresentSchemeHandler.ts +3 -0
- package/App/DeepLinking/URLSchemeListener/URLSchemeContextProvider.tsx +4 -1
- package/App/DeepLinking/logger.ts +1 -1
- package/App/ErrorBoundary/index.tsx +5 -2
- package/App/NavigationProvider/NavigationProvider.tsx +17 -11
- package/App/NavigationProvider/__tests__/navToolbox.test.ts +227 -0
- package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
- package/App/NavigationProvider/navToolbox.ts +224 -0
- package/App/NavigationProvider/utils.ts +23 -2
- package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +40 -4
- package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
- package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +167 -1
- package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +486 -0
- package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
- package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +204 -0
- package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
- package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
- package/App/ThemeManager/index.tsx +6 -1
- package/App/appLifeCycleManager/index.tsx +2 -2
- package/App/appRemoteDataLoader/index.tsx +2 -2
- package/App/appRemoteDataLoader/remoteDataLoader.ts +2 -2
- package/App/index.tsx +8 -2
- package/App/layoutPreview/LayoutPreviewReminder.tsx +136 -0
- package/App/layoutPreview/__tests__/persistence.test.ts +63 -0
- package/App/layoutPreview/persistence.ts +228 -0
- package/App/logger.js +2 -2
- package/App/remoteContextReloader/__tests__/getNativeRemoteContextData.test.ts +148 -0
- package/App/remoteContextReloader/__tests__/getPromiseForType.test.js +31 -0
- package/App/remoteContextReloader/getRemoteContextData/getNativeRemoteContextData.ts +107 -29
- package/App/remoteContextReloader/helpers.ts +2 -1
- package/App/uiActions/index.ts +13 -0
- package/App/uiActions/muteUnmute/__tests__/assetResolver.test.ts +226 -0
- package/App/uiActions/muteUnmute/__tests__/binding.test.ts +51 -0
- package/App/uiActions/muteUnmute/__tests__/defaultAssets.test.ts +47 -0
- package/App/uiActions/muteUnmute/__tests__/entryState.test.ts +57 -0
- package/App/uiActions/muteUnmute/__tests__/labelResolver.test.ts +35 -0
- package/App/uiActions/muteUnmute/assetResolver.ts +87 -0
- package/App/uiActions/muteUnmute/defaultAssets.ts +44 -0
- package/App/uiActions/muteUnmute/entryState.ts +28 -0
- package/App/uiActions/muteUnmute/index.ts +79 -0
- package/App/uiActions/muteUnmute/labelResolver.ts +15 -0
- package/App/uiActions/muteUnmute/types.ts +18 -0
- package/package.json +8 -8
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveActionAssets,
|
|
3
|
+
resolveButtonAssets,
|
|
4
|
+
isConfiguredAsset,
|
|
5
|
+
firstConfiguredAsset,
|
|
6
|
+
selectActionAsset,
|
|
7
|
+
toFlavourTuple,
|
|
8
|
+
} from "../assetResolver";
|
|
9
|
+
import { ACTION_DEFAULT_ASSETS, BUTTON_DEFAULT_ASSETS } from "../defaultAssets";
|
|
10
|
+
|
|
11
|
+
const createManifest = require("../../../../../../plugins/mute-unmute-action/manifests/manifest.config");
|
|
12
|
+
|
|
13
|
+
// distinct value per slot, so a resolver mixing up flavours or states is caught.
|
|
14
|
+
// The real defaults are covered by defaultAssets.test.ts
|
|
15
|
+
jest.mock("../defaultAssets", () => {
|
|
16
|
+
const action = (f: string) => ({
|
|
17
|
+
mute: `action_${f}_mute`,
|
|
18
|
+
unmute: `action_${f}_unmute`,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const button = (f: string) => ({
|
|
22
|
+
mute: `button_${f}_mute`,
|
|
23
|
+
unmute: `button_${f}_unmute`,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
ACTION_DEFAULT_ASSETS: { flavour_1: action("1"), flavour_2: action("2") },
|
|
28
|
+
BUTTON_DEFAULT_ASSETS: { flavour_1: button("1"), flavour_2: button("2") },
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const FLAVOURS = ["flavour_1", "flavour_2"];
|
|
33
|
+
const ACTION_STATES = ["mute", "unmute"];
|
|
34
|
+
const BUTTON_STATES = ["mute", "unmute"];
|
|
35
|
+
|
|
36
|
+
const actionKey = (flavour: string, state: string) =>
|
|
37
|
+
`mobile_action_${flavour}_${state}_asset`;
|
|
38
|
+
|
|
39
|
+
const buttonKey = (flavour: string, state: string) =>
|
|
40
|
+
`mobile_button_${flavour}_${state}_asset`;
|
|
41
|
+
|
|
42
|
+
describe("isConfiguredAsset", () => {
|
|
43
|
+
it.each([
|
|
44
|
+
["a URL", "https://x.com/a.png", true],
|
|
45
|
+
["an empty string", "", false],
|
|
46
|
+
["whitespace", " \n", false],
|
|
47
|
+
["undefined", undefined, false],
|
|
48
|
+
["null", null, false],
|
|
49
|
+
["a number", 5, false],
|
|
50
|
+
["an object", {}, false],
|
|
51
|
+
])("%s", (_, value, expected) => {
|
|
52
|
+
expect(isConfiguredAsset(value)).toBe(expected);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("firstConfiguredAsset", () => {
|
|
57
|
+
it("returns the first configured candidate", () => {
|
|
58
|
+
expect(firstConfiguredAsset(undefined, " ", "a", "b")).toBe("a");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns undefined when nothing is configured", () => {
|
|
62
|
+
expect(firstConfiguredAsset(undefined, "", null)).toBeUndefined();
|
|
63
|
+
expect(firstConfiguredAsset()).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("resolveActionAssets", () => {
|
|
68
|
+
it("equals the defaults without configuration", () => {
|
|
69
|
+
expect(resolveActionAssets({ configuration: {} })).toEqual(
|
|
70
|
+
ACTION_DEFAULT_ASSETS
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
FLAVOURS.forEach((flavour) =>
|
|
75
|
+
ACTION_STATES.forEach((state) =>
|
|
76
|
+
it(`overrides ${flavour} ${state} on its own`, () => {
|
|
77
|
+
const result = resolveActionAssets({
|
|
78
|
+
configuration: { [actionKey(flavour, state)]: "plugin.png" },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const expected = JSON.parse(JSON.stringify(ACTION_DEFAULT_ASSETS));
|
|
82
|
+
expected[flavour][state] = "plugin.png";
|
|
83
|
+
expect(result).toEqual(expected);
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
it("never returns an empty URI", () => {
|
|
89
|
+
const blank = (keys: string[]) =>
|
|
90
|
+
Object.fromEntries(keys.map((key) => [key, " "]));
|
|
91
|
+
|
|
92
|
+
const configuration = blank(
|
|
93
|
+
FLAVOURS.flatMap((f) => [
|
|
94
|
+
...ACTION_STATES.map((s) => actionKey(f, s)),
|
|
95
|
+
...BUTTON_STATES.map((s) => buttonKey(f, s)),
|
|
96
|
+
])
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const values = [
|
|
100
|
+
...Object.values(
|
|
101
|
+
resolveActionAssets({
|
|
102
|
+
configuration,
|
|
103
|
+
})
|
|
104
|
+
),
|
|
105
|
+
...Object.values(resolveButtonAssets({ configuration })),
|
|
106
|
+
].flatMap((assets) => Object.values(assets));
|
|
107
|
+
|
|
108
|
+
values.forEach((value) => expect(isConfiguredAsset(value)).toBe(true));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("applies a plugin override to its own key and flavour only", () => {
|
|
112
|
+
const result = resolveActionAssets({
|
|
113
|
+
configuration: { [actionKey("flavour_2", "unmute")]: "plugin.png" },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(result.flavour_2.unmute).toBe("plugin.png");
|
|
117
|
+
expect(result.flavour_2.mute).toBe(ACTION_DEFAULT_ASSETS.flavour_2.mute);
|
|
118
|
+
expect(result.flavour_1).toEqual(ACTION_DEFAULT_ASSETS.flavour_1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("falls through whitespace plugin values", () => {
|
|
122
|
+
const result = resolveActionAssets({
|
|
123
|
+
configuration: {
|
|
124
|
+
[actionKey("flavour_1", "mute")]: "plugin.png",
|
|
125
|
+
[actionKey("flavour_1", "unmute")]: " ",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.flavour_1.mute).toBe("plugin.png");
|
|
130
|
+
|
|
131
|
+
expect(result.flavour_1.unmute).toBe(
|
|
132
|
+
ACTION_DEFAULT_ASSETS.flavour_1.unmute
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("resolveButtonAssets", () => {
|
|
138
|
+
it("equals the defaults without configuration", () => {
|
|
139
|
+
expect(resolveButtonAssets({ configuration: {} })).toEqual(
|
|
140
|
+
BUTTON_DEFAULT_ASSETS
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
FLAVOURS.forEach((flavour) =>
|
|
145
|
+
BUTTON_STATES.forEach((state) =>
|
|
146
|
+
it(`overrides ${flavour} ${state} on its own`, () => {
|
|
147
|
+
const result = resolveButtonAssets({
|
|
148
|
+
configuration: { [buttonKey(flavour, state)]: "plugin.png" },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const expected = JSON.parse(JSON.stringify(BUTTON_DEFAULT_ASSETS));
|
|
152
|
+
expected[flavour][state] = "plugin.png";
|
|
153
|
+
expect(result).toEqual(expected);
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
it("falls back for whitespace values", () => {
|
|
159
|
+
const result = resolveButtonAssets({
|
|
160
|
+
configuration: { [buttonKey("flavour_1", "mute")]: " " },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(result).toEqual(BUTTON_DEFAULT_ASSETS);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("select helpers", () => {
|
|
168
|
+
const action = { mute: "m", unmute: "u" };
|
|
169
|
+
|
|
170
|
+
it.each([
|
|
171
|
+
[false, "m"],
|
|
172
|
+
[true, "u"],
|
|
173
|
+
])("selectActionAsset muted=%p -> %s", (muted, expected) => {
|
|
174
|
+
expect(selectActionAsset(action, muted)).toBe(expected);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("toFlavourTuple", () => {
|
|
179
|
+
it("returns [flavour_1, flavour_2] picks", () => {
|
|
180
|
+
expect(
|
|
181
|
+
toFlavourTuple(
|
|
182
|
+
{ flavour_1: { v: "a" }, flavour_2: { v: "b" } },
|
|
183
|
+
(x) => x.v
|
|
184
|
+
)
|
|
185
|
+
).toEqual(["a", "b"]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("manifest key contract", () => {
|
|
190
|
+
const manifestKeys = createManifest({
|
|
191
|
+
platform: "ios_for_quickbrick",
|
|
192
|
+
version: "1.0.0",
|
|
193
|
+
})
|
|
194
|
+
.custom_configuration_fields.flatMap((group) => group.fields)
|
|
195
|
+
.filter((field) => field.type === "uploader")
|
|
196
|
+
.map((field) => field.key)
|
|
197
|
+
.sort();
|
|
198
|
+
|
|
199
|
+
const readKeys = () => {
|
|
200
|
+
const read = new Set<string>();
|
|
201
|
+
|
|
202
|
+
const configuration = new Proxy(
|
|
203
|
+
{},
|
|
204
|
+
{
|
|
205
|
+
get: (_, key) => {
|
|
206
|
+
read.add(String(key));
|
|
207
|
+
|
|
208
|
+
return undefined;
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
resolveActionAssets({ configuration });
|
|
214
|
+
resolveButtonAssets({ configuration });
|
|
215
|
+
|
|
216
|
+
return [...read].sort();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
it("reads exactly the uploader keys defined in the manifest", () => {
|
|
220
|
+
expect(readKeys()).toEqual(manifestKeys);
|
|
221
|
+
|
|
222
|
+
expect(manifestKeys).toHaveLength(
|
|
223
|
+
FLAVOURS.length * (ACTION_STATES.length + BUTTON_STATES.length)
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
jest.mock("@applicaster/zapp-react-native-utils/pluginUtils", () => ({
|
|
2
|
+
getPluginByIdentifier: () => undefined,
|
|
3
|
+
}));
|
|
4
|
+
|
|
5
|
+
jest.mock(
|
|
6
|
+
"@applicaster/zapp-react-native-utils/reactHooks/usePluginConfiguration",
|
|
7
|
+
() => ({ getPluginConfiguration: () => ({}) })
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
jest.mock(
|
|
11
|
+
"@applicaster/zapp-react-native-ui-components/Components/VideoLive/MuteUnmuteController",
|
|
12
|
+
() => ({
|
|
13
|
+
MUTE_UNMUTE_ACTION_IDENTIFIER: "mute_unmute_action",
|
|
14
|
+
muteUnmuteController: {
|
|
15
|
+
getEntryState: () => ({ muted: true }),
|
|
16
|
+
subscribe: jest.fn(() => jest.fn()),
|
|
17
|
+
toggle: jest.fn(),
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
import { muteUnmuteUIAction } from "../index";
|
|
23
|
+
import { muteUnmuteController } from "@applicaster/zapp-react-native-ui-components/Components/VideoLive/MuteUnmuteController";
|
|
24
|
+
|
|
25
|
+
it("uses bundled visuals on the first read without a configuration plugin", () => {
|
|
26
|
+
expect(muteUnmuteUIAction.initialEntryState({ id: "1" })).toEqual(
|
|
27
|
+
expect.objectContaining({ checked: true, state: 1, label: "Unmute" })
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("delegates the command without a second imperative UI update", () => {
|
|
32
|
+
const updateState = jest.fn();
|
|
33
|
+
muteUnmuteUIAction.invokeAction({ id: "1" }, { updateState });
|
|
34
|
+
expect(muteUnmuteController.toggle).toHaveBeenCalledWith("1");
|
|
35
|
+
expect(updateState).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("hydrates a late subscriber and cleans up its controller subscription", () => {
|
|
39
|
+
const listener = jest.fn();
|
|
40
|
+
const remove = muteUnmuteUIAction.addListener("1", listener);
|
|
41
|
+
|
|
42
|
+
const controllerRemove = jest.mocked(muteUnmuteController.subscribe).mock
|
|
43
|
+
.results[0].value;
|
|
44
|
+
|
|
45
|
+
expect(listener).toHaveBeenCalledWith(
|
|
46
|
+
expect.objectContaining({ checked: true })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
remove();
|
|
50
|
+
expect(controllerRemove).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Image } from "react-native";
|
|
2
|
+
|
|
3
|
+
jest
|
|
4
|
+
.spyOn(Image, "resolveAssetSource")
|
|
5
|
+
.mockReturnValue({ uri: "asset://mock" } as any);
|
|
6
|
+
|
|
7
|
+
// imports are hoisted above the spy, so the module is loaded after it is set up
|
|
8
|
+
const {
|
|
9
|
+
ACTION_DEFAULT_ASSETS,
|
|
10
|
+
BUTTON_DEFAULT_ASSETS,
|
|
11
|
+
} = require("../defaultAssets");
|
|
12
|
+
|
|
13
|
+
const FLAVOURS = ["flavour_1", "flavour_2"];
|
|
14
|
+
const ACTION_STATES = ["mute", "unmute"];
|
|
15
|
+
const BUTTON_STATES = ["mute", "unmute"];
|
|
16
|
+
|
|
17
|
+
describe("defaultAssets", () => {
|
|
18
|
+
it("defines a non-empty action asset for every flavour and state", () => {
|
|
19
|
+
expect(Object.keys(ACTION_DEFAULT_ASSETS).sort()).toEqual(FLAVOURS);
|
|
20
|
+
|
|
21
|
+
FLAVOURS.forEach((flavour) => {
|
|
22
|
+
const assets = ACTION_DEFAULT_ASSETS[flavour];
|
|
23
|
+
|
|
24
|
+
expect(Object.keys(assets).sort()).toEqual([...ACTION_STATES].sort());
|
|
25
|
+
|
|
26
|
+
ACTION_STATES.forEach((state) => {
|
|
27
|
+
expect(typeof assets[state]).toBe("string");
|
|
28
|
+
expect(assets[state].length).toBeGreaterThan(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("defines a non-empty button asset for every flavour and state", () => {
|
|
34
|
+
expect(Object.keys(BUTTON_DEFAULT_ASSETS).sort()).toEqual(FLAVOURS);
|
|
35
|
+
|
|
36
|
+
FLAVOURS.forEach((flavour) => {
|
|
37
|
+
const assets = BUTTON_DEFAULT_ASSETS[flavour];
|
|
38
|
+
|
|
39
|
+
expect(Object.keys(assets).sort()).toEqual([...BUTTON_STATES].sort());
|
|
40
|
+
|
|
41
|
+
BUTTON_STATES.forEach((state) => {
|
|
42
|
+
expect(typeof assets[state]).toBe("string");
|
|
43
|
+
expect(assets[state].length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { buildEntryState } from "../entryState";
|
|
2
|
+
|
|
3
|
+
const actionAssets = {
|
|
4
|
+
flavour_1: { mute: "a1_mute", unmute: "a1_unmute" },
|
|
5
|
+
flavour_2: { mute: "a2_mute", unmute: "a2_unmute" },
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const buttonAssets = {
|
|
9
|
+
flavour_1: { mute: "b1_mute", unmute: "b1_unmute" },
|
|
10
|
+
flavour_2: { mute: "b2_mute", unmute: "b2_unmute" },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const labels = { mute: "Mute", unmute: "Unmute" };
|
|
14
|
+
|
|
15
|
+
const build = (muted: boolean) =>
|
|
16
|
+
buildEntryState({ state: { muted }, actionAssets, buttonAssets, labels });
|
|
17
|
+
|
|
18
|
+
describe("buildEntryState", () => {
|
|
19
|
+
it("sound on: Mute icons, state 0", () => {
|
|
20
|
+
expect(build(false)).toEqual({
|
|
21
|
+
state: 0,
|
|
22
|
+
asset: ["a1_mute", "a2_mute"],
|
|
23
|
+
mobileButtonAssets: ["b1_mute", "b2_mute"],
|
|
24
|
+
label: "Mute",
|
|
25
|
+
checked: false,
|
|
26
|
+
visibleInCells: [],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("muted: Unmute icons, state 1", () => {
|
|
31
|
+
expect(build(true)).toEqual({
|
|
32
|
+
state: 1,
|
|
33
|
+
asset: ["a1_unmute", "a2_unmute"],
|
|
34
|
+
mobileButtonAssets: ["b1_unmute", "b2_unmute"],
|
|
35
|
+
label: "Unmute",
|
|
36
|
+
checked: true,
|
|
37
|
+
visibleInCells: [],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("limits cell actions to the playing instance without removing button assets", () => {
|
|
42
|
+
expect(
|
|
43
|
+
buildEntryState({
|
|
44
|
+
state: { muted: true, activeCellId: "playing-cell" },
|
|
45
|
+
actionAssets,
|
|
46
|
+
buttonAssets,
|
|
47
|
+
labels,
|
|
48
|
+
})
|
|
49
|
+
).toEqual(
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
visibleInCells: ["playing-cell"],
|
|
52
|
+
mobileButtonAssets: ["b1_unmute", "b2_unmute"],
|
|
53
|
+
label: "Unmute",
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DEFAULT_LABELS, resolveLabels, selectLabel } from "../labelResolver";
|
|
2
|
+
|
|
3
|
+
describe("labelResolver", () => {
|
|
4
|
+
it("defaults to Mute / Unmute", () => {
|
|
5
|
+
expect(DEFAULT_LABELS).toEqual({ mute: "Mute", unmute: "Unmute" });
|
|
6
|
+
expect(resolveLabels()).toEqual(DEFAULT_LABELS);
|
|
7
|
+
expect(resolveLabels({})).toEqual(DEFAULT_LABELS);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("uses configured values", () => {
|
|
11
|
+
expect(
|
|
12
|
+
resolveLabels({ mute_label: "Silence", unmute_label: "Sound on" })
|
|
13
|
+
).toEqual({ mute: "Silence", unmute: "Sound on" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("falls back to the default for empty or whitespace values", () => {
|
|
17
|
+
expect(resolveLabels({ mute_label: "", unmute_label: " " })).toEqual(
|
|
18
|
+
DEFAULT_LABELS
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("overrides each label independently", () => {
|
|
23
|
+
expect(resolveLabels({ unmute_label: "Sound on" })).toEqual({
|
|
24
|
+
mute: "Mute",
|
|
25
|
+
unmute: "Sound on",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("selects the label describing the next action", () => {
|
|
30
|
+
const labels = { mute: "M", unmute: "U" };
|
|
31
|
+
|
|
32
|
+
expect(selectLabel(labels, false)).toBe("M");
|
|
33
|
+
expect(selectLabel(labels, true)).toBe("U");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ACTION_DEFAULT_ASSETS, BUTTON_DEFAULT_ASSETS } from "./defaultAssets";
|
|
2
|
+
import type {
|
|
3
|
+
ActionAssets,
|
|
4
|
+
ButtonAssets,
|
|
5
|
+
Flavour,
|
|
6
|
+
MuteUnmuteActionConfiguration,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
export const isConfiguredAsset = (value: unknown): value is string =>
|
|
10
|
+
typeof value === "string" && value.trim().length > 0;
|
|
11
|
+
|
|
12
|
+
export const firstConfiguredAsset = (
|
|
13
|
+
...candidates: unknown[]
|
|
14
|
+
): string | undefined => candidates.find(isConfiguredAsset);
|
|
15
|
+
|
|
16
|
+
// The only place in runtime code that defines the manifest key format.
|
|
17
|
+
const pluginAsset = (
|
|
18
|
+
configuration: MuteUnmuteActionConfiguration | undefined,
|
|
19
|
+
kind: "action" | "button",
|
|
20
|
+
flavour: Flavour,
|
|
21
|
+
state: keyof ButtonAssets
|
|
22
|
+
): unknown => configuration?.[`mobile_${kind}_${flavour}_${state}_asset`];
|
|
23
|
+
|
|
24
|
+
const resolveAsset = (fallback: string, ...candidates: unknown[]): string =>
|
|
25
|
+
firstConfiguredAsset(...candidates) ?? fallback;
|
|
26
|
+
|
|
27
|
+
export const resolveActionAssets = ({
|
|
28
|
+
configuration,
|
|
29
|
+
}: {
|
|
30
|
+
configuration?: MuteUnmuteActionConfiguration;
|
|
31
|
+
}): Record<Flavour, ActionAssets> => {
|
|
32
|
+
const forFlavour = (flavour: Flavour): ActionAssets => {
|
|
33
|
+
const defaults = ACTION_DEFAULT_ASSETS[flavour];
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
mute: resolveAsset(
|
|
37
|
+
defaults.mute,
|
|
38
|
+
pluginAsset(configuration, "action", flavour, "mute")
|
|
39
|
+
),
|
|
40
|
+
unmute: resolveAsset(
|
|
41
|
+
defaults.unmute,
|
|
42
|
+
pluginAsset(configuration, "action", flavour, "unmute")
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
flavour_1: forFlavour("flavour_1"),
|
|
49
|
+
flavour_2: forFlavour("flavour_2"),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const resolveButtonAssets = ({
|
|
54
|
+
configuration,
|
|
55
|
+
}: {
|
|
56
|
+
configuration?: MuteUnmuteActionConfiguration;
|
|
57
|
+
}): Record<Flavour, ButtonAssets> => {
|
|
58
|
+
const forFlavour = (flavour: Flavour): ButtonAssets => {
|
|
59
|
+
const defaults = BUTTON_DEFAULT_ASSETS[flavour];
|
|
60
|
+
|
|
61
|
+
const asset = (state: keyof ButtonAssets) =>
|
|
62
|
+
resolveAsset(
|
|
63
|
+
defaults[state],
|
|
64
|
+
pluginAsset(configuration, "button", flavour, state)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
mute: asset("mute"),
|
|
69
|
+
unmute: asset("unmute"),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
flavour_1: forFlavour("flavour_1"),
|
|
75
|
+
flavour_2: forFlavour("flavour_2"),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const selectActionAsset = (
|
|
80
|
+
assets: ActionAssets,
|
|
81
|
+
muted: boolean
|
|
82
|
+
): string => (muted ? assets.unmute : assets.mute);
|
|
83
|
+
|
|
84
|
+
export const toFlavourTuple = <T>(
|
|
85
|
+
byFlavour: Record<Flavour, T>,
|
|
86
|
+
pick: (assets: T) => string
|
|
87
|
+
): [string, string] => [pick(byFlavour.flavour_1), pick(byFlavour.flavour_2)];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resolveDefaultAssetUri } from "@applicaster/zapp-react-native-utils/actionUtils";
|
|
2
|
+
import { getQuickBrickAsset } from "@applicaster/zapp-react-native-utils/quickBrickAssets";
|
|
3
|
+
|
|
4
|
+
import type { ActionAssets, ButtonAssets, Flavour } from "./types";
|
|
5
|
+
|
|
6
|
+
const PLUGIN = "mute_unmute_action";
|
|
7
|
+
|
|
8
|
+
export const ACTION_DEFAULT_ASSETS: Record<Flavour, ActionAssets> = {
|
|
9
|
+
flavour_1: {
|
|
10
|
+
mute: resolveDefaultAssetUri(
|
|
11
|
+
getQuickBrickAsset(PLUGIN, "action/flavour_1/mute.png")
|
|
12
|
+
),
|
|
13
|
+
unmute: resolveDefaultAssetUri(
|
|
14
|
+
getQuickBrickAsset(PLUGIN, "action/flavour_1/unmute.png")
|
|
15
|
+
),
|
|
16
|
+
},
|
|
17
|
+
flavour_2: {
|
|
18
|
+
mute: resolveDefaultAssetUri(
|
|
19
|
+
getQuickBrickAsset(PLUGIN, "action/flavour_2/mute.png")
|
|
20
|
+
),
|
|
21
|
+
unmute: resolveDefaultAssetUri(
|
|
22
|
+
getQuickBrickAsset(PLUGIN, "action/flavour_2/unmute.png")
|
|
23
|
+
),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const BUTTON_DEFAULT_ASSETS: Record<Flavour, ButtonAssets> = {
|
|
28
|
+
flavour_1: {
|
|
29
|
+
mute: resolveDefaultAssetUri(
|
|
30
|
+
getQuickBrickAsset(PLUGIN, "button/flavour_1/mute.png")
|
|
31
|
+
),
|
|
32
|
+
unmute: resolveDefaultAssetUri(
|
|
33
|
+
getQuickBrickAsset(PLUGIN, "button/flavour_1/unmute.png")
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
flavour_2: {
|
|
37
|
+
mute: resolveDefaultAssetUri(
|
|
38
|
+
getQuickBrickAsset(PLUGIN, "button/flavour_2/mute.png")
|
|
39
|
+
),
|
|
40
|
+
unmute: resolveDefaultAssetUri(
|
|
41
|
+
getQuickBrickAsset(PLUGIN, "button/flavour_2/unmute.png")
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { selectActionAsset, toFlavourTuple } from "./assetResolver";
|
|
2
|
+
import { selectLabel } from "./labelResolver";
|
|
3
|
+
import type { ActionAssets, ButtonAssets, Flavour, Labels } from "./types";
|
|
4
|
+
import type { MuteUnmuteEntryState } from "@applicaster/zapp-react-native-ui-components/Components/VideoLive/MuteUnmuteController";
|
|
5
|
+
|
|
6
|
+
type BuildEntryStateArgs = {
|
|
7
|
+
state: MuteUnmuteEntryState;
|
|
8
|
+
actionAssets: Record<Flavour, ActionAssets>;
|
|
9
|
+
buttonAssets: Record<Flavour, ButtonAssets>;
|
|
10
|
+
labels: Labels;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const buildEntryState = ({
|
|
14
|
+
state: { muted, activeCellId },
|
|
15
|
+
actionAssets,
|
|
16
|
+
buttonAssets,
|
|
17
|
+
labels,
|
|
18
|
+
}: BuildEntryStateArgs): CellActionEntryState => ({
|
|
19
|
+
// Legacy player convention: state 1 = muted (shows the Unmute icon).
|
|
20
|
+
state: muted ? 1 : 0,
|
|
21
|
+
asset: toFlavourTuple(actionAssets, (a) => selectActionAsset(a, muted)),
|
|
22
|
+
mobileButtonAssets: toFlavourTuple(buttonAssets, (a) =>
|
|
23
|
+
selectActionAsset(a, muted)
|
|
24
|
+
),
|
|
25
|
+
label: selectLabel(labels, muted),
|
|
26
|
+
checked: muted,
|
|
27
|
+
visibleInCells: activeCellId ? [activeCellId] : [],
|
|
28
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
2
|
+
import { getPluginConfiguration } from "@applicaster/zapp-react-native-utils/reactHooks/usePluginConfiguration";
|
|
3
|
+
import {
|
|
4
|
+
uiActionsRegistry,
|
|
5
|
+
type RegisteredActionValue,
|
|
6
|
+
} from "@applicaster/zapp-react-native-utils/uiActionsRegistrator/registry";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
MUTE_UNMUTE_ACTION_IDENTIFIER,
|
|
10
|
+
muteUnmuteController,
|
|
11
|
+
type MuteUnmuteEntryState,
|
|
12
|
+
} from "@applicaster/zapp-react-native-ui-components/Components/VideoLive/MuteUnmuteController";
|
|
13
|
+
import { resolveActionAssets, resolveButtonAssets } from "./assetResolver";
|
|
14
|
+
import { resolveLabels } from "./labelResolver";
|
|
15
|
+
import { buildEntryState } from "./entryState";
|
|
16
|
+
import type {
|
|
17
|
+
MuteUnmuteActionConfiguration,
|
|
18
|
+
MuteUnmuteActionLocalizations,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
type Assets = Pick<
|
|
22
|
+
Parameters<typeof buildEntryState>[0],
|
|
23
|
+
"actionAssets" | "buttonAssets"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
let cache: { plugin: unknown; assets: Assets } | null = null;
|
|
27
|
+
|
|
28
|
+
// The `mute_unmute_action` plugin is configuration only: assets and labels.
|
|
29
|
+
// Raw values are read because every field is optional and has a bundled default.
|
|
30
|
+
const getVisuals = () => {
|
|
31
|
+
const plugin = getPluginByIdentifier(MUTE_UNMUTE_ACTION_IDENTIFIER, true);
|
|
32
|
+
|
|
33
|
+
if (!cache || cache.plugin !== plugin) {
|
|
34
|
+
const configuration = (plugin as any)?.configuration as
|
|
35
|
+
| MuteUnmuteActionConfiguration
|
|
36
|
+
| undefined;
|
|
37
|
+
|
|
38
|
+
cache = {
|
|
39
|
+
plugin,
|
|
40
|
+
assets: {
|
|
41
|
+
actionAssets: resolveActionAssets({ configuration }),
|
|
42
|
+
buttonAssets: resolveButtonAssets({ configuration }),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Not cached: the locale can change at runtime.
|
|
48
|
+
const { localizations } = getPluginConfiguration<
|
|
49
|
+
unknown,
|
|
50
|
+
MuteUnmuteActionLocalizations
|
|
51
|
+
>(plugin);
|
|
52
|
+
|
|
53
|
+
return { ...cache.assets, labels: resolveLabels(localizations) };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const toEntryState = (state: MuteUnmuteEntryState) =>
|
|
57
|
+
buildEntryState({ state, ...getVisuals() });
|
|
58
|
+
|
|
59
|
+
/** Visual binding: maps controller state to what cells and buttons render. */
|
|
60
|
+
export const muteUnmuteUIAction: RegisteredActionValue = {
|
|
61
|
+
initialEntryState: (item) =>
|
|
62
|
+
toEntryState(muteUnmuteController.getEntryState(String(item.id))),
|
|
63
|
+
addListener: (entryId, listener) => {
|
|
64
|
+
const remove = muteUnmuteController.subscribe(String(entryId), (state) =>
|
|
65
|
+
listener(toEntryState(state))
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
listener(toEntryState(muteUnmuteController.getEntryState(String(entryId))));
|
|
69
|
+
|
|
70
|
+
return remove;
|
|
71
|
+
},
|
|
72
|
+
invokeAction: (item) => muteUnmuteController.toggle(String(item.id)),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const registerMuteUnmuteUIAction = () =>
|
|
76
|
+
uiActionsRegistry.registerAction(
|
|
77
|
+
MUTE_UNMUTE_ACTION_IDENTIFIER,
|
|
78
|
+
muteUnmuteUIAction
|
|
79
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { firstConfiguredAsset } from "./assetResolver";
|
|
2
|
+
import type { Labels, MuteUnmuteActionLocalizations } from "./types";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_LABELS: Labels = { mute: "Mute", unmute: "Unmute" };
|
|
5
|
+
|
|
6
|
+
export const resolveLabels = (
|
|
7
|
+
localizations?: MuteUnmuteActionLocalizations
|
|
8
|
+
): Labels => ({
|
|
9
|
+
mute: firstConfiguredAsset(localizations?.mute_label) ?? DEFAULT_LABELS.mute,
|
|
10
|
+
unmute:
|
|
11
|
+
firstConfiguredAsset(localizations?.unmute_label) ?? DEFAULT_LABELS.unmute,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const selectLabel = (labels: Labels, muted: boolean): string =>
|
|
15
|
+
muted ? labels.unmute : labels.mute;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Flavour = "flavour_1" | "flavour_2";
|
|
2
|
+
|
|
3
|
+
export type ActionAssets = { mute: string; unmute: string };
|
|
4
|
+
|
|
5
|
+
export type ButtonAssets = ActionAssets;
|
|
6
|
+
|
|
7
|
+
export type Labels = { mute: string; unmute: string };
|
|
8
|
+
|
|
9
|
+
type MobileKind = "action" | "button";
|
|
10
|
+
|
|
11
|
+
export type MuteUnmuteActionConfiguration = Partial<
|
|
12
|
+
Record<`mobile_${MobileKind}_${Flavour}_${keyof ButtonAssets}_asset`, string>
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export type MuteUnmuteActionLocalizations = {
|
|
16
|
+
mute_label?: string;
|
|
17
|
+
unmute_label?: string;
|
|
18
|
+
};
|