diff --git a/packages/uiweb/package.json b/packages/uiweb/package.json index 86bd7905c..07799c86d 100644 --- a/packages/uiweb/package.json +++ b/packages/uiweb/package.json @@ -16,6 +16,8 @@ "@web3-onboard/react": "^2.8.9", "@web3-onboard/walletconnect": "^2.4.6", "@web3-react/injected-connector": "^6.0.7", + "animejs": "^2.2.0", + "classnames": "^2.2.5", "date-fns": "^2.28.0", "emoji-picker-react": "^4.4.9", "ethers": "^5.6.8", @@ -23,12 +25,13 @@ "html-react-parser": "^1.4.13", "livekit-client": "^1.13.3", "moment": "^2.29.4", + "openpgp": "^5.11.1", + "protobufjs": "^7.2.6", + "raf": "^3.4.0", "react-easy-crop": "^4.1.4", "react-icons": "^4.10.1", "react-image-file-resizer": "^0.4.7", - "animejs": "^2.2.0", - "classnames": "^2.2.5", - "raf": "^3.4.0", + "react-player": "^2.16.0", "react-toastify": "^9.1.3", "react-twitter-embed": "^4.0.4", "uuid": "^9.0.1" diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index 49a4afe39..ab9cbb9f2 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -1,20 +1,34 @@ -import { ReactElement, ReactNode, useContext, useEffect, useState } from 'react'; +import { ReactNode, useContext, useEffect, useRef, useState } from 'react'; import moment from 'moment'; +import { MdDownload } from 'react-icons/md'; import { TwitterTweetEmbed } from 'react-twitter-embed'; import styled from 'styled-components'; -import { MdDownload } from 'react-icons/md'; import { ChatDataContext } from '../../../context'; import { useChatData } from '../../../hooks'; -import { Image, Section, Span } from '../../reusables'; +import { Div, Image, Section, Span } from '../../reusables'; import { checkTwitterUrl } from '../helpers/twitter'; import { ThemeContext } from '../theme/ThemeProvider'; -import { FILE_ICON } from '../../../config'; -import { formatFileSize, getPfp, pCAIP10ToWallet, shortenText } from '../../../helpers'; -import { FileMessageContent } from '../../../types'; +import { useConnectWallet, useSetChain } from '@web3-onboard/react'; +import { ethers } from 'ethers'; +import { BsLightning } from 'react-icons/bs'; +import { FaBell, FaLink, FaRegThumbsUp } from 'react-icons/fa'; +import { MdError, MdOpenInNew } from 'react-icons/md'; +import { FILE_ICON, allowedNetworks, device } from '../../../config'; +import { formatFileSize, getPfp, pCAIP10ToWallet, shortenText, sign, toSerialisedHexString } from '../../../helpers'; +import { createBlockie } from '../../../helpers/blockies'; +import { FileMessageContent, FrameDetails, IFrame, IFrameButton } from '../../../types'; +import { extractWebLink, getFormattedMetadata, hasWebLink } from '../../../utilities'; import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; +import { Button, TextInput } from '../reusables'; + +import { FileCard } from './cards/file/FileCard'; +import { GIFCard } from './cards/gif/GIFCard'; +import { ImageCard } from './cards/image/ImageCard'; +import { MessageCard } from './cards/message/MessageCard'; +import { TwitterCard } from './cards/twitter/TwitterCard'; const SenderMessageAddress = ({ chat }: { chat: IMessagePayload }) => { const { user } = useContext(ChatDataContext); @@ -36,17 +50,53 @@ const SenderMessageAddress = ({ chat }: { chat: IMessagePayload }) => { const SenderMessageProfilePicture = ({ chat }: { chat: IMessagePayload }) => { const { user } = useContext(ChatDataContext); - const [pfp, setPfp] = useState(''); - const getUserPfp = async () => { - const pfp = await getPfp({ - user: user, - recipient: chat.fromCAIP10?.split(':')[1], - }); - if (pfp) { - setPfp(pfp); + const [chatPic, setChatPic] = useState({ + pfpsrc: null as string | null, + blockie: null as string | null, + }); + + // For blockie if icon is missing + const blockieContainerRef = useRef(null); + + useEffect(() => { + if (blockieContainerRef.current && chatPic.blockie && chatPic.pfpsrc === null) { + const blockie = createBlockie(chatPic.blockie || '', { size: 8, scale: 5 }); + blockieContainerRef.current.innerHTML = ''; // Clear the container to avoid duplicating the canvas + blockieContainerRef.current.appendChild(blockie); } - }; + }, [chatPic.blockie]); + useEffect(() => { + const getUserPfp = async () => { + try { + const pfp = await getPfp({ + user: user, + recipient: chat.fromCAIP10?.split(':')[1], + }); + + if (pfp) { + setChatPic({ + pfpsrc: pfp, + blockie: null, + }); + } else { + setChatPic({ + pfpsrc: null, + blockie: chat.fromCAIP10?.split(':')[1], + }); + } + } catch (error) { + console.error('UIWeb::components::chat::ChatViewBubble::SenderMessageProfilePicture::getUserPfp error', error); + + // fallback to blockie + setChatPic({ + pfpsrc: null, + blockie: chat.fromCAIP10?.split(':')[1], + }); + } + }; + + // resolve user pfp getUserPfp(); }, [chat.fromCAIP10]); @@ -55,17 +105,30 @@ const SenderMessageProfilePicture = ({ chat }: { chat: IMessagePayload }) => { justifyContent="start" alignItems="start" > - {chat.fromCAIP10 !== user?.account && ( -
- {pfp && ( + {chat.fromCAIP10?.split(':')[1] !== user?.account && ( +
+ {chatPic.pfpsrc && ( profile picture )} + + {!chatPic.pfpsrc && chatPic.blockie && ( +
+ )}
)}
@@ -76,23 +139,20 @@ const MessageWrapper = ({ chat, children, isGroup, - maxWidth, }: { chat: IMessagePayload; children: ReactNode; isGroup: boolean; - maxWidth?: string; }) => { const { user } = useChatData(); const theme = useContext(ThemeContext); return ( -
{isGroup && chat?.fromCAIP10 !== user?.account && }
} {children}
-
- ); -}; - -const MessageCard = ({ chat, position, isGroup }: { chat: IMessagePayload; position: number; isGroup: boolean }) => { - const theme = useContext(ThemeContext); - const time = moment(chat.timestamp).format('hh:mm a'); - return ( - - - {' '} -
- {chat?.messageContent?.split('\n').map((str) => ( - - {str} - - ))} -
- - {time} - -
-
- ); -}; - -const FileCard = ({ chat, isGroup }: { chat: IMessagePayload; position: number; isGroup: boolean }) => { - const fileContent: FileMessageContent = JSON.parse(chat?.messageContent); - const name = fileContent.name; - - const content = fileContent.content as string; - const size = fileContent.size; - - return ( - -
- extension icon -
- - {shortenText(name, 11)} - - - {formatFileSize(size)} - -
- - - -
-
- ); -}; - -const ImageCard = ({ chat, position, isGroup }: { chat: IMessagePayload; position: number; isGroup: boolean }) => { - return ( - -
- -
-
- ); -}; - -const GIFCard = ({ chat, position, isGroup }: { chat: IMessagePayload; position: number; isGroup: boolean }) => { - return ( - -
- -
-
- ); -}; - -const TwitterCard = ({ - chat, - tweetId, - isGroup, - position, -}: { - chat: IMessagePayload; - tweetId: string; - isGroup: boolean; - position: number; -}) => { - return ( - -
- -
-
+ ); }; export const ChatViewBubble = ({ decryptedMessagePayload, - isGroup = false, + isGroup, }: { decryptedMessagePayload: IMessagePayload; - isGroup?: boolean; + isGroup: boolean; }) => { const { user } = useChatData(); const position = @@ -333,55 +185,79 @@ export const ChatViewBubble = ({ decryptedMessagePayload.messageType = 'TwitterFeedLink'; } - if (decryptedMessagePayload.messageType === 'GIF') { - return ( - - ); - } - if (decryptedMessagePayload.messageType === 'Image') { - return ( - - ); - } - if (decryptedMessagePayload.messageType === 'File') { - return ( - - ); - } - if (decryptedMessagePayload.messageType === 'TwitterFeedLink') { - return ( - - ); - } return ( - + isGroup={isGroup} + > + {/* Message Card */} + {decryptedMessagePayload.messageType === 'Text' && ( + + )} + + {/* Image Card */} + {decryptedMessagePayload.messageType === 'Image' && ( + + )} + + {/* File Card */} + {decryptedMessagePayload.messageType === 'File' && ( + + )} + + {/* Gif Card */} + {decryptedMessagePayload.messageType === 'GIF' && ( + + )} + + {/* Twitter Card */} + {decryptedMessagePayload.messageType === 'TwitterFeedLink' && ( + + )} + + {/* Default Message Card */} + {decryptedMessagePayload.messageType !== 'Text' && + decryptedMessagePayload.messageType !== 'Image' && + decryptedMessagePayload.messageType !== 'File' && + decryptedMessagePayload.messageType !== 'GIF' && + decryptedMessagePayload.messageType !== 'TwitterFeedLink' && ( + + )} + ); }; -const FileDownloadIconAnchor = styled.a` - font-size: 20px; -`; -const MessageSection = styled(Section)<{ border: string }>` - border: ${(props) => props.border}; +const MessageSection = styled(Section)` + max-width: 70%; + + @media ${device.tablet} { + max-width: 90%; + } `; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx new file mode 100644 index 000000000..4a452cb80 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx @@ -0,0 +1,88 @@ +// React + Web3 Essentials + +// External Packages +import styled from 'styled-components'; + +// Internal Compoonents +import { + formatFileSize, + getPfp, + pCAIP10ToWallet, + shortenText, + sign, + toSerialisedHexString, +} from '../../../../../helpers'; +import { Image, Section, Span } from '../../../../reusables'; + +// Internal Configs +import { FILE_ICON, allowedNetworks } from '../../../../../config'; + +// Assets +import { MdDownload } from 'react-icons/md'; + +// Interfaces & Types +import { FileMessageContent, FrameDetails, IFrame, IFrameButton } from '../../../../../types'; +import { IMessagePayload } from '../../../exportedTypes'; + +// Constants + +// Exported Interfaces & Types + +// Exported Functions +export const FileCard = ({ chat, isGroup }: { chat: IMessagePayload; position: number; isGroup: boolean }) => { + const fileContent: FileMessageContent = JSON.parse(chat?.messageContent); + const name = fileContent.name; + + const content = fileContent.content as string; + const size = fileContent.size; + + return ( +
+ extension icon +
+ + {shortenText(name, 11)} + + + {formatFileSize(size)} + +
+ + + +
+ ); +}; + +const FileDownloadIconAnchor = styled.a` + font-size: 20px; +`; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/gif/GIFCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/gif/GIFCard.tsx new file mode 100644 index 000000000..ba671b577 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/gif/GIFCard.tsx @@ -0,0 +1,36 @@ +// React + Web3 Essentials + +// External Packages + +// Internal Compoonents +import { Image, Section, Span } from '../../../../reusables'; + +// Internal Configs + +// Assets + +// Interfaces & Types +import { IMessagePayload } from '../../../exportedTypes'; + +// Constants + +// Exported Interfaces & Types + +// Exported Functions +export const GIFCard = ({ chat, position, isGroup }: { chat: IMessagePayload; position: number; isGroup: boolean }) => { + return ( +
+ +
+ ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx new file mode 100644 index 000000000..12068eead --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx @@ -0,0 +1,45 @@ +// React + Web3 Essentials + +// External Packages + +// Internal Compoonents +import { Image, Section } from '../../../../reusables'; + +// Internal Configs + +// Assets + +// Interfaces & Types +import { IMessagePayload } from '../../../exportedTypes'; + +// Constants + +// Exported Interfaces & Types + +// Exported Functions + +export const ImageCard = ({ + chat, + position, + isGroup, +}: { + chat: IMessagePayload; + position: number; + isGroup: boolean; +}) => { + return ( +
+ +
+ ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/FrameRenderer.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/FrameRenderer.tsx new file mode 100644 index 000000000..a73d28ee3 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/FrameRenderer.tsx @@ -0,0 +1,526 @@ +// React + Web3 Essentials +import { useContext, useState } from 'react'; + +// External Packages +import { useConnectWallet, useSetChain } from '@web3-onboard/react'; +import { ethers } from 'ethers'; +import styled from 'styled-components'; + +// Internal Compoonents +import { FILE_ICON, allowedNetworks } from '../../../../../config'; +import { + formatFileSize, + getPfp, + pCAIP10ToWallet, + shortenText, + sign, + toSerialisedHexString, +} from '../../../../../helpers'; +import { useChatData } from '../../../../../hooks'; +import { extractWebLink, getFormattedMetadata, hasWebLink } from '../../../../../utilities'; +import { Anchor, Button, Image, Section, Span } from '../../../../reusables'; +import { TextInput } from '../../../reusables'; +import useToast from '../../../reusables/NewToast'; +import { ThemeContext } from '../../../theme/ThemeProvider'; + +// Internal Configs + +// Assets +import { BsLightning } from 'react-icons/bs'; +import { FaBell, FaLink, FaRegThumbsUp } from 'react-icons/fa'; +import { MdError, MdOpenInNew } from 'react-icons/md'; + +// Interfaces & Types +import { IFrame, IFrameButton } from '../../../../../types'; +import { IChatTheme } from '../../../exportedTypes'; +import { getAddress, toHex } from 'viem'; + +interface FrameInputProps extends React.InputHTMLAttributes { + theme: IChatTheme; +} + +// Constants + +// Exported Interfaces & Types + +// Exported Functions +export const FrameRenderer = ({ + url, + account, + messageId, + frameData, + proxyServer, +}: { + url: string; + account: string; + messageId: string; + frameData: IFrame; + proxyServer: string; +}) => { + const { env, user, pgpPrivateKey } = useChatData(); + + const [{ wallet }] = useConnectWallet(); + const [{ connectedChain }, setChain] = useSetChain(); + + const frameRenderer = useToast(); + const [FrameData, setFrameData] = useState