@applicaster/zapp-react-native-ui-components 16.0.0-rc.12 → 16.0.0-rc.121

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 (166) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/CellRendererResolver/index.ts +1 -1
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  8. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  9. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  10. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  11. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  15. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  16. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  17. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  18. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  19. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -0
  20. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  22. package/Components/GeneralContentScreen/index.ts +2 -0
  23. package/Components/Layout/FullWidthRow.tsx +38 -0
  24. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  25. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  30. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  31. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  34. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  35. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  36. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +14 -6
  37. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  38. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  39. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  40. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +34 -21
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  50. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  52. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  53. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  54. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  55. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  56. package/Components/MasterCell/dataAdapter.ts +15 -5
  57. package/Components/MasterCell/hooks/index.ts +2 -0
  58. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  59. package/Components/MasterCell/index.tsx +57 -8
  60. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  61. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  62. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  63. package/Components/MasterCell/utils/index.ts +103 -46
  64. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  65. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  66. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  67. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  68. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +392 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +772 -0
  71. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +728 -0
  72. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +1028 -0
  73. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  74. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  75. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  76. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  77. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +433 -0
  78. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +563 -0
  79. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  80. package/Components/ModalComponent/BottomSheetModalContent.tsx +440 -50
  81. package/Components/ModalComponent/Button/index.tsx +5 -3
  82. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  83. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  84. package/Components/ModalComponent/Header/index.tsx +20 -7
  85. package/Components/ModalComponent/Header/utils.ts +1 -1
  86. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  87. package/Components/ModalComponent/SortableList.tsx +156 -0
  88. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  89. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  90. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  91. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  92. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  93. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  94. package/Components/ModalComponent/__tests__/showTextInput.test.ts +120 -0
  95. package/Components/ModalComponent/index.tsx +18 -9
  96. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  97. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  98. package/Components/ModalComponent/presets/index.ts +27 -0
  99. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  100. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  101. package/Components/ModalComponent/presets/types.ts +35 -0
  102. package/Components/ModalComponent/showTextInput.ts +54 -0
  103. package/Components/ModalComponent/utils.ts +82 -21
  104. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  105. package/Components/OfflineHandler/__tests__/index.test.tsx +186 -17
  106. package/Components/OfflineHandler/hooks.ts +111 -0
  107. package/Components/OfflineHandler/index.tsx +91 -75
  108. package/Components/OfflineHandler/utils/index.ts +1 -13
  109. package/Components/PlayerContainer/PlayerContainer.tsx +28 -11
  110. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  111. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  112. package/Components/PreloaderWrapper/index.tsx +1 -1
  113. package/Components/River/ComponentsMap/ComponentsMap.tsx +91 -62
  114. package/Components/River/River.tsx +4 -0
  115. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +142 -89
  116. package/Components/River/__tests__/componentsMap.test.js +8 -3
  117. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  118. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  119. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  120. package/Components/Screen/index.tsx +13 -4
  121. package/Components/Screen/navigationHandler.ts +8 -8
  122. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  123. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  124. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  125. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  126. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  127. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  128. package/Components/TopCutoffOverlay/index.tsx +2 -2
  129. package/Components/Transitioner/AnimationManager.js +1 -1
  130. package/Components/Transitioner/Scene.tsx +4 -3
  131. package/Components/Transitioner/Transitioner.tsx +98 -49
  132. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  133. package/Components/VideoLive/LiveImageManager.ts +9 -3
  134. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  135. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  136. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  137. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  138. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  139. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  140. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  141. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  142. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  143. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  144. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  145. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  146. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  147. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  148. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  149. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  150. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  151. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  152. package/Helpers/index.js +0 -39
  153. package/Hooks/useEntryActionsExpander.ts +63 -0
  154. package/assets/empty-Info.png +0 -0
  155. package/package.json +6 -5
  156. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  157. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  158. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  159. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  160. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  161. package/Components/Screen/utils.ts +0 -16
  162. package/Helpers/Analytics/index.js +0 -95
  163. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  164. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  165. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  166. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -1,26 +1,49 @@
1
1
  import React from "react";
2
+ import { Text } from "react-native";
2
3
  import { render } from "@testing-library/react-native";
4
+ import { subscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
3
5
 
4
6
  jest.useFakeTimers({ legacyFakeTimers: true });
5
7
 
8
+ // `isOnline` (@applicaster/quick-brick-core/App/NetworkStatusProvider/utils)
9
+ // only treats connectionInfo as "ready" when it has NetInfoState-shaped
10
+ // isConnected/isInternetReachable/details fields, so the mock has to mirror
11
+ // that shape for online/offline to actually be distinguishable.
6
12
  let mockConnectionStatus = false;
7
- const mockPreviousValue = null;
13
+ let mockStillOfflineAfterResumeCount = 0;
8
14
 
9
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/connection", () => {
10
- return {
11
- ...jest.requireActual(
12
- "@applicaster/zapp-react-native-utils/reactHooks/connection"
13
- ),
14
- useConnectionInfo: jest.fn(() => mockConnectionStatus),
15
+ const mockComponentsLogger = {
16
+ log: jest.fn(),
17
+ debug: jest.fn(),
18
+ info: jest.fn(),
19
+ warning: jest.fn(),
20
+ error: jest.fn(),
21
+ };
22
+
23
+ jest.mock("../../../Helpers/logger", () => {
24
+ const proxy = {
25
+ log: (event) => mockComponentsLogger.log(event),
26
+ debug: (event) => mockComponentsLogger.debug(event),
27
+ info: (event) => mockComponentsLogger.info(event),
28
+ warning: (event) => mockComponentsLogger.warning(event),
29
+ error: (event) => mockComponentsLogger.error(event),
30
+ addSubsystem: () => proxy,
15
31
  };
32
+
33
+ return { componentsLogger: proxy };
16
34
  });
17
35
 
18
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => {
36
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/connection", () => {
19
37
  return {
20
38
  ...jest.requireActual(
21
- "@applicaster/zapp-react-native-utils/reactHooks/utils"
39
+ "@applicaster/zapp-react-native-utils/reactHooks/connection"
22
40
  ),
23
- usePrevious: jest.fn(() => mockPreviousValue),
41
+ useConnectionInfo: jest.fn(() => ({
42
+ details: {},
43
+ isConnected: mockConnectionStatus,
44
+ isInternetReachable: mockConnectionStatus,
45
+ stillOfflineAfterResumeCount: mockStillOfflineAfterResumeCount,
46
+ })),
24
47
  };
25
48
  });
26
49
 
@@ -54,21 +77,167 @@ jest.mock("react-native-safe-area-context", () => ({
54
77
  }));
55
78
 
56
79
  describe("OfflineHandler", () => {
57
- const { OfflineHandler, NotificationView } = require("../");
80
+ const { OfflineHandler } = require("../");
81
+
82
+ beforeEach(() => {
83
+ mockConnectionStatus = false;
84
+ mockStillOfflineAfterResumeCount = 0;
85
+ });
86
+
87
+ it("renders its children without wrapping them in a notification view", () => {
88
+ const { toJSON } = render(
89
+ <OfflineHandler>
90
+ <Text>child content</Text>
91
+ </OfflineHandler>
92
+ );
58
93
 
59
- it("renders", () => {
60
- const { toJSON } = render(<OfflineHandler />);
61
94
  expect(toJSON()).toMatchSnapshot();
62
95
  });
63
96
 
64
- it("renders Notification mode if component was rendered while online", () => {
97
+ it("emits a showToast event on mount when the device starts offline", () => {
98
+ mockConnectionStatus = false;
99
+
100
+ const handleShowToast = jest.fn();
101
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
102
+
103
+ render(<OfflineHandler />);
104
+
105
+ expect(handleShowToast).toHaveBeenCalledWith(
106
+ expect.objectContaining({
107
+ id: "offline-status",
108
+ message: "No internet connection",
109
+ timeout: 5000,
110
+ }),
111
+ // the shared event bus appends a dispose fn as the last handler arg
112
+ expect.any(Function)
113
+ );
114
+
115
+ unsubscribe();
116
+ });
117
+
118
+ it("does not emit a showToast event on mount when the device starts online", () => {
65
119
  mockConnectionStatus = true;
66
120
 
67
- const { rerender, UNSAFE_getByType } = render(<OfflineHandler />);
121
+ const handleShowToast = jest.fn();
122
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
123
+
124
+ render(<OfflineHandler />);
125
+
126
+ expect(handleShowToast).not.toHaveBeenCalled();
127
+
128
+ unsubscribe();
129
+ });
130
+
131
+ it("emits a showToast event when the device comes back online", () => {
132
+ mockConnectionStatus = false;
133
+
134
+ const handleShowToast = jest.fn();
135
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
68
136
 
137
+ const { rerender } = render(<OfflineHandler />);
138
+
139
+ // ignore the offline toast emitted on mount; we only care about the
140
+ // offline -> online transition here
141
+ handleShowToast.mockClear();
142
+
143
+ mockConnectionStatus = true;
69
144
  rerender(<OfflineHandler />);
70
145
 
71
- const notificationsView = UNSAFE_getByType(NotificationView);
72
- expect(notificationsView).toBeDefined();
146
+ expect(handleShowToast).toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ id: "offline-status",
149
+ message: "You are back online",
150
+ timeout: 2000,
151
+ }),
152
+ // the shared event bus appends a dispose fn as the last handler arg
153
+ expect.any(Function)
154
+ );
155
+
156
+ unsubscribe();
157
+ });
158
+
159
+ it("re-shows the offline toast when still offline after app resume", () => {
160
+ mockConnectionStatus = false;
161
+
162
+ const handleShowToast = jest.fn();
163
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
164
+
165
+ const { rerender } = render(<OfflineHandler />);
166
+ handleShowToast.mockClear();
167
+
168
+ mockStillOfflineAfterResumeCount = 1;
169
+ rerender(<OfflineHandler />);
170
+
171
+ expect(handleShowToast).toHaveBeenCalledTimes(1);
172
+
173
+ expect(handleShowToast).toHaveBeenCalledWith(
174
+ expect.objectContaining({
175
+ id: "offline-status",
176
+ message: "No internet connection",
177
+ timeout: 5000,
178
+ }),
179
+ expect.any(Function)
180
+ );
181
+
182
+ // further renders with the same counter must not re-emit
183
+ rerender(<OfflineHandler />);
184
+ expect(handleShowToast).toHaveBeenCalledTimes(1);
185
+
186
+ unsubscribe();
187
+ });
188
+
189
+ it("does not re-show the offline toast for a counter bump while online", () => {
190
+ mockConnectionStatus = true;
191
+
192
+ const handleShowToast = jest.fn();
193
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
194
+
195
+ const { rerender } = render(<OfflineHandler />);
196
+
197
+ mockStillOfflineAfterResumeCount = 1;
198
+ rerender(<OfflineHandler />);
199
+
200
+ expect(handleShowToast).not.toHaveBeenCalled();
201
+
202
+ unsubscribe();
203
+ });
204
+
205
+ it("does not emit on mount just because the counter is already > 0", () => {
206
+ mockConnectionStatus = true;
207
+ mockStillOfflineAfterResumeCount = 3;
208
+
209
+ const handleShowToast = jest.fn();
210
+ const unsubscribe = subscriberFor("showToast", handleShowToast);
211
+
212
+ render(<OfflineHandler />);
213
+
214
+ expect(handleShowToast).not.toHaveBeenCalled();
215
+
216
+ unsubscribe();
217
+ });
218
+
219
+ it("logs toast emissions at info level", () => {
220
+ mockConnectionStatus = false;
221
+ mockComponentsLogger.info.mockClear();
222
+ mockComponentsLogger.warning.mockClear();
223
+
224
+ const { rerender } = render(<OfflineHandler />);
225
+
226
+ mockConnectionStatus = true;
227
+ rerender(<OfflineHandler />);
228
+
229
+ expect(mockComponentsLogger.info).toHaveBeenCalledWith(
230
+ expect.objectContaining({
231
+ message: "OfflineHandler: device is offline, emit toast",
232
+ })
233
+ );
234
+
235
+ expect(mockComponentsLogger.info).toHaveBeenCalledWith(
236
+ expect.objectContaining({
237
+ message: "OfflineHandler: device is back online, emit toast",
238
+ })
239
+ );
240
+
241
+ expect(mockComponentsLogger.warning).not.toHaveBeenCalled();
73
242
  });
74
243
  });
@@ -0,0 +1,111 @@
1
+ import React, { useMemo } from "react";
2
+
3
+ import {
4
+ isWeb,
5
+ platformSelect,
6
+ } from "@applicaster/zapp-react-native-utils/reactUtils";
7
+ import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
8
+
9
+ import { usePlugins } from "@applicaster/zapp-react-native-redux";
10
+ import { textTransform } from "@applicaster/zapp-react-native-utils/cellUtils";
11
+ import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
12
+
13
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
14
+
15
+ const OFFLINE_EXPERIENCE = "offline-experience";
16
+
17
+ const NETWORK_STATUS_LOCALIZATIONS_KEY = "network_status_localizations";
18
+
19
+ const THEME_STORAGE_NAMESPACE = "quick-brick-theme";
20
+
21
+ export function useNetworkStatusLocalizations() {
22
+ const [storedLocalizations, setStoredLocalizations] = React.useState<Record<
23
+ string,
24
+ string
25
+ > | null>(null);
26
+
27
+ React.useEffect(() => {
28
+ async function loadStoredLocalizations() {
29
+ try {
30
+ const stored = await sessionStorage.getItem(
31
+ NETWORK_STATUS_LOCALIZATIONS_KEY,
32
+ THEME_STORAGE_NAMESPACE
33
+ );
34
+
35
+ if (stored) {
36
+ setStoredLocalizations(stored);
37
+ }
38
+ } catch (error) {
39
+ console.error("Error loading network status localizations", error);
40
+ }
41
+ }
42
+
43
+ loadStoredLocalizations();
44
+ }, []);
45
+
46
+ return storedLocalizations;
47
+ }
48
+
49
+ export function useOfflineHandlerProps(online: boolean) {
50
+ const theme = useTheme<BaseThemePropertiesTV>();
51
+ const storedLocalizations = useNetworkStatusLocalizations();
52
+ const plugins = usePlugins();
53
+ const offlinePlugin = findPluginByIdentifier(OFFLINE_EXPERIENCE, plugins);
54
+ const { useOfflineExperienceConfiguration } = offlinePlugin?.module ?? {};
55
+
56
+ const {
57
+ configurationFields,
58
+ localizations,
59
+ }: {
60
+ configurationFields: OfflineExperiencePluginConfiguration;
61
+ localizations: OfflineExperienceLocalizations;
62
+ } = useOfflineExperienceConfiguration?.() ?? {};
63
+
64
+ let title, subtitle, style;
65
+
66
+ if (isWeb()) {
67
+ title = online
68
+ ? (theme?.online_notification_title ??
69
+ storedLocalizations?.online_notification_title)
70
+ : (theme?.offline_notification_title ??
71
+ storedLocalizations?.offline_notification_title);
72
+
73
+ subtitle = online
74
+ ? (theme?.online_notification_subtitle ??
75
+ storedLocalizations?.online_notification_subtitle)
76
+ : (theme?.offline_notification_subtitle ??
77
+ storedLocalizations?.offline_notification_subtitle);
78
+ } else {
79
+ title = textTransform(
80
+ configurationFields.offline_toast_message_text_transform,
81
+ online
82
+ ? localizations.online_toast_message
83
+ : localizations.offline_toast_message
84
+ );
85
+
86
+ style = {
87
+ color: online
88
+ ? configurationFields.offline_toast_message_online_font_color
89
+ : configurationFields.offline_toast_message_font_color,
90
+ backgroundColor: online
91
+ ? configurationFields.offline_toast_online_background_color
92
+ : configurationFields.offline_toast_offline_background_color,
93
+ fontFamily: platformSelect({
94
+ ios: configurationFields.offline_toast_message_ios_font_family,
95
+ android: configurationFields.offline_toast_message_android_font_family,
96
+ }),
97
+ fontSize: configurationFields.offline_toast_message_font_size,
98
+ lineHeight: configurationFields.offline_toast_message_line_height,
99
+ letterSpacing: platformSelect({
100
+ ios: configurationFields.offline_toast_message_letter_spacing_ios,
101
+ android:
102
+ configurationFields.offline_toast_message_letter_spacing_android,
103
+ }),
104
+ };
105
+ }
106
+
107
+ return useMemo(
108
+ () => ({ message: title, extraMessage: subtitle, style }),
109
+ [title, subtitle, style]
110
+ );
111
+ }
@@ -1,105 +1,121 @@
1
- import React, { useCallback, useEffect, useRef, useState } from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { NotificationView as NotificationViewNative } from "./NotificationView/NotificationView";
4
- import { NotificationView as NotificationViewSamsung } from "./NotificationView/NotificationView.samsung";
5
- import { NotificationView as NotificationViewLG } from "./NotificationView/NotificationView.lg";
6
-
1
+ import React, { useCallback, useEffect } from "react";
7
2
  import { useConnectionInfo } from "@applicaster/zapp-react-native-utils/reactHooks/connection";
8
3
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
9
- import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
4
+ import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
10
5
 
6
+ import { useOfflineHandlerProps } from "./hooks";
11
7
  import { componentsLogger } from "../../Helpers/logger";
12
8
  import { parseConnectionInfo } from "./utils";
13
9
 
14
10
  const logger = componentsLogger.addSubsystem("OfflineHandler");
15
11
 
12
+ // mirrors NOTIFICATION_TOAST_EVENTS.SHOW_TOAST in
13
+ // @applicaster/quick-brick-core/App/NotificationToastRenderer/NotificationToastManager
14
+ // (kept as a literal here since this package must not depend on quick-brick-core)
15
+ const SHOW_TOAST_EVENT = "showToast";
16
+ const OFFLINE_STATUS_TOAST_ID = "offline-status";
17
+
16
18
  type Props = {
17
19
  children?: React.ReactElement<any>;
18
20
  };
19
21
 
20
- const styles = StyleSheet.create({
21
- container: {
22
- flex: 1,
23
- position: "relative",
24
- },
25
- });
26
-
27
22
  const timers = {
28
23
  goingOffline: 5000,
29
24
  goingOnline: 2000,
30
25
  };
31
26
 
32
- export const NotificationView = platformSelect({
33
- samsung_tv: NotificationViewSamsung,
34
- lg_tv: NotificationViewLG,
35
- default: NotificationViewNative,
36
- });
37
-
38
27
  export const OfflineHandler = ({ children }: Props) => {
39
28
  const connectionInfo = useConnectionInfo(false);
40
- const { online, deviceOnline } = parseConnectionInfo(connectionInfo);
29
+ const { online } = parseConnectionInfo(connectionInfo);
30
+ const prevOnline = usePrevious(online);
41
31
 
42
- const [hidden, setHidden] = useState<boolean>(true);
43
- const previousOnline: boolean = usePrevious(online);
44
- const timer = useRef<NodeJS.Timeout | null>(null);
45
- const initialRender = useRef(false);
32
+ const stillOfflineAfterResumeCount =
33
+ connectionInfo?.stillOfflineAfterResumeCount ?? 0;
46
34
 
47
- const dismiss = useCallback(() => {
48
- setHidden(true);
49
-
50
- if (timer.current) {
51
- clearTimeout(timer.current);
52
- timer.current = null;
53
- }
54
- }, []);
55
-
56
- const setTimer = useCallback((cb, duration) => {
57
- if (timer.current) {
58
- clearTimeout(timer.current);
59
- }
35
+ const prevStillOfflineAfterResumeCount = usePrevious(
36
+ stillOfflineAfterResumeCount
37
+ );
60
38
 
61
- timer.current = setTimeout(cb, duration);
62
- }, []);
39
+ const emitShowToast = useSubscriberFor(SHOW_TOAST_EVENT);
40
+
41
+ const { message, extraMessage, style } = useOfflineHandlerProps(online);
42
+
43
+ const showOnlineToast = useCallback(() => {
44
+ logger.info({
45
+ message: "OfflineHandler: device is back online, emit toast",
46
+ data: { online, prevOnline },
47
+ });
48
+
49
+ emitShowToast({
50
+ id: OFFLINE_STATUS_TOAST_ID,
51
+ message,
52
+ extraMessage,
53
+ style,
54
+ timeout: timers.goingOnline,
55
+ });
56
+ }, [emitShowToast, message, extraMessage, style, online, prevOnline]);
57
+
58
+ const showOfflineToast = useCallback(() => {
59
+ logger.info({
60
+ message: "OfflineHandler: device is offline, emit toast",
61
+ data: { online, prevOnline, stillOfflineAfterResumeCount },
62
+ });
63
+
64
+ emitShowToast({
65
+ id: OFFLINE_STATUS_TOAST_ID,
66
+ message,
67
+ extraMessage,
68
+ style,
69
+ timeout: timers.goingOffline,
70
+ });
71
+ }, [
72
+ emitShowToast,
73
+ message,
74
+ extraMessage,
75
+ style,
76
+ online,
77
+ prevOnline,
78
+ stillOfflineAfterResumeCount,
79
+ ]);
63
80
 
64
81
  useEffect(() => {
65
- if (initialRender.current) {
66
- const wentOffline = previousOnline && online === false;
67
- const wentOnline = !previousOnline && online;
68
- const renderedOffline = !online && previousOnline === null;
69
-
70
- if (!deviceOnline || wentOffline || renderedOffline) {
71
- logger.log("Device went offline");
72
- setTimer(() => setHidden(true), timers.goingOffline);
73
- setHidden(false);
82
+ // On mount there is no previous state yet: only surface the offline toast
83
+ // for a device that starts offline. Don't announce "back online" for a
84
+ // device that was online to begin with.
85
+ if (prevOnline === undefined) {
86
+ if (!online) {
87
+ showOfflineToast();
74
88
  }
75
89
 
76
- if (wentOnline) {
77
- logger.log("Device is back online");
78
- setHidden(false);
79
- setTimer(() => setHidden(true), timers.goingOnline);
80
- }
81
- } else {
82
- initialRender.current = true;
90
+ return;
83
91
  }
84
92
 
85
- return () => {
86
- if (timer.current) {
87
- clearTimeout(timer.current);
88
- timer.current = null;
93
+ // On subsequent renders, only emit a toast when the online/offline state changes.
94
+ if (prevOnline !== online) {
95
+ if (!online) {
96
+ showOfflineToast();
97
+ } else {
98
+ showOnlineToast();
89
99
  }
90
- };
91
- }, [connectionInfo]);
92
-
93
- return (
94
- <View style={styles.container}>
95
- <NotificationView
96
- hidden={hidden}
97
- online={online}
98
- previousOnline={previousOnline}
99
- dismiss={dismiss}
100
- >
101
- {children}
102
- </NotificationView>
103
- </View>
104
- );
100
+ }
101
+ }, [online, prevOnline, showOfflineToast, showOnlineToast]);
102
+
103
+ useEffect(() => {
104
+ // NetworkStatusProvider bumps this counter when the app resumed while offline
105
+ // and is still offline after the verification delay: remind the user.
106
+ if (
107
+ prevStillOfflineAfterResumeCount !== undefined &&
108
+ stillOfflineAfterResumeCount > prevStillOfflineAfterResumeCount &&
109
+ !online
110
+ ) {
111
+ showOfflineToast();
112
+ }
113
+ }, [
114
+ stillOfflineAfterResumeCount,
115
+ prevStillOfflineAfterResumeCount,
116
+ online,
117
+ showOfflineToast,
118
+ ]);
119
+
120
+ return children;
105
121
  };
@@ -1,9 +1,8 @@
1
1
  import * as R from "ramda";
2
-
3
2
  import { NetInfoState } from "@react-native-community/netinfo";
4
3
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
5
4
  import { isOnline as isOnlineUtils } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
6
- import { useSafeAreaInsets } from "react-native-safe-area-context";
5
+
7
6
  import { Platform } from "react-native";
8
7
 
9
8
  type ConnectionInfo = NetInfoState & { deviceStatus: NetStatus };
@@ -51,14 +50,3 @@ export const parseConnectionInfo: typeof parseConnectionInfoForDefault =
51
50
  samsung_tv: parseConnectionInfoForSamsung,
52
51
  default: parseConnectionInfoForDefault,
53
52
  });
54
-
55
- export const useNotificationHeight = () => {
56
- const insets = useSafeAreaInsets();
57
-
58
- const navBarHeight = platformSelect({ ios: 44, android: 56 });
59
-
60
- const statusHeight = insets.top;
61
- const notificationHeight = statusHeight + navBarHeight;
62
-
63
- return { statusHeight, notificationHeight };
64
- };
@@ -9,6 +9,7 @@ import {
9
9
  platformSelect,
10
10
  isAndroidTVPlatform,
11
11
  isTvOSPlatform,
12
+ isApplePlatform,
12
13
  } from "@applicaster/zapp-react-native-utils/reactUtils";
13
14
  import {
14
15
  isAudioItem,
@@ -60,6 +61,7 @@ import {
60
61
  PlayerNativeSendCommand,
61
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
62
63
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
+ import { PLAYER_EVENTS } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
63
65
 
64
66
  type Props = {
65
67
  Player: React.ComponentType<any>;
@@ -188,12 +190,10 @@ const renderApplePlayer = ({
188
190
  return null;
189
191
  }
190
192
 
191
- const { title, summary } = item || {};
192
-
193
+ // Title/Subtitle are not resolved here: `ProgramInfo` reads them from the
194
+ // player manager's content channel via `usePlayerContent`.
193
195
  return (
194
196
  <ProgramInfo
195
- title={title}
196
- subtitle={summary}
197
197
  plugin_configuration={pluginConfiguration}
198
198
  videoStyle={videoStyle}
199
199
  />
@@ -282,12 +282,16 @@ const PlayerContainerComponent = (props: Props) => {
282
282
  }
283
283
 
284
284
  log_debug(
285
- `onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}`
285
+ `onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}, outgoing item id: ${item?.id}`
286
286
  );
287
287
 
288
+ playNextOverlayState.hidePlayNext();
289
+
290
+ playerEvent(PLAYER_EVENTS.PlayNextTriggered, { id: item?.id });
291
+
288
292
  setIsLoadingNextVideo(true);
289
293
  playEntry(playNextOverlayState.entry);
290
- }, [playNextOverlayState]);
294
+ }, [playNextOverlayState, item?.id]);
291
295
 
292
296
  const playNextData: Nullable<PlayNextData> = React.useMemo(() => {
293
297
  if (!playNextOverlayState) {
@@ -344,11 +348,15 @@ const PlayerContainerComponent = (props: Props) => {
344
348
  setIsLoadingNextVideo(false);
345
349
 
346
350
  const resumeTime = Number(item?.extensions?.resumeTime);
351
+ const activePlayer = playerManager.getActivePlayer();
347
352
 
348
- // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast
353
+ // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast.
354
+ // Live streams and a missing/zero resume position are skipped: seeking to 0 on
355
+ // a live stream pulls playback off the live edge into a buffer-starvation seek loop.
349
356
  if (
350
- !playerManager.getActivePlayer()?.hasResumePosition() &&
351
- !isNaN(resumeTime)
357
+ !activePlayer?.hasResumePosition() &&
358
+ resumeTime > 0 &&
359
+ !activePlayer?.isLive()
352
360
  ) {
353
361
  player?.seekTo(resumeTime);
354
362
  }
@@ -396,10 +404,17 @@ const PlayerContainerComponent = (props: Props) => {
396
404
  const { eventType } = event;
397
405
 
398
406
  if (!isLanguageOverlayVisible && eventType === "menu") {
407
+ // First Back dismisses Play Next, the next one closes the player
408
+ if (playNextOverlayState) {
409
+ playNextOverlayState.handleUserCancelPlayNext();
410
+
411
+ return;
412
+ }
413
+
399
414
  close();
400
415
  }
401
416
  },
402
- [close]
417
+ [close, playNextOverlayState]
403
418
  );
404
419
 
405
420
  // Effects
@@ -466,6 +481,8 @@ const PlayerContainerComponent = (props: Props) => {
466
481
  if (isModal && mode === VideoModalMode.MAXIMIZED) {
467
482
  if (disableMiniPlayer) {
468
483
  navigator.closeVideoModal();
484
+ } else {
485
+ navigator.minimiseVideoModal();
469
486
  }
470
487
  }
471
488
 
@@ -489,7 +506,7 @@ const PlayerContainerComponent = (props: Props) => {
489
506
  const isAudioContent = isAudioItem(item);
490
507
 
491
508
  useEffect(() => {
492
- if (!isAudioContent) {
509
+ if (!isAudioContent && isApplePlatform()) {
493
510
  sendQuickBrickEvent(
494
511
  QUICK_BRICK_EVENTS.IDLE_TIMER_DISABLED as QuickBrickEvent,
495
512
  {