From cb17c937ae27f06ec1e33c03c98301edb18e9970 Mon Sep 17 00:00:00 2001 From: Ahmed_Kashkoush <89735230+ahmad-kashkoush@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:10:36 +0200 Subject: [PATCH 01/14] Fix: fix typo in README (#477) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7caed56247a4e76430ee4b4eeee8e9b2f0d1ddcb Mon Sep 17 00:00:00 2001 From: Umang Utkarsh <95426993+umangutkarsh@users.noreply.github.com> Date: Sun, 25 Feb 2024 00:30:34 +0530 Subject: [PATCH 02/14] fix: user mentions (#476) * searchToMentionUser-logic * improv-MembersList * logic-integrate/remove-store * fix/lint-err * fix/bugs * fix/linting * bug-fix * resolve-lint-issue * remove-logs * format-code * issues-fix --------- Co-authored-by: Sidharth Mohanty --- .../src/components/ChatInput/ChatInput.js | 129 +++++++++++------- .../src/components/Markup/elements/Mention.js | 36 ++++- .../src/components/Mentions/MembersList.js | 127 +++++++++++++++-- packages/react/src/lib/searchToMentionUser.js | 36 +++-- .../react/src/store/mentionmemberStore.js | 10 -- 5 files changed, 247 insertions(+), 91 deletions(-) delete mode 100644 packages/react/src/store/mentionmemberStore.js diff --git a/packages/react/src/components/ChatInput/ChatInput.js b/packages/react/src/components/ChatInput/ChatInput.js index 026b152f0..7852f90cb 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,6 +22,7 @@ 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'; @@ -45,12 +46,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 +80,19 @@ 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 { editMessage, @@ -143,7 +146,7 @@ const ChatInput = ({ scrollToBottom }) => { }; const sendMessage = async () => { - scrollToBottom(); + messageRef.current.focus(); messageRef.current.style.height = '44px'; const message = messageRef.current.value.trim(); if (!message.length || !isUserAuthenticated) { @@ -207,6 +210,8 @@ const ChatInput = ({ scrollToBottom }) => { setDisableButton(true); setEditMessage({}); } + + scrollToBottom(); }; const sendAttachment = (event) => { @@ -217,16 +222,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 +230,6 @@ const ChatInput = ({ scrollToBottom }) => { messageRef.current.value = ''; } }, [editMessage]); - useEffect(() => { - getAllChannelMembers(); - }, [getAllChannelMembers]); const username = useUserStore((state) => state.username); const timerRef = useRef(); @@ -285,6 +277,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 +340,7 @@ const ChatInput = ({ scrollToBottom }) => { } searchToMentionUser( messageRef.current.value, - roomMembers, + members, startReading, setStartReading, setFilteredMembers, @@ -386,36 +406,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 +459,14 @@ const ChatInput = ({ scrollToBottom }) => { `} > {showMembersList ? ( - + <> + + + ) : ( <> )} 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/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/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; From d35fed0117801ee7e66917f12b4db993d48e6699 Mon Sep 17 00:00:00 2001 From: Jeffrey Yu <35394596+JeffreytheCoder@users.noreply.github.com> Date: Thu, 29 Feb 2024 06:11:11 -0800 Subject: [PATCH 03/14] Align invitation sidebar UI with RocketChat (#483) --- .../src/components/Icon/icons/Clipboard.js | 14 +++++ .../react/src/components/Icon/icons/index.js | 2 + .../inviteMembers/InviteMembers.js | 62 +++++++++++++++---- 3 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 packages/react/src/components/Icon/icons/Clipboard.js 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/index.js b/packages/react/src/components/Icon/icons/index.js index 2ea3fa05a..ea226f3e4 100644 --- a/packages/react/src/components/Icon/icons/index.js +++ b/packages/react/src/components/Icon/icons/index.js @@ -38,6 +38,7 @@ import ArrowDown from './ArrowDown'; import PinFilled from './PinFilled'; import VideoRecorder from './VideoRecoder'; import DisabledRecorder from './DisableRecorder'; +import Clipboard from './Clipboard'; const icons = { file: File, @@ -80,6 +81,7 @@ const icons = { 'error-circle': ErrorCircle, 'arrow-down': ArrowDown, 'pin-filled': PinFilled, + clipboard: Clipboard, }; export default icons; diff --git a/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js b/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js index 42af40076..c09de515a 100644 --- a/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js +++ b/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import classes from '../RoomMember.module.css'; @@ -10,20 +10,26 @@ import { ActionButton } from '../../ActionButton'; const InviteMembers = ({ inviteData }) => { const toggleInviteView = useInviteStore((state) => state.toggleInviteView); + const [isCopied, setIsCopied] = useState(false); + const copyToClipboard = (url) => { + navigator.clipboard.writeText(url); + setIsCopied(true); + }; return (

- + + toggleInviteView()} ghost size="small"> + + + { > Invite Members - toggleInviteView()} ghost size="small"> - -

{ width: 100%; display: flex; flex-direction: column; + margin-bottom: 5px; `} > - Invite Link - + + Invite Link + + + + copyToClipboard(inviteData.url)} + ghost + size="small" + css={css` + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + padding: 0; + `} + > + + + - + Your invite link will expire on{' '} {new Date(inviteData.expires).toString().split('GMT')[0]} From 79ee4cb7ced0606527a7b320e7ba57a20ac2f208 Mon Sep 17 00:00:00 2001 From: Sayan4444 <112304873+Sayan4444@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:26:55 +0530 Subject: [PATCH 04/14] user avatar not visible after pinning a message (#484) * fix/User avatar not visible after pinning a message. #466 * prettier code formatting done * fixed linting issue --- .../Attachments/PinnedAttachment.js | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/Attachments/PinnedAttachment.js b/packages/react/src/components/Attachments/PinnedAttachment.js index ae694b2a6..0255cb9ea 100644 --- a/packages/react/src/components/Attachments/PinnedAttachment.js +++ b/packages/react/src/components/Attachments/PinnedAttachment.js @@ -1,18 +1,46 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { Box } from '../Box'; +import { Avatar } from '../Avatar'; +import RCContext from '../../context/RCInstance'; -const PinnedAttachment = ({ attachment }) => ( - - {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; From da8b9303d2e69c81221ba05e5c5ac05aa5f2b9a7 Mon Sep 17 00:00:00 2001 From: Zishan Ahmad Date: Sun, 3 Mar 2024 18:38:44 +0530 Subject: [PATCH 05/14] feat: added a user-mention menu (#480) Co-authored-by: Sidharth Mohanty --- packages/api/src/EmbeddedChatApi.ts | 20 ++ .../src/components/ChatHeader/ChatHeader.js | 16 +- .../react/src/components/Icon/icons/At.js | 17 ++ .../react/src/components/Icon/icons/index.js | 2 + .../react/src/components/Menu/Menu.stories.js | 5 + .../src/components/MessageList/MessageList.js | 7 +- .../components/UserMentions/UserMentions.js | 180 ++++++++++++++++++ .../UserMentions/UserMentions.module.css | 22 +++ packages/react/src/store/index.js | 1 + packages/react/src/store/mentionsStore.js | 8 + 10 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 packages/react/src/components/Icon/icons/At.js create mode 100644 packages/react/src/components/UserMentions/UserMentions.js create mode 100644 packages/react/src/components/UserMentions/UserMentions.module.css create mode 100644 packages/react/src/store/mentionsStore.js 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/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/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/index.js b/packages/react/src/components/Icon/icons/index.js index ea226f3e4..b66ac20cc 100644 --- a/packages/react/src/components/Icon/icons/index.js +++ b/packages/react/src/components/Icon/icons/index.js @@ -39,6 +39,7 @@ import PinFilled from './PinFilled'; import VideoRecorder from './VideoRecoder'; import DisabledRecorder from './DisableRecorder'; import Clipboard from './Clipboard'; +import At from './At'; const icons = { file: File, @@ -82,6 +83,7 @@ const icons = { 'arrow-down': ArrowDown, 'pin-filled': PinFilled, clipboard: Clipboard, + at: At, }; export default icons; diff --git a/packages/react/src/components/Menu/Menu.stories.js b/packages/react/src/components/Menu/Menu.stories.js index 6a5235714..92da52d89 100644 --- a/packages/react/src/components/Menu/Menu.stories.js +++ b/packages/react/src/components/Menu/Menu.stories.js @@ -18,6 +18,11 @@ export const Menu = { label: 'Threads', icon: 'thread', }, + { + id: 'mentions', + label: 'Mentions', + icon: 'at', + }, { id: 'members', label: 'Members', 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/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/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/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; From a6cd592141af607c7be9dd581a6180f94d28a7b6 Mon Sep 17 00:00:00 2001 From: Jeffrey Yu <35394596+JeffreytheCoder@users.noreply.github.com> Date: Fri, 8 Mar 2024 07:19:26 -0800 Subject: [PATCH 06/14] Add download function and file name to messages attachments (#489) * Add download function and file name to messages attachments * Fix linting errors --- .../Attachments/AttachmentMetadata.js | 39 +++++++++++++++++++ .../components/Attachments/AudioAttachment.js | 6 ++- .../components/Attachments/ImageAttachment.js | 6 ++- .../components/Attachments/VideoAttachment.js | 6 ++- .../src/components/Icon/icons/Download.js | 14 +++++++ .../react/src/components/Icon/icons/index.js | 2 + 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 packages/react/src/components/Attachments/AttachmentMetadata.js create mode 100644 packages/react/src/components/Icon/icons/Download.js 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?.description}

+ 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/index.js b/packages/react/src/components/Icon/icons/index.js index b66ac20cc..13cf98f97 100644 --- a/packages/react/src/components/Icon/icons/index.js +++ b/packages/react/src/components/Icon/icons/index.js @@ -39,6 +39,7 @@ import PinFilled from './PinFilled'; import VideoRecorder from './VideoRecoder'; import DisabledRecorder from './DisableRecorder'; import Clipboard from './Clipboard'; +import Download from './Download'; import At from './At'; const icons = { @@ -83,6 +84,7 @@ const icons = { 'arrow-down': ArrowDown, 'pin-filled': PinFilled, clipboard: Clipboard, + download: Download, at: At, }; From 29bf9ca056fbb257cbb78c28e57d80d89c4851e0 Mon Sep 17 00:00:00 2001 From: Akshun Kuthiala <93793691+Akshun-01@users.noreply.github.com> Date: Fri, 8 Mar 2024 20:51:30 +0530 Subject: [PATCH 07/14] Handle 'Message Too Long" (#493) * add alert for long message and logic to convert it into file * fix linting issues * prettier check --- .../src/components/ChatInput/ChatInput.js | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/packages/react/src/components/ChatInput/ChatInput.js b/packages/react/src/components/ChatInput/ChatInput.js index 7852f90cb..8f1237023 100644 --- a/packages/react/src/components/ChatInput/ChatInput.js +++ b/packages/react/src/components/ChatInput/ChatInput.js @@ -25,6 +25,7 @@ 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; @@ -94,6 +95,9 @@ const ChatInput = ({ scrollToBottom }) => { (state) => state.setIsLoginModalOpen ); + const [errorModal, setErrorModal] = useState(false); + const [isAttachmentMode, setIsAttachmentMode] = useState(false); + const { editMessage, setEditMessage, @@ -126,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) { @@ -149,6 +164,30 @@ const ChatInput = ({ 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) { @@ -214,6 +253,10 @@ const ChatInput = ({ scrollToBottom }) => { scrollToBottom(); }; + useEffect(() => { + if (isAttachmentMode) sendMessage(); + }, [isAttachmentMode, sendMessage]); + const sendAttachment = (event) => { const fileObj = event.target.files && event.target.files[0]; if (!fileObj) { @@ -530,6 +573,40 @@ const ChatInput = ({ scrollToBottom }) => { /> )}
+ {errorModal && ( + + + + + + Message Too Long! + + + + + {' '} + Send it as attachment instead?{' '} + + + + + + + + )}
); }; From 94d2222d8d030f0aba9cb6ad0f84f2464181634b Mon Sep 17 00:00:00 2001 From: Jeffrey Yu <35394596+JeffreytheCoder@users.noreply.github.com> Date: Fri, 8 Mar 2024 07:24:03 -0800 Subject: [PATCH 08/14] Make search tab consistent with threads & RC (#497) * Make search tab consistent with threads & RC * Fix linting errors --- .../react/src/components/Message/Message.js | 3 +- .../components/SearchMessage/SearchMessage.js | 188 +++++++++++------- .../SearchMessage/SearchMessage.module.css | 6 + 3 files changed, 125 insertions(+), 72 deletions(-) diff --git a/packages/react/src/components/Message/Message.js b/packages/react/src/components/Message/Message.js index f07cc9727..2bbb640a9 100644 --- a/packages/react/src/components/Message/Message.js +++ b/packages/react/src/components/Message/Message.js @@ -53,6 +53,7 @@ const Message = ({ showAvatar = false, className = '', style = {}, + showToolbox = true, }) => { const { classNames, styleOverrides } = useComponentOverrides( 'Message', @@ -215,7 +216,7 @@ const Message = ({ handleOpenThread={handleOpenThread} /> ) : null} - {!message.t ? ( + {!message.t && showToolbox ? ( { const { RCInstance } = useContext(RCContext); const setShowSearch = useSearchMessageStore((state) => state.setShowSearch); - const authenticatedUserUsername = useUserStore((state) => state.username); const toggleShowSearch = () => { setShowSearch(false); @@ -27,83 +22,134 @@ 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()) { + setMessageList([]); + } else { + debouncedSearch(); } - }; + + // Cleanup function to cancel the debounce on component unmount + return () => { + debouncedSearch.cancel(); + }; + }, [text, debouncedSearch]); 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); + const sequential = isMessageSequential(msg, prev, 300); + 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; From 4a2666b6d99111c5b4e7b5f6d48fa7f62324fda6 Mon Sep 17 00:00:00 2001 From: Jeffrey Yu <35394596+JeffreytheCoder@users.noreply.github.com> Date: Fri, 8 Mar 2024 07:24:36 -0800 Subject: [PATCH 09/14] Add tooltips to message toolbox (#487) Co-authored-by: Sidharth Mohanty --- .../src/components/Message/MessageToolbox.js | 115 +++++++++++------- .../react/src/components/Tooltip/Tooltip.js | 2 +- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/packages/react/src/components/Message/MessageToolbox.js b/packages/react/src/components/Message/MessageToolbox.js index 8826bc026..0e45bb379 100644 --- a/packages/react/src/components/Message/MessageToolbox.js +++ b/packages/react/src/components/Message/MessageToolbox.js @@ -10,6 +10,7 @@ import { Modal } from '../Modal'; import { Icon } from '../Icon'; import { Button } from '../Button'; import { parseEmoji } from '../../lib/emoji'; +import { Tooltip } from '../Tooltip'; const MessageToolboxWrapperCss = css` display: none; @@ -88,30 +89,44 @@ export const MessageToolbox = ({ {...props} > {!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)} - /> + handleClickDelete(message)} + icon={`${message.pinned ? 'pin-filled' : 'pin'}`} + onClick={() => handlePinMessage(message)} /> + + )} + {message.u._id === authenticatedUserId && ( + <> + + handleEditMessage(message)} + /> + + + handleClickDelete(message)} + /> + )} - handlerReportMessage(message)} - /> + + handlerReportMessage(message)} + /> +
{showDeleteModal && ( 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') { From c49d93c5c1815ec9468f44fa2717061a3d508d99 Mon Sep 17 00:00:00 2001 From: Zishan Ahmad Date: Fri, 8 Mar 2024 16:26:57 +0100 Subject: [PATCH 10/14] fix: added preview link (#495) * added link preview added chevron icons added dropdown to toggle link preview * removed console log --- .../src/components/Icon/icons/ChevronDown.js | 14 ++ .../src/components/Icon/icons/ChevronLeft.js | 15 ++ .../react/src/components/Icon/icons/index.js | 4 + .../src/components/LinkPreview/LinkPreview.js | 128 ++++++++++++++++++ .../react/src/components/LinkPreview/index.js | 1 + .../react/src/components/Message/Message.js | 14 ++ packages/react/tools/icons-generator.js | 2 + 7 files changed, 178 insertions(+) create mode 100644 packages/react/src/components/Icon/icons/ChevronDown.js create mode 100644 packages/react/src/components/Icon/icons/ChevronLeft.js create mode 100644 packages/react/src/components/LinkPreview/LinkPreview.js create mode 100644 packages/react/src/components/LinkPreview/index.js 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/index.js b/packages/react/src/components/Icon/icons/index.js index 13cf98f97..a74a56d5b 100644 --- a/packages/react/src/components/Icon/icons/index.js +++ b/packages/react/src/components/Icon/icons/index.js @@ -41,6 +41,8 @@ import DisabledRecorder from './DisableRecorder'; import Clipboard from './Clipboard'; import Download from './Download'; import At from './At'; +import ChevronDown from './ChevronDown'; +import ChevronLeft from './ChevronLeft'; const icons = { file: File, @@ -86,6 +88,8 @@ const icons = { clipboard: Clipboard, download: Download, at: At, + 'chevron-down': ChevronDown, + 'chevron-left': ChevronLeft, }; 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 && ( + + + {meta.ogImageAlt} + + + )} + + + + {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/Message/Message.js b/packages/react/src/components/Message/Message.js index 2bbb640a9..b2b5ccff9 100644 --- a/packages/react/src/components/Message/Message.js +++ b/packages/react/src/components/Message/Message.js @@ -20,6 +20,7 @@ import { MessageDivider } from './MessageDivider'; import { useToastBarDispatch } from '../../hooks/useToastBarDispatch'; import MessageAvatarContainer from './MessageAvatarContainer'; import MessageBodyContainer from './MessageBodyContainer'; +import { LinkPreview } from '../LinkPreview'; const MessageCss = css` display: flex; @@ -187,6 +188,19 @@ const Message = ({ ) : ( )} + + {message.urls && + message.urls.map( + (url, index) => + url.meta && ( + + ) + )} + {message.blocks && ( Date: Fri, 8 Mar 2024 22:42:28 +0530 Subject: [PATCH 11/14] fixed infinite render and console errors (#503) --- .../src/components/SearchMessage/SearchMessage.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/SearchMessage/SearchMessage.js b/packages/react/src/components/SearchMessage/SearchMessage.js index f6f775f8c..06d289734 100644 --- a/packages/react/src/components/SearchMessage/SearchMessage.js +++ b/packages/react/src/components/SearchMessage/SearchMessage.js @@ -1,15 +1,14 @@ -import React, { useState, useContext, useMemo, useEffect } 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 { useUserStore, useSearchMessageStore } from '../../store'; +import { useSearchMessageStore } from '../../store'; import { Box } from '../Box'; import { Icon } from '../Icon'; import { ActionButton } from '../ActionButton'; import { MessageDivider } from '../Message/MessageDivider'; import { Message } from '../Message'; -import isMessageSequential from '../../lib/isMessageSequential'; const Search = () => { const { RCInstance } = useContext(RCContext); @@ -37,7 +36,9 @@ const Search = () => { useEffect(() => { if (!text.trim()) { - setMessageList([]); + if (messageList.length > 0) { + setMessageList([]); + } } else { debouncedSearch(); } @@ -46,7 +47,7 @@ const Search = () => { return () => { debouncedSearch.cancel(); }; - }, [text, debouncedSearch]); + }, [text, debouncedSearch, messageList.length]); const isMessageNewDay = (current, previous) => !previous || !isSameDay(new Date(current.ts), new Date(previous.ts)); @@ -69,7 +70,7 @@ const Search = () => { Search Messages - + @@ -125,7 +126,6 @@ const Search = () => { messageList.map((msg, index, arr) => { const prev = arr[index + 1]; const newDay = isMessageNewDay(msg, prev); - const sequential = isMessageSequential(msg, prev, 300); return ( {newDay && ( From da9a856893a475f3a9dd419eaafb5f97ab456321 Mon Sep 17 00:00:00 2001 From: Shivang Yadav <125182653+shivang-16@users.noreply.github.com> Date: Sat, 9 Mar 2024 18:18:48 +0530 Subject: [PATCH 12/14] fixed toggle functioning in chat input formatting (#435) --- .../ChatInput/ChatInputFormattingToolbar.js | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) 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 = { From b14728805e9525f2a5a83353f43c270d408de1a1 Mon Sep 17 00:00:00 2001 From: Umang Utkarsh <95426993+umangutkarsh@users.noreply.github.com> Date: Sat, 9 Mar 2024 18:29:12 +0530 Subject: [PATCH 13/14] [Fix/#457]: Invite link appearance fixed/Copy to clipboard button added/Improvements in the slash command panel UI/Bug-fixes (#475) * copy-button/styles-improve * conditional-render * command-panel/improv * slash-commands/improv * fix/lint-errors * handle-errors * format-fixes --------- Co-authored-by: Sidharth Mohanty --- .../components/CommandList/CommandsList.js | 83 ++++++++++--- .../react/src/components/Icon/icons/Copy.js | 14 +++ .../react/src/components/Icon/icons/index.js | 2 + .../src/components/RoomMembers/RoomMember.js | 42 +++++-- .../inviteMembers/InviteMembers.js | 111 ++++++++++-------- 5 files changed, 177 insertions(+), 75 deletions(-) create mode 100644 packages/react/src/components/Icon/icons/Copy.js 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/Copy.js b/packages/react/src/components/Icon/icons/Copy.js new file mode 100644 index 000000000..6e621d958 --- /dev/null +++ b/packages/react/src/components/Icon/icons/Copy.js @@ -0,0 +1,14 @@ +import React from 'react'; + +const Copy = (props) => ( + + + +); + +export default Copy; diff --git a/packages/react/src/components/Icon/icons/index.js b/packages/react/src/components/Icon/icons/index.js index a74a56d5b..3f468a8f4 100644 --- a/packages/react/src/components/Icon/icons/index.js +++ b/packages/react/src/components/Icon/icons/index.js @@ -38,6 +38,7 @@ import ArrowDown from './ArrowDown'; 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'; @@ -53,6 +54,7 @@ const icons = { hash: Hash, computer: Computer, cross: Cross, + copy: Copy, mic: Mic, 'video-recorder': VideoRecorder, 'disabled-recorder': DisabledRecorder, 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 c09de515a..ec2bac803 100644 --- a/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js +++ b/packages/react/src/components/RoomMembers/inviteMembers/InviteMembers.js @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; +import React from 'react'; 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,10 +11,22 @@ import { ActionButton } from '../../ActionButton'; const InviteMembers = ({ inviteData }) => { const toggleInviteView = useInviteStore((state) => state.toggleInviteView); - const [isCopied, setIsCopied] = useState(false); - const copyToClipboard = (url) => { - navigator.clipboard.writeText(url); - setIsCopied(true); + 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 ( @@ -21,15 +34,14 @@ const InviteMembers = ({ inviteData }) => {

    - - toggleInviteView()} ghost size="small"> - - - + { > Invite Members + toggleInviteView()} ghost size="small"> + +

    - - - Invite Link - + {inviteData && ( - - copyToClipboard(inviteData.url)} - ghost - size="small" + > + + Invite Link + + + + + + + + )} +
    + {inviteData && ( +

    - - - - - - Your invite link will expire on{' '} - {new Date(inviteData.expires).toString().split('GMT')[0]} - + + Your invite link will expire on{' '} + {new Date(inviteData.expires).toString().split('GMT')[0]} + +

    + )} +
    ); }; From f68f9d94904fedc7c34270efb5444647db75caeb Mon Sep 17 00:00:00 2001 From: Zishan Ahmad Date: Sat, 9 Mar 2024 18:34:15 +0530 Subject: [PATCH 14/14] feat: UiKit Modal Support (#481) * added overflow menu and static select component in uiModalKit * added a multiselect box * fixed key icon log in console while login * added MessageGenericPreview component added PreviewBlock fixed link visibility styling remaining * fixed previewBlock styling * fixed uiKitModal styling * removed unused code * added support for multiple params --------- Co-authored-by: Sidharth Mohanty --- .../react/src/components/ChatBody/ChatBody.js | 27 +-- .../src/components/ChatInput/ChatInput.js | 4 +- .../react/src/components/Icon/icons/Key.js | 15 ++ .../react/src/components/Icon/icons/index.js | 2 + .../components/Markup/elements/BoldSpan.js | 13 ++ packages/react/src/components/Menu/Menu.js | 11 +- .../MessageGenericPreview.js | 28 +++ .../MessageGenericPreviewContent.js | 32 ++++ .../MessageGenericPreviewCoverImage.js | 34 ++++ .../MessageGenericPreviewDescription.js | 28 +++ .../MessageGenericPreviewFooter.js | 30 ++++ .../MessageGenericPreviewThumb.js | 17 ++ .../MessageGenericPreviewTitle.js | 40 +++++ .../components/MessageGenericPreview/index.js | 7 + .../src/components/MultiSelect/MultiSelect.js | 102 +++++++++++ .../react/src/components/MultiSelect/index.js | 1 + .../components/StaticSelect/StaticSelect.js | 88 ++++++++++ .../src/components/StaticSelect/index.js | 1 + .../uiKit/blocks/ContextBlock.Item.js | 3 +- .../components/uiKit/blocks/ContextBlock.js | 1 + .../components/uiKit/blocks/PreviewBlock.js | 162 ++++++++--------- .../src/components/uiKit/blocks/UiKitModal.js | 164 ------------------ .../elements/MultiStaticSelectElement.js | 71 ++++---- .../uiKit/elements/OverflowElement.js | 127 +++++--------- .../uiKit/elements/PlainTextInputElement.js | 1 - .../uiKit/elements/StaticSelectElement.js | 70 ++++---- .../uiKit/surfaces/FuselageSurfaceRenderer.js | 88 +++++----- .../components/uiKit/surfaces/ModalSurface.js | 11 +- packages/react/tools/icons-generator.js | 1 + 29 files changed, 709 insertions(+), 470 deletions(-) create mode 100644 packages/react/src/components/Icon/icons/Key.js create mode 100644 packages/react/src/components/MessageGenericPreview/MessageGenericPreview.js create mode 100644 packages/react/src/components/MessageGenericPreview/MessageGenericPreviewContent.js create mode 100644 packages/react/src/components/MessageGenericPreview/MessageGenericPreviewCoverImage.js create mode 100644 packages/react/src/components/MessageGenericPreview/MessageGenericPreviewDescription.js create mode 100644 packages/react/src/components/MessageGenericPreview/MessageGenericPreviewFooter.js create mode 100644 packages/react/src/components/MessageGenericPreview/MessageGenericPreviewThumb.js create mode 100644 packages/react/src/components/MessageGenericPreview/MessageGenericPreviewTitle.js create mode 100644 packages/react/src/components/MessageGenericPreview/index.js create mode 100644 packages/react/src/components/MultiSelect/MultiSelect.js create mode 100644 packages/react/src/components/MultiSelect/index.js create mode 100644 packages/react/src/components/StaticSelect/StaticSelect.js create mode 100644 packages/react/src/components/StaticSelect/index.js delete mode 100644 packages/react/src/components/uiKit/blocks/UiKitModal.js 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/ChatInput/ChatInput.js b/packages/react/src/components/ChatInput/ChatInput.js index 8f1237023..502445f6b 100644 --- a/packages/react/src/components/ChatInput/ChatInput.js +++ b/packages/react/src/components/ChatInput/ChatInput.js @@ -199,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); 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 3f468a8f4..60aed7b64 100644 --- a/packages/react/src/components/Icon/icons/index.js +++ b/packages/react/src/components/Icon/icons/index.js @@ -44,6 +44,7 @@ import Download from './Download'; import At from './At'; import ChevronDown from './ChevronDown'; import ChevronLeft from './ChevronLeft'; +import Key from './Key'; const icons = { file: File, @@ -92,6 +93,7 @@ const icons = { at: At, 'chevron-down': ChevronDown, 'chevron-left': ChevronLeft, + key: Key, }; export default icons; 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/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( + '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 ( +
    + {thumb} +
    +
    + ); +}; + +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/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/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/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/tools/icons-generator.js b/packages/react/tools/icons-generator.js index 12d55a300..710904c97 100644 --- a/packages/react/tools/icons-generator.js +++ b/packages/react/tools/icons-generator.js @@ -41,6 +41,7 @@ const iconsList = [ 'error-circle', 'chevron-down', 'chevron-left', + 'key', ]; const svgDirPath = path.join( __dirname,