diff --git a/README.md b/README.md index dd66a96..917befb 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,16 @@ The `@sirenapp/react-inbox` sdk is a comprehensive and customizable React UI kit ## 1. Installation -You can install the react sdk from npm +You can install the react sdk from npm ```bash -npm @sirenapp/react-inbox +npm install @sirenapp/react-inbox ``` + or from yarn ```bash -yarn @sirenapp/react-inbox +yarn add @sirenapp/react-inbox ``` #### Prerequisites @@ -22,7 +23,9 @@ yarn @sirenapp/react-inbox - React v16.8+ ## 2. Configuration + ### 2.1 Initialization + Initialize the sdk with user token and recipient id. Wrap the provider around your App's root. ```js @@ -37,15 +40,15 @@ const config = { ``` ### 2.2 Configure notification inbox + Once the provider is configured, next step is to configure the notification inbox Inbox is a paginated list view for displaying notifications. ```js -import { SirenInbox } from '@sirenapp/react-inbox'; +import { SirenInbox } from "@sirenapp/react-inbox"; - ``` #### Props for the notification inbox @@ -55,20 +58,17 @@ Below are optional props available for the inbox component: Prop | Description | Type | Default value | --- | --- | --- | --- | theme | Object for custom themes | Theme | {} | -title | Title of the notification inbox | string | "Notifications" | -loadMoreLabel | Text shown on the load more component | string | "Load More" -hideHeader | Toggle to hide or show the header section | boolean | false | -hideClearAll | Toggle to hide or show the clear all button | boolean | false | +loadMoreLabel | Text shown on the load more component | string | "Load More" | hideBadge | Toggle to hide or show the badge | boolean | false | darkMode | Toggle to enable dark mode | boolean | false | itemsPerFetch | Number of notifications fetch per api request (have a max cap of 50) | number | 20 | windowViewOnly | Toggle to enable fit-to-screen window or modal view | boolean | false | notificationIcon | Option to use custom notification Icon | JSX Element | null | -cardProps | Props for customizing the notification cards | { hideAvatar: boolean } | { hideAvatar: false } | +inboxHeaderProps | Props for customizing the header.
title - Title of the notification inbox
hideHeader - Toggle to hide or show the header section.
hideClearAll - Toggle to hide or show the clear all button.
customHeader - Custom header component. | InboxHeaderProps| { title: 'Notifications',
hideHeader: false,
hideClearAll: false,
customHeader: null } | +cardProps | Props for customizing the notification cards.
hideDelete - Toggle to hide or show delete icon
hideAvatar - Toggle to hide or show the avatar.
disableAutoMarkAsRead - Toggle to disable or enable the markAsRead functionality on card click | CardProps | { hideDelete: false,
hideAvatar: false,
disableAutoMarkAsRead: false } | customNotificationCard | Function for rendering custom notification cards | (notification)=> JSX Element | null | onNotificationCardClick | Custom click handler for notification cards | (notification)=> void | ()=>null | listEmptyComponent | Custom component for empty notification list | JSX Element | null | -customHeader | Custom header component | JSX Element | null | customFooter | Custom footer component | JSX Element | null | customLoader | Custom loader component | JSX Element | null | loadMoreComponent | Custom load more component | JSX Element | null | @@ -76,6 +76,7 @@ customErrorWindow | Custom error window | JSX Element | null | onError | Callback for handling errors | (error: SirenErrorType)=> void | null | ## 3. Customization + ### 3.1 Themes Here are the available theme options: @@ -101,7 +102,7 @@ type ThemeProps = { timerIcon?: string, clearAllIcon?: string, infiniteLoader?: string, - windowShadowColor?: string + windowShadowColor?: string, }, badgeStyle?: { color?: string, @@ -128,9 +129,10 @@ type ThemeProps = { loadMoreButton: { color?: string, background?: string, - } + }, }; ``` + ### 3.2 Style options Here are the custom style options for the notification inbox @@ -187,6 +189,27 @@ Please note that the badgeStyle, window shadow and border props are only applica } ``` +#### CardProps + +```js + type CardProps = { + hideDelete?: boolean; + hideAvatar?: boolean, + disableAutoMarkAsRead?: boolean, + }; +``` + +#### InboxHeaderProps + +```js + type InboxHeaderProps = { + title?: string; + hideHeader?: boolean, + hideClearAll?: boolean, + customHeader?: JSX.Element | null, + }; +``` + ## 4. Hooks `useSiren` is a hook that provides utility functions for modifying notifications. @@ -229,43 +252,41 @@ function MyComponent() { } ``` -#### useSiren functions +### useSiren functions -| Functions | Parameters | Type | Description | -| ----------------------------- | ----------------- |---------| ----------------------------------------------------------- | -| markNotificationsAsReadByDate | startDate | ISO date string | Sets the read status of notifications to true until the given date. | -| markAsRead | id | string | Set read status of a notification to true | -| deleteNotification | id | string | Delete a notification by id | -| deleteNotificationsByDate | startDate | ISO date string| Delete all notifications until given date | -| markNotificationsAsViewed | startDate | ISO date string | Sets the viewed status of notifications to true until the given date | +Functions | Parameters | Type | Description | +----------|------------|-------|------------| +markNotificationsAsReadByDate | startDate | ISO date string | Sets the read status of notifications to true until the given date | +markAsRead | id | string | Set read status of a notification to true | +deleteNotification | id | string | Delete a notification by id | +deleteNotificationsByDate | startDate | ISO date string | Delete all notifications until given date | +markNotificationsAsViewed | startDate | ISO date string |Sets the viewed status of notifications to true until the given date | ## 5. Error codes Given below are all possible error codes thrown by sdk. -| Error code | Description | -| ------------------------- | ------------------------------------------------------------------| -| INVALID_TOKEN | The token passed in the provider is invalid | -| INVALID_RECIPIENT_ID | The recipient id passed in the provider is invalid | -| TOKEN_VERIFICATION_FAILED | Verification of the given tokens has failed | -| GENERIC_API_ERROR | Occurrence of an unexpected api error | -| OUTSIDE_SIREN_CONTEXT | Attempting to invoke the functions outside the siren inbox context| -| MISSING_PARAMETER | The required parameter is missing | +Error code | Description | +------------------------- | ------------------------------------------------------------------| +INVALID_TOKEN | The token passed in the provider is invalid | +INVALID_RECIPIENT_ID | The recipient id passed in the provider is invalid | +TOKEN_VERIFICATION_FAILED | Verification of the given tokens has failed | +GENERIC_API_ERROR | Occurrence of an unexpected api error | +OUTSIDE_SIREN_CONTEXT | Attempting to invoke the functions outside the siren inbox context| +MISSING_PARAMETER | The required parameter is missing | ## Example Here's a basic example to help you get started ```js - -import React from 'react'; -import {SirenInbox,SirenProvider} from '@sirenapp/react-inbox'; +import React from "react"; +import { SirenInbox, SirenProvider } from "@sirenapp/react-inbox"; function App(): React.JSX.Element { - const config = { - userToken: 'your_user_token', - recipientId: 'your_recipient_id', + userToken: "your_user_token", + recipientId: "your_recipient_id", }; return ( @@ -278,15 +299,21 @@ function App(): React.JSX.Element { export default App; export function MyContainer(): React.JSX.Element { - return (
); -} \ No newline at end of file +} +``` diff --git a/example/src/App.tsx b/example/src/App.tsx index 5a4517c..f204c45 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -37,7 +37,7 @@ const App: React.FC = () => { const [showCustomNotificationCard, setShowCustomNotificationCard] = useState(false); - const { markNotificationsAsReadByDate, markAsRead } = useSiren(); + const { markNotificationsAsReadByDate } = useSiren(); const renderListEmpty = () => { return ( @@ -248,9 +248,12 @@ const App: React.FC = () => { return (
- { listEmptyComponent={ showCustomEmptyComponent ? renderListEmpty() : undefined } - customHeader={showCustomHeader ? renderCustomHeader() : undefined} customNotificationCard={ showCustomNotificationCard ? (notification: any) => renderCustomNotificationCard(notification) : undefined } - onNotificationCardClick={(notification: { id: any; }) => { + onNotificationCardClick={() => { console.log("click on notification"); - markAsRead(notification.id); }} onError={(error: any) => { console.log(`error: ${error}`); diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 4cfc0a8..bb644e5 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -8,6 +8,7 @@ import defaultAvatarLight from "../assets/light/defaultAvatarLight.png"; import type { NotificationCardProps } from "../types"; import { generateElapsedTimeText } from "../utils/commonUtils"; import "../styles/card.css"; +import useSiren from "../utils/sirenHook"; /** * Card component represents an individual notification card in the notification list. @@ -51,7 +52,10 @@ const Card: FC = ({ }) => { const { id, createdAt, message, isRead } = notification; const { avatar, header, subHeader, body } = message; - + const { hideAvatar, hideDelete, disableAutoMarkAsRead } = cardProps ?? {}; + const { + markAsRead + } = useSiren(); const onDelete = (event: React.MouseEvent) => { deleteNotificationById(id); event.stopPropagation(); @@ -69,20 +73,23 @@ const Card: FC = ({ backgroundColor: styles.activeCardMarker.backgroundColor, }; + const handleNotificationCardClick = () => { + onNotificationCardClick && onNotificationCardClick(notification); + !disableAutoMarkAsRead && markAsRead(notification.id); + } + return (
- onNotificationCardClick && onNotificationCardClick(notification) - } + onClick={handleNotificationCardClick} data-testid={`card-${notification.id}`} > - {!cardProps?.hideAvatar && ( + {!hideAvatar && (
= ({
-
- -
+ {!hideDelete && ( +
+ +
+ )} ); }; diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index 227ea3e..501f526 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -1,13 +1,17 @@ -import React from "react"; +import React, { type FC } from "react"; import "../styles/loader.css"; -import type { SirenStyleProps } from "../types"; +import type { LoaderProps } from "../types"; -const Loader = ({styles}: {styles: SirenStyleProps}) => { +const Loader : FC = ({ + hideAvatar, + styles, + +}) => { return (
-
-
+
+ {!hideAvatar && (
)}
diff --git a/src/components/SirenInbox.tsx b/src/components/SirenInbox.tsx index 1f1f1cd..b6c1397 100644 --- a/src/components/SirenInbox.tsx +++ b/src/components/SirenInbox.tsx @@ -4,28 +4,33 @@ import NotificationButton from "./SirenNotificationIcon"; import SirenPanel from "./SirenPanel"; import { useSirenContext } from "./SirenProvider"; import type { SirenProps } from "../types"; -import { Constants } from "../utils"; -import { applyTheme, calculateModalPosition } from "../utils/commonUtils"; -import { BadgeType, MAXIMUM_ITEMS_PER_FETCH, ThemeMode } from "../utils/constants"; +import { DefaultStyle } from "../utils"; +import { + applyTheme, + calculateModalPosition, + calculateModalWidth, + debounce, +} from "../utils/commonUtils"; +import { + BadgeType, + MAXIMUM_ITEMS_PER_FETCH, + ThemeMode, +} from "../utils/constants"; import "../styles/sirenInbox.css"; -const { DEFAULT_WINDOW_TITLE } = Constants; /** * SirenInbox Component * @param {Object} props - Props for the SirenInbox component * @param {Theme} props.theme - The theme for the SirenInbox component - * @param {string} props.title - The title of the notification panel. * @param {string} [props.title] - The title for the SirenInbox component * @param {boolean} [props.windowViewOnly=false] - Flag indicating if the window is view-only - * @param {boolean} [props.hideHeader] - Flag indicating if the header should be hidden - * @param {boolean} [props.hideClearAll] - Flag indicating if the clear all button should be hidden * @param {boolean} [props.hideBadge] - Flag indicating if the badge should be hidden + * @param {CardProps} [props.inboxHeaderProps] - Object containing props related to the inbox header * @param {boolean} [props.darkMode] - Flag indicating if the component is in dark mode * @param {CardProps} [props.cardProps] - Additional props for the card component * @param {ReactNode} [props.notificationIcon] - The notification icon for the window * @param {JSX.Element} [props.listEmptyComponent] - JSX element for rendering when the list is empty * @param {JSX.Element} [props.customFooter] - Custom footer JSX element for the window - * @param {JSX.Element} [props.customHeader] - Custom header JSX element for the window * @param {Function} [props.customNotificationCard] - Function to render custom notification card * @param {Function} [props.onNotificationCardClick] - Handler for notification card click event * @param {Function} [props.onError] - Handler for error events @@ -40,17 +45,14 @@ const { DEFAULT_WINDOW_TITLE } = Constants; const SirenInbox: FC = ({ theme, customStyles, - title = DEFAULT_WINDOW_TITLE, windowViewOnly = false, - hideHeader, hideBadge = false, - hideClearAll = false, darkMode = false, + inboxHeaderProps, cardProps, notificationIcon, listEmptyComponent, customFooter, - customHeader, customLoader, customErrorWindow, loadMoreComponent, @@ -59,18 +61,25 @@ const SirenInbox: FC = ({ onError, itemsPerFetch = 20, }) => { - const notificationsPerPage = - itemsPerFetch > MAXIMUM_ITEMS_PER_FETCH ? MAXIMUM_ITEMS_PER_FETCH : itemsPerFetch; + const notificationsPerPage = Math.max( + 0, + itemsPerFetch > MAXIMUM_ITEMS_PER_FETCH + ? MAXIMUM_ITEMS_PER_FETCH + : itemsPerFetch + ); + const [isModalOpen, setIsModalOpen] = useState(false); const { siren } = useSirenContext(); const iconRef = useRef(null); //ref for the modal const modalRef = useRef(null); const [modalPosition, setModalPosition] = useState<{ - top: string; right?: string; - }>({ top: "0" }); - + left?: string; + }>(); + const initialModalWidth = + customStyles?.window?.width || DefaultStyle.window.width; + const [updatedModalWidth, setUpdatedModalWidth] = useState(initialModalWidth); const styles = useMemo( () => applyTheme( @@ -95,18 +104,26 @@ const SirenInbox: FC = ({ }, []); useEffect(() => { - const updateModalPosition = () => { - const containerWidth = styles.container.width || 400; + const modalWidth = calculateModalWidth(initialModalWidth); + + if (window.outerWidth <= modalWidth) + // Subtract 40 pixels to account for padding within the window container + setUpdatedModalWidth(window.outerWidth - 40); + else setUpdatedModalWidth(initialModalWidth); + }, [window.outerWidth, initialModalWidth]); + useEffect(() => { + const containerWidth = styles.container.width || DefaultStyle.window.width; + const updateWindowViewMode = () => { setModalPosition(calculateModalPosition(iconRef, window, containerWidth)); }; - updateModalPosition(); // Initial calculation - window.addEventListener("resize", updateModalPosition); // Event listener for window resize + const debouncedUpdate = debounce(updateWindowViewMode, 200); - return () => { - window.removeEventListener("resize", updateModalPosition); - }; + updateWindowViewMode(); + window.addEventListener("resize", debouncedUpdate); + + return () => window.removeEventListener("resize", debouncedUpdate); }, [styles.container.width]); const onMouseUp = (event: Event): void => { @@ -123,62 +140,56 @@ const SirenInbox: FC = ({ }; return ( -
- {!windowViewOnly && ( -
- -
- )} - - {(isModalOpen || windowViewOnly) && ( -
- -
- )} +
+
+ {!windowViewOnly && ( +
+ +
+ )} + + {(isModalOpen || windowViewOnly) && ( +
+ +
+ )} +
); }; diff --git a/src/components/SirenPanel.tsx b/src/components/SirenPanel.tsx index 57ded89..4450a6e 100644 --- a/src/components/SirenPanel.tsx +++ b/src/components/SirenPanel.tsx @@ -24,7 +24,7 @@ import { mergeArrays, updateNotifications, } from "../utils/commonUtils"; -import { ERROR_TEXT, events, eventTypes, VerificationStatus } from "../utils/constants"; +import { DEFAULT_WINDOW_TITLE, ERROR_TEXT, events, eventTypes, VerificationStatus } from "../utils/constants"; import useSiren from "../utils/sirenHook"; /** @@ -46,20 +46,18 @@ import useSiren from "../utils/sirenHook"; * * @param {SirenPanelProps} props - The properties passed to the SirenWindow component. * @param {Object} props.styles - Custom styles applied to the notification panel and its elements. - * @param {string} props.title - The title of the notification panel. - * @param {boolean} props.hideHeader=false] - Whether to hide the header of the notification panel. - * @param {boolean} props.hideClearAll=false] - Flag indicating if the clear all button should be hidden * @param {boolean} [props.hideBadge] - Flag indicating if the badge should be hidden * @param {string} props.loadMoreLabel - Label for load more button + * @param {Object} props.inboxHeaderProps - Object containing props related to the inbox header. * @param {Object} props.cardProps - Optional properties to customize the appearance of notification cards. * @param {Function} props.renderListEmpty - Function to render content when the notification list is empty. * @param {ReactNode} props.customFooter - Custom footer component to be rendered below the notification list. - * @param {ReactNode} props.customHeader - Custom header component to be rendered above the notification list. * @param {ReactNode} pros.customLoader - Custom Loader component to be rendered while fetching notification list for the first time - * @param {ReactNode} pros.loadMoreComponent -Custom load more component to be rendered + * @param {ReactNode} pros.loadMoreComponent -Custom load more component to be rendered * @param {ReactNode} props.customErrorWindow -Custom error window component to be rendered when there is an error * @param {Function} props.customNotificationCard - Function to render custom notification cards. * @param {Function} props.onNotificationCardClick - Callback function executed when a notification card is clicked. + * @param {DimensionValue} props.modalWidth - The width of the notification panel. * @returns {ReactElement} The rendered SirenInbox component. */ @@ -72,14 +70,12 @@ type EventListenerDataType = { const SirenPanel: FC = ({ styles, - title, loadMoreLabel, - hideHeader, hideBadge, darkMode, + inboxHeaderProps, cardProps, customFooter, - customHeader, loadMoreComponent, fullScreen, customLoader, @@ -89,7 +85,7 @@ const SirenPanel: FC = ({ customNotificationCard, onNotificationCardClick, onError, - hideClearAll = false, + modalWidth, }) => { const { markNotificationsAsViewed, @@ -97,6 +93,7 @@ const SirenPanel: FC = ({ deleteNotification, } = useSiren(); const { siren, verificationStatus } = useSirenContext(); + const {hideHeader = false, hideClearAll = false, customHeader, title = DEFAULT_WINDOW_TITLE} = inboxHeaderProps ?? {}; const [notifications, setNotifications] = useState( [] ); @@ -131,7 +128,7 @@ const SirenPanel: FC = ({ notifications ); - if(!isEmptyArray(updatedNotifications)) handleMarkNotificationsAsViewed(updatedNotifications[0]?.createdAt); + if(!isEmptyArray(eventListenerData?.newNotifications)) handleMarkNotificationsAsViewed(updatedNotifications[0]?.createdAt); setNotifications(updatedNotifications); setEventListenerData(null); } @@ -144,7 +141,6 @@ const SirenPanel: FC = ({ } }, [siren, verificationStatus, hideBadge]); - const restartNotificationCountFetch = () => { try { siren?.startRealTimeUnviewedCountFetch(); @@ -178,7 +174,7 @@ const SirenPanel: FC = ({ const response = await deleteNotificationsByDate( notifications[0].createdAt ); - + response && triggerOnError(response); if (response && isValidResponse(response)) { @@ -202,7 +198,8 @@ const SirenPanel: FC = ({ setNotifications(updatedNotifications); - if(isRefresh)handleMarkNotificationsAsViewed(updatedNotifications[0].createdAt); + if (isRefresh) + handleMarkNotificationsAsViewed(updatedNotifications[0].createdAt); }; const fetchNotifications = async (isRefresh = false) => { @@ -226,17 +223,15 @@ const SirenPanel: FC = ({ if (!isEmptyArray(response.data ?? [])) { data = filterDataProperty(response); if (!data) return []; - if(response?.meta) { + if (response?.meta) { + const isLastPage = response?.meta?.last === "true"; - const isLastPage = response?.meta?.last === 'true'; - - if(isLastPage) setEndReached(true); + if (isLastPage) setEndReached(true); else setEndReached(false); } updateNotificationList(data, isRefresh); } - if (!data) - setEndReached(true); + if (!data) setEndReached(true); resetRealTimeFetch(isRefresh, data); } else { setEndReached(true); @@ -320,29 +315,38 @@ const SirenPanel: FC = ({ }; const renderList = () => { - if (isLoading && isEmptyArray(notifications)) + if (isLoading && isEmptyArray(notifications)) { + const hideAvatar = cardProps?.hideAvatar || false; + return (
{customLoader || <> - - - - - + + + + + }
); + } if (error) return ( - customErrorWindow || + customErrorWindow || ( + + ) ); if (isEmptyArray(notifications)) return ( listEmptyComponent || ( - + ) ); @@ -364,19 +368,24 @@ const SirenPanel: FC = ({ }; const renderListBottomComponent = () => { - if ( - isEmptyArray(notifications) - ) - return null; - if (isLoading && !endReached) + if (isEmptyArray(notifications)) return null; + if (isLoading && !endReached) return ( -
-
+
+
- ) + ); if (!isLoading && !endReached) return ( - + ); return null; @@ -388,12 +397,19 @@ const SirenPanel: FC = ({ "siren-sdk-content-container" } ${customFooter ? "siren-sdk-panel-no-border" : ""}`; + const panelStyle = { + ...(!fullScreen && styles.windowTopBorder), + ...(!fullScreen && { width: `${modalWidth}px` }), + ...(!fullScreen && styles.windowBottomBorder), + ...styles.container, + }; + return (
@@ -409,11 +425,17 @@ const SirenPanel: FC = ({ /> ))}
{renderList()} diff --git a/src/styles/header.css b/src/styles/header.css index a15220b..57e452f 100644 --- a/src/styles/header.css +++ b/src/styles/header.css @@ -16,7 +16,6 @@ font-weight: 500; font-size: 14px; line-height: 20px; - margin-bottom: 12px; } .siren-sdk-text-break { diff --git a/src/styles/loader.css b/src/styles/loader.css index 6367eba..91f96b6 100644 --- a/src/styles/loader.css +++ b/src/styles/loader.css @@ -15,7 +15,7 @@ } } -.siren-sdk-skeleton-grid { +.siren-sdk-skeleton-grid-with-avatar { display: grid; grid-template-columns: 70px 10px auto auto auto auto 25px; grid-template-areas: @@ -26,6 +26,17 @@ gap:8px ; } +.siren-sdk-skeleton-grid-without-avatar { + display: grid; + grid-template-columns: 10px auto auto auto auto 25px; + grid-template-areas: + 'header header header header header close' + 'subheader subheader subheader subheader subheader close' + 'body body body body body close' + 'icon footer footer footer footer close'; + gap:8px ; +} + .siren-sdk-skeleton-head { grid-area: header; height: 20px; diff --git a/src/styles/sirenInbox.css b/src/styles/sirenInbox.css index 0918d27..2705272 100644 --- a/src/styles/sirenInbox.css +++ b/src/styles/sirenInbox.css @@ -1,3 +1,8 @@ +.siren-sdk-inbox-root { + position: fixed; +} + .siren-sdk-inbox-container { width: fit-content; + position: relative; } \ No newline at end of file diff --git a/src/styles/sirenPanel.css b/src/styles/sirenPanel.css index 2e6b779..3d1d494 100644 --- a/src/styles/sirenPanel.css +++ b/src/styles/sirenPanel.css @@ -2,7 +2,7 @@ width: 100%; } .siren-sdk-panel-modal { - position: absolute; + position:absolute; box-shadow: rgb(0 0 0 / 30%) 0px 8px 24px; border-radius: 20px; background-color: #FFFFFF; diff --git a/src/types.ts b/src/types.ts index 48c8790..86ed51c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,10 +8,13 @@ import type { export type SirenInboxProps = { theme?: Theme; customStyles?: CustomStyle, - title?: string; loadMoreLabel?: string, - hideHeader?: boolean; - hideClearAll?: boolean; + inboxHeaderProps?: { + title?: string; + hideHeader?: boolean; + hideClearAll?: boolean; + customHeader?: JSX.Element; + } hideBadge?: boolean; darkMode?: boolean; itemsPerFetch?: number; @@ -19,7 +22,6 @@ export type SirenInboxProps = { listEmptyComponent?: JSX.Element; loadMoreComponent?:JSX.Element; customFooter?: JSX.Element; - customHeader?: JSX.Element; customLoader?: JSX.Element; customErrorWindow?: JSX.Element; customNotificationCard?: (notification: NotificationDataType) => JSX.Element; @@ -48,6 +50,8 @@ export type SirenProps = SirenInboxProps & export type CardProps = { hideAvatar?: boolean; showMedia?: boolean; + hideDelete?: boolean; + disableAutoMarkAsRead?: boolean; }; export type NotificationCardProps = { @@ -69,14 +73,12 @@ export type SirenNotificationButtonProps = { }; export type SirenPanelProps = Pick< SirenInboxProps, - | "hideHeader" | "hideBadge" | "cardProps" | "customFooter" - | "customHeader" | "customNotificationCard" | "onNotificationCardClick" - | "hideClearAll" + | "inboxHeaderProps" | "customLoader" | "loadMoreComponent" | "loadMoreLabel" @@ -85,10 +87,10 @@ export type SirenPanelProps = Pick< styles: SirenStyleProps; onError?: (error: SirenErrorType) => void; listEmptyComponent?: JSX.Element; - title: string; noOfNotificationsPerFetch: number; fullScreen: boolean; darkMode: boolean; + modalWidth: DimensionValue; }; export type HeaderProps = { @@ -100,6 +102,11 @@ export type HeaderProps = { handleClearAllNotification: () => void; }; +export type LoaderProps = { + styles: SirenStyleProps; + hideAvatar: boolean; +} + type BadgeType = "none" | "dot" | "default"; export type Theme = { diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts index 9b3e26d..7bef273 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -6,7 +6,12 @@ import type { NotificationsApiResponse, } from "@sirenapp/js-sdk/dist/esm/types"; -import { defaultBadgeStyle, eventTypes, LogLevel, ThemeMode } from "./constants"; +import { + defaultBadgeStyle, + eventTypes, + LogLevel, + ThemeMode, +} from "./constants"; import type { CustomStyle, DimensionValue, @@ -122,7 +127,6 @@ export const applyTheme = ( return { container: { - width: customStyle.window?.width || DefaultStyle.window.width, maxWidth: customStyle.window?.width || "100", }, windowShadow: { @@ -369,38 +373,64 @@ export const calculateModalPosition = ( ) => { if (iconRef.current) { const iconRect = iconRef.current.getBoundingClientRect(); - const screenWidth = window.innerWidth; + const screenWidth = window.outerWidth; const spaceRight = screenWidth - iconRect.x; const spaceLeft = iconRect.x; - let modalWidth = 400; + let modalWidth = calculateModalWidth(containerWidth); - if (typeof containerWidth === "string") - modalWidth = parseInt(containerWidth.slice(0, -2)); - else if (typeof containerWidth === "number") modalWidth = containerWidth; + const centerPosition = + Math.min(spaceLeft, spaceRight) + Math.abs(spaceLeft - spaceRight) / 2; - const topPosition = iconRect.bottom; - const leftPosition = screenWidth / 2 - modalWidth / 2; + if (window.outerWidth <= modalWidth) modalWidth = window.outerWidth - 40; - if ( - spaceLeft < modalWidth && - spaceRight < modalWidth && - screenWidth > modalWidth - ) { - return { top: `${topPosition}px`, left: `-${leftPosition}px` }; - } else { - const rightPosition = spaceRight < modalWidth + 30 ? "30px" : null; + if (spaceRight > modalWidth) { + return { + left: `0px`, + }; + } else if (spaceLeft > modalWidth) { + const rightPosition = spaceRight < modalWidth ? "30px" : null; return { - top: `${topPosition}px`, ...(rightPosition && { right: rightPosition }), }; + } else if ( + spaceLeft < modalWidth && + spaceRight < modalWidth && + spaceLeft > spaceRight + ) { + return { right: "30px" }; + } else { + return { left: `-${centerPosition - 40}px` }; } } +}; + +export const calculateModalWidth = (containerWidth: DimensionValue): number => { + let modalWidth = 500; + + if (typeof containerWidth === "string") + modalWidth = parseInt(containerWidth.slice(0, -2)) + 40; + else if (typeof containerWidth === "number") modalWidth = containerWidth + 40; - return { top: "0" }; + return modalWidth; }; + export const hexToRgba = (hex: string, alpha: number) => { const [r, g, b] = hex.match(/\w\w/g)?.map((x) => parseInt(x, 16)) ?? []; return `rgba(${r},${g},${b},${alpha})`; }; + +export const debounce = void>( + func: F, + delay: number +) => { + let timerId: ReturnType; + + return (...args: Parameters): void => { + clearTimeout(timerId); + timerId = setTimeout(() => { + func(...args); + }, delay); + }; +}; diff --git a/tests/components/__snapshots__/sirenInbox.spec.tsx.snap b/tests/components/__snapshots__/sirenInbox.spec.tsx.snap index 05a29f0..bb7e459 100644 --- a/tests/components/__snapshots__/sirenInbox.spec.tsx.snap +++ b/tests/components/__snapshots__/sirenInbox.spec.tsx.snap @@ -3,29 +3,33 @@ exports[`matches snapshot 1`] = `
-
- + + + + +
diff --git a/tests/components/__snapshots__/sirenPanel.spec.tsx.snap b/tests/components/__snapshots__/sirenPanel.spec.tsx.snap index 82ed4fb..187b803 100644 --- a/tests/components/__snapshots__/sirenPanel.spec.tsx.snap +++ b/tests/components/__snapshots__/sirenPanel.spec.tsx.snap @@ -5,7 +5,7 @@ exports[`matches snapshot 1`] = `
{ @@ -42,14 +43,15 @@ test("matches snapshot", () => { }); it("renders title when provided", () => { - const { getByText } = render(); + const { getByText } = render(); const title = getByText("Notifications"); expect(title).toBeTruthy(); }); it("hides header when hideHeader prop is true", () => { - const { queryByText } = render(); + const inboxHeaderProps = {hideHeader: true} + const { queryByText } = render(); const header = queryByText("Notifications"); expect(header).toBeFalsy(); @@ -57,8 +59,9 @@ it("hides header when hideHeader prop is true", () => { it("renders custom header when provided", () => { const customHeader =
Custom Header
; + const inboxHeaderProps = {customHeader} const { getByTestId } = render( - + ); const header = getByTestId("custom-header");