@applicaster/zapp-react-native-ui-components 12.0.0-alpha.4300857220 → 12.0.0-alpha.4324313105

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 (30) hide show
  1. package/Components/Layout/TV/index.tsx +12 -9
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +2 -0
  3. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +80 -109
  4. package/Components/MasterCell/DefaultComponents/DynamicBadge.tsx +4 -2
  5. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -1
  6. package/Components/MasterCell/DefaultComponents/Text/index.tsx +14 -2
  7. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +24 -9
  8. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -3
  9. package/Components/MasterCell/hooks/useAsyncRendering/index.ts +1 -1
  10. package/Components/MeasurmentsPortal/BasicMeasurementsPortal.tsx +11 -7
  11. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +10 -1
  12. package/Components/PlayerContainer/PlayerContainer.tsx +1 -0
  13. package/Components/River/ComponentsMap/ComponentsMap.tsx +24 -17
  14. package/Components/River/RefreshControl.tsx +55 -1
  15. package/Components/RouteManager/index.tsx +2 -2
  16. package/Components/Transitioner/Scene.tsx +24 -19
  17. package/Components/Transitioner/Transitioner.tsx +9 -2
  18. package/Components/Transitioner/__tests__/__snapshots__/Scene.test.js.snap +6 -2
  19. package/Components/Transitioner/__tests__/__snapshots__/transitioner.test.js.snap +1 -0
  20. package/Components/VideoLive/LiveImageManager.ts +3 -0
  21. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +13 -2
  22. package/Components/VideoModal/PlayerDetails.tsx +3 -3
  23. package/Components/VideoModal/PlayerWrapper.tsx +67 -60
  24. package/Components/VideoModal/VideoModal.tsx +19 -19
  25. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +7 -0
  26. package/Components/Viewport/ViewportTracker/ViewportTracker.tsx +31 -40
  27. package/Contexts/ScreenContext/index.tsx +2 -2
  28. package/Contexts/ScreenDataContext/index.ts +3 -0
  29. package/events/index.ts +1 -0
  30. package/package.json +5 -5
@@ -20,6 +20,7 @@ import { ScreenLayoutContextProvider } from "./ScreenLayoutContextProvider";
20
20
  import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
21
21
  import { PathnameContext } from "../../../Contexts/PathnameContext";
22
22
  import { useNavbarState } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
23
+ import { ScreenDataContext } from "../../../Contexts/ScreenDataContext";
23
24
 
24
25
  type Components = {
25
26
  NavBar: React.ComponentType<any>;
@@ -105,20 +106,22 @@ const Layout = ({ Components, ComponentsExtraProps, children }: Props) => {
105
106
  backgroundImageUrl={backgroundImageUrl}
106
107
  {...R.omit(["ref"])(ComponentsExtraProps?.Background)}
107
108
  >
108
- {!isTVOS ? navBarContainer : null}
109
- {navBarReady ? (
110
- /**
109
+ <ScreenDataContext.Provider value={navigator.data}>
110
+ <PathnameContext.Provider value={navigator.currentRoute}>
111
+ {!isTVOS ? navBarContainer : null}
112
+ {navBarReady ? (
113
+ /**
111
114
  Need to re-mount this component on each navigation event
112
115
  It's a hack to work around the fact that we don't support
113
116
  correct screen rendering in our navigator (shared currentRoute state)
114
117
  **/
115
- <PathnameContext.Provider value={navigator.currentRoute}>
116
- <ScreenContainer>{children}</ScreenContainer>
117
- </PathnameContext.Provider>
118
- ) : null}
119
- {/** Render NavBarContainer over ScreenContainer to let tvOS native
118
+ <ScreenContainer>{children}</ScreenContainer>
119
+ ) : null}
120
+ {/** Render NavBarContainer over ScreenContainer to let tvOS native
120
121
  focus manager works correctly to focus nav bar items first **/}
121
- {isTVOS ? navBarContainer : null}
122
+ {isTVOS ? navBarContainer : null}
123
+ </PathnameContext.Provider>
124
+ </ScreenDataContext.Provider>
122
125
  <TransitionOverlay />
123
126
  </Components.Background>
124
127
  </ScreenLayoutContextProvider>
@@ -25,6 +25,7 @@ type Props = {
25
25
  testID?: string;
26
26
  accessibilityLabel?: string;
27
27
  cellUUID?: string;
28
+ extraProps?: Record<string, any>;
28
29
  };
29
30
 
30
31
  function isStringAsset(asset) {
@@ -108,6 +109,7 @@ export function ActionButton(props: Props) {
108
109
  flavour={flavour}
109
110
  asset={getAssetValue(asset, flavour)}
110
111
  cellUUID={cellUUID}
112
+ {...(props?.extraProps ?? {})}
111
113
  />
112
114
  )}
113
115
  </TouchableOpacity>
@@ -7,50 +7,24 @@ type BorderPosition = "inside" | "outside" | "center";
7
7
  interface Props {
8
8
  style: ImageStyle | ViewStyle;
9
9
  borderPosition: BorderPosition;
10
+ borderPaddingTop: number;
11
+ borderPaddingRight: number;
12
+ borderPaddingBottom: number;
13
+ borderPaddingLeft: number;
10
14
  children: React.ReactNode;
11
15
  }
12
16
 
13
- const MARGINS = [
14
- "marginTop",
15
- "marginBottom",
16
- "marginLeft",
17
- "marginRight",
18
- ] as const;
17
+ const COLOR_TRANSPARENT = "transparent";
19
18
 
20
- const PADDINGS = [
21
- "paddingTop",
22
- "paddingBottom",
23
- "paddingLeft",
24
- "paddingRight",
25
- ] as const;
19
+ const styles = StyleSheet.create({
20
+ container: {
21
+ overflow: "visible",
22
+ borderWidth: 0,
23
+ borderColor: COLOR_TRANSPARENT,
24
+ },
25
+ });
26
26
 
27
- const isPercentageValue = (value: number | string) => {
28
- if (typeof value === "string" && value.endsWith("%")) {
29
- return true;
30
- }
31
-
32
- return false;
33
- };
34
-
35
- const calculateContainerSpacing = ({
36
- spacingKey,
37
- style,
38
- additionalSpacing,
39
- }: {
40
- spacingKey: typeof MARGINS[number] | typeof PADDINGS[number];
41
- style: ImageStyle | ViewStyle;
42
- additionalSpacing: number;
43
- }) => {
44
- return {
45
- [spacingKey]:
46
- toNumberWithDefaultZero(style[spacingKey]) + additionalSpacing,
47
- };
48
- };
49
-
50
- const getContainerBorderWidth = (
51
- borderPosition: BorderPosition,
52
- borderWidth
53
- ) => {
27
+ const getBorderPadding = (borderPosition: BorderPosition, borderWidth) => {
54
28
  switch (borderPosition) {
55
29
  case "inside":
56
30
  return 0;
@@ -63,104 +37,101 @@ const getContainerBorderWidth = (
63
37
  }
64
38
  };
65
39
 
66
- const getContainerPadding = (borderPosition: BorderPosition, borderWidth) => {
67
- switch (borderPosition) {
68
- case "inside":
69
- return borderWidth;
70
- case "center":
71
- return borderWidth / 2;
72
- case "outside":
73
- return 0;
74
- default:
75
- return 0;
76
- }
77
- };
78
-
79
- const InsideBorder = ({
40
+ const Border = ({
80
41
  style,
42
+ padding,
81
43
  borderPosition,
82
44
  }: {
83
45
  style: ViewStyle;
46
+ padding: ViewStyle;
84
47
  borderPosition: BorderPosition;
85
48
  }) => {
86
- if (borderPosition === "outside") {
49
+ if (!borderPosition) {
87
50
  return null;
88
51
  }
89
52
 
90
- const insideBorderWidth =
91
- borderPosition === "center" ? style.borderWidth / 2 : style.borderWidth;
53
+ const borderWidth = toNumberWithDefaultZero(style.borderWidth);
54
+ const containerPadding = getBorderPadding(borderPosition, borderWidth);
55
+
56
+ const initialBorderRadius = toNumberWithDefaultZero(style.borderRadius);
57
+ const paddingTop = toNumberWithDefaultZero(padding.paddingTop);
58
+ const paddingRight = toNumberWithDefaultZero(padding.paddingRight);
59
+ const paddingBottom = toNumberWithDefaultZero(padding.paddingBottom);
60
+ const paddingLeft = toNumberWithDefaultZero(padding.paddingLeft);
61
+
62
+ const borderRadiusStyle = initialBorderRadius
63
+ ? {
64
+ borderTopLeftRadius:
65
+ initialBorderRadius +
66
+ containerPadding +
67
+ Math.min(paddingTop, paddingLeft),
68
+ borderTopRightRadius:
69
+ initialBorderRadius +
70
+ containerPadding +
71
+ Math.min(paddingTop, paddingRight),
72
+ borderBottomLeftRadius:
73
+ initialBorderRadius +
74
+ containerPadding +
75
+ Math.min(paddingBottom, paddingLeft),
76
+ borderBottomRightRadius:
77
+ initialBorderRadius +
78
+ containerPadding +
79
+ Math.min(paddingBottom, paddingRight),
80
+ }
81
+ : {
82
+ borderTopLeftRadius: 0,
83
+ borderTopRightRadius: 0,
84
+ borderBottomLeftRadius: 0,
85
+ borderBottomRightRadius: 0,
86
+ };
92
87
 
93
88
  return (
94
89
  <View
95
90
  style={{
96
91
  ...StyleSheet.absoluteFillObject,
97
- borderWidth: insideBorderWidth,
92
+ right: -containerPadding - paddingRight,
93
+ bottom: -containerPadding - paddingBottom,
94
+ left: -containerPadding - paddingLeft,
95
+ top: -containerPadding - paddingTop,
96
+ borderWidth,
98
97
  borderColor: style.borderColor,
99
- borderRadius: style.borderRadius,
98
+ ...borderRadiusStyle,
100
99
  }}
101
100
  />
102
101
  );
103
102
  };
104
103
 
105
104
  export const BorderContainerView = (props: Props) => {
106
- const { style, children } = props;
107
- // Return border position prop after re-implementing
108
- const borderPosition = "outside";
109
-
110
- const borderWidth = toNumberWithDefaultZero(style.borderWidth);
111
- const spacing = getContainerPadding(borderPosition, borderWidth);
105
+ const {
106
+ borderPosition,
107
+ borderPaddingTop,
108
+ borderPaddingRight,
109
+ borderPaddingBottom,
110
+ borderPaddingLeft,
111
+ style,
112
+ children,
113
+ } = props;
112
114
 
113
115
  const borderStyleForMissingBorder =
114
116
  style?.borderWidth === 0 || !style?.borderColor ? { borderRadius: 0 } : {};
115
117
 
116
- const updatedMargins = MARGINS.reduce((acc, margin) => {
117
- const marginValue = calculateContainerSpacing({
118
- spacingKey: margin,
119
- style,
120
- additionalSpacing: spacing,
121
- });
122
-
123
- return { ...acc, ...marginValue };
124
- }, {});
125
-
126
- const updatedPaddings = PADDINGS.reduce((acc, padding) => {
127
- const paddingValue = calculateContainerSpacing({
128
- spacingKey: padding,
129
- style,
130
- additionalSpacing: spacing,
131
- });
132
-
133
- return { ...acc, ...paddingValue };
134
- }, {});
135
-
136
- const updatedWidth = style.width
137
- ? isPercentageValue(style.width)
138
- ? style.width
139
- : toNumberWithDefaultZero(style.width) - spacing * 2
140
- : undefined;
141
-
142
- const updatedHeight = style.height
143
- ? isPercentageValue(style.height)
144
- ? style.height
145
- : toNumberWithDefaultZero(style.height) - spacing * 2
146
- : undefined;
147
-
148
- const containerStyle = {
149
- borderWidth: getContainerBorderWidth(borderPosition, borderWidth),
150
- padding: spacing,
151
- ...updatedMargins,
152
- ...updatedPaddings,
153
- width: updatedWidth,
154
- height: updatedHeight,
155
- };
118
+ const padding =
119
+ borderPosition === "outside"
120
+ ? {
121
+ paddingTop: borderPaddingTop,
122
+ paddingRight: borderPaddingRight,
123
+ paddingBottom: borderPaddingBottom,
124
+ paddingLeft: borderPaddingLeft,
125
+ }
126
+ : {};
156
127
 
157
128
  return (
158
- <View {...props} style={[style, containerStyle]}>
129
+ <View
130
+ {...props}
131
+ style={[style, styles.container, borderStyleForMissingBorder]}
132
+ >
159
133
  {children}
160
- <InsideBorder
161
- style={{ ...style, ...borderStyleForMissingBorder }}
162
- borderPosition={borderPosition}
163
- />
134
+ <Border style={style} padding={padding} borderPosition={borderPosition} />
164
135
  </View>
165
136
  );
166
137
  };
@@ -4,6 +4,7 @@ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerM
4
4
 
5
5
  type DynamicBadgeProps = {
6
6
  cellUUID?: string;
7
+ uri?: string;
7
8
  };
8
9
 
9
10
  const DynamicBadgeComponent = (props: DynamicBadgeProps) => {
@@ -12,11 +13,12 @@ const DynamicBadgeComponent = (props: DynamicBadgeProps) => {
12
13
  // TODO: Initial value should depends on player state (playing or not)
13
14
  const [showImage, setShowImage] = React.useState(true);
14
15
  const player = usePlayer(playerId);
16
+ const uri = props?.uri;
15
17
 
16
18
  React.useEffect(() => {
17
19
  if (playerId && player) {
18
20
  return player.addListener({
19
- id: "dynamic-badges",
21
+ id: `dynamic-badges-${uri}`,
20
22
  listener: {
21
23
  onPlayerClose: () => {
22
24
  setShowImage(true);
@@ -35,7 +37,7 @@ const DynamicBadgeComponent = (props: DynamicBadgeProps) => {
35
37
  } else {
36
38
  setShowImage(true);
37
39
  }
38
- }, [player]);
40
+ }, [player, uri]);
39
41
 
40
42
  if (!showImage) {
41
43
  return null;
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { isVideoPreviewEnabled } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/utils";
3
3
  import { LiveImage } from "../LiveImage";
4
4
  import PureImage from "../Image";
5
+ import { useIsScreenActive } from "@applicaster/zapp-react-native-utils/reactHooks";
5
6
 
6
7
  type Props = {
7
8
  enable_video_preview: boolean;
@@ -9,7 +10,10 @@ type Props = {
9
10
  };
10
11
 
11
12
  export const ImageContainer = (props: Props) => {
12
- const Component = isVideoPreviewEnabled(props) ? LiveImage : PureImage;
13
+ const isActive = useIsScreenActive();
14
+
15
+ const Component =
16
+ isVideoPreviewEnabled(props) && isActive ? LiveImage : PureImage;
13
17
 
14
18
  return <Component {...props} />;
15
19
  };
@@ -1,7 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { Text } from "react-native";
3
3
  import { omit } from "ramda";
4
- import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
5
4
  import {
6
5
  textTransform,
7
6
  dateFormat,
@@ -9,6 +8,9 @@ import {
9
8
 
10
9
  import { useTextLabel, withFocusedStyles } from "./hooks";
11
10
  import { withScaledLineHeight } from "./utils";
11
+ import { toNumber } from "@applicaster/zapp-react-native-utils/numberUtils";
12
+ import { MeasurementPortalContext } from "../../../MeasurmentsPortal";
13
+ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
12
14
 
13
15
  type Props = {
14
16
  style: any;
@@ -32,14 +34,24 @@ const _Text = ({
32
34
  ...otherProps
33
35
  }: Props) => {
34
36
  const _label = useTextLabel({ label, entry });
37
+ const isMeasurement = React.useContext(MeasurementPortalContext);
35
38
 
36
39
  if (isNilOrEmpty(_label)) {
37
40
  return null;
38
41
  }
39
42
 
43
+ // set maximum possible height for the text in case of measurement
44
+ const height =
45
+ isMeasurement && otherProps.numberOfLines && style.lineHeight
46
+ ? toNumber(otherProps.numberOfLines) * toNumber(style.lineHeight)
47
+ : undefined;
48
+
40
49
  return (
41
50
  <Text
42
- style={withScaledLineHeight(withFocusedStyles({ style, otherProps }))}
51
+ style={[
52
+ withScaledLineHeight(withFocusedStyles({ style, otherProps })),
53
+ { height },
54
+ ]}
43
55
  {...withoutLabel(otherProps)}
44
56
  >
45
57
  {dateTransformEnabled
@@ -1,11 +1,23 @@
1
1
  import React from "react";
2
- import { Text } from "react-native";
2
+ import RN from "react-native";
3
3
  import { Provider } from "react-redux";
4
4
  import TestRenderer from "react-test-renderer";
5
- import configureStore from "redux-mock-store";
6
- import CustomText from "../Text";
5
+ import configureStoreFn from "redux-mock-store";
7
6
 
8
- const mockStore = configureStore();
7
+ const mockUseIsRTL = jest.fn(() => true);
8
+ const mockGetIsRTL = jest.fn(() => true);
9
+ const mockUseLocalizedStrings = jest.fn(() => ({}));
10
+ const mockGetUILanguage = jest.fn(() => "he-HE");
11
+
12
+ jest.mock("@applicaster/zapp-react-native-utils/localizationUtils", () => ({
13
+ useIsRTL: mockUseIsRTL,
14
+ getIsRTL: mockGetIsRTL,
15
+ useLocalizedStrings: mockUseLocalizedStrings,
16
+ getUILanguage: mockGetUILanguage,
17
+ }));
18
+
19
+ const CustomText = require("../Text").default;
20
+ const mockStore = configureStoreFn();
9
21
 
10
22
  const defaultProps = {
11
23
  entry: {},
@@ -22,7 +34,7 @@ const getRenderedText = (label: string, store: any) => {
22
34
  </Provider>
23
35
  );
24
36
 
25
- return testRenderer.root.findByType(Text).props.children;
37
+ return testRenderer.root.findByType(RN.Text).props.children;
26
38
  };
27
39
 
28
40
  describe("RTL app: Hebrew text contains english word", () => {
@@ -34,7 +46,7 @@ describe("RTL app: Hebrew text contains english word", () => {
34
46
  remoteConfigurations: { localizations: { he: {} } },
35
47
  appData: {
36
48
  languageCode: "he",
37
- countryCode: "HE",
49
+ countryCode: "IL",
38
50
  },
39
51
  });
40
52
 
@@ -47,18 +59,22 @@ describe("RTL app: Hebrew text contains english word", () => {
47
59
 
48
60
  it("Hebrew text contains not first english word", () => {
49
61
  const renderedText = getRenderedText(textWithNotFirstEnglishWord, store);
50
-
51
62
  expect(renderedText).toEqual(textWithNotFirstEnglishWord);
52
63
  });
53
64
 
54
65
  it("Hebrew text doesn't contain english word", () => {
55
66
  const renderedText = getRenderedText(textWithoutEnglishWord, store);
56
-
57
67
  expect(renderedText).toEqual(textWithoutEnglishWord);
58
68
  });
59
69
  });
60
70
 
61
71
  describe("LTR app: English text", () => {
72
+ beforeAll(() => {
73
+ mockUseIsRTL.mockReturnValue(false);
74
+ mockGetIsRTL.mockReturnValue(false);
75
+ mockGetUILanguage.mockReturnValue("en-US");
76
+ });
77
+
62
78
  const englishText = "Test sentence";
63
79
 
64
80
  const store = mockStore({
@@ -71,7 +87,6 @@ describe("LTR app: English text", () => {
71
87
 
72
88
  it("English text", () => {
73
89
  const renderedText = getRenderedText(englishText, store);
74
-
75
90
  expect(renderedText).toEqual(englishText);
76
91
  });
77
92
  });
@@ -51,9 +51,7 @@ export const CollapsibleTextContainer = ({
51
51
  func: (data) => {
52
52
  return getColorFromData({
53
53
  data,
54
- getValue,
55
- key: "background_color",
56
- defaultColor: additionalStyles.backgroundColor,
54
+ valueFromLayout: additionalStyles.backgroundColor,
57
55
  });
58
56
  },
59
57
  propName: "backgroundColor",
@@ -53,7 +53,7 @@ export const useAsyncRendering = (viewRef: React.RefObject<View>): Return => {
53
53
  measure(viewRef?.current, ({ width, height }) => {
54
54
  setDimensions({
55
55
  width,
56
- height,
56
+ height: Math.ceil(height),
57
57
  });
58
58
  });
59
59
  })
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { View, StyleSheet } from "react-native";
3
+ import { MeasurementPortalContext } from "./MeasurementsPortal";
3
4
 
4
5
  type Props = {
5
6
  Component: React.ElementType;
@@ -19,13 +20,16 @@ const _MeasurementsPortal = (
19
20
  { Component, onLayout, componentProps, containerStyle }: Props,
20
21
  ref
21
22
  ) => (
22
- <View
23
- onLayout={onLayout}
24
- style={[styles.container, containerStyle]}
25
- ref={ref}
26
- >
27
- <Component {...componentProps} />
28
- </View>
23
+ // hack for cell to determine if cell is in measurement mode
24
+ <MeasurementPortalContext.Provider value={{}}>
25
+ <View
26
+ onLayout={onLayout}
27
+ style={[styles.container, containerStyle]}
28
+ ref={ref}
29
+ >
30
+ <Component {...componentProps} />
31
+ </View>
32
+ </MeasurementPortalContext.Provider>
29
33
  );
30
34
 
31
35
  export const MeasurementsPortal = React.forwardRef(_MeasurementsPortal);
@@ -35,7 +35,16 @@ const MeasurementsPortal = ({ Component, onLayout, componentProps }: Props) => {
35
35
  );
36
36
  };
37
37
 
38
- const MeasurementPortalContext = React.createContext(null);
38
+ type MeasurementPortalContextType = {
39
+ measureComponent?: (
40
+ comp: React.ElementType,
41
+ props: Record<any, any>
42
+ ) => Promise<any>;
43
+ measuringInProgress?: boolean;
44
+ };
45
+
46
+ const MeasurementPortalContext =
47
+ React.createContext<null | MeasurementPortalContextType>(null);
39
48
 
40
49
  const MeasurementsPortalContextProvider = ({ children }) => {
41
50
  const Component = React.useRef(View);
@@ -671,6 +671,7 @@ const PlayerContainerComponent = (props: Props) => {
671
671
  setNextVideoPreloadThresholdPercentage={
672
672
  setNextVideoPreloadThresholdPercentage
673
673
  }
674
+ startComponentsAnimation={startComponentsAnimation}
674
675
  >
675
676
  {renderApplePlayer(applePlayerProps)}
676
677
  </Player>
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
- import { View, StyleSheet, Platform, Animated, FlatList } from "react-native";
3
+ import { View, StyleSheet, Platform, FlatList } from "react-native";
4
4
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
5
5
  import { RiverItem } from "../RiverItem";
6
6
  import { RiverFooter } from "../RiverFooter";
@@ -217,7 +217,7 @@ function ComponentsMapComponent(props: Props) {
217
217
 
218
218
  const { height: headerHeight, scrollState } = navBarStore.getState();
219
219
 
220
- const offsetY = nativeEvent.contentOffset.y;
220
+ const offsetY = Math.max(0, nativeEvent.contentOffset.y);
221
221
 
222
222
  if (
223
223
  !(scrollState === 0 || scrollState === -headerHeight) &&
@@ -240,7 +240,7 @@ function ComponentsMapComponent(props: Props) {
240
240
  const _onScrollEndDrag = React.useCallback(({ nativeEvent }) => {
241
241
  const { height: headerHeight, scrollState } = navBarStore.getState();
242
242
 
243
- const offsetY = nativeEvent.contentOffset.y;
243
+ const offsetY = Math.max(0, nativeEvent.contentOffset.y);
244
244
 
245
245
  if (
246
246
  !(scrollState === 0 || scrollState === -headerHeight) &&
@@ -260,6 +260,26 @@ function ComponentsMapComponent(props: Props) {
260
260
  }
261
261
  }, []);
262
262
 
263
+ const onScroll = React.useCallback((event) => {
264
+ const {
265
+ nativeEvent: {
266
+ contentOffset: { y },
267
+ layoutMeasurement: { height },
268
+ contentSize: { height: contentHeight },
269
+ },
270
+ } = event;
271
+
272
+ if (y >= 0 && y + height <= contentHeight) {
273
+ scrollY.setValue(y);
274
+ } else {
275
+ if (y <= 0) {
276
+ scrollY.setValue(0);
277
+ } else if (y + height > contentHeight) {
278
+ scrollY.setValue(contentHeight - height);
279
+ }
280
+ }
281
+ }, []);
282
+
263
283
  if (shouldShowLoadingError) {
264
284
  return <RiverError />;
265
285
  }
@@ -298,20 +318,7 @@ function ComponentsMapComponent(props: Props) {
298
318
  ListFooterComponent={renderFooter}
299
319
  refreshControl={refreshControl}
300
320
  onScrollBeginDrag={onScrollBeginDrag}
301
- onScroll={Animated.event(
302
- [
303
- {
304
- nativeEvent: {
305
- contentOffset: {
306
- y: scrollY,
307
- },
308
- },
309
- },
310
- ],
311
- {
312
- useNativeDriver: false,
313
- }
314
- )}
321
+ onScroll={onScroll}
315
322
  onMomentumScrollEnd={_onMomentumScrollEnd}
316
323
  onScrollEndDrag={_onScrollEndDrag}
317
324
  scrollEventThrottle={16}