diff --git a/src/components/loadingWindow.tsx b/src/components/loadingWindow.tsx
index 3b7d3d5..9057f06 100644
--- a/src/components/loadingWindow.tsx
+++ b/src/components/loadingWindow.tsx
@@ -24,6 +24,9 @@ type LoadingWindowProps = {
* @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 loadingArray = Array(10).fill(0).map((_, i) => i + 1);
+
const LoadingWindow = (props: LoadingWindowProps): ReactElement => {
const { styles, customLoader, hideAvatar, hideDelete } = props;
@@ -103,7 +106,11 @@ const LoadingWindow = (props: LoadingWindowProps): ReactElement => {
return (
{customLoader || (
-
+
)}
);
diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx
index ecea810..f59a0bd 100644
--- a/src/components/sirenInbox.tsx
+++ b/src/components/sirenInbox.tsx
@@ -1,19 +1,20 @@
import React, { type ReactElement, useEffect, useMemo, useState, useRef } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
-
-import PubSub from 'pubsub-js';
import type { Siren } from '@sirenapp/js-sdk';
import type { NotificationDataType, SirenErrorType } from '@sirenapp/js-sdk/dist/esm/types';
+import PubSub from 'pubsub-js';
+import { useSirenContext } from './sirenProvider';
+import type { SirenInboxProps } from '../types';
+import { CommonUtils, Constants, useSiren } from '../utils';
+import { FilterTypes } from '../utils/constants';
import Card from './card';
import EmptyWindow from './emptyWindow';
import ErrorWindow from './errorWindow';
import Header from './header';
import LoadingWindow from './loadingWindow';
import Spinner from './spinner';
-import { useSirenContext } from './sirenProvider';
-import type { SirenInboxProps } from '../types';
-import { CommonUtils, Constants, useSiren } from '../utils';
+import Tabs from './tab';
const {
DEFAULT_WINDOW_TITLE,
@@ -31,6 +32,7 @@ type fetchProps = {
size: number;
end?: string;
start?: string;
+ isRead?: boolean;
};
type NotificationFetchParams = {
@@ -38,6 +40,7 @@ type NotificationFetchParams = {
end?: string;
start?: string;
sort?: 'createdAt' | 'updatedAt';
+ isRead?: boolean;
};
/**
@@ -78,7 +81,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
hideAvatar: false,
disableAutoMarkAsRead: false,
hideDelete: false,
- hideMediaThumbnail: false,
+ hideMediaThumbnail: false
},
listEmptyComponent = null,
headerProps = {},
@@ -88,7 +91,15 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
customCard = null,
onCardClick = () => null,
onError = () => {},
- itemsPerFetch = 20
+ itemsPerFetch = 20,
+ hideTab = false,
+ tabProps = {
+ tabs: [
+ { key: FilterTypes.ALL, title: FilterTypes.ALL },
+ { key: FilterTypes.UNREAD, title: FilterTypes.UNREAD }
+ ],
+ activeTab: 0
+ }
} = props;
const {
@@ -113,6 +124,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
const [isLoading, setIsLoading] = useState(true);
const [endReached, setEndReached] = useState(false);
const [isError, setIsError] = useState(false);
+ const [filterType, setFilterType] = useState(tabProps.tabs[tabProps.activeTab].key);
+ const [activeTab, setActiveTab] = useState(tabProps.activeTab);
const [eventListenerData, setEventListenerData] = useState<{
id?: string;
action: string;
@@ -128,17 +141,23 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
return cleanUp();
}, []);
+ useEffect(() => {
+ const updatedActiveIndex = tabProps.tabs.findIndex((tab) => tab.key === filterType);
+
+ setActiveTab(updatedActiveIndex);
+ }, [filterType]);
+
useEffect(() => {
// Initialize Siren SDK and start polling notifications
if (verificationStatus === VerificationStatus.SUCCESS && siren) {
initialize();
- } else if(verificationStatus === VerificationStatus.FAILED) {
+ } else if (verificationStatus === VerificationStatus.FAILED) {
setIsError(true);
setIsLoading(false);
setNotifications([]);
if (onError) onError(errorMap.INVALID_CREDENTIALS);
}
- }, [siren, verificationStatus]);
+ }, [siren, verificationStatus, filterType]);
useEffect(() => {
if (eventListenerData) {
@@ -186,6 +205,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
// Initialize Siren SDK and fetch notifications
const initialize = async (): Promise => {
if (siren) {
+ setNotifications([]);
+ setEndReached(false);
siren?.stopRealTimeFetch(EventType.NOTIFICATION);
const allNotifications = await fetchNotifications(siren, true);
const notificationParams: fetchProps = { size: notificationsPerPage };
@@ -193,8 +214,13 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
if (isNonEmptyArray(allNotifications))
notificationParams.start = allNotifications[0].createdAt;
+ if (filterType === FilterTypes.UNREAD) notificationParams.isRead = false;
+
if (verificationStatus === VerificationStatus.SUCCESS)
- siren?.startRealTimeFetch({eventType: EventType.NOTIFICATION, params: notificationParams});
+ siren?.startRealTimeFetch({
+ eventType: EventType.NOTIFICATION,
+ params: notificationParams
+ });
}
};
@@ -204,6 +230,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
sort: 'createdAt'
};
+ if (filterType === FilterTypes.UNREAD) notificationParams.isRead = false;
+
if (attachEndDate) notificationParams.end = notifications[notifications.length - 1].createdAt;
return notificationParams;
@@ -267,6 +295,13 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
[theme, darkMode, customStyles]
);
+ const onChangeTab = (key: string) => {
+ setIsLoading(true);
+ setFilterType(key);
+ setNotifications([]);
+ setEndReached(false);
+ };
+
// Refresh notifications
const onRefresh = async (): Promise => {
if (siren)
@@ -283,8 +318,13 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
if (isNonEmptyArray(allNotifications))
notificationParams.start = allNotifications[0].createdAt;
+ if (filterType === FilterTypes.UNREAD) notificationParams.isRead = false;
+
if (verificationStatus === VerificationStatus.SUCCESS)
- siren?.startRealTimeFetch({eventType: EventType.NOTIFICATION, params:notificationParams});
+ siren?.startRealTimeFetch({
+ eventType: EventType.NOTIFICATION,
+ params: notificationParams
+ });
} catch (err) {
setIsLoading(false);
setIsError(true);
@@ -298,31 +338,6 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
if (fetchMore) fetchNotifications(siren, false);
};
- // Render empty window, error window, or custom empty component
- const renderListEmpty = (): JSX.Element | null => {
- if (!isLoading) {
- if (isError)
- return (
-
- );
-
- return (
-
- {listEmptyComponent || }
-
- );
- }
-
- return (
-
- );
- };
-
const onDelete = async (id: string, shouldUpdateList: boolean): Promise => {
let isSuccess = false;
@@ -330,7 +345,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
disableCardDelete.current = true;
const response = await deleteById(id, shouldUpdateList);
-
+
if (response?.data) isSuccess = true;
processError(response?.error);
disableCardDelete.current = false;
@@ -404,12 +419,60 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
);
};
+ const getScreens = (): JSX.Element[] => {
+ return tabProps.tabs.map((tab, index) => {
+ return renderList(activeTab === index, tab.key);
+ });
+ };
+
+ const renderTabs = (): JSX.Element | null => {
+ return (
+
+ );
+ };
+
const keyExtractor = (item: NotificationDataType) => item.id;
- const renderList = (): JSX.Element => {
+ const renderEmptyState = (isActiveTab: boolean): JSX.Element => {
+ if (isLoading || !isActiveTab)
+ return (
+
+
+
+ );
+
+ if (isError)
+ return (
+
+ );
+
+ return (
+
+
+ {listEmptyComponent || }
+
+
+ );
+ };
+
+ const renderList = (isActiveTab: boolean, key: string): JSX.Element => {
+ if (!isNonEmptyArray(notifications) || !isActiveTab) return renderEmptyState(isActiveTab);
+
return (
{
removeClippedSubviews
maxToRenderPerBatch={20}
windowSize={3}
+ showsVerticalScrollIndicator={false}
accessibilityLabel='siren-notification-list'
/>
);
@@ -428,7 +492,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => {
return (
{renderHeader()}
- {isNonEmptyArray(notifications) ? renderList() : renderListEmpty()}
+ {!hideTab && renderTabs()}
{customFooter || null}
);
@@ -438,6 +502,13 @@ const style = StyleSheet.create({
container: {
minWidth: 300,
flex: 1
+ },
+ swipeContainer: {
+ flex: 1
+ },
+ tabContainer: {
+ width: '100%',
+ height: '100%'
}
});
diff --git a/src/components/tab.tsx b/src/components/tab.tsx
new file mode 100644
index 0000000..2280215
--- /dev/null
+++ b/src/components/tab.tsx
@@ -0,0 +1,232 @@
+import React, { useEffect, useRef, useState, type ReactElement } from 'react';
+import {
+ Animated,
+ type LayoutChangeEvent,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+ Dimensions,
+ type NativeSyntheticEvent,
+ type NativeScrollEvent
+} from 'react-native';
+
+import type { StyleProps } from '../types';
+
+const { width } = Dimensions.get('window');
+
+/**
+ *
+ * @component
+ * @example
+ * console.log(index, key)}
+ * />
+ *
+ * @param {Object} props - The properties passed to the Tab component.
+ * @param {number} props.activeIndex - activeIndex control the tab selection.
+ * @param {Object} props.styles - Custom styles to apply to the header component.
+ * @param {Array} props.tabs - List of tab items to be rendered.
+ * @param {Function} props.onChangeTabItem - Callback function to handle tab item change.
+ * @param {Array} props.screens - List of screens to be rendered.
+ */
+
+type TabProps = {
+ activeIndex?: number;
+ tabs: Array<{ key: string; title: string }>;
+ styles: Partial;
+ onChangeTabItem?: (key: string) => void;
+ screens?: ReactElement[];
+};
+
+const defaultScreensColors = ['#b91919', '#2E8B57', '#0047AB'];
+const defaultWidth = '100%';
+
+const defaultScreens = defaultScreensColors.map((color, index) => (
+
+));
+
+const Tabs = (props: TabProps): ReactElement => {
+ const {
+ tabs,
+ activeIndex = 0,
+ styles,
+ onChangeTabItem = () => null,
+ screens = defaultScreens
+ } = props;
+ const translateX = useRef(new Animated.Value(0)).current;
+ const scaleWidth = useRef(new Animated.Value(0)).current;
+ const scrollViewRef = useRef(null);
+
+ const [activeTabIndex, setActiveTabIndex] = useState(activeIndex);
+ const [tabTitleWidths, setTabTitleWidths] = useState([]);
+
+ useEffect(() => {
+ if (tabTitleWidths.length > 0)
+ moveIndicator();
+ }, [tabTitleWidths, activeTabIndex]);
+
+ useEffect(() => {
+ setActiveTabIndex(activeIndex);
+ }, [activeIndex]);
+
+ useEffect(() => {
+ if (scrollViewRef.current)
+ scrollViewRef.current.scrollTo({ x: activeTabIndex * width, animated: true });
+ }, [activeTabIndex]);
+
+ const handleScroll = (event: NativeSyntheticEvent) => {
+ const contentOffsetX = event.nativeEvent.contentOffset.x;
+ const index = Math.round(contentOffsetX / width);
+
+ if (index !== activeTabIndex) onChangeTab(index);
+ };
+
+ const onChangeTab = (index: number) => {
+ setActiveTabIndex(index);
+ onChangeTabItem(tabs[index].key);
+ };
+
+ const getIndicatorPosition = () => {
+ let position = 0;
+
+ for (let i = 0; i < activeTabIndex; i++) position += tabTitleWidths[i];
+ position += (tabTitleWidths[activeTabIndex] || 0) / 2;
+
+ return position;
+ };
+
+ const moveIndicator = () => {
+ const position = getIndicatorPosition();
+ const width = tabTitleWidths[activeTabIndex] || 0;
+
+ Animated.parallel([
+ Animated.timing(translateX, {
+ toValue: position,
+ duration: 200,
+ useNativeDriver: true
+ }),
+ Animated.timing(scaleWidth, {
+ toValue: width * 0.85,
+ duration: 200,
+ useNativeDriver: true
+ })
+ ]).start();
+ };
+
+ const onTabTitleLayout = (index: number, event: LayoutChangeEvent) => {
+ const { width } = event.nativeEvent.layout;
+
+ const updatedWidths = [...tabTitleWidths];
+
+ updatedWidths[index] = width;
+ setTabTitleWidths(updatedWidths);
+ };
+
+ const renderTabHeader = () => {
+ return (
+
+
+ {tabs.map((tab, index) => (
+ onChangeTab(index)}
+ onLayout={(event) => onTabTitleLayout(index, event)}
+ >
+
+ {tab.title}
+
+
+ ))}
+
+
+
+ );
+ };
+
+ const renderTabContainer = () => {
+ return (
+
+
+ {screens.map((screen, index) => (
+
+ {screen}
+
+ ))}
+
+
+ );
+ };
+
+ return (
+
+ {renderTabHeader()}
+ {renderTabContainer()}
+
+ );
+};
+const style = StyleSheet.create({
+ container: {
+ flex: 1,
+ height: '100%',
+ width: '100%'
+ },
+ tabContainer: {
+ width: '100%',
+ flexDirection: 'row',
+ height: 46,
+ paddingHorizontal: 10,
+ borderBottomWidth: 0.6
+ },
+ tab: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ minWidth: 30,
+ },
+ tabText: {
+ color: '#000',
+ fontWeight: '600',
+ paddingHorizontal: 14,
+ fontSize: 14
+ },
+ activeIndicator: {
+ width: 1,
+ height: 3,
+ backgroundColor: '#000',
+ position: 'absolute',
+ bottom: 0
+ },
+ tabScreen: {
+ width,
+ overflow: 'hidden'
+ }
+});
+
+export default Tabs;
diff --git a/src/types.ts b/src/types.ts
index 15ae4c9..180ff5f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -27,6 +27,8 @@ export type SirenInboxProps = {
customLoader?: JSX.Element;
customErrorWindow?: JSX.Element;
itemsPerFetch?: number;
+ hideTab?: boolean;
+ tabProps?: TabProps;
customCard?: (notification: NotificationDataType) => JSX.Element;
onCardClick?: (notification: NotificationDataType) => void;
onError?: (error: SirenErrorType) => void;
@@ -143,6 +145,14 @@ export type ThemeProps = {
descriptionColor?: string;
dateColor?: string;
};
+ tabs?: {
+ containerBackgroundColor?: string;
+ activeTabBackgroundColor?: string;
+ inactiveTabBackgroundColor?: string;
+ activeTabTextColor?: string;
+ inactiveTabTextColor?: string;
+ indicatorColor?: string;
+ };
};
export type CustomStyleProps = {
@@ -170,7 +180,7 @@ export type CustomStyleProps = {
titleFontWeight?: TextStyle['fontWeight'];
titleSize?: number;
subtitleFontWeight?: TextStyle['fontWeight'];
- subtitleSize?: number
+ subtitleSize?: number;
descriptionFontWeight?: TextStyle['fontWeight'];
descriptionSize?: number;
dateSize?: number;
@@ -181,14 +191,23 @@ export type CustomStyleProps = {
top?: number;
right?: number;
};
- deleteIcon?:{
- size?: number
+ deleteIcon?: {
+ size?: number;
};
- timerIcon?:{
- size?: number
+ timerIcon?: {
+ size?: number;
};
- clearAllIcon?:{
- size?: number
+ clearAllIcon?: {
+ size?: number;
+ };
+ tabs?: {
+ containerHeight?: number;
+ activeTabTextSize?: number;
+ inactiveTabTextSize?: number;
+ activeTabTextWeight?: TextStyle['fontWeight'];
+ inactiveTabTextWeight?: TextStyle['fontWeight'];
+ indicatorHeight?: number;
+ tabPadding?: number;
};
};
@@ -248,4 +267,16 @@ export type StyleProps = {
highlighted: ViewStyle;
backIcon: ViewStyle;
mediaContainer: ViewStyle;
+ tabContainer: ViewStyle;
+ tab: ViewStyle;
+ activeTab: ViewStyle;
+ inActiveTab: ViewStyle;
+ activeTabText: TextStyle | object;
+ inActiveTabText: TextStyle | object;
+ activeIndicator: ViewStyle;
+};
+
+export type TabProps = {
+ tabs: Array<{ key: string; title: string }>;
+ activeTab: number;
};
diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts
index 4ba25a7..721e2a9 100644
--- a/src/utils/commonUtils.ts
+++ b/src/utils/commonUtils.ts
@@ -285,4 +285,35 @@ export const applyTheme = (
mediaContainer: {
backgroundColor: DefaultTheme[mode].notificationCard.mediaContainerBackground
},
+ tabContainer: {
+ backgroundColor:
+ theme.tabs?.containerBackgroundColor || DefaultTheme[mode].tabs.containerBackgroundColor,
+ borderBottomColor: theme.colors?.borderColor || DefaultTheme[mode].colors.borderColor,
+ height: customStyles.tabs?.containerHeight || defaultStyles.tabs.containerHeight
+ },
+ tab: {
+ paddingHorizontal: customStyles.tabs?.tabPadding || defaultStyles.tabs.tabPadding,
+ },
+ activeTab: {
+ backgroundColor:
+ theme.tabs?.activeTabBackgroundColor || DefaultTheme[mode].tabs.activeTabBackgroundColor
+ },
+ inActiveTab: {
+ backgroundColor:
+ theme.tabs?.inactiveTabBackgroundColor || DefaultTheme[mode].tabs.inactiveTabBackgroundColor
+ },
+ activeTabText: {
+ color: theme.tabs?.activeTabTextColor || DefaultTheme[mode].tabs.activeTabTextColor,
+ fontSize: customStyles.tabs?.activeTabTextSize || defaultStyles.tabs.activeTabTextSize,
+ fontWeight: customStyles.tabs?.activeTabTextWeight || defaultStyles.tabs.activeTabTextWeight
+ },
+ inActiveTabText: {
+ color: theme.tabs?.inactiveTabTextColor || DefaultTheme[mode].tabs.inactiveTabTextColor,
+ fontSize: customStyles.tabs?.inactiveTabTextSize || defaultStyles.tabs.inactiveTabTextSize,
+ fontWeight: customStyles.tabs?.inactiveTabTextWeight || defaultStyles.tabs.inactiveTabTextWeight
+ },
+ activeIndicator: {
+ backgroundColor: theme.tabs?.indicatorColor || DefaultTheme[mode].tabs.indicatorColor,
+ height: customStyles.tabs?.indicatorHeight || defaultStyles.tabs.indicatorHeight
+ }
});
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 99210e5..99b384e 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -82,6 +82,11 @@ export enum events {
NOTIFICATION_COUNT_EVENT = 'NOTIFICATION_COUNT_EVENT'
}
+export enum FilterTypes {
+ ALL = 'All',
+ UNREAD = 'Unread',
+}
+
export const LIST_EMPTY_TEXT = 'No new notifications';
export const LIST_EMPTY_DESCRIPTION = 'Check back later for updates and alerts';
export const ERROR_TEXT = 'Oops! Something went wrong.';
@@ -154,5 +159,14 @@ export const defaultStyles = {
},
clearAllIcon: {
size: 16
- }
+ },
+ tabs: {
+ containerHeight: 46,
+ activeTabTextSize: 14,
+ inactiveTabTextSize: 14,
+ activeTabTextWeight: '500',
+ inactiveTabTextWeight: '500',
+ indicatorHeight: 3,
+ tabPadding: 5,
+ },
};
diff --git a/src/utils/defaultTheme.ts b/src/utils/defaultTheme.ts
index 273e381..035bb7a 100644
--- a/src/utils/defaultTheme.ts
+++ b/src/utils/defaultTheme.ts
@@ -31,6 +31,14 @@ const defaultTheme = {
descriptionColor: COLORS[ThemeMode.LIGHT].textColor,
dateColor: COLORS[ThemeMode.LIGHT].textColor,
mediaContainerBackground: COLORS[ThemeMode.LIGHT].imageBackground,
+ },
+ tabs: {
+ containerBackgroundColor: COLORS[ThemeMode.LIGHT].neutralColor,
+ activeTabBackgroundColor: COLORS[ThemeMode.LIGHT].neutralColor,
+ inactiveTabBackgroundColor: COLORS[ThemeMode.LIGHT].neutralColor,
+ activeTabTextColor: COLORS[ThemeMode.LIGHT].primaryColor,
+ inactiveTabTextColor: COLORS[ThemeMode.LIGHT].textColor,
+ indicatorColor: COLORS[ThemeMode.LIGHT].primaryColor,
}
},
dark: {
@@ -63,6 +71,14 @@ const defaultTheme = {
descriptionColor: COLORS[ThemeMode.DARK].textColor,
dateColor: COLORS[ThemeMode.DARK].dateColor,
mediaContainerBackground: COLORS[ThemeMode.DARK].imageBackground,
+ },
+ tabs: {
+ containerBackgroundColor: COLORS[ThemeMode.DARK].neutralColor,
+ activeTabBackgroundColor: COLORS[ThemeMode.DARK].neutralColor,
+ inactiveTabBackgroundColor: COLORS[ThemeMode.DARK].neutralColor,
+ activeTabTextColor: COLORS[ThemeMode.DARK].primaryColor,
+ inactiveTabTextColor: COLORS[ThemeMode.DARK].textColor,
+ indicatorColor: COLORS[ThemeMode.DARK].primaryColor,
}
}
};