@applicaster/zapp-react-native-ui-components 13.0.0-alpha.7846284820 → 13.0.0-alpha.7854932241

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.
@@ -0,0 +1,126 @@
1
+ /** TODO: Remove this file when tvos FocusableGroup
2
+ * behaviour is aligned to the web one
3
+ * FocusableGroup should only send onFocus and onBlur events when
4
+ * Focus enters and leaves the Focusables inside the branch
5
+ */
6
+ import * as React from "react";
7
+
8
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
9
+ import * as FOCUS_EVENTS from "@applicaster/zapp-react-native-utils/appUtils/focusManager/events";
10
+ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
11
+ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
12
+
13
+ import { isAppleTV } from "../../Helpers/Platform";
14
+ import { useCellState } from "../MasterCell/utils";
15
+
16
+ const useCellFocusedState = (
17
+ skipFocusManagerRegistration: boolean,
18
+ groupId: string,
19
+ id: string
20
+ ) => {
21
+ const [currentCellFocused, setCurrentCellFocused] = React.useState(false);
22
+
23
+ React.useEffect(() => {
24
+ const isGroupItemFocused = () => {
25
+ if (!skipFocusManagerRegistration) {
26
+ const isFocused = focusManager.isGroupItemFocused(groupId, id);
27
+ setCurrentCellFocused(isFocused);
28
+ }
29
+ };
30
+
31
+ const handler = () => {
32
+ // tvOS hack for properly checking focus
33
+ if (isAppleTV()) {
34
+ setTimeout(() => {
35
+ isGroupItemFocused();
36
+ }, 0);
37
+ } else {
38
+ isGroupItemFocused();
39
+ }
40
+ };
41
+
42
+ focusManager.on(FOCUS_EVENTS.FOCUS, handler);
43
+
44
+ return () => {
45
+ focusManager.removeHandler(FOCUS_EVENTS.FOCUS, handler);
46
+ };
47
+ }, [groupId, skipFocusManagerRegistration]);
48
+
49
+ return currentCellFocused;
50
+ };
51
+
52
+ type Props = {
53
+ item: ZappEntry;
54
+ CellRenderer: React.FunctionComponent<any>;
55
+ id: string;
56
+ groupId: string;
57
+ onFocus: Function;
58
+ index: number;
59
+ scrollTo: Function;
60
+ preferredFocus?: boolean;
61
+ skipFocusManagerRegistration?: boolean;
62
+ isFocusable?: boolean;
63
+ behavior: Behavior;
64
+ focused?: boolean;
65
+ };
66
+
67
+ export function CellWithFocusable(props: Props) {
68
+ const {
69
+ index,
70
+ item,
71
+ CellRenderer,
72
+ id,
73
+ groupId,
74
+ onFocus,
75
+ scrollTo = noop,
76
+ preferredFocus,
77
+ skipFocusManagerRegistration,
78
+ isFocusable,
79
+ behavior,
80
+ focused,
81
+ } = props;
82
+
83
+ const isFocused = useCellFocusedState(
84
+ skipFocusManagerRegistration,
85
+ groupId,
86
+ id
87
+ );
88
+
89
+ const state = useCellState({
90
+ id: item.id,
91
+ behavior,
92
+ focused: isFocused || toBooleanWithDefaultFalse(focused),
93
+ });
94
+
95
+ const [focusedButtonId, setFocusedButtonId] = React.useState(undefined);
96
+
97
+ // for horizontal scrolling
98
+ React.useEffect(() => {
99
+ if (focusedButtonId) {
100
+ scrollTo(index);
101
+ }
102
+ }, [focusedButtonId]);
103
+
104
+ const handleToggleFocus = (value) => {
105
+ setFocusedButtonId(value.focusedButtonId);
106
+
107
+ if (value.focusable) {
108
+ onFocus(value.focusable, value.mouse);
109
+ }
110
+ };
111
+
112
+ return (
113
+ <CellRenderer
114
+ item={item}
115
+ groupId={groupId}
116
+ onToggleFocus={handleToggleFocus}
117
+ state={state}
118
+ prefixId={id}
119
+ focusedButtonId={focusedButtonId}
120
+ preferredFocus={preferredFocus}
121
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
122
+ isFocusable={isFocusable}
123
+ focused={focused}
124
+ />
125
+ );
126
+ }
@@ -1,48 +1,10 @@
1
1
  import * as React from "react";
2
2
 
3
- import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
4
- import * as FOCUS_EVENTS from "@applicaster/zapp-react-native-utils/appUtils/focusManager/events";
5
3
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
6
4
  import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
7
5
 
8
- import { isAppleTV } from "../../Helpers/Platform";
9
6
  import { useCellState } from "../MasterCell/utils";
10
-
11
- const useCellFocusedState = (
12
- skipFocusManagerRegistration: boolean,
13
- groupId: string,
14
- id: string
15
- ) => {
16
- const [currentCellFocused, setCurrentCellFocused] = React.useState(false);
17
-
18
- React.useEffect(() => {
19
- const isGroupItemFocused = () => {
20
- if (!skipFocusManagerRegistration) {
21
- const isFocused = focusManager.isGroupItemFocused(groupId, id);
22
- setCurrentCellFocused(isFocused);
23
- }
24
- };
25
-
26
- const handler = () => {
27
- // tvOS hack for properly checking focus
28
- if (isAppleTV()) {
29
- setTimeout(() => {
30
- isGroupItemFocused();
31
- }, 0);
32
- } else {
33
- isGroupItemFocused();
34
- }
35
- };
36
-
37
- focusManager.on(FOCUS_EVENTS.FOCUS, handler);
38
-
39
- return () => {
40
- focusManager.removeHandler(FOCUS_EVENTS.FOCUS, handler);
41
- };
42
- }, [groupId, skipFocusManagerRegistration]);
43
-
44
- return currentCellFocused;
45
- };
7
+ import { FocusableGroup } from "../FocusableGroup";
46
8
 
47
9
  type Props = {
48
10
  item: ZappEntry;
@@ -75,11 +37,7 @@ export function CellWithFocusable(props: Props) {
75
37
  focused,
76
38
  } = props;
77
39
 
78
- const isFocused = useCellFocusedState(
79
- skipFocusManagerRegistration,
80
- groupId,
81
- id
82
- );
40
+ const [isFocused, setIsFocused] = React.useState(false);
83
41
 
84
42
  const state = useCellState({
85
43
  id: item.id,
@@ -96,26 +54,52 @@ export function CellWithFocusable(props: Props) {
96
54
  }
97
55
  }, [focusedButtonId]);
98
56
 
99
- const handleToggleFocus = (value) => {
100
- setFocusedButtonId(value.focusedButtonId);
57
+ const handleToggleFocus = React.useCallback(
58
+ (value) => {
59
+ setFocusedButtonId(value.focusedButtonId);
60
+
61
+ if (value.focusable) {
62
+ onFocus(value.focusable, value.mouse);
63
+ }
64
+ },
65
+ [onFocus]
66
+ );
67
+
68
+ const onGroupFocus = React.useCallback(() => {
69
+ if (!skipFocusManagerRegistration) {
70
+ setIsFocused(true);
71
+ }
72
+ }, [skipFocusManagerRegistration]);
101
73
 
102
- if (value.focusable) {
103
- onFocus(value.focusable, value.mouse);
74
+ const onGroupBlur = React.useCallback(() => {
75
+ if (!skipFocusManagerRegistration) {
76
+ setIsFocused(false);
104
77
  }
105
- };
78
+ }, [skipFocusManagerRegistration]);
106
79
 
107
80
  return (
108
- <CellRenderer
109
- item={item}
81
+ <FocusableGroup
82
+ id={`focusable-cell-wrapper-${id}`}
83
+ testID={"cell-with-focusable-cell-renderer-focusable-group"}
110
84
  groupId={groupId}
111
- onToggleFocus={handleToggleFocus}
112
- state={state}
113
- prefixId={id}
114
- focusedButtonId={focusedButtonId}
115
85
  preferredFocus={preferredFocus}
116
- skipFocusManagerRegistration={skipFocusManagerRegistration}
117
- isFocusable={isFocusable}
118
- focused={focused}
119
- />
86
+ shouldUsePreferredFocus
87
+ onFocus={onGroupFocus}
88
+ onBlur={onGroupBlur}
89
+ >
90
+ <CellRenderer
91
+ testID={"cell-with-focusable-cell-renderer"}
92
+ item={item}
93
+ groupId={`focusable-cell-wrapper-${id}`}
94
+ onToggleFocus={handleToggleFocus}
95
+ state={state}
96
+ prefixId={id}
97
+ focusedButtonId={focusedButtonId}
98
+ preferredFocus={true}
99
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
100
+ isFocusable={isFocusable}
101
+ focused={focused}
102
+ />
103
+ </FocusableGroup>
120
104
  );
121
105
  }
@@ -1,7 +1,7 @@
1
1
  import { View } from "react-native";
2
2
  import React from "react";
3
3
  import { act, render } from "@testing-library/react-native";
4
- import { CellWithFocusable } from "../CellWithFocusable";
4
+ import { CellWithFocusable } from "../CellWithFocusable.tsx";
5
5
 
6
6
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
7
7
 
@@ -23,7 +23,9 @@ describe("CellWithFocusable", () => {
23
23
 
24
24
  const wrapper = renderWith(props);
25
25
 
26
- expect(wrapper.UNSAFE_getByType("View").props.state).toBe("default");
26
+ const element = wrapper.getByTestId("cell-with-focusable-cell-renderer");
27
+
28
+ expect(element.props.state).toBe("default");
27
29
  });
28
30
 
29
31
  it("should render in default state", () => {
@@ -40,8 +42,9 @@ describe("CellWithFocusable", () => {
40
42
  focusManager.isGroupItemFocused = jest.fn(() => true);
41
43
 
42
44
  const wrapper = renderWith(props);
45
+ const element = wrapper.getByTestId("cell-with-focusable-cell-renderer");
43
46
 
44
- expect(wrapper.UNSAFE_getByType("View").props.state).toBe("default");
47
+ expect(element.props.state).toBe("default");
45
48
  });
46
49
 
47
50
  it("should render in focused state", () => {
@@ -55,13 +58,17 @@ describe("CellWithFocusable", () => {
55
58
  scrollTo: jest.fn(),
56
59
  };
57
60
 
58
- focusManager.isGroupItemFocused = jest.fn(() => true);
59
61
  const wrapper = renderWith(props);
60
62
 
63
+ const focusableGroupComponent = wrapper.getByTestId(
64
+ "cell-with-focusable-cell-renderer-focusable-group"
65
+ );
66
+
61
67
  act(() => {
62
- focusManager.on.mock.calls[focusManager.on.mock.calls.length - 1][1]();
68
+ focusableGroupComponent.props.onFocus();
63
69
  });
64
70
 
65
- expect(wrapper.UNSAFE_getByType("View").props.state).toBe("focused");
71
+ const element = wrapper.getByTestId("cell-with-focusable-cell-renderer");
72
+ expect(element.props.state).toBe("focused");
66
73
  });
67
74
  });
@@ -16,13 +16,13 @@ export class MasterCellAsyncRenderManager {
16
16
  }
17
17
 
18
18
  public emitAsyncElementRegistrate = () => {
19
- this.asyncElementRegistrate$.next();
19
+ this.asyncElementRegistrate$.next(null);
20
20
 
21
21
  return this.decrementAsyncElementRegistrationCount;
22
22
  };
23
23
 
24
24
  public emitAsyncElementLayout = () => {
25
- this.asyncElementLayout$.next();
25
+ this.asyncElementLayout$.next(null);
26
26
  };
27
27
 
28
28
  public decrementAsyncElementRegistrationCount = () => {
@@ -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`,
@@ -17,7 +17,90 @@ exports[`componentsMap renders renders components map correctly 1`] = `
17
17
  }
18
18
  >
19
19
  <RCTScrollView
20
- ListFooterComponent={[Function]}
20
+ ListFooterComponent={
21
+ <Memo(RiverFooterComponent)
22
+ flatListHeight={null}
23
+ loadingState={
24
+ BehaviorSubject {
25
+ "_value": {
26
+ "done": false,
27
+ "index": -1,
28
+ "waitForAllComponents": false,
29
+ },
30
+ "closed": false,
31
+ "currentObservers": null,
32
+ "hasError": false,
33
+ "isStopped": false,
34
+ "observers": [
35
+ SafeSubscriber {
36
+ "_finalizers": [
37
+ Subscription {
38
+ "_finalizers": null,
39
+ "_parentage": [Circular],
40
+ "closed": false,
41
+ "initialTeardown": [Function],
42
+ },
43
+ ],
44
+ "_parentage": null,
45
+ "closed": false,
46
+ "destination": ConsumerObserver {
47
+ "partialObserver": {
48
+ "complete": undefined,
49
+ "error": undefined,
50
+ "next": [Function],
51
+ },
52
+ },
53
+ "initialTeardown": undefined,
54
+ "isStopped": false,
55
+ },
56
+ SafeSubscriber {
57
+ "_finalizers": [
58
+ Subscription {
59
+ "_finalizers": null,
60
+ "_parentage": [Circular],
61
+ "closed": false,
62
+ "initialTeardown": [Function],
63
+ },
64
+ ],
65
+ "_parentage": null,
66
+ "closed": false,
67
+ "destination": ConsumerObserver {
68
+ "partialObserver": {
69
+ "complete": undefined,
70
+ "error": undefined,
71
+ "next": [Function],
72
+ },
73
+ },
74
+ "initialTeardown": undefined,
75
+ "isStopped": false,
76
+ },
77
+ SafeSubscriber {
78
+ "_finalizers": [
79
+ Subscription {
80
+ "_finalizers": null,
81
+ "_parentage": [Circular],
82
+ "closed": false,
83
+ "initialTeardown": [Function],
84
+ },
85
+ ],
86
+ "_parentage": null,
87
+ "closed": false,
88
+ "destination": ConsumerObserver {
89
+ "partialObserver": {
90
+ "complete": undefined,
91
+ "error": undefined,
92
+ "next": [Function],
93
+ },
94
+ },
95
+ "initialTeardown": undefined,
96
+ "isStopped": false,
97
+ },
98
+ ],
99
+ "thrownError": null,
100
+ }
101
+ }
102
+ />
103
+ }
21
104
  contentContainerStyle={
22
105
  {
23
106
  "paddingBottom": undefined,
@@ -80,22 +163,14 @@ exports[`componentsMap renders renders components map correctly 1`] = `
80
163
  style={null}
81
164
  >
82
165
  <View
166
+ onLayout={[Function]}
83
167
  style={
84
168
  {
85
- "display": "flex",
169
+ "flex": 1,
86
170
  }
87
171
  }
88
172
  >
89
- <View
90
- onLayout={[Function]}
91
- style={
92
- {
93
- "flex": 1,
94
- }
95
- }
96
- >
97
- <View />
98
- </View>
173
+ <View />
99
174
  </View>
100
175
  </View>
101
176
  <View
@@ -103,23 +178,13 @@ exports[`componentsMap renders renders components map correctly 1`] = `
103
178
  style={null}
104
179
  >
105
180
  <View
181
+ onLayout={[Function]}
106
182
  style={
107
183
  {
108
- "display": "none",
184
+ "flex": 1,
109
185
  }
110
186
  }
111
- >
112
- <View
113
- onLayout={[Function]}
114
- style={
115
- {
116
- "flex": 1,
117
- }
118
- }
119
- >
120
- <View />
121
- </View>
122
- </View>
187
+ />
123
188
  </View>
124
189
  <View
125
190
  onLayout={[Function]}
@@ -127,12 +192,16 @@ exports[`componentsMap renders renders components map correctly 1`] = `
127
192
  <View
128
193
  renderToHardwareTextureAndroid={true}
129
194
  style={
130
- {
131
- "alignItems": "center",
132
- "height": null,
133
- "justifyContent": "center",
134
- "width": "100%",
135
- }
195
+ [
196
+ {
197
+ "alignItems": "center",
198
+ "justifyContent": "center",
199
+ "width": "100%",
200
+ },
201
+ {
202
+ "height": null,
203
+ },
204
+ ]
136
205
  }
137
206
  >
138
207
  <ActivityIndicator
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
- import TestRenderer from "react-test-renderer";
3
- import { cleanup } from "@testing-library/react-native";
2
+ import { cleanup, render } from "@testing-library/react-native";
4
3
  import { Provider } from "react-redux";
5
4
  import configureStore from "redux-mock-store";
6
5
 
@@ -183,12 +182,12 @@ describe("componentsMap", () => {
183
182
  .spyOn(themeUtils, "useTheme")
184
183
  .mockImplementation(() => () => theme);
185
184
 
186
- const wrapper = TestRenderer.create(
185
+ const { toJSON } = render(
187
186
  <Provider store={store}>
188
187
  <ComponentsMap {...props} />
189
188
  </Provider>
190
189
  );
191
190
 
192
- expect(wrapper.toJSON()).toMatchSnapshot();
191
+ expect(toJSON()).toMatchSnapshot();
193
192
  });
194
193
  });
@@ -0,0 +1,56 @@
1
+ import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
2
+ import {
3
+ useGetScreenOrientation,
4
+ isOrientationCompatible,
5
+ } from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
6
+ import {
7
+ useCurrentScreenData,
8
+ useDimensions,
9
+ useRoute,
10
+ } from "@applicaster/zapp-react-native-utils/reactHooks";
11
+ import { useMemo, useEffect, useState } from "react";
12
+
13
+ export const useWaitForValidOrientation = () => {
14
+ const {
15
+ width: screenWidth,
16
+ height,
17
+ deviceInfo,
18
+ } = useDimensions("screen", {
19
+ fullDimensions: true,
20
+ updateForInactiveScreens: false,
21
+ });
22
+
23
+ const currentScreenData = useCurrentScreenData();
24
+
25
+ const { screenData } = useRoute();
26
+
27
+ const [readyState, setReadyState] = useState(false);
28
+
29
+ const isTablet = deviceInfo?.isTablet;
30
+
31
+ const { appData } = usePickFromState(["appData"]);
32
+ const isTabletPortrait = appData?.isTabletPortrait;
33
+
34
+ const layoutData = useMemo(
35
+ () => ({ isTablet, isTabletPortrait, width: screenWidth, height }),
36
+ [isTablet, isTabletPortrait, screenWidth, height]
37
+ );
38
+
39
+ const targetScreenData =
40
+ currentScreenData || (screenData as any)?.targetScreen || screenData;
41
+
42
+ const orientation = useGetScreenOrientation(targetScreenData);
43
+
44
+ const isReadyForDisplay = isOrientationCompatible({
45
+ orientation,
46
+ layoutData,
47
+ });
48
+
49
+ useEffect(() => {
50
+ if (isReadyForDisplay && !readyState) {
51
+ setReadyState(true);
52
+ }
53
+ }, [readyState, orientation, layoutData]);
54
+
55
+ return readyState;
56
+ };
@@ -2,10 +2,6 @@
2
2
  import React from "react";
3
3
  import { View } from "react-native";
4
4
  import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
5
- import {
6
- isOrientationCompatible,
7
- useGetScreenOrientation,
8
- } from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
9
5
 
10
6
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
11
7
  import { getComponentModule } from "@applicaster/zapp-react-native-utils/pluginUtils";
@@ -15,7 +11,6 @@ import {
15
11
  getScreenId,
16
12
  } from "@applicaster/zapp-react-native-utils/navigationUtils";
17
13
  import {
18
- useDimensions,
19
14
  useRoute,
20
15
  useCurrentScreenData,
21
16
  useNavbarState,
@@ -27,6 +22,7 @@ import { getNavigationPluginModule } from "@applicaster/zapp-react-native-app/Ap
27
22
  import { RouteManager } from "../RouteManager";
28
23
  import { useScreenConfiguration } from "../River/useScreenConfiguration";
29
24
  import { isValidColor } from "./utils";
25
+ import { useWaitForValidOrientation } from "./hooks";
30
26
 
31
27
  const screenStyles = {
32
28
  flex: 1,
@@ -50,15 +46,6 @@ export function Screen(_props: Props) {
50
46
  const currentScreenData = useCurrentScreenData();
51
47
  const { backgroundColor } = useScreenConfiguration(currentScreenData.id);
52
48
 
53
- const {
54
- width: screenWidth,
55
- height,
56
- deviceInfo,
57
- } = useDimensions("screen", {
58
- updateForInactiveScreens: false,
59
- fullDimensions: true,
60
- });
61
-
62
49
  const { screenData, pathname } = useRoute();
63
50
 
64
51
  const currentRiver = useScreenData(
@@ -67,13 +54,6 @@ export function Screen(_props: Props) {
67
54
 
68
55
  const { title } = useNavbarState();
69
56
 
70
- const isTablet = deviceInfo?.isTablet;
71
-
72
- const { appData } = usePickFromState(["appData"]);
73
- const isTabletPortrait = appData?.isTabletPortrait;
74
-
75
- const layoutData = { isTablet, isTabletPortrait, width: screenWidth, height };
76
-
77
57
  const hasMenu = shouldNavBarDisplayMenu(currentRiver, plugins);
78
58
 
79
59
  const navBarProps = React.useMemo<MobileNavBarPluginProps | null>(
@@ -109,30 +89,23 @@ export function Screen(_props: Props) {
109
89
  [theme.app_background_color, backgroundColor]
110
90
  );
111
91
 
112
- const targetScreenData =
113
- currentScreenData || (screenData as any)?.targetScreen || screenData;
114
-
115
- const orientation = useGetScreenOrientation(targetScreenData);
92
+ // Set ready state when screen is rotated to desired orientation
93
+ const isReady = useWaitForValidOrientation();
116
94
 
117
95
  // We prevent rendering of the screen until UI is actually rotated to screen desired orientation.
118
96
  // This saves unnecessary re-renders and user will not see distorted aspect screen.
119
- if (
120
- !isOrientationCompatible({
121
- orientation,
122
- layoutData,
123
- })
124
- ) {
125
- return <View style={style} />;
126
- }
127
-
128
97
  return (
129
98
  <View style={style}>
130
- {navBarProps && <NavBar {...navBarProps} hasMenu={hasMenu} />}
131
-
132
- <OfflineFallbackScreen>
133
- {/* @TODO RouteManager doesn't use props, can they be removed ? */}
134
- <RouteManager pathname={pathname} screenData={screenData} />
135
- </OfflineFallbackScreen>
99
+ {isReady ? (
100
+ <>
101
+ {navBarProps && <NavBar {...navBarProps} hasMenu={hasMenu} />}
102
+
103
+ <OfflineFallbackScreen>
104
+ {/* @TODO RouteManager doesn't use props, can they be removed ? */}
105
+ <RouteManager pathname={pathname} screenData={screenData} />
106
+ </OfflineFallbackScreen>
107
+ </>
108
+ ) : null}
136
109
  </View>
137
110
  );
138
111
  }
@@ -118,11 +118,10 @@ const Provider = ({ children }: { children: React.ReactNode }) => {
118
118
  If bottomTabBarHeight is equal 0 it means an app does not use bottomTabBar.
119
119
  Because of this we need to minus bottom SafeArea offset.
120
120
  */
121
+
121
122
  const minValue =
122
123
  height -
123
- minimisedHeight -
124
- (bottomTabBarHeight || bottomSafeArea) -
125
- progressBarHeight;
124
+ (minimisedHeight + bottomTabBarHeight + progressBarHeight + bottomSafeArea);
126
125
 
127
126
  const modalSnapPoints = React.useMemo(() => [0, minValue], [minValue]);
128
127
  // Last snap state which will helps us to make smooth responder to scrollview animation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "13.0.0-alpha.7846284820",
3
+ "version": "13.0.0-alpha.7854932241",
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",
@@ -34,10 +34,10 @@
34
34
  "redux-mock-store": "^1.5.3"
35
35
  },
36
36
  "dependencies": {
37
- "@applicaster/applicaster-types": "13.0.0-alpha.7846284820",
38
- "@applicaster/zapp-react-native-bridge": "13.0.0-alpha.7846284820",
39
- "@applicaster/zapp-react-native-redux": "13.0.0-alpha.7846284820",
40
- "@applicaster/zapp-react-native-utils": "13.0.0-alpha.7846284820",
37
+ "@applicaster/applicaster-types": "13.0.0-alpha.7854932241",
38
+ "@applicaster/zapp-react-native-bridge": "13.0.0-alpha.7854932241",
39
+ "@applicaster/zapp-react-native-redux": "13.0.0-alpha.7854932241",
40
+ "@applicaster/zapp-react-native-utils": "13.0.0-alpha.7854932241",
41
41
  "promise": "^8.3.0",
42
42
  "react-router-native": "^5.1.2",
43
43
  "url": "^0.11.0",