diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 021a82c4d..b9a8cef32 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -1124,4 +1124,21 @@ export default class EmbeddedChatApi { const data = response.json(); return data; } + + async userData(username: string) { + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const response = await fetch( + `${this.host}/api/v1/users.info?username=${username}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, + }, + } + ); + const data = response.json(); + return data; + } } diff --git a/packages/markups/src/mentions/UserMention.js b/packages/markups/src/mentions/UserMention.js index 679ffc0d3..c860a4d70 100644 --- a/packages/markups/src/mentions/UserMention.js +++ b/packages/markups/src/mentions/UserMention.js @@ -1,11 +1,30 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { Box } from '@embeddedchat/ui-elements'; +import { useUserStore } from '@embeddedchat/react/src/store'; +import useSetExclusiveState from '@embeddedchat/react/src/hooks/useSetExclusiveState'; +import RCContext from '@embeddedchat/react/src/context/RCInstance'; import { MarkupInteractionContext } from '../MarkupInteractionContext'; import useMentionStyles from '../elements/elements.styles'; const UserMention = ({ contents }) => { const { members, username } = useContext(MarkupInteractionContext); + const { RCInstance } = useContext(RCContext); + const setExclusiveState = useSetExclusiveState(); + const { setShowCurrentUserInfo, setCurrentUser } = useUserStore((state) => ({ + setShowCurrentUserInfo: state.setShowCurrentUserInfo, + setCurrentUser: state.setCurrentUser, + })); + + const handleUserInfo = async (uname) => { + const data = await RCInstance.userData(uname); + setCurrentUser({ + _id: data.user._id, + username: data.user.username, + name: data.user.name, + }); + setExclusiveState(setShowCurrentUserInfo); + }; const hasMember = (user) => { if (user === 'all' || user === 'here') return true; @@ -23,7 +42,15 @@ const UserMention = ({ contents }) => { return ( <> {hasMember(contents.value) ? ( - + handleUserInfo(contents.value) + } + > {contents.value} ) : ( diff --git a/packages/react/src/hooks/useRCAuth.js b/packages/react/src/hooks/useRCAuth.js index 6a997cb56..c0b1d3a3b 100644 --- a/packages/react/src/hooks/useRCAuth.js +++ b/packages/react/src/hooks/useRCAuth.js @@ -1,7 +1,12 @@ import { useContext } from 'react'; import { useToastBarDispatch } from '@embeddedchat/ui-elements'; import RCContext from '../context/RCInstance'; -import { useUserStore, totpModalStore, useLoginStore } from '../store'; +import { + useUserStore, + totpModalStore, + useLoginStore, + useMessageStore, +} from '../store'; export const useRCAuth = () => { const { RCInstance } = useContext(RCContext); @@ -23,6 +28,9 @@ export const useRCAuth = () => { const setUserPinPermissions = useUserStore( (state) => state.setUserPinPermissions ); + const setEditMessagePermissions = useMessageStore( + (state) => state.setEditMessagePermissions + ); const dispatchToastMessage = useToastBarDispatch(); const handleLogin = async (userOrEmail, password, code) => { @@ -61,6 +69,7 @@ export const useRCAuth = () => { setEmailorUser(null); setPassword(null); setUserPinPermissions(permissions.update[150]); + setEditMessagePermissions(permissions.update[28]); dispatchToastMessage({ type: 'success', message: 'Successfully logged in', diff --git a/packages/react/src/store/messageStore.js b/packages/react/src/store/messageStore.js index 507ba3d14..676258c1c 100644 --- a/packages/react/src/store/messageStore.js +++ b/packages/react/src/store/messageStore.js @@ -71,6 +71,9 @@ const useMessageStore = create((set, get) => ({ } }, setEditMessage: (editMessage) => set(() => ({ editMessage })), + editMessagePermissions: {}, + setEditMessagePermissions: (editMessagePermissions) => + set((state) => ({ ...state, editMessagePermissions })), addQuoteMessage: (quoteMessage) => set((state) => ({ quoteMessage: [...state.quoteMessage, quoteMessage] })), removeQuoteMessage: (quoteMessage) => diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 99babdc51..010739977 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -21,6 +21,7 @@ import { usePinnedMessageStore, useStarredMessageStore, useFileStore, + useSidebarStore, } from '../../store'; import { DynamicHeader } from '../DynamicHeader'; import useFetchChatData from '../../hooks/useFetchChatData'; @@ -84,7 +85,7 @@ const ChatHeader = ({ const setIsUserAuthenticated = useUserStore( (state) => state.setIsUserAuthenticated ); - + const setShowSidebar = useSidebarStore((state) => state.setShowSidebar); const dispatchToastMessage = useToastBarDispatch(); const { getMessagesAndRoles } = useFetchChatData(showRoles); const setMessageLimit = useSettingsStore((state) => state.setMessageLimit); @@ -130,6 +131,7 @@ const ChatHeader = ({ try { await RCInstance.logout(); setMessages([]); + setShowSidebar(false); setUserAvatarUrl(null); useMessageStore.setState({ isMessageLoaded: false }); } catch (e) { diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index 581cbf0d2..86b4b5082 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -34,6 +34,7 @@ import useShowCommands from '../../hooks/useShowCommands'; import useSearchMentionUser from '../../hooks/useSearchMentionUser'; import formatSelection from '../../lib/formatSelection'; import { parseEmoji } from '../../lib/emoji'; +import { Markdown } from '../Markdown'; const ChatInput = ({ scrollToBottom }) => { const { styleOverrides, classNames } = useComponentOverrides('ChatInput'); diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index a7e258b44..00557f289 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -18,7 +18,7 @@ import { import { ChatLayout } from './ChatLayout'; import { ChatHeader } from './ChatHeader'; import { RCInstanceProvider } from '../context/RCInstance'; -import { useUserStore, useLoginStore } from '../store'; +import { useUserStore, useLoginStore, useMessageStore } from '../store'; import DefaultTheme from '../theme/DefaultTheme'; import { getTokenStorage } from '../lib/auth'; import { styles } from './EmbeddedChat.styles'; @@ -87,6 +87,9 @@ const EmbeddedChat = (props) => { (state) => state.setUserPinPermissions ); + const setEditMessagePermissions = useMessageStore( + (state) => state.setEditMessagePermissions + ); if (isClosable && !setClosableState) { throw Error( 'Please provide a setClosableState to props when isClosable = true' @@ -130,6 +133,7 @@ const EmbeddedChat = (props) => { await RCInstance.autoLogin(auth); const permissions = await RCInstance.permissionInfo(); setUserPinPermissions(permissions.update[150]); + setEditMessagePermissions(permissions.update[28]); } catch (error) { console.error(error); } finally { diff --git a/packages/react/src/views/GlobalStyles.js b/packages/react/src/views/GlobalStyles.js index b26977e63..c9c821fef 100644 --- a/packages/react/src/views/GlobalStyles.js +++ b/packages/react/src/views/GlobalStyles.js @@ -8,7 +8,6 @@ const getGlobalStyles = (theme) => css` margin: 0; padding: 0; } - .ec-embedded-chat body { font-family: ${theme.typography.default.fontFamily}; font-size: ${theme.typography.default.fontSize}px; @@ -36,6 +35,17 @@ const getGlobalStyles = (theme) => css` .ec-embedded-chat ::-webkit-scrollbar-button { display: none; } + @media (max-width: 780px) { + .ec-sidebar { + position: absolute; + width: 100% !important; + height: calc(100% - 56.39px) !important; + min-width: 250px !important; + left: 0; + bottom: 0; + background: ${theme.colors.background}!important; + } + } `; const GlobalStyles = () => { diff --git a/packages/react/src/views/Markdown/Markdown.js b/packages/react/src/views/Markdown/Markdown.js index d9ee6b2ad..b98fe7caa 100644 --- a/packages/react/src/views/Markdown/Markdown.js +++ b/packages/react/src/views/Markdown/Markdown.js @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import { Box } from '@embeddedchat/ui-elements'; -import { Markup, MarkupInteractionContext } from '@embeddedchat/markups'; +import { Markup, MarkupInteractionContext } from '@embeddedchat/markups/src'; import EmojiReaction from '../EmojiReaction/EmojiReaction'; import { useMemberStore, useUserStore } from '../../store'; diff --git a/packages/react/src/views/Message/Message.js b/packages/react/src/views/Message/Message.js index e460adad0..a17a3c3a3 100644 --- a/packages/react/src/views/Message/Message.js +++ b/packages/react/src/views/Message/Message.js @@ -11,7 +11,7 @@ import { import { Attachments } from '../AttachmentHandler'; import { Markdown } from '../Markdown'; import MessageHeader from './MessageHeader'; -import { useMessageStore, useUserStore } from '../../store'; +import { useMessageStore, useUserStore, useSidebarStore } from '../../store'; import RCContext from '../../context/RCInstance'; import { MessageBody } from './MessageBody'; import { MessageReactions } from './MessageReactions'; @@ -49,13 +49,16 @@ const Message = ({ const { RCInstance, ECOptions } = useContext(RCContext); showAvatar = ECOptions?.showAvatar && showAvatar; - + const { showSidebar, setShowSidebar } = useSidebarStore(); 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 editMessagePermissions = useMessageStore( + (state) => state.editMessagePermissions.roles + ); const [setMessageToReport, toggleShowReportMessage] = useMessageStore( (state) => [state.setMessageToReport, state.toggleShowReportMessage] ); @@ -73,6 +76,7 @@ const Message = ({ const styles = getMessageStyles(theme); const bubbleStyles = useBubbleStyles(isMe); const pinRoles = new Set(pinPermissions); + const editMessageRoles = new Set(editMessagePermissions); const variantStyles = !isInSidebar && variantOverrides === 'bubble' ? bubbleStyles : {}; @@ -114,6 +118,45 @@ const Message = ({ } }; + const handleCopyMessage = async (msg) => { + navigator.clipboard + .writeText(msg.msg) + .then(() => { + dispatchToastMessage({ + type: 'success', + message: 'Message copied successfully', + }); + }) + .catch(() => { + dispatchToastMessage({ + type: 'error', + message: 'Error in copying message', + }); + }); + }; + + const getMessageLink = async (id) => { + const host = await RCInstance.getHost(); + const res = await RCInstance.channelInfo(); + return `${host}/channel/${res.room.name}/?msg=${id}`; + }; + + const handleCopyMessageLink = async (msg) => { + try { + const messageLink = await getMessageLink(msg._id); + await navigator.clipboard.writeText(messageLink); + dispatchToastMessage({ + type: 'success', + message: 'Message link copied successfully', + }); + } catch (err) { + dispatchToastMessage({ + type: 'error', + message: 'Error in copying message link', + }); + } + }; + const handleDeleteMessage = async (msg) => { const res = await RCInstance.deleteMessage(msg._id); @@ -137,6 +180,7 @@ const Message = ({ const handleOpenThread = (msg) => async () => { openThread(msg); + setShowSidebar(false); }; const isStarred = message.starred?.find((u) => u._id === authenticatedUserId); @@ -209,6 +253,9 @@ const Message = ({ authenticatedUserId={authenticatedUserId} userRoles={userRoles} pinRoles={pinRoles} + editMessageRoles={editMessageRoles} + handleCopyMessage={handleCopyMessage} + handleCopyMessageLink={handleCopyMessageLink} handleOpenThread={handleOpenThread} handleDeleteMessage={handleDeleteMessage} handleStarMessage={handleStarMessage} diff --git a/packages/react/src/views/Message/Message.styles.js b/packages/react/src/views/Message/Message.styles.js index b6b978fb4..9f9358fd9 100644 --- a/packages/react/src/views/Message/Message.styles.js +++ b/packages/react/src/views/Message/Message.styles.js @@ -81,6 +81,9 @@ export const getMessageDividerStyles = (theme) => { margin-bottom: 0.75rem; padding-left: 1.25rem; padding-right: 1.25rem; + @media (max-width: 780px) { + z-index: 1; + } `, dividerContent: css` diff --git a/packages/react/src/views/Message/MessageToolbox.js b/packages/react/src/views/Message/MessageToolbox.js index 55af7f64d..a3e2c1ec7 100644 --- a/packages/react/src/views/Message/MessageToolbox.js +++ b/packages/react/src/views/Message/MessageToolbox.js @@ -10,9 +10,9 @@ import { useTheme, } from '@embeddedchat/ui-elements'; import { EmojiPicker } from '../EmojiPicker'; -import { parseEmoji } from '../../lib/emoji'; import { getMessageToolboxStyles } from './Message.styles'; import SurfaceMenu from '../SurfaceMenu/SurfaceMenu'; +import { Markdown } from '../Markdown'; export const MessageToolbox = ({ className = '', @@ -23,12 +23,15 @@ export const MessageToolbox = ({ authenticatedUserId, userRoles, pinRoles, + editMessageRoles, handleOpenThread, handleEmojiClick, handlePinMessage, handleStarMessage, handleDeleteMessage, handlerReportMessage, + handleCopyMessage, + handleCopyMessageLink, handleEditMessage, handleQuoteMessage, isEditing = false, @@ -38,6 +41,8 @@ export const MessageToolbox = ({ 'reply', 'quote', 'star', + 'copy', + 'link', 'pin', 'edit', 'delete', @@ -70,6 +75,11 @@ export const MessageToolbox = ({ }; const isAllowedToPin = userRoles.some((role) => pinRoles.has(role)); + const isAllowedToEditMessage = userRoles.some((role) => + editMessageRoles.has(role) + ) + ? true + : message.u._id === authenticatedUserId; const options = useMemo( () => ({ reply: { @@ -120,10 +130,24 @@ export const MessageToolbox = ({ id: 'edit', onClick: () => handleEditMessage(message), iconName: 'edit', - visible: message.u._id === authenticatedUserId, + visible: isAllowedToEditMessage, color: isEditing ? 'secondary' : 'default', ghost: !isEditing, }, + copy: { + label: 'Copy message', + id: 'copy', + onClick: () => handleCopyMessage(message), + iconName: 'copy', + visible: true, + }, + link: { + label: 'Copy link', + id: 'link', + onClick: () => handleCopyMessageLink(message), + iconName: 'link', + visible: true, + }, delete: { label: 'Delete', id: 'delete', @@ -152,6 +176,8 @@ export const MessageToolbox = ({ handlePinMessage, handleEditMessage, handlerReportMessage, + handleCopyMessage, + isAllowedToPin, ] ); @@ -243,7 +269,7 @@ export const MessageToolbox = ({ padding: '0 0.5rem 0.5rem', }} > - {parseEmoji(message.msg)} +