Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DApp-1846 Notification toast for Channel notifications. #1875

Merged
merged 12 commits into from
Oct 9, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@mui/material": "^5.5.0",
"@pushprotocol/restapi": "1.7.25",
"@pushprotocol/socket": "0.5.3",
"@pushprotocol/uiweb": "1.4.3",
"@pushprotocol/uiweb": "1.5.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-switch": "^1.1.0",
Expand Down
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import SpaceContextProvider from 'contexts/SpaceContext';
import { SpaceWidgetSection } from 'sections/space/SpaceWidgetSection';
import { blocksColors, getBlocksCSSVariables, Notification } from 'blocks';
import APP_PATHS from 'config/AppPaths';
import { useRewardsNotification } from 'common';
import { useInAppNotifications, useRewardsNotification } from 'common';

dotenv.config();

Expand Down Expand Up @@ -338,6 +338,8 @@ export default function App() {
location?.pathname.includes('/snap') ||
location?.pathname.includes(APP_PATHS.DiscordVerification);

useInAppNotifications();

return (
<ThemeProvider theme={darkMode ? themeDark : themeLight}>
{/* {(!isActive || !allowedChain) && (
Expand Down
52 changes: 31 additions & 21 deletions src/blocks/notification/Notification.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC } from 'react';
import styled from 'styled-components';
import { Cross } from '../icons';
import { NotificationProps } from './Notification.types';
import { Cross } from 'blocks';
import { toast, Toaster } from 'sonner';
import { getTextVariantStyles } from 'blocks/Blocks.utils';
import { deviceMediaQ } from 'blocks/theme';
Expand All @@ -24,7 +24,12 @@ const NotificationContainer = styled.div`
width: -webkit-fill-available;
}
`;

const StyledToaster = styled(Toaster)`
width: 397px;
@media${deviceMediaQ.mobileL} {
width: 100%;
}
`;
const TextContainer = styled.div`
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -64,37 +69,42 @@ const CloseButton = styled.div`
right: var(--spacing-xxs);
top: var(--spacing-xxs);
`;
const Container = styled.div``;

const NotificationItem: FC<NotificationProps> = ({ onClose, title, description, image, onClick }) => {
const NotificationItem: FC<NotificationProps> = ({ overlay, onClose, title, description, image, onClick }) => {
const handleNotificationClick = () => onClick?.();

const handleNotificationClose = () => {
onClose?.();
notification.hide();
};

return (
<NotificationContainer onClick={handleNotificationClick}>
<IconContainer>{image}</IconContainer>
<CloseButton
onClick={(e) => {
e.stopPropagation();
handleNotificationClose();
}}
>
<Cross size={16} />
</CloseButton>
<TextContainer>
<NotificationTitle>{title}</NotificationTitle>
<NotificationDescription>{description}</NotificationDescription>
</TextContainer>
</NotificationContainer>
<Container onClick={handleNotificationClick}>
{overlay ? (
<>{overlay}</>
) : (
<NotificationContainer>
{image && <IconContainer>{image}</IconContainer>}
<CloseButton
onClick={(e) => {
e.stopPropagation();
handleNotificationClose();
}}
>
<Cross size={16} />
</CloseButton>
<TextContainer>
<NotificationTitle>{title}</NotificationTitle>
<NotificationDescription>{description}</NotificationDescription>
</TextContainer>
</NotificationContainer>
)}
</Container>
);
};

const Notification = () => {
return (
<Toaster
<StyledToaster
offset={15}
visibleToasts={5}
/>
Expand Down
15 changes: 8 additions & 7 deletions src/blocks/notification/Notification.types.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { ReactNode } from 'react';

export type NotificationProps = {
/* Svg React component to be passed as the image. */
image: ReactNode;
image?: ReactNode;
/* Title of the notification */
title: string;
title?: string;
/* Description of the notification */
description: string;
/* Optional onClick event for the notification */
onClick?: () => void;
description?: string;
/* Optional onClose action for the notification */
onClose?: () => void;
/* Custom React component to be passed as the image. */
overlay?: ReactNode;
/* Optional onClick event for the notification */
onClick?: () => void;
/* Position of the notification */
position?: 'bottom-right' | 'bottom-left';
position?: 'bottom-right' | 'bottom-left' | 'top-center';
/* Optional duration of the notification component */
duration?: number;
};
1 change: 1 addition & 0 deletions src/common/Common.form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const URLRegex = /^(http:\/\/|https:\/\/|www\.)?([\w-]+\.)+[\w-]{2,}(\/[\w.-]*)*\/?$/;

export const getRequiredFieldMessage = (name: string) => `${name} is required.`;
export const getValidURLMessage = (name: string) => `${name} is invalid.`;

export const getMaxCharLimitFieldMessage = (limit: number) => `Maximum ${limit} characters allowed.`;

Expand Down
14 changes: 14 additions & 0 deletions src/common/Common.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,17 @@ export const getSelectChains = (chainIdList: Array<number>) => {
};
});
};

export const isValidURL = (str: string | undefined) => {
if (!str) return false;
const pattern = new RegExp(
'^(https?:\\/\\/)' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\S*)?$',
'i'
); // fragment locator
return !!pattern.test(str);
};
44 changes: 44 additions & 0 deletions src/common/components/InAppChannelNotifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NotificationEvent } from '@pushprotocol/restapi';
import { NotificationItem, chainNameType } from '@pushprotocol/uiweb';
import { Box, Link, notification } from 'blocks';
import { useBlocksTheme } from 'blocks/Blocks.hooks';
import APP_PATHS from 'config/AppPaths';
import { FC } from 'react';

type InAppNotificationsProps = {
notificationDetails: NotificationEvent | null;
};

const InAppChannelNotifications: FC<InAppNotificationsProps> = ({ notificationDetails }) => {
const payload = notificationDetails?.message?.payload;
const { mode } = useBlocksTheme();
return (
<Link
to={payload?.cta || APP_PATHS.Inbox}
target="_blank"
>
<Box
display="flex"
width="397px"
>
{notificationDetails && (
<NotificationItem
isToast
onClose={() => notification.hide()}
notificationTitle={payload?.title}
notificationBody={payload?.body}
cta={payload?.cta}
image={payload?.embed}
app={notificationDetails?.channel?.name}
icon={notificationDetails?.channel?.icon}
url={notificationDetails?.channel?.url}
chainName={notificationDetails?.source as chainNameType}
theme={mode}
/>
)}
</Box>
</Link>
);
};

export { InAppChannelNotifications };
1 change: 1 addition & 0 deletions src/common/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './UnsubscribeChannelDropdown';
export * from './ModalHeader';
export * from './StakingVariant';
export * from './TokenFaucet';
export * from './InAppChannelNotifications';
1 change: 1 addition & 0 deletions src/common/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { useIsVisible } from './useIsVisible';
export { usePushStakingStats } from './usePushStakingStats';
export { useRewardsNotification } from './useRewardsNotification';
export { useDisclosure } from './useDisclosure';
export { useInAppNotifications } from './useInAppNotifications';
68 changes: 68 additions & 0 deletions src/common/hooks/useInAppNotifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react';

import { CONSTANTS, NotificationEvent } from '@pushprotocol/restapi';
import { useSelector } from 'react-redux';

import { deviceSizes, notification } from 'blocks';
import { InAppChannelNotifications } from 'common';

import { useDeviceWidthCheck } from 'hooks';

export const useInAppNotifications = () => {
const [isStreamConnected, setIsStreamConnected] = useState<boolean>(false);
const isMobile = useDeviceWidthCheck(parseInt(deviceSizes.mobileL));
const { userPushSDKInstance } = useSelector((state: any) => {
return state.user;
});
const attachListeners = async () => {
userPushSDKInstance?.stream?.on(CONSTANTS.STREAM.CONNECT, (err: Error) => {
console.debug(
'src::common::hooks::useStream::attachListeners::CONNECT::',
userPushSDKInstance?.uid,
userPushSDKInstance?.stream?.uid,
userPushSDKInstance?.stream
);
setIsStreamConnected(true);
});

userPushSDKInstance?.stream?.on(CONSTANTS.STREAM.DISCONNECT, (err: Error) => {
console.debug(
'src::common::hooks::useStream::attachListeners::DISCONNECT::',
userPushSDKInstance?.uid,
userPushSDKInstance?.stream?.uid,
userPushSDKInstance?.stream
);
setIsStreamConnected(false);
});
userPushSDKInstance?.stream?.on(CONSTANTS.STREAM.NOTIF, (data: NotificationEvent) => {
console.debug(
'src::common::hooks::useStream::attachListeners::NOTIF::',
userPushSDKInstance,
userPushSDKInstance?.uid,
userPushSDKInstance?.stream?.uid,
userPushSDKInstance?.stream
);
notification.show({
overlay: <InAppChannelNotifications notificationDetails={data} />,
position: isMobile ? 'top-center' : 'bottom-right',
duration: 5000,
onClick: () => {
notification.hide();
},
});
});
};

useEffect(() => {
(async () => {
if (userPushSDKInstance && userPushSDKInstance?.stream) {
await attachListeners();
}
})();

// Cleanup listener on unmount
return () => {};
}, [userPushSDKInstance?.account]);

return { isStreamConnected };
};
30 changes: 15 additions & 15 deletions src/contexts/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
alpha: { feature: ['SCALABILITY_V2'] },
});

// sets up stream in read mode
await setupStream(userInstance);
mishramonalisha76 marked this conversation as resolved.
Show resolved Hide resolved
console.debug('src::contexts::AppContext::initializePushSdkReadMode::User Instance Initialized', userInstance);
dispatch(setUserPushSDKInstance(userInstance));
return userInstance;
Expand Down Expand Up @@ -300,21 +302,19 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {

const setupStream = async (userInstance: any) => {
// Connect stream as well
const stream = await userInstance.initStream([
CONSTANTS.STREAM.CONNECT,
CONSTANTS.STREAM.DISCONNECT,
CONSTANTS.STREAM.CHAT,
CONSTANTS.STREAM.CHAT_OPS,
CONSTANTS.STREAM.NOTIF,
CONSTANTS.STREAM.VIDEO,
]);

stream.on(CONSTANTS.STREAM.CONNECT, () => {
console.debug('src::contexts::AppContext::setupStream::CONNECT::');
});

await stream.connect();
console.debug('src::contexts::AppContext::setupStream::User Intance Stream Connected', userInstance);
if (!userInstance.stream) {
const stream = await userInstance.initStream([
CONSTANTS.STREAM.CONNECT,
CONSTANTS.STREAM.DISCONNECT,
CONSTANTS.STREAM.CHAT,
CONSTANTS.STREAM.CHAT_OPS,
CONSTANTS.STREAM.NOTIF,
CONSTANTS.STREAM.VIDEO,
]);

if (userInstance.readmode()) await stream.connect();
console.debug('src::contexts::AppContext::setupStream::User Intance Stream Connected', userInstance);
}
};

// To reformat errors
Expand Down
13 changes: 7 additions & 6 deletions src/hooks/useStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import { VideoCallContext } from 'contexts/VideoCallContext';
import { ADDITIONAL_META_TYPE } from '@pushprotocol/restapi/src/lib/payloads';
import { VideoCallStatus } from '@pushprotocol/restapi';
import { showNotifcationToast } from 'components/reusables/toasts/toastController';
import { useSelector } from 'react-redux';

const useSDKStream = () => {
Expand Down Expand Up @@ -60,8 +59,6 @@ const useSDKStream = () => {
retry: true,
});
}
} else {
showNotifcationToast(feedItem);
}
}
} catch (e) {
Expand All @@ -73,9 +70,13 @@ const useSDKStream = () => {
useEffect(() => {
if (userPushSDKInstance?.signer) {
(async () => {
const stream = await userPushSDKInstance.initStream([STREAM.CONNECT, STREAM.DISCONNECT, STREAM.NOTIF]);
stream.connect();
setStream(stream);
if (userPushSDKInstance?.stream && !userPushSDKInstance?.stream?.disconnected) {
setStream(userPushSDKInstance?.stream);
} else {
const stream = await userPushSDKInstance.initStream([STREAM.CONNECT, STREAM.DISCONNECT, STREAM.NOTIF]);
stream.connect();
setStream(stream);
}
})();
}
}, [userPushSDKInstance]);
Expand Down
Loading
Loading