From fe48fb90f21c48b4e201ac64efc032e5a405bfc0 Mon Sep 17 00:00:00 2001 From: harshrajat Date: Wed, 25 Sep 2024 01:20:46 +0400 Subject: [PATCH 1/9] 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" From 36a808a1ea5befbefb813ec131432c2dd89f8960 Mon Sep 17 00:00:00 2001 From: harshrajat Date: Wed, 25 Sep 2024 02:14:39 +0400 Subject: [PATCH 2/9] Reply cancel and replying to in UIWeb:MessageInput --- .../chat/MessageInput/MessageInput.tsx | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx index 98534a16a..0499ae01b 100644 --- a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx +++ b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx @@ -18,7 +18,7 @@ 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,6 +26,7 @@ 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'; @@ -420,13 +421,15 @@ export const MessageInput: React.FC = ({ setGifOpen(false); }; + console.log('UIWeb::MessageInput::sendTextMsg::replyPayload', 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" @@ -557,13 +560,60 @@ export const MessageInput: React.FC = ({ {/* Message bar logic */} {user && !user?.readmode() && (((isRules ? verified : true) && isMember) || (chatInfo && !groupInfo)) && ( - <> +
{/* Render reply message */} {replyPayload && ( - +
+
+ + {`Replying to `} + + {`${replyPayload.fromDID?.split(':')[1].slice(0, 6)}...${replyPayload.fromDID + ?.split(':')[1] + .slice(-6)}`} + + + +
+ +
)} {/* Render message bar */} @@ -683,7 +733,7 @@ export const MessageInput: React.FC = ({
)} - +
)} From e454e13781b854dfeb2287d32da7ace210419278 Mon Sep 17 00:00:00 2001 From: harshrajat Date: Fri, 27 Sep 2024 18:07:35 +0400 Subject: [PATCH 3/9] Reply Feature with styles --- .../chat/ChatViewBubble/ChatViewBubble.tsx | 1 - .../ChatViewBubble/reactions/Reactions.tsx | 9 +- .../chat/ChatViewBubbleCore/CardRenderer.tsx | 79 ++++++++++---- .../ChatViewBubbleCore/ChatViewBubbleCore.tsx | 101 +++++++++++------- .../cards/file/FileCard.tsx | 40 +++++-- .../ChatViewBubbleCore/cards/gif/GIFCard.tsx | 53 +++++++-- .../cards/image/ImageCard.tsx | 55 ++++++++-- .../cards/message/MessageCard.tsx | 77 ++++++++----- .../cards/message/PreviewRenderer.tsx | 30 +++++- .../cards/reply/ReplyCard.tsx | 88 +++++++++++---- .../chat/ChatViewBubbleCore/tag/Tag.tsx | 47 ++++++++ .../chat/ChatViewList/ChatViewList.tsx | 1 + .../chat/MessageInput/MessageInput.tsx | 17 ++- .../src/lib/components/chat/exportedTypes.ts | 2 +- .../src/lib/components/chat/helpers/helper.ts | 18 +++- .../lib/components/chat/helpers/twitter.ts | 19 ++-- .../src/lib/components/chat/theme/index.ts | 41 ++++++- .../lib/dataProviders/ChatDataProvider.tsx | 2 +- 18 files changed, 521 insertions(+), 159 deletions(-) create mode 100644 packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/tag/Tag.tsx diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index c50ad8a6b..5b39b6745 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -9,7 +9,6 @@ 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'; 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 ? (
(
{ +export const CardRenderer = ({ + chat, + position, + previewMode = false, + activeMode = false, +}: { + chat: IMessagePayload; + position: number; + previewMode?: boolean; + activeMode?: boolean; +}) => { // get theme const theme = useContext(ThemeContext); @@ -26,15 +36,6 @@ export const CardRenderer = ({ chat, position }: { chat: IMessagePayload; positi ? (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'; @@ -43,32 +44,71 @@ export const CardRenderer = ({ chat, position }: { chat: IMessagePayload; positi // 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' && } + {chat.messageType === 'Image' && ( + // Background only valid when no preview or active mode + + )} {/* File Card */} - {chat.messageType === 'File' && } + {chat.messageType === 'File' && ( + + )} {/* Gif Card */} - {chat.messageType === 'GIF' && } - - {/* Twitter Card */} - {chat.messageType === 'TwitterFeedLink' && ( - )} @@ -78,6 +118,9 @@ export const CardRenderer = ({ chat, position }: { chat: IMessagePayload; positi chat={chat} position={position} account={account} + color={fontColor} + previewMode={previewMode} + activeMode={activeMode} /> )} diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx index f762f6e5f..ee261f0f7 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx @@ -1,15 +1,32 @@ -import { ReactNode, useContext, useEffect, useRef, useState } from 'react'; +// React + Web3 Essentials +import { useContext } from 'react'; +// External Packages +import styled from 'styled-components'; + +// Internal Components 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 { 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; +} + +// Main Logic +// Deep Copy Helper Function function deepCopy(obj: T): T { if (obj === null || typeof obj !== 'object') { return obj; @@ -36,7 +53,18 @@ function deepCopy(obj: T): T { throw new Error(`Unable to copy obj! Its type isn't supported.`); } -export const ChatViewBubbleCore = ({ chat, chatId }: { chat: IMessagePayload; chatId: string | undefined }) => { +// 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); @@ -47,36 +75,6 @@ export const ChatViewBubbleCore = ({ chat, chatId }: { chat: IMessagePayload; ch 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[] = []; @@ -97,8 +95,9 @@ export const ChatViewBubbleCore = ({ chat, chatId }: { chat: IMessagePayload; ch // Reply is it's own card that calls ChatViewBubbleCardRenderer // This avoids transitive recursion - // Use replyReference to check and call reply card - if (replyReference !== '') { + // 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( ); } - return <>{components}; + // 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/ChatViewBubbleCore/cards/file/FileCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/file/FileCard.tsx index b64a3f281..5d43f53dd 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/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 index e4bdb3f53..48b80a091 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/gif/GIFCard.tsx @@ -1,9 +1,12 @@ // 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 @@ -17,21 +20,55 @@ import { IMessagePayload } from '../../../exportedTypes'; // Exported Interfaces & Types // Exported Functions -export const GIFCard = ({ chat }: { chat: IMessagePayload }) => { +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 index ad63c299f..94ff160cf 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx @@ -1,9 +1,12 @@ // React + Web3 Essentials +import { useContext } from 'react'; // External Packages // Internal Compoonents -import { Image, Section } from '../../../../reusables'; +import { Image, Section, Span } from '../../../../reusables'; +import { ThemeContext } from '../../../theme/ThemeProvider'; +import { Tag } from '../../tag/Tag'; // Internal Configs @@ -28,21 +31,55 @@ const getParsedMessage = (message: string) => { const getImageContent = (message: string) => getParsedMessage(message)?.content ?? ''; -export const ImageCard = ({ chat }: { chat: IMessagePayload }) => { +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/ChatViewBubbleCore/cards/message/MessageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx index bc7f9bd84..80bb888d4 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/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/ChatViewBubbleCore/cards/message/PreviewRenderer.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/PreviewRenderer.tsx index 5a0380774..3fa37a08d 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/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/ChatViewBubbleCore/cards/reply/ReplyCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx index 74935e1e7..9a617f8df 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx @@ -1,12 +1,14 @@ // React + Web3 Essentials -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; // External Packages +import styled from 'styled-components'; // Internal Compoonents import { useChatData } from '../../../../../hooks'; -import { Image, Section } from '../../../../reusables'; +import { Section, Span } from '../../../../reusables'; +import { ThemeContext } from '../../../theme/ThemeProvider'; import { CardRenderer } from '../../CardRenderer'; // Internal Configs @@ -19,19 +21,12 @@ import { IMessagePayload } from '../../../exportedTypes'; // Constants // Exported Interfaces & Types +// Extend Section via ReplySectionProps +interface ReplySectionProps extends React.ComponentProps { + borderBG?: string; +} // 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, @@ -41,7 +36,8 @@ export const ReplyCard = ({ chatId: string | undefined; position?: number; }) => { - console.debug('UIWeb::components::ChatViewBubble::ReplyCard::chat', reference); + // get theme + const theme = useContext(ThemeContext); // get user const { user } = useChatData(); @@ -94,19 +90,73 @@ export const ReplyCard = ({ // render return ( -
- {!replyPayloadManager.loaded &&
Loading...
} + {/* Initial State */} + {!replyPayloadManager.loaded && ( + + Loading Preview... + + )} + {/* Error State */} + {replyPayloadManager.loaded && replyPayloadManager.err && ( + + {replyPayloadManager.err} + + )} + + {/* Loaded State */} {replyPayloadManager.loaded && replyPayloadManager.payload && ( )} -
+ ); }; + +const ReplySection = styled(Section)` + border-left: 4px solid ${({ borderBG }) => borderBG || 'transparent'}; +`; 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 00448d82d..ecb41dad5 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx @@ -526,6 +526,7 @@ export const ChatViewList: React.FC = (options: IChatViewLis { = ({ setAccessControl(chatId, true); setVerified(false); setVerificationSuccessfull(false); - setReplyPayload?.(null); } } catch (error) { console.log(error); + } finally { + // reset reply payload + setReplyPayload?.(null); } }; @@ -421,7 +423,12 @@ export const MessageInput: React.FC = ({ setGifOpen(false); }; - console.log('UIWeb::MessageInput::sendTextMsg::replyPayload', replyPayload); + // To focus when replyPayload is truthly + useEffect(() => { + if (replyPayload) { + textAreaRef.current?.focus(); + } + }, [replyPayload]); return !(user && !user?.readmode()) && isConnected ? ( = ({ flexDirection="column" alignItems="flex-start" overflow="hidden" - gap="4px" + gap="8px" >
= ({
)} @@ -649,6 +658,7 @@ export const MessageInput: React.FC = ({ )} { @@ -660,7 +670,6 @@ export const MessageInput: React.FC = ({ placeholder="Type your message..." onChange={(e) => onChangeTypedMessage(e.target.value)} value={typedMessage} - ref={textAreaRef} rows={1} /> {gif && ( diff --git a/packages/uiweb/src/lib/components/chat/exportedTypes.ts b/packages/uiweb/src/lib/components/chat/exportedTypes.ts index 4ccd98fb1..2afedbd38 100644 --- a/packages/uiweb/src/lib/components/chat/exportedTypes.ts +++ b/packages/uiweb/src/lib/components/chat/exportedTypes.ts @@ -93,7 +93,7 @@ export interface IChatProfile { export interface TwitterFeedReturnType { tweetId: string; - messageType: string; + isTweet: boolean; } export interface IToast { diff --git a/packages/uiweb/src/lib/components/chat/helpers/helper.ts b/packages/uiweb/src/lib/components/chat/helpers/helper.ts index a9b238719..027b843e7 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/helper.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/helper.ts @@ -168,12 +168,20 @@ export const transformChatItems: (items: IFeeds[]) => IChatPreviewPayload[] = (i // 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; + + 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.messageObj.content.messageType; - messageContent = modItem.msg.messageObj.content.messageObj.content; + messageType = modItem.msg.messageType; + if (modItem.msg.messageObj) { + messageContent = modItem.msg.messageObj.content; + } } return { diff --git a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts index 5f4ee0034..8414bd317 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts @@ -1,30 +1,29 @@ 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 = typeof message === 'string' ? message.split(' ') : []; for (let i = 0; i < messageContent?.length; i++) { - if (URL_REGEX.test(messageContent[i]) && messageContent[i].toLowerCase().includes('twitter')) { + if ( + (URL_REGEX.test(messageContent[i]) && messageContent[i].toLowerCase().includes('twitter')) || + messageContent[i].toLowerCase().includes('x') + ) { // 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'; + isTweet = true; break; } else { - messageType = 'Text'; + isTweet = false; break; } } } - 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/dataProviders/ChatDataProvider.tsx b/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx index d9aeec563..967de57f0 100644 --- a/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx +++ b/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx @@ -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]); From d53f65a16c18ab9890fdb0fb8f37c022a390af89 Mon Sep 17 00:00:00 2001 From: abhishek-01k Date: Thu, 3 Oct 2024 13:42:39 +0530 Subject: [PATCH 4/9] fix: modified some parts in helper func --- .../lib/components/chat/helpers/twitter.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts index 8414bd317..1e8a98435 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/twitter.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/twitter.ts @@ -7,23 +7,25 @@ export const checkTwitterUrl = (message: string): TwitterFeedReturnType => { const URL_REGEX = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/)?([\w#!:.?+=&%@!-]+)/; 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')) || - messageContent[i].toLowerCase().includes('x') - ) { - // 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]; + 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('/'); + + // Ensure the URL has at least 6 parts to extract the tweetId + if (urlParts.length >= 6) { + tweetId = urlParts[urlParts.length - 1]; isTweet = true; - break; } else { isTweet = false; - break; } } - } + }); return { tweetId, isTweet }; }; From 2420a9f4084f6416b612b662f9d5a84e49594c38 Mon Sep 17 00:00:00 2001 From: abhishek-01k Date: Mon, 7 Oct 2024 13:28:37 +0530 Subject: [PATCH 5/9] 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 --- .../reactions/ReactionPicker.tsx | 11 +++++- .../ChatViewBubbleCore/ChatViewBubbleCore.tsx | 18 ++++----- .../cards/reply/ReplyCard.tsx | 37 +++++++++++++++---- 3 files changed, 48 insertions(+), 18 deletions(-) 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/ChatViewBubbleCore/ChatViewBubbleCore.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx index ee261f0f7..0f45fa9cd 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx @@ -87,8 +87,8 @@ export const ChatViewBubbleCore = ({ 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; + derivedMsg.messageType = derivedMsg?.messageObj?.content?.messageType; + derivedMsg.messageObj = derivedMsg?.messageObj?.content?.messageObj; } // Render cards - Anything not a reply is ChatViewBubbleCardRenderer @@ -130,12 +130,12 @@ export const ChatViewBubbleCore = ({ const background = activeMode ? theme.backgroundColor?.chatActivePreviewBubbleBackground : position - ? previewMode - ? theme.backgroundColor?.chatPreviewSentBubbleBackground - : theme.backgroundColor?.chatSentBubbleBackground - : previewMode - ? theme.backgroundColor?.chatPreviewRecievedBubbleBackground - : theme.backgroundColor?.chatReceivedBubbleBackground; + ? previewMode + ? theme.backgroundColor?.chatPreviewSentBubbleBackground + : theme.backgroundColor?.chatSentBubbleBackground + : previewMode + ? theme.backgroundColor?.chatPreviewRecievedBubbleBackground + : theme.backgroundColor?.chatReceivedBubbleBackground; return ( ` +const ChatViewBubbleCoreSection = styled(Section) ` border-left: ${({ borderBG, previewMode }) => (previewMode ? `4px solid ${borderBG || 'transparent'}` : 'none')}; `; 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 index 9a617f8df..d62186a7b 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx @@ -146,17 +146,40 @@ export const ReplyCard = ({ {/* Loaded State */} {replyPayloadManager.loaded && replyPayloadManager.payload && ( - +
+ + {`Replied to `} + + {`${replyPayloadManager.payload.fromDID?.split(':')[1].slice(0, 6)}...${replyPayloadManager.payload.fromDID + ?.split(':')[1] + .slice(-6)}`} + + + +
+ )} ); }; -const ReplySection = styled(Section)` +const ReplySection = styled(Section) ` border-left: 4px solid ${({ borderBG }) => borderBG || 'transparent'}; `; From 45d647fd041e940b23b34fc28563eb03e262f898 Mon Sep 17 00:00:00 2001 From: abhishek-01k Date: Mon, 7 Oct 2024 15:43:48 +0530 Subject: [PATCH 6/9] fix: fixed the UI for the reply feature changed the replying to to reply and also fixed the image positioning --- .../ChatViewBubbleCore/cards/image/ImageCard.tsx | 14 ++++++++------ .../ChatViewBubbleCore/cards/reply/ReplyCard.tsx | 1 - .../components/chat/MessageInput/MessageInput.tsx | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) 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 index 94ff160cf..896fc0b1c 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx @@ -55,11 +55,17 @@ export const ImageCard = ({
+ {previewMode && ( +
+ +
+ )} +
- {previewMode && ( -
- -
- )} +
); }; 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 index d62186a7b..9171fef7b 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/reply/ReplyCard.tsx @@ -157,7 +157,6 @@ export const ReplyCard = ({ fontSize="10px" color={theme.textColor?.chatSentBubbleText} > - {`Replied to `} = ({ fontSize="10px" color={theme.iconColor?.emoji} > - {`Replying to `} + {`Reply to `} = ({ ); }; -const TypebarSection = styled(Section)<{ border?: string }>` +const TypebarSection = styled(Section) <{ border?: string }>` // gap: 10px; border: ${(props) => props.border || 'none'}; @media ${device.mobileL} { From 10672f6d6b9ca9d4399ee5052f5d2733a8be0018 Mon Sep 17 00:00:00 2001 From: abhishek-01k Date: Tue, 8 Oct 2024 14:06:33 +0530 Subject: [PATCH 7/9] fix: fixed the issues moved the funcs to helpers and also removed the commented code --- .../ChatViewBubbleCore/ChatViewBubbleCore.tsx | 30 +------------------ .../cards/image/ImageCard.tsx | 12 ++------ .../cards/message/MessageCard.tsx | 2 +- .../chat/ChatViewList/ChatViewList.tsx | 22 ++------------ .../src/lib/components/chat/helpers/helper.ts | 9 ++++++ packages/uiweb/src/lib/helpers/utils.ts | 28 +++++++++++++++++ 6 files changed, 44 insertions(+), 59 deletions(-) diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx index 0f45fa9cd..aa4304368 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/ChatViewBubbleCore.tsx @@ -8,7 +8,7 @@ import styled from 'styled-components'; import { useChatData } from '../../../hooks'; import { ThemeContext } from '../theme/ThemeProvider'; -import { isMessageEncrypted, pCAIP10ToWallet } from '../../../helpers'; +import { deepCopy, isMessageEncrypted, pCAIP10ToWallet } from '../../../helpers'; import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; import { Section } from '../../reusables'; @@ -25,34 +25,6 @@ interface ChatViewBubbleCoreProps extends React.ComponentProps { previewMode?: boolean; } -// Main Logic -// Deep Copy Helper Function -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.`); -} - // Exported Default Component export const ChatViewBubbleCore = ({ chat, 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 index 896fc0b1c..cc0b33bf9 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/image/ImageCard.tsx @@ -8,6 +8,9 @@ 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 @@ -19,15 +22,6 @@ import { IMessagePayload } from '../../../exportedTypes'; // 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 ?? ''; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx index 80bb888d4..2bd09526d 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubbleCore/cards/message/MessageCard.tsx @@ -211,7 +211,7 @@ export const MessageCard = ({ {/* Preview Renderer - Start with assuming preview is there, callback handles no preview */} = (options: IChatViewLis scrollLocked = true; } - // 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()); @@ -248,15 +239,6 @@ export const ChatViewList: React.FC = (options: IChatViewLis if (scrollRef.current && height !== 0) { const scrollLocked = scrollRef.current.getAttribute('data-scroll-locked') === 'true' ? true : false; - // 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 scrollRef.current.setAttribute('data-programmable-scroll', 'true'); @@ -589,7 +571,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; @@ -602,6 +584,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/helpers/helper.ts b/packages/uiweb/src/lib/components/chat/helpers/helper.ts index 027b843e7..13357ad9c 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/helper.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/helper.ts @@ -268,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/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; From 0a67a7fee8bd85b3abb7b7b7841c371d3d8faf62 Mon Sep 17 00:00:00 2001 From: Monalisha Mishra Date: Tue, 8 Oct 2024 16:07:39 +0530 Subject: [PATCH 8/9] fix: fixed image in notification --- packages/uiweb/src/lib/components/notification/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/uiweb/src/lib/components/notification/index.tsx b/packages/uiweb/src/lib/components/notification/index.tsx index 02a010ef0..6628b2bb5 100644 --- a/packages/uiweb/src/lib/components/notification/index.tsx +++ b/packages/uiweb/src/lib/components/notification/index.tsx @@ -431,6 +431,7 @@ const ChainIconSVG = styled.div` const MobileImage = styled.div` overflow: hidden; + flex-shrink: 0; img, iframe, video { From ce42e5dc4099d5c45bdb151f66cb77d5f2b992e2 Mon Sep 17 00:00:00 2001 From: abhishek-01k Date: Wed, 9 Oct 2024 13:15:12 +0530 Subject: [PATCH 9/9] fix: added the commented code that was removed --- .../chat/ChatViewList/ChatViewList.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx index 2b3cf61b6..22878c89e 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx @@ -216,6 +216,15 @@ export const ChatViewList: React.FC = (options: IChatViewLis scrollLocked = true; } + // 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()); @@ -239,6 +248,15 @@ export const ChatViewList: React.FC = (options: IChatViewLis if (scrollRef.current && height !== 0) { const scrollLocked = scrollRef.current.getAttribute('data-scroll-locked') === 'true' ? true : false; + // 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 scrollRef.current.setAttribute('data-programmable-scroll', 'true');