From 720514c9c554fe6c0644518166c859c210dafad3 Mon Sep 17 00:00:00 2001 From: Abhay Date: Mon, 13 May 2024 21:29:58 +0530 Subject: [PATCH 01/19] feat: Implemented UI of Tab component with customizable props --- src/components/sirenInbox.tsx | 38 ++++++++++---- src/components/tabs.tsx | 93 +++++++++++++++++++++++++++++++++++ src/types.ts | 43 +++++++++++++--- src/utils/commonUtils.ts | 28 +++++++++++ src/utils/constants.ts | 10 +++- src/utils/defaultTheme.ts | 16 ++++++ 6 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 src/components/tabs.tsx diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index ecea810..6ea6671 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -5,15 +5,16 @@ import PubSub from 'pubsub-js'; import type { Siren } from '@sirenapp/js-sdk'; import type { NotificationDataType, SirenErrorType } from '@sirenapp/js-sdk/dist/esm/types'; +import { useSirenContext } from './sirenProvider'; +import type { SirenInboxProps } from '../types'; +import { CommonUtils, Constants, useSiren } from '../utils'; 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 './tabs'; const { DEFAULT_WINDOW_TITLE, @@ -78,7 +79,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, - hideMediaThumbnail: false, + hideMediaThumbnail: false }, listEmptyComponent = null, headerProps = {}, @@ -88,7 +89,15 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { customCard = null, onCardClick = () => null, onError = () => {}, - itemsPerFetch = 20 + itemsPerFetch = 20, + hideTab = false, + tabProps = { + tabs: [ + { key: 'All', title: 'All' }, + { key: 'Unread', title: 'Unread' } + ], + activeTab: 0 + } } = props; const { @@ -132,7 +141,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { // 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([]); @@ -194,7 +203,10 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { notificationParams.start = allNotifications[0].createdAt; if (verificationStatus === VerificationStatus.SUCCESS) - siren?.startRealTimeFetch({eventType: EventType.NOTIFICATION, params: notificationParams}); + siren?.startRealTimeFetch({ + eventType: EventType.NOTIFICATION, + params: notificationParams + }); } }; @@ -284,7 +296,10 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { notificationParams.start = allNotifications[0].createdAt; if (verificationStatus === VerificationStatus.SUCCESS) - siren?.startRealTimeFetch({eventType: EventType.NOTIFICATION, params:notificationParams}); + siren?.startRealTimeFetch({ + eventType: EventType.NOTIFICATION, + params: notificationParams + }); } catch (err) { setIsLoading(false); setIsError(true); @@ -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,6 +419,10 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { ); }; + const renderTabs = (): JSX.Element | null => { + return ; + }; + const keyExtractor = (item: NotificationDataType) => item.id; const renderList = (): JSX.Element => { @@ -428,6 +447,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { return ( {renderHeader()} + {!hideTab && renderTabs()} {isNonEmptyArray(notifications) ? renderList() : renderListEmpty()} {customFooter || null} diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx new file mode 100644 index 0000000..a6086e1 --- /dev/null +++ b/src/components/tabs.tsx @@ -0,0 +1,93 @@ +import React, { useState, type ReactElement } from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +import type { StyleProps } from '../types'; + +/** + * + * @component + * @example + * + * + * @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 renderd. + */ + +type TabProps = { + activeIndex?: number; + tabs: Array<{ key: string; title: string }>; + styles: Partial; + onPressTab?: (index: number, key: string) => void; +}; + +const Tabs = (props: TabProps): ReactElement => { + const { tabs, activeIndex = 0, styles, onPressTab = () => null } = props; + + const [activeTabIndex, setActiveTabIndex] = useState(activeIndex); + + const onPressTabItem = (index: number, key: string) => { + setActiveTabIndex(index); + onPressTab(index, key); + }; + + return ( + + {tabs.map((tab, index) => ( + onPressTabItem(index, tab?.key)} + > + + {tab.title} + + {activeTabIndex === index && ( + + )} + + ))} + + ); +}; + +const style = StyleSheet.create({ + tabContainer: { + width: '100%', + flexDirection: 'row', + height: 46, + paddingHorizontal: 10, + borderBottomWidth: 0.6 + }, + tab: { + justifyContent: 'center', + alignItems: 'center', + minWidth: 30, + marginHorizontal: 5 + }, + tabText: { + color: '#000', + fontWeight: '600', + paddingHorizontal: 14, + fontSize: 14 + }, + activeIndicator: { + width: '100%', + height: 3, + backgroundColor: '#000', + position: 'absolute', + bottom: 0 + } +}); + +export default Tabs; diff --git a/src/types.ts b/src/types.ts index 15ae4c9..719d97a 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,22 @@ 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; }; }; @@ -248,4 +266,15 @@ export type StyleProps = { highlighted: ViewStyle; backIcon: ViewStyle; mediaContainer: ViewStyle; + tabContainer: 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 f2da6d4..dcf1b0b 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -281,4 +281,32 @@ 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 + }, + 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..59658c8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -154,5 +154,13 @@ export const defaultStyles = { }, clearAllIcon: { size: 16 - } + }, + tabs: { + containerHeight: 46, + activeTabTextSize: 14, + inactiveTabTextSize: 14, + activeTabTextWeight: '500', + inactiveTabTextWeight: '500', + indicatorHeight: 3, + }, }; diff --git a/src/utils/defaultTheme.ts b/src/utils/defaultTheme.ts index 273e381..36c2f6d 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.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, } } }; From 42a6afbebd14c066e0ec3b87d8da83e1e9d659c7 Mon Sep 17 00:00:00 2001 From: Abhay Date: Tue, 14 May 2024 17:32:15 +0530 Subject: [PATCH 02/19] fix: updated tab indicator animation --- src/components/sirenInbox.tsx | 22 +++++++-- src/components/tabs.tsx | 90 ++++++++++++++++++++++++++--------- 2 files changed, 86 insertions(+), 26 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 6ea6671..32a37be 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -32,6 +32,7 @@ type fetchProps = { size: number; end?: string; start?: string; + isRead?: boolean; }; type NotificationFetchParams = { @@ -39,6 +40,7 @@ type NotificationFetchParams = { end?: string; start?: string; sort?: 'createdAt' | 'updatedAt'; + isRead?: boolean; }; /** @@ -94,7 +96,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { tabProps = { tabs: [ { key: 'All', title: 'All' }, - { key: 'Unread', title: 'Unread' } + { key: 'Unread', title: 'Unread' }, + { key: 'NewNotification', title: 'NewNotification' } ], activeTab: 0 } @@ -122,6 +125,7 @@ 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 [eventListenerData, setEventListenerData] = useState<{ id?: string; action: string; @@ -147,7 +151,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { setNotifications([]); if (onError) onError(errorMap.INVALID_CREDENTIALS); } - }, [siren, verificationStatus]); + }, [siren, verificationStatus, filterType]); useEffect(() => { if (eventListenerData) { @@ -202,6 +206,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { if (isNonEmptyArray(allNotifications)) notificationParams.start = allNotifications[0].createdAt; + if (filterType === 'Unread') notificationParams.isRead = false; + if (verificationStatus === VerificationStatus.SUCCESS) siren?.startRealTimeFetch({ eventType: EventType.NOTIFICATION, @@ -213,9 +219,11 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const generateNotificationParams = (attachEndDate: boolean): fetchProps => { const notificationParams: NotificationFetchParams = { size: notificationsPerPage, - sort: 'createdAt' + sort: 'createdAt', }; + if(filterType === 'Unread') notificationParams.isRead = false; + if (attachEndDate) notificationParams.end = notifications[notifications.length - 1].createdAt; return notificationParams; @@ -279,6 +287,10 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { [theme, darkMode, customStyles] ); + const onPressTab = (index: number, key: string) => { + setFilterType(key); + } + // Refresh notifications const onRefresh = async (): Promise => { if (siren) @@ -295,6 +307,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { if (isNonEmptyArray(allNotifications)) notificationParams.start = allNotifications[0].createdAt; + if (filterType === 'Unread') notificationParams.isRead = false; + if (verificationStatus === VerificationStatus.SUCCESS) siren?.startRealTimeFetch({ eventType: EventType.NOTIFICATION, @@ -420,7 +434,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { }; const renderTabs = (): JSX.Element | null => { - return ; + return ; }; const keyExtractor = (item: NotificationDataType) => item.id; diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx index a6086e1..228c5e5 100644 --- a/src/components/tabs.tsx +++ b/src/components/tabs.tsx @@ -1,5 +1,13 @@ -import React, { useState, type ReactElement } from 'react'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import React, { useEffect, useRef, useState, type ReactElement } from 'react'; +import { + Animated, + type LayoutChangeEvent, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View +} from 'react-native'; import type { StyleProps } from '../types'; @@ -27,36 +35,75 @@ type TabProps = { const Tabs = (props: TabProps): ReactElement => { const { tabs, activeIndex = 0, styles, onPressTab = () => null } = props; + const translateX = useRef(new Animated.Value(0)).current; const [activeTabIndex, setActiveTabIndex] = useState(activeIndex); + const [tabTitleWidths, setTabTitleWidths] = useState([]); + + useEffect(() => { + moveIndicator(); + }, [activeTabIndex]); const onPressTabItem = (index: number, key: string) => { setActiveTabIndex(index); onPressTab(index, key); }; + const getIndicatorPosition = () => { + let position = 5; + + for (let i = 0; i < activeTabIndex; i++) + position += tabTitleWidths[i] + 10; + + return position; + }; + + const moveIndicator = () => { + Animated.timing(translateX, { + toValue: getIndicatorPosition(), + duration: 300, + useNativeDriver: true + }).start(); + }; + + const onTabTitleLayout = (index: number, event: LayoutChangeEvent) => { + const { width } = event.nativeEvent.layout; + + const updatedWidths = [...tabTitleWidths]; + + updatedWidths[index] = width; + setTabTitleWidths(updatedWidths); + }; + return ( - {tabs.map((tab, index) => ( - onPressTabItem(index, tab?.key)} - > - + {tabs.map((tab, index) => ( + onPressTabItem(index, tab?.key)} + onLayout={(event) => onTabTitleLayout(index, event)} > - {tab.title} - - {activeTabIndex === index && ( - - )} - - ))} + + {tab.title} + + + ))} + + ); }; @@ -82,7 +129,6 @@ const style = StyleSheet.create({ fontSize: 14 }, activeIndicator: { - width: '100%', height: 3, backgroundColor: '#000', position: 'absolute', From b843f3c29a5baf98ce135f64365f2066acb473c1 Mon Sep 17 00:00:00 2001 From: Abhay Date: Thu, 16 May 2024 18:08:30 +0530 Subject: [PATCH 03/19] fix: Swipe functionality implement in tabs --- src/components/sirenInbox.tsx | 54 +++++++++++++++++++++++++++++++---- src/components/tabs.tsx | 4 +++ src/utils/defaultTheme.ts | 12 ++++---- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 32a37be..40944e9 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -1,5 +1,5 @@ import React, { type ReactElement, useEffect, useMemo, useState, useRef } from 'react'; -import { FlatList, StyleSheet, View } from 'react-native'; +import { Animated, FlatList, PanResponder, StyleSheet, View } from 'react-native'; import PubSub from 'pubsub-js'; import type { Siren } from '@sirenapp/js-sdk'; @@ -96,8 +96,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { tabProps = { tabs: [ { key: 'All', title: 'All' }, - { key: 'Unread', title: 'Unread' }, - { key: 'NewNotification', title: 'NewNotification' } + { key: 'Unread', title: 'Unread' } ], activeTab: 0 } @@ -126,6 +125,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { 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; @@ -134,6 +134,26 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { } | null>(null); const disableCardDelete = useRef(false); + const pan = useRef(new Animated.ValueXY()).current; + const panResponder = useRef( + PanResponder.create({ + onMoveShouldSetPanResponder: () => true, + onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], {useNativeDriver: false}), + onPanResponderRelease: (e) => { + Animated.timing(pan, { + toValue: { x: 0, y: 0 }, + duration: 300, + useNativeDriver: false + }).start(); + + if(e.nativeEvent.pageX === 0) + setFilterType(tabProps.tabs[1].key); + else + setFilterType(tabProps.tabs[0].key); + } + }) + ).current; + useEffect(() => { PubSub.subscribe(`${events.NOTIFICATION_LIST_EVENT}${id}`, notificationSubscriber); @@ -141,6 +161,12 @@ 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) { @@ -199,6 +225,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { // Initialize Siren SDK and fetch notifications const initialize = async (): Promise => { if (siren) { + setNotifications([]); siren?.stopRealTimeFetch(EventType.NOTIFICATION); const allNotifications = await fetchNotifications(siren, true); const notificationParams: fetchProps = { size: notificationsPerPage }; @@ -289,7 +316,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const onPressTab = (index: number, key: string) => { setFilterType(key); - } + setActiveTab(index); + }; // Refresh notifications const onRefresh = async (): Promise => { @@ -434,7 +462,14 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { }; const renderTabs = (): JSX.Element | null => { - return ; + return ( + + ); }; const keyExtractor = (item: NotificationDataType) => item.id; @@ -462,7 +497,14 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { {renderHeader()} {!hideTab && renderTabs()} - {isNonEmptyArray(notifications) ? renderList() : renderListEmpty()} + + {isNonEmptyArray(notifications) ? renderList() : renderListEmpty()} + {customFooter || null} ); diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx index 228c5e5..ca0f4fe 100644 --- a/src/components/tabs.tsx +++ b/src/components/tabs.tsx @@ -44,6 +44,10 @@ const Tabs = (props: TabProps): ReactElement => { moveIndicator(); }, [activeTabIndex]); + useEffect(() => { + setActiveTabIndex(activeIndex); + },[activeIndex]); + const onPressTabItem = (index: number, key: string) => { setActiveTabIndex(index); onPressTab(index, key); diff --git a/src/utils/defaultTheme.ts b/src/utils/defaultTheme.ts index 36c2f6d..035bb7a 100644 --- a/src/utils/defaultTheme.ts +++ b/src/utils/defaultTheme.ts @@ -73,12 +73,12 @@ const defaultTheme = { mediaContainerBackground: COLORS[ThemeMode.DARK].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, + 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, } } }; From 9fab5f6aae6d7043043ef7945f987fd4b4f96c07 Mon Sep 17 00:00:00 2001 From: Abhay Date: Fri, 17 May 2024 11:43:19 +0530 Subject: [PATCH 04/19] fix: ui changes --- src/components/sirenInbox.tsx | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 40944e9..82a14fd 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -138,7 +138,9 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: () => true, - onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], {useNativeDriver: false}), + onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], { + useNativeDriver: false + }), onPanResponderRelease: (e) => { Animated.timing(pan, { toValue: { x: 0, y: 0 }, @@ -146,15 +148,12 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { useNativeDriver: false }).start(); - if(e.nativeEvent.pageX === 0) - setFilterType(tabProps.tabs[1].key); - else - setFilterType(tabProps.tabs[0].key); + if (e.nativeEvent.pageX === 0) setFilterType(tabProps.tabs[1].key); + else setFilterType(tabProps.tabs[0].key); } }) ).current; - useEffect(() => { PubSub.subscribe(`${events.NOTIFICATION_LIST_EVENT}${id}`, notificationSubscriber); @@ -165,7 +164,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const updatedActiveIndex = tabProps.tabs.findIndex((tab) => tab.key === filterType); setActiveTab(updatedActiveIndex); - },[filterType]); + }, [filterType]); useEffect(() => { // Initialize Siren SDK and start polling notifications @@ -246,10 +245,10 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const generateNotificationParams = (attachEndDate: boolean): fetchProps => { const notificationParams: NotificationFetchParams = { size: notificationsPerPage, - sort: 'createdAt', + sort: 'createdAt' }; - if(filterType === 'Unread') notificationParams.isRead = false; + if (filterType === 'Unread') notificationParams.isRead = false; if (attachEndDate) notificationParams.end = notifications[notifications.length - 1].createdAt; @@ -463,12 +462,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const renderTabs = (): JSX.Element | null => { return ( - + ); }; @@ -498,9 +492,12 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { {renderHeader()} {!hideTab && renderTabs()} {isNonEmptyArray(notifications) ? renderList() : renderListEmpty()} @@ -514,6 +511,9 @@ const style = StyleSheet.create({ container: { minWidth: 300, flex: 1 + }, + swipeContainer: { + flex: 1 } }); From d9add42bedf61870390d818a2e9ff1bfa3c3cc97 Mon Sep 17 00:00:00 2001 From: Abhay Date: Wed, 22 May 2024 15:27:57 +0530 Subject: [PATCH 05/19] fix: enhanced swipe logic --- src/components/loadingWindow.tsx | 6 +++++- src/components/sirenInbox.tsx | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/loadingWindow.tsx b/src/components/loadingWindow.tsx index 3b7d3d5..b14e5d2 100644 --- a/src/components/loadingWindow.tsx +++ b/src/components/loadingWindow.tsx @@ -103,7 +103,11 @@ const LoadingWindow = (props: LoadingWindowProps): ReactElement => { return ( {customLoader || ( - + )} ); diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 82a14fd..7401aa4 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -134,22 +134,25 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { } | null>(null); const disableCardDelete = useRef(false); + const SWIPE_THRESHOLD= 50; const pan = useRef(new Animated.ValueXY()).current; const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: () => true, - onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], { + onPanResponderMove: Animated.event([null, { dx: pan.x}], { useNativeDriver: false }), onPanResponderRelease: (e) => { + const { locationX, pageX } = e.nativeEvent; + const dx = locationX - pageX; + + if (Math.abs(dx) > SWIPE_THRESHOLD) + setFilterType(dx > 0 ? tabProps.tabs[0].key : tabProps.tabs[1].key); // Set filter based on swipe direction (right: All, left: Unread) Animated.timing(pan, { toValue: { x: 0, y: 0 }, duration: 300, useNativeDriver: false }).start(); - - if (e.nativeEvent.pageX === 0) setFilterType(tabProps.tabs[1].key); - else setFilterType(tabProps.tabs[0].key); } }) ).current; @@ -482,6 +485,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { removeClippedSubviews maxToRenderPerBatch={20} windowSize={3} + showsVerticalScrollIndicator={false} accessibilityLabel='siren-notification-list' /> ); From 3bc7d900c3e79953e36333ea6d037c5cd23e31dd Mon Sep 17 00:00:00 2001 From: Abhay Date: Thu, 23 May 2024 12:34:41 +0530 Subject: [PATCH 06/19] fix: updated tab view --- src/components/sirenInbox.tsx | 102 +++++++++++++++++---------- src/components/{tabs.tsx => tab.tsx} | 91 +++++++++++++++--------- src/components/tabContainer.tsx | 89 +++++++++++++++++++++++ 3 files changed, 209 insertions(+), 73 deletions(-) rename src/components/{tabs.tsx => tab.tsx} (55%) create mode 100644 src/components/tabContainer.tsx diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 7401aa4..d4d5076 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -1,5 +1,5 @@ import React, { type ReactElement, useEffect, useMemo, useState, useRef } from 'react'; -import { Animated, FlatList, PanResponder, StyleSheet, View } from 'react-native'; +import { FlatList, StyleSheet, View } from 'react-native'; import PubSub from 'pubsub-js'; import type { Siren } from '@sirenapp/js-sdk'; @@ -14,7 +14,7 @@ import ErrorWindow from './errorWindow'; import Header from './header'; import LoadingWindow from './loadingWindow'; import Spinner from './spinner'; -import Tabs from './tabs'; +import Tabs from './tab'; const { DEFAULT_WINDOW_TITLE, @@ -96,7 +96,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { tabProps = { tabs: [ { key: 'All', title: 'All' }, - { key: 'Unread', title: 'Unread' } + { key: 'Unread', title: 'Unread' }, + { key: 'newNotifications', title: 'NewNotifications' } ], activeTab: 0 } @@ -134,28 +135,6 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { } | null>(null); const disableCardDelete = useRef(false); - const SWIPE_THRESHOLD= 50; - const pan = useRef(new Animated.ValueXY()).current; - const panResponder = useRef( - PanResponder.create({ - onMoveShouldSetPanResponder: () => true, - onPanResponderMove: Animated.event([null, { dx: pan.x}], { - useNativeDriver: false - }), - onPanResponderRelease: (e) => { - const { locationX, pageX } = e.nativeEvent; - const dx = locationX - pageX; - - if (Math.abs(dx) > SWIPE_THRESHOLD) - setFilterType(dx > 0 ? tabProps.tabs[0].key : tabProps.tabs[1].key); // Set filter based on swipe direction (right: All, left: Unread) - Animated.timing(pan, { - toValue: { x: 0, y: 0 }, - duration: 300, - useNativeDriver: false - }).start(); - } - }) - ).current; useEffect(() => { PubSub.subscribe(`${events.NOTIFICATION_LIST_EVENT}${id}`, notificationSubscriber); @@ -316,7 +295,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { [theme, darkMode, customStyles] ); - const onPressTab = (index: number, key: string) => { + const onChangeTab = (index: number, key: string) => { setFilterType(key); setActiveTab(index); }; @@ -465,15 +444,73 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const renderTabs = (): JSX.Element | null => { return ( - + ); }; const keyExtractor = (item: NotificationDataType) => item.id; const renderList = (): JSX.Element => { + if(notifications.length <= 0) + renderListEmpty(); + + return ( + + ); + }; + + const renderList2 = (): JSX.Element => { + if(notifications.length <= 0) + renderListEmpty(); + + return ( + + ); + }; + + const renderList3 = (): JSX.Element => { + if(notifications.length <= 0) + renderListEmpty(); + return ( { {renderHeader()} {!hideTab && renderTabs()} - - {isNonEmptyArray(notifications) ? renderList() : renderListEmpty()} - {customFooter || null} ); diff --git a/src/components/tabs.tsx b/src/components/tab.tsx similarity index 55% rename from src/components/tabs.tsx rename to src/components/tab.tsx index ca0f4fe..c6e465f 100644 --- a/src/components/tabs.tsx +++ b/src/components/tab.tsx @@ -10,6 +10,7 @@ import { } from 'react-native'; import type { StyleProps } from '../types'; +import TabContainer from './tabContainer'; /** * @@ -30,11 +31,22 @@ type TabProps = { activeIndex?: number; tabs: Array<{ key: string; title: string }>; styles: Partial; - onPressTab?: (index: number, key: string) => void; + onChangeTabItem?: (index: number, key: string) => void; + screens?: ReactElement[]; }; +const defaultScreens = ['red', 'green', 'blue'].map((color) => ( + +)); + const Tabs = (props: TabProps): ReactElement => { - const { tabs, activeIndex = 0, styles, onPressTab = () => null } = props; + const { + tabs, + activeIndex = 0, + styles, + onChangeTabItem = () => null, + screens = defaultScreens + } = props; const translateX = useRef(new Animated.Value(0)).current; const [activeTabIndex, setActiveTabIndex] = useState(activeIndex); @@ -46,18 +58,17 @@ const Tabs = (props: TabProps): ReactElement => { useEffect(() => { setActiveTabIndex(activeIndex); - },[activeIndex]); + }, [activeIndex]); - const onPressTabItem = (index: number, key: string) => { + const onChangeTab = (index: number) => { setActiveTabIndex(index); - onPressTab(index, key); + onChangeTabItem(index, tabs[index].key); }; const getIndicatorPosition = () => { let position = 5; - for (let i = 0; i < activeTabIndex; i++) - position += tabTitleWidths[i] + 10; + for (let i = 0; i < activeTabIndex; i++) position += tabTitleWidths[i] + 10; return position; }; @@ -74,45 +85,55 @@ const Tabs = (props: TabProps): ReactElement => { const { width } = event.nativeEvent.layout; const updatedWidths = [...tabTitleWidths]; - + updatedWidths[index] = width; setTabTitleWidths(updatedWidths); }; return ( - - - {tabs.map((tab, index) => ( - onPressTabItem(index, tab?.key)} - onLayout={(event) => onTabTitleLayout(index, event)} - > - + + + {tabs.map((tab, index) => ( + onChangeTab(index)} + onLayout={(event) => onTabTitleLayout(index, event)} > - {tab.title} - - - ))} - - + + {tab.title} + + + ))} + + + + + + ); }; const style = StyleSheet.create({ + container: { + flex: 1, + height: '100%', + width: '100%' + }, tabContainer: { width: '100%', flexDirection: 'row', diff --git a/src/components/tabContainer.tsx b/src/components/tabContainer.tsx new file mode 100644 index 0000000..bcd5b51 --- /dev/null +++ b/src/components/tabContainer.tsx @@ -0,0 +1,89 @@ +import React, { useRef } from 'react'; +import { Dimensions, StyleSheet, View, Animated, PanResponder } from 'react-native'; + +const { width } = Dimensions.get('window'); + +const getClosestSnapPoint = (value: number, points: number[]) => { + const point = points.reduce((prev, curr) => + Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev + ); + + return point; +}; + +const TabContainer = ({ + screens = [], + + onChangeTab +}: { + screens?: React.ReactNode[]; + onChangeTab: (index: number) => void; +}) => { + const translateX = useRef(new Animated.Value(0)).current; + const offsetX = useRef(0); + const index = useRef(0); + + const snapPoints = screens.map((_, i) => i * -width); + + const panResponder = useRef( + PanResponder.create({ + onMoveShouldSetPanResponder: () => true, + onPanResponderMove: (_, gestureState) => { + if (index.current === 0 && gestureState.dx > 0) + return; + + if (index.current === screens.length - 1 && gestureState.dx < 0) + return; + + translateX.setValue(offsetX.current + gestureState.dx); + }, + onPanResponderRelease: (_, gestureState) => { + const currentTranslateX = offsetX.current + gestureState.dx; + const toValue = getClosestSnapPoint(currentTranslateX, snapPoints); + + Animated.timing(translateX, { + toValue, + duration: 300, + useNativeDriver: false + }).start(() => { + offsetX.current = toValue; + index.current = Math.floor(-toValue / width); + onChangeTab(index.current); + translateX.setValue(toValue); + }); + } + }) + ).current; + + return ( + + + + {screens.map((screen, index) => ( + + {screen} + + ))} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject + }, + tabScreens: { + flexDirection: 'row' + }, + tabScreen: { + width, + overflow: 'hidden' + } +}); + +export default TabContainer; From bf277c1c97a30f0f59f309d920c9bd6903a2d287 Mon Sep 17 00:00:00 2001 From: Abhay Date: Thu, 23 May 2024 14:08:45 +0530 Subject: [PATCH 07/19] fix: optimize swipe --- src/components/sirenInbox.tsx | 75 ++++++++++------------------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index d4d5076..6bd96d1 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -337,7 +337,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { }; // Render empty window, error window, or custom empty component - const renderListEmpty = (): JSX.Element | null => { + const renderListEmpty = (): JSX.Element => { if (!isLoading) { if (isError) return ( @@ -442,11 +442,17 @@ 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 => { - if(notifications.length <= 0) - renderListEmpty(); - - return ( - - ); - }; - - const renderList2 = (): JSX.Element => { - if(notifications.length <= 0) - renderListEmpty(); - - return ( - - ); - }; + const renderList = (isActiveTab: boolean, key: string): JSX.Element => { + if (!isActiveTab) + return ( + + ); - const renderList3 = (): JSX.Element => { - if(notifications.length <= 0) - renderListEmpty(); + if (notifications.length === 0) return renderListEmpty(); return ( Date: Fri, 24 May 2024 11:48:04 +0530 Subject: [PATCH 08/19] fix: improve tab performance --- src/components/sirenInbox.tsx | 59 +++++++++++++++------------------ src/components/tab.tsx | 49 +++++++++++++++++++++------ src/components/tabContainer.tsx | 13 ++++++-- 3 files changed, 74 insertions(+), 47 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 6bd96d1..8ad22fb 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -336,31 +336,6 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { if (fetchMore) fetchNotifications(siren, false); }; - // Render empty window, error window, or custom empty component - const renderListEmpty = (): JSX.Element => { - if (!isLoading) { - if (isError) - return ( - - ); - - return ( - - {listEmptyComponent || } - - ); - } - - return ( - - ); - }; - const onDelete = async (id: string, shouldUpdateList: boolean): Promise => { let isSuccess = false; @@ -463,17 +438,31 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const keyExtractor = (item: NotificationDataType) => item.id; const renderList = (isActiveTab: boolean, key: string): JSX.Element => { - if (!isActiveTab) + if (isLoading || !isActiveTab) return ( - + + + + ); + + if (isError) + return ( + ); - if (notifications.length === 0) return renderListEmpty(); + if (!isNonEmptyArray(notifications)) + return ( + + + {listEmptyComponent || } + + + ); return ( ( - +const defaultScreensColors = ['red', 'green', 'blue']; + +const defaultScreens = defaultScreensColors.map((color, index) => ( + )); const Tabs = (props: TabProps): ReactElement => { @@ -48,6 +50,7 @@ const Tabs = (props: TabProps): ReactElement => { screens = defaultScreens } = props; const translateX = useRef(new Animated.Value(0)).current; + const scaleWidth = useRef(new Animated.Value(0)).current; const [activeTabIndex, setActiveTabIndex] = useState(activeIndex); const [tabTitleWidths, setTabTitleWidths] = useState([]); @@ -69,16 +72,27 @@ const Tabs = (props: TabProps): ReactElement => { let position = 5; for (let i = 0; i < activeTabIndex; i++) position += tabTitleWidths[i] + 10; + position += (tabTitleWidths[activeTabIndex] || 0) / 2; return position; }; const moveIndicator = () => { - Animated.timing(translateX, { - toValue: getIndicatorPosition(), - duration: 300, - useNativeDriver: true - }).start(); + const position = getIndicatorPosition(); + const width = tabTitleWidths[activeTabIndex] || 0; + + Animated.parallel([ + Animated.timing(translateX, { + toValue: position, + duration: 200, + useNativeDriver: true + }), + Animated.timing(scaleWidth, { + toValue: width, + duration: 200, + useNativeDriver: true + }) + ]).start(); }; const onTabTitleLayout = (index: number, event: LayoutChangeEvent) => { @@ -88,10 +102,15 @@ const Tabs = (props: TabProps): ReactElement => { updatedWidths[index] = width; setTabTitleWidths(updatedWidths); + + if (activeIndex === index) { + scaleWidth.setValue(width * 2); + translateX.setValue(getIndicatorPosition()); + } }; - return ( - + const renderTabHeader = () => { + return ( {tabs.map((tab, index) => ( @@ -116,13 +135,20 @@ const Tabs = (props: TabProps): ReactElement => { style={[ style.activeIndicator, styles.activeIndicator, - { transform: [{ translateX }], width: tabTitleWidths[activeTabIndex] } + { transform: [{ translateX }, { scaleX: scaleWidth }] } ]} /> + ); + }; + + return ( + + {renderTabHeader()} + - + ); @@ -154,6 +180,7 @@ const style = StyleSheet.create({ fontSize: 14 }, activeIndicator: { + width: 1, height: 3, backgroundColor: '#000', position: 'absolute', diff --git a/src/components/tabContainer.tsx b/src/components/tabContainer.tsx index bcd5b51..f0cd8de 100644 --- a/src/components/tabContainer.tsx +++ b/src/components/tabContainer.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import { Dimensions, StyleSheet, View, Animated, PanResponder } from 'react-native'; const { width } = Dimensions.get('window'); @@ -13,16 +13,23 @@ const getClosestSnapPoint = (value: number, points: number[]) => { const TabContainer = ({ screens = [], - + activeScreenIndex, onChangeTab }: { screens?: React.ReactNode[]; + activeScreenIndex: number; onChangeTab: (index: number) => void; }) => { const translateX = useRef(new Animated.Value(0)).current; const offsetX = useRef(0); const index = useRef(0); + useEffect(() => { + translateX.setValue(-activeScreenIndex * width); + offsetX.current = -activeScreenIndex * width; + index.current = activeScreenIndex; + }, [activeScreenIndex]); + const snapPoints = screens.map((_, i) => i * -width); const panResponder = useRef( @@ -43,7 +50,7 @@ const TabContainer = ({ Animated.timing(translateX, { toValue, - duration: 300, + duration: 200, useNativeDriver: false }).start(() => { offsetX.current = toValue; From 9e0b39560d0986a60e744a8e5d6c1a4368785e9e Mon Sep 17 00:00:00 2001 From: Abhay Date: Mon, 27 May 2024 10:32:42 +0530 Subject: [PATCH 09/19] fix: updated swipe logic --- src/components/sirenInbox.tsx | 1 + src/components/tabContainer.tsx | 82 +++++++++------------------------ 2 files changed, 24 insertions(+), 59 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 8ad22fb..0f24400 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -296,6 +296,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { ); const onChangeTab = (index: number, key: string) => { + setIsLoading(true); setFilterType(key); setActiveTab(index); }; diff --git a/src/components/tabContainer.tsx b/src/components/tabContainer.tsx index f0cd8de..4fc5c4e 100644 --- a/src/components/tabContainer.tsx +++ b/src/components/tabContainer.tsx @@ -1,16 +1,8 @@ import React, { useEffect, useRef } from 'react'; -import { Dimensions, StyleSheet, View, Animated, PanResponder } from 'react-native'; +import { Dimensions, StyleSheet, View, ScrollView, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native'; const { width } = Dimensions.get('window'); -const getClosestSnapPoint = (value: number, points: number[]) => { - const point = points.reduce((prev, curr) => - Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev - ); - - return point; -}; - const TabContainer = ({ screens = [], activeScreenIndex, @@ -20,62 +12,37 @@ const TabContainer = ({ activeScreenIndex: number; onChangeTab: (index: number) => void; }) => { - const translateX = useRef(new Animated.Value(0)).current; - const offsetX = useRef(0); - const index = useRef(0); + const scrollViewRef = useRef(null); useEffect(() => { - translateX.setValue(-activeScreenIndex * width); - offsetX.current = -activeScreenIndex * width; - index.current = activeScreenIndex; + if (scrollViewRef.current) + scrollViewRef.current.scrollTo({ x: activeScreenIndex * width, animated: true }); }, [activeScreenIndex]); - const snapPoints = screens.map((_, i) => i * -width); + const handleScroll = (event: NativeSyntheticEvent) => { + const contentOffsetX = event.nativeEvent.contentOffset.x; + const index = Math.round(contentOffsetX / width); - const panResponder = useRef( - PanResponder.create({ - onMoveShouldSetPanResponder: () => true, - onPanResponderMove: (_, gestureState) => { - if (index.current === 0 && gestureState.dx > 0) - return; - - if (index.current === screens.length - 1 && gestureState.dx < 0) - return; - - translateX.setValue(offsetX.current + gestureState.dx); - }, - onPanResponderRelease: (_, gestureState) => { - const currentTranslateX = offsetX.current + gestureState.dx; - const toValue = getClosestSnapPoint(currentTranslateX, snapPoints); - - Animated.timing(translateX, { - toValue, - duration: 200, - useNativeDriver: false - }).start(() => { - offsetX.current = toValue; - index.current = Math.floor(-toValue / width); - onChangeTab(index.current); - translateX.setValue(toValue); - }); - } - }) - ).current; + if (index !== activeScreenIndex) + onChangeTab(index); + }; return ( - - - {screens.map((screen, index) => ( - - {screen} - - ))} - - + {screens.map((screen, index) => ( + + {screen} + + ))} + ); }; @@ -84,9 +51,6 @@ const styles = StyleSheet.create({ container: { ...StyleSheet.absoluteFillObject }, - tabScreens: { - flexDirection: 'row' - }, tabScreen: { width, overflow: 'hidden' From f3cfdaa084abe6517c45485b53318ce4be4c0ffb Mon Sep 17 00:00:00 2001 From: Abhay Date: Mon, 27 May 2024 16:59:56 +0530 Subject: [PATCH 10/19] fix: bug fix --- src/components/sirenInbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 0f24400..6403ddf 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -439,7 +439,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const keyExtractor = (item: NotificationDataType) => item.id; const renderList = (isActiveTab: boolean, key: string): JSX.Element => { - if (isLoading || !isActiveTab) + if ((isLoading && !isNonEmptyArray(notifications)) || !isActiveTab) return ( Date: Mon, 27 May 2024 17:05:48 +0530 Subject: [PATCH 11/19] fix: bug fix --- src/components/sirenInbox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 6403ddf..ff9e1fb 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -207,6 +207,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const initialize = async (): Promise => { if (siren) { setNotifications([]); + setEndReached(false); siren?.stopRealTimeFetch(EventType.NOTIFICATION); const allNotifications = await fetchNotifications(siren, true); const notificationParams: fetchProps = { size: notificationsPerPage }; From 2e4cc5d7887e1df02b911873728e289940f7b481 Mon Sep 17 00:00:00 2001 From: Abhay Date: Mon, 27 May 2024 17:17:25 +0530 Subject: [PATCH 12/19] fix: bug fix --- src/components/sirenInbox.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index ff9e1fb..da8ca9f 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -300,6 +300,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { setIsLoading(true); setFilterType(key); setActiveTab(index); + setNotifications([]); + setEndReached(false); }; // Refresh notifications From a35a44d3d0e3ecb9521f36e149ec03bafd534f4c Mon Sep 17 00:00:00 2001 From: Abhay Date: Tue, 28 May 2024 13:51:12 +0530 Subject: [PATCH 13/19] refactor: code refactor --- src/components/sirenInbox.tsx | 6 ++-- src/components/tab.tsx | 62 +++++++++++++++++++++++++++------ src/components/tabContainer.tsx | 60 ------------------------------- 3 files changed, 54 insertions(+), 74 deletions(-) delete mode 100644 src/components/tabContainer.tsx diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index da8ca9f..77aa92c 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -1,9 +1,8 @@ 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'; @@ -96,8 +95,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { tabProps = { tabs: [ { key: 'All', title: 'All' }, - { key: 'Unread', title: 'Unread' }, - { key: 'newNotifications', title: 'NewNotifications' } + { key: 'Unread', title: 'Unread' } ], activeTab: 0 } diff --git a/src/components/tab.tsx b/src/components/tab.tsx index 40180e8..adb863c 100644 --- a/src/components/tab.tsx +++ b/src/components/tab.tsx @@ -6,11 +6,15 @@ import { StyleSheet, Text, TouchableOpacity, - View + View, + Dimensions, + type NativeSyntheticEvent, + type NativeScrollEvent } from 'react-native'; import type { StyleProps } from '../types'; -import TabContainer from './tabContainer'; + +const { width } = Dimensions.get('window'); /** * @@ -19,12 +23,15 @@ import TabContainer from './tabContainer'; * 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 renderd. + * @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 = { @@ -35,10 +42,11 @@ type TabProps = { screens?: ReactElement[]; }; -const defaultScreensColors = ['red', 'green', 'blue']; +const defaultScreensColors = ['#b91919', '#2E8B57', '#0047AB']; +const defaultWidth = '100%'; const defaultScreens = defaultScreensColors.map((color, index) => ( - + )); const Tabs = (props: TabProps): ReactElement => { @@ -51,6 +59,7 @@ const Tabs = (props: TabProps): ReactElement => { } = 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([]); @@ -63,6 +72,18 @@ const Tabs = (props: TabProps): ReactElement => { 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(index, tabs[index].key); @@ -143,17 +164,34 @@ const Tabs = (props: TabProps): ReactElement => { ); }; + const renderTabContainer = () => { + return ( + + + {screens.map((screen, index) => ( + + {screen} + + ))} + + + ); + }; + return ( {renderTabHeader()} - - - - + {renderTabContainer()} ); }; - const style = StyleSheet.create({ container: { flex: 1, @@ -185,6 +223,10 @@ const style = StyleSheet.create({ backgroundColor: '#000', position: 'absolute', bottom: 0 + }, + tabScreen: { + width, + overflow: 'hidden' } }); diff --git a/src/components/tabContainer.tsx b/src/components/tabContainer.tsx deleted file mode 100644 index 4fc5c4e..0000000 --- a/src/components/tabContainer.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import { Dimensions, StyleSheet, View, ScrollView, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native'; - -const { width } = Dimensions.get('window'); - -const TabContainer = ({ - screens = [], - activeScreenIndex, - onChangeTab -}: { - screens?: React.ReactNode[]; - activeScreenIndex: number; - onChangeTab: (index: number) => void; -}) => { - const scrollViewRef = useRef(null); - - useEffect(() => { - if (scrollViewRef.current) - scrollViewRef.current.scrollTo({ x: activeScreenIndex * width, animated: true }); - }, [activeScreenIndex]); - - const handleScroll = (event: NativeSyntheticEvent) => { - const contentOffsetX = event.nativeEvent.contentOffset.x; - const index = Math.round(contentOffsetX / width); - - if (index !== activeScreenIndex) - onChangeTab(index); - }; - - return ( - - - {screens.map((screen, index) => ( - - {screen} - - ))} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - ...StyleSheet.absoluteFillObject - }, - tabScreen: { - width, - overflow: 'hidden' - } -}); - -export default TabContainer; From 9804f9401026c8b61c20cd090ed6667cd6b7237f Mon Sep 17 00:00:00 2001 From: Abhay Date: Wed, 29 May 2024 12:12:28 +0530 Subject: [PATCH 14/19] fix: bug fixes --- src/components/sirenInbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 77aa92c..fa21a7c 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -452,7 +452,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { ); - if (isError) + if (isError && !isNonEmptyArray(notifications)) return ( ); From ede83499e92f6fb725d10205de0353961094217d Mon Sep 17 00:00:00 2001 From: Abhay Date: Wed, 29 May 2024 13:56:47 +0530 Subject: [PATCH 15/19] refactor: code refactor --- src/components/sirenInbox.tsx | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index fa21a7c..5a49f9b 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -439,8 +439,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const keyExtractor = (item: NotificationDataType) => item.id; - const renderList = (isActiveTab: boolean, key: string): JSX.Element => { - if ((isLoading && !isNonEmptyArray(notifications)) || !isActiveTab) + const renderEmptyState = (isActiveTab: boolean): JSX.Element => { + if (isLoading || !isActiveTab) return ( { /> ); - - if (isError && !isNonEmptyArray(notifications)) + + if (isError) return ( ); - if (!isNonEmptyArray(notifications)) - return ( - - - {listEmptyComponent || } - + return ( + + + {listEmptyComponent || } - ); + + ); + }; + const renderList = (isActiveTab: boolean, key: string): JSX.Element => { + if (!isNonEmptyArray(notifications) || !isActiveTab) return renderEmptyState(isActiveTab); + return ( Date: Thu, 30 May 2024 11:43:05 +0530 Subject: [PATCH 16/19] fix: add new prop tabPadding in custom styles --- src/components/tab.tsx | 9 ++++++--- src/types.ts | 2 ++ src/utils/commonUtils.ts | 3 +++ src/utils/constants.ts | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/tab.tsx b/src/components/tab.tsx index adb863c..157a3aa 100644 --- a/src/components/tab.tsx +++ b/src/components/tab.tsx @@ -90,7 +90,7 @@ const Tabs = (props: TabProps): ReactElement => { }; const getIndicatorPosition = () => { - let position = 5; + let position = 0; for (let i = 0; i < activeTabIndex; i++) position += tabTitleWidths[i] + 10; position += (tabTitleWidths[activeTabIndex] || 0) / 2; @@ -138,7 +138,11 @@ const Tabs = (props: TabProps): ReactElement => { onChangeTab(index)} onLayout={(event) => onTabTitleLayout(index, event)} > @@ -209,7 +213,6 @@ const style = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', minWidth: 30, - marginHorizontal: 5 }, tabText: { color: '#000', diff --git a/src/types.ts b/src/types.ts index 719d97a..180ff5f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -207,6 +207,7 @@ export type CustomStyleProps = { activeTabTextWeight?: TextStyle['fontWeight']; inactiveTabTextWeight?: TextStyle['fontWeight']; indicatorHeight?: number; + tabPadding?: number; }; }; @@ -267,6 +268,7 @@ export type StyleProps = { backIcon: ViewStyle; mediaContainer: ViewStyle; tabContainer: ViewStyle; + tab: ViewStyle; activeTab: ViewStyle; inActiveTab: ViewStyle; activeTabText: TextStyle | object; diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts index 602c15f..721e2a9 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -291,6 +291,9 @@ export const applyTheme = ( 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 diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 59658c8..cb953a4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -162,5 +162,6 @@ export const defaultStyles = { activeTabTextWeight: '500', inactiveTabTextWeight: '500', indicatorHeight: 3, + tabPadding: 5, }, }; From 1bad4f2dc933bc52370112ebe79bd64cce5f4b76 Mon Sep 17 00:00:00 2001 From: Abhay Date: Thu, 30 May 2024 13:48:11 +0530 Subject: [PATCH 17/19] fix: ui fixes --- src/components/tab.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/tab.tsx b/src/components/tab.tsx index 157a3aa..2c073aa 100644 --- a/src/components/tab.tsx +++ b/src/components/tab.tsx @@ -65,8 +65,9 @@ const Tabs = (props: TabProps): ReactElement => { const [tabTitleWidths, setTabTitleWidths] = useState([]); useEffect(() => { - moveIndicator(); - }, [activeTabIndex]); + if (tabTitleWidths.length > 0) + moveIndicator(); + }, [tabTitleWidths, activeTabIndex]); useEffect(() => { setActiveTabIndex(activeIndex); @@ -92,7 +93,7 @@ const Tabs = (props: TabProps): ReactElement => { const getIndicatorPosition = () => { let position = 0; - for (let i = 0; i < activeTabIndex; i++) position += tabTitleWidths[i] + 10; + for (let i = 0; i < activeTabIndex; i++) position += tabTitleWidths[i]; position += (tabTitleWidths[activeTabIndex] || 0) / 2; return position; @@ -109,7 +110,7 @@ const Tabs = (props: TabProps): ReactElement => { useNativeDriver: true }), Animated.timing(scaleWidth, { - toValue: width, + toValue: width * 0.85, duration: 200, useNativeDriver: true }) @@ -123,11 +124,6 @@ const Tabs = (props: TabProps): ReactElement => { updatedWidths[index] = width; setTabTitleWidths(updatedWidths); - - if (activeIndex === index) { - scaleWidth.setValue(width * 2); - translateX.setValue(getIndicatorPosition()); - } }; const renderTabHeader = () => { From fabd7668377f8c028e542994adc3d53e4aeb0271 Mon Sep 17 00:00:00 2001 From: Abhay Date: Wed, 5 Jun 2024 10:33:15 +0530 Subject: [PATCH 18/19] refactor: add filter types in constants --- src/components/loadingWindow.tsx | 5 ++++- src/components/sirenInbox.tsx | 11 ++++++----- src/utils/constants.ts | 5 +++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/loadingWindow.tsx b/src/components/loadingWindow.tsx index b14e5d2..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; @@ -105,7 +108,7 @@ const LoadingWindow = (props: LoadingWindowProps): ReactElement => { {customLoader || ( )} diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index 5a49f9b..b9eb82e 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -7,6 +7,7 @@ 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'; @@ -94,8 +95,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { hideTab = false, tabProps = { tabs: [ - { key: 'All', title: 'All' }, - { key: 'Unread', title: 'Unread' } + { key: FilterTypes.ALL, title: FilterTypes.ALL }, + { key: FilterTypes.ALL, title: FilterTypes.UNREAD } ], activeTab: 0 } @@ -213,7 +214,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { if (isNonEmptyArray(allNotifications)) notificationParams.start = allNotifications[0].createdAt; - if (filterType === 'Unread') notificationParams.isRead = false; + if (filterType === FilterTypes.UNREAD) notificationParams.isRead = false; if (verificationStatus === VerificationStatus.SUCCESS) siren?.startRealTimeFetch({ @@ -229,7 +230,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { sort: 'createdAt' }; - if (filterType === 'Unread') notificationParams.isRead = false; + if (filterType === FilterTypes.UNREAD) notificationParams.isRead = false; if (attachEndDate) notificationParams.end = notifications[notifications.length - 1].createdAt; @@ -318,7 +319,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { if (isNonEmptyArray(allNotifications)) notificationParams.start = allNotifications[0].createdAt; - if (filterType === 'Unread') notificationParams.isRead = false; + if (filterType === FilterTypes.UNREAD) notificationParams.isRead = false; if (verificationStatus === VerificationStatus.SUCCESS) siren?.startRealTimeFetch({ diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cb953a4..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.'; From 5d69f564fdd1edba64c8dccd07ba7f2b5e5a67a5 Mon Sep 17 00:00:00 2001 From: Abhay Date: Wed, 5 Jun 2024 10:54:47 +0530 Subject: [PATCH 19/19] fix: remove unwanted parameters from onChangeTab function --- src/components/sirenInbox.tsx | 5 ++--- src/components/tab.tsx | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index b9eb82e..f59a0bd 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -96,7 +96,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { tabProps = { tabs: [ { key: FilterTypes.ALL, title: FilterTypes.ALL }, - { key: FilterTypes.ALL, title: FilterTypes.UNREAD } + { key: FilterTypes.UNREAD, title: FilterTypes.UNREAD } ], activeTab: 0 } @@ -295,10 +295,9 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { [theme, darkMode, customStyles] ); - const onChangeTab = (index: number, key: string) => { + const onChangeTab = (key: string) => { setIsLoading(true); setFilterType(key); - setActiveTab(index); setNotifications([]); setEndReached(false); }; diff --git a/src/components/tab.tsx b/src/components/tab.tsx index 2c073aa..2280215 100644 --- a/src/components/tab.tsx +++ b/src/components/tab.tsx @@ -38,7 +38,7 @@ type TabProps = { activeIndex?: number; tabs: Array<{ key: string; title: string }>; styles: Partial; - onChangeTabItem?: (index: number, key: string) => void; + onChangeTabItem?: (key: string) => void; screens?: ReactElement[]; }; @@ -87,7 +87,7 @@ const Tabs = (props: TabProps): ReactElement => { const onChangeTab = (index: number) => { setActiveTabIndex(index); - onChangeTabItem(index, tabs[index].key); + onChangeTabItem(tabs[index].key); }; const getIndicatorPosition = () => {