diff --git a/README.md b/README.md
index e6a0a6ed1..e5abf5c7f 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ _EmbeddedChat is a full-stack React component node module of the RocketChat appl
## Installation and Usage
-Installtion and usage documentation could be found here [EmbeddedChat installation and usage](packages/react/README.md)
+Installation and usage documentation could be found here [EmbeddedChat installation and usage](packages/react/README.md)
## Development
diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts
index 6d3cc6987..7e0ff5eca 100644
--- a/packages/api/src/EmbeddedChatApi.ts
+++ b/packages/api/src/EmbeddedChatApi.ts
@@ -703,6 +703,26 @@ export default class EmbeddedChatApi {
}
}
+ async getMentionedMessages() {
+ try {
+ const { userId, authToken } = (await this.auth.getCurrentUser()) || {};
+ const response = await fetch(
+ `${this.host}/api/v1/chat.getMentionedMessages?roomId=${this.rid}`,
+ {
+ headers: {
+ "Content-Type": "application/json",
+ "X-Auth-Token": authToken,
+ "X-User-Id": userId,
+ },
+ method: "GET",
+ }
+ );
+ return await response.json();
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
async pinMessage(mid: string) {
try {
const { userId, authToken } = (await this.auth.getCurrentUser()) || {};
diff --git a/packages/react/src/components/Attachments/AttachmentMetadata.js b/packages/react/src/components/Attachments/AttachmentMetadata.js
new file mode 100644
index 000000000..3e8aee462
--- /dev/null
+++ b/packages/react/src/components/Attachments/AttachmentMetadata.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import { ActionButton } from '../ActionButton';
+import { Box } from '../Box';
+
+const AttachmentMetadata = ({ attachment, url }) => {
+ const handleDownload = () => {
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = attachment.title;
+
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ };
+
+ return (
+ <>
+
{attachment.description}
+
+ {attachment.title}
+
+
+ >
+ );
+};
+
+export default AttachmentMetadata;
diff --git a/packages/react/src/components/Attachments/AudioAttachment.js b/packages/react/src/components/Attachments/AudioAttachment.js
index f2349ce39..fe1da9406 100644
--- a/packages/react/src/components/Attachments/AudioAttachment.js
+++ b/packages/react/src/components/Attachments/AudioAttachment.js
@@ -1,10 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Box } from '../Box';
+import AttachmentMetadata from './AttachmentMetadata';
const AudioAttachment = ({ attachment, host }) => (
- {attachment?.description}
+
);
diff --git a/packages/react/src/components/Attachments/ImageAttachment.js b/packages/react/src/components/Attachments/ImageAttachment.js
index e3a650be8..7d356ba92 100644
--- a/packages/react/src/components/Attachments/ImageAttachment.js
+++ b/packages/react/src/components/Attachments/ImageAttachment.js
@@ -1,10 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Box } from '../Box';
+import AttachmentMetadata from './AttachmentMetadata';
const ImageAttachment = ({ attachment, host }) => (
- {attachment?.description}
+
(
-
- {attachment?.author_name}
- {attachment?.text}
-
-);
+const PinnedAttachment = ({ attachment }) => {
+ const { RCInstance } = useContext(RCContext);
+ const getUserAvatarUrl = (authorIcon) => {
+ const host = RCInstance.getHost();
+ const URL = `${host}${authorIcon}`;
+ return URL;
+ };
+ return (
+
+
+
+ {attachment?.author_name}
+
+
+ {attachment?.text}
+
+
+ );
+};
export default PinnedAttachment;
diff --git a/packages/react/src/components/Attachments/VideoAttachment.js b/packages/react/src/components/Attachments/VideoAttachment.js
index 304a3a4da..a98894fca 100644
--- a/packages/react/src/components/Attachments/VideoAttachment.js
+++ b/packages/react/src/components/Attachments/VideoAttachment.js
@@ -1,10 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Box } from '../Box';
+import AttachmentMetadata from './AttachmentMetadata';
const VideoAttachment = ({ attachment, host }) => (
- {attachment?.description}
+
diff --git a/packages/react/src/components/ChatBody/ChatBody.js b/packages/react/src/components/ChatBody/ChatBody.js
index a13a472f2..2899ecfdb 100644
--- a/packages/react/src/components/ChatBody/ChatBody.js
+++ b/packages/react/src/components/ChatBody/ChatBody.js
@@ -136,18 +136,21 @@ const ChatBody = ({
setViewData(null);
};
- const onModalSubmit = useCallback(async (data, value) => {
- console.log(data);
- // const { actionId, value, blockId, appId, viewId } = data;
- // await RCInstance?.triggerBlockAction({
- // rid: RCInstance.rid,
- // actionId,
- // value,
- // blockId,
- // appId,
- // viewId,
- // });
- });
+ const onModalSubmit = useCallback(
+ async (data) => {
+ console.log(data);
+ const { actionId, value, blockId, appId, viewId } = data;
+ await RCInstance?.triggerBlockAction({
+ rid: RCInstance.rid,
+ actionId,
+ value,
+ blockId,
+ appId,
+ viewId,
+ });
+ },
+ [RCInstance]
+ );
useEffect(() => {
RCInstance.auth.onAuthChange((user) => {
diff --git a/packages/react/src/components/ChatHeader/ChatHeader.js b/packages/react/src/components/ChatHeader/ChatHeader.js
index 0ec1a8183..08cfdc8bd 100644
--- a/packages/react/src/components/ChatHeader/ChatHeader.js
+++ b/packages/react/src/components/ChatHeader/ChatHeader.js
@@ -10,6 +10,8 @@ import {
useSearchMessageStore,
useChannelStore,
useToastStore,
+ useThreadsMessageStore,
+ useMentionsStore,
} from '../../store';
import { DynamicHeader } from '../DynamicHeader';
import { Tooltip } from '../Tooltip';
@@ -18,7 +20,6 @@ import useComponentOverrides from '../../theme/useComponentOverrides';
import { Icon } from '../Icon';
import { ActionButton } from '../ActionButton';
import { Menu } from '../Menu';
-import useThreadsMessageStore from '../../store/threadsMessageStore';
import { useToastBarDispatch } from '../../hooks/useToastBarDispatch';
import useFetchChatData from '../../hooks/useFetchChatData';
@@ -73,6 +74,7 @@ const ChatHeader = ({
const setShowAllThreads = useThreadsMessageStore(
(state) => state.setShowAllThreads
);
+ const setShowMentions = useMentionsStore((state) => state.setShowMentions);
const toastPosition = useToastStore((state) => state.position);
const handleGoBack = async () => {
@@ -141,6 +143,11 @@ const ChatHeader = ({
setShowSearch(false);
}, [setShowAllThreads, setShowSearch]);
+ const showMentions = useCallback(async () => {
+ setShowMentions(true);
+ setShowSearch(false);
+ }, [setShowMentions, setShowSearch]);
+
useEffect(() => {
const setMessageAllowed = async () => {
const permissionRes = await RCInstance.permissionInfo();
@@ -223,6 +230,12 @@ const ChatHeader = ({
label: 'Threads',
icon: 'thread',
},
+ {
+ id: 'mentions',
+ action: showMentions,
+ label: 'Mentions',
+ icon: 'at',
+ },
{
id: 'members',
action: showChannelMembers,
@@ -273,6 +286,7 @@ const ChatHeader = ({
moreOpts,
setFullScreen,
showAllThreads,
+ showMentions,
showChannelMembers,
showChannelinformation,
showPinnedMessage,
diff --git a/packages/react/src/components/ChatInput/ChatInput.js b/packages/react/src/components/ChatInput/ChatInput.js
index 026b152f0..502445f6b 100644
--- a/packages/react/src/components/ChatInput/ChatInput.js
+++ b/packages/react/src/components/ChatInput/ChatInput.js
@@ -8,11 +8,11 @@ import {
useMessageStore,
loginModalStore,
useChannelStore,
+ useMemberStore,
} from '../../store';
import ChatInputFormattingToolbar from './ChatInputFormattingToolbar';
import useAttachmentWindowStore from '../../store/attachmentwindow';
import MembersList from '../Mentions/MembersList';
-import mentionmemberStore from '../../store/mentionmemberStore';
import { searchToMentionUser } from '../../lib/searchToMentionUser';
import TypingUsers from '../TypingUsers';
import createPendingMessage from '../../lib/createPendingMessage';
@@ -22,8 +22,10 @@ import { Box } from '../Box';
import { Icon } from '../Icon';
import { CommandsList } from '../CommandList';
import { ActionButton } from '../ActionButton';
+import { Divider } from '../Divider';
import useComponentOverrides from '../../theme/useComponentOverrides';
import { useToastBarDispatch } from '../../hooks/useToastBarDispatch';
+import { Modal } from '../Modal';
const editingMessageCss = css`
background-color: #fff8e0;
@@ -45,12 +47,23 @@ const ChatInput = ({ scrollToBottom }) => {
(state) => state.setIsUserAuthenticated
);
+ const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate);
+
+ const members = useMemberStore((state) => state.members);
+ const setMembersHandler = useMemberStore((state) => state.setMembersHandler);
+
useEffect(() => {
RCInstance.auth.onAuthChange((user) => {
if (user) {
RCInstance.getCommandsList()
.then((data) => setCommands(data.commands || []))
.catch(console.error);
+
+ RCInstance.getChannelMembers(isChannelPrivate)
+ .then((channelMembers) =>
+ setMembersHandler(channelMembers.members || [])
+ )
+ .catch(console.error);
}
});
}, [RCInstance]);
@@ -68,28 +81,22 @@ const ChatInput = ({ scrollToBottom }) => {
const inputRef = useRef(null);
const typingRef = useRef();
- const messageRef = useRef();
+ const messageRef = useRef(null);
const [disableButton, setDisableButton] = useState(true);
- const roomMembers = mentionmemberStore((state) => state.roomMembers);
- const setRoomMembers = mentionmemberStore((state) => state.setRoomMembers);
-
const [filteredMembers, setFilteredMembers] = useState([]);
const [mentionIndex, setmentionIndex] = useState(-1);
const [startReading, setStartReading] = useState(false);
- const showMembersList = mentionmemberStore((state) => state.showMembersList);
- const setshowMembersList = mentionmemberStore(
- (state) => state.toggleShowMembers
- );
+ const [showMembersList, setshowMembersList] = useState(false);
+
const setIsLoginModalOpen = loginModalStore(
(state) => state.setIsLoginModalOpen
);
- const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate);
- const setIsChannelPrivate = useChannelStore(
- (state) => state.setIsChannelPrivate
- );
+
+ const [errorModal, setErrorModal] = useState(false);
+ const [isAttachmentMode, setIsAttachmentMode] = useState(false);
const {
editMessage,
@@ -123,6 +130,17 @@ const ChatInput = ({ scrollToBottom }) => {
const openLoginModal = () => {
setIsLoginModalOpen(true);
};
+ const openErrorModal = () => {
+ setErrorModal(true);
+ };
+ const closeErrorModal = () => {
+ setErrorModal(false);
+ };
+
+ const handleConvertToAttachment = () => {
+ setIsAttachmentMode(true);
+ closeErrorModal();
+ };
const onJoin = async () => {
if (!isUserAuthenticated) {
@@ -143,9 +161,33 @@ const ChatInput = ({ scrollToBottom }) => {
};
const sendMessage = async () => {
- scrollToBottom();
+ messageRef.current.focus();
messageRef.current.style.height = '44px';
const message = messageRef.current.value.trim();
+
+ if (isAttachmentMode) {
+ const messageBlob = new Blob([message], { type: 'text/plain' });
+ const file = new File([messageBlob], 'message.txt', {
+ type: 'text/plain',
+ lastModified: Date.now(),
+ });
+
+ // file upload logic
+ toggle();
+ setData(file);
+
+ messageRef.current.value = '';
+ setEditMessage({});
+ setIsAttachmentMode(false);
+ return;
+ }
+
+ const msgMaxLength = 500;
+ if (message.length > msgMaxLength) {
+ openErrorModal();
+ return;
+ }
+
if (!message.length || !isUserAuthenticated) {
messageRef.current.value = '';
if (editMessage.msg) {
@@ -157,7 +199,9 @@ const ChatInput = ({ scrollToBottom }) => {
if (!editMessage.msg) {
if (message.startsWith('/')) {
// its a slash command
- const [command, params] = message.split(/\s+/);
+ const [command, ...paramsArray] = message.split(' ');
+ const params = paramsArray.join(' ');
+
if (commands.find((c) => c.command === command.replace('/', ''))) {
messageRef.current.value = '';
setDisableButton(true);
@@ -207,8 +251,14 @@ const ChatInput = ({ scrollToBottom }) => {
setDisableButton(true);
setEditMessage({});
}
+
+ scrollToBottom();
};
+ useEffect(() => {
+ if (isAttachmentMode) sendMessage();
+ }, [isAttachmentMode, sendMessage]);
+
const sendAttachment = (event) => {
const fileObj = event.target.files && event.target.files[0];
if (!fileObj) {
@@ -217,16 +267,6 @@ const ChatInput = ({ scrollToBottom }) => {
toggle();
setData(event.target.files[0]);
};
- const getAllChannelMembers = useCallback(async () => {
- try {
- const channelMembers = await RCInstance.getChannelMembers(
- isChannelPrivate
- );
- setRoomMembers(channelMembers.members);
- } catch (e) {
- console.error(e);
- }
- }, [RCInstance, setRoomMembers, isChannelPrivate]);
useEffect(() => {
if (editMessage.msg) {
@@ -235,9 +275,6 @@ const ChatInput = ({ scrollToBottom }) => {
messageRef.current.value = '';
}
}, [editMessage]);
- useEffect(() => {
- getAllChannelMembers();
- }, [getAllChannelMembers]);
const username = useUserStore((state) => state.username);
const timerRef = useRef();
@@ -285,6 +322,34 @@ const ChatInput = ({ scrollToBottom }) => {
}
}, []);
+ const handleMemberClick = (selectedItem) => {
+ setshowMembersList(false);
+
+ let insertionText;
+ if (selectedItem === 'all') {
+ insertionText = `${messageRef.current.value.substring(
+ 0,
+ messageRef.current.value.lastIndexOf('@')
+ )}@all `;
+ } else if (selectedItem === 'here') {
+ insertionText = `${messageRef.current.value.substring(
+ 0,
+ messageRef.current.value.lastIndexOf('@')
+ )}@here `;
+ } else {
+ insertionText = `${messageRef.current.value.substring(
+ 0,
+ messageRef.current.value.lastIndexOf('@')
+ )}@${selectedItem.username} `;
+ }
+
+ messageRef.current.value = insertionText;
+
+ const cursorPosition = insertionText.length;
+ messageRef.current.setSelectionRange(cursorPosition, cursorPosition);
+ messageRef.current.focus();
+ };
+
const showCommands = useCallback(
async (e) => {
const cursor = e.target.selectionStart;
@@ -320,7 +385,7 @@ const ChatInput = ({ scrollToBottom }) => {
}
searchToMentionUser(
messageRef.current.value,
- roomMembers,
+ members,
startReading,
setStartReading,
setFilteredMembers,
@@ -386,36 +451,41 @@ const ChatInput = ({ scrollToBottom }) => {
}
if (e.key === 'ArrowDown') {
+ e.preventDefault();
setmentionIndex(
mentionIndex + 1 >= filteredMembers.length + 2 ? 0 : mentionIndex + 1
);
}
if (e.key === 'ArrowUp') {
+ e.preventDefault();
setmentionIndex(
mentionIndex - 1 < 0 ? filteredMembers.length + 1 : mentionIndex - 1
);
- }
- if (showMembersList && e.key === 'Enter') {
- e.preventDefault();
- let selectedMember = null;
- if (mentionIndex === filteredMembers.length) selectedMember = 'all';
- else if (mentionIndex === filteredMembers.length + 1)
- selectedMember = 'everyone';
- else selectedMember = filteredMembers[mentionIndex].username;
- messageRef.current.value = `${messageRef.current.value.substring(
- 0,
- messageRef.current.value.lastIndexOf('@')
- )}@${selectedMember}`;
- setshowMembersList(false);
-
- setStartReading(false);
- setFilteredMembers([]);
- setmentionIndex(-1);
+ const lastIndexOfAt = messageRef.current.value.lastIndexOf('@');
+ const cursorPosition = lastIndexOfAt === -1 ? 0 : lastIndexOfAt + 1;
+ messageRef.current.setSelectionRange(cursorPosition, cursorPosition);
}
if (e.key === 'Enter') {
- sendTypingStop();
+ e.preventDefault();
+ if (showMembersList) {
+ let selectedMember = null;
+ if (mentionIndex === filteredMembers.length) selectedMember = 'all';
+ else if (mentionIndex === filteredMembers.length + 1)
+ selectedMember = 'here';
+ else selectedMember = filteredMembers[mentionIndex].username;
+
+ handleMemberClick(selectedMember);
+
+ setshowMembersList(false);
+ setStartReading(false);
+ setFilteredMembers([]);
+ setmentionIndex(-1);
+ } else {
+ sendTypingStop();
+ sendMessage();
+ }
}
};
return (
@@ -434,10 +504,14 @@ const ChatInput = ({ scrollToBottom }) => {
`}
>
{showMembersList ? (
-
+ <>
+
+
+ >
) : (
<>>
)}
@@ -501,6 +575,40 @@ const ChatInput = ({ scrollToBottom }) => {
/>
)}
+ {errorModal && (
+
+
+
+
+
+ Message Too Long!
+
+
+
+
+ {' '}
+ Send it as attachment instead?{' '}
+
+
+
+
+
+
+
+ )}
);
};
diff --git a/packages/react/src/components/ChatInput/ChatInputFormattingToolbar.js b/packages/react/src/components/ChatInput/ChatInputFormattingToolbar.js
index f7ccf700f..29e1180ab 100644
--- a/packages/react/src/components/ChatInput/ChatInputFormattingToolbar.js
+++ b/packages/react/src/components/ChatInput/ChatInputFormattingToolbar.js
@@ -42,20 +42,34 @@ const ChatInputFormattingToolbar = ({ messageRef, inputRef }) => {
const initText = input.value.slice(0, selectionStart);
const selectedText = input.value.slice(selectionStart, selectionEnd);
const finalText = input.value.slice(selectionEnd, input.value.length);
- if (
- !document.execCommand ||
- !document.execCommand(
- 'insertText',
- false,
- pattern.replace('{{text}}', selectedText)
- )
- ) {
+
+ const startPattern = pattern.slice(0, pattern.indexOf('{{text}}'));
+ const endPattern = pattern.slice(
+ pattern.indexOf('{{text}}') + '{{text}}'.length
+ );
+
+ const startPatternFound = initText.endsWith(startPattern);
+ const endPatternFound = finalText.startsWith(endPattern);
+
+ if (startPatternFound && endPatternFound) {
+ // Text is already wrapped, so unwrap it
input.value =
- initText + pattern.replace('{{text}}', selectedText) + finalText;
- }
+ initText.slice(0, initText.length - startPattern.length) +
+ selectedText +
+ finalText.slice(endPattern.length);
+
+ input.selectionStart = selectionStart - startPattern.length;
+ input.selectionEnd = input.selectionStart + selectedText.length;
+ } else {
+ // Text is not wrapped, so wrap it
+ const wrappedText = startPattern + selectedText + endPattern;
+ if (!document.execCommand?.('insertText', false, wrappedText)) {
+ input.value = initText + wrappedText + finalText;
+ }
- input.selectionStart = selectionStart + pattern.indexOf('{{text}}');
- input.selectionEnd = input.selectionStart + selectedText.length;
+ input.selectionStart = selectionStart + startPattern.length;
+ input.selectionEnd = input.selectionStart + selectedText.length;
+ }
};
const popupStyle = {
diff --git a/packages/react/src/components/CommandList/CommandsList.js b/packages/react/src/components/CommandList/CommandsList.js
index 2fbf2ef44..4be7a4579 100644
--- a/packages/react/src/components/CommandList/CommandsList.js
+++ b/packages/react/src/components/CommandList/CommandsList.js
@@ -1,8 +1,8 @@
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, { useEffect } from 'react';
import { css } from '@emotion/react';
+import PropTypes from 'prop-types';
import { Box } from '../Box';
import useComponentOverrides from '../../theme/useComponentOverrides';
@@ -15,14 +15,71 @@ function CommandsList({
...props
}) {
const { classNames, styleOverrides } = useComponentOverrides('CommandsList');
- const classNameCommandsList = css`
+
+ const listStyle = css`
+ margin-bottom: 5px;
display: block;
max-height: 10rem;
overflow: scroll;
+ overflow-x: hidden;
+ max-height: 145px;
+ scrollbar-width: thin;
+ scrollbar-color: #e0e0e1 transparent;
+ &::-webkit-scrollbar {
+ width: 4px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background-color: #e0e0e1;
+ border-radius: 4px;
+ }
+ &::-webkit-scrollbar-thumb:hover {
+ background-color: #e0e0e1;
+ }
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+ `;
+
+ const listItemStyle = css`
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-left: 0;
+ padding-right: 2px;
+
+ &:hover {
+ background-color: #dddddd;
+ }
`;
+
+ const handleCommandClick = (command) => {
+ if (execCommand) {
+ execCommand(command);
+ }
+ if (onCommandClick) {
+ onCommandClick(command);
+ }
+ };
+
+ useEffect(() => {
+ const handleKeyPress = (event) => {
+ if (event.key === 'Enter') {
+ const selectedItem = filteredCommands[0];
+ handleCommandClick(selectedItem);
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyPress);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyPress);
+ };
+ }, [filteredCommands, handleCommandClick]);
+
return (
{filteredCommands.map((command) => (
{
- if (execCommand) {
- execCommand(command);
- }
- if (onCommandClick) {
- onCommandClick(command);
- }
- }}
key={command.command}
+ css={listItemStyle}
+ onClick={() => handleCommandClick(command)}
>
- {command.command}
+
+ {command.command}
+
+ {command.params}
+
+ {command.description}
))}
diff --git a/packages/react/src/components/Icon/icons/At.js b/packages/react/src/components/Icon/icons/At.js
new file mode 100644
index 000000000..aeb9061fd
--- /dev/null
+++ b/packages/react/src/components/Icon/icons/At.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+const At = (props) => (
+
+);
+
+export default At;
diff --git a/packages/react/src/components/Icon/icons/ChevronDown.js b/packages/react/src/components/Icon/icons/ChevronDown.js
new file mode 100644
index 000000000..760326aa3
--- /dev/null
+++ b/packages/react/src/components/Icon/icons/ChevronDown.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const ChevronDown = (props) => (
+
+);
+
+export default ChevronDown;
diff --git a/packages/react/src/components/Icon/icons/ChevronLeft.js b/packages/react/src/components/Icon/icons/ChevronLeft.js
new file mode 100644
index 000000000..56f4d9bd8
--- /dev/null
+++ b/packages/react/src/components/Icon/icons/ChevronLeft.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+const ChevronLeft = (props) => (
+
+);
+
+export default ChevronLeft;
diff --git a/packages/react/src/components/Icon/icons/Clipboard.js b/packages/react/src/components/Icon/icons/Clipboard.js
new file mode 100644
index 000000000..c7ac76e89
--- /dev/null
+++ b/packages/react/src/components/Icon/icons/Clipboard.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const Clipboard = (props) => (
+
+);
+
+export default Clipboard;
diff --git a/packages/react/src/components/Icon/icons/Copy.js b/packages/react/src/components/Icon/icons/Copy.js
index f32df0edd..6e621d958 100644
--- a/packages/react/src/components/Icon/icons/Copy.js
+++ b/packages/react/src/components/Icon/icons/Copy.js
@@ -1,34 +1,13 @@
import React from 'react';
-const Copy = () => (
+const Copy = (props) => (
);
diff --git a/packages/react/src/components/Icon/icons/Download.js b/packages/react/src/components/Icon/icons/Download.js
new file mode 100644
index 000000000..7127ed3e3
--- /dev/null
+++ b/packages/react/src/components/Icon/icons/Download.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const Download = (props) => (
+
+);
+
+export default Download;
diff --git a/packages/react/src/components/Icon/icons/Key.js b/packages/react/src/components/Icon/icons/Key.js
new file mode 100644
index 000000000..492f5cda0
--- /dev/null
+++ b/packages/react/src/components/Icon/icons/Key.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+const Key = (props) => (
+
+);
+
+export default Key;
diff --git a/packages/react/src/components/Icon/icons/index.js b/packages/react/src/components/Icon/icons/index.js
index 490e0982b..60aed7b64 100644
--- a/packages/react/src/components/Icon/icons/index.js
+++ b/packages/react/src/components/Icon/icons/index.js
@@ -39,6 +39,12 @@ import PinFilled from './PinFilled';
import VideoRecorder from './VideoRecoder';
import DisabledRecorder from './DisableRecorder';
import Copy from './Copy';
+import Clipboard from './Clipboard';
+import Download from './Download';
+import At from './At';
+import ChevronDown from './ChevronDown';
+import ChevronLeft from './ChevronLeft';
+import Key from './Key';
const icons = {
file: File,
@@ -49,6 +55,7 @@ const icons = {
hash: Hash,
computer: Computer,
cross: Cross,
+ copy: Copy,
mic: Mic,
'video-recorder': VideoRecorder,
'disabled-recorder': DisabledRecorder,
@@ -81,7 +88,12 @@ const icons = {
'error-circle': ErrorCircle,
'arrow-down': ArrowDown,
'pin-filled': PinFilled,
- copy: Copy,
+ clipboard: Clipboard,
+ download: Download,
+ at: At,
+ 'chevron-down': ChevronDown,
+ 'chevron-left': ChevronLeft,
+ key: Key,
};
export default icons;
diff --git a/packages/react/src/components/LinkPreview/LinkPreview.js b/packages/react/src/components/LinkPreview/LinkPreview.js
new file mode 100644
index 000000000..828c05269
--- /dev/null
+++ b/packages/react/src/components/LinkPreview/LinkPreview.js
@@ -0,0 +1,128 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { css } from '@emotion/react';
+import { Box } from '../Box';
+import { ActionButton } from '../ActionButton';
+import { Icon } from '../Icon';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const LinkPreview = ({ className = '', style = {}, url, meta, ...props }) => {
+ const { classNames, styleOverrides } = useComponentOverrides('LinkPreview');
+ const [isPreviewOpen, setIsPreviewOpen] = useState(true);
+
+ if (!meta || (typeof meta === 'object' && Object.keys(meta).length === 0)) {
+ return null;
+ }
+
+ const isDescription =
+ meta.oembedAuthorName || meta.ogDescription || meta.description;
+ const isTitle = meta.pageTitle || meta.ogTitle || meta.oembedTitle;
+ const isThumbnail = meta.oembedThumbnailUrl || meta.ogImage;
+ const isSiteName = meta.ogSiteName || meta.oembedProviderName;
+
+ const ArrowDropDownCss = css`
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ `;
+
+ const LinkPreviewBoxCss = css`
+ max-width: 22rem;
+ border: 1px solid #ccc;
+ border-radius: 0.25rem;
+ margin-bottom: 0.75rem;
+ overflow: hidden;
+ `;
+
+ const TextCss = css`
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ margin-block-start: 0rem;
+ margin-block-end: 0rem;
+ `;
+
+ const handleTogglePreview = () => {
+ setIsPreviewOpen((prev) => !prev);
+ };
+
+ return (
+ <>
+
+ Link Preview
+
+ {isPreviewOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isPreviewOpen && (
+
+ {isThumbnail && (
+
+
+
+
+
+ )}
+
+
+
+ {isTitle && {isTitle}
}
+
+ {isDescription && {isDescription}
}
+ {isSiteName && (
+
+ {isSiteName}
+
+ )}
+
+
+ )}
+ >
+ );
+};
+
+LinkPreview.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ color: PropTypes.string,
+ url: PropTypes.string,
+ meta: PropTypes.object,
+};
+export default LinkPreview;
diff --git a/packages/react/src/components/LinkPreview/index.js b/packages/react/src/components/LinkPreview/index.js
new file mode 100644
index 000000000..74dec51e8
--- /dev/null
+++ b/packages/react/src/components/LinkPreview/index.js
@@ -0,0 +1 @@
+export { default as LinkPreview } from './LinkPreview';
diff --git a/packages/react/src/components/Markup/elements/BoldSpan.js b/packages/react/src/components/Markup/elements/BoldSpan.js
index a921fba6a..36f1ee945 100644
--- a/packages/react/src/components/Markup/elements/BoldSpan.js
+++ b/packages/react/src/components/Markup/elements/BoldSpan.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import PlainSpan from './PlainSpan';
import ItalicSpan from './ItalicSpan';
import StrikeSpan from './StrikeSpan';
+import LinkSpan from './LinkSpan';
const BoldSpan = ({ contents }) => (
@@ -16,6 +17,18 @@ const BoldSpan = ({ contents }) => (
case 'ITALIC':
return ;
+ case 'LINK':
+ return (
+
+ );
default:
return null;
diff --git a/packages/react/src/components/Markup/elements/Mention.js b/packages/react/src/components/Markup/elements/Mention.js
index 720c2af0b..a0d4c140e 100644
--- a/packages/react/src/components/Markup/elements/Mention.js
+++ b/packages/react/src/components/Markup/elements/Mention.js
@@ -1,11 +1,37 @@
import React from 'react';
+import { css } from '@emotion/react';
import PropTypes from 'prop-types';
-import useMentionMemberStore from '../../../store/mentionmemberStore';
+import { useMemberStore, useUserStore } from '../../../store';
const Mention = ({ contents }) => {
- const members = useMentionMemberStore((state) => state.roomMembers || []);
+ const members = useMemberStore((state) => state.members);
+ const username = useUserStore((state) => state.username);
+
+ const mentionStyles = css`
+ background-color: ${contents.value === 'all' || contents.value === 'here'
+ ? '#f38c39'
+ : contents.value === username
+ ? '#ec0d2a'
+ : '#e4e7ea'};
+ color: ${contents.value === 'all' || contents.value === 'here'
+ ? '#ffffff'
+ : contents.value === username
+ ? '#ffffff'
+ : '#2f343d'};
+ font-weight: bold;
+ cursor: pointer;
+ padding: 1.5px;
+ border-radius: 3px;
+
+ &:hover {
+ text-decoration: ${contents.value === 'all' || contents.value === 'here'
+ ? 'none'
+ : 'underline'};
+ }
+ `;
+
const hasMember = (user) => {
- if (user === 'all' || user === 'everyone') return true;
+ if (user === 'all' || user === 'here') return true;
let found = false;
Object.keys(members).forEach((ele) => {
if (members[ele].username === user) {
@@ -17,9 +43,7 @@ const Mention = ({ contents }) => {
return (
<>
{hasMember(contents.value) === true ? (
-
- {contents.value}
-
+ {contents.value}
) : (
`@${contents.value}`
)}
diff --git a/packages/react/src/components/Mentions/MembersList.js b/packages/react/src/components/Mentions/MembersList.js
index 3b9f7482c..b7ab4d3a4 100644
--- a/packages/react/src/components/Mentions/MembersList.js
+++ b/packages/react/src/components/Mentions/MembersList.js
@@ -1,46 +1,151 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { css } from '@emotion/react';
import PropTypes from 'prop-types';
+import { Box } from '../Box';
+
+function MembersList({ mentionIndex, filteredMembers = [], onMemberClick }) {
+ const listStyle = css`
+ margin-bottom: 5px;
+ display: block;
+ max-height: 10rem;
+ overflow: scroll;
+ overflow-x: hidden;
+ max-height: 145px;
+ scrollbar-width: thin;
+ scrollbar-color: #e0e0e1 transparent;
+ &::-webkit-scrollbar {
+ width: 4px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background-color: #e0e0e1;
+ border-radius: 4px;
+ }
+ &::-webkit-scrollbar-thumb:hover {
+ background-color: #e0e0e1;
+ }
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+ `;
+
+ const listItemStyle = css`
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-left: 0;
+ padding-right: 2px;
+
+ &:hover {
+ background-color: #e8e8e8;
+ }
+ `;
+
+ const listTextStyle = css`
+ color: #000000;
+ font-weight: 600;
+ `;
+
+ const handleMemberClick = (selectedItem) => {
+ onMemberClick(selectedItem);
+ };
+
+ useEffect(() => {
+ const handleKeyPress = (event) => {
+ if (event.key === 'Enter') {
+ const selectedItem =
+ mentionIndex < filteredMembers.length
+ ? filteredMembers[mentionIndex]
+ : mentionIndex === filteredMembers.length
+ ? 'all'
+ : 'here';
+ handleMemberClick(selectedItem);
+ }
+ };
+
+ const handleKeyDown = (event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyPress);
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyPress);
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [mentionIndex, filteredMembers, handleMemberClick]);
-function MembersList({ mentionIndex, filteredMembers = [] }) {
return (
-
+
{filteredMembers.map((member, index) => (
- handleMemberClick(member)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleMemberClick(member);
+ }
+ }}
style={{
- backgroundColor: index === mentionIndex ? '#ddd' : 'white',
+ backgroundColor: index === mentionIndex && '#dddddd',
}}
>
- {member.name} @{member.username}
+
+ {member.name}
+
+ @{member.username}
+
))}
- handleMemberClick('all')}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleMemberClick('all');
+ }
+ }}
style={{
backgroundColor:
- mentionIndex === filteredMembers.length ? '#ddd' : 'white',
+ mentionIndex === filteredMembers.length && '#dddddd',
}}
>
- all
+ all
- handleMemberClick('here')}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleMemberClick('here');
+ }
+ }}
style={{
backgroundColor:
- mentionIndex === filteredMembers.length + 1 ? '#ddd' : 'white',
+ mentionIndex === filteredMembers.length + 1 && '#dddddd',
}}
>
- everyone
+ here
-
+
);
}
MembersList.propTypes = {
mentionIndex: PropTypes.any,
filteredMembers: PropTypes.array,
+ onMemberClick: PropTypes.func.isRequired,
};
export default MembersList;
diff --git a/packages/react/src/components/Menu/Menu.js b/packages/react/src/components/Menu/Menu.js
index e544228ba..9a1811634 100644
--- a/packages/react/src/components/Menu/Menu.js
+++ b/packages/react/src/components/Menu/Menu.js
@@ -31,6 +31,7 @@ const Menu = ({
className = '',
style = {},
anchor = 'right bottom',
+ isToolTip = true,
}) => {
const theme = useTheme();
const shadowCss = css`
@@ -84,10 +85,14 @@ const Menu = ({
className={appendClassNames('ec-menu-wrapper', wrapperClasses)}
style={wrapperStyles}
>
-
- {' '}
+ {isToolTip ? (
+
+ {' '}
+ setOpen(!isOpen)} />
+
+ ) : (
setOpen(!isOpen)} />
-
+ )}
{isOpen ? (
{
const { classNames, styleOverrides } = useComponentOverrides(
'Message',
@@ -223,6 +225,19 @@ const Message = ({
) : (
)}
+
+ {message.urls &&
+ message.urls.map(
+ (url, index) =>
+ url.meta && (
+
+ )
+ )}
+
{message.blocks && (
) : null}
- {!message.t ? (
+ {!message.t && showToolbox ? (
{!isThreadMessage ? (
+
+
+
+ ) : null}
+ u._id === authenticatedUserId)
+ ? 'Remove star'
+ : 'Star'
+ }
+ position="top"
+ >
u._id === authenticatedUserId)
+ ? 'star-filled'
+ : 'star'
+ }`}
+ onClick={() => handleStarMessage(message)}
/>
- ) : null}
- u._id === authenticatedUserId)
- ? 'star-filled'
- : 'star'
- }`}
- onClick={() => handleStarMessage(message)}
- />
- setEmojiOpen(true)}
- />
+
+
+ setEmojiOpen(true)}
+ />
+
{!isThreadMessage && (
- handlePinMessage(message)}
- />
- )}
- {message.u._id === authenticatedUserId && (
- <>
- handleEditMessage(message)}
- />
- handleCopyMessageLink(message)}
- />
- handleCopyMessage(message)}
- />
+
handleClickDelete(message)}
+ icon={`${message.pinned ? 'pin-filled' : 'pin'}`}
+ onClick={() => handlePinMessage(message)}
/>
+
+ )}
+ {message.u._id === authenticatedUserId && (
+ <>
+
+ handleEditMessage(message)}
+ />
+
+
+ handleCopyMessageLink(message)}
+ />
+
+
+ handleCopyMessage(message)}
+ />
+
+
+ handleClickDelete(message)}
+ />
+
>
)}
- handlerReportMessage(message)}
- />
+
+ handlerReportMessage(message)}
+ />
+
{showDeleteModal && (
diff --git a/packages/react/src/components/MessageGenericPreview/MessageGenericPreview.js b/packages/react/src/components/MessageGenericPreview/MessageGenericPreview.js
new file mode 100644
index 000000000..a5fce3727
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/MessageGenericPreview.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import { css } from '@emotion/react';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const MessageGenericPreview = (props, className = '', style = {}) => {
+ const { classNames, styleOverrides } = useComponentOverrides(
+ 'MessageGenericPreview'
+ );
+ const messageGenericPreviewStyles = css`
+ display: flex;
+ overflow: hidden;
+ flex-direction: column;
+ padding: 0.75rem;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ background-color: #eff0f1;
+ `;
+ return (
+
+ );
+};
+
+export default MessageGenericPreview;
diff --git a/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewContent.js b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewContent.js
new file mode 100644
index 000000000..67d780a3a
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewContent.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { css } from '@emotion/react';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const MessageGenericPreviewContent = ({
+ className = '',
+ style = {},
+ thumb,
+ ...props
+}) => {
+ const { classNames, styleOverrides } = useComponentOverrides(
+ 'MessageGenericPreviewContent'
+ );
+
+ const MessageGenericPreviewContentCss = css`
+ display: flex;
+ flex-direction: row;
+ `;
+
+ return (
+
+ );
+};
+
+export default MessageGenericPreviewContent;
diff --git a/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewCoverImage.js b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewCoverImage.js
new file mode 100644
index 000000000..d0e1fe5f6
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewCoverImage.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { css } from '@emotion/react';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const MessageGenericPreviewCoverImage = ({
+ className = '',
+ style = {},
+ url,
+ width,
+ height,
+ ...props
+}) => {
+ const { classNames, styleOverrides } = useComponentOverrides(
+ 'MessageGenericPreviewCoverImage'
+ );
+
+ const previewCoverImageCss = css`
+ background-image: url(${url});
+ max-width: 100%;
+ `;
+
+ return (
+
+ );
+};
+
+export default MessageGenericPreviewCoverImage;
diff --git a/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewDescription.js b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewDescription.js
new file mode 100644
index 000000000..6725848bf
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewDescription.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const MessageGenericPreviewDescription = ({
+ children,
+ clamp = false,
+ className = '',
+ style = {},
+ ...props
+}) => {
+ const { classNames, styleOverrides } = useComponentOverrides(
+ 'MessageGenericPreviewDescription'
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default MessageGenericPreviewDescription;
diff --git a/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewFooter.js b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewFooter.js
new file mode 100644
index 000000000..6648be05d
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewFooter.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { css } from '@emotion/react';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const MessageGenericPreviewFooter = ({
+ children,
+ className = '',
+ style = {},
+ ...props
+}) => {
+ const { classNames, styleOverrides } = useComponentOverrides(
+ 'MessageGenericPreviewFooter'
+ );
+ const MessageGenericPreviewFooterCss = css`
+ padding: 0.5rem 0;
+ `;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default MessageGenericPreviewFooter;
diff --git a/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewThumb.js b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewThumb.js
new file mode 100644
index 000000000..66a8b1cf4
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewThumb.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const MessageGenericPreviewThumb = (className = '', style = {}, ...props) => {
+ const { classNames, styleOverrides } = useComponentOverrides(
+ 'MessageGenericPreviewThumb'
+ );
+ return (
+
+ );
+};
+
+export default MessageGenericPreviewThumb;
diff --git a/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewTitle.js b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewTitle.js
new file mode 100644
index 000000000..c2980037e
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/MessageGenericPreviewTitle.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const MessageGenericPreviewTitle = ({
+ externalUrl,
+ children,
+ className = '',
+ style = {},
+ ...props
+}) => {
+ const { classNames, styleOverrides } = useComponentOverrides(
+ 'MessageGenericPreviewTitle'
+ );
+
+ if (externalUrl) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+};
+
+export default MessageGenericPreviewTitle;
diff --git a/packages/react/src/components/MessageGenericPreview/index.js b/packages/react/src/components/MessageGenericPreview/index.js
new file mode 100644
index 000000000..07df81c7c
--- /dev/null
+++ b/packages/react/src/components/MessageGenericPreview/index.js
@@ -0,0 +1,7 @@
+export { default as MessageGenericPreview } from './MessageGenericPreview';
+export { default as MessageGenericPreviewCoverImage } from './MessageGenericPreviewCoverImage';
+export { default as MessageGenericPreviewThumb } from './MessageGenericPreviewThumb';
+export { default as MessageGenericPreviewTitle } from './MessageGenericPreviewTitle';
+export { default as MessageGenericPreviewContent } from './MessageGenericPreviewContent';
+export { default as MessageGenericPreviewDescription } from './MessageGenericPreviewDescription';
+export { default as MessageGenericPreviewFooter } from './MessageGenericPreviewFooter';
diff --git a/packages/react/src/components/MessageList/MessageList.js b/packages/react/src/components/MessageList/MessageList.js
index 9a9203a89..2cad03c4d 100644
--- a/packages/react/src/components/MessageList/MessageList.js
+++ b/packages/react/src/components/MessageList/MessageList.js
@@ -7,6 +7,8 @@ import {
useSearchMessageStore,
useChannelStore,
useUserStore,
+ useMentionsStore,
+ useThreadsMessageStore,
} from '../../store';
import RoomMembers from '../RoomMembers/RoomMember';
import MessageReportWindow from '../ReportMessage/MessageReportWindow';
@@ -14,10 +16,9 @@ import isMessageSequential from '../../lib/isMessageSequential';
import SearchMessage from '../SearchMessage/SearchMessage';
import Roominfo from '../RoomInformation/RoomInformation';
import AllThreads from '../AllThreads/AllThreads';
+import UserMentions from '../UserMentions/UserMentions';
import { Message } from '../Message';
-import useThreadsMessageStore from '../../store/threadsMessageStore';
-
const MessageList = ({ messages }) => {
const showSearch = useSearchMessageStore((state) => state.showSearch);
const showChannelinfo = useChannelStore((state) => state.showChannelinfo);
@@ -29,6 +30,7 @@ const MessageList = ({ messages }) => {
const showAllThreads = useThreadsMessageStore(
(state) => state.showAllThreads
);
+ const showMentions = useMentionsStore((state) => state.showMentions);
const isMessageNewDay = (current, previous) =>
!previous || !isSameDay(new Date(current.ts), new Date(previous.ts));
@@ -59,6 +61,7 @@ const MessageList = ({ messages }) => {
{showSearch && }
{showChannelinfo && }
{showAllThreads && }
+ {showMentions && }
>
);
};
diff --git a/packages/react/src/components/MultiSelect/MultiSelect.js b/packages/react/src/components/MultiSelect/MultiSelect.js
new file mode 100644
index 000000000..bb1b9e070
--- /dev/null
+++ b/packages/react/src/components/MultiSelect/MultiSelect.js
@@ -0,0 +1,102 @@
+import React, { useState } from 'react';
+import { css, useTheme } from '@emotion/react';
+import PropTypes from 'prop-types';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+import { Box } from '../Box';
+
+const MultiSelect = ({
+ className = '',
+ style = {},
+ color = 'primary',
+ options = [],
+ onChange,
+ ...props
+}) => {
+ const { classNames, styleOverrides } = useComponentOverrides('MultiSelect');
+ const theme = useTheme();
+ const [selectedOptions, setSelectedOptions] = useState([]);
+
+ const handleOptionToggle = (value) => {
+ const isSelected = selectedOptions.includes(value);
+ if (isSelected) {
+ setSelectedOptions(selectedOptions.filter((item) => item !== value));
+ } else {
+ setSelectedOptions([...selectedOptions, value]);
+ }
+ };
+
+ const MultiSelectCss = css`
+ position: relative;
+ display: inline-flex;
+ flex: 1 0 auto;
+ min-width: 8rem;
+ padding: 0.5rem 0.9375rem;
+ vertical-align: baseline;
+ outline: 0;
+ background-color: transparent;
+ letter-spacing: 0rem;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.25rem;
+ overflow: hidden;
+ color: #2f343d;
+ border-right: 0.9375rem transparent;
+ border-width: 1px;
+ border-color: #cbced1;
+ border-style: solid;
+ border-radius: 0.25rem;
+ background-color: white;
+ box-shadow: none;
+ -webkit-appearance: none;
+ appearance: none;
+ transition: all 230ms;
+ &:focus {
+ border-color: ${theme.palette[color].main || 'currentColor'};
+ box-shadow: 0px 0px 2.5px ${theme.palette[color].light || 'currentColor'};
+ }
+ `;
+
+ const CheckboxCss = css`
+ margin-right: 8px;
+ cursor: pointer;
+ `;
+
+ return (
+
+
+ {options.map((option) => (
+ // eslint-disable-next-line jsx-a11y/label-has-associated-control
+
+ ))}
+
+
+ );
+};
+
+MultiSelect.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ color: PropTypes.string,
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.string,
+ label: PropTypes.string,
+ })
+ ),
+ onChange: PropTypes.func,
+};
+
+export default MultiSelect;
diff --git a/packages/react/src/components/MultiSelect/index.js b/packages/react/src/components/MultiSelect/index.js
new file mode 100644
index 000000000..7860a30ca
--- /dev/null
+++ b/packages/react/src/components/MultiSelect/index.js
@@ -0,0 +1 @@
+export { default as MultiSelect } from './MultiSelect';
diff --git a/packages/react/src/components/RoomMembers/RoomMember.js b/packages/react/src/components/RoomMembers/RoomMember.js
index 4c37bb47d..ae0d9440b 100644
--- a/packages/react/src/components/RoomMembers/RoomMember.js
+++ b/packages/react/src/components/RoomMembers/RoomMember.js
@@ -1,4 +1,4 @@
-import React, { useContext, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import RoomMemberItem from './RoomMemberItem';
import classes from './RoomMember.module.css';
@@ -20,6 +20,24 @@ const RoomMembers = ({ members }) => {
const toggleInviteView = useInviteStore((state) => state.toggleInviteView);
const showInvite = useInviteStore((state) => state.showInvite);
+ const [userInfo, setUserInfo] = useState(null);
+
+ useEffect(() => {
+ const getUserInfo = async () => {
+ try {
+ const res = await RCInstance.me();
+ setUserInfo(res);
+ } catch (error) {
+ console.error('Error fetching user info:', error);
+ }
+ };
+
+ getUserInfo();
+ }, [RCInstance]);
+
+ const roles = userInfo && userInfo.roles ? userInfo.roles : [];
+ const isAdmin = roles.includes('admin');
+
const [inviteData, setInviteData] = useState(null);
if (showInvite) return ;
@@ -44,16 +62,18 @@ const RoomMembers = ({ members }) => {
))}
-
+ {isAdmin && (
+
+ )}
);
};
diff --git a/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js b/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js
index 42af40076..ec2bac803 100644
--- a/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js
+++ b/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { css } from '@emotion/react';
import classes from '../RoomMember.module.css';
import useInviteStore from '../../../store/inviteStore';
+import { useToastBarDispatch } from '../../../hooks/useToastBarDispatch';
import { Box } from '../../Box';
import { Icon } from '../../Icon';
import { Input } from '../../Input';
@@ -10,6 +11,23 @@ import { ActionButton } from '../../ActionButton';
const InviteMembers = ({ inviteData }) => {
const toggleInviteView = useInviteStore((state) => state.toggleInviteView);
+ const dispatchToastMessage = useToastBarDispatch();
+
+ const copyToClipboard = () => {
+ if (inviteData && inviteData.url) {
+ navigator.clipboard
+ .writeText(inviteData.url)
+ .then(() => {
+ dispatchToastMessage({
+ type: 'success',
+ message: 'Copied to clipboard',
+ });
+ })
+ .catch((error) => {
+ console.error('Error copying to clipboard:', error);
+ });
+ }
+ };
return (
@@ -37,20 +55,51 @@ const InviteMembers = ({ inviteData }) => {
-
+
+
+ Invite Link
+
+
+
+
+
+
+
+ )}
+
-
Invite Link
-
-
-
- Your invite link will expire on{' '}
- {new Date(inviteData.expires).toString().split('GMT')[0]}
-
+ {inviteData && (
+
+
+ Your invite link will expire on{' '}
+ {new Date(inviteData.expires).toString().split('GMT')[0]}
+
+
+ )}
+
);
};
diff --git a/packages/react/src/components/SearchMessage/SearchMessage.js b/packages/react/src/components/SearchMessage/SearchMessage.js
index fa44ddf97..06d289734 100644
--- a/packages/react/src/components/SearchMessage/SearchMessage.js
+++ b/packages/react/src/components/SearchMessage/SearchMessage.js
@@ -1,24 +1,18 @@
-import React, { useState, useContext } from 'react';
+import React, { useState, useContext, useEffect } from 'react';
import { isSameDay, format } from 'date-fns';
+import { debounce } from 'lodash';
import RCContext from '../../context/RCInstance';
import classes from './SearchMessage.module.css';
-import { Markdown } from '../Markdown/index';
-import { useUserStore, useSearchMessageStore } from '../../store';
-import { Button } from '../Button';
+import { useSearchMessageStore } from '../../store';
import { Box } from '../Box';
import { Icon } from '../Icon';
import { ActionButton } from '../ActionButton';
-import { MessageContainer } from '../Message/MessageContainer';
import { MessageDivider } from '../Message/MessageDivider';
-import { MessageReactions } from '../Message/MessageReactions';
-import MessageHeader from '../Message/MessageHeader';
-import { MessageBody } from '../Message/MessageBody';
-import MessageBodyContainer from '../Message/MessageBodyContainer';
+import { Message } from '../Message';
const Search = () => {
const { RCInstance } = useContext(RCContext);
const setShowSearch = useSearchMessageStore((state) => state.setShowSearch);
- const authenticatedUserUsername = useUserStore((state) => state.username);
const toggleShowSearch = () => {
setShowSearch(false);
@@ -27,83 +21,135 @@ const Search = () => {
const [text, setText] = useState('');
const [messageList, setMessageList] = useState([]);
+ const handleInputChange = (e) => {
+ setText(e.target.value);
+ };
+
const searchMessages = async () => {
const { messages } = await RCInstance.getSearchMessages(text);
setMessageList(messages);
};
- const handleKeyPress = (e) => {
- if (e.key === 'Enter') {
- searchMessages();
+ const debouncedSearch = debounce(async () => {
+ await searchMessages();
+ }, 500); // 500ms delay
+
+ useEffect(() => {
+ if (!text.trim()) {
+ if (messageList.length > 0) {
+ setMessageList([]);
+ }
+ } else {
+ debouncedSearch();
}
- };
+
+ // Cleanup function to cancel the debounce on component unmount
+ return () => {
+ debouncedSearch.cancel();
+ };
+ }, [text, debouncedSearch, messageList.length]);
const isMessageNewDay = (current, previous) =>
!previous || !isSameDay(new Date(current.ts), new Date(previous.ts));
return (
-
-
-
- Search Messages
-
-
-
-
-
-
- setText(e.target.value)}
- onKeyDown={handleKeyPress}
- className={classes.textInput}
- />
+
+
+
+
+
+ Search Messages
+
+
+
+
+
+
+
+
-
-
- {messageList &&
- messageList.map((msg, index, arr) => {
- const prev = arr[index + 1];
- const newDay = isMessageNewDay(msg, prev);
- return (
-
- {newDay && (
-
- {format(new Date(msg.ts), 'MMMM d, yyyy')}
-
- )}
-
-
-
-
-
-
-
+
+
+ {messageList.length === 0 ? (
+
+
+
+ No results found
+
+
+ ) : (
+ messageList.map((msg, index, arr) => {
+ const prev = arr[index + 1];
+ const newDay = isMessageNewDay(msg, prev);
+ return (
+
+ {newDay && (
+
+
+ {format(new Date(msg.ts), 'MMMM d, yyyy')}
+
+
+ )}
+
-
-
-
- );
- })}
+
+ );
+ })
+ )}
+
+
);
};
diff --git a/packages/react/src/components/SearchMessage/SearchMessage.module.css b/packages/react/src/components/SearchMessage/SearchMessage.module.css
index 8369e2381..a90e7f4c6 100644
--- a/packages/react/src/components/SearchMessage/SearchMessage.module.css
+++ b/packages/react/src/components/SearchMessage/SearchMessage.module.css
@@ -11,6 +11,12 @@
z-index: 100;
}
+.wrapContainer {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
.container {
display: flex;
align-items: center;
diff --git a/packages/react/src/components/StaticSelect/StaticSelect.js b/packages/react/src/components/StaticSelect/StaticSelect.js
new file mode 100644
index 000000000..003f55611
--- /dev/null
+++ b/packages/react/src/components/StaticSelect/StaticSelect.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import { css, useTheme } from '@emotion/react';
+import PropTypes from 'prop-types';
+import useComponentOverrides from '../../theme/useComponentOverrides';
+
+const StaticSelect = ({
+ className = '',
+ style = {},
+ color = 'primary',
+ options = [],
+ placeholder = '',
+ onChange,
+ ...props
+}) => {
+ const { classNames, styleOverrides } = useComponentOverrides('StaticSelect');
+ const theme = useTheme();
+
+ const SelectCss = css`
+ position: relative;
+ display: inline-flex;
+ flex: 1 0 auto;
+ min-width: 8rem;
+ padding: 0.5rem 0.9375rem;
+ vertical-align: baseline;
+ outline: 0;
+ background-color: transparent;
+ letter-spacing: 0rem;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.25rem;
+ overflow: hidden;
+ color: #2f343d;
+ border-right: 0.9375rem transparent;
+ border-width: 1px;
+ border-color: #cbced1;
+ border-style: solid;
+ border-radius: 0.25rem;
+ background-color: white;
+ box-shadow: none;
+ -webkit-appearance: none;
+ appearance: none;
+ transition: all 230ms;
+ background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23111111" width="24px" height="24px"%3E%3Cpath d="M0 0h24v24H0z" fill="none"/%3E%3Cpath d="M7 10l5 5 5-5z" /%3E%3C/svg%3E');
+ background-size: 24px;
+ background-repeat: no-repeat;
+ background-position: calc(100% - 8px) center;
+ &:focus {
+ border-color: ${theme.palette[color].main || 'currentColor'};
+ box-shadow: 0px 0px 2.5px ${theme.palette[color].light || 'currentColor'};
+ }
+ `;
+
+ return (
+
+ );
+};
+
+StaticSelect.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ color: PropTypes.string,
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.string,
+ label: PropTypes.string,
+ })
+ ),
+ onChange: PropTypes.func,
+};
+
+export default StaticSelect;
diff --git a/packages/react/src/components/StaticSelect/index.js b/packages/react/src/components/StaticSelect/index.js
new file mode 100644
index 000000000..be1b753c7
--- /dev/null
+++ b/packages/react/src/components/StaticSelect/index.js
@@ -0,0 +1 @@
+export { default as StaticSelect } from './StaticSelect';
diff --git a/packages/react/src/components/Tooltip/Tooltip.js b/packages/react/src/components/Tooltip/Tooltip.js
index f014ae9dc..2e7612c2d 100644
--- a/packages/react/src/components/Tooltip/Tooltip.js
+++ b/packages/react/src/components/Tooltip/Tooltip.js
@@ -31,7 +31,7 @@ const Tooltip = ({ children, text, position }) => {
// Add more positions according to your needs and modify tooltipStyle and tooltipArrowStyle accordingly
if (position === 'top') {
- tooltipStyle.top = '-100%';
+ tooltipStyle.top = 'calc(-100% - 10px)'; // avoid overlaying the element
tooltipArrowStyle.top = '100%';
tooltipArrowStyle.transform = 'translateX(-50%)';
} else if (position === 'bottom') {
diff --git a/packages/react/src/components/UserMentions/UserMentions.js b/packages/react/src/components/UserMentions/UserMentions.js
new file mode 100644
index 000000000..1bd655aa6
--- /dev/null
+++ b/packages/react/src/components/UserMentions/UserMentions.js
@@ -0,0 +1,180 @@
+import React, { useState, useEffect } from 'react';
+import { css } from '@emotion/react';
+import { isSameDay, format } from 'date-fns';
+import classes from './UserMentions.module.css';
+import { Icon } from '../Icon';
+import { Box } from '../Box';
+import { Attachments } from '../Attachments';
+import { ActionButton } from '../ActionButton';
+import { useMessageStore, useUserStore, useMentionsStore } from '../../store';
+import { MessageBody } from '../Message/MessageBody';
+import { MessageMetrics } from '../Message/MessageMetrics';
+import { useRCContext } from '../../context/RCInstance';
+import { Markdown } from '../Markdown';
+import { MessageDivider } from '../Message/MessageDivider';
+import MessageAvatarContainer from '../Message/MessageAvatarContainer';
+import MessageBodyContainer from '../Message/MessageBodyContainer';
+import MessageHeader from '../Message/MessageHeader';
+
+const MessageCss = css`
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ padding-top: 0.5rem;
+ -webkit-padding-before: 0.5rem;
+ padding-block-start: 0.5rem;
+ padding-bottom: 0.25rem;
+ -webkit-padding-after: 0.25rem;
+ padding-block-end: 0.25rem;
+ padding-left: 1.25rem;
+ padding-right: 1.25rem;
+ padding-inline: 1.25rem;
+ &:hover {
+ background: #f2f3f5;
+ }
+`;
+
+const UserMentions = () => {
+ const showAvatar = useUserStore((state) => state.showAvatar);
+ const setShowMentions = useMentionsStore((state) => state.setShowMentions);
+ const { RCInstance } = useRCContext();
+ const [mentionedMessages, setMentionedMessages] = useState([]);
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ const openThread = useMessageStore((state) => state.openThread);
+
+ const toggleShowMentions = () => {
+ setShowMentions(false);
+ };
+ const handleOpenThread = (msg) => () => {
+ openThread(msg);
+ toggleShowMentions(false);
+ };
+ const isMessageNewDay = (current, previous) =>
+ !previous || !isSameDay(new Date(current.ts), new Date(previous.ts));
+
+ useEffect(() => {
+ const fetchMentionedMsgs = async () => {
+ const response = await RCInstance.getMentionedMessages();
+ if (response && response.messages) {
+ setMentionedMessages(response.messages);
+ setIsLoaded(true);
+ }
+ };
+ fetchMentionedMsgs();
+ }, [RCInstance, setMentionedMessages]);
+
+ return (
+
+
+
+
+
+
+
+ Mentions
+
+
+
+
+
+
+
+
+ {isLoaded && (
+
+ {mentionedMessages.length === 0 ? (
+
+
+
+ No mentions found
+
+
+ ) : (
+ mentionedMessages.map((message, index, arr) => {
+ const newDay =
+ index === 0 || isMessageNewDay(message, arr[index - 1]);
+ return (
+
+ {newDay ? (
+
+ {format(new Date(message.ts), 'MMMM d, yyyy')}
+
+ ) : null}
+
+ {showAvatar && (
+
+ )}
+
+
+
+
+ {message.attachments &&
+ message.attachments.length > 0 ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+ {!message.t && message.tcount && (
+
+ )}
+
+
+
+ );
+ })
+ )}
+
+ )}
+
+
+ );
+};
+
+export default UserMentions;
diff --git a/packages/react/src/components/UserMentions/UserMentions.module.css b/packages/react/src/components/UserMentions/UserMentions.module.css
new file mode 100644
index 000000000..cca051f27
--- /dev/null
+++ b/packages/react/src/components/UserMentions/UserMentions.module.css
@@ -0,0 +1,22 @@
+.component {
+ position: fixed;
+ right: 0;
+ top: 0;
+ width: 350px;
+ height: 100%;
+ overflow: hidden;
+ background-color: white;
+ box-shadow: -1px 0px 5px rgb(0 0 0 / 25%);
+ z-index: 100;
+}
+.wrapContainer {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+@media (max-width: 550px) {
+ .component {
+ width: 100vw;
+ }
+}
diff --git a/packages/react/src/components/uiKit/blocks/ContextBlock.Item.js b/packages/react/src/components/uiKit/blocks/ContextBlock.Item.js
index ee77575f3..45641507d 100644
--- a/packages/react/src/components/uiKit/blocks/ContextBlock.Item.js
+++ b/packages/react/src/components/uiKit/blocks/ContextBlock.Item.js
@@ -9,8 +9,9 @@ const Item = ({ block: element, surfaceRenderer: parser, index }) => {
useComponentOverrides('ContextBlockItem');
const ContextBlockCss = css`
display: inline-block;
+ padding: 0 0.75rem;
font-size: 0.8rem;
- color: #ffffff3f;
+ color: #3d3d3d;
margin: -0.25rem;
`;
const renderedElement = parser.renderContextBlockElement(element, index);
diff --git a/packages/react/src/components/uiKit/blocks/ContextBlock.js b/packages/react/src/components/uiKit/blocks/ContextBlock.js
index 8eaff8b41..d891693ee 100644
--- a/packages/react/src/components/uiKit/blocks/ContextBlock.js
+++ b/packages/react/src/components/uiKit/blocks/ContextBlock.js
@@ -30,6 +30,7 @@ const ContextBlock = ({ className, block, surfaceRenderer }) => {
>
{itemElements.map((element, i) => (
- (
-//
-//
-// {isPreviewBlockWithPreview(block) && block.preview?.dimensions && (
-//
-// )}
-//
-//
-//
-// ) : undefined
-// }
-// >
-// {Array.isArray(block.title) ? (
-//
-// {block.title.map((title) =>
-// surfaceRenderer.renderTextObject(
-// title,
-// 0,
-// UiKit.BlockContext.NONE
-// )
-// )}
-//
-// ) : null}
-// {Array.isArray(block.description) ? (
-//
-// {block.description.map((description) =>
-// surfaceRenderer.renderTextObject(
-// description,
-// 0,
-// UiKit.BlockContext.NONE
-// )
-// )}
-//
-// ) : null}
-// {block.footer && (
-//
-//
-//
-// )}
-//
-//
-//
-// );
+const PreviewBlock = ({ block, surfaceRenderer }) => (
+
+
+ {isPreviewBlockWithPreview(block) && block.preview?.dimensions && (
+
+ )}
+
+
+
+ ) : undefined
+ }
+ >
+ {Array.isArray(block.title) ? (
+
+ {block.title.map((title, index) =>
+ surfaceRenderer.renderTextObject(
+ title,
+ index,
+ UiKit.BlockContext.NONE
+ )
+ )}
+
+ ) : null}
+ {Array.isArray(block.description) ? (
+
+ {block.description.map((description) =>
+ surfaceRenderer.renderTextObject(
+ description,
+ 0,
+ UiKit.BlockContext.NONE
+ )
+ )}
+
+ ) : null}
+ {block.footer && (
+
+
+
+ )}
+
+
+
+);
-// export default memo(PreviewBlock);
+export default memo(PreviewBlock);
diff --git a/packages/react/src/components/uiKit/blocks/UiKitModal.js b/packages/react/src/components/uiKit/blocks/UiKitModal.js
deleted file mode 100644
index 7a3929bf7..000000000
--- a/packages/react/src/components/uiKit/blocks/UiKitModal.js
+++ /dev/null
@@ -1,164 +0,0 @@
-/* eslint-disable react/jsx-no-undef */
-/* eslint-disable import/extensions */
-/* eslint-disable import/no-unresolved */
-/* eslint-disable no-shadow */
-/* eslint-disable react/jsx-no-constructed-context-values */
-import React from 'react';
-import {
- useDebouncedCallback,
- useMutableCallback,
-} from '@rocket.chat/fuselage-hooks';
-import { kitContext } from '../contexts/kitContext';
-
-import * as ActionManager from '../../../../app/ui-message/client/ActionManager';
-import { detectEmoji } from '../../../lib/utils/detectEmoji';
-import ModalBlock from './ModalBlock';
-import { useActionManagerState } from './hooks/useActionManagerState';
-import { useValues } from './hooks/useValues';
-
-const UiKitModal = (props) => {
- const state = useActionManagerState(props);
-
- const { appId, viewId, mid: _mid, errors, view } = state;
-
- const [values, updateValues] = useValues(view.blocks);
-
- const groupStateByBlockId = (values) =>
- Object.entries(values).reduce((obj, [key, { blockId, value }]) => {
- obj[blockId] = obj[blockId] || {};
- obj[blockId][key] = value;
-
- return obj;
- }, {});
-
- const prevent = (e) => {
- if (e) {
- (e.nativeEvent || e).stopImmediatePropagation();
- e.stopPropagation();
- e.preventDefault();
- }
- };
-
- const debouncedBlockAction = useDebouncedCallback(
- (actionId, appId, value, blockId, mid) => {
- ActionManager.triggerBlockAction({
- container: {
- type: 'view',
- id: viewId,
- },
- actionId,
- appId,
- value,
- blockId,
- mid,
- });
- },
- 700
- );
-
- // TODO: this structure is atrociously wrong; we should revisit this
- const context = {
- // @ts-expect-error Property 'mid' does not exist on type 'ActionParams'.
- action: ({
- actionId,
- appId,
- value,
- blockId,
- mid = _mid,
- dispatchActionConfig,
- }) => {
- if (
- Array.isArray(dispatchActionConfig) &&
- dispatchActionConfig.includes('on_character_entered')
- ) {
- debouncedBlockAction(actionId, appId, value, blockId, mid);
- } else {
- ActionManager.triggerBlockAction({
- container: {
- type: 'view',
- id: viewId,
- },
- actionId,
- appId,
- value,
- blockId,
- mid,
- });
- }
- },
-
- state: ({ actionId, value, /* ,appId, */ blockId = 'default' }) => {
- updateValues({
- actionId,
- payload: {
- blockId,
- value,
- },
- });
- },
- ...state,
- values,
- };
-
- const handleSubmit = useMutableCallback((e) => {
- prevent(e);
- ActionManager.triggerSubmitView({
- viewId,
- appId,
- payload: {
- view: {
- ...view,
- id: viewId,
- state: groupStateByBlockId(values),
- },
- },
- });
- });
-
- const handleCancel = useMutableCallback((e) => {
- prevent(e);
- ActionManager.triggerCancel({
- viewId,
- appId,
- view: {
- ...view,
- id: viewId,
- state: groupStateByBlockId(values),
- },
- });
- });
-
- const handleClose = useMutableCallback(() => {
- ActionManager.triggerCancel({
- viewId,
- appId,
- view: {
- ...view,
- id: viewId,
- state: groupStateByBlockId(values),
- },
- isCleared: true,
- });
- });
-
- return (
-
-
-
-
-
- );
-};
-
-export default UiKitModal;
diff --git a/packages/react/src/components/uiKit/elements/MultiStaticSelectElement.js b/packages/react/src/components/uiKit/elements/MultiStaticSelectElement.js
index 7d389c847..6a150cd0f 100644
--- a/packages/react/src/components/uiKit/elements/MultiStaticSelectElement.js
+++ b/packages/react/src/components/uiKit/elements/MultiStaticSelectElement.js
@@ -1,44 +1,39 @@
-// import { MultiSelectFiltered } from '@rocket.chat/fuselage';
-// import React, { memo, useCallback, useMemo } from 'react';
+import React, { memo, useMemo, useCallback } from 'react';
-// import { useUiKitState } from '../hooks/useUiKitState';
-// import { fromTextObjectToString } from '../utils/fromTextObjectToString';
+import { useUiKitState } from '../hooks/useUiKitState';
+import { fromTextObjectToString } from '../utils/fromTextObjectToString';
+import { MultiSelect } from '../../MultiSelect';
-// const MultiStaticSelectElement = ({ block, context, surfaceRenderer }) => {
-// const [{ loading, value, error }, action] = useUiKitState(block, context);
+const MultiStaticSelectElement = ({ block, context, surfaceRenderer }) => {
+ const [{ loading }, action] = useUiKitState(block, context);
-// const options = useMemo(
-// () =>
-// // eslint-disable-next-line no-shadow
-// block.options.map(({ value, text }, i) => [
-// value,
-// fromTextObjectToString(surfaceRenderer, text, i) ?? '',
-// ]),
-// [block.options, surfaceRenderer]
-// );
+ const options = useMemo(
+ () =>
+ block.options.map((option, i) => ({
+ value: option.value,
+ label: fromTextObjectToString(surfaceRenderer, option.text, i) ?? '',
+ })),
+ [block.options, surfaceRenderer]
+ );
-// const handleChange = useCallback(
-// // eslint-disable-next-line no-shadow
-// (value) => {
-// action({ target: { value } });
-// },
-// [action]
-// );
+ const handleChange = useCallback(
+ (val) => {
+ action({ target: { val } });
+ },
+ [action]
+ );
-// return (
-//
-// );
-// };
+ return (
+
+ );
+};
-// export default memo(MultiStaticSelectElement);
+export default memo(MultiStaticSelectElement);
diff --git a/packages/react/src/components/uiKit/elements/OverflowElement.js b/packages/react/src/components/uiKit/elements/OverflowElement.js
index 66c085813..462ae212d 100644
--- a/packages/react/src/components/uiKit/elements/OverflowElement.js
+++ b/packages/react/src/components/uiKit/elements/OverflowElement.js
@@ -1,83 +1,44 @@
-// import {
-// IconButton,
-// PositionAnimated,
-// Options,
-// useCursor,
-// } from '@rocket.chat/fuselage';
-// import React, { useRef, useCallback, useMemo } from 'react';
-
-// import { useUiKitState } from '../hooks/useUiKitState';
-// import { fromTextObjectToString } from '../utils/fromTextObjectToString';
-
-// const OverflowElement = ({ block, context, surfaceRenderer }) => {
-// const [{ loading }, action] = useUiKitState(block, context);
-
-// const fireChange = useCallback(
-// ([value]) => action({ target: { value } }),
-// [action]
-// );
-
-// const options = useMemo(
-// () =>
-// block.options.map(({ value, text, url }, i) => [
-// value,
-// fromTextObjectToString(surfaceRenderer, text, i) ?? '',
-// undefined,
-// undefined,
-// undefined,
-// url,
-// ]),
-// [block.options, surfaceRenderer]
-// );
-
-// const [cursor, handleKeyDown, handleKeyUp, reset, [visible, hide, show]] =
-// // eslint-disable-next-line no-shadow
-// useCursor(-1, options, (selectedOption, [, hide]) => {
-// fireChange([selectedOption[0], selectedOption[1]]);
-// reset();
-// hide();
-// });
-
-// const ref = useRef(null);
-// const onClick = useCallback(() => {
-// ref.current?.focus();
-// show();
-// }, [show]);
-
-// const handleSelection = useCallback(
-// ([value, _label, _selected, _type, url]) => {
-// if (url) {
-// window.open(url);
-// }
-// action({ target: { value: String(value) } });
-// reset();
-// hide();
-// },
-// [action, hide, reset]
-// );
-
-// return (
-// <>
-//
-//
-//
-//
-// >
-// );
-// };
-
-// export default OverflowElement;
+import React, { useMemo, memo } from 'react';
+import { Menu } from '../../Menu';
+import { Box } from '../../Box';
+import { useUiKitState } from '../hooks/useUiKitState';
+import { fromTextObjectToString } from '../utils/fromTextObjectToString';
+
+const OverflowElement = ({ block, context, surfaceRenderer }) => {
+ const [{ loading }, action] = useUiKitState(block, context);
+
+ const options = useMemo(
+ () =>
+ block.options.map(({ value, text, url }, i) => ({
+ id: value,
+ label: fromTextObjectToString(surfaceRenderer, text, i) ?? '',
+ icon: undefined,
+ action: () => {
+ if (url) {
+ window.open(url);
+ }
+ action({ target: { value: String(value) } });
+ },
+ })),
+ [action, block.options, surfaceRenderer]
+ );
+
+ return (
+
+
+
+ );
+};
+
+export default memo(OverflowElement);
diff --git a/packages/react/src/components/uiKit/elements/PlainTextInputElement.js b/packages/react/src/components/uiKit/elements/PlainTextInputElement.js
index fc640853a..c2fbfc378 100644
--- a/packages/react/src/components/uiKit/elements/PlainTextInputElement.js
+++ b/packages/react/src/components/uiKit/elements/PlainTextInputElement.js
@@ -28,7 +28,6 @@ const PlainTextInputElement = ({ block, context, surfaceRenderer }) => {
return (
{
-// const [{ loading, value, error }, action] = useUiKitState(block, context);
+const StaticSelectElement = ({ block, context, surfaceRenderer }) => {
+ const [{ loading }, action] = useUiKitState(block, context);
-// const options = useMemo(
-// () =>
-// block.options.map((option, i) => [
-// option.value,
-// fromTextObjectToString(surfaceRenderer, option.text, i) ?? '',
-// ]),
-// [block.options, surfaceRenderer]
-// );
+ const options = useMemo(
+ () =>
+ block.options.map((option, i) => ({
+ value: option.value,
+ label: fromTextObjectToString(surfaceRenderer, option.text, i) ?? '',
+ })),
+ [block.options, surfaceRenderer]
+ );
-// const handleChange = useCallback(
-// // eslint-disable-next-line no-shadow
-// (value) => {
-// action({ target: { value } });
-// },
-// [action]
-// );
+ const handleChange = useCallback(
+ (val) => {
+ action({ target: { val } });
+ },
+ [action]
+ );
-// return (
-//
-// );
-// };
+ return (
+
+ );
+};
-// export default memo(StaticSelectElement);
+export default memo(StaticSelectElement);
diff --git a/packages/react/src/components/uiKit/surfaces/FuselageSurfaceRenderer.js b/packages/react/src/components/uiKit/surfaces/FuselageSurfaceRenderer.js
index 12fabb478..a301ba51a 100644
--- a/packages/react/src/components/uiKit/surfaces/FuselageSurfaceRenderer.js
+++ b/packages/react/src/components/uiKit/surfaces/FuselageSurfaceRenderer.js
@@ -6,16 +6,16 @@ import ContextBlock from '../blocks/ContextBlock';
import DividerBlock from '../blocks/DividerBlock';
import ImageBlock from '../blocks/ImageBlock';
import InputBlock from '../blocks/InputBlock';
-// import PreviewBlock from '../blocks/PreviewBlock';
+import PreviewBlock from '../blocks/PreviewBlock';
import SectionBlock from '../blocks/SectionBlock';
import ButtonElement from '../elements/ButtonElement';
import DatePickerElement from '../elements/DatePickerElement';
import ImageElement from '../elements/ImageElement';
import LinearScaleElement from '../elements/LinearScaleElement';
-// import MultiStaticSelectElement from '../elements/MultiStaticSelectElement';
-// import OverflowElement from '../elements/OverflowElement';
+import MultiStaticSelectElement from '../elements/MultiStaticSelectElement';
+import OverflowElement from '../elements/OverflowElement';
import PlainTextInputElement from '../elements/PlainTextInputElement';
-// import StaticSelectElement from '../elements/StaticSelectElement';
+import StaticSelectElement from '../elements/StaticSelectElement';
import { Markup } from '../../Markup';
export class FuselageSurfaceRenderer extends UiKit.SurfaceRenderer {
@@ -79,18 +79,16 @@ export class FuselageSurfaceRenderer extends UiKit.SurfaceRenderer {
if (context !== UiKit.BlockContext.BLOCK) {
return null;
}
- return null;
- // TODO: Implement this without fuselage.
- // return (
- //
- // );
+ return (
+
+ );
}
context(block, context, index) {
@@ -218,17 +216,15 @@ export class FuselageSurfaceRenderer extends UiKit.SurfaceRenderer {
return null;
}
- return null;
- // implement this without fuselage
- // return (
- //
- // );
+ return (
+
+ );
}
multi_static_select(block, context, index) {
@@ -236,17 +232,15 @@ export class FuselageSurfaceRenderer extends UiKit.SurfaceRenderer {
return null;
}
- return null;
- // implement this without fuselage
- // return (
- //
- // );
+ return (
+
+ );
}
overflow(block, context, index) {
@@ -254,17 +248,15 @@ export class FuselageSurfaceRenderer extends UiKit.SurfaceRenderer {
return null;
}
- return null;
- // implement this without fuselage
- // return (
- //
- // );
+ return (
+
+ );
}
plain_text_input(block, context, index) {
diff --git a/packages/react/src/components/uiKit/surfaces/ModalSurface.js b/packages/react/src/components/uiKit/surfaces/ModalSurface.js
index b7be565b5..865619588 100644
--- a/packages/react/src/components/uiKit/surfaces/ModalSurface.js
+++ b/packages/react/src/components/uiKit/surfaces/ModalSurface.js
@@ -4,7 +4,16 @@ import { Box } from '../../Box';
const ModalSurface = ({ children }) => (
- {children}
+
+ {children}
+
);
diff --git a/packages/react/src/lib/searchToMentionUser.js b/packages/react/src/lib/searchToMentionUser.js
index 6dea02772..5ca80136c 100644
--- a/packages/react/src/lib/searchToMentionUser.js
+++ b/packages/react/src/lib/searchToMentionUser.js
@@ -7,7 +7,7 @@ export const searchToMentionUser = (
setmentionIndex,
setshowMembersList
) => {
- const lastChar = message[message.length - 1];
+ const lastChar = message ? message[message.length - 1] : '';
if (message.length === 0) {
setshowMembersList(false);
setStartReading(false);
@@ -29,22 +29,30 @@ export const searchToMentionUser = (
setmentionIndex(-1);
setshowMembersList(false);
} else {
- const c = message.lastIndexOf('@');
+ const query = message
+ .substring(message.lastIndexOf('@') + 1)
+ .toLowerCase();
+ const filteredMentionMembers = roomMembers.filter(
+ (member) =>
+ member.name.toLowerCase().includes(query) ||
+ member.username.toLowerCase().includes(query)
+ );
+
+ setFilteredMembers(filteredMentionMembers);
- setFilteredMembers(
- roomMembers.filter(
- (member) =>
- member.name
- .toLowerCase()
- .includes(message.substring(c + 1).toLowerCase()) ||
- member.username
- .toLowerCase()
- .includes(message.substring(c + 1).toLowerCase())
- )
+ const isValidUsername = roomMembers.some(
+ (member) =>
+ member.name.toLowerCase().includes(query) ||
+ member.username.toLowerCase().includes(query)
);
- setshowMembersList(true);
- setmentionIndex(0);
+ if (isValidUsername) {
+ setshowMembersList(true);
+ setmentionIndex(0);
+ } else {
+ setshowMembersList(false);
+ setmentionIndex(-1);
+ }
}
}
};
diff --git a/packages/react/src/store/index.js b/packages/react/src/store/index.js
index 1d734fbbe..78da56784 100644
--- a/packages/react/src/store/index.js
+++ b/packages/react/src/store/index.js
@@ -7,3 +7,4 @@ export { default as useSearchMessageStore } from './searchMessageStore';
export { default as loginModalStore } from './loginmodalStore';
export { default as useChannelStore } from './channelStore';
export { default as useThreadsMessageStore } from './threadsMessageStore';
+export { default as useMentionsStore } from './mentionsStore';
diff --git a/packages/react/src/store/mentionmemberStore.js b/packages/react/src/store/mentionmemberStore.js
deleted file mode 100644
index 3025c9248..000000000
--- a/packages/react/src/store/mentionmemberStore.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { create } from 'zustand';
-
-const mentionmemberStore = create((set) => ({
- roomMembers: {},
- showMembersList: false,
- toggleShowMembers: (showMembersList) => set({ showMembersList }),
- setRoomMembers: (roomMembers) => set({ roomMembers }),
-}));
-
-export default mentionmemberStore;
diff --git a/packages/react/src/store/mentionsStore.js b/packages/react/src/store/mentionsStore.js
new file mode 100644
index 000000000..240f0602a
--- /dev/null
+++ b/packages/react/src/store/mentionsStore.js
@@ -0,0 +1,8 @@
+import { create } from 'zustand';
+
+const useMentionsStore = create((set) => ({
+ showMentions: false,
+ setShowMentions: (showMentions) => set(() => ({ showMentions })),
+}));
+
+export default useMentionsStore;
diff --git a/packages/react/tools/icons-generator.js b/packages/react/tools/icons-generator.js
index c39f180c6..710904c97 100644
--- a/packages/react/tools/icons-generator.js
+++ b/packages/react/tools/icons-generator.js
@@ -39,6 +39,9 @@ const iconsList = [
'kebab',
'check',
'error-circle',
+ 'chevron-down',
+ 'chevron-left',
+ 'key',
];
const svgDirPath = path.join(
__dirname,