@applicaster/zapp-react-native-ui-components 15.0.0-rc.14 → 15.0.0-rc.141

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.
Files changed (197) hide show
  1. package/Components/AnimatedInOut/index.tsx +69 -26
  2. package/Components/BaseFocusable/index.ios.ts +12 -2
  3. package/Components/Cell/Cell.tsx +14 -3
  4. package/Components/Cell/CellWithFocusable.tsx +9 -0
  5. package/Components/Cell/FocusableWrapper.tsx +3 -0
  6. package/Components/Cell/TvOSCellComponent.tsx +25 -6
  7. package/Components/Focusable/Focusable.tsx +4 -2
  8. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  9. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  10. package/Components/FocusableGroup/FocusableTvOS.tsx +32 -1
  11. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
  12. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  13. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  14. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  15. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  16. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +22 -6
  17. package/Components/HandlePlayable/HandlePlayable.tsx +33 -94
  18. package/Components/HandlePlayable/const.ts +3 -0
  19. package/Components/HandlePlayable/utils.ts +105 -0
  20. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  21. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  22. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  23. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  24. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  25. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  26. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  27. package/Components/Layout/TV/index.tsx +3 -4
  28. package/Components/Layout/TV/index.web.tsx +3 -4
  29. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  31. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  32. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  33. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  34. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  35. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  36. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  37. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  38. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  39. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +16 -1
  40. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +30 -2
  41. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  42. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
  43. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  44. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  45. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  46. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  47. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  48. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +43 -22
  49. package/Components/MasterCell/DefaultComponents/PressableView.tsx +261 -0
  50. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
  51. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
  52. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
  53. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
  54. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  55. package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
  56. package/Components/MasterCell/DefaultComponents/Text/index.tsx +10 -14
  57. package/Components/MasterCell/DefaultComponents/index.ts +2 -0
  58. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +42 -0
  59. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +127 -0
  60. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +23 -0
  61. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  62. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  63. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +32 -0
  64. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +195 -0
  65. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +140 -0
  66. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +222 -0
  67. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  68. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +104 -0
  69. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  70. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +73 -0
  71. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +86 -0
  72. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +35 -48
  73. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  74. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +39 -144
  75. package/Components/MasterCell/elementMapper.tsx +1 -0
  76. package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
  77. package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
  78. package/Components/MasterCell/index.tsx +2 -0
  79. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
  80. package/Components/MasterCell/utils/index.ts +61 -31
  81. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
  82. package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
  83. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
  84. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
  85. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  86. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  87. package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
  88. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  89. package/Components/PlayerContainer/PlayerContainer.tsx +44 -65
  90. package/Components/PlayerImageBackground/index.tsx +3 -22
  91. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  92. package/Components/PreloaderWrapper/index.tsx +15 -0
  93. package/Components/River/ComponentsMap/ComponentsMap.tsx +18 -16
  94. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  95. package/Components/River/RefreshControl.tsx +19 -82
  96. package/Components/River/River.tsx +9 -82
  97. package/Components/River/RiverItem.tsx +26 -20
  98. package/Components/River/TV/River.tsx +31 -14
  99. package/Components/River/TV/index.tsx +8 -4
  100. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  101. package/Components/River/TV/utils/index.ts +4 -0
  102. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  103. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  104. package/Components/River/__tests__/componentsMap.test.js +38 -0
  105. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  106. package/Components/River/hooks/index.ts +1 -0
  107. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  108. package/Components/Screen/TV/index.web.tsx +4 -2
  109. package/Components/Screen/__tests__/Screen.test.tsx +66 -42
  110. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  111. package/Components/Screen/hooks.ts +75 -6
  112. package/Components/Screen/index.tsx +9 -4
  113. package/Components/Screen/navigationHandler.ts +49 -24
  114. package/Components/Screen/orientationHandler.ts +10 -13
  115. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  116. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  117. package/Components/ScreenFeedLoader/index.ts +1 -0
  118. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  119. package/Components/ScreenResolver/hooks/index.ts +3 -0
  120. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  121. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  122. package/Components/ScreenResolver/index.tsx +15 -111
  123. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  124. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  125. package/Components/ScreenResolver/utils/index.ts +1 -0
  126. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  127. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  128. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  129. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  130. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  131. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  132. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +44 -26
  133. package/Components/Tabs/TV/Tabs.tsx +20 -3
  134. package/Components/Tabs/TabContent.tsx +7 -4
  135. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  136. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  137. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  138. package/Components/TopCutoffOverlay/index.tsx +55 -0
  139. package/Components/Transitioner/Scene.tsx +10 -3
  140. package/Components/Transitioner/index.js +3 -3
  141. package/Components/VideoLive/LiveImageManager.ts +199 -54
  142. package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
  143. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  144. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +1 -0
  145. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +118 -171
  146. package/Components/VideoModal/ModalAnimation/index.ts +2 -13
  147. package/Components/VideoModal/ModalAnimation/utils.ts +1 -327
  148. package/Components/VideoModal/PlayerWrapper.tsx +14 -88
  149. package/Components/VideoModal/VideoModal.tsx +1 -5
  150. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  151. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  152. package/Components/VideoModal/hooks/useModalSize.ts +10 -5
  153. package/Components/VideoModal/playerWrapperStyle.ts +70 -0
  154. package/Components/VideoModal/playerWrapperUtils.ts +91 -0
  155. package/Components/VideoModal/utils.ts +19 -9
  156. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  157. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  158. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  159. package/Components/ZappUIComponent/index.tsx +12 -6
  160. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  161. package/Components/index.js +1 -1
  162. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  163. package/Contexts/ScreenContext/index.tsx +71 -19
  164. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  165. package/Contexts/ZappHookModalContext/index.tsx +37 -61
  166. package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
  167. package/Contexts/index.ts +0 -2
  168. package/Decorators/Analytics/index.tsx +6 -5
  169. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  170. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  171. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
  172. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
  173. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  174. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  175. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  176. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  177. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  178. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  179. package/Helpers/DataSourceHelper/index.ts +19 -0
  180. package/events/index.ts +3 -0
  181. package/events/scrollEndReached.ts +15 -0
  182. package/index.d.ts +7 -0
  183. package/package.json +6 -5
  184. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  185. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  186. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  187. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  188. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  189. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +0 -60
  190. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +0 -417
  191. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +0 -294
  192. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +0 -176
  193. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +0 -93
  194. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +0 -500
  195. package/Components/VideoModal/ModalAnimation/__tests__/getMoveUpValue.test.ts +0 -108
  196. package/Helpers/DataSourceHelper/index.js +0 -19
  197. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -0,0 +1,352 @@
1
+ import { prepareEntry } from "../index";
2
+
3
+ const makeEntry = (overrides = {}): any => ({
4
+ content: { src: "https://example.com/video.mp4", type: "video/mp4" },
5
+ extensions: {},
6
+ ...overrides,
7
+ });
8
+
9
+ describe("prepareEntry", () => {
10
+ it("should return null when entry is null or undefined", () => {
11
+ expect(prepareEntry(null)).toBeNull();
12
+ expect(prepareEntry(undefined)).toBeNull();
13
+ });
14
+
15
+ describe("preview_playback_override", () => {
16
+ it("should deep merge override with entry, override taking precedence", () => {
17
+ const override = {
18
+ link: { href: "https://video-preload.com/id", rel: "self" },
19
+ extensions: {
20
+ brightcove: { video_id: 1234 },
21
+ preview: true,
22
+ free: true,
23
+ requires_authentication: false,
24
+ },
25
+ };
26
+
27
+ const entry = makeEntry({
28
+ extensions: {
29
+ preview_playback_override: override,
30
+ existing_field: "keep-me",
31
+ },
32
+ });
33
+
34
+ const result = prepareEntry(entry);
35
+
36
+ expect(result.extensions.brightcove.video_id).toBe(1234);
37
+ expect(result.extensions.preview).toBe(true);
38
+ expect(result.extensions.existing_field).toBe("keep-me");
39
+
40
+ expect(result.link).toEqual({
41
+ href: "https://video-preload.com/id",
42
+ rel: "self",
43
+ });
44
+
45
+ expect(result.content).toEqual(entry.content);
46
+ });
47
+
48
+ it("should override entry fields when both have the same key", () => {
49
+ const override = {
50
+ extensions: {
51
+ brightcove: { video_id: 9999 },
52
+ },
53
+ content: { src: "https://override.com/video.mp4" },
54
+ };
55
+
56
+ const entry = makeEntry({
57
+ extensions: {
58
+ preview_playback_override: override,
59
+ brightcove: { video_id: 1111, account_id: "abc" },
60
+ },
61
+ content: { src: "https://original.com/video.mp4", type: "video/mp4" },
62
+ });
63
+
64
+ const result = prepareEntry(entry);
65
+
66
+ expect(result.extensions.brightcove.video_id).toBe(9999);
67
+ expect(result.extensions.brightcove.account_id).toBe("abc");
68
+ expect(result.content.src).toBe("https://override.com/video.mp4");
69
+ expect(result.content.type).toBe("video/mp4");
70
+ });
71
+
72
+ it("should take priority over other playback sources", () => {
73
+ const override = {
74
+ link: { href: "https://override.com" },
75
+ extensions: { brightcove: { video_id: 5555 } },
76
+ };
77
+
78
+ const entry = makeEntry({
79
+ extensions: {
80
+ preview_playback_override: override,
81
+ brightcove: { preview_playback: "other-id" },
82
+ preview_playback: "https://other.com/video.mp4",
83
+ },
84
+ content: {
85
+ src: "https://example.com/video.mp4",
86
+ teaser: "https://teaser.com/video.mp4",
87
+ },
88
+ });
89
+
90
+ const result = prepareEntry(entry);
91
+
92
+ expect(result.link).toEqual({ href: "https://override.com" });
93
+ expect(result.extensions.brightcove.video_id).toBe(5555);
94
+ });
95
+ });
96
+
97
+ describe("brightcove preview_playback", () => {
98
+ it("should return entry with brightcove video_id replaced by preview_playback", () => {
99
+ const entry = makeEntry({
100
+ extensions: {
101
+ brightcove: {
102
+ video_id: "original-id",
103
+ preview_playback: "preview-id",
104
+ },
105
+ some_other: "value",
106
+ },
107
+ });
108
+
109
+ const result = prepareEntry(entry);
110
+
111
+ expect(result).toEqual({
112
+ ...entry,
113
+ extensions: {
114
+ ...entry.extensions,
115
+ brightcove: {
116
+ video_id: "preview-id",
117
+ preview_playback: "preview-id",
118
+ },
119
+ },
120
+ });
121
+ });
122
+
123
+ it("should preserve other extensions", () => {
124
+ const entry = makeEntry({
125
+ extensions: {
126
+ brightcove: { preview_playback: "preview-id" },
127
+ custom_field: "keep-me",
128
+ },
129
+ });
130
+
131
+ const result = prepareEntry(entry);
132
+
133
+ expect(result.extensions.custom_field).toBe("keep-me");
134
+ });
135
+ });
136
+
137
+ describe("extensions.preview_playback (non-brightcove)", () => {
138
+ it("should return entry with content.src set to preview_playback URL", () => {
139
+ const entry = makeEntry({
140
+ extensions: { preview_playback: "https://preview.com/video.m3u8" },
141
+ content: { src: "https://original.com/video.mp4" },
142
+ });
143
+
144
+ const result = prepareEntry(entry);
145
+
146
+ expect(result.content.src).toBe("https://preview.com/video.m3u8");
147
+ });
148
+
149
+ it("should preserve other content fields", () => {
150
+ const entry = makeEntry({
151
+ extensions: { preview_playback: "https://preview.com/video.m3u8" },
152
+ content: { src: "https://original.com/video.mp4", type: "video/mp4" },
153
+ });
154
+
155
+ const result = prepareEntry(entry);
156
+
157
+ expect(result.content.type).toBe("video/mp4");
158
+ });
159
+ });
160
+
161
+ describe("content.teaser", () => {
162
+ it("should return entry with content.src set to teaser URL", () => {
163
+ const entry = makeEntry({
164
+ extensions: {},
165
+ content: {
166
+ src: "https://original.com/video.mp4",
167
+ teaser: "https://teaser.com/teaser.mp4",
168
+ },
169
+ });
170
+
171
+ const result = prepareEntry(entry);
172
+
173
+ expect(result.content.src).toBe("https://teaser.com/teaser.mp4");
174
+ });
175
+ });
176
+
177
+ describe("brightcove video_id (without preview_playback)", () => {
178
+ it("should return the entry as-is when brightcove video_id exists", () => {
179
+ const entry = makeEntry({
180
+ extensions: { brightcove: { video_id: "some-id" } },
181
+ content: { src: "" },
182
+ });
183
+
184
+ const result = prepareEntry(entry);
185
+
186
+ expect(result).toBe(entry);
187
+ });
188
+ });
189
+
190
+ describe("content.src fallback", () => {
191
+ it("should return null when content.src is missing", () => {
192
+ const entry = makeEntry({
193
+ extensions: {},
194
+ content: {},
195
+ });
196
+
197
+ expect(prepareEntry(entry)).toBeNull();
198
+ });
199
+
200
+ it("should return null when content.src is empty", () => {
201
+ const entry = makeEntry({
202
+ extensions: {},
203
+ content: { src: "" },
204
+ });
205
+
206
+ expect(prepareEntry(entry)).toBeNull();
207
+ });
208
+
209
+ it("should return entry when content.type starts with video", () => {
210
+ const entry = makeEntry({
211
+ extensions: {},
212
+ content: { src: "https://example.com/stream", type: "video/mp4" },
213
+ });
214
+
215
+ expect(prepareEntry(entry)).toBe(entry);
216
+ });
217
+
218
+ it("should return entry when src has a known video extension", () => {
219
+ const extensions = ["m3u8", "mp4", "m4v", "mkv", "mov", "mpd", "ogv"];
220
+
221
+ for (const ext of extensions) {
222
+ const entry = makeEntry({
223
+ extensions: {},
224
+ content: { src: `https://example.com/video.${ext}` },
225
+ });
226
+
227
+ expect(prepareEntry(entry)).toBe(entry);
228
+ }
229
+ });
230
+
231
+ it("should return entry for audio extensions", () => {
232
+ const extensions = ["mp3", "oga", "opus"];
233
+
234
+ for (const ext of extensions) {
235
+ const entry = makeEntry({
236
+ extensions: {},
237
+ content: { src: `https://example.com/audio.${ext}` },
238
+ });
239
+
240
+ expect(prepareEntry(entry)).toBe(entry);
241
+ }
242
+ });
243
+
244
+ it("should return null for non-video/non-audio URLs", () => {
245
+ const entry = makeEntry({
246
+ extensions: {},
247
+ content: { src: "https://example.com/page.html" },
248
+ });
249
+
250
+ expect(prepareEntry(entry)).toBeNull();
251
+ });
252
+
253
+ it("should return null for URLs without extensions", () => {
254
+ const entry = makeEntry({
255
+ extensions: {},
256
+ content: { src: "https://example.com/stream" },
257
+ });
258
+
259
+ expect(prepareEntry(entry)).toBeNull();
260
+ });
261
+ });
262
+
263
+ describe("priority order", () => {
264
+ it("should prefer brightcove.preview_playback over extensions.preview_playback", () => {
265
+ const entry = makeEntry({
266
+ extensions: {
267
+ brightcove: { preview_playback: "bc-preview" },
268
+ preview_playback: "ext-preview",
269
+ },
270
+ });
271
+
272
+ const result = prepareEntry(entry);
273
+
274
+ expect(result.extensions.brightcove.video_id).toBe("bc-preview");
275
+ });
276
+
277
+ it("should prefer extensions.preview_playback over content.teaser", () => {
278
+ const entry = makeEntry({
279
+ extensions: { preview_playback: "https://preview.com/video.mp4" },
280
+ content: {
281
+ src: "https://original.com/video.mp4",
282
+ teaser: "https://teaser.com/video.mp4",
283
+ },
284
+ });
285
+
286
+ const result = prepareEntry(entry);
287
+
288
+ expect(result.content.src).toBe("https://preview.com/video.mp4");
289
+ });
290
+
291
+ it("should prefer preview_playback_override over all other sources", () => {
292
+ const override = {
293
+ extensions: { brightcove: { video_id: "override-id" } },
294
+ };
295
+
296
+ const entry = makeEntry({
297
+ extensions: {
298
+ preview_playback_override: override,
299
+ brightcove: { preview_playback: "bc-preview", video_id: "original" },
300
+ preview_playback: "https://ext-preview.com/video.mp4",
301
+ },
302
+ content: {
303
+ src: "https://original.com/video.mp4",
304
+ teaser: "https://teaser.com/video.mp4",
305
+ },
306
+ });
307
+
308
+ const result = prepareEntry(entry);
309
+
310
+ expect(result.extensions.brightcove.video_id).toBe("override-id");
311
+ });
312
+
313
+ it("should prefer content.teaser over brightcove.video_id", () => {
314
+ const entry = makeEntry({
315
+ extensions: { brightcove: { video_id: "some-id" } },
316
+ content: {
317
+ src: "https://original.com/video.mp4",
318
+ teaser: "https://teaser.com/teaser.mp4",
319
+ },
320
+ });
321
+
322
+ const result = prepareEntry(entry);
323
+
324
+ expect(result.content.src).toBe("https://teaser.com/teaser.mp4");
325
+ });
326
+
327
+ it("should prefer content.teaser over content.src URL check", () => {
328
+ const entry = makeEntry({
329
+ extensions: {},
330
+ content: {
331
+ src: "https://original.com/video.mp4",
332
+ teaser: "https://teaser.com/teaser.m3u8",
333
+ },
334
+ });
335
+
336
+ const result = prepareEntry(entry);
337
+
338
+ expect(result.content.src).toBe("https://teaser.com/teaser.m3u8");
339
+ });
340
+
341
+ it("should prefer brightcove.video_id over content.src URL check", () => {
342
+ const entry = makeEntry({
343
+ extensions: { brightcove: { video_id: "bc-id" } },
344
+ content: { src: "https://example.com/page.html" },
345
+ });
346
+
347
+ const result = prepareEntry(entry);
348
+
349
+ expect(result).toBe(entry);
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,136 @@
1
+ import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
2
+ import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
3
+ import { loggerLiveImageManager } from "../../../VideoLive/loggerHelper";
4
+
5
+ const { log_debug, log_error } = loggerLiveImageManager;
6
+
7
+ export type PreloadHookConfig = ZappPreloadPlugins & {
8
+ configuration: any;
9
+ };
10
+
11
+ function runHookWithCallback(
12
+ invoke: (
13
+ payload: any,
14
+ callback: (result: hookCallbackArgs) => void,
15
+ abortCallback: () => void
16
+ ) => void,
17
+ payload: any
18
+ ): Promise<{ payload: any; aborted: boolean }> {
19
+ return new Promise((resolve, reject) => {
20
+ const abortCallback = () => {
21
+ resolve({ payload, aborted: true });
22
+ };
23
+
24
+ try {
25
+ invoke(
26
+ payload,
27
+ ({
28
+ success,
29
+ error,
30
+ payload: resultPayload,
31
+ abort,
32
+ }: hookCallbackArgs) => {
33
+ if (error) {
34
+ reject(error);
35
+ } else if (!success || abort) {
36
+ resolve({ payload: resultPayload ?? payload, aborted: true });
37
+ } else {
38
+ resolve({ payload: resultPayload ?? payload, aborted: false });
39
+ }
40
+ },
41
+ abortCallback
42
+ );
43
+ } catch (error) {
44
+ log_error(`Error running preload hook: ${error.message}`, { error });
45
+ reject(error);
46
+ }
47
+ });
48
+ }
49
+
50
+ // TODO: Move this function to a more generic place,
51
+ // as it can be used by other components that want to run preload hooks
52
+ // and add test coverage for it
53
+ export async function executePreloadHooks({
54
+ preloadHooks,
55
+ entry,
56
+ }: {
57
+ preloadHooks: PreloadHookConfig[];
58
+ entry: ZappEntry;
59
+ }): Promise<ZappEntry | null> {
60
+ if (!preloadHooks?.length) {
61
+ return entry;
62
+ }
63
+
64
+ const sortedHooks = [...preloadHooks].sort(
65
+ (a, b) => (a.weight || 0) - (b.weight || 0)
66
+ );
67
+
68
+ const hookNames = sortedHooks.map((hook) => hook.identifier).join(", ");
69
+
70
+ log_debug(
71
+ `Preload hook sequence: ${hookNames}, entry: ${entry.id} - ${entry.title}`
72
+ );
73
+
74
+ let payload: any = entry;
75
+
76
+ let needsAbort = false;
77
+ const plugins = appStore.get("plugins");
78
+
79
+ for (const hookConfig of sortedHooks) {
80
+ const plugin = findPluginByIdentifier(hookConfig.identifier, plugins, true);
81
+
82
+ if (!plugin?.module) {
83
+ log_debug(
84
+ `Preload hook plugin not found: ${hookConfig.identifier}, skipping`
85
+ );
86
+
87
+ continue;
88
+ }
89
+
90
+ const { module: hookModule } = plugin;
91
+
92
+ if (hookModule.skipHook?.(payload)) {
93
+ log_debug(`Skipping hook: ${hookConfig.identifier}`);
94
+
95
+ continue;
96
+ }
97
+
98
+ if (hookModule.runInBackground) {
99
+ log_debug(
100
+ `Running background hook: ${hookConfig.identifier}, entry: ${payload?.id} - ${payload?.title}`
101
+ );
102
+
103
+ const result = await runHookWithCallback(
104
+ (item, callback, abortCallback) =>
105
+ hookModule
106
+ .runInBackground(item, callback, hookConfig, abortCallback)
107
+ ?.catch?.((error: Error) => {
108
+ log_error(
109
+ `Error running background preload hook: ${hookConfig.identifier}, error: ${error.message}`,
110
+ { error }
111
+ );
112
+
113
+ callback({ success: false, payload: item, error });
114
+ }),
115
+ payload
116
+ );
117
+
118
+ payload = result.payload;
119
+ needsAbort = result.aborted;
120
+ }
121
+
122
+ if (needsAbort) {
123
+ log_debug(
124
+ `Preload hook requested abort: ${hookConfig.identifier}, stopping chain`
125
+ );
126
+
127
+ break;
128
+ }
129
+ }
130
+
131
+ if (needsAbort) {
132
+ return null;
133
+ }
134
+
135
+ return payload;
136
+ }
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import * as R from "ramda";
2
+ import { merge, clone } from "@applicaster/zapp-react-native-utils/utils";
3
3
  import { Platform, View, ViewStyle } from "react-native";
4
4
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
5
5
  import { isTV, isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
@@ -9,6 +9,7 @@ import { useTrackCurrentAutoScrollingElement } from "@applicaster/zapp-react-nat
9
9
  import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
10
10
  import { getPropComponentType } from "@applicaster/zapp-react-native-utils/cellUtils";
11
11
  import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
12
+ import { useAccessibilityState } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
12
13
 
13
14
  type LiveImageProps = {
14
15
  item: ZappEntry;
@@ -82,34 +83,25 @@ const removeAdsFromEntry = (entry) => {
82
83
  return entry;
83
84
  }
84
85
 
85
- const newEntry = R.clone(entry);
86
+ const newEntry = clone(entry);
86
87
 
87
88
  delete newEntry.extensions.video_ads;
88
89
 
89
90
  return newEntry;
90
91
  };
91
92
 
92
- const prepareEntry = (entry) => {
93
+ export const prepareEntry = (entry: ZappEntry) => {
93
94
  if (!entry) {
94
95
  return null;
95
96
  }
96
97
 
97
- if (entry.extensions?.preview_playback) {
98
- return {
99
- ...entry,
100
- content: { ...entry?.content, src: entry?.extensions?.preview_playback },
101
- };
102
- }
98
+ const previewPlaybackOverride = entry.extensions?.preview_playback_override;
103
99
 
104
- if (entry.content?.teaser) {
105
- return {
106
- ...entry,
107
- content: { ...entry?.content, src: entry?.content?.teaser },
108
- };
100
+ if (previewPlaybackOverride) {
101
+ return merge({}, entry, previewPlaybackOverride);
109
102
  }
110
103
 
111
- const previewPlayback =
112
- entry.extensions?.["brightcove"]?.["preview_playback"];
104
+ const previewPlayback = entry.extensions?.brightcove?.preview_playback;
113
105
 
114
106
  if (previewPlayback) {
115
107
  return {
@@ -117,14 +109,28 @@ const prepareEntry = (entry) => {
117
109
  extensions: {
118
110
  ...entry.extensions,
119
111
  brightcove: {
120
- ...entry?.extensions?.["brightcove"],
112
+ ...entry?.extensions?.brightcove,
121
113
  video_id: previewPlayback,
122
114
  },
123
115
  },
124
116
  };
125
117
  }
126
118
 
127
- if (entry.extensions?.["brightcove"]?.["video_id"]) {
119
+ if (entry.extensions?.preview_playback) {
120
+ return {
121
+ ...entry,
122
+ content: { ...entry?.content, src: entry?.extensions?.preview_playback },
123
+ };
124
+ }
125
+
126
+ if (entry.content?.teaser) {
127
+ return {
128
+ ...entry,
129
+ content: { ...entry?.content, src: entry?.content?.teaser },
130
+ };
131
+ }
132
+
133
+ if (entry.extensions?.brightcove?.video_id) {
128
134
  return entry;
129
135
  }
130
136
 
@@ -167,14 +173,24 @@ function getFocusedState(
167
173
  const getPlayerConfig = (player_screen_id, actionIdentifier) => {
168
174
  if (player_screen_id) {
169
175
  const rivers = appStore.get("rivers");
176
+ const plugins = appStore.get("plugins");
170
177
  const playerScreen = rivers?.[player_screen_id] ?? null;
171
178
 
179
+ const preloadHooks = playerScreen?.hooks?.preload_plugins?.map((hook) => {
180
+ const configuration =
181
+ plugins.find((plugin) => plugin.identifier === hook.identifier)
182
+ ?.configuration || {};
183
+
184
+ return { ...hook, configuration, ...rivers[hook.screen_id] };
185
+ });
186
+
172
187
  // TODO: Check is it a player later
173
188
 
174
189
  // TODO: Add more dict if needed from the screen component, styles, data etc
175
190
  return {
176
191
  playerPluginId: playerScreen?.type ?? DEFAULT_PLAYER_IDENTIFIER,
177
- screenConfig: playerScreen?.["general"],
192
+ screenConfig: playerScreen?.general,
193
+ preloadHooks,
178
194
  };
179
195
  }
180
196
 
@@ -206,6 +222,9 @@ const LiveImageComponent = (props: LiveImageProps) => {
206
222
  state,
207
223
  } = props;
208
224
 
225
+ const accessibilityState = useAccessibilityState({});
226
+ const isScreenReaderEnabled = accessibilityState.screenReaderEnabled;
227
+
209
228
  const component = useUIComponentContext();
210
229
 
211
230
  // Fix for blinking on state change
@@ -226,7 +245,7 @@ const LiveImageComponent = (props: LiveImageProps) => {
226
245
 
227
246
  const isCurrentlyFocused = useTrackCurrentAutoScrollingElement(componentId);
228
247
 
229
- const { playerPluginId, screenConfig } =
248
+ const { playerPluginId, screenConfig, preloadHooks } =
230
249
  getPlayerConfig(player_screen_id, actionIdentifier) ?? {};
231
250
 
232
251
  const playableEntry = playerPluginId ? getPlayableEntry(item) : null;
@@ -236,10 +255,11 @@ const LiveImageComponent = (props: LiveImageProps) => {
236
255
 
237
256
  const isShouldRender =
238
257
  playerPlugin &&
239
- getFocusedState(state, componentType, isCurrentlyFocused) &&
240
258
  playableEntry &&
259
+ getFocusedState(state, componentType, isCurrentlyFocused) &&
241
260
  cellUUID &&
242
- isSupportedTVForLiveImage();
261
+ isSupportedTVForLiveImage() &&
262
+ !isScreenReaderEnabled;
243
263
 
244
264
  return (
245
265
  <>
@@ -255,6 +275,7 @@ const LiveImageComponent = (props: LiveImageProps) => {
255
275
  imageKey={imageKey}
256
276
  item={playableEntry}
257
277
  audioMutedByDefault={audio_muted_by_default}
278
+ preloadHooks={preloadHooks}
258
279
  />
259
280
  </View>
260
281
  ) : null}