Skip to content
Draft
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
2 changes: 1 addition & 1 deletion example/src/components/Embedded/Embedded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const Embedded = () => {
IterableEmbeddedMessage[]
>([]);
const [selectedViewType, setSelectedViewType] =
useState<IterableEmbeddedViewType>(IterableEmbeddedViewType.Banner);
useState<IterableEmbeddedViewType>(IterableEmbeddedViewType.Notification);

const syncEmbeddedMessages = useCallback(() => {
Iterable.embeddedManager.syncMessages();
Expand Down
1 change: 1 addition & 0 deletions src/core/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './useAppStateListener';
export * from './useDeviceOrientation';
export * from './useComponentVisibility';
156 changes: 156 additions & 0 deletions src/core/hooks/useComponentVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
View,
Dimensions,
AppState,
type LayoutChangeEvent,
} from 'react-native';
import { useRef, useState, useCallback, useEffect } from 'react';

interface UseVisibilityOptions {
threshold?: number; // Percentage of component that must be visible (0-1)
checkOnAppState?: boolean; // Whether to check app state (active/background)
checkInterval?: number; // How often to check visibility in ms (0 = only on layout changes)
enablePeriodicCheck?: boolean; // Whether to enable periodic checking for navigation changes
}

interface LayoutInfo {
x: number;
y: number;
width: number;
height: number;
}

export const useComponentVisibility = (options: UseVisibilityOptions = {}) => {
const {
threshold = 0.1,
checkOnAppState = true,
checkInterval = 0, // Default to only check on layout changes
enablePeriodicCheck = true, // Enable periodic checking by default for navigation
} = options;

const [isVisible, setIsVisible] = useState(false);
const [appState, setAppState] = useState(AppState.currentState);
const componentRef = useRef<View>(null);
const [layout, setLayout] = useState<LayoutInfo>({
x: 0,
y: 0,
width: 0,
height: 0,
});
const intervalRef = useRef<NodeJS.Timeout | null>(null);

// Handle layout changes
const handleLayout = useCallback((event: LayoutChangeEvent) => {
const { x, y, width, height } = event.nativeEvent.layout;
setLayout({ x, y, width, height });
}, []);

// Check if component is visible on screen using measure
const checkVisibility = useCallback((): Promise<boolean> => {
if (!componentRef.current || layout.width === 0 || layout.height === 0) {
return Promise.resolve(false);
}

return new Promise<boolean>((resolve) => {
componentRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
const screenHeight = Dimensions.get('window').height;
const screenWidth = Dimensions.get('window').width;

// Calculate visible area using page coordinates
const visibleTop = Math.max(0, pageY);
const visibleBottom = Math.min(screenHeight, pageY + height);
const visibleLeft = Math.max(0, pageX);
const visibleRight = Math.min(screenWidth, pageX + width);

const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibleWidth = Math.max(0, visibleRight - visibleLeft);

const visibleArea = visibleHeight * visibleWidth;
const totalArea = height * width;
const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;

resolve(visibilityRatio >= threshold);
});
}).catch(() => {
// Fallback to layout-based calculation if measure fails
const screenHeight = Dimensions.get('window').height;
const screenWidth = Dimensions.get('window').width;

const visibleTop = Math.max(0, layout.y);
const visibleBottom = Math.min(screenHeight, layout.y + layout.height);
const visibleLeft = Math.max(0, layout.x);
const visibleRight = Math.min(screenWidth, layout.x + layout.width);

const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibleWidth = Math.max(0, visibleRight - visibleLeft);

const visibleArea = visibleHeight * visibleWidth;
const totalArea = layout.height * layout.width;
const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;

return visibilityRatio >= threshold;
});
}, [layout, threshold]);

// Update visibility state
const updateVisibility = useCallback(async () => {
const isComponentVisible = await checkVisibility();
const isAppActive = !checkOnAppState || appState === 'active';
const newVisibility = isComponentVisible && isAppActive;

setIsVisible(newVisibility);
}, [checkVisibility, appState, checkOnAppState]);

// Update visibility when layout or app state changes
useEffect(() => {
updateVisibility();
}, [updateVisibility]);

// Set up periodic checking for navigation changes
useEffect(() => {
const interval =
checkInterval > 0 ? checkInterval : enablePeriodicCheck ? 500 : 0;

if (interval > 0) {
intervalRef.current = setInterval(updateVisibility, interval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}
return undefined;
}, [checkInterval, enablePeriodicCheck, updateVisibility]);

// Listen to app state changes
useEffect(() => {
if (!checkOnAppState) return;

const handleAppStateChange = (nextAppState: string) => {
setAppState(nextAppState as typeof AppState.currentState);
};

const subscription = AppState.addEventListener(
'change',
handleAppStateChange
);
return () => subscription?.remove();
}, [checkOnAppState]);

// Clean up interval on unmount
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);

return {
isVisible,
componentRef,
handleLayout,
appState,
layout,
};
Comment on lines +23 to +155
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 issues:

1. Function with high complexity (count = 36): useComponentVisibility [qlty:function-complexity]


2. Function with many returns (count = 8): useComponentVisibility [qlty:return-statements]

};
22 changes: 0 additions & 22 deletions src/embedded/components/IterableEmbeddedNotification.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
body: {
alignSelf: 'stretch',
fontSize: 14,
fontWeight: '400',
lineHeight: 20,
},
bodyContainer: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
flexShrink: 1,
gap: 4,
width: '100%',
},
button: {
borderRadius: 32,
gap: 8,
paddingHorizontal: 12,
paddingVertical: 8,
},
buttonContainer: {
alignItems: 'flex-start',
alignSelf: 'stretch',
display: 'flex',
flexDirection: 'row',
gap: 12,
width: '100%',
},
buttonText: {
fontSize: 14,
fontWeight: '700',
lineHeight: 20,
},
container: {
alignItems: 'flex-start',
borderStyle: 'solid',
boxShadow:
'0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 0 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.08)',
display: 'flex',
flexDirection: 'column',
gap: 8,
justifyContent: 'center',
padding: 16,
width: '100%',
},
title: {
fontSize: 16,
fontWeight: '700',
lineHeight: 24,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
Text,
TouchableOpacity,
View,
type TextStyle,
type ViewStyle,
Pressable,
} from 'react-native';

import { IterableEmbeddedViewType } from '../../enums/IterableEmbeddedViewType';
import { useEmbeddedView } from '../../hooks/useEmbeddedView';
import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps';
import { styles } from './IterableEmbeddedNotification.styles';

export const IterableEmbeddedNotification = ({
config,
message,
onButtonClick = () => {},
onMessageClick = () => {},
}: IterableEmbeddedComponentProps) => {
const { parsedStyles, handleButtonClick, handleMessageClick } =
useEmbeddedView(IterableEmbeddedViewType.Notification, {
message,
config,
onButtonClick,
onMessageClick,
});

const buttons = message.elements?.buttons ?? [];

return (
<Pressable onPress={() => handleMessageClick()}>
<View
style={[
styles.container,
{
backgroundColor: parsedStyles.backgroundColor,
borderColor: parsedStyles.borderColor,
borderRadius: parsedStyles.borderCornerRadius,
borderWidth: parsedStyles.borderWidth,
} as ViewStyle,
]}
>
{}
<View style={styles.bodyContainer}>
<Text
style={[
styles.title,
{ color: parsedStyles.titleTextColor } as TextStyle,
]}
>
{message.elements?.title}
</Text>
<Text
style={[
styles.body,
{ color: parsedStyles.bodyTextColor } as TextStyle,
]}
>
{message.elements?.body}
</Text>
</View>
{buttons.length > 0 && (
<View style={styles.buttonContainer}>
{buttons.map((button, index) => {
const backgroundColor =
index === 0
? parsedStyles.primaryBtnBackgroundColor
: parsedStyles.secondaryBtnBackgroundColor;
const textColor =
index === 0
? parsedStyles.primaryBtnTextColor
: parsedStyles.secondaryBtnTextColor;
return (
<TouchableOpacity
style={[styles.button, { backgroundColor } as ViewStyle]}
onPress={() => handleButtonClick(button)}
key={button.id}
>
<Text
style={[
styles.buttonText,
{ color: textColor } as TextStyle,
]}
>
{button.title}
</Text>
</TouchableOpacity>
);
})}
</View>
)}
</View>
</Pressable>
);
Comment on lines +15 to +95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 7): IterableEmbeddedNotification [qlty:function-complexity]

};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './IterableEmbeddedNotification';
2 changes: 1 addition & 1 deletion src/embedded/components/IterableEmbeddedView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IterableEmbeddedViewType } from '../enums/IterableEmbeddedViewType';

import { IterableEmbeddedBanner } from './IterableEmbeddedBanner';
import { IterableEmbeddedCard } from './IterableEmbeddedCard';
import { IterableEmbeddedNotification } from './IterableEmbeddedNotification';
import { IterableEmbeddedNotification } from './IterableEmbeddedNotification/IterableEmbeddedNotification';
import type { IterableEmbeddedComponentProps } from '../types/IterableEmbeddedComponentProps';

/**
Expand Down
2 changes: 1 addition & 1 deletion src/embedded/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './IterableEmbeddedBanner';
export * from './IterableEmbeddedCard';
export * from './IterableEmbeddedNotification';
export * from './IterableEmbeddedNotification/IterableEmbeddedNotification';
export * from './IterableEmbeddedView';
Loading