@applicaster/zapp-react-native-ui-components 12.0.0-alpha.3977160850 → 12.0.0-alpha.4433422595

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 (36) hide show
  1. package/Components/BaseFocusable/index.tsx +1 -1
  2. package/Components/Cell/Cell.tsx +7 -3
  3. package/Components/Cell/CellWithFocusable.tsx +21 -14
  4. package/Components/Cell/TvOSCellComponent.tsx +2 -2
  5. package/Components/ErrorScreen/index.tsx +1 -1
  6. package/Components/FocusableCell/index.tsx +7 -7
  7. package/Components/FocusableGroup/index.tsx +1 -0
  8. package/Components/HookRenderer/index.tsx +1 -3
  9. package/Components/Image/Image.android.tsx +3 -25
  10. package/Components/Image/Image.tsx +18 -2
  11. package/Components/Image/utils/index.ts +39 -0
  12. package/Components/Layout/TV/ScreenLayoutContextProvider.tsx +2 -2
  13. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +41 -1
  14. package/Components/Layout/TV/__tests__/index.test.tsx +1 -13
  15. package/Components/Layout/TV/index.tsx +116 -25
  16. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +9 -26
  17. package/Components/MasterCell/DefaultComponents/Image/hooks/useImageSize.ts +65 -0
  18. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +3 -1
  19. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +3 -2
  20. package/Components/MasterCell/DefaultComponents/Toggle.tsx +5 -1
  21. package/Components/MasterCell/utils/index.ts +2 -2
  22. package/Components/PlayerContainer/PlayerContainer.tsx +46 -28
  23. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -1
  24. package/Components/River/__tests__/componentsMap.test.js +1 -0
  25. package/Components/RouteManager/index.tsx +2 -2
  26. package/Components/Screen/TV/index.web.tsx +88 -0
  27. package/Components/Screen/__tests__/Screen.test.tsx +5 -1
  28. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +10 -0
  29. package/Components/Screen/index.tsx +15 -6
  30. package/Components/Tabs/TV/styles.ts +1 -1
  31. package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -28
  32. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +30 -3
  33. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +22 -27
  34. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +211 -163
  35. package/Decorators/ComponentsMapWrapper/withComponentsMapProvider.tsx +7 -2
  36. package/package.json +5 -5
@@ -73,7 +73,7 @@ export class BaseFocusable<
73
73
  getId(props?: { id: string; groupId: string }) {
74
74
  const { id, groupId = null } = props || this.props;
75
75
 
76
- return groupId ? `${groupId}-${id}` : id;
76
+ return groupId ? `groupId:${groupId}___${id}` : id;
77
77
  }
78
78
 
79
79
  getRect() {
@@ -202,12 +202,16 @@ export class CellComponent extends React.Component<Props> {
202
202
  groupId,
203
203
  isFocusable = true,
204
204
  skipFocusManagerRegistration,
205
- behavior
205
+ behavior,
206
206
  } = this.props;
207
207
 
208
208
  const { id } = item;
209
209
 
210
- const focusableId = R.join("-", [component?.id, id, index]);
210
+ const focusableId = R.join("-", [
211
+ `componentId:${component?.id}`,
212
+ `id:${id}`,
213
+ `index:${index}`,
214
+ ]);
211
215
 
212
216
  const handleFocus = (focusable, mouse) => {
213
217
  const focusFn = onFocus || noop;
@@ -263,7 +267,7 @@ export class CellComponent extends React.Component<Props> {
263
267
  item,
264
268
  focused: this.isCellFocused(focused),
265
269
  scrollTo: this.scrollToIndex(event),
266
- behavior
270
+ behavior,
267
271
  }}
268
272
  />
269
273
  );
@@ -35,18 +35,18 @@ const useCellFocusedState = (groupId: string, id: string) => {
35
35
  };
36
36
 
37
37
  type Props = {
38
- item: ZappEntry,
39
- CellRenderer: React.ComponentType<{ item: Object, state: string }>,
40
- id: string,
41
- groupId: string,
42
- onFocus: Function,
43
- index: number,
44
- scrollTo: Function,
45
- preferredFocus?: boolean,
46
- skipFocusManagerRegistration?: boolean,
47
- isFocusable?: boolean,
48
- behavior: Behavior,
49
- selected?: boolean,
38
+ item: ZappEntry;
39
+ CellRenderer: React.ComponentType<{ item: Object; state: string }>;
40
+ id: string;
41
+ groupId: string;
42
+ onFocus: Function;
43
+ index: number;
44
+ scrollTo: Function;
45
+ preferredFocus?: boolean;
46
+ skipFocusManagerRegistration?: boolean;
47
+ isFocusable?: boolean;
48
+ behavior: Behavior;
49
+ selected?: boolean;
50
50
  };
51
51
 
52
52
  export function CellWithFocusable(props: Props) {
@@ -62,11 +62,18 @@ export function CellWithFocusable(props: Props) {
62
62
  skipFocusManagerRegistration,
63
63
  isFocusable,
64
64
  behavior,
65
- selected
65
+ selected,
66
66
  } = props;
67
67
 
68
68
  const isFocused = useCellFocusedState(groupId, id);
69
- const state = useCellState({ id: item.id, behavior, focused: isFocused, selected });
69
+
70
+ const state = useCellState({
71
+ id: item.id,
72
+ behavior,
73
+ focused: isFocused,
74
+ selected,
75
+ });
76
+
70
77
  const [focusedButtonId, setFocusedButtonId] = React.useState(undefined);
71
78
 
72
79
  // for horizontal scrolling
@@ -249,7 +249,7 @@ export class TvOSCellComponent extends React.Component<Props, State> {
249
249
  hasReceivedFocus,
250
250
  groupId,
251
251
  isFocusable,
252
- behavior
252
+ behavior,
253
253
  } = this.props;
254
254
 
255
255
  const { id } = item;
@@ -310,7 +310,7 @@ export class TvOSCellComponent extends React.Component<Props, State> {
310
310
  item,
311
311
  focused: this.props.focused || selectedCell || focused,
312
312
  scrollTo: this.scrollTo,
313
- behavior
313
+ behavior,
314
314
  }}
315
315
  />
316
316
  );
@@ -3,7 +3,6 @@ import * as R from "ramda";
3
3
  import {
4
4
  View,
5
5
  StyleSheet,
6
- Image,
7
6
  Text,
8
7
  ViewStyle,
9
8
  TextStyle,
@@ -13,6 +12,7 @@ import {
13
12
  import { defaultAsset } from "./defaultAsset";
14
13
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
15
14
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
15
+ import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
16
16
  import { Button } from "./Button";
17
17
 
18
18
  type ButtonProp = {
@@ -3,12 +3,12 @@ import { Platform } from "react-native";
3
3
  import { useCellState } from "../MasterCell/utils";
4
4
 
5
5
  type Props = {
6
- index: number,
7
- item: ZappEntry,
8
- focused: boolean,
9
- scrollTo?: (index: number) => void,
10
- CellRenderer: React.ComponentType<{ item: Object, state: string }>,
11
- behavior: Behavior
6
+ index: number;
7
+ item: ZappEntry;
8
+ focused: boolean;
9
+ scrollTo?: (index: number) => void;
10
+ CellRenderer: React.ComponentType<{ item: Object; state: string }>;
11
+ behavior: Behavior;
12
12
  };
13
13
 
14
14
  const isAndroid = Platform.OS === "android";
@@ -20,7 +20,7 @@ export function FocusableCell(props: Props) {
20
20
  CellRenderer,
21
21
  focused,
22
22
  scrollTo = null,
23
- behavior
23
+ behavior,
24
24
  } = props;
25
25
 
26
26
  const state = useCellState({ id: item.id, behavior, focused });
@@ -26,6 +26,7 @@ type Props = {
26
26
  nextFocusLeft?: any;
27
27
  nextFocusRight?: any;
28
28
  onLayout?: any;
29
+ excludeFromFocusSearching?: boolean;
29
30
  };
30
31
 
31
32
  export class FocusableGroup extends BaseFocusable<Props> {
@@ -1,3 +1 @@
1
- import { HookRenderer as HookRendererComponent } from "./HookRenderer";
2
-
3
- export const HookRenderer = HookRendererComponent;
1
+ export { HookRenderer } from "./HookRenderer";
@@ -3,32 +3,10 @@ import FastImage, {
3
3
  FastImageProps,
4
4
  } from "@applicaster/react-native-fast-image";
5
5
 
6
- export const QBImage = (props: FastImageProps) => {
7
- const source = React.useMemo(() => {
8
- if (typeof props.source === "number") {
9
- return props.source;
10
- }
6
+ import { useImageSource } from "./utils";
11
7
 
12
- return {
13
- uri: props.source.uri,
14
- headers: props.source.headers,
15
- priority: props.source.priority,
16
- cache: props.source.cache,
17
- maxWidth: props.source.maxWidth,
18
- maxHeight: props.source.maxHeight,
19
- };
20
- }, [
21
- ...(typeof props.source === "object"
22
- ? [
23
- props.source?.uri,
24
- props.source?.priority,
25
- props.source?.cache,
26
- props.source?.headers,
27
- props.source?.maxWidth,
28
- props.source?.maxHeight,
29
- ]
30
- : []),
31
- ]);
8
+ export const QBImage = (props: FastImageProps) => {
9
+ const source = useImageSource(props.source);
32
10
 
33
11
  return <FastImage {...props} source={source} />;
34
12
  };
@@ -1,8 +1,24 @@
1
- import { Image, ImageProps, ImageStyle, StyleProp } from "react-native";
1
+ import React from "react";
2
+ import {
3
+ Image,
4
+ ImageBackground,
5
+ ImageProps,
6
+ ImageStyle,
7
+ StyleProp,
8
+ } from "react-native";
2
9
 
3
10
  export interface Props extends ImageProps {
4
11
  style?: StyleProp<ImageStyle> | undefined;
5
12
  }
6
13
 
7
14
  // TODO - React.FC<Props> QBImage should process incoming url and apply any transformations if needed
8
- export const QBImage = Image;
15
+ export const QBImage = ({
16
+ children,
17
+ ...props
18
+ }: Props & { children?: React.ReactNode }) => {
19
+ if (children) {
20
+ return <ImageBackground {...props}>{children}</ImageBackground>;
21
+ }
22
+
23
+ return <Image {...props} />;
24
+ };
@@ -0,0 +1,39 @@
1
+ import FastImage, { Source } from "@applicaster/react-native-fast-image";
2
+ import { useMemo } from "react";
3
+ import { ImageRequireSource } from "react-native";
4
+
5
+ function getSourceCache(cache: Source["cache"]) {
6
+ return cache || FastImage.cacheControl.web;
7
+ }
8
+
9
+ export const useImageSource = (source?: Source | ImageRequireSource) => {
10
+ return useMemo(() => {
11
+ if (source) {
12
+ if (typeof source === "number") {
13
+ return source;
14
+ }
15
+
16
+ return {
17
+ uri: source.uri,
18
+ headers: source.headers,
19
+ priority: source.priority,
20
+ cache: getSourceCache(source?.cache),
21
+ maxWidth: source.maxWidth,
22
+ maxHeight: source.maxHeight,
23
+ };
24
+ }
25
+
26
+ return undefined;
27
+ }, [
28
+ ...(typeof source === "object"
29
+ ? [
30
+ source?.uri,
31
+ source?.priority,
32
+ source?.cache,
33
+ source?.headers,
34
+ source?.maxWidth,
35
+ source?.maxHeight,
36
+ ]
37
+ : []),
38
+ ]);
39
+ };
@@ -60,7 +60,7 @@ export function ScreenLayoutContextProvider(props: {
60
60
  initialState
61
61
  );
62
62
 
63
- const screeenLayoutValue = React.useMemo(
63
+ const screenLayoutValue = React.useMemo(
64
64
  () => ({
65
65
  ...screenLayout,
66
66
  setScreenLayout,
@@ -69,7 +69,7 @@ export function ScreenLayoutContextProvider(props: {
69
69
  );
70
70
 
71
71
  return (
72
- <ScreenLayoutContext.Provider value={screeenLayoutValue}>
72
+ <ScreenLayoutContext.Provider value={screenLayoutValue}>
73
73
  {props.children}
74
74
  </ScreenLayoutContext.Provider>
75
75
  );
@@ -14,6 +14,46 @@ exports[`Layout TV renders 1`] = `
14
14
  <View
15
15
  backgroundImageUrl="http://applicaster.com"
16
16
  testID="background-component"
17
- />
17
+ >
18
+ <View
19
+ style={
20
+ Array [
21
+ Object {
22
+ "position": "absolute",
23
+ "top": 0,
24
+ "width": 1920,
25
+ "zIndex": 10,
26
+ },
27
+ Object {
28
+ "maxHeight": 10,
29
+ },
30
+ ]
31
+ }
32
+ testID="nav-bar-container"
33
+ >
34
+ <View
35
+ isVisible={true}
36
+ menuPlugin={
37
+ Object {
38
+ "module": [Function],
39
+ "name": "resolved-menu-plug",
40
+ "type": "menu",
41
+ }
42
+ }
43
+ navigationProps={
44
+ Object {
45
+ "assets": undefined,
46
+ "home": undefined,
47
+ "nav_items": undefined,
48
+ "rules": undefined,
49
+ "selected": undefined,
50
+ "styles": undefined,
51
+ "title": "",
52
+ }
53
+ }
54
+ testID="nav-bar-component"
55
+ />
56
+ </View>
57
+ </View>
18
58
  </View>
19
59
  `;
@@ -12,19 +12,7 @@ jest.mock("@applicaster/quick-brick-core/App/ActionSetters", () => ({
12
12
  useRiverInitialState: () => ({}),
13
13
  }));
14
14
 
15
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
16
- useCurrentScreenData: jest.fn(() => ({
17
- navigations: [
18
- {
19
- category: "menu",
20
- navigation_type: "menu",
21
- styles: {
22
- hide_secondary_level_menu: false,
23
- },
24
- },
25
- ],
26
- })),
27
- }));
15
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen");
28
16
 
29
17
  const Layout = require("..").default;
30
18
 
@@ -1,11 +1,15 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
+ import { View, StyleSheet } from "react-native";
3
4
  import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
4
5
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
5
6
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
+ import { useNavbarState } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
8
+ import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
6
9
 
7
10
  import {
8
11
  getNavigationProps,
12
+ getPathAttributes,
9
13
  resolveNavigationPlugin,
10
14
  routeIsPlayerScreen,
11
15
  } from "@applicaster/zapp-react-native-utils/navigationUtils";
@@ -17,9 +21,23 @@ import { ScreenContainer } from "./ScreenContainer";
17
21
  import { TransitionOverlay } from "./TransitionOverlay";
18
22
 
19
23
  import { ScreenLayoutContextProvider } from "./ScreenLayoutContextProvider";
20
- import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
21
24
  import { PathnameContext } from "../../../Contexts/PathnameContext";
22
- import { useNavbarState } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
25
+
26
+ import { FocusableGroup } from "../../FocusableGroup";
27
+ import { Screen } from "../../Screen/TV/index.web";
28
+
29
+ const styles = StyleSheet.create({
30
+ container: {
31
+ position: "absolute",
32
+ width: 1920,
33
+ height: 1080,
34
+ backgroundColor: "black",
35
+ // opacity: 0.5,
36
+
37
+ borderWidth: 4,
38
+ borderColor: "magenta",
39
+ },
40
+ });
23
41
 
24
42
  type Components = {
25
43
  NavBar: React.ComponentType<any>;
@@ -27,16 +45,65 @@ type Components = {
27
45
  };
28
46
 
29
47
  type ComponentsExtraProps = {
30
- Background?: Record<string, any>;
31
48
  NavBar?: Record<string, any>;
49
+ Background?: Record<string, any>;
32
50
  };
33
51
 
34
52
  type Props = {
35
53
  Components: Components;
36
- ComponentsExtraProps?: ComponentsExtraProps;
37
- children: React.ReactNode;
54
+ componentsExtraProps?: ComponentsExtraProps;
55
+ };
56
+
57
+ const StackNavigator = ({ Components }: Props) => {
58
+ // @ts-ignore
59
+ const { mainStack, currentRoute } = useNavigation();
60
+
61
+ console.log("DEBUG", { mainStack });
62
+
63
+ const renderScreen = ({ route, action, key, state }) => {
64
+ const pathAttributes = getPathAttributes({ pathname: route });
65
+
66
+ if (R.isEmpty(pathAttributes)) {
67
+ return null;
68
+ }
69
+
70
+ console.log("SCENE:", { route, state, key, action });
71
+
72
+ const id = `${route}`;
73
+
74
+ return (
75
+ <FocusableGroup
76
+ id={id}
77
+ preferredFocus={route === currentRoute}
78
+ excludeFromFocusSearching
79
+ key={id}
80
+ >
81
+ <View style={styles.container}>
82
+ <PathnameContext.Provider value={route}>
83
+ <Screen route={route} Components={Components} />
84
+ </PathnameContext.Provider>
85
+ </View>
86
+ </FocusableGroup>
87
+ );
88
+ };
89
+
90
+ return mainStack.map(renderScreen);
91
+ };
92
+
93
+ const TransitionerTV = ({ componentsExtraProps, Components }: Props) => {
94
+ return (
95
+ <>
96
+ <ScreenLayoutContextProvider>
97
+ <StackNavigator
98
+ componentsExtraProps={componentsExtraProps}
99
+ Components={Components}
100
+ />
101
+ </ScreenLayoutContextProvider>
102
+ </>
103
+ );
38
104
  };
39
105
 
106
+ // @ts-ignore
40
107
  const Layout = ({ Components, ComponentsExtraProps, children }: Props) => {
41
108
  const navigator = useNavigation();
42
109
  const theme = useTheme();
@@ -97,33 +164,57 @@ const Layout = ({ Components, ComponentsExtraProps, children }: Props) => {
97
164
  return null;
98
165
  }
99
166
 
167
+ console.log("DEBUG", "TV Layout");
168
+
100
169
  return (
101
- <LayoutContainer>
102
- <ScreenLayoutContextProvider>
103
- <Components.Background
104
- backgroundColor={backgroundColor}
105
- backgroundImageUrl={backgroundImageUrl}
106
- {...R.omit(["ref"])(ComponentsExtraProps?.Background)}
107
- >
108
- {!isTVOS ? navBarContainer : null}
109
- {navBarReady ? (
110
- /**
170
+ // @ts-ignore
171
+ <TransitionerTV
172
+ componentsExtraProps={ComponentsExtraProps}
173
+ Components={Components}
174
+ >
175
+ <LayoutContainer>
176
+ <ScreenLayoutContextProvider>
177
+ <Components.Background
178
+ backgroundColor={backgroundColor}
179
+ backgroundImageUrl={backgroundImageUrl}
180
+ {...R.omit(["ref"])(ComponentsExtraProps?.Background)}
181
+ >
182
+ {!isTVOS ? navBarContainer : null}
183
+ {navBarReady ? (
184
+ /**
111
185
  Need to re-mount this component on each navigation event
112
186
  It's a hack to work around the fact that we don't support
113
187
  correct screen rendering in our navigator (shared currentRoute state)
114
188
  **/
115
- <PathnameContext.Provider value={navigator.currentRoute}>
116
- <ScreenContainer>{children}</ScreenContainer>
117
- </PathnameContext.Provider>
118
- ) : null}
119
- {/** Render NavBarContainer over ScreenContainer to let tvOS native
189
+ <PathnameContext.Provider value={navigator.currentRoute}>
190
+ <ScreenContainer>{children}</ScreenContainer>
191
+ </PathnameContext.Provider>
192
+ ) : null}
193
+ {/** Render NavBarContainer over ScreenContainer to let tvOS native
120
194
  focus manager works correctly to focus nav bar items first **/}
121
- {!isTVOS ? navBarContainer : null}
122
- <TransitionOverlay />
123
- </Components.Background>
124
- </ScreenLayoutContextProvider>
125
- </LayoutContainer>
195
+ {isTVOS ? navBarContainer : null}
196
+ <TransitionOverlay />
197
+ </Components.Background>
198
+ </ScreenLayoutContextProvider>
199
+ </LayoutContainer>
200
+ </TransitionerTV>
126
201
  );
127
202
  };
128
203
 
129
204
  export default Layout;
205
+
206
+ /// Transitioner:
207
+ /*
208
+ Component: Transitioner
209
+ Responsibilities:
210
+ - Transitione between screens.
211
+ Tehere is no transition on Tv so no need for transitioner.
212
+
213
+ Component: StackNavigator
214
+ Responsibilities:
215
+ - Render the screens in the stack.
216
+ - Render the NavBar? I guess navbar should be part of the screen
217
+ - Render the screen.
218
+
219
+ Q
220
+ */
@@ -1,11 +1,11 @@
1
1
  import * as React from "react";
2
2
  import { ImageStyle, View } from "react-native";
3
3
  import * as R from "ramda";
4
- import ImageComponent from "@applicaster/zapp-react-native-ui-components/Components/FastImage";
4
+ import { QBImage } from "@applicaster/zapp-react-native-ui-components/Components/Image";
5
5
  import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
6
- import { toFiniteNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
7
6
 
8
7
  import { useImageSourceWithDefault, useCheckFailedUri } from "./hooks";
8
+ import useImageSize from "./hooks/useImageSize";
9
9
 
10
10
  type Source = {
11
11
  uri: string;
@@ -19,32 +19,13 @@ type Props = Record<string, any> & {
19
19
  withDimensions: (source: Source) => Source;
20
20
  };
21
21
 
22
- const MemoizedImage = React.memo(ImageComponent, R.equals);
23
-
24
- const normalize = Math.ceil;
22
+ const MemoizedImage = React.memo(QBImage, R.equals);
25
23
 
26
24
  const isValidExternalSource = (source): boolean =>
27
25
  source?.uri?.startsWith("http") ||
28
26
  source?.uri?.startsWith("data:image") ||
29
27
  source?.uri?.startsWith("file://");
30
28
 
31
- const isValidSize = (size: unknown): boolean =>
32
- toFiniteNumberWithDefault(0, size) > 0;
33
-
34
- const getSourceWithMaxSizes = (applyMaxSize, style, source) => {
35
- const { width, height } = style;
36
-
37
- if (!applyMaxSize || !isValidSize(width) || !isValidSize(height)) {
38
- return source;
39
- }
40
-
41
- return {
42
- ...source,
43
- maxWidth: normalize(width),
44
- maxHeight: normalize(height),
45
- };
46
- };
47
-
48
29
  export default function Image({
49
30
  style,
50
31
  uri,
@@ -82,10 +63,12 @@ export default function Image({
82
63
 
83
64
  const applyMaxSize = toBooleanWithDefaultFalse(otherProps?.applyMaxSize);
84
65
 
85
- const sourceWithMaxSizes = React.useMemo(
86
- () => withDimensions(getSourceWithMaxSizes(applyMaxSize, style, source)),
87
- [applyMaxSize, style?.height, style?.width, source?.uri, withDimensions]
88
- );
66
+ const sourceWithMaxSizes = useImageSize({
67
+ style,
68
+ applyMaxSize,
69
+ source,
70
+ withDimensions,
71
+ });
89
72
 
90
73
  if (isFailedToLoadUri(uri) || !isValidExternalSource(source)) {
91
74
  return <View style={style} />;
@@ -0,0 +1,65 @@
1
+ import * as React from "react";
2
+ import { ImageStyle } from "react-native";
3
+ import { toFiniteNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
4
+
5
+ type Source = {
6
+ uri: string;
7
+ };
8
+
9
+ type Props = Record<string, any> & {
10
+ style: ImageStyle;
11
+ uri?: string;
12
+ placeholderImage?: string;
13
+ entry?: ZappEntry | ZappFeed;
14
+ withDimensions: (source: Source) => Source;
15
+ };
16
+
17
+ const normalize = Math.ceil;
18
+
19
+ const isValidSize = (size: unknown): boolean =>
20
+ toFiniteNumberWithDefault(0, size) > 0;
21
+
22
+ type WidthDimensions = { aspectRatio?: number; width: number; height?: number };
23
+ type HeightDimensions = {
24
+ aspectRatio?: number;
25
+ width?: number;
26
+ height: number;
27
+ };
28
+ type FullDimensions = { aspectRatio?: number; width: number; height: number };
29
+ type Dimensions = WidthDimensions | HeightDimensions | FullDimensions;
30
+
31
+ const calculateDimensions = ({ aspectRatio, width, height }: Dimensions) => {
32
+ if (aspectRatio) {
33
+ return {
34
+ width: width || height * aspectRatio,
35
+ height: height || width / aspectRatio,
36
+ };
37
+ }
38
+
39
+ return { width, height };
40
+ };
41
+
42
+ const getSourceWithMaxSizes = (applyMaxSize, style, source) => {
43
+ const { width, height } = calculateDimensions(style);
44
+
45
+ if (!applyMaxSize || !isValidSize(width) || !isValidSize(height)) {
46
+ return source;
47
+ }
48
+
49
+ return {
50
+ ...source,
51
+ maxWidth: normalize(width),
52
+ maxHeight: normalize(height),
53
+ };
54
+ };
55
+
56
+ export default function useImageSize(props: Props) {
57
+ const { style, applyMaxSize, source, withDimensions } = props;
58
+
59
+ const sourceWithMaxSizes = React.useMemo(
60
+ () => withDimensions(getSourceWithMaxSizes(applyMaxSize, style, source)),
61
+ [applyMaxSize, style?.height, style?.width, source?.uri, withDimensions]
62
+ );
63
+
64
+ return sourceWithMaxSizes;
65
+ }