diff --git a/README.md b/README.md index 0f62fd1..0000fce 100644 --- a/README.md +++ b/README.md @@ -110,16 +110,13 @@ Prop | Description | Type | Default value | --- | --- | --- | --- | theme | Object for custom themes | Theme | {} | customStyles | Object for custom styling | StyleProps | {} | -title | Title of the notification inbox | string | "Notifications" | -hideHeader | Toggle to hide or show the header section | boolean | false | -hideClearAll | Toggle to hide or show the clear all button | boolean | false | darkMode | Toggle to enable dark mode| boolean | false | itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | number | 20 | -cardProps | Props for customizing the notification cards | { hideAvatar: boolean } | { hideAvatar: false } | +cardProps | Props for customizing the notification cards | CardProps | { hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false } | customNotificationCard | Function for rendering custom notification cards | (notification)=> JSX Element | null | onNotificationCardClick | Custom click handler for notification cards | (notification)=> void | ()=>null | listEmptyComponent | Custom component for empty notification list | JSX Element | null | -customHeader | Custom header component | JSX Element | null | +inboxHeaderProps | Props for customizing the header | InboxHeaderProps | { title: "Notifications", hideHeader: false, hideClearAll: false, customHeader: null, showBackButton:false, backButton: null, onBackPress: ()=> null } | customFooter | Custom footer component | JSX Element | null | customLoader | Custom component to display the initial loading state| JSX Element | null | customErrorWindow | Custom error window | JSX Element | null | @@ -174,7 +171,7 @@ Here are the available theme options: Here are the custom style options for the notification inbox: ```js - export type StyleProps = { + type StyleProps = { notificationIcon?: { size?: number; }; @@ -216,6 +213,27 @@ Here are the custom style options for the notification inbox: }; }; ``` +#### CardProps +```js + type CardProps = { + hideAvatar?: boolean; + disableAutoMarkAsRead?: boolean; + hideDelete?: boolean; + }; +``` + +#### InboxHeaderProps +```js + type InboxHeaderProps = { + title?: string; + hideHeader?: boolean; + hideClearAll?: boolean; + customHeader?: JSX.Element | null; + showBackButton?: boolean; + backButton?: JSX.Element; + onBackPress?: () => void; + }; +``` ## 3. Hooks @@ -297,7 +315,7 @@ function MyContainer(): React.JSX.Element { title="Notifications" hideHeader={false} darkMode={false} - cardProps={{hideAvatar: false}} + cardProps={{hideAvatar: false, disableAutoMarkAsRead: false}} /> ); diff --git a/example/App.tsx b/example/App.tsx index 9eb2f95..72d7076 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -11,7 +11,13 @@ const Stack = createNativeStackNavigator(); function MyStack() { return ( - + - - {MyStack()} - + {MyStack()} ); } diff --git a/example/screens/home.tsx b/example/screens/home.tsx index 54e1790..10335b4 100644 --- a/example/screens/home.tsx +++ b/example/screens/home.tsx @@ -19,24 +19,24 @@ const badgeThemes = [ { light: { badgeStyle: { - color: 'black', - }, + color: 'black' + } }, dark: { badgeStyle: { - color: 'green', + color: 'green' } } }, { light: { badgeStyle: { - color: 'blue', + color: 'blue' } }, dark: { badgeStyle: { - color: 'pink', + color: 'pink' } } } @@ -48,7 +48,7 @@ function Home(): React.JSX.Element { const [showTestingWindow, setShowTestingWindow] = useState(false); const [showCustomNotification, setShowCustomNotification] = useState(false); const [badgeThemeIndex, setBadgeThemeIndex] = useState(0); - const [showNetwork, setShowNetwork] = useState(true); + const [showNetwork, setShowNetwork] = useState(false); const backgroundStyle = { backgroundColor: isDarkMode ? '#000' : '#FFF' @@ -79,12 +79,9 @@ function Home(): React.JSX.Element { {showTestingWindow && ( - {renderButton( - `${showNetwork ? 'Hide' : 'Show'} network`, - () => { - setShowNetwork((showNetwork) => !showNetwork); - } - )} + {renderButton(`${showNetwork ? 'Hide' : 'Show'} network`, () => { + setShowNetwork((showNetwork) => !showNetwork); + })} {renderButton(`${showCustomNotification ? 'Default' : 'Custom'}-N-Icon`, () => setShowCustomNotification((showCustomNotification) => !showCustomNotification) )} @@ -120,20 +117,24 @@ function Home(): React.JSX.Element { backgroundColor={backgroundStyle.backgroundColor} /> - navigation.navigate('Notifications')} - /> - Siren Notification Icon Theme Testing - {showNetwork && } + + navigation.navigate('Notifications')} + /> + + + Home screen + {showNetwork && } + + {testingWindow()} - {testingWindow()} ); } @@ -171,7 +172,7 @@ const styles = StyleSheet.create({ }, testingWindowInnerContainer: { flexWrap: 'wrap', - flexDirection: 'row', + flexDirection: 'row' }, whiteLabel: { color: '#fff', @@ -187,6 +188,12 @@ const styles = StyleSheet.create({ margin: 6, borderRadius: 4, height: 30 + }, + iconContainer: { + position: 'absolute', + top: 0, + right: 10, + zIndex:2, } }); diff --git a/example/screens/notifications.tsx b/example/screens/notifications.tsx index 1ed0f5a..a973086 100644 --- a/example/screens/notifications.tsx +++ b/example/screens/notifications.tsx @@ -40,10 +40,10 @@ function Notifications(): React.JSX.Element { const [showTestingWindow, setShowTestingWindow] = useState(false); const [sdkDarkModeEnabled, setSdkDarkModeEnabled] = useState(false); const [showCustomHeader, setShowCustomHeader] = useState(false); - const [showCustomFooter, setShowCustomFooter] = useState(true); + const [showCustomFooter, setShowCustomFooter] = useState(false); const [hideHeader, setHideHeader] = useState(false); const [hideAvatar, setHideAvatar] = useState(false); - const [showNetwork, setShowNetwork] = useState(true); + const [showNetwork, setShowNetwork] = useState(false); const [windowThemeIndex, setWindowThemeIndex] = useState(0); const [showCustomEmptyComponent, setShowCustomEmptyComponent] = useState(false); const [showCustomNotificationCard, setShowCustomNotificationCard] = useState(false); @@ -52,7 +52,7 @@ function Notifications(): React.JSX.Element { backgroundColor: isDarkMode ? '#000' : '#FFF' }; - const { markNotificationsAsReadByDate, markAsRead } = useSiren(); + const { markNotificationsAsReadByDate } = useSiren(); const renderListEmpty = () => { return ( @@ -164,13 +164,17 @@ function Notifications(): React.JSX.Element { navigation.goBack(), + }} darkMode={sdkDarkModeEnabled} - cardProps={{ hideAvatar: hideAvatar, showMedia: true }} + cardProps={{ hideAvatar: hideAvatar, disableAutoMarkAsRead: false }} theme={windowThemes[windowThemeIndex]} customFooter={showCustomFooter ? renderCustomFooter() : undefined} listEmptyComponent={showCustomEmptyComponent ? renderListEmpty() : undefined} - customHeader={showCustomHeader ? renderCustomHeader() : undefined} customStyles={{ notificationCard: { avatarSize: 30, @@ -183,7 +187,6 @@ function Notifications(): React.JSX.Element { } onNotificationCardClick={(notification: NotificationDataType) => { console.log('click on notification'); - markAsRead(notification.id); }} onError={(error: SirenErrorType) => { console.log(`error: ${error}`); diff --git a/package.json b/package.json index 312035d..6880512 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "lint": "eslint src", "lint:fix": "npm run lint -- --fix", "build": "bob build", - "test-build": "npm run build && npm pack && mv siren-react-native-inbox-1.0.0.tgz example/siren-sdk01.tgz && cd example && npm install", + "test-build": "npm run build && npm pack && mv sirenapp-react-native-inbox-0.0.1.tgz example/siren-sdk01.tgz && cd example && npm install", "test": "npx jest", "coverage": "npm run test -- --coverage" }, diff --git a/src/components/backIcon.tsx b/src/components/backIcon.tsx new file mode 100644 index 0000000..123279f --- /dev/null +++ b/src/components/backIcon.tsx @@ -0,0 +1,41 @@ +import React, { type ReactElement } from 'react'; +import { StyleSheet, View } from 'react-native'; + +import type { SirenStyleProps } from '../types'; + +const BackIcon = ({ styles }: { styles: Partial }): ReactElement => { + return ( + + + + + ); +}; + +const style = StyleSheet.create({ + backIconContainer: { + justifyContent: 'center', + alignItems: 'flex-start', + height: 30, + width: 20, + }, + backIconLine1: { + width: 12, + height: 2, + backgroundColor: 'red', + transform: [{ + rotate: '130deg' + }] + }, + backIconLine2: { + marginTop: 6, + width: 12, + height: 2, + backgroundColor: 'red', + transform: [{ + rotate: '230deg' + }] + } +}); + +export default BackIcon; diff --git a/src/components/card.tsx b/src/components/card.tsx index 9359b7f..5b0bf0f 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -2,7 +2,7 @@ import React, { useState, type ReactElement, useMemo, useEffect } from 'react'; import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import type { NotificationCardProps } from '../types'; -import { CommonUtils } from '../utils'; +import { CommonUtils, useSiren } from '../utils'; import CloseIcon from './closeIcon'; import TimerIcon from './timerIcon'; @@ -27,7 +27,7 @@ import TimerIcon from './timerIcon'; * onCardClick={() => console.log('Notification clicked')} * onDelete={() => console.log('Notification deleted')} * notification={notification} - * cardProps={{ hideAvatar: false, showMedia: true }} + * cardProps={{ hideAvatar: false, disableAutoMarkAsRead: false }} * styles={customStyles} * /> * @@ -40,7 +40,9 @@ import TimerIcon from './timerIcon'; */ const Card = (props: NotificationCardProps): ReactElement => { - const { onCardClick, notification, cardProps, styles, onDelete, darkMode } = props; + const { onCardClick, notification, cardProps = {}, styles, onDelete, darkMode } = props; + const { hideAvatar, disableAutoMarkAsRead, hideDelete = false } = cardProps; + const { markAsRead } = useSiren(); const emptyState = () => { return darkMode ? require('../assets/emptyDark.png') : require('../assets/emptyLight.png'); @@ -60,6 +62,15 @@ const Card = (props: NotificationCardProps): ReactElement => { ); }, [notification, darkMode]); + const cardClick = (): void => { + onCardClick(notification); + if (!disableAutoMarkAsRead) markAsRead(notification.id); + }; + + const onError = (): void => { + setImageSource(emptyState()); + }; + const renderAvatar = useMemo((): JSX.Element => { return ( @@ -68,7 +79,7 @@ const Card = (props: NotificationCardProps): ReactElement => { source={imageSource} resizeMode='cover' style={style.cardAvatarStyle} - onError={() => setImageSource(emptyState())} + onError={onError} /> @@ -77,7 +88,7 @@ const Card = (props: NotificationCardProps): ReactElement => { return ( onCardClick(notification)} + onPress={cardClick} activeOpacity={0.6} testID='card-touchable' style={[style.cardWrapper, styles.cardWrapper, !notification?.isRead && styles.highlighted]} @@ -90,13 +101,15 @@ const Card = (props: NotificationCardProps): ReactElement => { ]} /> - {!cardProps?.hideAvatar && renderAvatar} + {!hideAvatar && renderAvatar} {notification.message?.header} - + {!hideDelete && ( + + )} {Boolean(notification.message?.subHeader) && ( @@ -127,10 +140,10 @@ const style = StyleSheet.create({ cardContainer: { width: '100%', flexDirection: 'row', - paddingRight: 16 }, cardIconContainer: { - paddingHorizontal: 10, + paddingLeft: 6, + paddingRight: 12, paddingTop: 4 }, cardTitle: { diff --git a/src/components/header.tsx b/src/components/header.tsx index a366392..d941668 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -4,6 +4,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import type { SirenStyleProps } from '../types'; import { Constants } from '../utils'; import ClearIcon from './clearIcon'; +import BackIcon from './backIcon'; /** * Renders a header component with a title and a "Clear All" (deletes all the notifications till date) action. @@ -22,22 +23,53 @@ import ClearIcon from './clearIcon'; * @param {Object} props.styles - Custom styles to apply to the header component. * @param {Function} props.onClearAllNotifications - A callback function that is called when the "Clear All" action is triggered. * @param {boolean} props.clearAllDisabled - Disables the clear all button. + * @param {boolean} props.showBackButton - Toggle for show back button in header. + * @param {boolean} props.backButton - Custom back button. + * @param {boolean} props.onBackPress - A callback function that is called when back button is pressed. */ -const Header = (props: { +type HeaderProps = { title: string; styles: Partial; onPressClearAll: () => void; clearAllDisabled: boolean; hideClearAll?: boolean; -}): ReactElement => { - const { title = '', styles, onPressClearAll, clearAllDisabled = false, hideClearAll } = props; + showBackButton?: boolean; + backButton?: JSX.Element; + onBackPress?: () => void; +}; + +const Header = (props: HeaderProps): ReactElement => { + const { + title = '', + styles, + onPressClearAll, + clearAllDisabled = false, + hideClearAll, + showBackButton = false, + backButton, + onBackPress = () => null + } = props; + + const renderBackButton = () => { + if (showBackButton) + return ( + + {backButton || } + + ); + + return null; + }; return ( - - {title} - + + {renderBackButton()} + + {title} + + {!hideClearAll && ( ; customLoader?: JSX.Element | null; + hideAvatar?: boolean; + hideDelete?: boolean; }; /** * Displays a loading indicator within a window, @@ -19,9 +21,11 @@ type LoadingWindowProps = { * @param {Object} props - The properties passed to the component. * @param {Object} props.styles - Custom styles applied to the loading window container. * @param {Object} props.customLoader - Custom loader to be displayed within the loading window container. + * @param {Object} props.hideAvatar - Flag for hide avatar placeholder circle from loading window card. + * @param {Object} props.hideDelete - Flag for hide delete icon placeholder square from loading window card. */ const LoadingWindow = (props: LoadingWindowProps): ReactElement => { - const { styles, customLoader } = props; + const { styles, customLoader, hideAvatar, hideDelete } = props; const pulseAnim = useRef(new Animated.Value(0)).current; @@ -59,9 +63,11 @@ const LoadingWindow = (props: LoadingWindowProps): ReactElement => { const renderSkeltonCard = ({ index }: { index: number }) => { return ( - + {!hideAvatar && ( + + )} { /> - + {!hideDelete && ( + + )} ); }; @@ -112,7 +120,7 @@ const style = StyleSheet.create({ width: '100%', flexDirection: 'row', justifyContent: 'space-between', - padding: 12, + padding: 14, paddingHorizontal: 16, borderBottomColor: '#98A2B3', borderBottomWidth: 0.4, @@ -132,7 +140,7 @@ const style = StyleSheet.create({ flex: 1, height: 40, borderRadius: 5, - marginBottom: 4, + marginBottom: 4 }, loadingRectangle3: { marginRight: 16, diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 28010e6..580f18c 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -49,7 +49,6 @@ type NotificationFetchParams = { * }; * console.log(error)} @@ -57,12 +56,10 @@ type NotificationFetchParams = { * * @param {Object} props - The props for the SirenInbox component. * @param {Object} [props.theme={}] - Theme object for custom styling. - * @param {string} [props.title=DEFAULT_WINDOW_TITLE] - Title of the notification window. - * @param {boolean} [props.hideHeader=false] - Flag to hide or show the header. * @param {boolean} [props.darkMode=false] - Flag to enable dark mode. * @param {Object} [props.cardProps={ hideAvatar: false, showMedia: true }] - Props for customizing the notification cards. * @param {JSX.Element} [props.listEmptyComponent=null] - Custom component to display when the notification list is empty. - * @param {JSX.Element} [props.customHeader=null] - Custom header component. + * @param {CardProps} [props.inboxHeaderProps] - Object containing props related to the inbox header * @param {JSX.Element} [props.customFooter=null] - Custom footer component. * @param {JSX.Element} [props.customLoader=null] - Custom loader component. * @param {JSX.Element} [props.customErrorWindow=null] - Custom error component. @@ -74,24 +71,32 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const { theme = { dark: {}, light: {} }, customStyles = {}, - title = DEFAULT_WINDOW_TITLE, - hideHeader = false, darkMode = false, - cardProps = { hideAvatar: false, showMedia: true }, + cardProps = { hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false }, listEmptyComponent = null, - customHeader = null, + inboxHeaderProps = {}, customFooter = null, customLoader = null, customErrorWindow = null, customNotificationCard = null, onNotificationCardClick = () => null, onError = () => {}, - hideClearAll = false, itemsPerFetch = 20 } = props; - const notificationsPerPage = - itemsPerFetch > MAXIMUM_ITEMS_PER_FETCH ? MAXIMUM_ITEMS_PER_FETCH : itemsPerFetch; + const { + title = DEFAULT_WINDOW_TITLE, + hideHeader, + hideClearAll, + customHeader, + showBackButton, + backButton, + onBackPress + } = inboxHeaderProps; + const notificationsPerPage = Math.max( + 0, + itemsPerFetch > MAXIMUM_ITEMS_PER_FETCH ? MAXIMUM_ITEMS_PER_FETCH : itemsPerFetch + ); const { siren, verificationStatus } = useSirenContext(); @@ -134,11 +139,13 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { }, [eventListenerData]); const handleMarkNotificationsAsViewed = async (newNotifications = notifications) => { - if (isNonEmptyArray(newNotifications)) { - const response = await markNotificationsAsViewed(newNotifications[0].createdAt); + const currentTimestamp = new Date().getTime(); + const isoString = new Date(currentTimestamp).toISOString(); + const response = await markNotificationsAsViewed( + isNonEmptyArray(newNotifications) ? newNotifications[0].createdAt : isoString + ); - processError(response?.error); - } + processError(response?.error); }; const processError = (error?: SirenErrorType | null) => { @@ -288,7 +295,14 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { return listEmptyComponent || ; } - return ; + return ( + + ); }; const onDelete = async (id: string): Promise => { @@ -349,30 +363,31 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { }; const renderHeader = (): JSX.Element | null => { + if (hideHeader) return null; + if (customHeader) return customHeader; - if (!hideHeader) { - if (customHeader) return customHeader; - - return ( -
- ); - } - - return null; + return ( +
+ ); }; + const keyExtractor = (item: NotificationDataType) => item.id; + const renderList = (): JSX.Element => { return ( item.id} + keyExtractor={keyExtractor} onRefresh={onRefresh} refreshing={false} contentContainerStyle={styles.contentContainer} diff --git a/src/types.ts b/src/types.ts index 50003b7..ba4c498 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,16 +19,13 @@ import type { NotificationDataType, SirenErrorType } from '@sirenapp/js-sdk/dist export type SirenInboxProps = { theme?: Theme; customStyles?: StyleProps; - title?: string; - hideHeader?: boolean; darkMode?: boolean; cardProps?: CardProps; listEmptyComponent?: JSX.Element; customFooter?: JSX.Element; - customHeader?: JSX.Element; + inboxHeaderProps?: InboxHeaderProps; customLoader?: JSX.Element; customErrorWindow?: JSX.Element; - hideClearAll?: boolean; itemsPerFetch?: number; customNotificationCard?: (notification: NotificationDataType) => JSX.Element; onNotificationCardClick?: (notification: NotificationDataType) => void; @@ -54,6 +51,16 @@ export type SirenInboxIconProps = { hideBadge?: boolean; }; +export type InboxHeaderProps = { + title?: string; + hideHeader?: boolean; + hideClearAll?: boolean; + customHeader?: JSX.Element; + showBackButton?: boolean; + backButton?: JSX.Element; + onBackPress?: () => void; +}; + /** * Defines the configuration properties required by the SirenProvider. * @@ -74,7 +81,8 @@ export type SirenProviderConfigProps = { */ type CardProps = { hideAvatar?: boolean; - showMedia?: boolean; + disableAutoMarkAsRead?: boolean; + hideDelete?: boolean; }; /** @@ -232,4 +240,5 @@ export type SirenStyleProps = { timerIconLine2: ViewStyle; skeltonLoaderColor: ViewStyle; highlighted: ViewStyle; + backIcon: ViewStyle; }; diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts index 67c922c..fc43c12 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -249,5 +249,10 @@ export const applyTheme = ( highlighted: { backgroundColor: theme.colors?.highlightedCardColor || DefaultTheme[mode].colors.highlightedCardColor + }, + backIcon: { + backgroundColor: theme.windowHeader?.titleColor || + theme.colors?.textColor || + DefaultTheme[mode].windowHeader.titleColor, } }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 405d276..06f17ca 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -119,7 +119,7 @@ export const defaultStyles = { padding: 0 }, notificationCard: { - padding: 10, + padding: 14, borderWidth: 0.6, avatarSize: 40, titleFontWeight: '500', diff --git a/tests/components/sirenInbox.test.tsx b/tests/components/sirenInbox.test.tsx index b794f5c..ad1a330 100644 --- a/tests/components/sirenInbox.test.tsx +++ b/tests/components/sirenInbox.test.tsx @@ -11,7 +11,6 @@ describe('SirenInbox', () => { render( console.log(error)} /> @@ -21,7 +20,7 @@ describe('SirenInbox', () => { test("should render custom header ", () => { const { getByTestId } = render( My notifications} + inboxHeaderProps={{customHeader: My notifications}} /> ); @@ -30,7 +29,7 @@ describe('SirenInbox', () => { test("should render custom footer ", () => { const { getByTestId } = render( My notifications} + customFooter={My notifications} /> );