import { useRef } from 'react';
import {
Animated,
Image,
PanResponder,
StyleSheet,
Text,
TouchableOpacity,
View,
type PanResponderGestureState,
type TextStyle,
type ViewStyle,
} from 'react-native';
import { IterableInboxDataModel } from '../classes';
import type {
IterableInboxCustomizations,
IterableInboxRowViewModel,
} from '../types';
import { ITERABLE_INBOX_COLORS } from '../constants';
/**
* Renders a default layout for a message list item in the inbox.
*
* TODO: Change to component
*
* @param last - Indicates if this is the last item in the list.
* @param dataModel - The data model containing the message data.
* @param rowViewModel - The view model for the current row.
* @param customizations - Custom styles and configurations.
* @param isPortrait - Indicates if the device is in portrait mode.
*/
function defaultMessageListLayout(
last: boolean,
dataModel: IterableInboxDataModel,
rowViewModel: IterableInboxRowViewModel,
customizations: IterableInboxCustomizations,
isPortrait: boolean
) {
const messageTitle = rowViewModel.inAppMessage.inboxMetadata?.title ?? '';
const messageBody = rowViewModel.inAppMessage.inboxMetadata?.subtitle ?? '';
const messageCreatedAt =
dataModel.getFormattedDate(rowViewModel.inAppMessage) ?? '';
const thumbnailURL = rowViewModel.imageUrl;
const styles = StyleSheet.create({
body: {
color: ITERABLE_INBOX_COLORS.TEXT_MUTED,
flexWrap: 'wrap',
fontSize: 15,
paddingBottom: 10,
width: '85%',
},
createdAt: {
color: ITERABLE_INBOX_COLORS.TEXT_MUTED,
fontSize: 12,
},
messageContainer: {
flexDirection: 'column',
justifyContent: 'center',
paddingLeft: 10,
width: '75%',
},
messageRow: {
backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND_LIGHT,
borderColor: ITERABLE_INBOX_COLORS.BORDER,
borderStyle: 'solid',
borderTopWidth: 1,
flexDirection: 'row',
height: 150,
paddingBottom: 10,
paddingTop: 10,
width: '100%',
},
readMessageThumbnailContainer: {
flexDirection: 'column',
justifyContent: 'center',
paddingLeft: 30,
},
title: {
fontSize: 22,
paddingBottom: 10,
width: '85%',
},
unreadIndicator: {
backgroundColor: ITERABLE_INBOX_COLORS.UNREAD,
borderRadius: 15 / 2,
height: 15,
marginLeft: 10,
marginRight: 5,
marginTop: 10,
width: 15,
},
unreadIndicatorContainer: {
flexDirection: 'column',
height: '100%',
justifyContent: 'flex-start',
},
unreadMessageThumbnailContainer: {
flexDirection: 'column',
justifyContent: 'center',
paddingLeft: 10,
},
});
const resolvedStyles = { ...styles, ...customizations };
const {
unreadIndicatorContainer,
unreadMessageThumbnailContainer,
title,
body,
createdAt,
messageRow,
} = resolvedStyles;
let { unreadIndicator, readMessageThumbnailContainer, messageContainer } =
resolvedStyles;
unreadIndicator = !isPortrait
? { ...unreadIndicator, marginLeft: 40 }
: unreadIndicator;
readMessageThumbnailContainer = !isPortrait
? { ...readMessageThumbnailContainer, paddingLeft: 65 }
: readMessageThumbnailContainer;
messageContainer = !isPortrait
? { ...messageContainer, width: '90%' }
: messageContainer;
function messageRowStyle(_rowViewModel: IterableInboxRowViewModel) {
return last ? { ...messageRow, borderBottomWidth: 1 } : messageRow;
}
return (
{rowViewModel.read ? null : }
{thumbnailURL ? (
) : null}
{messageTitle}
{messageBody}
{messageCreatedAt}
);
}
/**
* Props for the IterableInboxMessageCell component.
*/
export interface IterableInboxMessageCellProps {
/**
* The index of the message cell.
*/
index: number;
/**
* Indicates if this is the last message cell in a list.
*/
last: boolean;
/**
* The data model for the inbox message.
*/
dataModel: IterableInboxDataModel;
/**
* The view model for the inbox row.
*/
rowViewModel: IterableInboxRowViewModel;
/**
* Customizations for the inbox message cell.
*/
customizations: IterableInboxCustomizations;
/**
* Function to check if swiping should be enabled.
* @param swiping - Should swiping be enabled?
*/
swipingCheck: (swiping: boolean) => void;
/**
* Function to specify a layout for the message row.
*
* @remarks
* To specify a custom layout for your inbox rows, when you instantiate your
* `IterableInbox`, assign a function to its `messageListItemLayout` prop. The
* inbox will call this function for each of its rows, and it should return:
*
* 1. JSX that represents the custom layout for the row.
* 2. The height of the row (must be the same for all rows).
*
* @param isLast - Is this the last message in the list?
* @param rowViewModel - The view model for the inbox row.
*
* @returns A tuple containing a React node and a number, or undefined/null.
*
* @example
* ```tsx
* const ROW_HEIGHT = 100;
*
* // Custom layout for the message row
* const renderCustomLayout = (
* isLast: boolean,
* rowViewModel: IterableInboxRowViewModel,
* ) => {
* return [
* // Component shown in the message row
*
* Title: {rowViewModel.inAppMessage.inboxMetadata?.title}
* Body: {rowViewModel.inAppMessage.inboxMetadata?.subtitle}
* Date: {rowViewModel.createdAt}
* Has been read: {rowViewModel.read === true}
* ,
* // Height of the row
* ROW_HEIGHT,
* ];
* }
*
*
* ```
*/
messageListItemLayout: (
isLast: boolean,
rowViewModel: IterableInboxRowViewModel
) => [React.ReactNode, number] | undefined | null;
/**
* Function to delete a message row.
* @param messageId - The ID of the message to delete.
*/
deleteRow: (messageId: string) => void;
/**
* Function to handle message selection.
* @param messageId - The ID of the message to select.
* @param index - The index of the message to select.
*/
handleMessageSelect: (messageId: string, index: number) => void;
/**
* The width of the content.
*/
contentWidth: number;
/**
* Indicates if the device is in portrait mode.
*/
isPortrait: boolean;
}
/**
* Component which renders a single message cell in the Iterable inbox.
*/
export const IterableInboxMessageCell = ({
index,
last,
dataModel,
rowViewModel,
customizations,
swipingCheck,
messageListItemLayout,
deleteRow,
handleMessageSelect,
contentWidth,
isPortrait,
}: IterableInboxMessageCellProps) => {
const position = useRef(new Animated.ValueXY()).current;
let deleteSliderHeight = customizations.messageRow?.height
? customizations.messageRow.height
: 150;
if (messageListItemLayout(last, rowViewModel)) {
deleteSliderHeight =
messageListItemLayout(last, rowViewModel)?.[1] ?? deleteSliderHeight;
}
const styles = StyleSheet.create({
deleteSlider: {
alignItems: 'center',
backgroundColor: ITERABLE_INBOX_COLORS.DESTRUCTIVE,
elevation: 1,
flexDirection: 'row',
height: deleteSliderHeight,
justifyContent: 'flex-end',
paddingRight: 10,
position: 'absolute',
width: '100%',
...(isPortrait ? {} : { paddingRight: 40 }),
},
textContainer: {
elevation: 2,
width: '100%',
},
textStyle: {
color: ITERABLE_INBOX_COLORS.TEXT_INVERSE,
fontSize: 15,
fontWeight: 'bold',
},
});
const scrollThreshold = contentWidth / 15;
const FORCING_DURATION = 350;
//If user swipes, either complete swipe or reset
function userSwipedLeft(gesture: PanResponderGestureState) {
if (gesture.dx < -0.6 * contentWidth) {
completeSwipe();
} else {
resetPosition();
}
}
function completeSwipe() {
const x = -2000;
Animated.timing(position, {
toValue: { x, y: 0 },
duration: FORCING_DURATION,
useNativeDriver: false,
}).start(() => deleteRow(rowViewModel.inAppMessage.messageId));
}
function resetPosition() {
Animated.timing(position, {
toValue: { x: 0, y: 0 },
duration: 200,
useNativeDriver: false,
}).start();
}
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder: (_event, gestureState) => {
const { dx, dy } = gestureState;
// return true if user is swiping, return false if it's a single click
return Math.abs(dx) !== 0 && Math.abs(dy) !== 0;
},
onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
const { dx, dy } = gestureState;
// return true if user is swiping, return false if it's a single click
return Math.abs(dx) !== 0 && Math.abs(dy) !== 0;
},
onPanResponderTerminationRequest: () => false,
onPanResponderGrant: () => {
position.setValue({ x: 0, y: 0 });
},
onPanResponderMove: (_event, gesture) => {
if (gesture.dx <= -scrollThreshold) {
//enables swipeing when threshold is reached
swipingCheck(true);
//threshold value is deleted from movement
const x = gesture.dx;
//position is set to the new value
position.setValue({ x, y: 0 });
}
},
onPanResponderRelease: (_event, gesture) => {
position.flattenOffset();
if (gesture.dx < 0) {
userSwipedLeft(gesture);
} else {
resetPosition();
}
swipingCheck(false);
},
})
).current;
return (
<>
DELETE
{
handleMessageSelect(rowViewModel.inAppMessage.messageId, index);
}}
>
{messageListItemLayout(last, rowViewModel)
? messageListItemLayout(last, rowViewModel)?.[0]
: defaultMessageListLayout(
last,
dataModel,
rowViewModel,
customizations,
isPortrait
)}
>
);
};