@applicaster/zapp-react-native-ui-components 13.0.0-alpha.7122212380 → 13.0.0-alpha.7196055925

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.
@@ -35,6 +35,7 @@ function retrieveData(entry, func, args) {
35
35
  * @param {?[Object]} element.elements Optional array of nested elements to render within the current node
36
36
  * @returns {Object} inflated configuration, ready to be rendered by the master cell's element mapper
37
37
  */
38
+
38
39
  export function configInflater(
39
40
  entry: any,
40
41
  {
@@ -64,7 +65,7 @@ export function configInflater(
64
65
 
65
66
  if (Array.isArray(elements)) {
66
67
  adjustedElements = elements.map((element) =>
67
- configInflater(entry, element)
68
+ configInflater(entry, element as any)
68
69
  );
69
70
  }
70
71
 
@@ -76,14 +77,6 @@ export function configInflater(
76
77
  };
77
78
  }
78
79
 
79
- /**
80
- * Memoized version of configInflater to cache results based on entry and masterCellConfig
81
- */
82
- const memoizedConfigInflater = R.memoizeWith(
83
- (entry, config) => `${entry.id}-${JSON.stringify(config)}`,
84
- configInflater
85
- );
86
-
87
80
  /**
88
81
  * Return the proper view tree according to entry's content type and UI state.
89
82
  * Falls back to "default" content type or state.
@@ -104,7 +97,7 @@ function resolveElementsNode(entry, state, elements) {
104
97
  export function defaultDataAdapter(elements) {
105
98
  return function elementsBuilder({ entry, state = "default" }) {
106
99
  return resolveElementsNode(entry, state, elements).map((element) =>
107
- memoizedConfigInflater(entry, element)
100
+ configInflater(entry, element)
108
101
  );
109
102
  };
110
103
  }
@@ -78,17 +78,13 @@ export function resolveColor(entry, style) {
78
78
  return style;
79
79
  }
80
80
 
81
- // Iterate through style object keys and resolve any color properties
82
- // non-color properties are passed through unchanged
83
- const resolvedStyle = Object.keys(style).reduce((acc, key) => {
84
- if (key.includes("color") || key.includes("Color")) {
85
- return { ...acc, ...resolveColorForProp(entry, acc, key) };
86
- }
81
+ // TODO can be optimized to remove 3 O(n) loops
82
+ const styleKeys = Object.keys(style);
83
+ const colorKeys = styleKeys.filter((key) => /color/i.test(key));
87
84
 
88
- return acc;
85
+ return colorKeys.reduce((acc, value) => {
86
+ return { ...style, ...resolveColorForProp(entry, acc, value) };
89
87
  }, style);
90
-
91
- return resolvedStyle;
92
88
  }
93
89
 
94
90
  export function isVideoPreviewEnabled({
@@ -343,23 +339,13 @@ export const useCellState = ({
343
339
  return getCellState({ focused, selected: _isSelected });
344
340
  };
345
341
 
346
- const cache = new Map<string, any>();
347
-
348
342
  export const hasFocusableInsideBuilder = (elementsBuilder) => (item) => {
349
343
  const elements = elementsBuilder({ entry: item });
350
344
 
351
- if (cache.has(item.id)) {
352
- return cache.get(item.id);
353
- }
354
-
355
- const result = R.anyPass([
345
+ return R.anyPass([
356
346
  hasElementsSpecificViewType("ButtonContainerView"),
357
347
  hasElementsSpecificViewType("FocusableView"),
358
348
  ])(elements);
359
-
360
- cache.set(item.id, result);
361
-
362
- return result;
363
349
  };
364
350
 
365
351
  export function getEntryState(state, selected) {
@@ -1,5 +1,5 @@
1
1
  import React, { useMemo } from "react";
2
- import { View, StyleSheet, LayoutChangeEvent } from "react-native";
2
+ import { LayoutChangeEvent, StyleSheet, View } from "react-native";
3
3
  import { Title, TitleProps } from "./Title";
4
4
  import { CloseButton, CloseButtonProps } from "./CloseButton";
5
5
  import { getCloseButtonProps, getTitleProps } from "./utils";
@@ -60,8 +60,8 @@ export function ModalHeader(props: Props) {
60
60
  width: width - buttonsContainerWidth,
61
61
  }}
62
62
  >
63
- {title && <Title {...titleProps} />}
64
- {summary && <Title {...summaryProps} />}
63
+ {title ? <Title {...titleProps} /> : null}
64
+ {summary ? <Title {...summaryProps} /> : null}
65
65
  </View>
66
66
  <View style={{ width: buttonsContainerWidth }}>
67
67
  <CloseButton {...closeButtonProps} />
@@ -70,21 +70,6 @@ function ComponentsMapComponent(props: Props) {
70
70
 
71
71
  const [flatListHeight, setFlatListHeight] = React.useState(null);
72
72
 
73
- const {
74
- isAnyLoading,
75
- isAllLoaded,
76
- waitForAllComponents,
77
- onLoadFinished,
78
- onLoadFailed,
79
- shouldShowLoadingError,
80
- isAnyLoaded,
81
- arePreviousComponentsLoaded,
82
- } = useLoadingState(feed?.entry?.length || components.length);
83
-
84
- const theme = useTheme();
85
-
86
- const logTimestamp = useProfilerLogging();
87
-
88
73
  const riverComponents = React.useMemo(() => {
89
74
  if (feed?.entry?.length < components.length) {
90
75
  return R.slice(0, feed?.entry?.length, components);
@@ -93,55 +78,42 @@ function ComponentsMapComponent(props: Props) {
93
78
  return components;
94
79
  }, [components, feed]);
95
80
 
81
+ const logTimestamp = useProfilerLogging();
82
+
83
+ const onLoadDone = React.useCallback(() => {
84
+ logTimestamp(riverId?.toString());
85
+ }, [logTimestamp]);
86
+
87
+ const { loadingState, onLoadFinished, onLoadFailed, shouldShowLoadingError } =
88
+ useLoadingState(riverComponents.length, onLoadDone);
89
+
90
+ const theme = useTheme();
91
+
96
92
  const renderRiverItem = React.useCallback(
97
93
  ({ item, index }) => {
98
- const readyToBeDisplayed = arePreviousComponentsLoaded(index);
99
-
100
- const styles = {
101
- display: !readyToBeDisplayed ? "none" : ("flex" as "none" | "flex"),
94
+ const riverItemProps = {
95
+ riverId,
96
+ item,
97
+ index,
98
+ isScreenWrappedInContainer,
99
+ feed,
100
+ groupId,
101
+ onLoadFailed,
102
+ onLoadFinished,
103
+ getStaticComponentFeed,
104
+ isLast: isLast(index, riverComponents.length),
105
+ loadingState,
102
106
  };
103
107
 
104
108
  return (
105
- <View style={styles}>
106
- <ScreenLoadingMeasurementsListItemWrapper index={index}>
107
- <RiverItem
108
- {...{
109
- readyToBeDisplayed,
110
- riverId,
111
- item,
112
- index,
113
- isScreenWrappedInContainer,
114
- feed,
115
- groupId,
116
- onLoadFailed,
117
- onLoadFinished,
118
- getStaticComponentFeed,
119
- isLast: isLast(index, riverComponents.length),
120
- }}
121
- />
122
- </ScreenLoadingMeasurementsListItemWrapper>
123
- </View>
109
+ <ScreenLoadingMeasurementsListItemWrapper index={index}>
110
+ <RiverItem {...riverItemProps} />
111
+ </ScreenLoadingMeasurementsListItemWrapper>
124
112
  );
125
113
  },
126
- [
127
- feed,
128
- getStaticComponentFeed,
129
- arePreviousComponentsLoaded,
130
- onLoadFailed,
131
- onLoadFinished,
132
- ]
114
+ [feed, getStaticComponentFeed, onLoadFailed, onLoadFinished]
133
115
  );
134
116
 
135
- const renderFooter = React.useCallback(() => {
136
- return (
137
- <RiverFooter
138
- visible={isAnyLoading}
139
- flatListHeight={flatListHeight}
140
- isAnyLoaded={isAnyLoaded || waitForAllComponents}
141
- />
142
- );
143
- }, [flatListHeight, isAnyLoading, isAnyLoaded]);
144
-
145
117
  const screenStyle = React.useMemo(
146
118
  () => ({
147
119
  paddingTop: ifEmptyUseFallback(
@@ -179,12 +151,6 @@ function ComponentsMapComponent(props: Props) {
179
151
 
180
152
  usePipesCacheReset(riverId, riverComponents);
181
153
 
182
- React.useEffect(() => {
183
- if (isAllLoaded) {
184
- logTimestamp(riverId?.toString());
185
- }
186
- }, [isAllLoaded]);
187
-
188
154
  const refreshControl = React.useMemo(
189
155
  () =>
190
156
  pullToRefreshEnabled ? (
@@ -280,6 +246,11 @@ function ComponentsMapComponent(props: Props) {
280
246
  }
281
247
  }, []);
282
248
 
249
+ const contentContainerStyle = React.useMemo(
250
+ () => (isScreenWrappedInContainer ? {} : screenStyle),
251
+ [isScreenWrappedInContainer, screenStyle]
252
+ );
253
+
283
254
  if (shouldShowLoadingError) {
284
255
  return <RiverError />;
285
256
  }
@@ -291,7 +262,7 @@ function ComponentsMapComponent(props: Props) {
291
262
  <View style={styles.container}>
292
263
  <ScreenLoadingMeasurements
293
264
  riverId={riverId}
294
- numberOfComponents={feed?.entry?.length || components.length}
265
+ numberOfComponents={riverComponents.length}
295
266
  >
296
267
  <ViewportTracker>
297
268
  <FlatList
@@ -312,10 +283,13 @@ function ComponentsMapComponent(props: Props) {
312
283
  keyExtractor={keyExtractor}
313
284
  renderItem={renderRiverItem}
314
285
  data={riverComponents}
315
- contentContainerStyle={
316
- isScreenWrappedInContainer ? {} : screenStyle
286
+ contentContainerStyle={contentContainerStyle}
287
+ ListFooterComponent={
288
+ <RiverFooter
289
+ flatListHeight={flatListHeight}
290
+ loadingState={loadingState}
291
+ />
317
292
  }
318
- ListFooterComponent={renderFooter}
319
293
  refreshControl={refreshControl}
320
294
  onScrollBeginDrag={onScrollBeginDrag}
321
295
  onScroll={onScroll}
@@ -1,50 +1,86 @@
1
1
  import * as React from "react";
2
- import * as R from "ramda";
3
-
4
- const allTrue = R.all(R.equals(true));
5
- const anyFalse = R.any(R.equals(false));
6
- const anyTrue = R.any(R.equals(true));
2
+ import { isNil, set, lensIndex, T, slice } from "ramda";
3
+ import { BehaviorSubject } from "rxjs";
4
+ import { useRefWithInitialValue } from "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue";
7
5
 
8
6
  const reducer = (state, { payload }) => {
9
- if (!R.isNil(payload) && !state[payload]) {
10
- return R.set(R.lensIndex(payload), true)(state);
7
+ if (!isNil(payload) && !state[payload]) {
8
+ return set(lensIndex(payload), true)(state);
11
9
  }
12
10
 
13
11
  return state;
14
12
  };
15
13
 
16
- type Return = {
17
- isAnyLoading: boolean;
18
- isAllLoaded: boolean;
14
+ type LoadingState = {
15
+ index: number;
16
+ done: boolean;
19
17
  waitForAllComponents: boolean;
18
+ };
19
+
20
+ type Return = {
21
+ loadingState: BehaviorSubject<LoadingState>;
20
22
  onLoadFinished: (index: number) => void;
21
23
  onLoadFailed: ({ error, index }: { error: Error; index: number }) => void;
22
24
  shouldShowLoadingError: boolean;
23
- isAnyLoaded: boolean;
24
25
  arePreviousComponentsLoaded: (index: number) => boolean;
25
26
  };
26
27
 
27
- type Action = { payload: { index: number } };
28
-
29
- type Loaded = true;
30
- type Loading = false;
31
-
32
- type LoadingState = Array<Loaded | Loading>;
33
-
34
28
  // TODO: Take this value from Zapp configuration, when feature is added to GeneralScreen
35
29
  const SHOULD_FAIL_ON_COMPONENT_LOADING = false;
36
30
 
37
- export const useLoadingState = (count: number): Return => {
31
+ const createLoadingStateObservable = () =>
32
+ new BehaviorSubject<LoadingState>({
33
+ index: -1,
34
+ done: false,
35
+ waitForAllComponents: SHOULD_FAIL_ON_COMPONENT_LOADING,
36
+ });
37
+
38
+ export const useLoadingState = (
39
+ count: number,
40
+ onLoadDone: () => void
41
+ ): Return => {
42
+ const componentStateRef = React.useRef(new Array(count).fill(false));
38
43
  const [loadingError, setLoadingError] = React.useState(null);
39
44
 
40
- const [componentsState, dispatch] = React.useReducer<
41
- React.Reducer<LoadingState, Action>
42
- >(reducer, new Array(count).fill(false));
45
+ const loadingState = useRefWithInitialValue<BehaviorSubject<LoadingState>>(
46
+ createLoadingStateObservable
47
+ );
48
+
49
+ const arePreviousComponentsLoaded = React.useCallback((index) => {
50
+ if (index === 0) {
51
+ return true;
52
+ }
43
53
 
44
- const handleComponentLoaded = React.useCallback((index) => {
45
- dispatch({ payload: index });
54
+ const componentsBefore = slice(0, index, componentStateRef.current);
55
+
56
+ return componentsBefore.every(T);
46
57
  }, []);
47
58
 
59
+ const dispatch = React.useCallback(({ payload }) => {
60
+ const newState = reducer(componentStateRef.current, { payload });
61
+ componentStateRef.current = newState;
62
+ const isDone = arePreviousComponentsLoaded(count - 1);
63
+
64
+ const state = loadingState.current.getValue();
65
+
66
+ const newLoadingState = {
67
+ ...state,
68
+ index: state.index < payload ? payload : state.index,
69
+ done: isDone,
70
+ };
71
+
72
+ loadingState.current.next(newLoadingState);
73
+
74
+ if (isDone) {
75
+ onLoadDone();
76
+ }
77
+ }, []);
78
+
79
+ const handleComponentLoaded = React.useCallback(
80
+ (index) => dispatch({ payload: index }),
81
+ []
82
+ );
83
+
48
84
  const handleComponentLoadErrorWhenNeedToFail = React.useCallback(
49
85
  ({ error }) => {
50
86
  if (error !== loadingError) {
@@ -55,35 +91,26 @@ export const useLoadingState = (count: number): Return => {
55
91
  );
56
92
 
57
93
  const handleComponentLoadErrorWhenNoNeedToFail = React.useCallback(
58
- ({ index }) => {
59
- handleComponentLoaded(index);
60
- },
94
+ ({ index }) => handleComponentLoaded(index),
61
95
  []
62
96
  );
63
97
 
64
- const arePreviousComponentsLoaded = React.useCallback(
65
- (index) => {
66
- if (index === 0) {
67
- return true;
68
- }
69
-
70
- const componentsBefore = R.slice(0, index, componentsState);
71
-
72
- return allTrue(componentsBefore);
73
- },
74
- [componentsState]
98
+ return React.useMemo(
99
+ () => ({
100
+ loadingState: loadingState.current,
101
+ onLoadFinished: handleComponentLoaded,
102
+ onLoadFailed: SHOULD_FAIL_ON_COMPONENT_LOADING
103
+ ? handleComponentLoadErrorWhenNeedToFail
104
+ : handleComponentLoadErrorWhenNoNeedToFail,
105
+ shouldShowLoadingError: SHOULD_FAIL_ON_COMPONENT_LOADING && loadingError,
106
+ arePreviousComponentsLoaded,
107
+ }),
108
+ [
109
+ loadingError,
110
+ handleComponentLoaded,
111
+ handleComponentLoadErrorWhenNeedToFail,
112
+ handleComponentLoadErrorWhenNoNeedToFail,
113
+ arePreviousComponentsLoaded,
114
+ ]
75
115
  );
76
-
77
- return {
78
- isAnyLoading: anyFalse(componentsState),
79
- isAllLoaded: allTrue(componentsState),
80
- onLoadFinished: handleComponentLoaded,
81
- onLoadFailed: SHOULD_FAIL_ON_COMPONENT_LOADING
82
- ? handleComponentLoadErrorWhenNeedToFail
83
- : handleComponentLoadErrorWhenNoNeedToFail,
84
- shouldShowLoadingError: SHOULD_FAIL_ON_COMPONENT_LOADING && loadingError,
85
- isAnyLoaded: anyTrue(componentsState),
86
- waitForAllComponents: SHOULD_FAIL_ON_COMPONENT_LOADING,
87
- arePreviousComponentsLoaded,
88
- };
89
116
  };
@@ -1,11 +1,23 @@
1
- import React from "react";
1
+ import React, { useCallback } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
3
  import { Spinner } from "@applicaster/zapp-react-native-ui-components/Components/Spinner";
4
+ import type { Subject } from "rxjs";
5
+ import { useStateFromSubscribe } from "@applicaster/zapp-react-native-utils/reactHooks/state/useStateFromSubscribe";
6
+
7
+ type LoadingState = {
8
+ waitForAllComponents: boolean;
9
+ index: number;
10
+ done: boolean;
11
+ };
12
+
13
+ type State = {
14
+ visible: boolean;
15
+ isAnyLoaded: boolean;
16
+ };
4
17
 
5
18
  type Props = {
6
- visible?: boolean;
7
19
  flatListHeight?: number;
8
- isAnyLoaded: boolean;
20
+ loadingState: Subject<LoadingState>;
9
21
  };
10
22
 
11
23
  const FOOTER_COMPONENT_HEIGHT = 200;
@@ -19,17 +31,35 @@ const footerStyles = StyleSheet.create({
19
31
  });
20
32
 
21
33
  function RiverFooterComponent(props: Props) {
22
- const { visible = true, flatListHeight, isAnyLoaded } = props;
34
+ const { flatListHeight, loadingState } = props;
35
+
36
+ const { visible, isAnyLoaded } = useStateFromSubscribe<LoadingState, State>(
37
+ loadingState,
38
+ useCallback(
39
+ ({ index, done, waitForAllComponents }, setState) =>
40
+ setState(() => ({
41
+ visible: !done,
42
+ isAnyLoaded: index >= 0 || waitForAllComponents,
43
+ })),
44
+ []
45
+ ),
46
+ {
47
+ visible: true,
48
+ isAnyLoaded: false,
49
+ }
50
+ );
23
51
 
24
52
  if (!visible) return null;
25
53
 
26
54
  return (
27
55
  <View
28
- renderToHardwareTextureAndroid={true}
29
- style={{
30
- ...footerStyles.container,
31
- height: isAnyLoaded ? FOOTER_COMPONENT_HEIGHT : flatListHeight,
32
- }}
56
+ renderToHardwareTextureAndroid
57
+ style={[
58
+ footerStyles.container,
59
+ {
60
+ height: isAnyLoaded ? FOOTER_COMPONENT_HEIGHT : flatListHeight,
61
+ },
62
+ ]}
33
63
  >
34
64
  <Spinner size={isAnyLoaded ? "small" : "large"} />
35
65
  </View>
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useEffect } from "react";
2
2
  import * as R from "ramda";
3
3
 
4
4
  import { applyDecorators } from "../../Decorators";
@@ -11,6 +11,7 @@ import {
11
11
  import { riverLogger } from "./logger";
12
12
  import { tvPluginsWithCellRenderer } from "../../const";
13
13
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
14
+ import type { BehaviorSubject } from "rxjs";
14
15
 
15
16
  export type RiverItemType = {
16
17
  item: ZappUIComponent;
@@ -24,6 +25,7 @@ export type RiverItemType = {
24
25
  getStaticComponentFeed: GeneralContentScreenProps["getStaticComponentFeed"];
25
26
  readyToBeDisplayed?: boolean;
26
27
  isLast: boolean;
28
+ loadingState: BehaviorSubject<{ index: number }>;
27
29
  };
28
30
 
29
31
  function getFeedUrl(feed: ZappFeed, index: number) {
@@ -36,6 +38,33 @@ function getFeedUrl(feed: ZappFeed, index: number) {
36
38
  }
37
39
  }
38
40
 
41
+ /**
42
+ * useLoadingState for RiverItemComponent
43
+ * takes currentIndex and loadingState as arguments
44
+ **/
45
+ const useLoadingState = (
46
+ currentIndex: number,
47
+ loadingState: RiverItemType["loadingState"]
48
+ ) => {
49
+ const [readyToBeDisplayed, setReadyToBeDisplayed] = React.useState(
50
+ loadingState.getValue().index + 1 === currentIndex
51
+ );
52
+
53
+ useEffect(() => {
54
+ const subscription = loadingState.subscribe(({ index }) => {
55
+ if (index + 1 === currentIndex) {
56
+ setReadyToBeDisplayed(true);
57
+ }
58
+ });
59
+
60
+ return () => {
61
+ subscription.unsubscribe();
62
+ };
63
+ }, [loadingState, currentIndex]);
64
+
65
+ return readyToBeDisplayed;
66
+ };
67
+
39
68
  function RiverItemComponent(props: RiverItemType) {
40
69
  const {
41
70
  item,
@@ -47,10 +76,12 @@ function RiverItemComponent(props: RiverItemType) {
47
76
  onLoadFinished,
48
77
  onLoadFailed,
49
78
  getStaticComponentFeed,
50
- readyToBeDisplayed,
51
79
  isLast,
80
+ loadingState,
52
81
  } = props;
53
82
 
83
+ const readyToBeDisplayed = useLoadingState(index, loadingState);
84
+
54
85
  const feedUrl = getFeedUrl(feed, index);
55
86
 
56
87
  const Component = useComponentResolver(
@@ -90,6 +121,10 @@ function RiverItemComponent(props: RiverItemType) {
90
121
  }
91
122
  }, []);
92
123
 
124
+ if (!readyToBeDisplayed) {
125
+ return null;
126
+ }
127
+
93
128
  if (Component === null || typeof Component === "undefined") {
94
129
  riverLogger.warning({
95
130
  message: `Component ${item.component_type} is null - skipping rendering`,