diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d3557..7100124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,16 @@ All notable changes to this project will be documented in this file. -## [1.0.0] - 2024-03-26 +## 1.1.0 + +### Added +- Added support for custom delete icon and a flag to toggle the visibility of the delete icon. +- Added functionality to display thumbnail URL previews for media content. +- Exposed avatar click property. +- Implemented specific error code mapping. +- Enhanced style and theme customizations. + +## 1.0.0 ### Added diff --git a/README.md b/README.md index 7645c49..14db430 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ theme | Object for custom themes | Theme | {} | customStyles | Object for custom styling | CustomStyleProps | {} | 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 card | CardProps | { hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteIcon: JSX.Element, onAvatarClick: ()=> null } | +cardProps | Props for customizing the card | CardProps | { hideAvatar: false, disableAutoMarkAsRead: false, hideDelete: false, deleteIcon: JSX.Element, onAvatarClick: ()=> null, hideMediaThumbnailL: false, onMediaThumbnailClick: ()=> null} | customCard | Function for rendering custom card | (notification)=> JSX Element | null | onCardClick | Custom click handler for card | (notification)=> void | ()=>null | listEmptyComponent | Custom component for empty list | JSX Element | null | @@ -200,7 +200,7 @@ Here are the custom style options for the notification inbox: deleteIcon?:{ size?: number }; - dateIcon?:{ + timerIcon?:{ size?: number }; clearAllIcon?:{ @@ -216,6 +216,8 @@ Here are the custom style options for the notification inbox: disableAutoMarkAsRead?: boolean; deleteIcon?: JSX.Element; hideDelete?: boolean; + hideMediaThumbnail?: boolean; + onMediaThumbnailClick?: (notification: NotificationDataType) => void; }; ``` @@ -246,10 +248,6 @@ function MyComponent() { markAsReadById(id); } - function handleDeleteNotification(id) { - deleteById(id); - } - return ( {/* Your component logic */} ); diff --git a/example/package.json b/example/package.json index f119386..3940b67 100644 --- a/example/package.json +++ b/example/package.json @@ -13,7 +13,7 @@ "@react-navigation/bottom-tabs": "^6.5.14", "@react-navigation/native": "^6.1.12", "@react-navigation/native-stack": "^6.9.20", - "@sirenapp/react-native-inbox": "^1.0.0", + "@sirenapp/react-native-inbox": "^1.1.0", "react": "18.2.0", "react-native": "0.73.4", "react-native-modal": "^13.0.1", diff --git a/package-lock.json b/package-lock.json index b024c42..f77497c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@sirenapp/js-sdk": "^1.0.0", + "@sirenapp/js-sdk": "^1.1.0", "pubsub-js": "^1.9.4" }, "devDependencies": { @@ -4827,9 +4827,9 @@ } }, "node_modules/@sirenapp/js-sdk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sirenapp/js-sdk/-/js-sdk-1.0.0.tgz", - "integrity": "sha512-zypXxRdTNkWRzJhiIYPxlmrpIj2vMjh5H2u0Bbrw0uI+RPLap47HxKBMGMfaPwFP9UxdoZUuRze3Bizj4vt6AQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sirenapp/js-sdk/-/js-sdk-1.1.0.tgz", + "integrity": "sha512-4V6nltHLhPLOsPMQO7OqllUeFEELvgeb8RSPb/uuwZ1qr05pClQEgoCy5/AlX0glsfHCdoQUfAyWt3akJEenrw==", "dependencies": { "promise-polyfill": "^8.3.0", "tslib": "^2.6.2", @@ -19030,9 +19030,9 @@ } }, "@sirenapp/js-sdk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sirenapp/js-sdk/-/js-sdk-1.0.0.tgz", - "integrity": "sha512-zypXxRdTNkWRzJhiIYPxlmrpIj2vMjh5H2u0Bbrw0uI+RPLap47HxKBMGMfaPwFP9UxdoZUuRze3Bizj4vt6AQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sirenapp/js-sdk/-/js-sdk-1.1.0.tgz", + "integrity": "sha512-4V6nltHLhPLOsPMQO7OqllUeFEELvgeb8RSPb/uuwZ1qr05pClQEgoCy5/AlX0glsfHCdoQUfAyWt3akJEenrw==", "requires": { "promise-polyfill": "^8.3.0", "tslib": "^2.6.2", diff --git a/package.json b/package.json index ac91656..6ffad70 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,6 @@ ], "dependencies": { "pubsub-js": "^1.9.4", - "@sirenapp/js-sdk": "^1.0.0" + "@sirenapp/js-sdk": "^1.1.0" } } diff --git a/src/assets/failedImageDark.png b/src/assets/failedImageDark.png new file mode 100644 index 0000000..3f5f056 Binary files /dev/null and b/src/assets/failedImageDark.png differ diff --git a/src/assets/failedImageLight.png b/src/assets/failedImageLight.png new file mode 100644 index 0000000..74ab4a8 Binary files /dev/null and b/src/assets/failedImageLight.png differ diff --git a/src/components/card.tsx b/src/components/card.tsx index 92eb4a4..dc3e389 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -4,6 +4,7 @@ import { Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react import type { NotificationCardProps } from '../types'; import { CommonUtils, useSiren } from '../utils'; import { eventTypes, events } from '../utils/constants'; +import { useSirenContext } from './sirenProvider'; import CloseIcon from './closeIcon'; import TimerIcon from './timerIcon'; @@ -47,9 +48,12 @@ const Card = (props: NotificationCardProps): ReactElement => { disableAutoMarkAsRead, hideDelete = false, onAvatarClick, - deleteIcon = null + deleteIcon = null, + hideMediaThumbnail = false, + onMediaThumbnailClick } = cardProps; const { markAsReadById } = useSiren(); + const { id: providerId } = useSirenContext(); const opacity = useRef(new Animated.Value(1)).current; @@ -57,18 +61,26 @@ const Card = (props: NotificationCardProps): ReactElement => { return darkMode ? require('../assets/emptyDark.png') : require('../assets/emptyLight.png'); }; + const failedState = () => { + return darkMode + ? require('../assets/failedImageDark.png') + : require('../assets/failedImageLight.png'); + }; + + const avatarUrl = notification?.message?.avatar?.imageUrl || ''; + const thumbnailUrl = notification?.message?.thumbnailUrl || ''; + const [imageSource, setImageSource] = useState( - notification?.message?.avatar?.imageUrl?.length > 0 - ? { uri: notification.message?.avatar?.imageUrl } - : emptyState() + avatarUrl?.length > 0 ? { uri: avatarUrl } : emptyState() + ); + + const [mediaSource, setMediaSource] = useState( + thumbnailUrl?.length > 0 ? { uri: thumbnailUrl } : emptyState() ); useEffect(() => { - setImageSource( - notification?.message?.avatar?.imageUrl?.length > 0 - ? { uri: notification.message?.avatar?.imageUrl } - : emptyState() - ); + setImageSource(avatarUrl?.length > 0 ? { uri: avatarUrl } : emptyState()); + setMediaSource(thumbnailUrl?.length > 0 ? { uri: thumbnailUrl } : failedState()); }, [notification, darkMode]); const cardClick = (): void => { @@ -80,10 +92,18 @@ const Card = (props: NotificationCardProps): ReactElement => { setImageSource(emptyState()); }; + const onErrorMedia = (): void => { + setMediaSource(failedState()); + }; + const avatarClick = () => { if (onAvatarClick) onAvatarClick(notification); }; + const mediaClick = () => { + if (onMediaThumbnailClick) onMediaThumbnailClick(notification); + }; + const renderAvatar = useMemo((): JSX.Element => { return ( @@ -104,6 +124,18 @@ const Card = (props: NotificationCardProps): ReactElement => { ); }, [styles, darkMode, imageSource, onAvatarClick]); + const renderMediaThumbnail = useMemo((): JSX.Element => { + return ( + + + + ); + }, [darkMode, mediaSource, onMediaThumbnailClick]); + const onDeleteItem = async (): Promise => { const isSuccess = await onDelete(notification.id, false); @@ -115,7 +147,7 @@ const Card = (props: NotificationCardProps): ReactElement => { }).start(() => { const payload = { id: notification.id, action: eventTypes.DELETE_ITEM }; - PubSub.publish(events.NOTIFICATION_LIST_EVENT, JSON.stringify(payload)); + PubSub.publish(`${events.NOTIFICATION_LIST_EVENT}${providerId}`, JSON.stringify(payload)); }); }; @@ -141,19 +173,28 @@ const Card = (props: NotificationCardProps): ReactElement => { {notification.message?.header} - {!hideDelete && - (deleteIcon || ( - - ))} + {!hideDelete && ( + + )} {Boolean(notification.message?.subHeader) && ( {notification.message?.subHeader} )} - - {notification.message?.body} - + {Boolean(notification.message?.body) && ( + + {notification.message?.body} + + )} + {!hideMediaThumbnail && + Boolean(notification.message?.thumbnailUrl) && + renderMediaThumbnail} @@ -228,6 +269,14 @@ const style = StyleSheet.create({ }, transparent: { backgroundColor: 'transparent' + }, + mediaContainer: { + width: '100%', + height: 130, + borderRadius: 6, + marginBottom: 10, + overflow: 'hidden', + backgroundColor: '#D3D3D3' } }); diff --git a/src/components/closeIcon.tsx b/src/components/closeIcon.tsx index d08bfde..f9da79a 100644 --- a/src/components/closeIcon.tsx +++ b/src/components/closeIcon.tsx @@ -8,10 +8,12 @@ import type { StyleProps } from '../types'; const CloseIcon = ({ notification, styles, + customIcon, onDelete }: { notification: NotificationDataType; styles: Partial; + customIcon?: ReactElement | null; onDelete: (id: string) => void; }): ReactElement => { const icon: JSX.Element[] = []; @@ -36,7 +38,7 @@ const CloseIcon = ({ testID='delete-button' accessibilityLabel={`siren-notification-delete${notification.id}`} > - <>{icon} + <>{customIcon || icon} ); }; diff --git a/src/components/sirenInbox.tsx b/src/components/sirenInbox.tsx index aa7c6e7..ecea810 100644 --- a/src/components/sirenInbox.tsx +++ b/src/components/sirenInbox.tsx @@ -22,6 +22,7 @@ const { TOKEN_VERIFICATION_PENDING, MAXIMUM_ITEMS_PER_FETCH, VerificationStatus, + EventType, errorMap } = Constants; const { applyTheme, isNonEmptyArray, updateNotifications } = CommonUtils; @@ -76,7 +77,8 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { cardProps = { hideAvatar: false, disableAutoMarkAsRead: false, - hideDelete: false + hideDelete: false, + hideMediaThumbnail: false, }, listEmptyComponent = null, headerProps = {}, @@ -103,7 +105,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { itemsPerFetch > MAXIMUM_ITEMS_PER_FETCH ? MAXIMUM_ITEMS_PER_FETCH : itemsPerFetch ); - const { siren, verificationStatus } = useSirenContext(); + const { siren, verificationStatus, id } = useSirenContext(); const { deleteById, deleteByDate, markAllAsViewed } = useSiren(); @@ -121,19 +123,20 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { const disableCardDelete = useRef(false); useEffect(() => { - PubSub.subscribe(events.NOTIFICATION_LIST_EVENT, notificationSubscriber); + PubSub.subscribe(`${events.NOTIFICATION_LIST_EVENT}${id}`, notificationSubscriber); return cleanUp(); }, []); useEffect(() => { // Initialize Siren SDK and start polling notifications - if (verificationStatus !== VerificationStatus.PENDING && siren) { + if (verificationStatus === VerificationStatus.SUCCESS && siren) { initialize(); } else if(verificationStatus === VerificationStatus.FAILED) { setIsError(true); setIsLoading(false); - if (onError) onError(errorMap.MISSING_PARAMETER); + setNotifications([]); + if (onError) onError(errorMap.INVALID_CREDENTIALS); } }, [siren, verificationStatus]); @@ -149,7 +152,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { } }, [eventListenerData]); - const handleMarkNotificationsAsViewed = async (newNotifications = notifications) => { + const handleMarkAllAsViewed = async (newNotifications = notifications) => { const currentTimestamp = new Date().getTime(); const isoString = new Date(currentTimestamp).toISOString(); const response = await markAllAsViewed( @@ -168,10 +171,10 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { // Clean up - stop polling when component unmounts const cleanUp = () => () => { - siren?.stopRealTimeNotificationFetch(); + siren?.stopRealTimeFetch(EventType.NOTIFICATION); setNotifications([]); - PubSub.unsubscribe(events.NOTIFICATION_LIST_EVENT); - handleMarkNotificationsAsViewed(); + PubSub.unsubscribe(`${events.NOTIFICATION_LIST_EVENT}${id}`); + handleMarkAllAsViewed(); }; const notificationSubscriber = async (type: string, dataString: string) => { @@ -183,7 +186,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { // Initialize Siren SDK and fetch notifications const initialize = async (): Promise => { if (siren) { - siren?.stopRealTimeNotificationFetch(); + siren?.stopRealTimeFetch(EventType.NOTIFICATION); const allNotifications = await fetchNotifications(siren, true); const notificationParams: fetchProps = { size: notificationsPerPage }; @@ -191,7 +194,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { notificationParams.start = allNotifications[0].createdAt; if (verificationStatus === VerificationStatus.SUCCESS) - siren?.startRealTimeNotificationFetch(notificationParams); + siren?.startRealTimeFetch({eventType: EventType.NOTIFICATION, params: notificationParams}); } }; @@ -214,7 +217,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { if (nonEmptyResponse) { const updatedNotifications = isResetList ? responseData : [...notifications, ...responseData]; - isResetList && handleMarkNotificationsAsViewed(updatedNotifications); + isResetList && handleMarkAllAsViewed(updatedNotifications); setNotifications(updatedNotifications); return updatedNotifications; @@ -273,7 +276,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { setNotifications([]); setIsLoading(true); - siren?.stopRealTimeNotificationFetch(); + siren?.stopRealTimeFetch(EventType.NOTIFICATION); const allNotifications = (await fetchNotifications(siren, true)) || []; const notificationParams: fetchProps = { size: notificationsPerPage }; @@ -281,7 +284,7 @@ const SirenInbox = (props: SirenInboxProps): ReactElement => { notificationParams.start = allNotifications[0].createdAt; if (verificationStatus === VerificationStatus.SUCCESS) - siren?.startRealTimeNotificationFetch(notificationParams); + siren?.startRealTimeFetch({eventType: EventType.NOTIFICATION, params:notificationParams}); } catch (err) { setIsLoading(false); setIsError(true); diff --git a/src/components/sirenInboxIcon.tsx b/src/components/sirenInboxIcon.tsx index 6481c8a..355914c 100644 --- a/src/components/sirenInboxIcon.tsx +++ b/src/components/sirenInboxIcon.tsx @@ -14,6 +14,7 @@ const { eventTypes, events, defaultStyles, + EventType, VerificationStatus, errorMap } = Constants; @@ -50,7 +51,7 @@ const SirenInboxIcon = (props: SirenInboxIconProps) => { onError = () => null } = props; - const { siren, verificationStatus } = useSirenContext(); + const { siren, verificationStatus, id } = useSirenContext(); const [unviewedCount, seUnviewedCount] = useState(0); @@ -67,8 +68,8 @@ const SirenInboxIcon = (props: SirenInboxIconProps) => { // Clean up - stop polling when component unmounts const cleanUp = () => () => { - siren?.stopRealTimeUnviewedCountFetch(); - PubSub.unsubscribe(events.NOTIFICATION_COUNT_EVENT); + siren?.stopRealTimeFetch(EventType.UNVIEWED_COUNT); + PubSub.unsubscribe(`${events.NOTIFICATION_COUNT_EVENT}${id}`); seUnviewedCount(0); }; @@ -81,7 +82,7 @@ const SirenInboxIcon = (props: SirenInboxIconProps) => { }; useEffect(() => { - PubSub.subscribe(events.NOTIFICATION_COUNT_EVENT, notificationSubscriber); + PubSub.subscribe(`${events.NOTIFICATION_COUNT_EVENT}${id}`, notificationSubscriber); return cleanUp(); }, []); @@ -102,7 +103,7 @@ const SirenInboxIcon = (props: SirenInboxIconProps) => { const unViewed: UnviewedCountReturnResponse | null = await siren.fetchUnviewedNotificationsCount(); - siren.startRealTimeUnviewedCountFetch(); + siren.startRealTimeFetch({eventType: EventType.UNVIEWED_COUNT}); if (unViewed?.data) seUnviewedCount(unViewed.data?.unviewedCount || 0); if (unViewed?.error) onError(unViewed?.error); } diff --git a/src/components/sirenProvider.tsx b/src/components/sirenProvider.tsx index e26df72..c9f8999 100644 --- a/src/components/sirenProvider.tsx +++ b/src/components/sirenProvider.tsx @@ -7,23 +7,25 @@ import type { NotificationDataType, NotificationsApiResponse, SirenErrorType, - UnviewedCountApiResponse, + UnviewedCountApiResponse } from '@sirenapp/js-sdk/dist/esm/types'; import type { SirenProviderConfigProps } from '../types'; -import { isNonEmptyArray, logger } from '../utils/commonUtils'; +import { generateUniqueId, isNonEmptyArray, logger } from '../utils/commonUtils'; import { events, eventTypes, IN_APP_RECIPIENT_UNAUTHENTICATED, MAXIMUM_RETRY_COUNT, - VerificationStatus + VerificationStatus, + EventType } from '../utils/constants'; import { useSiren } from '../utils'; type SirenContextProp = { siren: Siren | null; verificationStatus: VerificationStatus; + id: string; }; interface SirenProvider { @@ -33,7 +35,8 @@ interface SirenProvider { export const SirenContext = createContext({ siren: null, - verificationStatus: VerificationStatus.PENDING + verificationStatus: VerificationStatus.PENDING, + id: '' }); /** @@ -74,10 +77,12 @@ const SirenProvider: React.FC = ({ config, children }) => { let retryCount = 0; const { markAllAsViewed } = useSiren(); - + + const [id] = useState(generateUniqueId()); const [siren, setSiren] = useState(null); - const [verificationStatus, setVerificationStatus] = useState(VerificationStatus.PENDING); - + const [verificationStatus, setVerificationStatus] = useState( + VerificationStatus.PENDING + ); useEffect(() => { if (config?.recipientId && config?.userToken) { @@ -90,8 +95,8 @@ const SirenProvider: React.FC = ({ config, children }) => { }, [config]); const stopRealTimeFetch = (): void => { - siren?.stopRealTimeNotificationFetch(); - siren?.stopRealTimeUnviewedCountFetch(); + siren?.stopRealTimeFetch(EventType.NOTIFICATION); + siren?.stopRealTimeFetch(EventType.UNVIEWED_COUNT); }; const sendResetDataEvents = () => { @@ -102,40 +107,61 @@ const SirenProvider: React.FC = ({ config, children }) => { action: eventTypes.RESET_NOTIFICATIONS }; - PubSub.publish(events.NOTIFICATION_COUNT_EVENT, JSON.stringify(updateCountPayload)); - PubSub.publish(events.NOTIFICATION_LIST_EVENT, JSON.stringify(updateNotificationPayload)); + PubSub.publish(`${events.NOTIFICATION_COUNT_EVENT}${id}`, JSON.stringify(updateCountPayload)); + PubSub.publish(`${events.NOTIFICATION_LIST_EVENT}${id}`, JSON.stringify(updateNotificationPayload)); }; - const onUnViewedCountReceived = (response: UnviewedCountApiResponse): void => { - const totalUnviewed = response?.data?.totalUnviewed; + const onNewNotificationEvent = (responseData: NotificationDataType[]) => { + logger.info(`new notifications : ${JSON.stringify(responseData)}`); + + markAllAsViewed(responseData[0].createdAt); + const payload = { newNotifications: responseData, action: eventTypes.NEW_NOTIFICATIONS }; + + PubSub.publish(`${events.NOTIFICATION_LIST_EVENT}${id}`, JSON.stringify(payload)); + }; + const onTotalUnviewedCountEvent = (response: UnviewedCountApiResponse) => { + const totalUnviewed = response.data?.totalUnviewed; const payload = { unviewedCount: totalUnviewed, action: eventTypes.UPDATE_NOTIFICATIONS_COUNT }; - PubSub.publish(events.NOTIFICATION_COUNT_EVENT, JSON.stringify(payload)); + PubSub.publish(`${events.NOTIFICATION_COUNT_EVENT}${id}`, JSON.stringify(payload)); }; - const onNotificationReceived = (response: NotificationsApiResponse): void => { - const responseData: NotificationDataType[] = response?.data || []; + const handleNotificationEvent = (response: NotificationsApiResponse) => { + const responseData = response?.data; - if (isNonEmptyArray(responseData)) { - logger.info(`new notifications : ${JSON.stringify(responseData)}`); - - markAllAsViewed(responseData[0].createdAt); - const payload = { newNotifications: response?.data, action: eventTypes.NEW_NOTIFICATIONS }; + if (Array.isArray(responseData) && isNonEmptyArray(responseData)) + onNewNotificationEvent(responseData); - PubSub.publish(events.NOTIFICATION_LIST_EVENT, JSON.stringify(payload)); - } }; + const handleUnviewedCountEvent = (response: UnviewedCountApiResponse) => { + const responseData = response?.data; + if (responseData && 'totalUnviewed' in responseData) + onTotalUnviewedCountEvent(response); + + }; + const onEventReceive = ( + response: NotificationsApiResponse | UnviewedCountApiResponse = {}, + eventType: EventType + ) => { + switch (eventType) { + case EventType.NOTIFICATION: + handleNotificationEvent(response as NotificationsApiResponse); + break; + case EventType.UNVIEWED_COUNT: + handleUnviewedCountEvent(response as UnviewedCountApiResponse); + break; + } + }; const onStatusChange = (status: VerificationStatus) => { setVerificationStatus(status); }; - - const actionCallbacks = { onUnViewedCountReceived, onNotificationReceived, onStatusChange }; + const actionCallbacks = { onEventReceive, onStatusChange }; const getDataParams = () => { return { @@ -147,7 +173,11 @@ const SirenProvider: React.FC = ({ config, children }) => { }; const retryVerification = (error: SirenErrorType) => { - if (error.Code === IN_APP_RECIPIENT_UNAUTHENTICATED && retryCount < MAXIMUM_RETRY_COUNT) + if ( + error.Code === IN_APP_RECIPIENT_UNAUTHENTICATED && + retryCount < MAXIMUM_RETRY_COUNT && + verificationStatus === VerificationStatus.FAILED + ) setTimeout(() => { initialize(); retryCount++; @@ -158,6 +188,7 @@ const SirenProvider: React.FC = ({ config, children }) => { // Function to initialize the Siren SDK and fetch notifications const initialize = (): void => { + setVerificationStatus(VerificationStatus.PENDING); const dataParams: InitConfigType = getDataParams(); const siren = new Siren(dataParams); @@ -167,6 +198,7 @@ const SirenProvider: React.FC = ({ config, children }) => { return ( void; }; /** @@ -182,7 +184,7 @@ export type CustomStyleProps = { deleteIcon?:{ size?: number }; - dateIcon?:{ + timerIcon?:{ size?: number }; clearAllIcon?:{ @@ -245,4 +247,5 @@ export type StyleProps = { skeltonLoaderColor: ViewStyle; highlighted: ViewStyle; backIcon: ViewStyle; + mediaContainer: ViewStyle; }; diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts index 5718509..4ba25a7 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -8,6 +8,10 @@ import { DefaultTheme } from './index'; export const isNonEmptyArray = (arr?: unknown[] | null) => Boolean(arr && typeof arr === 'object' && arr instanceof Array && arr.length > 0); +export const generateUniqueId = (): string => { + return Math.random().toString(36).substring(2, 15); +}; + export const updateNotifications = ( eventData: { id?: string; @@ -157,23 +161,23 @@ export const applyTheme = ( }, timerIcon: { borderColor: theme.colors?.timerIcon || DefaultTheme[mode].colors.timerIcon, - width: customStyles.dateIcon?.size || defaultStyles.dateIcon.size, - height: customStyles.dateIcon?.size || defaultStyles.dateIcon.size, - borderRadius: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) / 2, - borderWidth: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.1, - padding: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.1 + width: customStyles.timerIcon?.size || defaultStyles.timerIcon.size, + height: customStyles.timerIcon?.size || defaultStyles.timerIcon.size, + borderRadius: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) / 2, + borderWidth: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.1, + padding: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.1 }, timerIconLine: { backgroundColor: theme.colors?.timerIcon || DefaultTheme[mode].colors.timerIcon, - height: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.36, - width: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.1 + height: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.36, + width: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.1 }, timerIconLine2: { backgroundColor: theme.colors?.timerIcon || DefaultTheme[mode].colors.timerIcon, - height: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.28, - width: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.1, - marginLeft: (customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.22, - marginTop: -(customStyles.dateIcon?.size || defaultStyles.dateIcon.size) * 0.1 + height: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.28, + width: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.1, + marginLeft: (customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.22, + marginTop: -(customStyles.timerIcon?.size || defaultStyles.timerIcon.size) * 0.1 }, cardContainer: { padding: customStyles.notificationCard?.padding || defaultStyles.notificationCard.padding @@ -277,5 +281,8 @@ export const applyTheme = ( theme.windowHeader?.titleColor || theme.colors?.textColor || DefaultTheme[mode].windowHeader.titleColor - } + }, + mediaContainer: { + backgroundColor: DefaultTheme[mode].notificationCard.mediaContainerBackground + }, }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 448b9ba..99210e5 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -9,7 +9,8 @@ export const COLORS = { deleteIcon: '#34405499', timerIcon: '#667185', clearAllIcon: '#667185', - infiniteLoader: '#F56630' + infiniteLoader: '#F56630', + imageBackground: '#F0F2F5' }, dark: { primaryColor: '#FA9874', @@ -18,10 +19,11 @@ export const COLORS = { neutralColor: '#232326', borderColor: '#344054', dateColor: '#98A2B3', - deleteIcon: '#98A2B3', + deleteIcon: '#D0D5DD', timerIcon: '#98A2B3', clearAllIcon: '#D0D5DD', - infiniteLoader: '#F56630' + infiniteLoader: '#F56630', + imageBackground: '#4C4C4C' } }; @@ -58,6 +60,11 @@ export const levelLogFns = { [LogLevel.ERROR]: console.error }; +export enum EventType { + NOTIFICATION = "NOTIFICATIONS", + UNVIEWED_COUNT = "UNVIEWED_COUNT" +} + export enum eventTypes { MARK_ITEM_AS_VIEWED = 'MARK_ITEM_AS_VIEWED', MARK_ITEM_AS_READ = 'MARK_ITEM_AS_READ', @@ -142,7 +149,7 @@ export const defaultStyles = { deleteIcon: { size: 14 }, - dateIcon: { + timerIcon: { size: 12 }, clearAllIcon: { diff --git a/src/utils/defaultTheme.ts b/src/utils/defaultTheme.ts index 106ded0..273e381 100644 --- a/src/utils/defaultTheme.ts +++ b/src/utils/defaultTheme.ts @@ -30,6 +30,7 @@ const defaultTheme = { subTitleColor: COLORS[ThemeMode.LIGHT].textColor, descriptionColor: COLORS[ThemeMode.LIGHT].textColor, dateColor: COLORS[ThemeMode.LIGHT].textColor, + mediaContainerBackground: COLORS[ThemeMode.LIGHT].imageBackground, } }, dark: { @@ -60,7 +61,8 @@ const defaultTheme = { titleColor: COLORS[ThemeMode.DARK].textColor, subTitleColor: COLORS[ThemeMode.DARK].textColor, descriptionColor: COLORS[ThemeMode.DARK].textColor, - dateColor: COLORS[ThemeMode.DARK].dateColor + dateColor: COLORS[ThemeMode.DARK].dateColor, + mediaContainerBackground: COLORS[ThemeMode.DARK].imageBackground, } } }; diff --git a/src/utils/sirenHook.ts b/src/utils/sirenHook.ts index f6c799e..f863b17 100644 --- a/src/utils/sirenHook.ts +++ b/src/utils/sirenHook.ts @@ -4,17 +4,17 @@ import { errorMap, events, eventTypes } from './constants'; import { useSirenContext } from '../components/sirenProvider'; const useSiren = () => { - const { siren } = useSirenContext(); + const { siren, id: providerId } = useSirenContext(); const markAsReadById = async (id: string) => { if (siren) if (id?.length > 0) { - const response = await siren?.markNotificationAsReadById(id); + const response = await siren?.markAsReadById(id); if (response?.data) { const payload = { id, action: eventTypes.MARK_ITEM_AS_READ }; - PubSub.publish(events.NOTIFICATION_LIST_EVENT, JSON.stringify(payload)); + PubSub.publish(`${events.NOTIFICATION_LIST_EVENT}${providerId}`, JSON.stringify(payload)); } return response; @@ -27,12 +27,12 @@ const useSiren = () => { const markAsReadByDate = async (untilDate: string) => { if (siren && untilDate) { - const response = await siren?.markNotificationsAsReadByDate(untilDate); + const response = await siren?.markAsReadByDate(untilDate); if (response?.data) { const payload = { action: eventTypes.MARK_ALL_AS_READ }; - PubSub.publish(events.NOTIFICATION_LIST_EVENT, JSON.stringify(payload)); + PubSub.publish(`${events.NOTIFICATION_LIST_EVENT}${providerId}`, JSON.stringify(payload)); } return response; @@ -44,12 +44,12 @@ const useSiren = () => { const deleteById = async (id: string, shouldUpdateList: boolean = true) => { if (siren) if (id?.length > 0) { - const response = await siren?.deleteNotificationById(id); + const response = await siren?.deleteById(id); if (response?.data && shouldUpdateList) { const payload = { id, action: eventTypes.DELETE_ITEM }; - PubSub.publish(events.NOTIFICATION_LIST_EVENT, JSON.stringify(payload)); + PubSub.publish(`${events.NOTIFICATION_LIST_EVENT}${providerId}`, JSON.stringify(payload)); } return response; @@ -62,12 +62,12 @@ const useSiren = () => { const deleteByDate = async (untilDate: string) => { if (siren && untilDate) { - const response = await siren.deleteNotificationsByDate(untilDate); + const response = await siren.deleteByDate(untilDate); if (response?.data) { const payload = { action: eventTypes.DELETE_ALL_ITEM }; - PubSub.publish(events.NOTIFICATION_LIST_EVENT, JSON.stringify(payload)); + PubSub.publish(`${events.NOTIFICATION_LIST_EVENT}${providerId}`, JSON.stringify(payload)); } return response; @@ -78,12 +78,12 @@ const useSiren = () => { const markAllAsViewed = async (untilDate: string) => { if (siren && untilDate) { - const response = await siren?.markNotificationsAsViewed(untilDate); + const response = await siren?.markAllAsViewed(untilDate); if (response?.data) { const payload = { unviewedCount: 0, action: eventTypes.UPDATE_NOTIFICATIONS_COUNT }; - PubSub.publish(events.NOTIFICATION_COUNT_EVENT, JSON.stringify(payload)); + PubSub.publish(`${events.NOTIFICATION_COUNT_EVENT}${providerId}`, JSON.stringify(payload)); } return response; diff --git a/tests/components/sirenNotificationIcon.test.tsx b/tests/components/sirenNotificationIcon.test.tsx index 238f600..0e29724 100644 --- a/tests/components/sirenNotificationIcon.test.tsx +++ b/tests/components/sirenNotificationIcon.test.tsx @@ -34,18 +34,16 @@ describe('SirenInboxIcon', () => { const notificationIcon = ; const mockSiren:Pick = { - markNotificationAsReadById: jest.fn(), - markNotificationsAsReadByDate: jest.fn(), - deleteNotificationById: jest.fn(), - deleteNotificationsByDate: jest.fn(), - markNotificationsAsViewed: jest.fn(), + markAsReadById: jest.fn(), + markAsReadByDate: jest.fn(), + deleteById: jest.fn(), + deleteByDate: jest.fn(), + markAllAsViewed: jest.fn(), verifyToken: jest.fn(), fetchUnviewedNotificationsCount: jest.fn(async () => UnviewedCountReturnResponse), fetchAllNotifications: jest.fn(), - startRealTimeNotificationFetch: jest.fn(), - stopRealTimeNotificationFetch: jest.fn(), - startRealTimeUnviewedCountFetch: jest.fn(), - stopRealTimeUnviewedCountFetch: jest.fn(), + startRealTimeFetch: jest.fn(), + stopRealTimeFetch: jest.fn(), }; jest.spyOn(sirenProvider, 'useSirenContext').mockReturnValue({ diff --git a/tests/components/sirenProvider.test.tsx b/tests/components/sirenProvider.test.tsx index b155e4b..0c5b0c2 100644 --- a/tests/components/sirenProvider.test.tsx +++ b/tests/components/sirenProvider.test.tsx @@ -24,16 +24,14 @@ describe('SirenProvider', () => { ); const mocErrorFn = jest.fn(); - const mockNotificationHandler = jest.fn(); - const mockCountHandler = jest.fn(); + const mockEventHandler = jest.fn(); const sirenObject = new Siren({ token: 'user-token', recipientId: 'recipient-id', onError: mocErrorFn, actionCallbacks: { - onNotificationReceived: mockNotificationHandler, - onUnViewedCountReceived: mockCountHandler + onEventReceive: mockEventHandler, } }); diff --git a/tests/utils/sirenHook.test.ts b/tests/utils/sirenHook.test.ts index 848bb5a..979b2e7 100644 --- a/tests/utils/sirenHook.test.ts +++ b/tests/utils/sirenHook.test.ts @@ -73,21 +73,19 @@ const MarkAsViewedResponse = { describe('useSiren hook', () => { const mockSiren: Pick = { - markNotificationAsReadById: jest.fn(async () => Response), - markNotificationsAsReadByDate: jest.fn(async () => ActionResponse), - deleteNotificationById: jest.fn(async () => ActionResponse), - deleteNotificationsByDate: jest.fn(async () => ActionResponse), - markNotificationsAsViewed: jest.fn(async () => MarkAsViewedResponse), + markAsReadById: jest.fn(async () => Response), + markAsReadByDate: jest.fn(async () => ActionResponse), + deleteById: jest.fn(async () => ActionResponse), + deleteByDate: jest.fn(async () => ActionResponse), + markAllAsViewed: jest.fn(async () => MarkAsViewedResponse), verifyToken: jest.fn(), fetchUnviewedNotificationsCount: jest.fn(), fetchAllNotifications: jest.fn(), - startRealTimeNotificationFetch: jest.fn(), - stopRealTimeNotificationFetch: jest.fn(), - startRealTimeUnviewedCountFetch: jest.fn(), - stopRealTimeUnviewedCountFetch: jest.fn() + startRealTimeFetch: jest.fn(), + stopRealTimeFetch: jest.fn(), }; - it('should call siren.markNotificationAsReadById and update notifications list when siren exists and id is not empty', async () => { + it('should call siren.markAsReadById and update notifications list when siren exists and id is not empty', async () => { // Mock useSirenContext jest.spyOn(sirenProvider, 'useSirenContext').mockReturnValue({ @@ -98,11 +96,11 @@ describe('useSiren hook', () => { const { markAsReadById } = useSiren(); const response = await markAsReadById('xyz'); - expect(mockSiren.markNotificationAsReadById).toHaveBeenCalledWith('xyz'); + expect(mockSiren.markAsReadById).toHaveBeenCalledWith('xyz'); expect(response).toEqual(Response); }); - it('should call siren.markNotificationsAsReadByDate and update notifications list when siren exists and untilDate is provided', async () => { + it('should call siren.markAsReadByDate and update notifications list when siren exists and untilDate is provided', async () => { jest.spyOn(sirenProvider, 'useSirenContext').mockReturnValue({ siren: mockSiren as Siren, verificationStatus: VerificationStatus.SUCCESS @@ -112,11 +110,11 @@ describe('useSiren hook', () => { const untilDate = '2024-02-28T00:00:00Z'; const response = await markAsReadByDate(untilDate); - expect(mockSiren.markNotificationsAsReadByDate).toHaveBeenCalledWith(untilDate); + expect(mockSiren.markAsReadByDate).toHaveBeenCalledWith(untilDate); expect(response).toEqual(ActionResponse); }); - it('should call siren.deleteNotificationById and update notifications list when siren exists and id is not empty', async () => { + it('should call siren.deleteById and update notifications list when siren exists and id is not empty', async () => { jest.spyOn(sirenProvider, 'useSirenContext').mockReturnValue({ siren: mockSiren as Siren, @@ -126,11 +124,11 @@ describe('useSiren hook', () => { const { deleteById } = useSiren(); const response = await deleteById('xyz'); - expect(mockSiren.deleteNotificationById).toHaveBeenCalledWith('xyz'); + expect(mockSiren.deleteById).toHaveBeenCalledWith('xyz'); expect(response).toEqual(ActionResponse); }); - it('should call siren.deleteNotificationsByDate and update notifications list when siren exists and untilDate is provided', async () => {; + it('should call siren.deleteByDate and update notifications list when siren exists and untilDate is provided', async () => {; jest.spyOn(sirenProvider, 'useSirenContext').mockReturnValue({ siren: mockSiren as Siren, @@ -141,11 +139,11 @@ describe('useSiren hook', () => { const untilDate = '2024-02-28T00:00:00Z'; const response = await deleteByDate(untilDate); - expect(mockSiren.deleteNotificationsByDate).toHaveBeenCalledWith(untilDate); + expect(mockSiren.deleteByDate).toHaveBeenCalledWith(untilDate); expect(response).toEqual(ActionResponse); }); - it('should call siren.markNotificationsAsViewed and update notifications list when siren exists and untilDate is provided', async () => { + it('should call siren.markAllAsViewed and update notifications list when siren exists and untilDate is provided', async () => { jest.spyOn(sirenProvider, 'useSirenContext').mockReturnValue({ siren: mockSiren as Siren, @@ -156,7 +154,7 @@ describe('useSiren hook', () => { const untilDate = '2024-02-28T00:00:00Z'; const response = await markAllAsViewed(untilDate); - expect(mockSiren.markNotificationsAsViewed).toHaveBeenCalledWith(untilDate); + expect(mockSiren.markAllAsViewed).toHaveBeenCalledWith(untilDate); expect(response).toEqual(MarkAsViewedResponse); }); });