@iterable/react-native-sdk
Version:
Iterable SDK for React Native.
302 lines (293 loc) • 10.8 kB
JavaScript
"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