diff --git a/packages/react/src/hooks/useFetchChatData.js b/packages/react/src/hooks/useFetchChatData.js index 1d08f0eaf..99c48aa1e 100644 --- a/packages/react/src/hooks/useFetchChatData.js +++ b/packages/react/src/hooks/useFetchChatData.js @@ -5,6 +5,7 @@ import { useChannelStore, useMemberStore, useMessageStore, + useStarredMessageStore, } from '../store'; const useFetchChatData = (showRoles) => { @@ -13,6 +14,9 @@ const useFetchChatData = (showRoles) => { const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate); const setMessages = useMessageStore((state) => state.setMessages); const setAdmins = useMemberStore((state) => state.setAdmins); + const setStarredMessages = useStarredMessageStore( + (state) => state.setStarredMessages + ); const isUserAuthenticated = useUserStore( (state) => state.isUserAuthenticated ); @@ -80,7 +84,24 @@ const useFetchChatData = (showRoles) => { ] ); - return getMessagesAndRoles; + const getStarredMessages = useCallback( + async (anonymousMode) => { + if (isUserAuthenticated) { + try { + if (!isUserAuthenticated && !anonymousMode) { + return; + } + const { messages } = await RCInstance.getStarredMessages(); + setStarredMessages(messages); + } catch (e) { + console.error(e); + } + } + }, + [isUserAuthenticated, RCInstance, setStarredMessages] + ); + + return { getMessagesAndRoles, getStarredMessages }; }; export default useFetchChatData; diff --git a/packages/react/src/hooks/useRCAuth.js b/packages/react/src/hooks/useRCAuth.js index 83b013353..6a997cb56 100644 --- a/packages/react/src/hooks/useRCAuth.js +++ b/packages/react/src/hooks/useRCAuth.js @@ -20,11 +20,15 @@ export const useRCAuth = () => { ); const setPassword = useUserStore((state) => state.setPassword); const setEmailorUser = useUserStore((state) => state.setEmailorUser); + const setUserPinPermissions = useUserStore( + (state) => state.setUserPinPermissions + ); const dispatchToastMessage = useToastBarDispatch(); const handleLogin = async (userOrEmail, password, code) => { try { const res = await RCInstance.login(userOrEmail, password, code); + const permissions = await RCInstance.permissionInfo(); if (res.error === 'Unauthorized' || res.error === 403) { dispatchToastMessage({ type: 'error', @@ -56,6 +60,7 @@ export const useRCAuth = () => { setIsTotpModalOpen(false); setEmailorUser(null); setPassword(null); + setUserPinPermissions(permissions.update[150]); dispatchToastMessage({ type: 'success', message: 'Successfully logged in', diff --git a/packages/react/src/store/messageStore.js b/packages/react/src/store/messageStore.js index 012a97e1e..507ba3d14 100644 --- a/packages/react/src/store/messageStore.js +++ b/packages/react/src/store/messageStore.js @@ -8,7 +8,7 @@ const useMessageStore = create((set, get) => ({ threadMessages: [], filtered: false, editMessage: {}, - quoteMessage: {}, + quoteMessage: [], messageToReport: NaN, showReportMessage: false, isRecordingMessage: false, @@ -71,7 +71,13 @@ const useMessageStore = create((set, get) => ({ } }, setEditMessage: (editMessage) => set(() => ({ editMessage })), - setQuoteMessage: (quoteMessage) => set(() => ({ quoteMessage })), + addQuoteMessage: (quoteMessage) => + set((state) => ({ quoteMessage: [...state.quoteMessage, quoteMessage] })), + removeQuoteMessage: (quoteMessage) => + set((state) => ({ + quoteMessage: state.quoteMessage.filter((i) => i !== quoteMessage), + })), + clearQuoteMessages: () => set({ quoteMessage: [] }), setMessageToReport: (messageId) => set(() => ({ messageToReport: messageId })), toggleShowReportMessage: () => { diff --git a/packages/react/src/store/starredMessageStore.js b/packages/react/src/store/starredMessageStore.js index a564df3c4..989ec8b6f 100644 --- a/packages/react/src/store/starredMessageStore.js +++ b/packages/react/src/store/starredMessageStore.js @@ -3,6 +3,8 @@ import { create } from 'zustand'; const useStarredMessageStore = create((set) => ({ showStarred: false, setShowStarred: (showStarred) => set(() => ({ showStarred })), + starredMessages: [], + setStarredMessages: (messages) => set(() => ({ starredMessages: messages })), })); export default useStarredMessageStore; diff --git a/packages/react/src/store/userStore.js b/packages/react/src/store/userStore.js index c4128cf46..d3c3aa24c 100644 --- a/packages/react/src/store/userStore.js +++ b/packages/react/src/store/userStore.js @@ -27,8 +27,11 @@ const useUserStore = create((set) => ({ setPassword: (password) => set(() => ({ password })), emailoruser: null, setEmailorUser: (emailoruser) => set(() => ({ emailoruser })), - roles: {}, + roles: [], setRoles: (roles) => set((state) => ({ ...state, roles })), + userPinPermissions: {}, + setUserPinPermissions: (userPinPermissions) => + set((state) => ({ ...state, userPinPermissions })), showCurrentUserInfo: false, setShowCurrentUserInfo: (showCurrentUserInfo) => set(() => ({ showCurrentUserInfo })), diff --git a/packages/react/src/views/AttachmentHandler/Attachment.js b/packages/react/src/views/AttachmentHandler/Attachment.js index bc07a740c..594bfa7d7 100644 --- a/packages/react/src/views/AttachmentHandler/Attachment.js +++ b/packages/react/src/views/AttachmentHandler/Attachment.js @@ -8,11 +8,16 @@ import VideoAttachment from './VideoAttachment'; import TextAttachment from './TextAttachment'; const Attachment = ({ attachment, host, type, variantStyles = {} }) => { + const author = { + authorIcon: attachment?.author_icon, + authorName: attachment?.author_name, + }; if (attachment && attachment.audio_url) { return ( ); @@ -22,6 +27,7 @@ const Attachment = ({ attachment, host, type, variantStyles = {} }) => { ); @@ -31,6 +37,7 @@ const Attachment = ({ attachment, host, type, variantStyles = {} }) => { ); @@ -40,10 +47,56 @@ const Attachment = ({ attachment, host, type, variantStyles = {} }) => { ); } + if ( + attachment.attachments && + Array.isArray(attachment.attachments) && + attachment.attachments[0]?.image_url + ) { + return ( + + ); + } + if ( + attachment.attachments && + Array.isArray(attachment.attachments) && + attachment.attachments[0]?.audio_url + ) { + return ( + + ); + } + if ( + attachment.attachments && + Array.isArray(attachment.attachments) && + attachment.attachments[0]?.video_url + ) { + return ( + + ); + } return ( {

diff --git a/packages/react/src/views/AttachmentHandler/AudioAttachment.js b/packages/react/src/views/AttachmentHandler/AudioAttachment.js index ce8481582..0f5824aa0 100644 --- a/packages/react/src/views/AttachmentHandler/AudioAttachment.js +++ b/packages/react/src/views/AttachmentHandler/AudioAttachment.js @@ -1,18 +1,129 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; -import { Box } from '@embeddedchat/ui-elements'; +import { css } from '@emotion/react'; +import { Box, Avatar, useTheme } from '@embeddedchat/ui-elements'; import AttachmentMetadata from './AttachmentMetadata'; +import RCContext from '../../context/RCInstance'; -const AudioAttachment = ({ attachment, host, variantStyles }) => ( - - - -); +const AudioAttachment = ({ attachment, host, type, author, variantStyles }) => { + const { RCInstance } = useContext(RCContext); + const { theme } = useTheme(); + const getUserAvatarUrl = (icon) => { + const instanceHost = RCInstance.getHost(); + const URL = `${instanceHost}${icon}`; + return URL; + }; + const { authorIcon, authorName } = author; + return ( + + + {type === 'file' ? ( + <> + + + @{authorName} + + + ) : ( + '' + )} + + + + ); +}; export default AudioAttachment; diff --git a/packages/react/src/views/AttachmentHandler/ImageAttachment.js b/packages/react/src/views/AttachmentHandler/ImageAttachment.js index 893778256..b9e6533ad 100644 --- a/packages/react/src/views/AttachmentHandler/ImageAttachment.js +++ b/packages/react/src/views/AttachmentHandler/ImageAttachment.js @@ -1,32 +1,81 @@ -import React, { useState } from 'react'; +import React, { useState, useContext } from 'react'; import { css } from '@emotion/react'; import PropTypes from 'prop-types'; -import { Box } from '@embeddedchat/ui-elements'; +import { Box, Avatar, useTheme } from '@embeddedchat/ui-elements'; import AttachmentMetadata from './AttachmentMetadata'; import ImageGallery from '../ImageGallery/ImageGallery'; +import RCContext from '../../context/RCInstance'; -const ImageAttachment = ({ attachment, host, variantStyles = {} }) => { +const ImageAttachment = ({ + attachment, + host, + type, + author, + variantStyles = {}, +}) => { + const { RCInstance } = useContext(RCContext); const [showGallery, setShowGallery] = useState(false); + const getUserAvatarUrl = (icon) => { + const instanceHost = RCInstance.getHost(); + const URL = `${instanceHost}${icon}`; + return URL; + }; const extractIdFromUrl = (url) => { const match = url.match(/\/file-upload\/(.*?)\//); return match ? match[1] : null; }; + const { theme } = useTheme(); + + const { authorIcon, authorName } = author; + return ( - setShowGallery(true)} - css={css` - cursor: pointer; - border-radius: inherit; - line-height: 0; - `} + css={[ + css` + cursor: pointer; + border-radius: inherit; + line-height: 0; + padding: 0.5rem; + `, + (type ? variantStyles.pinnedContainer : '') || + css` + ${type === 'file' + ? `border: 2px solid ${theme.colors.border};` + : ''} + `, + ]} > + {type === 'file' ? ( + <> + + + @{authorName} + + + ) : ( + '' + )} + { borderBottomRightRadius: 'inherit', }} /> + {attachment.attachments && + attachment.attachments.map((nestedAttachment, index) => ( + + setShowGallery(true)} + css={[ + css` + cursor: pointer; + border-radius: inherit; + line-height: 0; + padding: 0.5rem; + `, + (nestedAttachment.attachments[0].type + ? variantStyles.pinnedContainer + : variantStyles.quoteContainer) || + css` + ${nestedAttachment.attachments[0].type === 'file' + ? `border: 2px solid ${theme.colors.border};` + : ''} + `, + ]} + > + {nestedAttachment.type === 'file' ? ( + <> + + + @{nestedAttachment.author_name} + + + ) : ( + '' + )} + + + + {showGallery && ( + + )} + + ))} {showGallery && ( { return URL; }; - let attachmentText = attachment?.text; - if (attachmentText.includes(')')) { - attachmentText = attachmentText.split(')')[1] || ''; - } - const { theme } = useTheme(); return ( @@ -67,7 +62,77 @@ const TextAttachment = ({ attachment, type, variantStyles = {} }) => { white-space: pre-line; `} > - {attachmentText} + {attachment?.text + ? attachment.text[0] === '[' + ? attachment.text.match(/\n(.*)/)?.[1] || '' + : attachment.text + : ''} + {attachment?.attachments && + attachment.attachments.map((nestedAttachment, index) => ( + + + {nestedAttachment?.author_name && ( + <> + + @{nestedAttachment?.author_name} + + )} + + + {nestedAttachment?.text + ? nestedAttachment.text[0] === '[' + ? nestedAttachment.text.match(/\n(.*)/)?.[1] || '' + : nestedAttachment.text + : ''} + + + ))} ); diff --git a/packages/react/src/views/AttachmentHandler/VideoAttachment.js b/packages/react/src/views/AttachmentHandler/VideoAttachment.js index 0472d34c2..06e345316 100644 --- a/packages/react/src/views/AttachmentHandler/VideoAttachment.js +++ b/packages/react/src/views/AttachmentHandler/VideoAttachment.js @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; -import { Box } from '@embeddedchat/ui-elements'; +import { Box, Avatar, useTheme } from '@embeddedchat/ui-elements'; import AttachmentMetadata from './AttachmentMetadata'; +import RCContext from '../../context/RCInstance'; const userAgentMIMETypeFallback = (type) => { const userAgent = navigator.userAgent.toLocaleLowerCase(); @@ -14,35 +15,152 @@ const userAgentMIMETypeFallback = (type) => { return type; }; -const VideoAttachment = ({ attachment, host, variantStyles = {} }) => ( - - - - + + {attachment.attachments && + attachment.attachments.map((nestedAttachment, index) => ( + + + {nestedAttachment.type === 'file' ? ( + <> + + + @{authorName} + + + ) : ( + '' + )} + + + + + ))} + - -); + ); +}; export default VideoAttachment; diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index e5a6bd1a3..a582244d0 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -69,7 +69,7 @@ const ChatBody = ({ const username = useUserStore((state) => state.username); - const getMessagesAndRoles = useFetchChatData(showRoles); + const { getMessagesAndRoles } = useFetchChatData(showRoles); const getThreadMessages = useCallback(async () => { if (isUserAuthenticated && threadMainMessage?._id) { diff --git a/packages/react/src/views/ChatBody/ChatBody.styles.js b/packages/react/src/views/ChatBody/ChatBody.styles.js index 36fc549b5..06be313a1 100644 --- a/packages/react/src/views/ChatBody/ChatBody.styles.js +++ b/packages/react/src/views/ChatBody/ChatBody.styles.js @@ -9,7 +9,7 @@ export const getChatbodyStyles = () => { overflow-x: hidden; display: flex; flex-direction: column-reverse; - max-height: 600px; + max-height: 100%; position: relative; padding-top: 70px; margin-top: 0.25rem; diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 7c43056a6..99babdc51 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -86,7 +86,7 @@ const ChatHeader = ({ ); const dispatchToastMessage = useToastBarDispatch(); - const getMessagesAndRoles = useFetchChatData(showRoles); + const { getMessagesAndRoles } = useFetchChatData(showRoles); const setMessageLimit = useSettingsStore((state) => state.setMessageLimit); const setMessages = useMessageStore((state) => state.setMessages); const avatarUrl = useUserStore((state) => state.avatarUrl); diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index eae9573ae..581cbf0d2 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -90,20 +90,20 @@ const ChatInput = ({ scrollToBottom }) => { editMessage, setEditMessage, quoteMessage, - setQuoteMessage, isRecordingMessage, upsertMessage, replaceMessage, + clearQuoteMessages, threadId, } = useMessageStore((state) => ({ editMessage: state.editMessage, setEditMessage: state.setEditMessage, quoteMessage: state.quoteMessage, - setQuoteMessage: state.setQuoteMessage, isRecordingMessage: state.isRecordingMessage, upsertMessage: state.upsertMessage, replaceMessage: state.replaceMessage, threadId: state.threadMainMessage?._id, + clearQuoteMessages: state.clearQuoteMessages, })); const setIsLoginModalOpen = useLoginStore( @@ -255,14 +255,31 @@ const ChatInput = ({ scrollToBottom }) => { messageRef.current.value = ''; setDisableButton(true); - const { msg, attachments, _id } = quoteMessage; let pendingMessage = ''; - - if (msg || attachments) { - setQuoteMessage({}); - const msgLink = await getMessageLink(_id); + let quotedMessages = ''; + + if (quoteMessage.length > 0) { + // for (const quote of quoteMessage) { + // const { msg, attachments, _id } = quote; + // if (msg || attachments) { + // const msgLink = await getMessageLink(_id); + // quotedMessages += `[ ](${msgLink})`; + // } + // } + + const quoteArray = await Promise.all( + quoteMessage.map(async (quote) => { + const { msg, attachments, _id } = quote; + if (msg || attachments) { + const msgLink = await getMessageLink(_id); + quotedMessages += `[ ](${msgLink})`; + } + return quotedMessages; + }) + ); + quotedMessages = quoteArray.join(''); pendingMessage = createPendingMessage( - `[ ](${msgLink})\n ${message}`, + `${quotedMessages}\n${message}`, userInfo ); } else { @@ -283,10 +300,9 @@ const ChatInput = ({ scrollToBottom }) => { ECOptions.enableThreads ? threadId : undefined ); - if (!res.success) { - handleSendError('Error sending message, login again'); - } else { - replaceMessage(pendingMessage._id, res.message); + if (res.success) { + clearQuoteMessages(); + replaceMessage(pendingMessage, res.message); } }; @@ -425,14 +441,14 @@ const ChatInput = ({ scrollToBottom }) => { return ( - - {(quoteMessage.msg || quoteMessage.attachments) && ( - - )} + +

+ {quoteMessage && + quoteMessage.length > 0 && + quoteMessage.map((message, index) => ( + + ))} +
{editMessage.msg || editMessage.attachments || isChannelReadOnly ? ( { justify-content: center; flex-direction: row; padding: 0.5rem; + @media (max-width: 383px) { + min-height: 100px; + } `, iconCursor: css` @@ -51,6 +54,13 @@ export const getChatInputStyles = (theme) => { &::placeholder { padding-left: 5px; } + @media (max-width: 383px) { + font-size: 18px; + } + `, + quoteContainer: css` + max-height: 300px; + overflow: scroll; `, }; @@ -68,9 +78,12 @@ export const getChatInputFormattingToolbarStyles = ({ theme, mode }) => { : lighten(theme.colors.background, 1)}; display: flex; position: relative; - flex-direction: row; - gap: 0.375rem; + gap: 0.1rem; border-radius: 0 0 ${theme.radius} ${theme.radius}; + @media (max-width: 383px) { + display: grid; + grid-template-columns: repeat(5, 0.2fr); + } `, }; return styles; diff --git a/packages/react/src/views/ChatLayout/ChatLayout.js b/packages/react/src/views/ChatLayout/ChatLayout.js index 6243dc0cc..ee6b0c1b9 100644 --- a/packages/react/src/views/ChatLayout/ChatLayout.js +++ b/packages/react/src/views/ChatLayout/ChatLayout.js @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useRef, useCallback, useState } from 'react'; import { Box, useComponentOverrides } from '@embeddedchat/ui-elements'; import styles from './ChatLayout.styles'; import { @@ -36,9 +36,15 @@ import useUiKitStore from '../../store/uiKitStore'; const ChatLayout = () => { const messageListRef = useRef(null); const { classNames, styleOverrides } = useComponentOverrides('ChatBody'); - const { ECOptions } = useRCContext(); + const { RCInstance, ECOptions } = useRCContext(); const anonymousMode = ECOptions?.anonymousMode; const showRoles = ECOptions?.anonymousMode; + const setStarredMessages = useStarredMessageStore( + (state) => state.setStarredMessages + ); + const starredMessages = useStarredMessageStore( + (state) => state.starredMessages + ); const showSidebar = useSidebarStore((state) => state.showSidebar); const showMentions = useMentionsStore((state) => state.showMentions); const showAllFiles = useFileStore((state) => state.showAllFiles); @@ -57,6 +63,9 @@ const ChatLayout = () => { const attachmentWindowOpen = useAttachmentWindowStore( (state) => state.attachmentWindowOpen ); + const isUserAuthenticated = useUserStore( + (state) => state.isUserAuthenticated + ); const { data, handleDrag, handleDragDrop } = useDropBox(); const { uiKitContextualBarOpen, uiKitContextualBarData } = useUiKitStore( (state) => ({ @@ -72,7 +81,22 @@ const ChatLayout = () => { }); } }; - + const getStarredMessages = useCallback(async () => { + if (isUserAuthenticated) { + try { + if (!isUserAuthenticated && !anonymousMode) { + return; + } + const { messages } = await RCInstance.getStarredMessages(); + setStarredMessages(messages); + } catch (e) { + console.error(e); + } + } + }, [isUserAuthenticated, anonymousMode, RCInstance]); + useEffect(() => { + getStarredMessages(); + }, [showSidebar]); return ( { })); const setIsLoginIn = useLoginStore((state) => state.setIsLoginIn); + const setUserPinPermissions = useUserStore( + (state) => state.setUserPinPermissions + ); if (isClosable && !setClosableState) { throw Error( @@ -125,6 +128,8 @@ const EmbeddedChat = (props) => { setIsLoginIn(true); try { await RCInstance.autoLogin(auth); + const permissions = await RCInstance.permissionInfo(); + setUserPinPermissions(permissions.update[150]); } catch (error) { console.error(error); } finally { diff --git a/packages/react/src/views/Message/BubbleVariant/Bubble.styles.js b/packages/react/src/views/Message/BubbleVariant/Bubble.styles.js index 42f978f9e..5f87c0b5e 100644 --- a/packages/react/src/views/Message/BubbleVariant/Bubble.styles.js +++ b/packages/react/src/views/Message/BubbleVariant/Bubble.styles.js @@ -94,7 +94,7 @@ export const getBubbleStyles = (theme) => { overflow: hidden; `, pinnedContainer: css` - max-width: 80%; + max-width: 100%; `, quoteContainer: css` @@ -112,7 +112,7 @@ export const getBubbleStyles = (theme) => { `, attachmentMetaContainer: css` - padding: 2.5% 2.5% 0; + padding: 2.5% 0 0; `, emojiPickerStyles: css` @@ -172,7 +172,7 @@ export const getBubbleStylesMe = (theme) => { pinnedContainerMe: css` border-inline-start: none; - border-inline-end: 3px solid ${theme.colors.border}; + border-inline-end: none; `, textUserInfoMe: css` diff --git a/packages/react/src/views/Message/Message.js b/packages/react/src/views/Message/Message.js index 8ce3d8bf5..e460adad0 100644 --- a/packages/react/src/views/Message/Message.js +++ b/packages/react/src/views/Message/Message.js @@ -24,6 +24,7 @@ import { LinkPreview } from '../LinkPreview'; import { getMessageStyles } from './Message.styles'; import useBubbleStyles from './BubbleVariant/useBubbleStyles'; import UiKitMessageBlock from './uiKit/UiKitMessageBlock'; +import useFetchChatData from '../../hooks/useFetchChatData'; const Message = ({ message, @@ -51,12 +52,16 @@ const Message = ({ const authenticatedUserId = useUserStore((state) => state.userId); const authenticatedUserUsername = useUserStore((state) => state.username); + const userRoles = useUserStore((state) => state.roles); + const pinPermissions = useUserStore( + (state) => state.userPinPermissions.roles + ); const [setMessageToReport, toggleShowReportMessage] = useMessageStore( (state) => [state.setMessageToReport, state.toggleShowReportMessage] ); - const setQuoteMessage = useMessageStore((state) => state.setQuoteMessage); + const addQuoteMessage = useMessageStore((state) => state.addQuoteMessage); const openThread = useMessageStore((state) => state.openThread); - + const { getStarredMessages } = useFetchChatData(); const dispatchToastMessage = useToastBarDispatch(); const { editMessage, setEditMessage } = useMessageStore((state) => ({ editMessage: state.editMessage, @@ -67,6 +72,7 @@ const Message = ({ const theme = useTheme(); const styles = getMessageStyles(theme); const bubbleStyles = useBubbleStyles(isMe); + const pinRoles = new Set(pinPermissions); const variantStyles = !isInSidebar && variantOverrides === 'bubble' ? bubbleStyles : {}; @@ -87,6 +93,7 @@ const Message = ({ message: 'Message unstarred', }); } + getStarredMessages(); }; const handlePinMessage = async (msg) => { @@ -200,6 +207,8 @@ const Message = ({ message={message} isEditing={editMessage._id === message._id} authenticatedUserId={authenticatedUserId} + userRoles={userRoles} + pinRoles={pinRoles} handleOpenThread={handleOpenThread} handleDeleteMessage={handleDeleteMessage} handleStarMessage={handleStarMessage} @@ -211,7 +220,7 @@ const Message = ({ setEditMessage(message); } }} - handleQuoteMessage={() => setQuoteMessage(message)} + handleQuoteMessage={() => addQuoteMessage(message)} handleEmojiClick={handleEmojiClick} handlerReportMessage={() => { setMessageToReport(message._id); diff --git a/packages/react/src/views/Message/MessageToolbox.js b/packages/react/src/views/Message/MessageToolbox.js index 248dd3558..55af7f64d 100644 --- a/packages/react/src/views/Message/MessageToolbox.js +++ b/packages/react/src/views/Message/MessageToolbox.js @@ -21,6 +21,8 @@ export const MessageToolbox = ({ style = {}, isThreadMessage = false, authenticatedUserId, + userRoles, + pinRoles, handleOpenThread, handleEmojiClick, handlePinMessage, @@ -67,6 +69,7 @@ export const MessageToolbox = ({ setShowDeleteModal(false); }; + const isAllowedToPin = userRoles.some((role) => pinRoles.has(role)); const options = useMemo( () => ({ reply: { @@ -110,7 +113,7 @@ export const MessageToolbox = ({ id: 'pin', onClick: () => handlePinMessage(message), iconName: message.pinned ? 'pin-filled' : 'pin', - visible: !isThreadMessage, + visible: !isThreadMessage && isAllowedToPin, }, edit: { label: 'Edit', diff --git a/packages/react/src/views/MessageAggregators/StarredMessages.js b/packages/react/src/views/MessageAggregators/StarredMessages.js index b7d77b864..5ced944f0 100644 --- a/packages/react/src/views/MessageAggregators/StarredMessages.js +++ b/packages/react/src/views/MessageAggregators/StarredMessages.js @@ -1,12 +1,15 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useComponentOverrides } from '@embeddedchat/ui-elements'; -import { useUserStore } from '../../store'; +import { useStarredMessageStore, useUserStore } from '../../store'; import { MessageAggregator } from './common/MessageAggregator'; const StarredMessages = () => { const authenticatedUserId = useUserStore((state) => state.userId); const { variantOverrides } = useComponentOverrides('StarredMessages'); const viewType = variantOverrides.viewType || 'Sidebar'; + const starredMessages = useStarredMessageStore( + (state) => state.starredMessages + ); const shouldRender = useCallback( (msg) => msg.starred && @@ -18,6 +21,7 @@ const StarredMessages = () => { title="Starred Messages" iconName="star" noMessageInfo="No Starred Messages" + fetchedMessageList={starredMessages} shouldRender={shouldRender} viewType={viewType} /> diff --git a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js index 079582e86..3f4f62e73 100644 --- a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js +++ b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js @@ -1,10 +1,17 @@ import React, { useState, useMemo } from 'react'; import { isSameDay, format } from 'date-fns'; -import { Box, Sidebar, Popup, useTheme } from '@embeddedchat/ui-elements'; +import { + Box, + Sidebar, + Popup, + useTheme, + ActionButton, + Icon, +} from '@embeddedchat/ui-elements'; import { MessageDivider } from '../../Message/MessageDivider'; import Message from '../../Message/Message'; import getMessageAggregatorStyles from './MessageAggregator.styles'; -import { useMessageStore } from '../../../store'; +import { useMessageStore, useSidebarStore } from '../../../store'; import { useSetMessageList } from '../../../hooks/useSetMessageList'; import LoadingIndicator from './LoadingIndicator'; import NoMessagesIndicator from './NoMessageIndicator'; @@ -16,6 +23,7 @@ export const MessageAggregator = ({ iconName, noMessageInfo, shouldRender, + fetchedMessageList, searchProps, searchFiltered, fetching, @@ -33,13 +41,24 @@ export const MessageAggregator = ({ ); const [messageRendered, setMessageRendered] = useState(false); const { loading, messageList } = useSetMessageList( - searchFiltered || allMessages, + fetchedMessageList || searchFiltered || allMessages, shouldRender ); + const setShowSidebar = useSidebarStore((state) => state.setShowSidebar); + const setJumpToMessage = (msgId) => { + if (msgId) { + const element = document.getElementById(`ec-message-body-${msgId}`); + if (element) { + setShowSidebar(false); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + }; + const isMessageNewDay = (current, previous) => !previous || - !shouldRender(previous) || + shouldRender(previous) || !isSameDay(new Date(current.ts), new Date(previous.ts)); const noMessages = messageList?.length === 0 || !messageRendered; @@ -90,20 +109,38 @@ export const MessageAggregator = ({ fileMessage={msg} /> ) : ( - + > + + + setJumpToMessage(msg._id)} + css={{ + position: 'relative', + zIndex: 10, + }} + > + + + )} ); diff --git a/packages/react/src/views/QuoteMessage/QuoteMessage.js b/packages/react/src/views/QuoteMessage/QuoteMessage.js index fab425eba..be715cf0e 100644 --- a/packages/react/src/views/QuoteMessage/QuoteMessage.js +++ b/packages/react/src/views/QuoteMessage/QuoteMessage.js @@ -11,17 +11,21 @@ import { import RCContext from '../../context/RCInstance'; import { useMessageStore } from '../../store'; import getQuoteMessageStyles from './QuoteMessage.styles'; +import Attachment from '../AttachmentHandler/Attachment'; const QuoteMessage = ({ className = '', style = {}, message }) => { const { RCInstance } = useContext(RCContext); + const instanceHost = RCInstance.getHost(); const getUserAvatarUrl = (username) => { - const host = RCInstance.getHost(); + const host = instanceHost; const URL = `${host}/avatar/${username}`; return URL; }; const { theme } = useTheme(); const styles = getQuoteMessageStyles(theme); - const setQuoteMessage = useMessageStore((state) => state.setQuoteMessage); + const removeQuoteMessage = useMessageStore( + (state) => state.removeQuoteMessage + ); const { classNames, styleOverrides } = useComponentOverrides('QuoteMessage'); return ( @@ -31,7 +35,11 @@ const QuoteMessage = ({ className = '', style = {}, message }) => { css={styles.messageContainer} > - setQuoteMessage({})} size="small"> + removeQuoteMessage(message)} + size="small" + > @@ -45,11 +53,62 @@ const QuoteMessage = ({ className = '', style = {}, message }) => { {format(new Date(message.ts), 'h:mm a')}
- {message.msg - ? message.msg - : `${message.file?.name} (${ - message.file?.size ? (message.file.size / 1024).toFixed(2) : 0 - } kB)`} + {message.file ? ( + message.file.type.startsWith('image/') ? ( +
+ {message.file.name} +
{`${message.file.name} (${(message.file.size / 1024).toFixed( + 2 + )} kB)`}
+
+ ) : message.file.type.startsWith('video/') ? ( + + ) : message.file.type.startsWith('audio/') ? ( + + ) : ( + + {message.msg + ? message.msg + : `${message.file?.name} (${ + message.file?.size + ? (message.file.size / 1024).toFixed(2) + : 0 + } kB)`} + + ) + ) : message?.msg[0] === '[' ? ( + message?.msg.match(/\n(.*)/)[1] + ) : ( + message?.msg + )} + {message.attachments && + message.attachments.length > 0 && + message.msg && + message.msg[0] === '[' && + message.attachments.map((attachment, index) => ( + + ))}
);