Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TW-1429 Add blured background for banner ads #1077

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ PODS:
- glog
- react-native-biometrics (3.0.1):
- React-Core
- react-native-blur (4.4.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- react-native-camera (4.2.1):
- React-Core
- react-native-camera/RCT (= 4.2.1)
Expand Down Expand Up @@ -800,6 +803,7 @@ DEPENDENCIES:
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-biometrics (from `../node_modules/react-native-biometrics`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-camera (from `../node_modules/react-native-camera`)
- react-native-cloud-fs (from `../node_modules/react-native-cloud-fs`)
- react-native-config (from `../node_modules/react-native-config`)
Expand Down Expand Up @@ -946,6 +950,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-biometrics:
:path: "../node_modules/react-native-biometrics"
react-native-blur:
:path: "../node_modules/@react-native-community/blur"
react-native-camera:
:path: "../node_modules/react-native-camera"
react-native-cloud-fs:
Expand Down Expand Up @@ -1121,6 +1127,7 @@ SPEC CHECKSUMS:
React-jsinspector: 8e291ed0ab371314de269001d6b9b25db6aabf42
React-logger: d4010de0b0564e63637ad08373bc73b5d919974b
react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc
react-native-blur: 507cf3dd4434eb9d5ca5f183e49d8bcccdd66826
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
react-native-cloud-fs: cd21282c5cecaf285225e447dd7f3cf2f9015496
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@jitsu/sdk-js": "madfish-solutions/jitsu-js#87c49de334f5747952f7beda573b95ceb5d86903",
"@react-native-async-storage/async-storage": "^1.21.0",
"@react-native-clipboard/clipboard": "^1.11.1",
"@react-native-community/blur": "^4.4.0",
"@react-native-community/masked-view": "^0.1.11",
"@react-native-community/netinfo": "^11.2.1",
"@react-native-community/push-notification-ios": "^1.11.0",
Expand Down
87 changes: 84 additions & 3 deletions src/components/image-promotion-view/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, memo, PropsWithChildren } from 'react';
import { View } from 'react-native';
import { BlurView } from '@react-native-community/blur';
import React, { useCallback, memo, PropsWithChildren, useState, useMemo } from 'react';
import { LayoutChangeEvent, View } from 'react-native';

import { Bage } from 'src/components/bage/bage';
import { Icon } from 'src/components/icon/icon';
Expand All @@ -8,32 +9,112 @@ import { TouchableWithAnalytics } from 'src/components/touchable-with-analytics'
import { TestIdProps } from 'src/interfaces/test-id.props';
import { formatSize } from 'src/styles/format-size';
import { useColors } from 'src/styles/use-colors';
import { isDefined } from 'src/utils/is-defined';
import { openUrl } from 'src/utils/linking';

import { SimplePlayer } from '../simple-player';
import { UnknownTypeImage } from '../unknown-type-image';

import { PromotionItemSelectors } from './selectors';
import { useImagePromotionViewStyles } from './styles';

export interface BackgroundAsset {
type: 'image' | 'video';
uri: string;
width: number;
height: number;
}

interface ImagePromotionViewProps extends TestIdProps {
href: string;
isVisible: boolean;
shouldShowCloseButton: boolean;
shouldShowAdBage: boolean;
backgroundAsset?: BackgroundAsset;
onClose: EmptyFn;
}

export const ImagePromotionView = memo<PropsWithChildren<ImagePromotionViewProps>>(
({ children, href, isVisible, shouldShowCloseButton, shouldShowAdBage, onClose, ...testIDProps }) => {
({
children,
href,
isVisible,
shouldShowCloseButton,
shouldShowAdBage,
backgroundAsset,
onClose,
...testIDProps
}) => {
const colors = useColors();
const styles = useImagePromotionViewStyles();
const [layoutSize, setLayoutSize] = useState<{ width: number; height: number }>();

const openLink = useCallback(() => href && openUrl(href), [href]);
const handleLayout = useCallback((event: LayoutChangeEvent) => {
event.persist();
const { width, height } = event.nativeEvent.layout;
setLayoutSize({ width, height });
}, []);

const backgroundSize = useMemo(() => {
if (!backgroundAsset) {
return undefined;
}

if (!layoutSize) {
return { width: backgroundAsset.width, height: backgroundAsset.height };
}

const { width, height } = layoutSize;

if (backgroundAsset.height === 0) {
return { width, height: 0 };
}

const aspectRatio = backgroundAsset.width / backgroundAsset.height;
const layoutAspectRatio = width / height;

if (aspectRatio === 0) {
return { width: 0, height };
}

if (aspectRatio < layoutAspectRatio) {
return { width, height: width / aspectRatio };
}

return { width: height * aspectRatio, height };
}, [backgroundAsset, layoutSize]);

return (
<TouchableWithAnalytics
{...testIDProps}
style={[styles.container, !isVisible && styles.invisible]}
onPress={openLink}
onLayout={handleLayout}
>
{isDefined(backgroundAsset) && (
<>
<View style={styles.centeredWithOverflowWrapper}>
{backgroundAsset.type === 'image' ? (
<UnknownTypeImage
width={backgroundSize?.width ?? backgroundAsset.width}
height={backgroundSize?.height ?? backgroundAsset.height}
uri={backgroundAsset.uri}
/>
) : (
<SimplePlayer
uri={backgroundAsset.uri}
width={backgroundSize?.width ?? backgroundAsset.width}
height={backgroundSize?.height ?? backgroundAsset.height}
isVideo
shouldShowLoader={false}
/>
)}
</View>
<BlurView style={styles.blurView} blurType="light" blurAmount={formatSize(10)} />
</>
)}

{children}

{shouldShowAdBage && (
Expand Down
21 changes: 20 additions & 1 deletion src/components/image-promotion-view/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,26 @@ export const useImagePromotionViewStyles = createUseStylesMemoized(({ colors })
justifyContent: 'center',
alignItems: 'center',
height: formatSize(112),
position: 'relative'
position: 'relative',
overflow: 'hidden',
borderRadius: formatSize(10)
},
blurView: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0
},
centeredWithOverflowWrapper: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden'
},
invisible: {
display: 'none'
Expand Down
16 changes: 15 additions & 1 deletion src/components/optimal-promotion/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import FastImage from 'react-native-fast-image';
import useSWR from 'swr';

Expand Down Expand Up @@ -68,6 +68,19 @@ export const OptimalPromotion: FC<SingleProviderPromotionProps & SelfRefreshingP

const handleTextPromotionReady = useCallback(() => setAdViewIsReady(true), []);

const backgroundAsset = useMemo(
() =>
isImageAd && isDefined(promo) && isDefined(promo.image)
? {
type: 'image' as const,
uri: promo.image,
width: 321,
height: 101
}
: undefined,
[promo, isImageAd]
);

if (isDefined(error) || promotionIsEmpty || isImageBroken || !promo) {
return null;
}
Expand All @@ -83,6 +96,7 @@ export const OptimalPromotion: FC<SingleProviderPromotionProps & SelfRefreshingP
href={href}
isVisible={isVisible}
shouldShowAdBage
backgroundAsset={backgroundAsset}
{...testIDProps}
>
<FastImage style={styles.bannerImage} source={{ uri: imageSrc }} onError={onImageError} />
Expand Down
131 changes: 70 additions & 61 deletions src/components/simple-player/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect, useState } from 'react';
import { StyleProp, ViewStyle } from 'react-native';
import { LoadError, OnLoadData as NativeOnLoadData } from 'react-native-video';
import VideoPlayer from 'react-native-video-controls';
import WebView from 'react-native-webview';
import { WebView } from 'react-native-webview';

import { emptyFn } from 'src/config/general';
import { useAppStateStatus } from 'src/hooks/use-app-state-status.hook';
Expand All @@ -13,79 +13,88 @@ import { ActivityIndicator } from '../activity-indicator';

interface Props {
uri: string;
size: number;
width: number;
height: number;
style?: StyleProp<ViewStyle>;
onError?: SyncFn<LoadError>;
onLoad?: EmptyFn;
isVideo?: boolean;
shouldShowLoader?: boolean;
}

const BUFFER_DURATION = 8000;

export const SimplePlayer = memo<Props>(({ uri, size, style, onError = emptyFn, isVideo = false }) => {
const atBootsplash = useAtBootsplash();
const { isLocked } = useAppLock();
export const SimplePlayer = memo<Props>(
({ uri, width, height, style, onError = emptyFn, onLoad = emptyFn, isVideo = false, shouldShowLoader = true }) => {
const atBootsplash = useAtBootsplash();
const { isLocked } = useAppLock();

const [shouldUseNativePlayer, setShouldUseNativePlayer] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [appIsActive, setAppIsActive] = useState(true);
const [shouldUseNativePlayer, setShouldUseNativePlayer] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [appIsActive, setAppIsActive] = useState(true);

const onAppActiveState = useCallback(() => setAppIsActive(true), []);
const onAppOtherState = useCallback(() => setAppIsActive(false), []);
useAppStateStatus({ onAppActiveState, onAppInactiveState: onAppOtherState, onAppBackgroundState: onAppOtherState });
const onAppActiveState = useCallback(() => setAppIsActive(true), []);
const onAppOtherState = useCallback(() => setAppIsActive(false), []);
useAppStateStatus({ onAppActiveState, onAppInactiveState: onAppOtherState, onAppBackgroundState: onAppOtherState });

useEffect(() => setShouldUseNativePlayer(true), [uri]);
useEffect(() => setShouldUseNativePlayer(true), [uri]);

const handleNativePlayerLoad = useCallback(
(data: NativeOnLoadData) => {
const { width, height } = data.naturalSize;
const handleNativePlayerLoad = useCallback(
(data: NativeOnLoadData) => {
const { width, height } = data.naturalSize;

if (width === 0 && height === 0 && isVideo) {
setShouldUseNativePlayer(false);
} else {
setIsLoading(false);
}
},
[isVideo]
);
if (width === 0 && height === 0 && isVideo) {
setShouldUseNativePlayer(false);
} else {
setIsLoading(false);
onLoad();
}
},
[isVideo, onLoad]
);

const nativePlayerLoadStart = useCallback(() => setIsLoading(true), []);
const handleWebViewLoad = useCallback(() => setIsLoading(false), []);
const nativePlayerLoadStart = useCallback(() => setIsLoading(true), []);
const handleWebViewLoad = useCallback(() => {
onLoad();
setIsLoading(false);
}, [onLoad]);

const handleWebViewError = useCallback(() => {
onError({ error: { '': '', errorString: 'Failed to load video, it may be invalid or unsupported' } });
}, [onError]);
const handleWebViewError = useCallback(() => {
onError({ error: { '': '', errorString: 'Failed to load video, it may be invalid or unsupported' } });
}, [onError]);

return (
<>
{shouldUseNativePlayer ? (
<VideoPlayer
repeat
source={{ uri }}
style={[{ width: size, height: size }, style]}
paused={atBootsplash || isLocked || !appIsActive}
resizeMode="contain"
ignoreSilentSwitch="ignore"
bufferConfig={{
bufferForPlaybackMs: BUFFER_DURATION,
bufferForPlaybackAfterRebufferMs: BUFFER_DURATION * 2
}}
onError={onError}
onLoadStart={nativePlayerLoadStart}
onLoad={handleNativePlayerLoad}
disableFullscreen
disableBack
disableVolume
/>
) : (
<WebView
source={{ uri }}
style={[{ width: size, height: size }, style]}
onError={handleWebViewError}
onLoadEnd={handleWebViewLoad}
/>
)}
return (
<>
{shouldUseNativePlayer ? (
<VideoPlayer
repeat
source={{ uri }}
style={[{ width, height }, style]}
paused={atBootsplash || isLocked || !appIsActive}
resizeMode="contain"
ignoreSilentSwitch="ignore"
bufferConfig={{
bufferForPlaybackMs: BUFFER_DURATION,
bufferForPlaybackAfterRebufferMs: BUFFER_DURATION * 2
}}
onError={onError}
onLoadStart={nativePlayerLoadStart}
onLoad={handleNativePlayerLoad}
disableFullscreen
disableBack
disableVolume
/>
) : (
<WebView
source={{ uri }}
style={[{ width, height }, style]}
onError={handleWebViewError}
onLoadEnd={handleWebViewLoad}
/>
)}

{isLoading && !shouldUseNativePlayer ? <ActivityIndicator size="large" /> : null}
</>
);
});
{isLoading && !shouldUseNativePlayer && shouldShowLoader ? <ActivityIndicator size="large" /> : null}
</>
);
}
);
Loading
Loading