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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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;