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

In-App chat notifications #1945

Merged
merged 16 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
"@metamask/eth-sig-util": "^4.0.0",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.5.0",
"@pushprotocol/restapi": "1.7.25",
"@pushprotocol/restapi": "1.7.29",
"@pushprotocol/socket": "0.5.3",
"@pushprotocol/uiweb": "1.7.1",
"@pushprotocol/uiweb": "1.7.2",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-switch": "^1.1.0",
Expand Down
32 changes: 32 additions & 0 deletions src/blocks/icons/components/FillCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FC } from 'react';
import { IconWrapper } from '../IconWrapper';
import { IconProps } from '../Icons.types';

const FillCircle: FC<IconProps> = (allProps) => {
const { svgProps: props, ...restProps } = allProps;
return (
<IconWrapper
componentName="FillCircle"
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="inherit"
height="inherit"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<circle
cx="10"
cy="10"
r="10"
fill="currentColor"
/>
</svg>
}
{...restProps}
/>
);
};

export default FillCircle;
55 changes: 55 additions & 0 deletions src/blocks/icons/components/Image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { FC } from 'react';
import { IconWrapper } from '../IconWrapper';
import { IconProps } from '../Icons.types';

const Image: FC<IconProps> = (allProps) => {
const { svgProps: props, ...restProps } = allProps;
return (
<IconWrapper
componentName="Image"
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="inherit"
height="inherit"
viewBox="0 0 24 22"
fill="none"
{...props}
>
<path
d="M15.2109 9.20528C15.97 9.20528 16.5854 8.58987 16.5854 7.83073C16.5854 7.07158 15.97 6.45618 15.2109 6.45618C14.4517 6.45618 13.8363 7.07158 13.8363 7.83073C13.8363 8.58987 14.4517 9.20528 15.2109 9.20528Z"
fill="currentColor"
/>
<path
d="M14.2155 15.1617L17.1581 12.2224C17.33 12.0507 17.563 11.9542 17.8059 11.9542C18.0488 11.9542 18.2818 12.0507 18.4537 12.2224L23 16.7722"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M1.0072 15.6989L7.23163 9.47329C7.31674 9.38809 7.41781 9.3205 7.52905 9.27438C7.6403 9.22827 7.75954 9.20453 7.87997 9.20453C8.00039 9.20453 8.11963 9.22827 8.23088 9.27438C8.34213 9.3205 8.44319 9.38809 8.5283 9.47329L19.2555 20.2017"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<rect
x="1"
y="1.79828"
width="22"
height="18.4034"
rx="4"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
}
{...restProps}
/>
);
};

export default Image;
2 changes: 2 additions & 0 deletions src/blocks/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ export { default as ErrorFilled } from './components/ErrorFilled';
export { default as ExternalLink } from './components/ExternalLink';

export { default as Front } from './components/Front';
export { default as FillCircle } from './components/FillCircle';

export { default as Gif } from './components/Gif';

export { default as InfoFilled } from './components/InfoFilled';
export { default as Image } from './components/Image';

export { default as Governance } from './components/Governance';
export { default as GovernanceFilled } from './components/GovernanceFilled';
Expand Down
6 changes: 4 additions & 2 deletions src/blocks/notification/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ const toastIds: Array<string | number> = [];

// Export the notification object with show and hide methods
const notification = {
show: (config: NotificationProps) => {
show: (config: NotificationProps, id?: string) => {
const toastId = toast.custom(() => <NotificationItem {...config} />, {
id: id,
duration: config.duration || Infinity,
position: config.position || 'bottom-right',
onAutoClose: config.onAutoClose,
});
toastIds.push(toastId);
if (!toastIds.find((toastId) => toastId === id)) toastIds.push(toastId);
},
hide: () => {
if (toastIds.length > 0) {
Expand Down
2 changes: 2 additions & 0 deletions src/blocks/notification/Notification.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export type NotificationProps = {
position?: 'bottom-right' | 'bottom-left' | 'top-center';
/* Optional duration of the notification component */
duration?: number;
/* Optional onAutoClose event for the notification called after it's timeout */
onAutoClose?: () => void;
};
17 changes: 17 additions & 0 deletions src/common/Common.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { appConfig } from 'config';
import { LOGO_ALIAS_CHAIN } from './Common.constants';
import { networkName } from 'helpers/UtilityHelper';
import { EnvType } from './Common.types';
import moment from 'moment';

export const allowedNetworks = appConfig.allowedNetworks.filter(
(chain: number) => chain != appConfig.coreContractChain
Expand Down Expand Up @@ -53,3 +54,19 @@ export const isValidURL = (str: string | undefined) => {
export const getCurrentEnv = (): EnvType => {
return appConfig.appEnv;
};

export function convertTimeStamp(timestamp: string) {
const date = moment.unix(Number(timestamp));
const now = moment();

const diffInSeconds = now.diff(date, 'seconds');
const diffInMinutes = now.diff(date, 'minutes');

if (diffInSeconds < 60) {
return 'now';
} else if (diffInMinutes < 60) {
return `${diffInMinutes} minutes ago`;
} else {
return date.format('hh:mm A');
}
}
222 changes: 222 additions & 0 deletions src/common/components/InAppChatNotifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { FC, useContext } from 'react';

import { useNavigate } from 'react-router-dom';
import { css } from 'styled-components';

import { Box, Cross, Pin, Text, Image, FillCircle, ChatFilled, EditProfile } from 'blocks';

import { AppContextType } from 'types/context';
import { AppContext } from 'contexts/AppContext';

import { convertTimeStamp } from 'common/Common.utils';
import { shortenText } from 'helpers/UtilityHelper';
import { caip10ToWallet } from 'helpers/w2w';

import { useResolveWeb3Name } from 'hooks/useResolveWeb3Name';
import { useGetGroupInfo, useGetUserProfileDetails } from 'queries';

type InAppChatNotificationsProps = {
chatDetails: Array<any>;
onClose: () => void;
};

const getContentText = (chatDetail: any) => {
if (chatDetail.message.type === 'Text') return chatDetail.message.content;
if (chatDetail.message.type === 'Image') return 'Image';
if (chatDetail.message.type === 'File') return 'File';
if (chatDetail.message.type === 'MediaEmbed' || chatDetail.message.type === 'GIF') return 'GIF';
};
const getContentImage = (chatDetail: any) => {
if (
chatDetail.message.type === 'Image' ||
chatDetail.message.type === 'MediaEmbed' ||
chatDetail.message.type === 'GIF'
)
return (
<Image
size={16}
color="icon-tertiary"
/>
);
if (chatDetail.message.type === 'File')
return (
<Pin
size={16}
color="icon-tertiary"
/>
);
};

const InAppChatNotifications: FC<InAppChatNotificationsProps> = ({ chatDetails, onClose }) => {
const { web3NameList }: AppContextType = useContext(AppContext)!;
const fromAddress = caip10ToWallet(chatDetails[0]?.from);
const { data: userProfileDetails } = useGetUserProfileDetails(fromAddress, {
refetchOnWindowFocus: false,
staleTime: Infinity,
refetchInterval: 3600000, // 1 hour,
});
const { data: groupInfo } = useGetGroupInfo(chatDetails[0]?.meta?.group ? chatDetails[0].chatId : '', {
refetchOnWindowFocus: false,
staleTime: Infinity,
refetchInterval: 3600000, // 1 hour,
});

const navigate = useNavigate();

useResolveWeb3Name(fromAddress);
const web3Name = web3NameList[fromAddress];
const sender = web3Name ? web3Name : shortenText(fromAddress, 6);
const displayName = chatDetails[0]?.meta?.group
? groupInfo?.groupName || shortenText(chatDetails[0]?.chatId, 6)
: web3Name || shortenText(fromAddress, 6);

const latestTimestamp = convertTimeStamp(chatDetails[chatDetails.length - 1]?.timestamp);

//optimise it and fix the close button z-index
return (
<Box
width="397px"
display="flex"
>
{chatDetails && userProfileDetails && (
<Box
padding="spacing-sm"
display="flex"
borderRadius="radius-sm"
flexDirection="column"
gap="spacing-xxs"
border="border-sm solid stroke-tertiary"
backgroundColor="surface-primary"
width="inherit"
cursor="pointer"
onClick={() => navigate(`/chat/chatid:${chatDetails[0].chatId}`)}
>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box
display="flex"
gap="spacing-xxs"
alignItems="center"
>
<Box
width="24px"
height="24px"
overflow="hidden"
borderRadius="radius-round"
css={css`
flex-shrink: 0;
`}
>
{chatDetails[0].event === 'chat.request' ? (
<ChatFilled
size={24}
color="icon-brand-medium"
/>
) : (
<img
width="100%"
height="100%"
src={userProfileDetails?.picture || groupInfo?.groupImage || ''}
alt={displayName}
/>
)}
</Box>
<Text
color="text-primary"
variant="bes-semibold"
>
{chatDetails[0].event === 'chat.request' ? 'Push Chat' : displayName}
</Text>
<FillCircle
color="icon-tertiary"
size={4}
/>
<Text
color="text-tertiary"
variant="c-semibold"
>
{latestTimestamp}
</Text>
</Box>
<Box
onClick={(e) => {
e.stopPropagation();
onClose();
}}
cursor="pointer"
>
<Cross
color="icon-primary"
size={16}
/>
</Box>
</Box>
{chatDetails.map((chatDetail: any) =>
chatDetail.event === 'chat.request' ? (
<Box
display="flex"
gap="spacing-xxxs"
alignItems="center"
>
<EditProfile
size={16}
color="icon-tertiary"
/>
<Box>
<Text
color="text-primary"
variant="bes-bold"
as="span"
>
{displayName}{' '}
</Text>
<Text
color="text-secondary"
variant="bes-regular"
as="span"
>
has sent you a chat request
</Text>
</Box>
</Box>
) : (
<Box
display="flex"
flexDirection="column"
>
<Box
display="flex"
gap="spacing-xxxs"
alignItems="center"
>
{chatDetails[0]?.meta?.group && (
<Text
color="text-primary"
variant="bes-bold"
as="span"
>
{sender}{' '}
</Text>
)}
{chatDetail.message.type !== 'Text' ? <Box>{getContentImage(chatDetail)}</Box> : null}
<Text
color="text-secondary"
variant="bes-regular"
numberOfLines={2}
>
{getContentText(chatDetail)}
</Text>
</Box>
</Box>
)
)}
</Box>
)}
</Box>
);
};

export { InAppChatNotifications };
1 change: 1 addition & 0 deletions src/common/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './TokenFaucet';
export * from './CopyButton';
export * from './VerifiedChannelTooltipContent';
export * from './InAppChannelNotifications';
export * from './InAppChatNotifications';
Loading
Loading