diff --git a/packages/uiweb/package.json b/packages/uiweb/package.json index 2ccb5f19b..a87ba1a70 100644 --- a/packages/uiweb/package.json +++ b/packages/uiweb/package.json @@ -10,7 +10,7 @@ "@livepeer/react": "^2.6.0", "@pushprotocol/socket": "^0.5.0", "@unstoppabledomains/resolution": "^8.5.0", - "@web3-name-sdk/core": "^0.1.15", + "@web3-name-sdk/core": "^0.2.0", "@web3-onboard/coinbase": "^2.2.5", "@web3-onboard/core": "^2.21.1", "@web3-onboard/injected-wallets": "^2.10.5", diff --git a/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx b/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx index 6acb80fee..721b36653 100644 --- a/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatPreview/ChatPreview.tsx @@ -3,21 +3,23 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { useChatData } from '../../../hooks'; -import { Div, Button, Image, Section } from '../../reusables'; +import { Button, Div, Image, Section } from '../../reusables'; import { CONSTANTS } from '@pushprotocol/restapi'; import { ethers } from 'ethers'; import { CiImageOn } from 'react-icons/ci'; import { FaFile } from 'react-icons/fa'; import { CoreContractChainId, InfuraAPIKey } from '../../../config'; -import { resolveWeb3Name, shortenText } from '../../../helpers'; +import { pushBotAddress } from '../../../config/constants'; +import { pCAIP10ToWallet, resolveWeb3Name, shortenText } from '../../../helpers'; +import { createBlockie } from '../../../helpers/blockies'; import { IChatPreviewProps } from '../exportedTypes'; import { formatAddress, formatDate } from '../helpers'; -import { pCAIP10ToWallet } from '../../../helpers'; -import { createBlockie } from '../../../helpers/blockies'; import { IChatTheme } from '../theme'; import { ThemeContext } from '../theme/ThemeProvider'; -import { pushBotAddress } from '../../../config/constants'; + +import { ReplyIcon } from '../../../icons/PushIcons'; + /** * @interface IThemeProps * this interface is used for defining the props for styled components @@ -53,7 +55,9 @@ export const ChatPreview: React.FC = (options: IChatPreviewPr const hasBadgeCount = !!options?.badge?.count; const isSelected = options?.selected; - const isBot = options?.chatPreviewPayload?.chatParticipant === "PushBot" || options?.chatPreviewPayload?.chatParticipant === pushBotAddress; + const isBot = + options?.chatPreviewPayload?.chatParticipant === 'PushBot' || + options?.chatPreviewPayload?.chatParticipant === pushBotAddress; // For blockie if icon is missing const blockieContainerRef = useRef(null); @@ -75,6 +79,49 @@ export const ChatPreview: React.FC = (options: IChatPreviewPr return options.chatPreviewPayload?.chatGroup ? formattedAddress : web3Name ? web3Name : formattedAddress; }; + // collate all message components + const msgComponents: React.ReactNode[] = []; + let includeText = false; + + // If reply, check message meta to see + // Always check this first + if (options?.chatPreviewPayload?.chatMsg?.messageMeta === 'Reply') { + msgComponents.push( + + ); + + // Include text in rendering as well + includeText = true; + } + + // If image, gif, mediaembed + if ( + options?.chatPreviewPayload?.chatMsg?.messageType === 'Image' || + options?.chatPreviewPayload?.chatMsg?.messageType === 'GIF' || + options?.chatPreviewPayload?.chatMsg?.messageType === 'MediaEmbed' + ) { + msgComponents.push(); + msgComponents.push(Media); + } + + // If file + if (options?.chatPreviewPayload?.chatMsg?.messageType === 'File') { + msgComponents.push(); + msgComponents.push(File); + } + + // Add content + if ( + includeText || + options?.chatPreviewPayload?.chatMsg?.messageType === 'Text' || + options?.chatPreviewPayload?.chatMsg?.messageType === 'Reaction' + ) { + msgComponents.push({options?.chatPreviewPayload?.chatMsg?.messageContent}); + } + return ( = (options: IChatPreviewPr animation={theme.skeletonBG} > - {options?.chatPreviewPayload?.chatMsg?.messageType === 'Image' || - options?.chatPreviewPayload?.chatMsg?.messageType === 'GIF' || - options?.chatPreviewPayload?.chatMsg?.messageType === 'MediaEmbed' ? ( -
- - Media -
- ) : options?.chatPreviewPayload?.chatMsg?.messageType === 'File' ? ( -
- - File -
- ) : ( - options?.chatPreviewPayload?.chatMsg?.messageContent - )} +
+ {msgComponents} +
- - {hasBadgeCount && !(isBot || (isSelected && hasBadgeCount)) && {options.badge?.count}} - + + {hasBadgeCount && !(isBot || (isSelected && hasBadgeCount)) && ( + {options.badge?.count} + )} +
diff --git a/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx b/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx index a31fe7d39..a5b02ebe5 100644 --- a/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx @@ -261,7 +261,7 @@ export const ChatPreviewList: React.FC = (options: IChatP items.forEach((item) => { // only increment if not selected if (chatPreviewListMeta.selectedChatId !== item.chatId) { - console.debug('::ChatPreviewList::incrementing badge', item); + console.debug('UIWeb::ChatPreviewList::incrementing badge', item); setBadge( item.chatId!, chatPreviewListMeta.badges[item.chatId!] ? chatPreviewListMeta.badges[item.chatId!] + 1 : 1 @@ -297,6 +297,7 @@ export const ChatPreviewList: React.FC = (options: IChatP chatGroup: true, chatTimestamp: undefined, chatMsg: { + messageMeta: '', messageType: '', messageContent: '', }, @@ -360,71 +361,6 @@ export const ChatPreviewList: React.FC = (options: IChatP return { type, overrideAccount }; }; - // //Initialise chat -- Deprecated - // const initializeChatList = async () => { - // // Load chat type from options, if not present, default to CHATS - // const { type, overrideAccount } = getTypeAndAccount(); - // const newpage = 1; - - // // store current nonce and page - // const currentNonce = chatPreviewList.nonce; - // if (type === 'SEARCH') { - // await handleSearch(currentNonce); - // } else { - // const chatList = await fetchChatList({ - // type, - // page: newpage, - // limit: CHAT_PAGE_LIMIT, - // overrideAccount, - // }); - // if (chatList) { - // // get and transform chats - // const transformedChats = transformChatItems(chatList); - - // // return if nonce doesn't match or if page is not 1 - // if (currentNonce !== chatPreviewList.nonce || chatPreviewList.page !== 0) { - // return; - // } - - // setChatPreviewList((prev) => ({ - // nonce: generateRandomNonce(), - // items: transformedChats, - // page: 1, - // loading: false, - // loaded: false, - // reset: false, - // resume: false, - // errored: false, - // error: null, - // })); - - // if (options?.onPreload) { - // options.onPreload(transformedChats); - // } - // } else { - // // return if nonce doesn't match - // if (currentNonce !== chatPreviewList.nonce) { - // return; - // } - - // setChatPreviewList({ - // nonce: generateRandomNonce(), - // items: [], - // page: 0, - // loading: false, - // loaded: false, - // reset: false, - // resume: false, - // errored: true, - // error: { - // code: ChatPreviewListErrorCodes.CHAT_PREVIEW_LIST_PRELOAD_ERROR, - // message: 'No chats found', - // }, - // }); - // } - // } - // }; - // Define Chat Preview List Meta Functions // Set selected badge const setSelectedBadge: (chatId: string, chatParticipant: string) => void = ( @@ -594,154 +530,6 @@ export const ChatPreviewList: React.FC = (options: IChatP } }, [chatRejectStream]); - //search method for a chatId - const handleSearch = async (currentNonce: string) => { - let error; - let searchedChat: IChatPreviewPayload = { - chatId: undefined, - chatPic: null, - chatParticipant: '', - chatGroup: false, - chatTimestamp: undefined, - chatMsg: { - messageType: '', - messageContent: '', - }, - }; - //check if searchParamter is there - try { - if (options?.searchParamter) - if (options?.searchParamter) { - let formattedChatId: string | null = options?.searchParamter; - let userProfile: IUser | undefined = undefined; - let groupProfile: Group; - - if (getDomainIfExists(formattedChatId)) { - const address = await getAddress(formattedChatId, user ? user.env : CONSTANTS.ENV.PROD); - if (address) formattedChatId = pCAIP10ToWallet(address); - else { - error = { - code: ChatPreviewListErrorCodes.CHAT_PREVIEW_LIST_INVALID_SEARCH_ERROR, - message: 'Invalid search', - }; - } - } - if (pCAIP10ToWallet(formattedChatId) === pCAIP10ToWallet(user?.account || '')) { - error = { - code: ChatPreviewListErrorCodes.CHAT_PREVIEW_LIST_INVALID_SEARCH_ERROR, - message: 'Invalid search', - }; - } - - if (!error) { - const chatInfo = await fetchChat({ chatId: formattedChatId }); - if (chatInfo && chatInfo?.meta?.group) - groupProfile = await getGroupByIDnew({ - groupId: formattedChatId, - }); - else if (user?.account) - formattedChatId = pCAIP10ToWallet( - chatInfo?.participants.find((address) => address != walletToPCAIP10(user?.account)) || formattedChatId - ); - //fetch profile - if (!groupProfile) { - userProfile = await getNewChatUser({ - searchText: formattedChatId, - env: user?.env ? user?.env : CONSTANTS.ENV.PROD, - fetchChatProfile: fetchUserProfile, - user, - }); - } - - if (!userProfile && !groupProfile) { - error = { - code: ChatPreviewListErrorCodes.CHAT_PREVIEW_LIST_INVALID_SEARCH_ERROR, - message: 'Invalid search', - }; - } else { - searchedChat = { - ...searchedChat, - chatId: chatInfo?.chatId || formattedChatId, - chatGroup: !!groupProfile, - chatPic: (userProfile?.profile?.picture ?? groupProfile?.groupImage) || null, - chatParticipant: groupProfile ? groupProfile?.groupName : formattedChatId!, - }; - //fetch latest chat - const latestMessage = await fetchLatestMessage({ - chatId: formattedChatId, - }); - if (latestMessage) { - searchedChat = { - ...searchedChat, - chatMsg: { - messageType: latestMessage[0]?.messageType, - messageContent: latestMessage[0]?.messageContent, - }, - chatTimestamp: latestMessage[0]?.timestamp, - }; - } - - // return if nonce doesn't match or if page is not 1 - if (currentNonce !== chatPreviewList.nonce || chatPreviewList.page !== 1) { - return; - } - setChatPreviewList((prev) => ({ - nonce: generateRandomNonce(), - items: [...[searchedChat]], - page: 1, - loading: false, - loaded: false, - reset: false, - resume: false, - errored: false, - error: null, - })); - } - } - } else { - error = { - code: ChatPreviewListErrorCodes.CHAT_PREVIEW_LIST_INSUFFICIENT_INPUT, - message: 'Insufficient input for search', - }; - } - if (error) { - setChatPreviewList({ - nonce: generateRandomNonce(), - items: [], - page: 1, - loading: false, - loaded: false, - reset: false, - resume: false, - errored: true, - error: error, - }); - } - } catch (e) { - // return if nonce doesn't match - console.debug(e); - console.debug(`Errored: currentNonce: ${currentNonce}, chatPreviewList.nonce: ${chatPreviewList.nonce}`); - if (currentNonce !== chatPreviewList.nonce) { - return; - } - - setChatPreviewList({ - nonce: generateRandomNonce(), - items: [], - page: 1, - loading: false, - loaded: false, - reset: false, - resume: false, - errored: true, - error: { - code: ChatPreviewListErrorCodes.CHAT_PREVIEW_LIST_PRELOAD_ERROR, - message: 'Error in searching', - }, - }); - } - }; - // Attach scroll listener const onScroll = async () => { const element = listInnerRef.current; diff --git a/packages/uiweb/src/lib/components/chat/ChatPreviewSearchList/ChatPreviewSearchList.tsx b/packages/uiweb/src/lib/components/chat/ChatPreviewSearchList/ChatPreviewSearchList.tsx index a26083bd8..717d31f5a 100644 --- a/packages/uiweb/src/lib/components/chat/ChatPreviewSearchList/ChatPreviewSearchList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatPreviewSearchList/ChatPreviewSearchList.tsx @@ -21,9 +21,9 @@ import { ThemeContext } from '../theme/ThemeProvider'; // Interfaces & Types import { ChatPreviewSearchListErrorCodes, + IChatPreviewPayload, IChatPreviewSearchListError, IChatPreviewSearchListProps, - IChatPreviewPayload, } from '../exportedTypes'; import { IChatTheme } from '../theme'; @@ -155,6 +155,7 @@ export const ChatPreviewSearchList: React.FC = (opt chatGroup: false, chatTimestamp: undefined, chatMsg: { + messageMeta: '', messageType: '', messageContent: '', }, @@ -199,6 +200,7 @@ export const ChatPreviewSearchList: React.FC = (opt chatGroup: true, chatPic: groupInfo?.groupImage || null, chatMsg: { + messageMeta: 'Text', messageType: 'Text', messageContent: chatInfo?.list === 'CHATS' ? 'Resume Conversation!' : 'Join Group!', }, @@ -216,6 +218,7 @@ export const ChatPreviewSearchList: React.FC = (opt chatGroup: false, chatPic: userProfile?.profile?.picture || null, chatMsg: { + messageMeta: 'Text', messageType: 'Text', messageContent: chatInfo?.list === 'CHATS' ? 'Resume Chat!' : 'Start Chat!', }, diff --git a/packages/uiweb/src/lib/components/chat/ChatView/ChatViewComponent.tsx b/packages/uiweb/src/lib/components/chat/ChatView/ChatViewComponent.tsx index 615e6fd51..cecdf7ebc 100644 --- a/packages/uiweb/src/lib/components/chat/ChatView/ChatViewComponent.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatView/ChatViewComponent.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { MODAL_BACKGROUND_TYPE, MODAL_POSITION_TYPE } from '../../../types'; -import { IChatTheme, IChatViewComponentProps } from '../exportedTypes'; +import { IChatTheme, IChatViewComponentProps, IMessagePayload } from '../exportedTypes'; import { chatLimit, device } from '../../../config'; import { deriveChatId } from '../../../helpers'; @@ -33,6 +33,7 @@ export const ChatViewComponent: React.FC = (options: IC emoji = true, file = true, gif = true, + handleReply = true, isConnected = true, autoConnect = false, onVerificationFail, @@ -43,7 +44,7 @@ export const ChatViewComponent: React.FC = (options: IC chatProfileRightHelperComponent = null, chatProfileLeftHelperComponent = null, welcomeComponent = null, - closeChatProfileInfoModalOnClickAway = false + closeChatProfileInfoModalOnClickAway = false, } = options || {}; const { user } = useChatData(); @@ -63,6 +64,8 @@ export const ChatViewComponent: React.FC = (options: IC derivedChatId: '', }); + const [replyPayload, setReplyPayload] = useState(null); + useEffect(() => { const fetchDerivedChatId = async () => { setInitialized((currentState) => ({ ...currentState, loading: true })); @@ -137,6 +140,7 @@ export const ChatViewComponent: React.FC = (options: IC chatFilterList={chatFilterList} limit={limit} chatId={initialized.derivedChatId} + setReplyPayload={setReplyPayload} /> )} @@ -156,6 +160,8 @@ export const ChatViewComponent: React.FC = (options: IC file={file} emoji={emoji} gif={gif} + replyPayload={handleReply ? replyPayload : null} + setReplyPayload={setReplyPayload} isConnected={isConnected} verificationFailModalBackground={verificationFailModalBackground} verificationFailModalPosition={verificationFailModalPosition} @@ -172,12 +178,12 @@ export const ChatViewComponent: React.FC = (options: IC }; //styles -const Conatiner = styled(Section) ` +const Conatiner = styled(Section)` border: ${(props) => props.theme.border?.chatViewComponent}; box-sizing: border-box; `; -const ChatViewSection = styled(Section) ` +const ChatViewSection = styled(Section)` @media (${device.mobileL}) { margin: 0; } diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index c3770fde1..5b39b6745 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -7,8 +7,8 @@ import styled from 'styled-components'; import { ChatDataContext } from '../../../context'; import { useChatData } from '../../../hooks'; +import { ReplyIcon } from '../../../icons/PushIcons'; import { Div, Image, Section, Span } from '../../reusables'; -import { checkTwitterUrl } from '../helpers/twitter'; import { ThemeContext } from '../theme/ThemeProvider'; import { useConnectWallet, useSetChain } from '@web3-onboard/react'; @@ -20,11 +20,11 @@ import { FILE_ICON, allowedNetworks, device } from '../../../config'; import { formatFileSize, getPfp, + isMessageEncrypted, pCAIP10ToWallet, shortenText, sign, toSerialisedHexString, - isMessageEncrypted, } from '../../../helpers'; import { createBlockie } from '../../../helpers/blockies'; import { FileMessageContent, FrameDetails, IFrame, IFrameButton, IReactionsForChatMessages } from '../../../types'; @@ -32,14 +32,12 @@ import { extractWebLink, getFormattedMetadata, hasWebLink } from '../../../utili 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'; +import { Button as RButton } from '../../reusables'; + +import { ChatViewBubbleCore } from '../ChatViewBubbleCore'; -import { Reactions } from './reactions/Reactions'; import { ReactionPicker } from './reactions/ReactionPicker'; +import { Reactions } from './reactions/Reactions'; const SenderMessageAddress = ({ chat }: { chat: IMessagePayload }) => { const { user } = useContext(ChatDataContext); @@ -188,6 +186,7 @@ export const ChatViewBubble = ({ decryptedMessagePayload, chatPayload: payload, chatReactions, + setReplyPayload, showChatMeta = false, chatId, actionId, @@ -197,6 +196,7 @@ export const ChatViewBubble = ({ decryptedMessagePayload: IMessagePayload; chatPayload?: IMessagePayload; chatReactions?: any; + setReplyPayload?: (payload: IMessagePayload) => void; showChatMeta?: boolean; chatId?: string; actionId?: string | null | undefined; @@ -220,26 +220,6 @@ export const ChatViewBubble = ({ const chatPosition = pCAIP10ToWallet(chatPayload.fromDID).toLowerCase() !== pCAIP10ToWallet(user?.account ?? '')?.toLowerCase() ? 0 : 1; - // derive message - const message = - typeof chatPayload.messageObj === 'object' - ? (chatPayload.messageObj?.content as string) ?? '' - : (chatPayload.messageObj as string); - - // check and render tweets - const { tweetId, messageType }: TwitterFeedReturnType = checkTwitterUrl({ - message: message, - }); - - if (messageType === 'TwitterFeedLink') { - chatPayload.messageType = 'TwitterFeedLink'; - } - - // test if the payload is encrypted, if so convert it to text - if (isMessageEncrypted(message)) { - chatPayload.messageType = 'Text'; - } - // attach a ref to chat sidebar const chatSidebarRef = useRef(null); @@ -262,6 +242,7 @@ export const ChatViewBubble = ({ {/* hide overflow for chat cards and border them */}
- {/* Message Card */} - {chatPayload.messageType === 'Text' && ( - - )} - - {/* Image Card */} - {chatPayload.messageType === 'Image' && } - - {/* File Card */} - {chatPayload.messageType === 'File' && } - - {/* Gif Card */} - {chatPayload.messageType === 'GIF' && } - - {/* Twitter Card */} - {chatPayload.messageType === 'TwitterFeedLink' && ( - - )} - - {/* Default Message Card */} - {chatPayload.messageType !== 'Text' && - chatPayload.messageType !== 'Image' && - chatPayload.messageType !== 'File' && - chatPayload.messageType !== 'GIF' && - chatPayload.messageType !== 'TwitterFeedLink' && ( - - )} +
{/* render if reactions are present */} @@ -328,9 +275,11 @@ export const ChatViewBubble = ({ + <> + {/* Reply Icon */} + { + e.stopPropagation(); + setReplyPayload?.(chatPayload); + }} + > + + + + {/* Reaction Picker */} + + )} 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 deleted file mode 100644 index e4bdb3f53..000000000 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/gif/GIFCard.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// 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 }: { chat: IMessagePayload }) => { - // derive message - const message = - typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); - - 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 deleted file mode 100644 index ad63c299f..000000000 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// 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 -const getParsedMessage = (message: string) => { - try { - return JSON.parse(message); - } catch (error) { - console.error('UIWeb::components::ChatViewBubble::ImageCard::error while parsing image', error); - return null; - } -}; - -const getImageContent = (message: string) => getParsedMessage(message)?.content ?? ''; - -export const ImageCard = ({ chat }: { chat: IMessagePayload }) => { - // derive message - const message = - typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); - - return ( -
- -
- ); -}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/ReactionPicker.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/ReactionPicker.tsx index f2defeccf..352b0fd8c 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/ReactionPicker.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/ReactionPicker.tsx @@ -16,6 +16,7 @@ import { EmojiCircleIcon } from '../../../../icons/PushIcons'; // Interfaces & Types import { IMessagePayload } from '../../exportedTypes'; +import { pCAIP10ToWallet } from '../../../../helpers'; // Constants @@ -112,8 +113,14 @@ export const ReactionPicker = ({ } }, [sendingReaction]); + const chatPosition = + pCAIP10ToWallet(chat.fromDID).toLowerCase() !== pCAIP10ToWallet(user?.account ?? '')?.toLowerCase() ? 0 : 1; + + return ( - <> +
{/* To display emoji picker */}
); }; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/Reactions.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/Reactions.tsx index 36b883948..554163c03 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/Reactions.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/Reactions.tsx @@ -1,10 +1,10 @@ // React + Web3 Essentials -import { useContext, useRef, useState, useEffect, RefObject } from 'react'; +import { RefObject, useContext, useEffect, useRef, useState } from 'react'; // External Packages // Internal Compoonents -import { Image, Section, Button, Spinner, Span } from '../../../reusables'; +import { Button, Image, Section, Span, Spinner } from '../../../reusables'; import { ThemeContext } from '../../theme/ThemeProvider'; // Internal Configs @@ -42,6 +42,9 @@ export const Reactions = ({ chatReactions }: { chatReactions: IReactionsForChatM return acc; }, {} as IReactions); + // generate a unique key for the reactions + const reactionsKey = chatReactions.map((reaction) => reaction.reference).join('-'); + console.debug('UIWeb::components::ChatViewBubble::Reactions::uniqueReactions', uniqueReactions); // render reactions @@ -50,6 +53,7 @@ export const Reactions = ({ chatReactions }: { chatReactions: IReactionsForChatM <> {Object.keys(uniqueReactions).length > 2 ? (
(
{ + // get theme + const theme = useContext(ThemeContext); + + // get user + const { user } = useChatData(); + + // extract message to perform checks + const message = + typeof chat.messageObj === 'object' + ? (typeof chat.messageObj?.content === 'string' ? chat.messageObj?.content : '') ?? '' + : (chat.messageObj as string); + + // test if the payload is encrypted, if so convert it to text + if (isMessageEncrypted(message)) { + chat.messageType = 'Text'; + } + + // get user account + const account = user?.account ?? ''; + + // deduce font color + const fontColor = + position && !activeMode ? theme.textColor?.chatSentBubbleText : theme.textColor?.chatReceivedBubbleText; + + // Render the card render + return ( + <> + {/* Message Card */} + {/* Twitter Card is handled by PreviewRenderer */} + {/* Frame Card is handled by PreviewRenderer */} + {/* Code Card is handled by CodeRenderer */} + {chat && chat.messageType === 'Text' && ( + + )} + + {/* Image Card */} + {chat.messageType === 'Image' && ( + // Background only valid when no preview or active mode + + )} + + {/* File Card */} + {chat.messageType === 'File' && ( + + )} + + {/* Gif Card */} + {chat.messageType === 'GIF' && ( + + )} + + {/* Default Message Card - Only support limited message types like Reaction */} + {chat.messageType === 'Reaction' && ( + + )} + + ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx new file mode 100644 index 000000000..aa4304368 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx @@ -0,0 +1,130 @@ +// React + Web3 Essentials +import { useContext } from 'react'; + +// External Packages +import styled from 'styled-components'; + +// Internal Components +import { useChatData } from '../../../hooks'; +import { ThemeContext } from '../theme/ThemeProvider'; + +import { deepCopy, isMessageEncrypted, pCAIP10ToWallet } from '../../../helpers'; +import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; + +import { Section } from '../../reusables'; +import { CardRenderer } from './CardRenderer'; +import { ReplyCard } from './cards/reply/ReplyCard'; + +// Internal Configs + +// Assets + +// Interfaces & Types +interface ChatViewBubbleCoreProps extends React.ComponentProps { + borderBG?: string; + previewMode?: boolean; +} + +// Exported Default Component +export const ChatViewBubbleCore = ({ + chat, + chatId, + previewMode = false, + activeMode = false, +}: { + chat: IMessagePayload; + chatId: string | undefined; + previewMode?: boolean; + activeMode?: boolean; +}) => { + // get theme + const theme = useContext(ThemeContext); + + // get user + const { user } = useChatData(); + + // get chat position + const chatPosition = + pCAIP10ToWallet(chat.fromDID).toLowerCase() !== pCAIP10ToWallet(user?.account ?? '')?.toLowerCase() ? 0 : 1; + + const renderBubble = (chat: IMessagePayload, position: number) => { + const components: JSX.Element[] = []; + + // replace derivedMsg with chat as that's the original + // take reference from derivedMsg which forms the reply + // Create a deep copy of chat + const derivedMsg = deepCopy(chat) as any; + let replyReference = ''; + + if (chat && chat.messageType === 'Reply') { + // Reply messageObj content contains messageObj and messageType; + replyReference = (chat as any).messageObj?.reference ?? null; + derivedMsg.messageType = derivedMsg?.messageObj?.content?.messageType; + derivedMsg.messageObj = derivedMsg?.messageObj?.content?.messageObj; + } + + // Render cards - Anything not a reply is ChatViewBubbleCardRenderer + // Reply is it's own card that calls ChatViewBubbleCardRenderer + // This avoids transitive recursion + + // Use replyReference to check and call reply card but only if activeMode is false + // as activeMode will be true when user is replying to a message + if (replyReference !== '' && !activeMode) { + // Add Reply Card + components.push( + + ); + } + + // Use derivedMsg to render other cards + if (derivedMsg) { + // Add Message Card + components.push( + + ); + } + + // deduce background color + // if active mode, use the normal background color as this is user replying to a message + // if preview mode, use the reply background color + // if not preview mode, use the normal background color + const background = activeMode + ? theme.backgroundColor?.chatActivePreviewBubbleBackground + : position + ? previewMode + ? theme.backgroundColor?.chatPreviewSentBubbleBackground + : theme.backgroundColor?.chatSentBubbleBackground + : previewMode + ? theme.backgroundColor?.chatPreviewRecievedBubbleBackground + : theme.backgroundColor?.chatReceivedBubbleBackground; + + return ( + + {components} + + ); + }; + + return renderBubble(chat, chatPosition); +}; + +const ChatViewBubbleCoreSection = styled(Section) ` + border-left: ${({ borderBG, previewMode }) => (previewMode ? `4px solid ${borderBG || 'transparent'}` : 'none')}; +`; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/file/FileCard.tsx similarity index 69% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/file/FileCard.tsx index b64a3f281..5d43f53dd 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/file/FileCard.tsx @@ -1,4 +1,5 @@ // React + Web3 Essentials +import { useContext } from 'react'; // External Packages import styled from 'styled-components'; @@ -13,6 +14,7 @@ import { toSerialisedHexString, } from '../../../../../helpers'; import { Image, Section, Span } from '../../../../reusables'; +import { ThemeContext } from '../../../theme/ThemeProvider'; // Internal Configs import { FILE_ICON, allowedNetworks } from '../../../../../config'; @@ -44,7 +46,22 @@ const getParsedMessage = (message: string): FileMessageContent => { } }; -export const FileCard = ({ chat }: { chat: IMessagePayload }) => { +export const FileCard = ({ + chat, + background, + color, + previewMode, + activeMode, +}: { + chat: IMessagePayload; + background?: string; + color?: string; + previewMode: boolean; + activeMode: boolean; +}) => { + // get theme + const theme = useContext(ThemeContext); + // derive message const message = typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); @@ -54,13 +71,14 @@ export const FileCard = ({ chat }: { chat: IMessagePayload }) => { return (
{ />
{shortenText(parsedMessage.name, 11)} {formatFileSize(parsedMessage.size)} @@ -91,7 +111,7 @@ export const FileCard = ({ chat }: { chat: IMessagePayload }) => { rel="noopener noreferrer" download > - +
); diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx new file mode 100644 index 000000000..48b80a091 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx @@ -0,0 +1,74 @@ +// React + Web3 Essentials +import { useContext } from 'react'; + +// External Packages + +// Internal Compoonents +import { Image, Section, Span } from '../../../../reusables'; +import { ThemeContext } from '../../../theme/ThemeProvider'; +import { Tag } from '../../tag/Tag'; + +// Internal Configs + +// Assets + +// Interfaces & Types +import { IMessagePayload } from '../../../exportedTypes'; + +// Constants + +// Exported Interfaces & Types + +// Exported Functions +export const GIFCard = ({ + chat, + background = 'transparent', + color = 'inherit', // default to inherit + previewMode = false, + activeMode = false, +}: { + chat: IMessagePayload; + background?: string; + color?: string; + previewMode?: boolean; + activeMode?: boolean; +}) => { + // get theme + const theme = useContext(ThemeContext); + + // derive message + const message = + typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); + + return ( +
+
+ +
+ + {previewMode && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx new file mode 100644 index 000000000..cc0b33bf9 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx @@ -0,0 +1,81 @@ +// React + Web3 Essentials +import { useContext } from 'react'; + +// External Packages + +// Internal Compoonents +import { Image, Section, Span } from '../../../../reusables'; +import { ThemeContext } from '../../../theme/ThemeProvider'; +import { Tag } from '../../tag/Tag'; + +// Helper functions +import { getParsedMessage } from '../../../helpers'; + +// Internal Configs + +// Assets + +// Interfaces & Types +import { IMessagePayload } from '../../../exportedTypes'; + +// Constants + +// Exported Interfaces & Types + + +const getImageContent = (message: string) => getParsedMessage(message)?.content ?? ''; + +export const ImageCard = ({ + chat, + background = 'transparent', + color = 'inherit', // default to inherit + previewMode = false, + activeMode = false, +}: { + chat: IMessagePayload; + background?: string; + color?: string; + previewMode?: boolean; + activeMode?: boolean; +}) => { + // get theme + const theme = useContext(ThemeContext); + + // derive message + const message = + typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); + + return ( +
+ {previewMode && ( +
+ +
+ )} + +
+ +
+ + +
+ ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/FrameRenderer.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/FrameRenderer.tsx similarity index 100% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/FrameRenderer.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/FrameRenderer.tsx diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/MessageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx similarity index 83% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/MessageCard.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx index bc7f9bd84..2bd09526d 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/MessageCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx @@ -34,10 +34,16 @@ export const MessageCard = ({ chat, position, account, + color = 'inherit', // default to inherit + previewMode = false, + activeMode = false, }: { chat: IMessagePayload; position: number; account: string; + color?: string; + previewMode?: boolean; + activeMode?: boolean; }) => { // get theme const theme = useContext(ThemeContext); @@ -126,8 +132,19 @@ export const MessageCard = ({ return chunks; }; + // if preview mode, reduce the message to 100 characters and only 3 lines + const reduceMessage = (message: string) => { + const limitedMessage = message.slice(0, 100); + const lines = limitedMessage.split('\n'); + const reducedMessage = lines.slice(0, 3).join(' '); + return reducedMessage; + }; + // convert to fragments which can have different types - const fragments = splitMessageToMessages({ msg: message, type: 'text' }); + // if preview mode, skip fragments and only reduce message + const fragments = previewMode + ? [{ msg: reduceMessage(message), type: 'text' }] + : splitMessageToMessages({ msg: message, type: 'text' }); // To render individual fragments const renderTxtFragments = (message: string, fragmentIndex: number): ReactNode => { @@ -141,7 +158,7 @@ export const MessageCard = ({ fontWeight={ position ? `${theme.fontWeight?.chatSentBubbleText}` : `${theme.fontWeight?.chatReceivedBubbleText}` } - color={position ? `${theme.textColor?.chatSentBubbleText}` : `${theme.textColor?.chatReceivedBubbleText}`} + color={color} > {line.split(' ').map((word: string, wordIndex: number) => { const link = hasWebLink(word) ? extractWebLink(word) : ''; @@ -191,37 +208,37 @@ export const MessageCard = ({ // Render entire message return ( - + {/* Preview Renderer - Start with assuming preview is there, callback handles no preview */} {/* Message Rendering - Always happens */}
- {/* Timestamp rendering */} - - {time} - + {/* Timestamp rendering only when no preview mode */} + {!previewMode && ( + + {time} + + )} ); diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/PreviewRenderer.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/PreviewRenderer.tsx similarity index 77% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/PreviewRenderer.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/PreviewRenderer.tsx index 5a0380774..3fa37a08d 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/PreviewRenderer.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/PreviewRenderer.tsx @@ -2,10 +2,12 @@ import { useEffect, useState } from 'react'; // External Packages +import { TwitterTweetEmbed } from 'react-twitter-embed'; // Internal Compoonents import { IFrame } from '../../../../../types'; import { extractWebLink, getFormattedMetadata, hasWebLink, isSupportedVideoLink } from '../../../../../utilities'; +import { checkTwitterUrl } from '../../../helpers/twitter'; import { FrameRenderer } from './FrameRenderer'; import { VideoRenderer } from './VideoRenderer'; @@ -22,7 +24,7 @@ const PROXY_SERVER = 'https://proxy.push.org'; // Exported Interfaces & Types export interface IPreviewCallback { loading: boolean; - urlType: 'video' | 'frame' | 'other'; + urlType: 'video' | 'frame' | 'twitter' | 'other'; error: unknown | null; } @@ -32,18 +34,20 @@ export const PreviewRenderer = ({ account, messageId, previewCallback, + previewMode = false, }: { message: string | undefined; account: string; messageId: string; previewCallback?: (callback: IPreviewCallback) => void; + previewMode?: boolean; }) => { // setup frame data const [initialized, setInitialized] = useState({ loading: true, frameData: {} as IFrame, url: null as string | null, - urlType: 'other' as 'video' | 'frame' | 'other', + urlType: 'other' as 'video' | 'frame' | 'twitter' | 'other', error: null as unknown | null, }); @@ -90,9 +94,23 @@ export const PreviewRenderer = ({ } }; - if (message && hasWebLink(message)) { - const url = extractWebLink(message); - fetchMetaTags(url ?? ''); + if (message && hasWebLink(message) && !previewMode) { + // first check for twitter url + const twitterUrl = checkTwitterUrl(message); + + if (twitterUrl.isTweet) { + setInitialized((prevState) => ({ + ...prevState, + loading: false, + error: null, + url: `${twitterUrl.tweetId}`, + urlType: 'twitter', + })); + } else { + // extract web link and process + const url = extractWebLink(message); + fetchMetaTags(url ?? ''); + } } else { // Initiate the callback setInitialized((prevState) => ({ @@ -130,5 +148,7 @@ export const PreviewRenderer = ({ url={initialized.url} frameData={initialized.frameData} /> + ) : !initialized.loading && !initialized.error && initialized.url && initialized.urlType === 'twitter' ? ( + ) : null; }; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/VideoRenderer.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/VideoRenderer.tsx similarity index 100% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/VideoRenderer.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/VideoRenderer.tsx diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx new file mode 100644 index 000000000..9171fef7b --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx @@ -0,0 +1,184 @@ +// React + Web3 Essentials +import { useContext, useEffect, useState } from 'react'; + +// External Packages +import styled from 'styled-components'; + +// Internal Compoonents +import { useChatData } from '../../../../../hooks'; +import { Section, Span } from '../../../../reusables'; + +import { ThemeContext } from '../../../theme/ThemeProvider'; +import { CardRenderer } from '../../CardRenderer'; + +// Internal Configs + +// Assets + +// Interfaces & Types +import { IMessagePayload } from '../../../exportedTypes'; + +// Constants + +// Exported Interfaces & Types +// Extend Section via ReplySectionProps +interface ReplySectionProps extends React.ComponentProps { + borderBG?: string; +} + +// Exported Functions +export const ReplyCard = ({ + reference, + chatId, + position, +}: { + reference: string | null; + chatId: string | undefined; + position?: number; +}) => { + // get theme + const theme = useContext(ThemeContext); + + // get user + const { user } = useChatData(); + + // set and get reply payload + const [replyPayloadManager, setReplyPayloadManager] = useState<{ + payload: IMessagePayload | null; + loaded: boolean; + err: string | null; + }>({ payload: null, loaded: false, err: null }); + + // resolve reply payload + useEffect(() => { + const resolveReplyPayload = async () => { + if (!replyPayloadManager.loaded) { + if (reference && chatId) { + try { + const payloads = await user?.chat.history(chatId, { reference: reference, limit: 1 }); + const payload = payloads ? payloads[0] : null; + + // check if payload is reply + // if so, change the message type to content one + if (payload?.messageType === 'Reply') { + payload.messageType = payload?.messageObj?.content?.messageType; + payload.messageObj = payload?.messageObj?.content?.messageObj; + } + + // finally set the reply + setReplyPayloadManager({ ...replyPayloadManager, payload: payload, loaded: true }); + } catch (err) { + setReplyPayloadManager({ + ...replyPayloadManager, + payload: null, + loaded: true, + err: 'Unable to load Preview', + }); + } + } else { + setReplyPayloadManager({ + ...replyPayloadManager, + payload: null, + loaded: true, + err: 'Reply reference not found', + }); + } + } + }; + resolveReplyPayload(); + }, [replyPayloadManager, reference, user?.chat, chatId]); + + // render + return ( + + {/* Initial State */} + {!replyPayloadManager.loaded && ( + + Loading Preview... + + )} + + {/* Error State */} + {replyPayloadManager.loaded && replyPayloadManager.err && ( + + {replyPayloadManager.err} + + )} + + {/* Loaded State */} + {replyPayloadManager.loaded && replyPayloadManager.payload && ( +
+ + + {`${replyPayloadManager.payload.fromDID?.split(':')[1].slice(0, 6)}...${replyPayloadManager.payload.fromDID + ?.split(':')[1] + .slice(-6)}`} + + + +
+ + )} +
+ ); +}; + +const ReplySection = styled(Section) ` + border-left: 4px solid ${({ borderBG }) => borderBG || 'transparent'}; +`; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/twitter/TwitterCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/twitter/TwitterCard.tsx similarity index 100% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/twitter/TwitterCard.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/twitter/TwitterCard.tsx diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/index.ts b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/index.ts new file mode 100644 index 000000000..c38a34c6e --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/index.ts @@ -0,0 +1 @@ +export { ChatViewBubbleCore } from './ChatViewBubbleCore'; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/tag/Tag.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/tag/Tag.tsx new file mode 100644 index 000000000..e9296472e --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/tag/Tag.tsx @@ -0,0 +1,47 @@ +// React + Web3 Essentials +import React, { useContext } from 'react'; + +// External Packages +import styled from 'styled-components'; + +// Internal Compoonents +import { Span } from '../../../reusables'; +import { ThemeContext } from '../../theme/ThemeProvider'; + +// Internal Configs + +// Assets + +// Interfaces & Types +import { IMessagePayload } from '../../exportedTypes'; + +// Constants + +// Exported Interfaces & Types +interface TagProps { + type: 'Image' | 'GIF' | 'Video' | 'Audio'; +} + +export const Tag = ({ type }: TagProps) => { + // get theme + const theme = useContext(ThemeContext); + + return ( + + {type} + + ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx index 9317e3aca..22878c89e 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx @@ -66,7 +66,7 @@ export const ChatViewList: React.FC = (options: IChatViewLis invalidChat: false, }); - const { chatId, limit = chatLimit, chatFilterList = [] } = options || {}; + const { chatId, limit = chatLimit, chatFilterList = [], setReplyPayload } = options || {}; const { user, toast } = useChatData(); // const [chatStatusText, setChatStatusText] = useState(''); @@ -216,13 +216,14 @@ export const ChatViewList: React.FC = (options: IChatViewLis scrollLocked = true; } - console.debug( - `UIWeb::ChatViewList::onScroll::scrollLocked ${new Date().toISOString()}`, - scrollRef.current.scrollTop, - scrollRef.current.clientHeight, - scrollRef.current.scrollHeight, - scrollLocked - ); + // Turning it off as it overfills debug + // console.debug( + // `UIWeb::ChatViewList::onScroll::scrollLocked ${new Date().toISOString()}`, + // scrollRef.current.scrollTop, + // scrollRef.current.clientHeight, + // scrollRef.current.scrollHeight, + // scrollLocked + // ); // update scroll-locked attribute scrollRef.current.setAttribute('data-scroll-locked', scrollLocked.toString()); @@ -247,13 +248,14 @@ export const ChatViewList: React.FC = (options: IChatViewLis if (scrollRef.current && height !== 0) { const scrollLocked = scrollRef.current.getAttribute('data-scroll-locked') === 'true' ? true : false; - console.debug( - `UIWeb::ChatViewList::onScroll::scrollLocked Observer ${new Date().toISOString()}`, - scrollRef.current.scrollTop, - scrollRef.current.clientHeight, - scrollRef.current.scrollHeight, - scrollLocked - ); + // Turning it off as it overfills debug + // console.debug( + // `UIWeb::ChatViewList::onScroll::scrollLocked Observer ${new Date().toISOString()}`, + // scrollRef.current.scrollTop, + // scrollRef.current.clientHeight, + // scrollRef.current.scrollHeight, + // scrollLocked + // ); if (height !== 0 && scrollLocked) { // update programmable-scroll attribute @@ -524,6 +526,7 @@ export const ChatViewList: React.FC = (options: IChatViewLis { = (options: IChatViewLis decryptedMessagePayload={chat} chatPayload={chat} chatReactions={reactions[(chat as any).cid] || []} + setReplyPayload={setReplyPayload} showChatMeta={initialized.chatInfo?.meta?.group ?? false} chatId={chatId} actionId={(chat as any).cid} @@ -585,7 +589,7 @@ export const ChatViewList: React.FC = (options: IChatViewLis }; //styles -const ChatViewListCard = styled(Section)` +const ChatViewListCard = styled(Section) ` &::-webkit-scrollbar-thumb { background: ${(props) => props.theme.scrollbarColor}; border-radius: 10px; @@ -598,6 +602,6 @@ const ChatViewListCard = styled(Section)` overscroll-behavior: contain; `; -const ChatViewListCardInner = styled(Section)` +const ChatViewListCardInner = styled(Section) ` filter: ${(props) => (props.blur ? 'blur(12px)' : 'none')}; `; diff --git a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx index 104d6f28a..7b34d991d 100644 --- a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx +++ b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx @@ -14,11 +14,11 @@ import useGroupMemberUtilities from '../../../hooks/chat/useGroupMemberUtilities import usePushSendMessage from '../../../hooks/chat/usePushSendMessage'; import useVerifyAccessControl from '../../../hooks/chat/useVerifyAccessControl'; import { AttachmentIcon } from '../../../icons/Attachment'; -import { EmojiCircleIcon } from '../../../icons/PushIcons'; import { GifIcon } from '../../../icons/Gif'; import OpenLink from '../../../icons/OpenLink'; +import { EmojiCircleIcon } from '../../../icons/PushIcons'; import { SendCompIcon } from '../../../icons/SendCompIcon'; -import { Div, Section, Span, Spinner } from '../../reusables'; +import { Button, Div, Section, Span, Spinner } from '../../reusables'; import { ConditionsInformation } from '../ChatProfile/ChatProfileInfoModal'; import { ConnectButton } from '../ConnectButton'; import { Modal, ModalHeader } from '../reusables/Modal'; @@ -26,12 +26,15 @@ import { ThemeContext } from '../theme/ThemeProvider'; import { PUBLIC_GOOGLE_TOKEN, device } from '../../../config'; import usePushUser from '../../../hooks/usePushUser'; +import { CancelCircleIcon } from '../../../icons/PushIcons'; import { MODAL_BACKGROUND_TYPE, MODAL_POSITION_TYPE, type FileMessageContent } from '../../../types'; import { GIFType, Group, IChatTheme, MessageInputProps } from '../exportedTypes'; import { checkIfAccessVerifiedGroup } from '../helpers'; import { InfoContainer } from '../reusables'; import { IChatInfoResponse } from '../types'; +import { ChatViewBubbleCore } from '../ChatViewBubbleCore'; + /** * @interface IThemeProps * this interface is used for defining the props for styled components @@ -70,6 +73,8 @@ export const MessageInput: React.FC = ({ emoji = true, gif = true, file = true, + replyPayload = null, + setReplyPayload, isConnected = true, autoConnect = false, verificationFailModalBackground = MODAL_BACKGROUND_TYPE.OVERLAY, @@ -350,8 +355,8 @@ export const MessageInput: React.FC = ({ try { const TWO_MB = 1024 * 1024 * 2; if (file.size > TWO_MB) { - console.log('Files larger than 2mb is now allowed'); - throw new Error('Files larger than 2mb is now allowed'); + console.log('Files larger than 2mb is not allowed'); + throw new Error('Files larger than 2mb is not allowed'); } setFileUploading(true); const messageType = file.type.startsWith('image') ? 'Image' : 'File'; @@ -388,9 +393,10 @@ export const MessageInput: React.FC = ({ const sendPushMessage = async (content: string, type: string) => { try { const sendMessageResponse = await sendMessage({ - message: content, chatId: formattedChatId, + message: content, messageType: type as any, + replyRef: replyPayload?.cid || undefined, }); if (sendMessageResponse && typeof sendMessageResponse === 'string' && sendMessageResponse.includes('403')) { setAccessControl(chatId, true); @@ -399,6 +405,9 @@ export const MessageInput: React.FC = ({ } } catch (error) { console.log(error); + } finally { + // reset reply payload + setReplyPayload?.(null); } }; @@ -414,13 +423,20 @@ export const MessageInput: React.FC = ({ setGifOpen(false); }; + // To focus when replyPayload is truthly + useEffect(() => { + if (replyPayload) { + textAreaRef.current?.focus(); + } + }, [replyPayload]); + return !(user && !user?.readmode()) && isConnected ? ( = ({ borderRadius={theme.borderRadius?.messageInput} position="static" border={theme.border?.messageInput} - padding={` ${user && !user?.readmode() ? '13px 16px' : ''}`} + padding={` ${user && !user?.readmode() ? '14px 16px' : ''}`} background={`${theme.backgroundColor?.messageInputBackground}`} alignItems="center" justifyContent="space-between" @@ -548,123 +564,185 @@ export const MessageInput: React.FC = ({ )} ) : null} + + {/* Message bar logic */} {user && !user?.readmode() && (((isRules ? verified : true) && isMember) || (chatInfo && !groupInfo)) && ( - - {emoji && ( -
setShowEmojis(!showEmojis)} - > - -
- )} - {showEmojis && ( +
+ {/* Render reply message */} + {replyPayload && (
- + + {`Reply to `} + + {`${replyPayload.fromDID?.split(':')[1].slice(0, 6)}...${replyPayload.fromDID + ?.split(':')[1] + .slice(-6)}`} + + + +
+
)} - { - if (event.key === 'Enter' && !event.shiftKey) { - event.preventDefault(); - sendTextMsg(); - } - }} - placeholder="Type your message..." - onChange={(e) => onChangeTypedMessage(e.target.value)} - value={typedMessage} - ref={textAreaRef} - rows={1} - /> - {gif && ( -
setGifOpen(!gifOpen)} - > - -
- )} - {gifOpen && ( -
- -
- )} -
- {!fileUploading && file && ( - <> -
- -
- uploadFile(e)} + {/* Render message bar */} + + {emoji && ( +
setShowEmojis(!showEmojis)} + > + - +
+ )} + {showEmojis && ( +
+ +
)} -
- {!(loading || fileUploading) && ( -
sendTextMsg()} - > - -
- )} - {(loading || fileUploading) && ( -
- + { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + sendTextMsg(); + } + }} + placeholder="Type your message..." + onChange={(e) => onChangeTypedMessage(e.target.value)} + value={typedMessage} + rows={1} + /> + {gif && ( +
setGifOpen(!gifOpen)} + > + +
+ )} + {gifOpen && ( +
+ +
+ )} +
+ {!fileUploading && file && ( + <> +
+ +
+ uploadFile(e)} + /> + + )}
- )} - + {!(loading || fileUploading) && ( +
sendTextMsg()} + > + +
+ )} + + {(loading || fileUploading) && ( +
+ +
+ )} + +
)}
@@ -673,7 +751,7 @@ export const MessageInput: React.FC = ({ ); }; -const TypebarSection = styled(Section)<{ border?: string }>` +const TypebarSection = styled(Section) <{ border?: string }>` // gap: 10px; border: ${(props) => props.border || 'none'}; @media ${device.mobileL} { diff --git a/packages/uiweb/src/lib/components/chat/exportedTypes.ts b/packages/uiweb/src/lib/components/chat/exportedTypes.ts index 0b98cb753..2afedbd38 100644 --- a/packages/uiweb/src/lib/components/chat/exportedTypes.ts +++ b/packages/uiweb/src/lib/components/chat/exportedTypes.ts @@ -9,6 +9,7 @@ export interface IChatPreviewPayload { chatGroup: boolean; chatTimestamp: number | undefined; chatMsg?: { + messageMeta: string; messageType: string; messageContent: string | object; }; @@ -54,6 +55,7 @@ export interface IChatViewListProps { chatId: string; chatFilterList?: Array; limit?: number; + setReplyPayload?: (payload: IMessagePayload) => void; } export interface IChatViewComponentProps { @@ -66,6 +68,7 @@ export interface IChatViewComponentProps { emoji?: boolean; gif?: boolean; file?: boolean; + handleReply?: boolean; isConnected?: boolean; autoConnect?: boolean; groupInfoModalBackground?: ModalBackgroundType; @@ -90,7 +93,7 @@ export interface IChatProfile { export interface TwitterFeedReturnType { tweetId: string; - messageType: string; + isTweet: boolean; } export interface IToast { @@ -98,7 +101,10 @@ export interface IToast { status: string; } -export type IMessagePayload = IMessageIPFS; +export type IMessagePayload = IMessageIPFS & { + cid?: string; + reference?: string; +}; export const CHAT_THEME_OPTIONS = { LIGHT: 'light', @@ -116,6 +122,8 @@ export interface MessageInputProps { emoji?: boolean; gif?: boolean; file?: boolean; + replyPayload?: IMessagePayload | null; + setReplyPayload?: (payload: IMessagePayload | null) => void; isConnected?: boolean; autoConnect?: boolean; verificationFailModalBackground?: ModalBackgroundType; diff --git a/packages/uiweb/src/lib/components/chat/helpers/helper.ts b/packages/uiweb/src/lib/components/chat/helpers/helper.ts index d03f7e3ac..13357ad9c 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/helper.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/helper.ts @@ -161,23 +161,63 @@ export const generateRandomNonce: () => string = () => { export const transformChatItems: (items: IFeeds[]) => IChatPreviewPayload[] = (items: IFeeds[]) => { // map but also filter to remove any duplicates which might creep in if stream sends a message const transformedItems: IChatPreviewPayload[] = items - .map((item: IFeeds) => ({ - chatId: item.chatId, - chatPic: item.groupInformation ? item.groupInformation.groupImage : item.profilePicture, - chatParticipant: item.groupInformation ? item.groupInformation.groupName : item.did, - chatGroup: item.groupInformation ? true : false, - chatTimestamp: item.msg.timestamp, - chatMsg: { - messageType: item.msg.messageType, - messageContent: item.msg.messageContent, - }, - })) + .map((item: IFeeds) => { + let messageType = ''; + let messageContent = ''; + + // Typescript doesn't know about the messageObj property + // Workaround: cast to any + const modItem = item as any; + + if (modItem.msg.messageType === 'Reply') { + if (typeof modItem.msg.messageObj === 'object' && !Array.isArray(modItem.msg.messageObj)) { + messageType = modItem.msg.messageObj.content.messageType; + + if (modItem.msg.messageObj.content.messageObj) { + messageContent = modItem.msg.messageObj.content.messageObj.content; + } + } + } else if (typeof modItem.msg.messageObj === 'object' && !Array.isArray(modItem.msg.messageObj)) { + messageType = modItem.msg.messageType; + if (modItem.msg.messageObj) { + messageContent = modItem.msg.messageObj.content; + } + } + + return { + chatId: item.chatId, + chatPic: item.groupInformation ? item.groupInformation.groupImage : item.profilePicture, + chatParticipant: item.groupInformation ? item.groupInformation.groupName : item.did, + chatGroup: item.groupInformation ? true : false, + chatTimestamp: item.msg.timestamp, + chatMsg: { + messageMeta: item.msg.messageType, + messageType: messageType, + messageContent: messageContent, + }, + }; + }) .filter((item, index, self) => index === self.findIndex((t) => t.chatId === item.chatId)); return transformedItems; }; export const transformStreamToIChatPreviewPayload: (item: any) => IChatPreviewPayload = (item: any) => { + let messageType = ''; + let messageContent = ''; + let messageMeta = ''; + + const modItem = item as any; + if (modItem.message.type === 'Reply') { + messageMeta = modItem.message.type; + messageType = modItem.message.content.messageType; + messageContent = modItem.message.content.messageObj.content; + } else { + messageMeta = modItem.message.type; + messageType = modItem.message.type; + messageContent = modItem.message.content; + } + // transform the item const transformedItem: IChatPreviewPayload = { chatId: item.chatId, @@ -192,8 +232,9 @@ export const transformStreamToIChatPreviewPayload: (item: any) => IChatPreviewPa chatGroup: item.meta.group, chatTimestamp: Number(item.timestamp), chatMsg: { - messageType: item?.message?.type, - messageContent: item?.message?.content, + messageMeta: messageType, + messageType: messageType, + messageContent: messageContent, }, }; @@ -227,6 +268,15 @@ export const transformStreamToIMessageIPFSWithCID: (item: any) => IMessageIPFSWi return transformedItem; }; +export const getParsedMessage = (message: string) => { + try { + return JSON.parse(message); + } catch (error) { + console.error('UIWeb::components::ChatViewBubble::ImageCard::error while parsing image', error); + return null; + } +}; + export const getChatParticipantDisplayName = (derivedChatId: string, chatId: string) => { return derivedChatId ? getDomainIfExists(chatId) ?? derivedChatId : derivedChatId; }; diff --git a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts index 2983e1ae6..1e8a98435 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts @@ -1,30 +1,31 @@ import { TwitterFeedReturnType } from '../exportedTypes'; -interface TwitterFeedProps { - message: string; -} - -export const checkTwitterUrl = ({ message }: TwitterFeedProps): TwitterFeedReturnType => { +export const checkTwitterUrl = (message: string): TwitterFeedReturnType => { let tweetId = ''; - let messageType = ''; + let isTweet = false; const URL_REGEX = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/)?([\w#!:.?+=&%@!-]+)/; - const messageContent = message?.split(' '); + const messageContent = typeof message === 'string' ? message.split(' ') : []; + + messageContent?.forEach((message) => { + if (isTweet) return; // Exit the iteration if the tweet was already found + + const lowerCaseMessage = message.toLowerCase(); + + // Check if the message contains a Twitter URL or the letter 'x' + if (URL_REGEX.test(message) && (lowerCaseMessage.includes('twitter') || lowerCaseMessage.includes('x'))) { + // Extract tweetId by splitting the URL before the '?' and then splitting by '/' + const urlParts = message.split('?')[0].split('/'); - for (let i = 0; i < messageContent?.length; i++) { - if (URL_REGEX.test(messageContent[i]) && messageContent[i].toLowerCase().includes('twitter')) { - // Extracting tweetId - const wordArray = messageContent[i].split('?')[0].split('/'); // split url at '?' and take first element and split at '/' - if (wordArray?.length >= 6) { - tweetId = wordArray[wordArray?.length - 1]; - messageType = 'TwitterFeedLink'; - break; + // Ensure the URL has at least 6 parts to extract the tweetId + if (urlParts.length >= 6) { + tweetId = urlParts[urlParts.length - 1]; + isTweet = true; } else { - messageType = 'Text'; - break; + isTweet = false; } } - } + }); - return { tweetId, messageType }; + return { tweetId, isTweet }; }; diff --git a/packages/uiweb/src/lib/components/chat/theme/index.ts b/packages/uiweb/src/lib/components/chat/theme/index.ts index 98e0ac550..36d8417da 100644 --- a/packages/uiweb/src/lib/components/chat/theme/index.ts +++ b/packages/uiweb/src/lib/components/chat/theme/index.ts @@ -1,8 +1,8 @@ /** * @file theme file: all the predefined themes are defined here */ +import styled, { css, keyframes } from 'styled-components'; import { CHAT_THEME_OPTIONS } from '../exportedTypes'; -import styled, { keyframes, css } from 'styled-components'; // bgColorPrimary: "#fff", // bgColorSecondary: "#D53A94", // textColorPrimary: "#1e1e1e", @@ -39,6 +39,8 @@ interface IBorderRadius { userProfile?: string; chatWidget?: string; chatBubbleBorderRadius?: string; + chatBubbleContentBorderRadius?: string; + chatBubbleReplyBorderRadius?: string; reactionsPickerBorderRadius?: string; reactionsBorderRadius?: string; } @@ -52,6 +54,8 @@ interface IPadding { messageInputPadding?: string; chatBubbleSenderPadding?: string; chatBubbleReceiverPadding?: string; + chatBubbleContentPadding?: string; + chatBubbleInnerContentPadding?: string; reactionsPickerPadding?: string; reactionsPadding?: string; } @@ -65,6 +69,8 @@ interface IMargin { messageInputMargin?: string; chatBubbleSenderMargin?: string; chatBubbleReceiverMargin?: string; + chatBubbleContentMargin?: string; + chatBubbleReplyMargin?: string; } interface IBackgroundColor { @@ -75,6 +81,13 @@ interface IBackgroundColor { messageInputBackground?: string; chatSentBubbleBackground?: string; chatReceivedBubbleBackground?: string; + chatPreviewSentBubbleBackground?: string; + chatPreviewSentBorderBubbleBackground?: string; + chatPreviewRecievedBubbleBackground?: string; + chatPreviewRecievedBorderBubbleBackground?: string; + chatActivePreviewBubbleBackground?: string; + chatActivePreviewBorderBubbleBackground?: string; + chatPreviewTagBackground?: string; chatFrameBackground?: string; encryptionMessageBackground?: string; buttonBackground?: string; @@ -237,6 +250,8 @@ export const lightChatTheme: IChatTheme = { userProfile: '0px', chatWidget: '24px', chatBubbleBorderRadius: '12px', + chatBubbleContentBorderRadius: '8px', + chatBubbleReplyBorderRadius: '12px', reactionsPickerBorderRadius: '12px', reactionsBorderRadius: '24px', }, @@ -250,6 +265,8 @@ export const lightChatTheme: IChatTheme = { messageInputPadding: '0px', chatBubbleSenderPadding: '0px', chatBubbleReceiverPadding: '0px', + chatBubbleContentPadding: '8px 16px', + chatBubbleInnerContentPadding: '8px 12px', reactionsPickerPadding: '4px', reactionsPadding: '4px 8px', }, @@ -263,6 +280,8 @@ export const lightChatTheme: IChatTheme = { messageInputMargin: '2px 10px 10px 10px', chatBubbleSenderMargin: '16px 8px', chatBubbleReceiverMargin: '16px 8px', + chatBubbleContentMargin: '8px', + chatBubbleReplyMargin: '8px 8px 0px 8px', }, backgroundColor: { @@ -274,6 +293,13 @@ export const lightChatTheme: IChatTheme = { messageInputBackground: '#fff', chatSentBubbleBackground: 'rgb(202, 89, 155)', chatReceivedBubbleBackground: '#fff', + chatPreviewSentBubbleBackground: 'rgba(255, 255, 255, 0.1)', + chatPreviewSentBorderBubbleBackground: 'rgba(255, 255, 255, 0.5)', + chatPreviewRecievedBubbleBackground: 'rgba(0, 0, 0, 0.1)', + chatPreviewRecievedBorderBubbleBackground: 'rgba(0, 0, 0, 0.5)', + chatActivePreviewBubbleBackground: '#22222210', + chatActivePreviewBorderBubbleBackground: '#22222299', + chatPreviewTagBackground: 'rgba(0, 0, 0, 0.25)', chatFrameBackground: '#f5f5f5', encryptionMessageBackground: '#fff', buttonBackground: 'rgb(202, 89, 155)', @@ -412,6 +438,8 @@ export const darkChatTheme: IChatTheme = { userProfile: '0px', chatWidget: '24px', chatBubbleBorderRadius: '12px', + chatBubbleContentBorderRadius: '8px', + chatBubbleReplyBorderRadius: '8px', reactionsPickerBorderRadius: '12px', reactionsBorderRadius: '24px', }, @@ -425,6 +453,8 @@ export const darkChatTheme: IChatTheme = { messageInputPadding: '0px', chatBubbleSenderPadding: '0px', chatBubbleReceiverPadding: '0px', + chatBubbleContentPadding: '8px 16px', + chatBubbleInnerContentPadding: '8px 12px', reactionsPickerPadding: '4px', reactionsPadding: '4px 8px', }, @@ -438,6 +468,8 @@ export const darkChatTheme: IChatTheme = { messageInputMargin: '2px 10px 10px 10px', chatBubbleSenderMargin: '16px 8px', chatBubbleReceiverMargin: '16px 8px', + chatBubbleContentMargin: '8px', + chatBubbleReplyMargin: '8px', }, backgroundColor: { @@ -449,6 +481,13 @@ export const darkChatTheme: IChatTheme = { messageInputBackground: 'rgb(64, 70, 80)', chatSentBubbleBackground: 'rgb(202, 89, 155)', chatReceivedBubbleBackground: 'rgb(64, 70, 80)', + chatPreviewSentBubbleBackground: 'rgba(255, 255, 255, 0.1)', + chatPreviewSentBorderBubbleBackground: 'rgba(255, 255, 255, 0.5)', + chatPreviewRecievedBubbleBackground: 'rgba(0, 0, 0, 0.1)', + chatPreviewRecievedBorderBubbleBackground: 'rgba(0, 0, 0, 0.5)', + chatActivePreviewBubbleBackground: '#ffffff10', + chatActivePreviewBorderBubbleBackground: '#ffffff99', + chatPreviewTagBackground: 'rgba(255, 255, 255, 0.25)', chatFrameBackground: '#343536', encryptionMessageBackground: 'rgb(64, 70, 80)', buttonBackground: 'rgb(202, 89, 155)', diff --git a/packages/uiweb/src/lib/components/notification/index.tsx b/packages/uiweb/src/lib/components/notification/index.tsx index a8751c65e..17fe99109 100644 --- a/packages/uiweb/src/lib/components/notification/index.tsx +++ b/packages/uiweb/src/lib/components/notification/index.tsx @@ -257,39 +257,39 @@ export const NotificationItem: React.FC = ({ /> ) : // if its a youtube url, RENDER THIS - MediaHelper.isMediaYoutube(notifImage) ? ( - - - - ) : ( - // if its aN MP4 url, RENDER THIS - - - - ))} + + + ) : ( + // if its aN MP4 url, RENDER THIS + + + + ))} {/* section for media content */} {/* section for text content */} @@ -467,6 +467,7 @@ const ChainIconSVG = styled.div` const MobileImage = styled.div` overflow: hidden; + flex-shrink: 0; width: ${(props) => props?.size}; height: ${(props) => props?.size}; img, diff --git a/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx b/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx index 78f0ae73d..967de57f0 100644 --- a/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx +++ b/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx @@ -10,8 +10,8 @@ import { pCAIP10ToWallet } from '../helpers'; import usePushUserInfoUtilities from '../hooks/chat/useUserInfoUtilities'; -import usePushUser from '../hooks/usePushUser'; import useToast from '../components/chat/reusables/NewToast'; // Re-write this later +import usePushUser from '../hooks/usePushUser'; // Internal Configs import { lightChatTheme } from '../components/chat/theme'; diff --git a/packages/uiweb/src/lib/helpers/utils.ts b/packages/uiweb/src/lib/helpers/utils.ts index f865e5567..4d13e1063 100644 --- a/packages/uiweb/src/lib/helpers/utils.ts +++ b/packages/uiweb/src/lib/helpers/utils.ts @@ -35,6 +35,34 @@ export const deriveChatId = async (chatId: string, user: PushAPI | undefined): P return chatId; }; +// Main Logic +// Deep Copy Helper Function +export function deepCopy(obj: T): T { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (obj instanceof Date) { + return new Date(obj.getTime()) as any; + } + + if (obj instanceof Array) { + return obj.reduce((arr, item, i) => { + arr[i] = deepCopy(item); + return arr; + }, [] as any[]) as any; + } + + if (obj instanceof Object) { + return Object.keys(obj).reduce((newObj, key) => { + newObj[key as keyof T] = deepCopy((obj as any)[key]); + return newObj; + }, {} as T); + } + + throw new Error(`Unable to copy obj! Its type isn't supported.`); +} + export const isMessageEncrypted = (message: string) => { if (!message) return false; diff --git a/packages/uiweb/src/lib/hooks/chat/usePushSendMessage.ts b/packages/uiweb/src/lib/hooks/chat/usePushSendMessage.ts index 76d57742a..7600d1185 100644 --- a/packages/uiweb/src/lib/hooks/chat/usePushSendMessage.ts +++ b/packages/uiweb/src/lib/hooks/chat/usePushSendMessage.ts @@ -1,14 +1,15 @@ import * as PushAPI from '@pushprotocol/restapi'; import { useCallback, useContext, useState } from 'react'; -import useVerifyAccessControl from './useVerifyAccessControl'; import { useChatData } from '..'; import { ENV } from '../../config'; import { setAccessControl } from '../../helpers'; +import useVerifyAccessControl from './useVerifyAccessControl'; interface SendMessageParams { message: string; chatId: string; - messageType?: 'Text' | 'Image' | 'File' | 'GIF' | 'MediaEmbed'; + messageType?: 'Text' | 'Image' | 'File' | 'GIF' | 'MediaEmbed' | 'Reply'; + replyRef?: string; } const usePushSendMessage = () => { @@ -19,13 +20,26 @@ const usePushSendMessage = () => { const sendMessage = useCallback( async (options: SendMessageParams) => { - const { chatId, message, messageType } = options || {}; + const { chatId, message, messageType, replyRef } = options || {}; setLoading(true); - try { - const response = await user?.chat.send(chatId, { + + const messagePayload: any = { + type: messageType, + content: message, + }; + + if (replyRef !== undefined) { + messagePayload.type = 'Reply'; + messagePayload.content = { type: messageType, content: message, - }); + }; + messagePayload.reference = replyRef; + } + console.log(messagePayload); + + try { + const response = await user?.chat.send(chatId, messagePayload); setLoading(false); if (!response) { return false; diff --git a/packages/uiweb/src/lib/icons/PushIcons.tsx b/packages/uiweb/src/lib/icons/PushIcons.tsx index a9ffea5e4..c9c8e9743 100644 --- a/packages/uiweb/src/lib/icons/PushIcons.tsx +++ b/packages/uiweb/src/lib/icons/PushIcons.tsx @@ -6,19 +6,43 @@ enum ICON_COLOR { // HELPERS interface IconProps { - size: number | { width?: number; height?: number }; + size: number | { width?: number; height?: number } | string | undefined | null; color?: string | ICON_COLOR; } -const returnWSize = (size: number | { width?: number; height?: number }) => { +const returnWSize = (size: number | { width?: number; height?: number } | string | undefined | null) => { + if (typeof size === 'string') { + size = parseInt(size); + } + + if (typeof size === 'undefined' || size === null) { + return '100%'; + } + return typeof size === 'number' ? size.toString() : size.width ? size.width.toString() : '100%'; }; -const returnHSize = (size: number | { width?: number; height?: number }) => { +const returnHSize = (size: number | { width?: number; height?: number } | string | undefined | null) => { + if (typeof size === 'string') { + size = parseInt(size); + } + + if (typeof size === 'undefined' || size === null) { + return '100%'; + } + return typeof size === 'number' ? size.toString() : size.height ? size.height.toString() : '100%'; }; -const returnViewBox = (size: number | { width?: number; height?: number }, ratio = 1) => { +const returnViewBox = (size: number | { width?: number; height?: number } | string | undefined | null, ratio = 1) => { + if (typeof size === 'string') { + size = parseInt(size); + } + + if (typeof size === 'undefined' || size === null) { + size = 20; // default viewport size + } + if (typeof size === 'number') { return `0 0 ${size * ratio} ${size * ratio}`; } else if (size.width && size.height) { @@ -226,24 +250,25 @@ export const EmojiCircleIcon: React.FC = ({ size, color }) => { export const ReplyIcon: React.FC = ({ size, color }) => { return ( - - - + + ); diff --git a/packages/uiweb/yarn.lock b/packages/uiweb/yarn.lock index 21fa7077f..a568a6c25 100644 --- a/packages/uiweb/yarn.lock +++ b/packages/uiweb/yarn.lock @@ -1257,7 +1257,7 @@ __metadata: "@livepeer/react": "npm:^2.6.0" "@pushprotocol/socket": "npm:^0.5.0" "@unstoppabledomains/resolution": "npm:^8.5.0" - "@web3-name-sdk/core": "npm:^0.1.15" + "@web3-name-sdk/core": "npm:^0.2.0" "@web3-onboard/coinbase": "npm:^2.2.5" "@web3-onboard/core": "npm:^2.21.1" "@web3-onboard/injected-wallets": "npm:^2.10.5" @@ -2812,20 +2812,20 @@ __metadata: languageName: node linkType: hard -"@web3-name-sdk/core@npm:^0.1.15": - version: 0.1.18 - resolution: "@web3-name-sdk/core@npm:0.1.18" +"@web3-name-sdk/core@npm:^0.2.0": + version: 0.2.0 + resolution: "@web3-name-sdk/core@npm:0.2.0" dependencies: "@adraffy/ens-normalize": "npm:^1.10.0" "@ensdomains/ens-validation": "npm:^0.1.0" - viem: "npm:^1.20" peerDependencies: - "@bonfida/spl-name-service": ^1.4.0 + "@bonfida/spl-name-service": ^2.5.1 "@sei-js/core": ^3.1.0 "@siddomains/injective-sidjs": 0.0.2-beta "@siddomains/sei-sidjs": ^0.0.4 "@solana/web3.js": ^1.75.0 - checksum: 10c0/2f2c4611ba1868fbd683ec2249d2581d31aafaa24bdc187a1fd437cf08ffb13dcfda637b6b322afa12d6aea799c5a1fccbd03aacb808218fe315938be4005fd6 + viem: ^2.15.1 + checksum: 10c0/c7503dc312f23d3411def0dd76a4d02bc38ba1867c36ca28461336548fc78abdfac6607f960bbe1aee9199fe1b4aa1480c27b7f9403ec25377fc6bd3b0a47c82 languageName: node linkType: hard @@ -2939,21 +2939,6 @@ __metadata: languageName: node linkType: hard -"abitype@npm:0.9.8": - version: 0.9.8 - resolution: "abitype@npm:0.9.8" - peerDependencies: - typescript: ">=5.0.4" - zod: ^3 >=3.19.1 - peerDependenciesMeta: - typescript: - optional: true - zod: - optional: true - checksum: 10c0/ec559461d901d456820faf307e21b2c129583d44f4c68257ed9d0d44eae461114a7049046e715e069bc6fa70c410f644e06bdd2c798ac30d0ada794cd2a6c51e - languageName: node - linkType: hard - "abitype@npm:1.0.0": version: 1.0.0 resolution: "abitype@npm:1.0.0" @@ -4660,12 +4645,13 @@ __metadata: languageName: node linkType: hard -"isows@npm:1.0.3": - version: 1.0.3 - resolution: "isows@npm:1.0.3" - peerDependencies: - ws: "*" - checksum: 10c0/adec15db704bb66615dd8ef33f889d41ae2a70866b21fa629855da98cc82a628ae072ee221fe9779a9a19866cad2a3e72593f2d161a0ce0e168b4484c7df9cd2 +"isomorphic-unfetch@npm:3.1.0": + version: 3.1.0 + resolution: "isomorphic-unfetch@npm:3.1.0" + dependencies: + node-fetch: "npm:^2.6.1" + unfetch: "npm:^4.2.0" + checksum: 10c0/d3b61fca06304db692b7f76bdfd3a00f410e42cfa7403c3b250546bf71589d18cf2f355922f57198e4cc4a9872d3647b20397a5c3edf1a347c90d57c83cf2a89 languageName: node linkType: hard @@ -7028,27 +7014,6 @@ __metadata: languageName: node linkType: hard -"viem@npm:^1.20": - version: 1.21.4 - resolution: "viem@npm:1.21.4" - dependencies: - "@adraffy/ens-normalize": "npm:1.10.0" - "@noble/curves": "npm:1.2.0" - "@noble/hashes": "npm:1.3.2" - "@scure/bip32": "npm:1.3.2" - "@scure/bip39": "npm:1.2.1" - abitype: "npm:0.9.8" - isows: "npm:1.0.3" - ws: "npm:8.13.0" - peerDependencies: - typescript: ">=5.0.4" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/8b29c790181e44c4c95b9ffed1a8c1b6c2396eb949b95697cc390ca8c49d88ef9e2cd56bd4800b90a9bbc93681ae8d63045fc6fa06e00d84f532bef77967e751 - languageName: node - linkType: hard - "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1"