diff --git a/packages/examples/boilerplate/index.js b/packages/examples/boilerplate/index.js index d413fc9c5..b6e6f5998 100644 --- a/packages/examples/boilerplate/index.js +++ b/packages/examples/boilerplate/index.js @@ -8,7 +8,7 @@ const signerAlice = ethers.Wallet.createRandom(); const userAlice = await PushAPI.initialize(signerAlice, { env: CONSTANTS.ENV.PROD, }); -const userBobAddress = '0x60cD05eb31cc16cC37163D514bEF162406d482e1'; +const userBobAddress = '0x0149C2723496fEF62e6e7fa79A31E5ea22bA70C7'; const generateRandomWordsWithTimestamp = () => { return `${Math.random() @@ -17,7 +17,13 @@ const generateRandomWordsWithTimestamp = () => { }; userAlice.chat.send(userBobAddress, { - content: "Gm gm! It's a me... Alice! - " + generateRandomWordsWithTimestamp(), + type: 'Reaction', + content: '👍', + reference: 'bafyreia2okco5ocdxmoxon72erviypaht74u3dqunf3vydu237ybju4kw4', }); console.log('Message sent from Alice to ', userBobAddress); +// const groupPermissions = await userAlice.chat.group.info( +// 'a7d0581affdaea7b80be836ea5f8a982c0dfd56fb30ee2b01c64980afb152af7' +// ); +// console.log('info', groupPermissions); diff --git a/packages/uiweb/.babelrc b/packages/uiweb/.babelrc index 37b9291d3..404251f62 100644 --- a/packages/uiweb/.babelrc +++ b/packages/uiweb/.babelrc @@ -15,7 +15,8 @@ { "root": ["./src"], "alias": { - "components": "./src/lib/components" + "@components": "./src/lib/components", + "@icons": "./src/lib/icons" // Add more aliases as needed } } diff --git a/packages/uiweb/package.json b/packages/uiweb/package.json index f4b300bf4..5036e390a 100644 --- a/packages/uiweb/package.json +++ b/packages/uiweb/package.json @@ -37,7 +37,7 @@ "uuid": "^9.0.1" }, "peerDependencies": { - "@pushprotocol/restapi": "1.7.17", + "@pushprotocol/restapi": "1.7.19", "@pushprotocol/socket": "^0.5.0", "react": ">=16.8.0", "styled-components": "^6.0.8" diff --git a/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx b/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx index 2e415bff4..5d16295de 100644 --- a/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatPreviewList/ChatPreviewList.tsx @@ -174,7 +174,9 @@ export const ChatPreviewList: React.FC = (options: IChatP overrideAccount, }); - console.debug('UIWeb::ChatPreviewList::loadMoreChats:: Fetched', type, nextpage, currentNonce, chatList); + console.debug( + `UIWeb::ChatPreviewList::loadMoreChats:: Fetched type - ${type} - nextpage - ${nextpage} - currentNonce - ${currentNonce} - chatList - ${chatList}` + ); if (chatList) { // get and transform chats @@ -489,12 +491,25 @@ export const ChatPreviewList: React.FC = (options: IChatP useEffect(() => { if ( + chatPreviewList.page !== 0 && listInnerRef && listInnerRef?.current && listInnerRef?.current?.parentElement && - !chatPreviewList.loading && - !chatPreviewList.loaded + !chatPreviewList.loading ) { + console.debug( + 'UIWeb::ChatPreviewList::useEffect[chatPreviewList.items]::Checking if we need to load more chats::', + chatPreviewList, + listInnerRef.current.clientHeight, + SCROLL_LIMIT, + listInnerRef.current.parentElement.clientHeight, + listInnerRef.current.clientHeight + SCROLL_LIMIT < listInnerRef.current.parentElement.clientHeight + ); + + if (chatPreviewList.loaded) { + return; + } + if (listInnerRef.current.clientHeight + SCROLL_LIMIT < listInnerRef.current.parentElement.clientHeight) { // set loading to true setChatPreviewList((prev) => ({ @@ -504,7 +519,7 @@ export const ChatPreviewList: React.FC = (options: IChatP })); } } - }, [chatPreviewList.page]); + }, [chatPreviewList.items]); // If badges count change useEffect(() => { @@ -610,7 +625,7 @@ export const ChatPreviewList: React.FC = (options: IChatP message: 'Invalid search', }; } - console.debug(error); + if (!error) { const chatInfo = await fetchChat({ chatId: formattedChatId }); if (chatInfo && chatInfo?.meta?.group) diff --git a/packages/uiweb/src/lib/components/chat/ChatProfile/ChatProfile.tsx b/packages/uiweb/src/lib/components/chat/ChatProfile/ChatProfile.tsx index 239197f47..277e2e20d 100644 --- a/packages/uiweb/src/lib/components/chat/ChatProfile/ChatProfile.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatProfile/ChatProfile.tsx @@ -355,7 +355,6 @@ export const ChatProfile: React.FC = ({ const Container = styled(Section)` width: auto; max-width: 100%; - overflow: hidden; background: ${(props) => props.theme.backgroundColor.chatProfileBackground}; border: ${(props) => props.theme.border?.chatProfile}; border-radius: ${(props) => props.theme.borderRadius?.chatProfile}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index 64ce72f93..c3770fde1 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -17,9 +17,17 @@ import { BsLightning } from 'react-icons/bs'; import { FaBell, FaLink, FaRegThumbsUp } from 'react-icons/fa'; import { MdError, MdOpenInNew } from 'react-icons/md'; import { FILE_ICON, allowedNetworks, device } from '../../../config'; -import { formatFileSize, getPfp, pCAIP10ToWallet, shortenText, sign, toSerialisedHexString } from '../../../helpers'; +import { + formatFileSize, + getPfp, + pCAIP10ToWallet, + shortenText, + sign, + toSerialisedHexString, + isMessageEncrypted, +} from '../../../helpers'; import { createBlockie } from '../../../helpers/blockies'; -import { FileMessageContent, FrameDetails, IFrame, IFrameButton } from '../../../types'; +import { FileMessageContent, FrameDetails, IFrame, IFrameButton, IReactionsForChatMessages } from '../../../types'; import { extractWebLink, getFormattedMetadata, hasWebLink } from '../../../utilities'; import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; import { Button, TextInput } from '../reusables'; @@ -30,6 +38,9 @@ import { ImageCard } from './cards/image/ImageCard'; import { MessageCard } from './cards/message/MessageCard'; import { TwitterCard } from './cards/twitter/TwitterCard'; +import { Reactions } from './reactions/Reactions'; +import { ReactionPicker } from './reactions/ReactionPicker'; + const SenderMessageAddress = ({ chat }: { chat: IMessagePayload }) => { const { user } = useContext(ChatDataContext); const theme = useContext(ThemeContext); @@ -136,13 +147,13 @@ const SenderMessageProfilePicture = ({ chat }: { chat: IMessagePayload }) => { }; const MessageWrapper = ({ - chat, + chatPayload, + showChatMeta, children, - isGroup, }: { - chat: IMessagePayload; + chatPayload: IMessagePayload; + showChatMeta: boolean; children: ReactNode; - isGroup: boolean; }) => { const { user } = useChatData(); const theme = useContext(ThemeContext); @@ -152,18 +163,20 @@ const MessageWrapper = ({ flexDirection="row" justifyContent="start" gap="6px" + width="100%" maxWidth="100%" > - {isGroup && pCAIP10ToWallet(chat?.fromCAIP10) !== pCAIP10ToWallet(user?.account ?? '') && ( - + {showChatMeta && pCAIP10ToWallet(chatPayload?.fromCAIP10) !== pCAIP10ToWallet(user?.account ?? '') && ( + )}
- {isGroup && pCAIP10ToWallet(chat?.fromCAIP10) !== pCAIP10ToWallet(user?.account ?? '') && ( - + {showChatMeta && pCAIP10ToWallet(chatPayload?.fromCAIP10) !== pCAIP10ToWallet(user?.account ?? '') && ( + )} {children}
@@ -173,94 +186,183 @@ const MessageWrapper = ({ export const ChatViewBubble = ({ decryptedMessagePayload, - isGroup, + chatPayload: payload, + chatReactions, + showChatMeta = false, + chatId, + actionId, + singularActionId, + setSingularActionId, }: { decryptedMessagePayload: IMessagePayload; - isGroup: boolean; + chatPayload?: IMessagePayload; + chatReactions?: any; + showChatMeta?: boolean; + chatId?: string; + actionId?: string | null | undefined; + singularActionId?: string | null | undefined; + setSingularActionId?: (singularActionId: string | null | undefined) => void; }) => { + // get theme + const theme = useContext(ThemeContext); + + // TODO: Remove decryptedMessagePayload in v2 component + const chatPayload = payload ?? decryptedMessagePayload; + + // setup reactions picker visibility + const [showReactionPicker, setShowReactionPicker] = useState(false); + const [userSelectingReaction, setUserSelectingReaction] = useState(false); + + // get user const { user } = useChatData(); - const position = - pCAIP10ToWallet(decryptedMessagePayload.fromDID).toLowerCase() !== - pCAIP10ToWallet(user?.account ?? '')?.toLowerCase() - ? 0 - : 1; + + // get chat position + 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: decryptedMessagePayload?.messageContent, + message: message, }); + if (messageType === 'TwitterFeedLink') { - decryptedMessagePayload.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); + return ( - {/* Message Card */} - {decryptedMessagePayload.messageType === 'Text' && ( - - )} + {/* Chat Card + Reaction Container */} + setShowReactionPicker(true)} + onMouseLeave={() => setShowReactionPicker(false)} + > + + {/* hide overflow for chat cards and border them */} +
+ {/* Message Card */} + {chatPayload.messageType === 'Text' && ( + + )} - {/* Image Card */} - {decryptedMessagePayload.messageType === 'Image' && ( - - )} + {/* Image Card */} + {chatPayload.messageType === 'Image' && } - {/* File Card */} - {decryptedMessagePayload.messageType === 'File' && ( - - )} + {/* File Card */} + {chatPayload.messageType === 'File' && } - {/* Gif Card */} - {decryptedMessagePayload.messageType === 'GIF' && ( - - )} + {/* Gif Card */} + {chatPayload.messageType === 'GIF' && } - {/* Twitter Card */} - {decryptedMessagePayload.messageType === 'TwitterFeedLink' && ( - - )} + {/* Twitter Card */} + {chatPayload.messageType === 'TwitterFeedLink' && ( + + )} - {/* Default Message Card */} - {decryptedMessagePayload.messageType !== 'Text' && - decryptedMessagePayload.messageType !== 'Image' && - decryptedMessagePayload.messageType !== 'File' && - decryptedMessagePayload.messageType !== 'GIF' && - decryptedMessagePayload.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 */} + {chatReactions && !!chatReactions.length && ( +
+ +
+ )} +
+ + + {/* Only render if user and user readmode is false} + {/* For reaction - additional condition - only render if chatId is passed and setSelectedChatMsgId is passed */} + {user && !user.readmode() && chatId && ( + + )} + +
); }; -const MessageSection = styled(Section)` +const MessageSection = styled(Section)``; + +const ChatWrapperSection = styled(Section)``; + +const ChatBubbleSection = styled(Section)` max-width: 70%; @media ${device.tablet} { @@ -271,3 +373,8 @@ const MessageSection = styled(Section)` max-width: 90%; } `; + +const ChatBubbleSidebarSection = styled(Section)` + width: auto; + position: relative; +`; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx index 4a452cb80..b64a3f281 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/file/FileCard.tsx @@ -29,18 +29,32 @@ import { IMessagePayload } from '../../../exportedTypes'; // Exported Interfaces & Types // Exported Functions -export const FileCard = ({ chat, isGroup }: { chat: IMessagePayload; position: number; isGroup: boolean }) => { - const fileContent: FileMessageContent = JSON.parse(chat?.messageContent); - const name = fileContent.name; - const content = fileContent.content as string; - const size = fileContent.size; +const getParsedMessage = (message: string): FileMessageContent => { + try { + return JSON.parse(message); + } catch (error) { + console.error('UIWeb::components::ChatViewBubble::FileCard::error while parsing image', error); + return { + name: 'Unable to load file', + content: '', + size: 0, + type: '', + }; + } +}; + +export const FileCard = ({ chat }: { chat: IMessagePayload }) => { + // derive message + const message = + typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); + + const parsedMessage = getParsedMessage(message); return (
extension icon - {shortenText(name, 11)} + {shortenText(parsedMessage.name, 11)} - {formatFileSize(size)} + {formatFileSize(parsedMessage.size)}
{ +export const GIFCard = ({ chat }: { chat: IMessagePayload }) => { + // derive message + const message = + typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); + return (
); diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx index 12068eead..ad63c299f 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/image/ImageCard.tsx @@ -17,28 +17,31 @@ 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 ?? ''; + +export const ImageCard = ({ chat }: { chat: IMessagePayload }) => { + // derive message + const message = + typeof chat.messageObj === 'object' ? (chat.messageObj?.content as string) ?? '' : (chat.messageObj as string); -export const ImageCard = ({ - chat, - position, - isGroup, -}: { - chat: IMessagePayload; - position: number; - isGroup: boolean; -}) => { return (
); diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/MessageCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/MessageCard.tsx index 1d1517cb8..b740e8a75 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/MessageCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/message/MessageCard.tsx @@ -33,12 +33,10 @@ interface IMsgFragments { export const MessageCard = ({ chat, position, - isGroup, account, }: { chat: IMessagePayload; position: number; - isGroup: boolean; account: string; }) => { // get theme @@ -195,12 +193,9 @@ export const MessageCard = ({ {/* Preview Renderer - Start with assuming preview is there, callback handles no preview */}
{fragments.map((fragment, fragmentIndex) => { @@ -246,7 +239,6 @@ export const MessageCard = ({ {/* Timestamp rendering */} {time} diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/twitter/TwitterCard.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/twitter/TwitterCard.tsx index 38702d238..a7cb9a85c 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/twitter/TwitterCard.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/cards/twitter/TwitterCard.tsx @@ -19,20 +19,9 @@ import { IMessagePayload } from '../../../exportedTypes'; // Exported Functions -export const TwitterCard = ({ - chat, - tweetId, - isGroup, - position, -}: { - chat: IMessagePayload; - tweetId: string; - isGroup: boolean; - position: number; -}) => { +export const TwitterCard = ({ chat, tweetId }: { chat: IMessagePayload; tweetId: string }) => { return (
void; + actionId?: string | null | undefined; + singularActionId?: string | null | undefined; + setSingularActionId?: (singularActionId: string | null | undefined) => void; + chatSidebarRef: RefObject; +}) => { + // get theme + const theme = useContext(ThemeContext); + + // to adjust reaction selection position + const reactionSelectionRef: RefObject = useRef(null); + + // adjust position of reaction selection + const adjustPosition = () => { + // TODO: Resize should adjust reaction picker + // const element = reactionSelectionRef.current; + // if (element) { + // const rect = element.getBoundingClientRect(); + // const elementWidth = rect.width; + // const overflowRight = rect.left + elementWidth + 50 - window.innerWidth; + // if (overflowRight > 0) { + // element.style.left = `${overflowRight - rect.left}px`; // Adjust left so the element is 30px inside the window + // } + // } + }; + + useEffect(() => { + adjustPosition(); + window.addEventListener('resize', adjustPosition); + return () => window.removeEventListener('resize', adjustPosition); + }, []); + + // get user + const { user } = useChatData(); + + // when sending reaction + const [sendingReaction, setSendingReaction] = useState(null); + + // prepare to send reaction + const processSendReaction = (reaction: string) => { + // reset user selecting reaction + setUserSelectingReaction(!userSelectingReaction); + + // set sending reaction + setSendingReaction(reaction); + }; + + useEffect(() => { + // to send reaction + const sendReaction = async (reaction: string) => { + // try to send reaction + user?.chat + .send(chatId, { + type: 'Reaction', + content: reaction, + reference: (chat as any).cid, + }) + .then((response) => { + console.debug( + 'UIWeb::components::ChatViewBubble::ReactionPicker::sendReaction success with response:', + response + ); + }) + .catch((error) => { + console.error('UIWeb::components::ChatViewBubble::ReactionPicker::sendReaction error:', error); + }) + .finally(() => { + setSendingReaction(''); + }); + }; + + if (sendingReaction) { + sendReaction(sendingReaction); + } + }, [sendingReaction]); + + return ( + <> + {/* To display emoji picker */} + + + {/* To pick emoji from emoji picker render and if actionId matches singularActionId */} + {userSelectingReaction && actionId === singularActionId && ( +
+ {/* To display processing if sending reaction is not null */} + {sendingReaction && ( +
+ +
+ )} + + {/* To display reaction only when sending reaction is null */} + {!sendingReaction && ( + <> + + + + + + + + + + + + + )} +
+ )} + + ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/Reactions.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/Reactions.tsx new file mode 100644 index 000000000..36b883948 --- /dev/null +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/reactions/Reactions.tsx @@ -0,0 +1,103 @@ +// React + Web3 Essentials +import { useContext, useRef, useState, useEffect, RefObject } from 'react'; + +// External Packages + +// Internal Compoonents +import { Image, Section, Button, Spinner, Span } from '../../../reusables'; +import { ThemeContext } from '../../theme/ThemeProvider'; + +// Internal Configs + +// Assets + +// Interfaces & Types +import { IReactionsForChatMessages } from '../../../../types'; + +interface IReactions { + [key: string]: string[]; +} + +// Constants + +// Exported Interfaces & Types + +// Exported Functions +export const Reactions = ({ chatReactions }: { chatReactions: IReactionsForChatMessages[] }) => { + // get theme + const theme = useContext(ThemeContext); + + // transform to IReactions + const uniqueReactions = chatReactions.reduce((acc, reaction) => { + const contentKey = (reaction as any).messageObj?.content || ''; + if (!acc[contentKey]) { + acc[contentKey] = []; + } + + // eliminate duplicate + if (!acc[contentKey].includes((reaction as any).fromCAIP10)) { + acc[contentKey].push((reaction as any).fromCAIP10); + } + + return acc; + }, {} as IReactions); + + console.debug('UIWeb::components::ChatViewBubble::Reactions::uniqueReactions', uniqueReactions); + + // render reactions + return ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + {Object.keys(uniqueReactions).length > 2 ? ( +
+ + {Object.keys(uniqueReactions).join(' ')} + + + {Object.values(uniqueReactions).reduce((total, reactions) => total + reactions.length, 0)} + +
+ ) : ( + Object.entries(uniqueReactions).map(([content, reactions]) => ( +
+ + {content} + + + {reactions.length} + +
+ )) + )} + + ); +}; diff --git a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx index 48429eccc..f51098d0b 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewList/ChatViewList.tsx @@ -25,6 +25,7 @@ import { ThemeContext } from '../theme/ThemeProvider'; // Assets // Interfaces & Types +import { IReactionsForChatMessages } from '../../../types'; import { Group, IChatViewListProps } from '../exportedTypes'; import { IChatTheme } from '../theme'; import { IChatInfoResponse } from '../types'; @@ -51,6 +52,8 @@ const CHAT_STATUS = { INVALID_CHAT: 'Invalid chatId', }; +const SCROLL_LIMIT = 25; + // Exported Interfaces & Types // Exported Functions @@ -68,11 +71,16 @@ export const ChatViewList: React.FC = (options: IChatViewLis // const [chatStatusText, setChatStatusText] = useState(''); const [messages, setMessages] = useState([]); + const [reactions, setReactions] = useState({}); + const { historyMessages, historyLoading: messageLoading } = useFetchMessageUtilities(); - const listInnerRef = useRef(null); + const scrollRef = useRef(null); const [stopPagination, setStopPagination] = useState(false); const { fetchChat } = useFetchChat(); + // keep tab on singular action id, useful to ensure only one action takes place + const [singularActionId, setSingularActionId] = useState(null); + // for stream const { chatStream, @@ -87,7 +95,7 @@ export const ChatViewList: React.FC = (options: IChatViewLis const dates = new Set(); // Primary Hook that fetches and sets ChatInfo which then fetches and sets UserInfo - // Which then calls await getMessagesCall(); to fetch messages + // Which then calls await fetchChatMessages(); to fetch messages useEffect(() => { (async () => { if (!user) return; @@ -133,15 +141,143 @@ export const ChatViewList: React.FC = (options: IChatViewLis }; }, [chatId, user]); - // When loading is done + // When loading is done - fetch chat messages useEffect(() => { if (initialized.loading) return; (async function () { - await getMessagesCall(); + await fetchChatMessages(); })(); }, [initialized.loading]); + // when chat messages are changed or chat reactions are changed + useEffect(() => { + const checkForScrollAndFetchMessages = async () => { + if ( + !initialized.loading && + scrollRef && + scrollRef?.current && + scrollRef?.current?.parentElement && + !messageLoading && + !stopPagination + ) { + console.debug( + 'UIWeb::ChatViewList::useEffect[messages, reactions]::Checking if we need to load more chats::', + messages, + reactions, + scrollRef.current.clientHeight, + SCROLL_LIMIT, + scrollRef.current.parentElement.clientHeight, + scrollRef.current.clientHeight + SCROLL_LIMIT < scrollRef.current.parentElement.clientHeight + ); + + if (scrollRef.current.clientHeight + SCROLL_LIMIT < scrollRef.current.parentElement.clientHeight) { + await fetchChatMessages(); + } + } + }; + + // new messages are loaded, calculate new top and adjust since render is done + if (scrollRef.current) { + const content = scrollRef.current; + + const oldScrollHeight = parseInt(content.getAttribute('data-old-scroll-height') || '0', 10); // Old scroll height before messages are added + const newScrollHeight = content.scrollHeight; // New scroll height after messages are added + const scrollHeightDifference = newScrollHeight - oldScrollHeight; // Calculate how much the scroll height has increased plus some variance for spinner + + // Adjust the scroll position by the difference in scroll height to maintain the same view + content.scrollTop += scrollHeightDifference; + } + + // check and fetch messages + checkForScrollAndFetchMessages(); + }, [messages]); + + // Smart Scrolling + // Scroll to bottom if user hasn't scrolled or if scroll is at bottom + // Else leave the scroll as it is + // to get scroll lock + const onScroll = async () => { + if (scrollRef.current) { + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + + let scrollLocked = scrollRef.current.getAttribute('data-scroll-locked') === 'true' ? true : false; + const programmableScroll = scrollRef.current.getAttribute('data-programmable-scroll') === 'true' ? true : false; + const programmableScrollTop = scrollRef.current.getAttribute('data-programmable-scroll-top') || 0; + + // user has scrolled away so scroll should not be locked + if (programmableScroll === false) { + scrollLocked = false; + } + + // lock scroll if user is at bottom + if (scrollTop + clientHeight >= scrollHeight - 10) { + // add 10 for variability + scrollLocked = true; + } + + 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()); + + if (scrollTop === 0) { + const content = scrollRef.current; + const oldScrollHeight = content.scrollHeight; // Capture the old scroll height before new messages are added + scrollRef.current.setAttribute('data-old-scroll-height', oldScrollHeight.toString()); + + await fetchChatMessages(); + } + } + }; + + // To enable smart scrolling when content height gets adjsuted + const chatContainerRef = useRef(null); + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { height } = entry.contentRect; + + 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 + ); + + if (height !== 0 && scrollLocked) { + // update programmable-scroll attribute + scrollRef.current.setAttribute('data-programmable-scroll', 'true'); + scrollRef.current?.scrollTo(0, scrollRef.current?.scrollHeight); + + // update programmable-scroll attribute after timeout of 1000ms for previews to render + setTimeout(() => { + if (scrollRef.current) { + scrollRef.current.setAttribute('data-programmable-scroll', 'false'); + } + }, 1000); + } + } + } + }); + + if (chatContainerRef.current) { + resizeObserver.observe(chatContainerRef.current); + } + + return () => resizeObserver.disconnect(); // clean up + }, [chatContainerRef.current]); + // Change listtype to 'CHATS' and hidden to false when chatAcceptStream is received useEffect(() => { if (Object.keys(chatAcceptStream || {}).length > 0 && chatAcceptStream.constructor === Object) { @@ -156,6 +292,8 @@ export const ChatViewList: React.FC = (options: IChatViewLis return () => clearTimeout(timer); } + + return () => {}; }, [chatAcceptStream, participantJoinStream]); // Change listtype to 'UINITIALIZED' and hidden to true when participantRemoveStream or participantLeaveStream is received @@ -172,16 +310,12 @@ export const ChatViewList: React.FC = (options: IChatViewLis useEffect(() => { if (Object.keys(chatStream || {}).length > 0 && chatStream.constructor === Object) { transformSteamMessage(chatStream); - // setChatStatusText(''); - scrollToBottom(); } }, [chatStream]); useEffect(() => { if (Object.keys(chatRequestStream || {}).length > 0 && chatRequestStream.constructor === Object) { transformSteamMessage(chatRequestStream); - // setChatStatusText(''); - scrollToBottom(); } }, [chatRequestStream]); @@ -194,97 +328,112 @@ export const ChatViewList: React.FC = (options: IChatViewLis const transformedMessage = transformStreamToIMessageIPFSWithCID(item); if (messages && messages.length) { const newChatViewList = appendUniqueMessages(messages, [transformedMessage], false); - setFilteredMessages(newChatViewList); + filterChatMessages(newChatViewList); } else { - setFilteredMessages([transformedMessage]); + filterChatMessages([transformedMessage]); } } }; - useEffect(() => { - if (messages && messages?.length && messages?.length <= limit) { - // setChatStatusText(''); - scrollToBottom(); - } - }, [messages]); - - //methods - const scrollToBottom = () => { - requestAnimationFrame(() => { - if (listInnerRef.current) { - listInnerRef.current.scrollTop = listInnerRef.current.scrollHeight; - } - }); - }; - - const onScroll = async () => { - if (listInnerRef.current) { - const { scrollTop } = listInnerRef.current; - if (scrollTop === 0) { - const content = listInnerRef.current; - const curScrollPos = content.scrollTop; - const oldScroll = content.scrollHeight - content.clientHeight; - - await getMessagesCall(); - - const newScroll = content.scrollHeight - content.clientHeight; - content.scrollTop = newScroll - oldScroll; - } - } - }; - - const getMessagesCall = async () => { - let reference = null; - let stopFetchingChats = false; - if (messages && messages?.length) { - reference = messages[0].link; - if (!reference) { - stopFetchingChats = true; - setStopPagination(stopFetchingChats); - } - } - - if (user && !stopFetchingChats) { + const fetchChatMessages = async () => { + if (user && !stopPagination && !messageLoading) { + const reference = messages && messages?.length ? messages[0].link : null; const chatHistory = await historyMessages({ limit: limit, chatId: chatId, reference, }); - if (chatHistory?.length) { + if (chatHistory && chatHistory?.length) { const reversedChatHistory = chatHistory?.reverse(); if (messages && messages?.length) { const newChatViewList = appendUniqueMessages(messages, reversedChatHistory, true); - setFilteredMessages(newChatViewList as IMessageIPFSWithCID[]); + filterChatMessages(newChatViewList as IMessageIPFSWithCID[]); } else { - setFilteredMessages(reversedChatHistory as IMessageIPFSWithCID[]); + filterChatMessages(reversedChatHistory as IMessageIPFSWithCID[]); + } + } + + // check and stop pagination if user is readmode and chatInfo visibility is false + if ( + (user && user.readmode() && initialized.chatInfo?.meta?.visibility === false) || + initialized.chatInfo?.meta?.group === false + ) { + // not a public group + setStopPagination(true); + } + + // check and stop pagination if all chats are fetched + if (!chatHistory || chatHistory?.length < limit) { + setStopPagination(true); + } + } + }; + + const processChatReactions = (messageList: Array) => { + const reactionMessages = reactions; + + for (const message of messageList) { + if (message.messageType === 'Reaction') { + const reaction = message as IMessageIPFSWithCID; + + // TODO: This should be present as an interface in the restapi package + const reference = (reaction as any).messageObj?.reference ?? ''; + + if (!reactionMessages[reference]) { + reactionMessages[reference] = []; } + // Push the reaction directly into the array + reactionMessages[reference].push(reaction); } } + + return reactionMessages; }; - const setFilteredMessages = (messageList: Array) => { - const updatedMessageList = messageList.filter((msg) => !chatFilterList.includes(msg.cid)); + const filterChatMessages = (messageList: Array) => { + // filter duplicates + const uniqueMessagesList = messageList.filter((msg) => !chatFilterList.includes(msg.cid)); - if (updatedMessageList && updatedMessageList.length) { - setMessages([...updatedMessageList]); + // remove reactions into reactions + const reactionMessages = processChatReactions(uniqueMessagesList); + + console.debug( + `UIWeb::ChatViewList::filterChatMessages::uniqueMessageList::${new Date().toISOString()}`, + uniqueMessagesList + ); + console.debug( + `UIWeb::ChatViewList::filterChatMessages::reactionMessages::${new Date().toISOString()}`, + reactionMessages + ); + + if (uniqueMessagesList && uniqueMessagesList.length) { + setMessages([...uniqueMessagesList]); + } + + if (reactionMessages && reactionMessages.length) { + // deep copy to update + setReactions(JSON.parse(JSON.stringify(reactionMessages))); } }; type RenderDataType = { chat: IMessageIPFS; dateNum: string; + uid: string; }; - const renderDate = ({ chat, dateNum }: RenderDataType) => { + const renderDate = ({ chat, dateNum, uid }: RenderDataType) => { const timestampDate = dateToFromNowDaily(chat.timestamp as number); dates.add(dateNum); return ( {timestampDate} @@ -293,12 +442,15 @@ export const ChatViewList: React.FC = (options: IChatViewLis return ( = (options: IChatViewLis e.stopPropagation(); if (!stopPagination) onScroll(); }} + onClick={() => { + // cancel any singular action + setSingularActionId(null); + }} >
= (options: IChatViewLis flexDirection="column" justifyContent="start" width="100%" + ref={chatContainerRef} blur={initialized.isHidden} > {messages && messages?.map((chat: IMessageIPFS, index: number) => { + // If message is a reaction, then skip it + if (chat?.messageType === 'Reaction') return null; + const dateNum = moment(chat.timestamp).format('L'); // TODO: This is a hack as chat.fromDID is converted with eip to match with user.account creating a bug for omnichain const position = pCAIP10ToWallet(chat.fromDID)?.toLowerCase() !== pCAIP10ToWallet(user?.account ?? '')?.toLowerCase() ? 0 : 1; + + // define zIndex, really big number minus 1 + const uid = `${999999999 - index}`; + return ( <> - {dates.has(dateNum) ? null : renderDate({ chat, dateNum })} + {dates.has(dateNum) ? null : renderDate({ chat, dateNum, uid: uid })}
+ {/* TODO: Remove decryptedMessagePayload in v2 component */}
@@ -410,7 +582,6 @@ const ChatViewListCard = styled(Section)` } overscroll-behavior: contain; - scroll-behavior: smooth; `; const ChatViewListCardInner = styled(Section)` diff --git a/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupType.tsx b/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupType.tsx index 9a6f8691f..360243f0f 100644 --- a/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupType.tsx +++ b/packages/uiweb/src/lib/components/chat/CreateGroup/CreateGroupType.tsx @@ -10,10 +10,7 @@ import { Button } from '../reusables'; import { ModalHeaderProps } from './CreateGroupModal'; import { GroupTypeState } from './CreateGroupModal'; import { ThemeContext } from '../theme/ThemeProvider'; -import { - ConditionType, - CriteriaStateType, -} from '../types/tokenGatedGroupCreationType'; +import { ConditionType, CriteriaStateType } from '../types/tokenGatedGroupCreationType'; import ConditionsComponent from './ConditionsComponent'; import { OperatorContainer } from './OperatorContainer'; import { SelectedCriteria } from '../../../hooks/chat/useCriteriaState'; @@ -41,12 +38,7 @@ interface AddConditionProps { criteriaState: CriteriaStateType; } -const AddConditionSection = ({ - heading, - subHeading, - handleNext, - criteriaState, -}: AddConditionProps) => { +const AddConditionSection = ({ heading, subHeading, handleNext, criteriaState }: AddConditionProps) => { const theme = useContext(ThemeContext); const generateMapping = () => { @@ -57,8 +49,17 @@ const AddConditionSection = ({ }; return ( -
-
+
+
{criteriaState.entryOptionsDataArray.length > 1 && ( -
+
{ @@ -87,10 +88,7 @@ const AddConditionSection = ({ )} { criteriaState.deleteEntryOptionsDataArray(idx); }} @@ -135,11 +133,13 @@ export const CreateGroupType = ({ setGroupInputDetails, groupInputDetails, }: ModalHeaderProps & GroupTypeState) => { - const theme = useContext(ThemeContext); return ( -
+
-
+
({ ...prevState, groupEncryptionType: newEl, - })) + })); } console.debug(newEl); }} @@ -169,20 +173,21 @@ export const CreateGroupType = ({ setChecked ? setChecked(!checked) : null} + onToggle={() => (setChecked ? setChecked(!checked) : null)} /> {checked && ( -
+
{ if (handleNext) { - criteriaStateManager.setSelectedCriteria( - SelectedCriteria.ENTRY - ); + criteriaStateManager.setSelectedCriteria(SelectedCriteria.ENTRY); handleNext(); } }} @@ -191,9 +196,7 @@ export const CreateGroupType = ({ { if (handleNext) { - criteriaStateManager.setSelectedCriteria( - SelectedCriteria.CHAT - ); + criteriaStateManager.setSelectedCriteria(SelectedCriteria.CHAT); handleNext(); } }} @@ -204,18 +207,27 @@ export const CreateGroupType = ({ )}
-
- - +
); }; //styles -const ScrollSection = styled(Section) <{ theme: IChatTheme }>` +const ScrollSection = styled(Section)<{ theme: IChatTheme }>` &::-webkit-scrollbar-thumb { background: ${(props) => props.theme.scrollbarColor}; border-radius: 10px; @@ -227,4 +239,4 @@ const ScrollSection = styled(Section) <{ theme: IChatTheme }>` &::-webkit-scrollbar { width: 4px; } -`; \ No newline at end of file +`; diff --git a/packages/uiweb/src/lib/components/chat/CreateGroup/DefineCondition.tsx b/packages/uiweb/src/lib/components/chat/CreateGroup/DefineCondition.tsx index 422b89913..010b0f467 100644 --- a/packages/uiweb/src/lib/components/chat/CreateGroup/DefineCondition.tsx +++ b/packages/uiweb/src/lib/components/chat/CreateGroup/DefineCondition.tsx @@ -16,12 +16,7 @@ import { IChatTheme } from '../theme'; import { device } from '../../../config'; import { OPERATOR_OPTIONS_INFO } from '../constants'; -export const DefineCondtion = ({ - onClose, - handlePrevious, - handleNext, - criteriaStateManager, -}: ModalHeaderProps) => { +export const DefineCondtion = ({ onClose, handlePrevious, handleNext, criteriaStateManager }: ModalHeaderProps) => { const theme = useContext(ThemeContext); const isMobile = useMediaQuery(device.mobileL); @@ -32,10 +27,7 @@ export const DefineCondtion = ({ criteriaState.selectedRules.length < 1 ? theme.backgroundColor?.buttonDisableBackground : theme.backgroundColor?.buttonBackground, - color: - criteriaState.selectedRules.length < 1 - ? theme.textColor?.buttonDisableText - : theme.textColor?.buttonText, + color: criteriaState.selectedRules.length < 1 ? theme.textColor?.buttonDisableText : theme.textColor?.buttonText, }; const verifyAndDoNext = () => { @@ -43,26 +35,19 @@ export const DefineCondtion = ({ }; const getRules = () => { - return [ - [{ operator: criteriaState.entryRuleTypeCondition }], - ...criteriaState.selectedRules.map((el) => [el]), - ]; + return [[{ operator: criteriaState.entryRuleTypeCondition }], ...criteriaState.selectedRules.map((el) => [el])]; }; // set state for edit condition useEffect(() => { if (criteriaState.isCondtionUpdateEnabled()) { criteriaState.setEntryRuleTypeCondition( - criteriaState.entryOptionTypeArray[ - criteriaState.entryOptionsDataArrayUpdate - ] + criteriaState.entryOptionTypeArray[criteriaState.entryOptionsDataArrayUpdate] ); if (criteriaState.selectedRules.length === 0) { criteriaState.setSelectedRule([ - ...criteriaState.entryOptionsDataArray[ - criteriaState.entryOptionsDataArrayUpdate - ], + ...criteriaState.entryOptionsDataArray[criteriaState.entryOptionsDataArrayUpdate], ]); } } else { @@ -77,29 +62,23 @@ export const DefineCondtion = ({ width={isMobile ? '300px' : '400px'} > -
- - {criteriaState.selectedRules.length > 1 && ( -
- { - criteriaState.setEntryRuleTypeCondition( - newEl as keyof typeof OPERATOR_OPTIONS_INFO - ); - }} - /> -
- )} - {criteriaState.selectedRules.length > 0 && + {criteriaState.selectedRules.length > 1 && ( +
+ { + criteriaState.setEntryRuleTypeCondition(newEl as keyof typeof OPERATOR_OPTIONS_INFO); + }} + /> +
+ )} + {criteriaState.selectedRules.length > 0 && ( + - } - - - -
- {!criteriaState.selectedRules.length && + )} + + +
+ {!criteriaState.selectedRules.length && ( + You must add at least 1 criteria to enable gating - } - - + */} - +
); }; @@ -157,4 +145,4 @@ const ConditionSection = styled(Section)<{ theme: IChatTheme }>` &::-webkit-scrollbar { width: 4px; } -`; \ No newline at end of file +`; diff --git a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx index f44e8c274..2c6a75eb6 100644 --- a/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx +++ b/packages/uiweb/src/lib/components/chat/MessageInput/MessageInput.tsx @@ -14,7 +14,7 @@ import useGroupMemberUtilities from '../../../hooks/chat/useGroupMemberUtilities import usePushSendMessage from '../../../hooks/chat/usePushSendMessage'; import useVerifyAccessControl from '../../../hooks/chat/useVerifyAccessControl'; import { AttachmentIcon } from '../../../icons/Attachment'; -import { EmojiIcon } from '../../../icons/Emoji'; +import { EmojiCircleIcon } from '../../../icons/PushIcons'; import { GifIcon } from '../../../icons/Gif'; import OpenLink from '../../../icons/OpenLink'; import { SendCompIcon } from '../../../icons/SendCompIcon'; @@ -436,7 +436,6 @@ export const MessageInput: React.FC = ({ > = ({ alignSelf="end" onClick={() => setShowEmojis(!showEmojis)} > - + )} {showEmojis && (
= ({ {gifOpen && (
= [ - { - heading: 'ALL', - value: 'all', - }, - { - heading: 'ANY', - value: 'any', - }, - { - heading: 'SPECIFIC', - value: 'specific', - }, - ]; + owner: 'Only Owner can invite', + admin: 'Only Admin can invite', +}; - export const OPERATOR_OPTIONS = [ - { - heading: 'Any', - value: 'any', - }, - { - heading: 'All', - value: 'all', - } -] +export const GUILD_COMPARISON_OPTIONS: Array = [ + { + heading: 'ALL', + value: 'all', + }, + { + heading: 'ANY', + value: 'any', + }, + { + heading: 'SPECIFIC', + value: 'specific', + }, +]; +export const OPERATOR_OPTIONS = [ + { + heading: 'Any', + value: 'any', + }, + { + heading: 'All', + value: 'all', + }, +]; export const OPERATOR_OPTIONS_INFO = { - any:{ - head:'Any one', - tail:'of the following criteria must be true' - }, - all:{ - head:'All', - tail:'of the following criteria must be true' - } -} ; - + any: { + head: 'Any one', + tail: 'of the following criteria must be true', + }, + all: { + head: 'All', + tail: 'of the following criteria must be true', + }, +}; export const ACCESS_TYPE_TITLE = { ENTRY: { heading: 'Conditions to Join', - subHeading: 'Add a condition to join or leave it open for everyone', + subHeading: 'Add a condition to join or remove all conditions for no rules', }, CHAT: { heading: 'Conditions to Chat', - subHeading: 'Add a condition to join or leave it open for everyone', + subHeading: 'Add a condition to chat or leave it empty for no rules', }, }; diff --git a/packages/uiweb/src/lib/components/chat/helpers/helper.ts b/packages/uiweb/src/lib/components/chat/helpers/helper.ts index b31f978d3..f5e7460ce 100644 --- a/packages/uiweb/src/lib/components/chat/helpers/helper.ts +++ b/packages/uiweb/src/lib/components/chat/helpers/helper.ts @@ -215,7 +215,7 @@ export const transformStreamToIMessageIPFSWithCID: (item: any) => IMessageIPFSWi fromDID: item?.from, toDID: item?.to[0], messageType: item?.message?.type, - messageObj: { content: item?.message?.content }, + messageObj: { content: item?.message?.content, reference: item?.message?.reference }, sigType: item?.raw?.sigType || '', link: `previous:v2${item?.reference}`, timestamp: parseInt(item?.timestamp), diff --git a/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx b/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx index d11c75c3b..795bc74fa 100644 --- a/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx +++ b/packages/uiweb/src/lib/components/chat/reusables/ProfileContainer.tsx @@ -2,11 +2,13 @@ import { useEffect, useRef, useState } from 'react'; // External Packages +import styled from 'styled-components'; // Internal Compoonents import { copyToClipboard, pCAIP10ToWallet } from '../../../helpers'; import { createBlockie } from '../../../helpers/blockies'; import { Div, Image, Section, Span, Tooltip } from '../../reusables'; +import { device } from '../../../config'; // Internal Configs @@ -130,7 +132,7 @@ export const ProfileContainer = ({ theme, member, copy, customStyle, loading }: }} className={loading ? 'skeleton' : ''} > - {member?.name && member?.web3Name ? `${member?.web3Name} | ${member.abbrRecipient}` : member.abbrRecipient} - + {copy && copyText && (
); }; + +const RecipientSpan = styled(Span)` + text-wrap: nowrap; + + @media ${device.mobileL} { + text-wrap: pretty; + } +`; diff --git a/packages/uiweb/src/lib/components/chat/theme/index.ts b/packages/uiweb/src/lib/components/chat/theme/index.ts index e2c114375..a45dbaef3 100644 --- a/packages/uiweb/src/lib/components/chat/theme/index.ts +++ b/packages/uiweb/src/lib/components/chat/theme/index.ts @@ -24,6 +24,8 @@ interface IBorder { chatWidget?: string; chatSentBubble?: string; chatReceivedBubble?: string; + reactionsBorder?: string; + reactionsHoverBorder?: string; } interface IBorderRadius { chatViewComponent?: string; @@ -35,6 +37,9 @@ interface IBorderRadius { chatPreview?: string; userProfile?: string; chatWidget?: string; + chatBubbleBorderRadius?: string; + reactionsPickerBorderRadius?: string; + reactionsBorderRadius?: string; } interface IPadding { @@ -46,6 +51,8 @@ interface IPadding { messageInputPadding?: string; chatBubbleSenderPadding?: string; chatBubbleReceiverPadding?: string; + reactionsPickerPadding?: string; + reactionsPadding?: string; } interface IMargin { @@ -200,6 +207,9 @@ export const lightChatTheme: IChatTheme = { chatPreview: '24px', userProfile: '0px', chatWidget: '24px', + chatBubbleBorderRadius: '12px', + reactionsPickerBorderRadius: '12px', + reactionsBorderRadius: '24px', }, padding: { @@ -211,6 +221,8 @@ export const lightChatTheme: IChatTheme = { messageInputPadding: '0px', chatBubbleSenderPadding: '0px', chatBubbleReceiverPadding: '0px', + reactionsPickerPadding: '4px', + reactionsPadding: '4px 8px', }, margin: { @@ -220,8 +232,8 @@ export const lightChatTheme: IChatTheme = { chatViewMargin: '0px', chatViewListMargin: '0px 0px 0px 10px', messageInputMargin: '2px 10px 10px 10px', - chatBubbleSenderMargin: '8px 8px 8px 0px', - chatBubbleReceiverMargin: '8px 0px 8px 8px', + chatBubbleSenderMargin: '16px 8px', + chatBubbleReceiverMargin: '16px 8px', }, backgroundColor: { @@ -311,6 +323,8 @@ export const lightChatTheme: IChatTheme = { chatWidget: '1px solid #E4E8EF', chatReceivedBubble: 'none', chatSentBubble: 'none', + reactionsBorder: '1px solid transparent', + reactionsHoverBorder: '1px solid #DFDFDF', }, iconColor: { @@ -367,6 +381,9 @@ export const darkChatTheme: IChatTheme = { chatPreview: '24px', userProfile: '0px', chatWidget: '24px', + chatBubbleBorderRadius: '12px', + reactionsPickerBorderRadius: '12px', + reactionsBorderRadius: '24px', }, padding: { @@ -378,6 +395,8 @@ export const darkChatTheme: IChatTheme = { messageInputPadding: '0px', chatBubbleSenderPadding: '0px', chatBubbleReceiverPadding: '0px', + reactionsPickerPadding: '4px', + reactionsPadding: '4px 8px', }, margin: { @@ -387,8 +406,8 @@ export const darkChatTheme: IChatTheme = { chatViewMargin: '0px', chatViewListMargin: '0px 0px 0px 10px', messageInputMargin: '2px 10px 10px 10px', - chatBubbleSenderMargin: '8px 8px 8px 0px', - chatBubbleReceiverMargin: '8px 0px 8px 8px', + chatBubbleSenderMargin: '16px 8px', + chatBubbleReceiverMargin: '16px 8px', }, backgroundColor: { @@ -477,6 +496,8 @@ export const darkChatTheme: IChatTheme = { userProfile: 'none', chatReceivedBubble: 'none', chatSentBubble: 'none', + reactionsBorder: '1px solid transparent', + reactionsHoverBorder: '1px solid #282A2E', }, iconColor: { diff --git a/packages/uiweb/src/lib/components/chatAndNotification/modal/messageBox/typebar/Typebar.tsx b/packages/uiweb/src/lib/components/chatAndNotification/modal/messageBox/typebar/Typebar.tsx index fac3c793b..e33c70af5 100644 --- a/packages/uiweb/src/lib/components/chatAndNotification/modal/messageBox/typebar/Typebar.tsx +++ b/packages/uiweb/src/lib/components/chatAndNotification/modal/messageBox/typebar/Typebar.tsx @@ -2,15 +2,13 @@ import type { ChangeEvent } from 'react'; import React, { useState, useContext, useRef, useEffect } from 'react'; import styled from 'styled-components'; import { Div, Section } from '../../../../reusables/sharedStyling'; -import { EmojiIcon } from '../../../../../icons/Emoji'; +import { EmojiCircleIcon } from '../../../../../icons/PushIcons'; +import { ThemeContext } from '../../../../chat/theme/ThemeProvider'; import { SendIcon } from '../../../../../icons/Send'; import { GifIcon } from '../../../../../icons/Gif'; import { AttachmentIcon } from '../../../../../icons/Attachment'; import usePushSendMessage from '../../../../../hooks/chatAndNotification/chat/usePushSendMessage'; -import { - ChatAndNotificationMainContext, - ChatMainStateContext, -} from '../../../../../context'; +import { ChatAndNotificationMainContext, ChatMainStateContext } from '../../../../../context'; import useFetchRequests from '../../../../../hooks/chatAndNotification/chat/useFetchRequests'; import { Spinner } from '../../../../reusables/Spinner'; import type { EmojiClickData } from 'emoji-picker-react'; @@ -34,6 +32,9 @@ type TypebarPropType = { const requestLimit = 30; const page = 1; export const Typebar: React.FC = ({ scrollToBottom }) => { + // get theme + const theme = useContext(ThemeContext); + const [typedMessage, setTypedMessage] = useState(''); const [showEmojis, setShowEmojis] = useState(false); const [gifOpen, setGifOpen] = useState(false); @@ -41,10 +42,7 @@ export const Typebar: React.FC = ({ scrollToBottom }) => { const fileUploadInputRef = React.useRef(null); const { selectedChatId, chatsFeed, setSearchedChats, requestsFeed } = useContext(ChatMainStateContext); - const { newChat, setNewChat } = - useContext( - ChatAndNotificationMainContext - ); + const { newChat, setNewChat } = useContext(ChatAndNotificationMainContext); const { sendMessage, loading } = usePushSendMessage(); const [filesUploading, setFileUploading] = useState(false); const { fetchRequests } = useFetchRequests(); @@ -66,14 +64,9 @@ export const Typebar: React.FC = ({ scrollToBottom }) => { }); scrollToBottom(); - if ( - chatsFeed[selectedChatId as string] || - requestsFeed[selectedChatId as string] - ) - setSearchedChats(null); + if (chatsFeed[selectedChatId as string] || requestsFeed[selectedChatId as string]) setSearchedChats(null); if (newChat) setNewChat(false); - if (!chatsFeed[selectedChatId as string]) - fetchRequests({ page, requestLimit }); + if (!chatsFeed[selectedChatId as string]) fetchRequests({ page, requestLimit }); } catch (error) { console.log(error); //handle error @@ -102,20 +95,14 @@ export const Typebar: React.FC = ({ scrollToBottom }) => { } }; - const uploadFile = async ( - e: ChangeEvent - ): Promise => { + const uploadFile = async (e: ChangeEvent): Promise => { if (!(e.target instanceof HTMLInputElement)) { return; } if (!e.target.files) { return; } - if ( - e.target && - (e.target as HTMLInputElement).files && - ((e.target as HTMLInputElement).files as FileList).length - ) { + if (e.target && (e.target as HTMLInputElement).files && ((e.target as HTMLInputElement).files as FileList).length) { const file: File = e.target.files[0]; if (file) { try { @@ -170,7 +157,10 @@ export const Typebar: React.FC = ({ scrollToBottom }) => { alignItems="center" justifyContent="space-between" > -
+
= ({ scrollToBottom }) => { alignSelf="end" onClick={() => setShowEmojis(!showEmojis)} > - +
{showEmojis && ( @@ -209,7 +202,7 @@ export const Typebar: React.FC = ({ scrollToBottom }) => { rows={1} />
- +
= ({ scrollToBottom }) => { )} {(loading || filesUploading) && ( -
+
)} diff --git a/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx b/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx index 5ab16ba78..806fb13b3 100644 --- a/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx +++ b/packages/uiweb/src/lib/components/reusables/sharedStyling.tsx @@ -44,6 +44,7 @@ type SectionStyleProps = { whiteSpace?: string; visibility?: string; zIndex?: string; + fontSize?: string; }; export const Section = styled.div` @@ -79,6 +80,7 @@ export const Section = styled.div` z-index: ${(props) => props.zIndex || '0'}; white-space: ${(props) => props.whiteSpace || 'normal'}; border: ${(props) => props.border || 'initial'}; + font-size: ${(props) => props.fontSize || 'initial'}; &.skeleton { > * { @@ -334,8 +336,8 @@ type ButtonStyleProps = { }; export const Button = styled.button` - display: ${(props) => props.display || 'initial'}; - line-height: ${(props) => props.lineHeight || '26px'}; + display: ${(props) => props.display || 'flex'}; + line-height: ${(props) => props.lineHeight || 'normal'}; flex: ${(props) => props.flex || 'initial'}; flex-direction: ${(props) => props.flexDirection || 'row'}; align-self: ${(props) => props.alignSelf || 'auto'}; @@ -385,7 +387,7 @@ export const Button = styled.button` } &:hover { - border: ${(props) => props.hoverBorder || 'inherit'}; + border: ${(props) => props.hoverBorder || 'none'}; & svg > path { stroke: ${(props) => props.hoverSVGPathStroke || 'auto'}; @@ -397,10 +399,11 @@ export const Button = styled.button` } &:hover:after { - opacity: 0.08; + opacity: ${(props) => (props.hoverBackground ? 1 : 0.08)}; } + &:active:after { - opacity: 0.15; + opacity: ${(props) => (props.hoverBackground ? 1 : 0.15)}; } & > div { diff --git a/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx b/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx index 491669807..78f0ae73d 100644 --- a/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx +++ b/packages/uiweb/src/lib/dataProviders/ChatDataProvider.tsx @@ -221,10 +221,10 @@ export const ChatUIProvider = ({ // To setup debug parameters useEffect(() => { if (debug) { - console.debug('UIWeb::ChatDataProvider::Debug mode enabled'); + console.debug('UIWeb::ChatDataProvider::Debug mode enabled, console logs are enabled'); enableConsole(); } else { - console.warn('UIWeb::ChatDataProvider::Debug mode disabled'); + console.warn('UIWeb::ChatDataProvider::Debug mode is turned off, console logs are suppressed'); disableConsole(); } }, [debug]); diff --git a/packages/uiweb/src/lib/helpers/address.ts b/packages/uiweb/src/lib/helpers/address.ts index 86554855f..cc44ce5df 100644 --- a/packages/uiweb/src/lib/helpers/address.ts +++ b/packages/uiweb/src/lib/helpers/address.ts @@ -96,6 +96,10 @@ export const resolveWeb3Name = async (address: string, user: PushAPI | undefined } else { try { const udResolver = getUdResolver(user ? user.env : CONSTANTS.ENV.PROD); + if (!udResolver) { + throw new Error('UIWeb::helpers::address::resolveWeb3Name::Error in UD resolver'); + } + // attempt reverse resolution on provided address const udName = await udResolver.reverse(checksumWallet); if (udName) { @@ -111,7 +115,7 @@ export const resolveWeb3Name = async (address: string, user: PushAPI | undefined console.error('UIWeb::helpers::address::resolveWeb3Name::Error in resolving via ENS', err); } - console.debug(`UIWeb::helpers::address::resolveWeb3Name::Wallet: ${checksumWallet} resolved to ${result}`); + // console.debug(`UIWeb::helpers ::address::resolveWeb3Name::Wallet: ${checksumWallet} resolved to ${result}`); return result; }; diff --git a/packages/uiweb/src/lib/helpers/chat/user.ts b/packages/uiweb/src/lib/helpers/chat/user.ts index 7c3a694e6..4414fd60f 100644 --- a/packages/uiweb/src/lib/helpers/chat/user.ts +++ b/packages/uiweb/src/lib/helpers/chat/user.ts @@ -1,12 +1,8 @@ import type { Env, IUser } from '@pushprotocol/restapi'; -import { - - ProfilePicture, -} from '../../config'; +import { ProfilePicture } from '../../config'; import { ethers } from 'ethers'; import { getUdResolver } from '../udResolver'; - export const displayDefaultUser = ({ caip10 }: { caip10: string }): IUser => { const userCreated: IUser = { did: caip10, @@ -61,6 +57,9 @@ export const getUnstoppableName = async ( ) => { // Unstoppable Domains resolution library const udResolver = getUdResolver(env); + if (!udResolver) { + return null; + } // attempt reverse resolution on provided address let udName = await udResolver.reverse(checksumWallet); @@ -71,5 +70,3 @@ export const getUnstoppableName = async ( } return udName; }; - - diff --git a/packages/uiweb/src/lib/helpers/udResolver.ts b/packages/uiweb/src/lib/helpers/udResolver.ts index 47071a3c5..9670443fd 100644 --- a/packages/uiweb/src/lib/helpers/udResolver.ts +++ b/packages/uiweb/src/lib/helpers/udResolver.ts @@ -3,7 +3,7 @@ import Resolution from '@unstoppabledomains/resolution'; import { ethers } from 'ethers'; import { allowedNetworks, InfuraAPIKey, NETWORK_DETAILS } from '../config'; -export const getUdResolver = (env: Env): Resolution => { +export const getUdResolver = (env: Env): Resolution | undefined => { try { const l1ChainId = allowedNetworks[env].includes(1) ? 1 : 5; const l2ChainId = allowedNetworks[env].includes(137) ? 137 : 80002; @@ -25,5 +25,6 @@ export const getUdResolver = (env: Env): Resolution => { }); } catch (e) { console.debug(`Errored:UIWeb::helpers::getUdResolver::UD doesnot provide support for the network`); + return undefined; } }; diff --git a/packages/uiweb/src/lib/helpers/utils.ts b/packages/uiweb/src/lib/helpers/utils.ts index 2adbb5e36..8cd145e13 100644 --- a/packages/uiweb/src/lib/helpers/utils.ts +++ b/packages/uiweb/src/lib/helpers/utils.ts @@ -19,6 +19,7 @@ import { getAddress } from './'; // Exported Functions +// Derive Chat Id export const deriveChatId = async (chatId: string, user: PushAPI | undefined): Promise => { // check if chatid: is appened, if so remove it if (chatId?.startsWith('chatid:')) { @@ -33,3 +34,9 @@ export const deriveChatId = async (chatId: string, user: PushAPI | undefined): P return chatId; }; + +export const isMessageEncrypted = (message: string) => { + if (!message) return false; + + return message.startsWith('U2FsdGVkX1'); +}; diff --git a/packages/uiweb/src/lib/hooks/chat/useFetchMessageUtilities.ts b/packages/uiweb/src/lib/hooks/chat/useFetchMessageUtilities.ts index dd2af7c26..0919c61aa 100644 --- a/packages/uiweb/src/lib/hooks/chat/useFetchMessageUtilities.ts +++ b/packages/uiweb/src/lib/hooks/chat/useFetchMessageUtilities.ts @@ -35,7 +35,6 @@ const useFetchMessageUtilities = () => { page: page, limit: limit, }); - console.debug(chats, 'chats from hook'); return chats; } catch (error: Error | any) { setChatListLoading(false); diff --git a/packages/uiweb/src/lib/hooks/chatAndNotification/chat/useFetchHistoryMessages.ts b/packages/uiweb/src/lib/hooks/chatAndNotification/chat/useFetchHistoryMessages.ts index 48f23c7d0..54c2d8e99 100644 --- a/packages/uiweb/src/lib/hooks/chatAndNotification/chat/useFetchHistoryMessages.ts +++ b/packages/uiweb/src/lib/hooks/chatAndNotification/chat/useFetchHistoryMessages.ts @@ -1,4 +1,3 @@ - import * as PushAPI from '@pushprotocol/restapi'; import type { IMessageIPFS } from '@pushprotocol/restapi'; import { Env } from '@pushprotocol/restapi'; @@ -7,65 +6,60 @@ import { Constants } from '../../../config'; import { ChatMainStateContext, ChatAndNotificationPropsContext } from '../../../context'; import type { ChatMainStateContextType } from '../../../context/chatAndNotification/chat/chatMainStateContext'; +interface HistoryMessagesParams { + threadHash: string; + limit?: number; +} - - interface HistoryMessagesParams { - threadHash: string; - limit?: number; - } - - -const useFetchHistoryMessages - = () => { +const useFetchHistoryMessages = () => { const [error, setError] = useState(); const [loading, setLoading] = useState(false); - const { chats,setChat,selectedChatId} = - useContext(ChatMainStateContext); - const { account, env,decryptedPgpPvtKey } = - useContext(ChatAndNotificationPropsContext); + const { chats, setChat, selectedChatId } = useContext(ChatMainStateContext); + const { account, env, decryptedPgpPvtKey } = useContext(ChatAndNotificationPropsContext); - const historyMessages = useCallback(async ({threadHash,limit = 10,}:HistoryMessagesParams) => { + const historyMessages = useCallback( + async ({ threadHash, limit = 10 }: HistoryMessagesParams) => { + setLoading(true); + try { + const chatHistory: IMessageIPFS[] = await PushAPI.chat.history({ + threadhash: threadHash, + account: account, + toDecrypt: decryptedPgpPvtKey ? true : false, + pgpPrivateKey: String(decryptedPgpPvtKey), + limit: limit, + env: env, + }); - setLoading(true); - try { - const chatHistory:IMessageIPFS[] = await PushAPI.chat.history({ - threadhash: threadHash, - account: account, - toDecrypt: decryptedPgpPvtKey ? true : false, - pgpPrivateKey: String(decryptedPgpPvtKey), - limit: limit, - env: env + chatHistory.reverse(); + if (chats.get(selectedChatId as string)) { + const uniqueMap: { [timestamp: number]: IMessageIPFS } = {}; + const messages = Object.values( + [...chatHistory, ...chats.get(selectedChatId as string)!.messages].reduce((uniqueMap, message) => { + if (message.timestamp && !uniqueMap[message.timestamp]) { + uniqueMap[message.timestamp] = message; + } + return uniqueMap; + }, uniqueMap) + ); + setChat(selectedChatId as string, { + messages: messages, + lastThreadHash: chatHistory[0].link, }); - console.log(chatHistory) - chatHistory.reverse(); - if (chats.get(selectedChatId as string)) { - const uniqueMap: { [timestamp: number]: IMessageIPFS } = {}; - const messages = Object.values( - [...chatHistory, ...chats.get(selectedChatId as string)!.messages].reduce((uniqueMap, message) => { - if (message.timestamp && !uniqueMap[message.timestamp]) { - uniqueMap[message.timestamp] = message; - } - return uniqueMap; - }, uniqueMap) - ); - setChat(selectedChatId as string, { - messages: messages, - lastThreadHash: chatHistory[0].link - }); - } else { - setChat(selectedChatId as string, { messages: chatHistory, lastThreadHash: chatHistory[0].link }); - } - } catch (error: Error | any) { - setLoading(false); - setError(error.message); - console.log(error); - } finally { - setLoading(false); - } - }, [chats]); + } else { + setChat(selectedChatId as string, { messages: chatHistory, lastThreadHash: chatHistory[0].link }); + } + } catch (error: Error | any) { + setLoading(false); + setError(error.message); + console.log(error); + } finally { + setLoading(false); + } + }, + [chats] + ); return { historyMessages, error, loading }; }; -export default useFetchHistoryMessages -; +export default useFetchHistoryMessages; diff --git a/packages/uiweb/src/lib/icons/Emoji.tsx b/packages/uiweb/src/lib/icons/Emoji.tsx deleted file mode 100644 index 748bceb64..000000000 --- a/packages/uiweb/src/lib/icons/Emoji.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -type EmojiIconsProps = { - color?: string; -} - -export const EmojiIcon: React.FC = ({color="#494D5F"}) => { - return ( - - - - - - - - ); -}; \ No newline at end of file diff --git a/packages/uiweb/src/lib/icons/PushIcons.tsx b/packages/uiweb/src/lib/icons/PushIcons.tsx index d12c93de7..a9ffea5e4 100644 --- a/packages/uiweb/src/lib/icons/PushIcons.tsx +++ b/packages/uiweb/src/lib/icons/PushIcons.tsx @@ -182,3 +182,69 @@ export const CancelCircleIcon: React.FC = ({ size, color }) => { ); }; + +// ------ +// CATEGORY - REACTION & EMOJI +// ------ +// Emoji Icon +export const EmojiCircleIcon: React.FC = ({ size, color }) => { + return ( + + + + + + + ); +}; + +// Reply Icon +export const ReplyIcon: React.FC = ({ size, color }) => { + return ( + + + + + + + + ); +}; diff --git a/packages/uiweb/src/lib/types/index.ts b/packages/uiweb/src/lib/types/index.ts index e9267943f..42e51afb3 100644 --- a/packages/uiweb/src/lib/types/index.ts +++ b/packages/uiweb/src/lib/types/index.ts @@ -1,4 +1,4 @@ -import type { IFeeds, ParsedResponseType, PushAPI, Rules } from '@pushprotocol/restapi'; +import type { IFeeds, IMessageIPFSWithCID, ParsedResponseType, PushAPI, Rules } from '@pushprotocol/restapi'; import { Bytes, TypedDataDomain, TypedDataField, providers } from 'ethers'; import type { ReactElement } from 'react'; import type { ENV } from '../config'; @@ -127,12 +127,12 @@ export const SIDEBAR_PLACEHOLDER_KEYS = { NEW_CHAT: 'NEW_CHAT', } as const; -export type SidebarPlaceholderKeys = (typeof SIDEBAR_PLACEHOLDER_KEYS)[keyof typeof SIDEBAR_PLACEHOLDER_KEYS]; +export type SidebarPlaceholderKeys = typeof SIDEBAR_PLACEHOLDER_KEYS[keyof typeof SIDEBAR_PLACEHOLDER_KEYS]; -export type LocalStorageKeys = (typeof LOCAL_STORAGE_KEYS)[keyof typeof LOCAL_STORAGE_KEYS]; -export type PushTabs = (typeof PUSH_TABS)[keyof typeof PUSH_TABS]; -export type PushSubTabs = (typeof PUSH_SUB_TABS)[keyof typeof PUSH_SUB_TABS]; -export type SocketType = (typeof SOCKET_TYPE)[keyof typeof SOCKET_TYPE]; +export type LocalStorageKeys = typeof LOCAL_STORAGE_KEYS[keyof typeof LOCAL_STORAGE_KEYS]; +export type PushTabs = typeof PUSH_TABS[keyof typeof PUSH_TABS]; +export type PushSubTabs = typeof PUSH_SUB_TABS[keyof typeof PUSH_SUB_TABS]; +export type SocketType = typeof SOCKET_TYPE[keyof typeof SOCKET_TYPE]; export interface FileMessageContent { content: string; @@ -216,6 +216,10 @@ export interface IFrame { message?: string; } +export interface IReactionsForChatMessages { + [key: string]: IMessageIPFSWithCID[]; // key is the message CID, value is an array of reactions +} + export type WalletType = { name: string; url: string; diff --git a/packages/uiweb/tsconfig.json b/packages/uiweb/tsconfig.json index 961fa8169..36346f9e0 100644 --- a/packages/uiweb/tsconfig.json +++ b/packages/uiweb/tsconfig.json @@ -12,7 +12,11 @@ "noPropertyAccessFromIndexSignature": false, // "isolatedModules": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@components/*": ["src/lib/components/*"], + "@icons/*": ["src/lib/icons/*"] + } }, "files": [], "include": [],