@applicaster/zapp-react-native-ui-components 12.0.0-alpha.8417280639 → 12.0.0-alpha.8690239043

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 (43) hide show
  1. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +13 -6
  2. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +1 -1
  3. package/Components/Layout/TV/index.tsx +12 -9
  4. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +2 -0
  5. package/Components/MasterCell/DefaultComponents/DynamicBadge.tsx +4 -2
  6. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +5 -3
  7. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -1
  8. package/Components/MasterCell/DefaultComponents/Text/index.tsx +14 -2
  9. package/Components/MasterCell/DefaultComponents/__tests__/image.test.js +1 -1
  10. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +24 -9
  11. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -3
  12. package/Components/MasterCell/hooks/useAsyncRendering/index.ts +1 -1
  13. package/Components/MeasurmentsPortal/BasicMeasurementsPortal.tsx +11 -7
  14. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +10 -1
  15. package/Components/PlayerContainer/PlayerContainer.tsx +9 -5
  16. package/Components/River/ComponentsMap/ComponentsMap.tsx +92 -22
  17. package/Components/River/RefreshControl.tsx +55 -1
  18. package/Components/RouteManager/index.tsx +2 -2
  19. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +4 -0
  20. package/Components/Screen/index.tsx +35 -24
  21. package/Components/Transitioner/Scene.tsx +24 -19
  22. package/Components/Transitioner/Transitioner.tsx +9 -2
  23. package/Components/Transitioner/__tests__/__snapshots__/Scene.test.js.snap +6 -2
  24. package/Components/Transitioner/__tests__/__snapshots__/transitioner.test.js.snap +1 -0
  25. package/Components/VideoLive/LiveImageManager.ts +3 -0
  26. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +0 -5
  27. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +11 -4
  28. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +9 -2
  29. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +13 -2
  30. package/Components/VideoModal/PlayerWrapper.tsx +67 -60
  31. package/Components/VideoModal/VideoModal.tsx +19 -19
  32. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +7 -0
  33. package/Components/Viewport/ViewportTracker/ViewportTracker.tsx +31 -40
  34. package/Components/Viewport/ViewportTracker/index.tsx +1 -0
  35. package/Components/Viewport/utils.js +1 -1
  36. package/Contexts/MenuToggleContext/{index.js → index.ts} +16 -0
  37. package/Contexts/ScreenContext/__tests__/__snapshots__/index.test.tsx.snap +3 -0
  38. package/Contexts/ScreenContext/__tests__/index.test.tsx +27 -0
  39. package/Contexts/ScreenContext/index.tsx +94 -25
  40. package/Contexts/ScreenDataContext/index.ts +3 -0
  41. package/events/index.ts +1 -0
  42. package/package.json +5 -5
  43. /package/Contexts/{index.js → index.ts} +0 -0
@@ -50,7 +50,11 @@ describe("getTransformedPreset should return the passed components if smartCompo
50
50
  );
51
51
 
52
52
  expect(transformed_preset).toBeDefined();
53
- expect(transformed_preset.id).toEqual("comp10"); // id was transformed
53
+
54
+ expect(transformed_preset.id).toEqual(
55
+ `${mockComp.id}-${mockPreset.feed_url}-0`
56
+ ); // id was transformed
57
+
54
58
  expect(transformed_preset.data.source).toEqual(mockPreset.feed_url); // source was replaced with preset feed_url.
55
59
  });
56
60
  });
@@ -117,7 +121,10 @@ describe("getTransformedPreset should return the passed components if smartCompo
117
121
 
118
122
  expect(Array.isArray(result)).toBe(true);
119
123
  expect(result).toHaveLength(2);
120
- expect(result[0].id).toEqual("smart10");
124
+
125
+ expect(result[0].id).toEqual(
126
+ `${mockSmartComp.id}-${mockFeeds.preset_source.data.entry[0].feed_url}-0`
127
+ );
121
128
 
122
129
  expect(result[0].data.source).toEqual(
123
130
  mockFeeds.preset_source.data.entry[0].feed_url
@@ -287,7 +294,7 @@ describe("getTransformedPreset should return the passed components if smartCompo
287
294
  mockComponents[0],
288
295
  {
289
296
  ...mockLayoutPresets.preset_one,
290
- id: mockComponents[1].id + 1,
297
+ id: `${mockComponents[1].id}-${mockPresetEntry[0].feed_url}-1`,
291
298
  data: { source: mockPresetEntry[0].feed_url },
292
299
  },
293
300
  ];
@@ -344,12 +351,12 @@ describe("getTransformedPreset should return the passed components if smartCompo
344
351
  const mockTransformedComponents = [
345
352
  {
346
353
  ...mockLayoutPresets.preset_one,
347
- id: mockComponents[0].id + 0,
354
+ id: `${mockComponents[0].id}-${mockPresetEntry1[0].feed_url}-0`,
348
355
  data: { source: mockPresetEntry1[0].feed_url },
349
356
  },
350
357
  {
351
358
  ...mockLayoutPresets.preset_one,
352
- id: mockComponents[1].id + 1,
359
+ id: `${mockComponents[1].id}-${mockPresetEntry2[0].feed_url}-1`,
353
360
  data: { source: mockPresetEntry2[0].feed_url },
354
361
  },
355
362
  ];
@@ -412,7 +419,7 @@ describe("getTransformedPreset should return the passed components if smartCompo
412
419
  const mockTransformedComponents = mockPresetEntry.map((preset, i) => {
413
420
  return {
414
421
  ...mockLayoutPresets[preset.preset_name],
415
- id: mockComponents[0].id + i,
422
+ id: `${mockComponents[0].id}-${preset.feed_url}-${i}`,
416
423
  data: { source: preset.feed_url },
417
424
  };
418
425
  });
@@ -41,7 +41,7 @@ export const getTransformedPreset = (
41
41
  }
42
42
 
43
43
  const updatedPresetComponent = produce(presetComponent, (draft) => {
44
- draft.id = `${componentId}${index}`;
44
+ draft.id = `${componentId}-${preset?.feed_url}-${index}`;
45
45
  draft.data.source = preset?.feed_url;
46
46
 
47
47
  if (draft.ui_components) {
@@ -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>
@@ -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;
@@ -43,14 +43,16 @@ function Image({
43
43
  [source, error]
44
44
  );
45
45
 
46
+ const _source = shouldRenderImg
47
+ ? withDimensions(source)
48
+ : defaultPlaceHolderImage;
49
+
46
50
  return (
47
51
  // @ts-ignore
48
52
  <MemoizedImage
49
53
  style={style as ImageStyle}
50
54
  onError={() => setErrorState(true)}
51
- source={
52
- shouldRenderImg ? withDimensions(source) : defaultPlaceHolderImage
53
- }
55
+ source={_source?.uri ? _source : undefined}
54
56
  {...R.omit(["source"], otherProps)}
55
57
  />
56
58
  );
@@ -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
@@ -39,6 +39,6 @@ describe("image with no source", () => {
39
39
  );
40
40
 
41
41
  const testInstance = testRenderer.root;
42
- expect(testInstance.findByType(Image).props.source).toEqual({ uri: "" });
42
+ expect(testInstance.findByType(Image).props.source).toBeUndefined();
43
43
  });
44
44
  });
@@ -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);
@@ -54,7 +54,10 @@ import { toNumber } from "@applicaster/zapp-react-native-utils/numberUtils";
54
54
  import { usePlayNextOverlay } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayNextOverlay";
55
55
  import { PlayNextState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/OverlaysObserver";
56
56
 
57
- import { useModalAnimationContext } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
57
+ import {
58
+ useModalAnimationContext,
59
+ PlayerAnimationStateEnum,
60
+ } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
58
61
 
59
62
  type Props = {
60
63
  Player: React.ComponentType<any>;
@@ -231,6 +234,9 @@ const PlayerContainerComponent = (props: Props) => {
231
234
  const screenData = useTargetScreenData(item);
232
235
  const { setVisible: showNavBar } = useSetNavbarState();
233
236
 
237
+ const { isActiveGesture, startComponentsAnimation, setPlayerAnimationState } =
238
+ useModalAnimationContext();
239
+
234
240
  const playerEvent = (event, ...args) => {
235
241
  playerManager.invokeHandler(event, ...args);
236
242
  };
@@ -448,7 +454,7 @@ const PlayerContainerComponent = (props: Props) => {
448
454
  if (disableMiniPlayer) {
449
455
  navigator.closeVideoModal();
450
456
  } else {
451
- navigator.minimiseVideoModal();
457
+ setPlayerAnimationState(PlayerAnimationStateEnum.minimize);
452
458
  }
453
459
  }
454
460
 
@@ -600,9 +606,6 @@ const PlayerContainerComponent = (props: Props) => {
600
606
  pluginConfiguration,
601
607
  };
602
608
 
603
- const { isActiveGesture, startComponentsAnimation } =
604
- useModalAnimationContext();
605
-
606
609
  return (
607
610
  <PlayerStateContext.Provider value={value}>
608
611
  <PlayerContainerContextProvider
@@ -668,6 +671,7 @@ const PlayerContainerComponent = (props: Props) => {
668
671
  setNextVideoPreloadThresholdPercentage={
669
672
  setNextVideoPreloadThresholdPercentage
670
673
  }
674
+ startComponentsAnimation={startComponentsAnimation}
671
675
  >
672
676
  {renderApplePlayer(applePlayerProps)}
673
677
  </Player>
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
- import { View, FlatList, StyleSheet, Platform } 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";
@@ -21,7 +21,8 @@ import {
21
21
  import { useScreenData } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenData";
22
22
  import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
23
23
  import { withComponentsMapProvider } from "@applicaster/zapp-react-native-ui-components/Decorators/ComponentsMapWrapper";
24
- import { useScreenScrollStore } from "@applicaster/zapp-react-native-utils/screenState";
24
+ import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
25
+ import { useShallow } from "zustand/react/shallow";
25
26
 
26
27
  type Props = {
27
28
  feed: ZappFeed;
@@ -41,8 +42,10 @@ const styles = StyleSheet.create({
41
42
  },
42
43
  });
43
44
 
44
- const scrollIndicatorInsets = { right: 1 };
45
+ const getNearestValue = (value, optionA, optionB) =>
46
+ Math.abs(value - optionA) < Math.abs(value - optionB) ? optionA : optionB;
45
47
 
48
+ const scrollIndicatorInsets = { right: 1 };
46
49
  const keyExtractor = R.compose(String, R.prop("id"));
47
50
 
48
51
  function ComponentsMapComponent(props: Props) {
@@ -60,6 +63,7 @@ function ComponentsMapComponent(props: Props) {
60
63
  refreshingPipesV1,
61
64
  } = props;
62
65
 
66
+ const flatListRef = React.useRef<FlatList | null>(null);
63
67
  const screenConfig = useScreenConfiguration(riverId);
64
68
  const screenData = useScreenData(riverId);
65
69
  const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
@@ -194,26 +198,87 @@ function ComponentsMapComponent(props: Props) {
194
198
  []
195
199
  );
196
200
 
197
- const onScrollFlatList = React.useCallback(
198
- ({ nativeEvent }, isScrollingDone = false) => {
199
- const {
200
- contentOffset: { y },
201
- } = nativeEvent;
201
+ const navBarStore = useScreenContextV2()._navBarStore;
202
202
 
203
- useScreenScrollStore.setState({
204
- scrollingVelocity: isScrollingDone ? 0 : 1,
205
- scrollPosition: y,
206
- });
207
- },
208
- []
203
+ const scrollY = navBarStore(
204
+ useShallow(({ scrollYAnimated }) => scrollYAnimated)
209
205
  );
210
206
 
211
- const onMomentumScrollEnd = React.useCallback(
212
- ({ nativeEvent }) => {
213
- onScrollFlatList({ nativeEvent }, true);
214
- },
215
- [onScrollFlatList]
216
- );
207
+ const canMomentum = React.useRef(false);
208
+
209
+ const onScrollBeginDrag = React.useCallback(() => {
210
+ canMomentum.current = true;
211
+ }, []);
212
+
213
+ /* onMomentumScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
214
+ const _onMomentumScrollEnd = React.useCallback(({ nativeEvent }) => {
215
+ if (!canMomentum.current) return;
216
+ canMomentum.current = false;
217
+
218
+ const { height: headerHeight, scrollState } = navBarStore.getState();
219
+
220
+ const offsetY = Math.max(0, nativeEvent.contentOffset.y);
221
+
222
+ if (
223
+ !(scrollState === 0 || scrollState === -headerHeight) &&
224
+ flatListRef.current
225
+ ) {
226
+ const offset =
227
+ getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
228
+ -headerHeight
229
+ ? Math.round(offsetY + scrollState + headerHeight)
230
+ : Math.round(offsetY + scrollState);
231
+
232
+ flatListRef.current.scrollToOffset({
233
+ animated: true,
234
+ offset,
235
+ });
236
+ }
237
+ }, []);
238
+
239
+ /* onScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
240
+ const _onScrollEndDrag = React.useCallback(({ nativeEvent }) => {
241
+ const { height: headerHeight, scrollState } = navBarStore.getState();
242
+
243
+ const offsetY = Math.max(0, nativeEvent.contentOffset.y);
244
+
245
+ if (
246
+ !(scrollState === 0 || scrollState === -headerHeight) &&
247
+ flatListRef.current &&
248
+ nativeEvent.velocity.y === 0
249
+ ) {
250
+ const offset =
251
+ getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
252
+ -headerHeight
253
+ ? Math.round(offsetY + scrollState + headerHeight)
254
+ : Math.round(offsetY + scrollState);
255
+
256
+ flatListRef.current.scrollToOffset({
257
+ animated: true,
258
+ offset,
259
+ });
260
+ }
261
+ }, []);
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
+ }, []);
217
282
 
218
283
  if (shouldShowLoadingError) {
219
284
  return <RiverError />;
@@ -230,6 +295,9 @@ function ComponentsMapComponent(props: Props) {
230
295
  >
231
296
  <ViewportTracker>
232
297
  <FlatList
298
+ ref={(ref) => {
299
+ flatListRef.current = ref;
300
+ }}
233
301
  // Fix for WebView rerender crashes on Android API 28+
234
302
  // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
235
303
  overScrollMode={Platform.OS === "android" ? "never" : "auto"}
@@ -249,8 +317,10 @@ function ComponentsMapComponent(props: Props) {
249
317
  }
250
318
  ListFooterComponent={renderFooter}
251
319
  refreshControl={refreshControl}
252
- onScroll={onScrollFlatList}
253
- onMomentumScrollEnd={onMomentumScrollEnd}
320
+ onScrollBeginDrag={onScrollBeginDrag}
321
+ onScroll={onScroll}
322
+ onMomentumScrollEnd={_onMomentumScrollEnd}
323
+ onScrollEndDrag={_onScrollEndDrag}
254
324
  scrollEventThrottle={16}
255
325
  {...scrollViewExtraProps}
256
326
  />