@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9530606740 → 16.0.0-alpha.9739533780

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.
@@ -1,105 +1,83 @@
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 emitShowToast = useSubscriberFor(SHOW_TOAST_EVENT);
46
33
 
47
- const dismiss = useCallback(() => {
48
- setHidden(true);
34
+ const { message, extraMessage, style } = useOfflineHandlerProps(online);
49
35
 
50
- if (timer.current) {
51
- clearTimeout(timer.current);
52
- timer.current = null;
53
- }
54
- }, []);
36
+ const showOnlineToast = useCallback(() => {
37
+ logger.log("Device is back online, emit toast");
55
38
 
56
- const setTimer = useCallback((cb, duration) => {
57
- if (timer.current) {
58
- clearTimeout(timer.current);
59
- }
39
+ emitShowToast({
40
+ id: OFFLINE_STATUS_TOAST_ID,
41
+ message,
42
+ extraMessage,
43
+ style,
44
+ timeout: timers.goingOnline,
45
+ });
46
+ }, [emitShowToast, message, extraMessage, style]);
47
+
48
+ const showOfflineToast = useCallback(() => {
49
+ logger.log("Device went offline, emit toast");
60
50
 
61
- timer.current = setTimeout(cb, duration);
62
- }, []);
51
+ emitShowToast({
52
+ id: OFFLINE_STATUS_TOAST_ID,
53
+ message,
54
+ extraMessage,
55
+ style,
56
+ timeout: timers.goingOffline,
57
+ });
58
+ }, [emitShowToast, message, extraMessage, style]);
63
59
 
64
60
  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);
61
+ // On mount there is no previous state yet: only surface the offline toast
62
+ // for a device that starts offline. Don't announce "back online" for a
63
+ // device that was online to begin with.
64
+ if (prevOnline === undefined) {
65
+ if (!online) {
66
+ showOfflineToast();
74
67
  }
75
68
 
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;
69
+ return;
83
70
  }
84
71
 
85
- return () => {
86
- if (timer.current) {
87
- clearTimeout(timer.current);
88
- timer.current = null;
72
+ // On subsequent renders, only emit a toast when the online/offline state changes.
73
+ if (prevOnline !== online) {
74
+ if (!online) {
75
+ showOfflineToast();
76
+ } else {
77
+ showOnlineToast();
89
78
  }
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
- );
79
+ }
80
+ }, [online, prevOnline, showOfflineToast, showOnlineToast]);
81
+
82
+ return children;
105
83
  };
@@ -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
- };
@@ -2,7 +2,7 @@ import React from "react";
2
2
 
3
3
  type PreloaderWrapperProps = {
4
4
  showPreloader?: boolean;
5
- children?: React.ReactNode;
5
+ children: React.ReactElement | null;
6
6
  };
7
7
 
8
8
  export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
@@ -8,9 +8,11 @@ import { componentsLogger } from "../../Helpers/logger";
8
8
  const logger = componentsLogger.addSubsystem("ScreenFeedLoader");
9
9
 
10
10
  /** Loads and provides `feedData` and store to */
11
- export const ScreenFeedLoader: React.FC<
12
- React.PropsWithChildren<{ id: string; feedData: any }>
13
- > = ({ id, feedData, children }) => {
11
+ export const ScreenFeedLoader: React.FC<{
12
+ id: string;
13
+ feedData: any;
14
+ children: React.ReactElement | null;
15
+ }> = ({ id, feedData, children }) => {
14
16
  const { source: feedUrl, mapping } = feedData;
15
17
 
16
18
  const { data, loading, error } = useFeedLoader({
@@ -8,7 +8,7 @@ export const ScreenResolverFeedProvider = ({
8
8
  useFeedData,
9
9
  }: {
10
10
  id: string;
11
- children: React.ReactNode;
11
+ children: React.ReactElement | null;
12
12
  useFeedData: (id: string) => Option<ZappDataSource>;
13
13
  }) => {
14
14
  const feedData = useFeedData(id);
@@ -7,7 +7,7 @@ import { useMarginTop } from "./hooks";
7
7
 
8
8
  interface IProps {
9
9
  targetScreenId?: string;
10
- children?: React.ReactNode;
10
+ children: React.ReactElement | null;
11
11
  applyTopCutoff?: boolean;
12
12
  }
13
13
 
@@ -21,7 +21,7 @@ export const TopCutoffOverlay: React.FC<IProps> = ({
21
21
  targetScreenId,
22
22
  children,
23
23
  applyTopCutoff = true,
24
- }: IProps) => {
24
+ }) => {
25
25
  const cutoffHeight = useMarginTop(targetScreenId);
26
26
 
27
27
  const { backgroundColor: screenBackgroundColor } =
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { View } from "react-native";
3
- import { append, compose, identity, isNil, tail, take, update } from "ramda";
3
+ import { append, compose, identity, tail, take, update } from "ramda";
4
4
 
5
5
  import { AnimationManager } from "./AnimationManager";
6
6
  import { Scene } from "./Scene";
@@ -10,6 +10,7 @@ import {
10
10
  shouldSkipAnimationForPreviousWebViewScene,
11
11
  } from "./utils";
12
12
  import { v4 as uuid_v4 } from "uuid";
13
+ import { isScreenActiveForRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive";
13
14
 
14
15
  export const NAV_ACTION_PUSH = "PUSH";
15
16
 
@@ -50,16 +51,16 @@ function scenesForBackTransition(props: SceneProps, { scenes }) {
50
51
  : [prepareScene(props.children), scenes[0]];
51
52
  }
52
53
 
53
- const isModalClosed = (navigator) => {
54
- return !!isNil(navigator?.modalData);
55
- };
56
-
57
54
  type Props = {
58
55
  transitionConfig: any;
59
56
  children: React.ReactNode;
60
57
  navigator: {
61
58
  previousAction: string;
62
59
  currentRoute: string;
60
+ videoModalState?: {
61
+ visible?: boolean;
62
+ mode?: string;
63
+ };
63
64
  screenData: {
64
65
  [key: string]: any;
65
66
  };
@@ -283,8 +284,10 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
283
284
  // so only a single scene should be rendered,
284
285
  // instead of dangerous nulls or duplicates.
285
286
  renderSingleScene({ scenes, to }) {
286
- const { contentStyle } = this.props;
287
- const screenData = this.props.children.props.screenData;
287
+ const { contentStyle, navigator, children } = this.props;
288
+
289
+ const { currentRoute, videoModalState } = navigator;
290
+ const screenData = children.props.screenData;
288
291
  const pathname = scenes[to.index].key;
289
292
 
290
293
  return (
@@ -293,7 +296,11 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
293
296
  <Scene
294
297
  {...{ style: to?.style || {} }}
295
298
  pathname={pathname}
296
- isActive={pathname === this.props.navigator.currentRoute}
299
+ isActive={isScreenActiveForRoute(
300
+ pathname,
301
+ currentRoute,
302
+ videoModalState
303
+ )}
297
304
  contentStyle={contentStyle}
298
305
  key={scenes[to.index].key}
299
306
  screenUniqueId={scenes[to.index].screenUniqueId}
@@ -311,16 +318,19 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
311
318
  // Looks redundant, but when written like this
312
319
  // we do not need to pass or generate keys.
313
320
  renderScenes({ scenes, from, to }) {
314
- const { contentStyle } = this.props;
321
+ const { contentStyle, navigator } = this.props;
322
+
323
+ const { currentRoute, videoModalState } = navigator;
315
324
 
316
325
  const fromScene = scenes[from.index] && (
317
326
  <Scene
318
327
  key={scenes[from.index].key}
319
328
  pathname={scenes[from.index].key}
320
- isActive={
321
- scenes[from.index].key === this.props.navigator.currentRoute &&
322
- isModalClosed(this.props.navigator)
323
- }
329
+ isActive={isScreenActiveForRoute(
330
+ scenes[from.index].key,
331
+ currentRoute,
332
+ videoModalState
333
+ )}
324
334
  style={from.style}
325
335
  pointerEvents="none"
326
336
  overlayStyle={from.overlayStyle}
@@ -336,10 +346,11 @@ export class TransitionerComponent extends React.PureComponent<Props, State> {
336
346
  const toScene = scenes[to.index] && (
337
347
  <Scene
338
348
  pathname={scenes[to.index].key}
339
- isActive={
340
- scenes[to.index].key === this.props.navigator.currentRoute &&
341
- isModalClosed(this.props.navigator)
342
- }
349
+ isActive={isScreenActiveForRoute(
350
+ scenes[to.index].key,
351
+ currentRoute,
352
+ videoModalState
353
+ )}
343
354
  key={scenes[to.index].key}
344
355
  style={to.style}
345
356
  overlayStyle={to.overlayStyle}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "16.0.0-alpha.9530606740",
3
+ "version": "16.0.0-alpha.9739533780",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-alpha.9530606740",
32
- "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.9530606740",
33
- "@applicaster/zapp-react-native-redux": "16.0.0-alpha.9530606740",
34
- "@applicaster/zapp-react-native-utils": "16.0.0-alpha.9530606740",
31
+ "@applicaster/applicaster-types": "16.0.0-alpha.9739533780",
32
+ "@applicaster/zapp-react-native-bridge": "16.0.0-alpha.9739533780",
33
+ "@applicaster/zapp-react-native-redux": "16.0.0-alpha.9739533780",
34
+ "@applicaster/zapp-react-native-utils": "16.0.0-alpha.9739533780",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",
@@ -1,146 +0,0 @@
1
- import React, { useEffect, useState } from "react";
2
- import {
3
- ActivityIndicator,
4
- ScrollView,
5
- StyleSheet,
6
- Text,
7
- View,
8
- } from "react-native";
9
- import {
10
- ContentViewModel,
11
- isPresentSubMenuAction,
12
- MenuItem,
13
- modalOrchestrator,
14
- } from "@applicaster/zapp-react-native-utils/modalState";
15
- import {
16
- AudioPlayerHeader,
17
- VARIANT_HEADER_WITH_ACTIONS,
18
- } from "./Components/Header";
19
- import {
20
- AudioPlayerTrackItem,
21
- mapMenuItemToPlaylistData,
22
- VARIANT_PLAYLIST_ITEM,
23
- } from "./Components/Item";
24
-
25
- const styles = StyleSheet.create({
26
- container: {
27
- flex: 1,
28
- backgroundColor: "#1e1e1e",
29
- borderRadius: 12,
30
- overflow: "hidden",
31
- minHeight: 400,
32
- },
33
- list: {
34
- flex: 1,
35
- },
36
- centred: {
37
- flex: 1,
38
- justifyContent: "center",
39
- alignItems: "center",
40
- padding: 16,
41
- },
42
- errorText: {
43
- color: "#ff453a",
44
- fontSize: 14,
45
- textAlign: "center",
46
- },
47
- });
48
-
49
- type Props = {
50
- items: MenuItem[];
51
- itemsUrl?: string;
52
- title?: string;
53
- summary?: string;
54
- onPress: (item: MenuItem) => void | Promise<void>;
55
- width: number;
56
- maxHeight: number;
57
- dismiss: () => void;
58
- viewModel: ContentViewModel;
59
- };
60
-
61
- export function MultiLevelBottomSheetContent(props: Props) {
62
- const { title, onPress, viewModel } = props;
63
-
64
- const canGoBack = modalOrchestrator.canGoBack;
65
- const [state, setState] = useState(viewModel.getState());
66
-
67
- // Subscribe to the viewModel state changes
68
- useEffect(() => {
69
- return viewModel.subscribe((nextState) => {
70
- setState(nextState);
71
- });
72
- }, [viewModel]);
73
-
74
- /**
75
- * Handles an item press:
76
- * - Delegates to the parent onPress (which runs the action and awaits it).
77
- * - If the item was a leaf action (not a submenu), triggers viewModel.refetch()
78
- * so that checkmarks (isSelected) reflect the updated server state.
79
- */
80
- const handleItemPress = async (item: MenuItem) => {
81
- const isSubMenu =
82
- (item.submenu && item.submenu.length > 0) ||
83
- (item.action && isPresentSubMenuAction(item.action));
84
-
85
- await onPress(item);
86
-
87
- if (!isSubMenu) {
88
- await viewModel.refetch();
89
- }
90
- };
91
-
92
- const showCentralLoading = state.isLoading && state.items.length === 0;
93
-
94
- return (
95
- <View style={styles.container}>
96
- {/* Refactored Header */}
97
- <AudioPlayerHeader
98
- configuration={VARIANT_HEADER_WITH_ACTIONS}
99
- data={{ title: title || "" }}
100
- onCancel={
101
- canGoBack
102
- ? () => modalOrchestrator.back()
103
- : () => modalOrchestrator.close()
104
- }
105
- onDone={() => modalOrchestrator.close()}
106
- />
107
-
108
- {/* Content */}
109
- {showCentralLoading ? (
110
- <View style={styles.centred}>
111
- <ActivityIndicator color="#aaaaaa" />
112
- </View>
113
- ) : state.error ? (
114
- <View style={styles.centred}>
115
- <Text style={styles.errorText}>
116
- {typeof state.error === "string"
117
- ? state.error
118
- : state.error.message}
119
- </Text>
120
- </View>
121
- ) : (
122
- <ScrollView style={styles.list}>
123
- {state.items.map((item, idx) => {
124
- return (
125
- <AudioPlayerTrackItem
126
- key={idx}
127
- configuration={VARIANT_PLAYLIST_ITEM}
128
- data={mapMenuItemToPlaylistData(item)}
129
- onPress={() => handleItemPress(item)}
130
- onTrailingButtonPress={
131
- item.secondaryAction?.action
132
- ? () =>
133
- onPress({
134
- title: item.title,
135
- action: item.secondaryAction.action,
136
- })
137
- : undefined
138
- }
139
- />
140
- );
141
- })}
142
- </ScrollView>
143
- )}
144
- </View>
145
- );
146
- }
@@ -1,112 +0,0 @@
1
- import * as React from "react";
2
- import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
- import { useNetworkStatusLocalizations } from "./utils";
4
-
5
- type Props = {
6
- children?: React.ReactNode;
7
- hidden?: boolean;
8
- online?: boolean;
9
- previousOnline?: boolean;
10
- dismiss: () => void;
11
- };
12
-
13
- const NOTIF_DURATION = 4500;
14
-
15
- const styles: Record<any, React.CSSProperties> = {
16
- body: {
17
- position: "absolute",
18
- bottom: 0,
19
- width: 1920,
20
- height: 200,
21
- background:
22
- "linear-gradient(180deg, rgba(17,17,17,0.9) 0%, rgba(17,17,17,1) 49%, rgba(0,0,0,0.85) 100%)",
23
- display: "flex",
24
- alignItems: "center",
25
- justifyContent: "center",
26
- flexDirection: "column",
27
- color: "#fff",
28
- paddingBottom: 15,
29
- },
30
- title: {
31
- fontSize: 28,
32
- margin: 0,
33
- padding: 0,
34
- },
35
- subtitle: {
36
- fontSize: 20,
37
- margin: 0,
38
- padding: 0,
39
- },
40
- };
41
-
42
- export const NotificationView = (props: Props) => {
43
- const { children, hidden, dismiss, previousOnline, online } = props;
44
-
45
- const theme = useTheme<BaseThemePropertiesTV>();
46
- const storedLocalizations = useNetworkStatusLocalizations();
47
-
48
- const [open, setOpen] = React.useState<boolean | null>(null);
49
-
50
- const timeout = React.useRef<NodeJS.Timeout>();
51
- const wentOnline = !previousOnline && props.online;
52
-
53
- const onClose = () => {
54
- setOpen(false);
55
- dismiss();
56
- };
57
-
58
- const closeNotification = () => {
59
- clearTimeout(timeout.current as NodeJS.Timeout);
60
-
61
- timeout.current = setTimeout(() => {
62
- setOpen(false);
63
- }, NOTIF_DURATION);
64
- };
65
-
66
- const openNotification = () => {
67
- if (!open && !hidden) {
68
- setOpen(true);
69
- }
70
- };
71
-
72
- const handleNotificationChange = () => {
73
- if (open !== null) {
74
- openNotification();
75
- }
76
-
77
- closeNotification();
78
- };
79
-
80
- React.useEffect(() => {
81
- handleNotificationChange();
82
- }, [hidden, online]);
83
-
84
- const showOnlineMsg = wentOnline || online;
85
-
86
- const MSG = showOnlineMsg
87
- ? (theme?.online_notification_title ??
88
- storedLocalizations?.online_notification_title)
89
- : (theme?.offline_notification_title ??
90
- storedLocalizations?.offline_notification_title);
91
-
92
- const EXTRA_MSG = showOnlineMsg
93
- ? (theme?.online_notification_subtitle ??
94
- storedLocalizations?.online_notification_subtitle)
95
- : (theme?.offline_notification_subtitle ??
96
- storedLocalizations?.offline_notification_subtitle);
97
-
98
- return (
99
- <div onClick={onClose}>
100
- {children}
101
- {open ? (
102
- <div style={styles.body}>
103
- <div style={styles.title}>
104
- <h2>{MSG}</h2>
105
- </div>
106
-
107
- <h4 style={styles.subtitle}>{EXTRA_MSG}</h4>
108
- </div>
109
- ) : null}
110
- </div>
111
- );
112
- };