UNPKG

@iterable/react-native-sdk

Version:
302 lines (293 loc) 10.8 kB
"use strict"; import { useIsFocused } from '@react-navigation/native'; import { useEffect, useState } from 'react'; import { Animated, NativeEventEmitter, Platform, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Iterable, RNIterableAPI, useAppStateListener, useDeviceOrientation } from "../../core/index.js"; import { IterableInAppDeleteSource, IterableInAppLocation } from "../../inApp/index.js"; import { IterableInboxDataModel } from "../classes/index.js"; import { ITERABLE_INBOX_COLORS } from "../constants/index.js"; import { IterableInboxEmptyState } from "./IterableInboxEmptyState.js"; import { IterableInboxMessageDisplay } from "./IterableInboxMessageDisplay.js"; import { IterableInboxMessageList } from "./IterableInboxMessageList.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); const DEFAULT_HEADLINE_HEIGHT = 60; const ANDROID_HEADLINE_HEIGHT = 70; const HEADLINE_PADDING_LEFT_PORTRAIT = 30; const HEADLINE_PADDING_LEFT_LANDSCAPE = 70; /** * Props for the IterableInbox component. */ /** * The `IterableInbox` component is responsible for displaying an inbox of messages. * It handles fetching messages, displaying them in a list, and showing individual message details. * It also manages the state of the inbox, including loading state, selected message, and visible message impressions. * * @example * ```tsx * const [visible, setVisible] = useState<boolean>(false); * * return ( * <> * <Button title="Return to Inbox" onPress={() => setVisible(!visible)} /> * {visible && ( * <IterableInbox * returnToInboxTrigger={visible} * customizations={{ * navTitle: 'My Inbox', * unreadIndicator: { * backgroundColor: 'red', * height: 10, * } * }} * showNavTitle={true} * tabBarHeight={80} * /> * )} * </> * ) * ``` */ export const IterableInbox = ({ returnToInboxTrigger = true, messageListItemLayout = () => null, customizations = {}, tabBarHeight = 80, tabBarPadding = 20, safeAreaMode = true, showNavTitle = true }) => { const defaultInboxTitle = 'Inbox'; const inboxDataModel = new IterableInboxDataModel(); const { height, width, isPortrait } = useDeviceOrientation(); const appState = useAppStateListener(); const isFocused = useIsFocused(); const [selectedRowViewModelIdx, setSelectedRowViewModelIdx] = useState(0); const [rowViewModels, setRowViewModels] = useState([]); const [loading, setLoading] = useState(true); const [animatedValue] = useState(new Animated.Value(0)); const [isMessageDisplay, setIsMessageDisplay] = useState(false); const [visibleMessageImpressions, setVisibleMessageImpressions] = useState([]); const styles = StyleSheet.create({ container: { alignItems: 'center', flex: 1, flexDirection: 'row', height: '100%', justifyContent: 'flex-start', paddingBottom: 0, paddingLeft: 0, paddingRight: 0, width: 2 * width }, headline: { backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND, fontSize: 40, fontWeight: 'bold', height: Platform.OS === 'android' ? ANDROID_HEADLINE_HEIGHT : DEFAULT_HEADLINE_HEIGHT, marginTop: 0, paddingBottom: 10, paddingLeft: isPortrait ? HEADLINE_PADDING_LEFT_PORTRAIT : HEADLINE_PADDING_LEFT_LANDSCAPE, paddingTop: 10, width: '100%' }, loadingScreen: { backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND, height: '100%' }, messageListContainer: { flexDirection: 'column', height: '100%', justifyContent: 'flex-start', width: width } }); const navTitleHeight = DEFAULT_HEADLINE_HEIGHT + styles.headline.paddingTop + styles.headline.paddingBottom; //fetches inbox messages and adds listener for inbox changes on mount useEffect(() => { fetchInboxMessages(); addInboxChangedListener(); //removes listener for inbox changes on unmount and ends inbox session return () => { removeInboxChangedListener(); inboxDataModel.endSession(visibleMessageImpressions); }; // MOB-10427: figure out if missing dependency is a bug // eslint-disable-next-line react-hooks/exhaustive-deps }, []); //starts session when user is on inbox and app is active //ends session when app is in background or app is closed useEffect(() => { if (isFocused) { if (appState === 'active') { inboxDataModel.startSession(visibleMessageImpressions); } else if (appState === 'background' && Platform.OS === 'android' || appState === 'inactive') { inboxDataModel.endSession(visibleMessageImpressions); } } // MOB-10427: figure out if missing dependency is a bug // eslint-disable-next-line react-hooks/exhaustive-deps }, [appState]); //starts session when user is on inbox //ends session when user navigates away from inbox useEffect(() => { if (appState === 'active') { if (isFocused) { inboxDataModel.startSession(visibleMessageImpressions); } else { inboxDataModel.endSession(visibleMessageImpressions); } } // MOB-10427: figure out if missing dependency is a bug // eslint-disable-next-line react-hooks/exhaustive-deps }, [isFocused]); //updates the visible rows when visible messages changes useEffect(() => { inboxDataModel.updateVisibleRows(visibleMessageImpressions); // MOB-10427: figure out if missing dependency is a bug // eslint-disable-next-line react-hooks/exhaustive-deps }, [visibleMessageImpressions]); //if return to inbox trigger is provided, runs the return to inbox animation whenever the trigger is toggled useEffect(() => { if (isMessageDisplay) { returnToInbox(); } // MOB-10427: figure out if missing dependency is a bug // eslint-disable-next-line react-hooks/exhaustive-deps }, [returnToInboxTrigger]); function addInboxChangedListener() { RNEventEmitter.addListener('receivedIterableInboxChanged', () => { fetchInboxMessages(); }); } function removeInboxChangedListener() { RNEventEmitter.removeAllListeners('receivedIterableInboxChanged'); } async function fetchInboxMessages() { let newMessages = await inboxDataModel.refresh(); newMessages = newMessages.map((message, index) => { return { ...message, last: index === newMessages.length - 1 }; }); setRowViewModels(newMessages); setLoading(false); } function getHtmlContentForRow(id) { return inboxDataModel.getHtmlContentForMessageId(id); } function handleMessageSelect(id, index, models) { const newRowViewModels = models.map(rowViewModel => { return rowViewModel.inAppMessage.messageId === id ? { ...rowViewModel, read: true } : rowViewModel; }); setRowViewModels(newRowViewModels); inboxDataModel.setMessageAsRead(id); setSelectedRowViewModelIdx(index); Iterable.trackInAppOpen( // MOB-10428: Have a safety check for models[index].inAppMessage // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore models[index].inAppMessage, IterableInAppLocation.inbox); slideLeft(); } function deleteRow(messageId) { inboxDataModel.deleteItemById(messageId, IterableInAppDeleteSource.inboxSwipe); fetchInboxMessages(); } function returnToInbox(callback) { Animated.timing(animatedValue, { toValue: 0, duration: 300, useNativeDriver: false }).start(() => typeof callback === 'function' && callback()); setIsMessageDisplay(false); } function updateVisibleMessageImpressions(messageImpressions) { setVisibleMessageImpressions(messageImpressions); } function showMessageDisplay(rowViewModelList, index) { const selectedRowViewModel = rowViewModelList[index]; return selectedRowViewModel ? /*#__PURE__*/_jsx(IterableInboxMessageDisplay, { rowViewModel: selectedRowViewModel, inAppContentPromise: getHtmlContentForRow(selectedRowViewModel.inAppMessage.messageId), returnToInbox: returnToInbox, deleteRow: messageId => deleteRow(messageId), contentWidth: width, isPortrait: isPortrait }) : null; } function showMessageList(_loading) { return /*#__PURE__*/_jsxs(View, { style: styles.messageListContainer, children: [showNavTitle ? /*#__PURE__*/_jsx(Text, { style: styles.headline, children: customizations?.navTitle ? customizations?.navTitle : defaultInboxTitle }) : null, rowViewModels.length ? /*#__PURE__*/_jsx(IterableInboxMessageList, { dataModel: inboxDataModel, rowViewModels: rowViewModels, customizations: customizations, messageListItemLayout: messageListItemLayout, deleteRow: messageId => deleteRow(messageId), handleMessageSelect: (messageId, index) => handleMessageSelect(messageId, index, rowViewModels), updateVisibleMessageImpressions: messageImpressions => updateVisibleMessageImpressions(messageImpressions), contentWidth: width, isPortrait: isPortrait }) : renderEmptyState()] }); } function renderEmptyState() { return loading ? /*#__PURE__*/_jsx(View, { style: styles.loadingScreen }) : /*#__PURE__*/_jsx(IterableInboxEmptyState, { customizations: customizations, tabBarHeight: tabBarHeight, tabBarPadding: tabBarPadding, navTitleHeight: navTitleHeight, contentWidth: width, height: height, isPortrait: isPortrait }); } function slideLeft() { Animated.timing(animatedValue, { toValue: 1, duration: 300, useNativeDriver: false }).start(); setIsMessageDisplay(true); } const inboxAnimatedView = /*#__PURE__*/_jsxs(Animated.View, { // MOB-10429: Change to use `StyleSheet.create` for styles, per best practices // eslint-disable-next-line react-native/no-inline-styles style: { transform: [{ translateX: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [0, -width] }) }], height: '100%', flexDirection: 'row', width: 2 * width, justifyContent: 'flex-start' }, children: [showMessageList(loading), showMessageDisplay(rowViewModels, selectedRowViewModelIdx)] }); return safeAreaMode ? /*#__PURE__*/_jsx(SafeAreaView, { style: styles.container, children: inboxAnimatedView }) : /*#__PURE__*/_jsx(View, { style: styles.container, children: inboxAnimatedView }); }; //# sourceMappingURL=IterableInbox.js.map