From dbb6fe12bd86a619dd14aac87a9b714bfbc3de38 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Wed, 9 Oct 2024 13:59:54 +0530 Subject: [PATCH] Main Release 1.6.0 (#1405) * fix: fixed font sizes and dark theme (#1376) * fix: fixed font sizes and dark theme * fix: fixed line height * Add erc1155 to token gating group conditions (#1382) * feat: add erc1155 to token gating group conditions * fix: add tokenId to fetchContractInfo function * lock file updated --------- Co-authored-by: rohitmalhotra1420 * Replaced useResolveWeb3Name hook with resolveWeb3Name helper function (#1390) * fix: added common resolveweb3 for domain name * fix: fixed review comments * Notification Ui change (#1396) * feat: new notification ui * fix: fixed review comments * lock file changed * fix: fixed color and cta hover * fix: fixed the review comments * fix: fixed link icon --------- Co-authored-by: rohitmalhotra1420 * Push Chat Reply Feature (#1399) * Functioning reply in ChatPreviewList, ChatList and Input * Reply cancel and replying to in UIWeb:MessageInput * Reply Feature with styles * fix: modified some parts in helper func * fix: added the replied to text and also fixed the emoji picker position added the replied to text in the chat bubble and fixed the emoji picker position also fixed the breaking of the chat due to the messagetype issue * fix: fixed the UI for the reply feature changed the replying to to reply and also fixed the image positioning * fix: fixed the issues moved the funcs to helpers and also removed the commented code * fix: fixed image in notification * fix: added the commented code that was removed --------- Co-authored-by: abhishek-01k Co-authored-by: Monalisha Mishra * uiweb lock file updated --------- Co-authored-by: Monalisha Mishra <42746736+mishramonalisha76@users.noreply.github.com> Co-authored-by: Kalash Shah <81062983+kalashshah@users.noreply.github.com> Co-authored-by: Harsh | Push Co-authored-by: abhishek-01k Co-authored-by: Monalisha Mishra --- 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 | 127 +++----- .../chat/ChatViewBubble/cards/gif/GIFCard.tsx | 37 --- .../ChatViewBubble/cards/image/ImageCard.tsx | 48 --- .../reactions/ReactionPicker.tsx | 11 +- .../ChatViewBubble/reactions/Reactions.tsx | 9 +- .../chat/ChatViewBubbleCore/CardRenderer.tsx | 128 ++++++++ .../ChatViewBubbleCore/ChatViewBubbleCore.tsx | 130 ++++++++ .../cards/file/FileCard.tsx | 40 ++- .../ChatViewBubbleCore/cards/gif/GIFCard.tsx | 74 +++++ .../cards/image/ImageCard.tsx | 81 +++++ .../cards/message/FrameRenderer.tsx | 0 .../cards/message/MessageCard.tsx | 77 +++-- .../cards/message/PreviewRenderer.tsx | 30 +- .../cards/message/VideoRenderer.tsx | 0 .../cards/reply/ReplyCard.tsx | 184 +++++++++++ .../cards/twitter/TwitterCard.tsx | 0 .../chat/ChatViewBubbleCore/index.ts | 1 + .../chat/ChatViewBubbleCore/tag/Tag.tsx | 47 +++ .../chat/ChatViewList/ChatViewList.tsx | 38 ++- .../chat/MessageInput/MessageInput.tsx | 306 +++++++++++------- .../src/lib/components/chat/exportedTypes.ts | 12 +- .../src/lib/components/chat/helpers/helper.ts | 76 ++++- .../lib/components/chat/helpers/twitter.ts | 39 +-- .../src/lib/components/chat/theme/index.ts | 41 ++- .../src/lib/components/notification/index.tsx | 65 ++-- .../lib/dataProviders/ChatDataProvider.tsx | 2 +- packages/uiweb/src/lib/helpers/utils.ts | 28 ++ .../src/lib/hooks/chat/usePushSendMessage.ts | 26 +- packages/uiweb/src/lib/icons/PushIcons.tsx | 53 ++- packages/uiweb/yarn.lock | 59 +--- 35 files changed, 1373 insertions(+), 742 deletions(-) delete mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/gif/GIFCard.tsx delete mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx 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 (69%) create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx 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 (83%) rename packages/uiweb/src/lib/components/chat/{ChatViewBubble => ChatViewBubbleCore}/cards/message/PreviewRenderer.tsx (77%) 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 create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/tag/Tag.tsx 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..044dd7da2 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,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" @@ -7028,27 +7004,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"