diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 7e0ff5eca..7dd03dcbe 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -627,6 +627,27 @@ export default class EmbeddedChatApi { } } + async getAllFiles(isChannelPrivate = false) { + const roomType = isChannelPrivate ? "groups" : "channels"; + try { + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const response = await fetch( + `${this.host}/api/v1/${roomType}.files?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 starMessage(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 08cfdc8bd..8b9392196 100644 --- a/packages/react/src/components/ChatHeader/ChatHeader.js +++ b/packages/react/src/components/ChatHeader/ChatHeader.js @@ -12,6 +12,7 @@ import { useToastStore, useThreadsMessageStore, useMentionsStore, + useFileStore, } from '../../store'; import { DynamicHeader } from '../DynamicHeader'; import { Tooltip } from '../Tooltip'; @@ -74,6 +75,7 @@ const ChatHeader = ({ const setShowAllThreads = useThreadsMessageStore( (state) => state.setShowAllThreads ); + const setShowAllFiles = useFileStore((state) => state.setShowAllFiles); const setShowMentions = useMentionsStore((state) => state.setShowMentions); const toastPosition = useToastStore((state) => state.position); @@ -143,6 +145,11 @@ const ChatHeader = ({ setShowSearch(false); }, [setShowAllThreads, setShowSearch]); + const showAllFiles = useCallback(async () => { + setShowAllFiles(true); + setShowSearch(false); + }, [setShowAllFiles, setShowSearch]); + const showMentions = useCallback(async () => { setShowMentions(true); setShowSearch(false); @@ -242,6 +249,12 @@ const ChatHeader = ({ label: 'Members', icon: 'members', }, + { + id: 'files', + action: showAllFiles, + label: 'Files', + icon: 'clip', + }, { id: 'starred', action: showStarredMessage, @@ -285,6 +298,7 @@ const ChatHeader = ({ isUserAuthenticated, moreOpts, setFullScreen, + showAllFiles, showAllThreads, showMentions, showChannelMembers, diff --git a/packages/react/src/components/EmbeddedChat.js b/packages/react/src/components/EmbeddedChat.js index 264786d20..ea0f8ab74 100644 --- a/packages/react/src/components/EmbeddedChat.js +++ b/packages/react/src/components/EmbeddedChat.js @@ -50,7 +50,7 @@ const EmbeddedChat = ({ useEffect(() => { setToastbarPosition(toastBarPosition); setShowAvatar(showAvatar); - }, [toastBarPosition, showAvatar]); + }, [toastBarPosition, showAvatar, setShowAvatar, setToastbarPosition]); const { onDrag, diff --git a/packages/react/src/components/Files/FileMetrics.js b/packages/react/src/components/Files/FileMetrics.js new file mode 100644 index 000000000..ef1b0c115 --- /dev/null +++ b/packages/react/src/components/Files/FileMetrics.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { formatDistance } from 'date-fns'; +import useComponentOverrides from '../../theme/useComponentOverrides'; +import { Box } from '../Box'; +import { appendClassNames } from '../../lib/appendClassNames'; +import { Icon } from '../Icon'; + +const FileMetricsCss = css` + display: flex; + margin-left: -0.25rem; + margin-right: -0.25rem; + margin-inline: -0.25rem; + margin-top: 0.5rem; +`; + +const FileMetricsItemCss = css` + letter-spacing: 0rem; + font-size: 0.625rem; + font-weight: 700; + line-height: 0.75rem; + display: flex; + justify-content: center; + align-items: center; + margin-left: 0.25rem; + color: #6c727a; +`; + +const FileMetricsItemLabelCss = css` + margin: 0.25rem; + margin-inline-start: 0.25rem; + white-space: nowrap; +`; + +export const FileMetrics = ({ className = '', file, style = {}, ...props }) => { + const { styleOverrides, classNames } = useComponentOverrides( + 'MessageMetrics', + className, + style + ); + return ( + + + + + {formatDistance(new Date(file.uploadedAt), new Date(), { + addSuffix: true, + })} + + + + ); +}; diff --git a/packages/react/src/components/Files/FilePreviewContainer.js b/packages/react/src/components/Files/FilePreviewContainer.js new file mode 100644 index 000000000..55980dc66 --- /dev/null +++ b/packages/react/src/components/Files/FilePreviewContainer.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { Avatar } from '../Avatar'; +import { Box } from '../Box'; +import { Icon } from '../Icon'; + +const FilePreviewContainer = ({ file, sequential, isStarred }) => { + const FilePreviewContainerCss = css` + margin: 3px; + width: 2.25em; + max-height: 2.25em; + display: flex; + justify-content: flex-end; + `; + + return ( + + {!sequential ? ( + + ) : isStarred ? ( + + ) : null} + + ); +}; + +export default FilePreviewContainer; diff --git a/packages/react/src/components/Files/FilePreviewHeader.js b/packages/react/src/components/Files/FilePreviewHeader.js new file mode 100644 index 000000000..886959dca --- /dev/null +++ b/packages/react/src/components/Files/FilePreviewHeader.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { css } from '@emotion/react'; +import { format } from 'date-fns'; +import { useUserStore } from '../../store'; +import { Icon } from '../Icon'; +import useComponentOverrides from '../../theme/useComponentOverrides'; +import { Box } from '../Box'; +import { appendClassNames } from '../../lib/appendClassNames'; + +const FilePreviewHeaderCss = css` + display: flex; + overflow-x: hidden; + flex-direction: row; + flex-grow: 0; + flex-shrink: 1; + min-width: 1px; + padding-right: 3px; + margin-top: 0.125rem; + margin-bottom: 0.125rem; + margin-block: 0.125rem; + gap: 0.125rem; + align-items: center; + max-width: 85%; +`; + +const FilePreviewHeaderNameCss = css` + letter-spacing: 0rem; + display: inline-block; + font-size: 0.875rem; + font-weight: 700; + line-height: 1.25rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 1; + color: #2f343d; +`; + +const FilePreviewHeaderTimestapCss = css` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + letter-spacing: 0rem; + font-size: 0.75rem; + font-weight: 400; + line-height: 1rem; + flex-shrink: 0; + color: #9ea2a8; +`; + +const FilePreviewHeader = ({ file, isTimeStamped = true }) => { + const { styleOverrides, classNames } = useComponentOverrides('MessageHeader'); + const authenticatedUserId = useUserStore((state) => state.userId); + const isStarred = + file.starred && file.starred.find((u) => u._id === authenticatedUserId); + + return ( + + + {file.name} + + + {isTimeStamped && ( + + {format(new Date(file.ts), 'h:mm a')} + + )} + {isStarred ? ( + + ) : null} + + ); +}; + +export default FilePreviewHeader; + +FilePreviewHeader.propTypes = { + file: PropTypes.any, +}; diff --git a/packages/react/src/components/Files/Files.js b/packages/react/src/components/Files/Files.js new file mode 100644 index 000000000..f3f830e44 --- /dev/null +++ b/packages/react/src/components/Files/Files.js @@ -0,0 +1,227 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { css } from '@emotion/react'; +import { Icon } from '../Icon'; +import { Box } from '../Box'; +import { ActionButton } from '../ActionButton'; +import { useChannelStore, useFileStore } from '../../store'; +import { useRCContext } from '../../context/RCInstance'; +import { MessageBody } from '../Message/MessageBody'; +import MessageBodyContainer from '../Message/MessageBodyContainer'; +import FilePreviewContainer from './FilePreviewContainer'; +import FilePreviewHeader from './FilePreviewHeader'; +import { FileMetrics } from './FileMetrics'; + +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; + cursor: pointer; + &:hover { + background: #f2f3f5; + } +`; + +const componentStyle = css` + 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; + @media (max-width: 550px) { + width: 100vw; + } +`; + +const wrapContainerStyle = css` + height: 100%; + display: flex; + flex-direction: column; +`; + +const searchContainerStyle = css` + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + border: 2px solid #ddd; + position: relative; +`; + +const textInputStyle = css` + width: 75%; + height: 2.5rem; + border: none; + outline: none; + &::placeholder { + padding-left: 5px; + } +`; + +const FilePreviewUsernameCss = css` + letter-spacing: 0rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.25rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 1; + color: #6c727a; +`; + +const Files = () => { + const { RCInstance } = useRCContext(); + const setShowAllFiles = useFileStore((state) => state.setShowAllFiles); + const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate); + + const [text, setText] = useState(''); + const [isFilesFetched, setIsFilesFetched] = useState(false); + const [files, setFiles] = useState([]); + + const toggleShowAllFiles = () => { + setShowAllFiles(false); + }; + + const handleInputChange = (e) => { + setText(e.target.value); + }; + + const filteredFiles = useMemo( + () => + files.filter((file) => + file.name.toLowerCase().includes(text.toLowerCase()) + ), + [files, text] + ); + + useEffect(() => { + const fetchAllFiles = async () => { + const res = await RCInstance.getAllFiles(isChannelPrivate); + if (res?.files) { + const sortedFiles = res.files.sort( + (a, b) => new Date(b.uploadedAt) - new Date(a.uploadedAt) + ); + setFiles(sortedFiles); + setIsFilesFetched(true); + } + }; + fetchAllFiles(); + }, [RCInstance, isChannelPrivate, setFiles, setIsFilesFetched]); + + return ( + + + + + + + + Files + + + + + + + + + + + + + + + {isFilesFetched && ( + + {filteredFiles.length === 0 ? ( + + + + No files found + + + ) : ( + filteredFiles.map( + (file) => + file.path && ( + + + + + + + @{file.user.username} + + + + + + ) + ) + )} + + )} + + + ); +}; + +export default Files; diff --git a/packages/react/src/components/Files/index.js b/packages/react/src/components/Files/index.js new file mode 100644 index 000000000..7ccfadf96 --- /dev/null +++ b/packages/react/src/components/Files/index.js @@ -0,0 +1 @@ +export { default as Files } from './Files'; diff --git a/packages/react/src/components/Flex/FlexItem.js b/packages/react/src/components/Flex/FlexItem.js index 81bc00edd..26bd35ef2 100644 --- a/packages/react/src/components/Flex/FlexItem.js +++ b/packages/react/src/components/Flex/FlexItem.js @@ -37,7 +37,7 @@ function FlexItem({ } return style; - }, [align, basis, grow, order, shrink]); + }, [align, basis, grow, order, shrink, style]); return ( diff --git a/packages/react/src/components/Icon/icons/Clip.js b/packages/react/src/components/Icon/icons/Clip.js new file mode 100644 index 000000000..c2c5588ab --- /dev/null +++ b/packages/react/src/components/Icon/icons/Clip.js @@ -0,0 +1,14 @@ +import React from 'react'; + +const Clip = (props) => ( + + + +); + +export default Clip; diff --git a/packages/react/src/components/Icon/icons/index.js b/packages/react/src/components/Icon/icons/index.js index 60aed7b64..198064cb0 100644 --- a/packages/react/src/components/Icon/icons/index.js +++ b/packages/react/src/components/Icon/icons/index.js @@ -40,6 +40,7 @@ import VideoRecorder from './VideoRecoder'; import DisabledRecorder from './DisableRecorder'; import Copy from './Copy'; import Clipboard from './Clipboard'; +import Clip from './Clip'; import Download from './Download'; import At from './At'; import ChevronDown from './ChevronDown'; @@ -89,6 +90,7 @@ const icons = { 'arrow-down': ArrowDown, 'pin-filled': PinFilled, clipboard: Clipboard, + clip: Clip, download: Download, at: At, 'chevron-down': ChevronDown, diff --git a/packages/react/src/components/Markdown/Markdown.styles.js b/packages/react/src/components/Markdown/Markdown.styles.js index 2aad12cbf..0979b9c1a 100644 --- a/packages/react/src/components/Markdown/Markdown.styles.js +++ b/packages/react/src/components/Markdown/Markdown.styles.js @@ -1,4 +1,4 @@ -import { css, keyframes } from '@emotion/react'; +import { css } from '@emotion/react'; export const markdownStyles = { p: css` diff --git a/packages/react/src/components/Mentions/MembersList.js b/packages/react/src/components/Mentions/MembersList.js index 5d4100e40..bc5d75a98 100644 --- a/packages/react/src/components/Mentions/MembersList.js +++ b/packages/react/src/components/Mentions/MembersList.js @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { css } from '@emotion/react'; import PropTypes from 'prop-types'; import { Box } from '../Box'; @@ -52,6 +52,7 @@ function MembersList({ mentionIndex, filteredMembers = [], onMemberClick }) { }, [onMemberClick] ); + useEffect(() => { const handleKeyPress = (event) => { if (event.key === 'Enter') { diff --git a/packages/react/src/components/Menu/Menu.stories.js b/packages/react/src/components/Menu/Menu.stories.js index 92da52d89..c66a97942 100644 --- a/packages/react/src/components/Menu/Menu.stories.js +++ b/packages/react/src/components/Menu/Menu.stories.js @@ -28,6 +28,11 @@ export const Menu = { label: 'Members', icon: 'members', }, + { + id: 'files', + label: 'Files', + icon: 'clip', + }, { id: 'starred', label: 'Starred', diff --git a/packages/react/src/components/Message/MessageHeader.js b/packages/react/src/components/Message/MessageHeader.js index fd3e91199..ba877d639 100644 --- a/packages/react/src/components/Message/MessageHeader.js +++ b/packages/react/src/components/Message/MessageHeader.js @@ -59,7 +59,6 @@ const MessageHeaderTimestapCss = css` const MessageHeader = ({ message, isTimeStamped = true }) => { const { styleOverrides, classNames } = useComponentOverrides('MessageHeader'); - const roles = useUserStore((state) => state.roles); const authenticatedUserId = useUserStore((state) => state.userId); const isStarred = message.starred && @@ -83,9 +82,9 @@ const MessageHeader = ({ message, isTimeStamped = true }) => { } }; - const userRoles = roles[message.u.username] - ? roles[message.u.username].roles - : null; + // const userRoles = roles[message.u.username] + // ? roles[message.u.username].roles + // : null; if (!message.t) { return ( diff --git a/packages/react/src/components/MessageList/MessageList.js b/packages/react/src/components/MessageList/MessageList.js index 2b6eae33f..14bf6fc63 100644 --- a/packages/react/src/components/MessageList/MessageList.js +++ b/packages/react/src/components/MessageList/MessageList.js @@ -7,6 +7,7 @@ import { useSearchMessageStore, useChannelStore, useUserStore, + useFileStore, useMentionsStore, useThreadsMessageStore, } from '../../store'; @@ -17,6 +18,7 @@ import UserMentions from '../UserMentions/UserMentions'; import SearchMessage from '../SearchMessage/SearchMessage'; import Roominfo from '../RoomInformation/RoomInformation'; import AllThreads from '../AllThreads/AllThreads'; +import { Files } from '../Files'; import { Message } from '../Message'; import { Icon } from '../Icon'; @@ -30,6 +32,7 @@ const MessageList = ({ messages }) => { const showAvatar = useUserStore((state) => state.showAvatar); const headerTitle = useMessageStore((state) => state.headerTitle); const showMentions = useMentionsStore((state) => state.showMentions); + const showAllFiles = useFileStore((state) => state.showAllFiles); const showAllThreads = useThreadsMessageStore( (state) => state.showAllThreads ); @@ -81,6 +84,7 @@ const MessageList = ({ messages }) => { {showSearch && } {showChannelinfo && } {showAllThreads && } + {showAllFiles && } {showMentions && } > ); diff --git a/packages/react/src/components/ReportMessage/ReportWindowButtons.js b/packages/react/src/components/ReportMessage/ReportWindowButtons.js index 4906582fd..10b654fc9 100644 --- a/packages/react/src/components/ReportMessage/ReportWindowButtons.js +++ b/packages/react/src/components/ReportMessage/ReportWindowButtons.js @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; -import { useMessageStore, useToastStore } from '../../store'; +import { useMessageStore } from '../../store'; import RCContext from '../../context/RCInstance'; import { Button } from '../Button'; import { Icon } from '../Icon'; @@ -14,7 +14,6 @@ const ReportWindowButtons = ({ children, reportDescription, messageId }) => { ]); const { RCInstance } = useContext(RCContext); const dispatchToastMessage = useToastBarDispatch(); - const toastPosition = useToastStore((state) => state.position); const handleOnClose = () => { toggleReportMessage(); diff --git a/packages/react/src/components/SearchMessage/SearchMessage.js b/packages/react/src/components/SearchMessage/SearchMessage.js index 06d289734..0e9b5557e 100644 --- a/packages/react/src/components/SearchMessage/SearchMessage.js +++ b/packages/react/src/components/SearchMessage/SearchMessage.js @@ -3,7 +3,8 @@ import { isSameDay, format } from 'date-fns'; import { debounce } from 'lodash'; import RCContext from '../../context/RCInstance'; import classes from './SearchMessage.module.css'; -import { useSearchMessageStore } from '../../store'; +import { Markdown } from '../Markdown/index'; +import { useUserStore, useSearchMessageStore } from '../../store'; import { Box } from '../Box'; import { Icon } from '../Icon'; import { ActionButton } from '../ActionButton'; diff --git a/packages/react/src/store/fileStore.js b/packages/react/src/store/fileStore.js new file mode 100644 index 000000000..5f7db6f25 --- /dev/null +++ b/packages/react/src/store/fileStore.js @@ -0,0 +1,8 @@ +import { create } from 'zustand'; + +const useFileStore = create((set) => ({ + showAllFiles: false, + setShowAllFiles: (showAllFiles) => set(() => ({ showAllFiles })), +})); + +export default useFileStore; diff --git a/packages/react/src/store/index.js b/packages/react/src/store/index.js index 78da56784..9fd11e4c3 100644 --- a/packages/react/src/store/index.js +++ b/packages/react/src/store/index.js @@ -7,4 +7,5 @@ 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 useFileStore } from './fileStore'; export { default as useMentionsStore } from './mentionsStore';