From fe48fb90f21c48b4e201ac64efc032e5a405bfc0 Mon Sep 17 00:00:00 2001 From: harshrajat Date: Wed, 25 Sep 2024 01:20:46 +0400 Subject: [PATCH] Functioning reply in ChatPreviewList, ChatList and Input --- packages/uiweb/package.json | 2 +- .../chat/ChatPreview/ChatPreview.tsx | 109 +++++--- .../chat/ChatPreviewList/ChatPreviewList.tsx | 216 +-------------- .../ChatPreviewSearchList.tsx | 5 +- .../chat/ChatView/ChatViewComponent.tsx | 14 +- .../chat/ChatViewBubble/ChatViewBubble.tsx | 126 ++++----- .../chat/ChatViewBubbleCore/CardRenderer.tsx | 85 ++++++ .../ChatViewBubbleCore/ChatViewBubbleCore.tsx | 129 +++++++++ .../cards/file/FileCard.tsx | 0 .../cards/gif/GIFCard.tsx | 0 .../cards/image/ImageCard.tsx | 0 .../cards/message/FrameRenderer.tsx | 0 .../cards/message/MessageCard.tsx | 0 .../cards/message/PreviewRenderer.tsx | 0 .../cards/message/VideoRenderer.tsx | 0 .../cards/reply/ReplyCard.tsx | 112 ++++++++ .../cards/twitter/TwitterCard.tsx | 0 .../chat/ChatViewBubbleCore/index.ts | 1 + .../chat/ChatViewList/ChatViewList.tsx | 33 +-- .../chat/MessageInput/MessageInput.tsx | 247 ++++++++++-------- .../src/lib/components/chat/exportedTypes.ts | 10 +- .../src/lib/components/chat/helpers/helper.ts | 59 ++++- .../lib/components/chat/helpers/twitter.ts | 2 +- .../lib/dataProviders/ChatDataProvider.tsx | 4 +- .../src/lib/hooks/chat/usePushSendMessage.ts | 26 +- packages/uiweb/src/lib/icons/PushIcons.tsx | 53 +++- packages/uiweb/yarn.lock | 59 +---- 27 files changed, 737 insertions(+), 555 deletions(-) create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/CardRenderer.tsx create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/file/FileCard.tsx (100%) rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/gif/GIFCard.tsx (100%) rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/image/ImageCard.tsx (100%) rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/message/FrameRenderer.tsx (100%) rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/message/MessageCard.tsx (100%) rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/message/PreviewRenderer.tsx (100%) rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/message/VideoRenderer.tsx (100%) create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/twitter/TwitterCard.tsx (100%) create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/index.ts 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..c50ad8a6b 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -7,6 +7,7 @@ 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'; @@ -20,11 +21,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 +33,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 +187,7 @@ export const ChatViewBubble = ({ decryptedMessagePayload, chatPayload: payload, chatReactions, + setReplyPayload, showChatMeta = false, chatId, actionId, @@ -197,6 +197,7 @@ export const ChatViewBubble = ({ decryptedMessagePayload: IMessagePayload; chatPayload?: IMessagePayload; chatReactions?: any; + setReplyPayload?: (payload: IMessagePayload) => void; showChatMeta?: boolean; chatId?: string; actionId?: string | null | undefined; @@ -220,26 +221,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 +243,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 +276,11 @@ export const ChatViewBubble = ({ + <> + {/* Reply Icon */} + { + e.stopPropagation(); + setReplyPayload?.(chatPayload); + }} + > + + + + {/* Reaction Picker */} + + )} diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/CardRenderer.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/CardRenderer.tsx new file mode 100644 index 000000000..69c371af2 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/CardRenderer.tsx @@ -0,0 +1,85 @@ +import { ReactNode, useContext, useEffect, useRef, useState } from 'react'; + +import { useChatData } from '../../../hooks'; +import { checkTwitterUrl } from '../helpers/twitter'; +import { ThemeContext } from '../theme/ThemeProvider'; + +import { isMessageEncrypted, pCAIP10ToWallet } from '../../../helpers'; +import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; + +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'; + +export const CardRenderer = ({ chat, position }: { chat: IMessagePayload; position: number }) => { + // 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); + + // check and render tweets + const { tweetId, messageType }: TwitterFeedReturnType = checkTwitterUrl({ + message: message, + }); + + if (messageType === 'TwitterFeedLink') { + chat.messageType = 'TwitterFeedLink'; + } + + // 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 ?? ''; + + // Render the card render + return ( + <> + {/* Message Card */} + {chat && chat.messageType === 'Text' && ( + + )} + + {/* Image Card */} + {chat.messageType === 'Image' && } + + {/* File Card */} + {chat.messageType === 'File' && } + + {/* Gif Card */} + {chat.messageType === 'GIF' && } + + {/* Twitter Card */} + {chat.messageType === 'TwitterFeedLink' && ( + + )} + + {/* 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..f762f6e5f --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx @@ -0,0 +1,129 @@ +import { ReactNode, useContext, useEffect, useRef, useState } from 'react'; + +import { useChatData } from '../../../hooks'; +import { checkTwitterUrl } from '../helpers/twitter'; +import { ThemeContext } from '../theme/ThemeProvider'; + +import { isMessageEncrypted, pCAIP10ToWallet } from '../../../helpers'; +import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; + +import { CardRenderer } from './CardRenderer'; +import { ReplyCard } from './cards/reply/ReplyCard'; + +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 ChatViewBubbleCore = ({ chat, chatId }: { chat: IMessagePayload; chatId: string | undefined }) => { + // 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; + + // // manale reply payload loader + // type ReplyPayloadManagerType = { + // payload: IMessagePayload | null; + // loading: boolean; + // loaded: boolean; + // err: string | null; + // }; + + // const [replyPayloadManager, setReplyPayloadManager] = useState({ + // payload: null, + // loading: true, + // loaded: false, + // err: null, + // }); + + // const resolveReplyPayload = async (chat: IMessagePayload, reference: string | null) => { + // if (reference && chatId) { + // try { + // const payloads = await user?.chat.history(chatId, { reference: reference, limit: 1 }); + // const payload = payloads ? payloads[0] : null; + // console.log('resolving reply payload', payload); + // setReplyPayloadManager({ payload: payload, loading: false, loaded: true, err: null }); + // } catch (err) { + // setReplyPayloadManager({ payload: null, loading: false, loaded: true, err: 'Unable to load Preview' }); + // } + // } else { + // setReplyPayloadManager({ payload: null, loading: false, loaded: true, err: 'Reference not found' }); + // } + // }; + + 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 + if (replyReference !== '') { + // Add Reply Card + components.push( + + ); + } + + // Use derivedMsg to render other cards + if (derivedMsg) { + // Add Message Card + components.push( + + ); + } + + return <>{components}; + }; + + return renderBubble(chat, chatPosition); +}; 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 100% 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 diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/gif/GIFCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx similarity index 100% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/gif/GIFCard.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx similarity index 100% rename from packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx rename to packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx 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 100% 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 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 100% 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 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..74935e1e7 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx @@ -0,0 +1,112 @@ +// React + Web3 Essentials +import { useEffect, useState } from 'react'; + +// External Packages + +// Internal Compoonents +import { useChatData } from '../../../../../hooks'; +import { Image, Section } from '../../../../reusables'; + +import { CardRenderer } from '../../CardRenderer'; + +// 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 ReplyCard = ({ + reference, + chatId, + position, +}: { + reference: string | null; + chatId: string | undefined; + position?: number; +}) => { + console.debug('UIWeb::components::ChatViewBubble::ReplyCard::chat', reference); + + // 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 ( +
+ {!replyPayloadManager.loaded &&
Loading...
} + + {replyPayloadManager.loaded && replyPayloadManager.payload && ( + + )} +
+ ); +}; 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/ChatViewList/ChatViewList.tsx b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx index 9317e3aca..00448d82d 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 @@ -562,6 +564,7 @@ export const ChatViewList: React.FC = (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} diff --git a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx index 104d6f28a..98534a16a 100644 --- a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx +++ b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx @@ -14,9 +14,9 @@ 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 { ConditionsInformation } from '../ChatProfile/ChatProfileInfoModal'; @@ -32,6 +32,8 @@ 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 +72,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 +354,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,14 +392,16 @@ 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); setVerified(false); setVerificationSuccessfull(false); + setReplyPayload?.(null); } } catch (error) { console.log(error); @@ -548,123 +554,136 @@ 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 && ( + )} - { - 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} + ref={textAreaRef} + rows={1} + /> + {gif && ( +
setGifOpen(!gifOpen)} + > + +
+ )} + {gifOpen && ( +
+ +
+ )} +
+ {!fileUploading && file && ( + <> +
+ +
+ uploadFile(e)} + /> + + )}
- )} - + {!(loading || fileUploading) && ( +
sendTextMsg()} + > + +
+ )} + + {(loading || fileUploading) && ( +
+ +
+ )} + + )} diff --git a/packages/uiweb/src/lib/components/chat/exportedTypes.ts b/packages/uiweb/src/lib/components/chat/exportedTypes.ts index 0b98cb753..4ccd98fb1 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; @@ -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..a9b238719 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/helper.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/helper.ts @@ -161,23 +161,55 @@ 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') { + messageType = modItem.msg.messageType; + messageContent = modItem.msg.messageObj.content; + } else if (typeof modItem.msg.messageObj === 'object' && !Array.isArray(modItem.msg.messageObj)) { + messageType = modItem.msg.messageObj.content.messageType; + messageContent = modItem.msg.messageObj.content.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 +224,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, }, }; diff --git a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts index 2983e1ae6..5f4ee0034 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts @@ -9,7 +9,7 @@ export const checkTwitterUrl = ({ message }: TwitterFeedProps): TwitterFeedRetur let messageType = ''; 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(' ') : []; for (let i = 0; i < messageContent?.length; i++) { if (URL_REGEX.test(messageContent[i]) && messageContent[i].toLowerCase().includes('twitter')) { diff --git a/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx b/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx index 78f0ae73d..d9aeec563 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'; @@ -225,7 +225,7 @@ export const ChatUIProvider = ({ enableConsole(); } else { console.warn('UIWeb::ChatDataProvider::Debug mode is turned off, console logs are suppressed'); - disableConsole(); + // disableConsole(); } }, [debug]); 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 1f9e9f2a0..0d5b6e05c 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" @@ -2798,20 +2798,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 @@ -2925,21 +2925,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,15 +4645,6 @@ __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 - languageName: node - linkType: hard - "isows@npm:1.0.4": version: 1.0.4 resolution: "isows@npm:1.0.4" @@ -7035,27 +7011,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"