diff --git a/frontend/package.json b/frontend/package.json index af548cc6c..604608a9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "raven-ui", "private": true, "license": "AGPL-3.0-only", - "version": "1.6.7", + "version": "1.6.8", "type": "module", "scripts": { "dev": "vite", @@ -12,49 +12,49 @@ }, "dependencies": { "@radix-ui/themes": "^3.1.3", - "@tiptap/extension-code-block-lowlight": "^2.5.9", - "@tiptap/extension-highlight": "^2.5.9", - "@tiptap/extension-image": "^2.5.9", - "@tiptap/extension-link": "^2.5.9", - "@tiptap/extension-mention": "^2.5.9", - "@tiptap/extension-placeholder": "^2.5.9", - "@tiptap/extension-table": "^2.5.9", - "@tiptap/extension-table-cell": "^2.5.9", - "@tiptap/extension-table-header": "^2.5.9", - "@tiptap/extension-table-row": "^2.5.9", - "@tiptap/extension-typography": "^2.5.9", - "@tiptap/extension-underline": "^2.5.9", - "@tiptap/pm": "^2.5.9", - "@tiptap/react": "^2.5.9", - "@tiptap/starter-kit": "^2.5.9", - "@tiptap/suggestion": "^2.5.9", + "@tiptap/extension-code-block-lowlight": "^2.6.4", + "@tiptap/extension-highlight": "^2.6.4", + "@tiptap/extension-image": "^2.6.4", + "@tiptap/extension-link": "^2.6.4", + "@tiptap/extension-mention": "^2.6.4", + "@tiptap/extension-placeholder": "^2.6.4", + "@tiptap/extension-table": "^2.6.4", + "@tiptap/extension-table-cell": "^2.6.4", + "@tiptap/extension-table-header": "^2.6.4", + "@tiptap/extension-table-row": "^2.6.4", + "@tiptap/extension-typography": "^2.6.4", + "@tiptap/extension-underline": "^2.6.4", + "@tiptap/pm": "^2.6.4", + "@tiptap/react": "^2.6.4", + "@tiptap/starter-kit": "^2.6.4", + "@tiptap/suggestion": "^2.6.4", "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.17", + "autoprefixer": "^10.4.20", "cal-sans": "^1.0.1", "clsx": "^2.1.0", "cmdk": "^1.0.0", "cva": "npm:class-variance-authority", "dayjs": "^1.11.11", "downshift": "^8.3.1", - "emoji-picker-element": "^1.22.2", + "emoji-picker-element": "^1.22.3", "firebase": "^10.9.0", - "frappe-react-sdk": "^1.7.1", + "frappe-react-sdk": "^1.8.0", "highlight.js": "^11.9.0", "html-react-parser": "^5.1.8", - "jotai": "^2.9.2", + "jotai": "^2.9.3", "js-cookie": "^3.0.5", "lowlight": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", - "react-hook-form": "^7.51.5", - "react-icons": "^5.2.1", + "react-hook-form": "^7.52.2", + "react-icons": "^5.3.0", "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.10.3", - "react-router-dom": "^6.23.1", + "react-router-dom": "^6.26.1", "react-zoom-pan-pinch": "^3.4.4", "sonner": "^1.5.0", - "tailwindcss": "^3.4.4", + "tailwindcss": "^3.4.10", "tailwindcss-animate": "^1.0.7", "tippy.js": "^6.3.7", "turndown": "^7.2.0", diff --git a/frontend/src/components/common/LinkField/LinkField.tsx b/frontend/src/components/common/LinkField/LinkField.tsx new file mode 100644 index 000000000..10828203c --- /dev/null +++ b/frontend/src/components/common/LinkField/LinkField.tsx @@ -0,0 +1,98 @@ + + +import { useCombobox } from "downshift"; +import { Filter, SearchResult, useSearch } from "frappe-react-sdk"; +import { useState } from "react"; +import { Label } from "../Form"; +import { Text, TextField } from "@radix-ui/themes"; +import { useIsDesktop } from "@/hooks/useMediaQuery"; +import clsx from "clsx"; + +export interface LinkFieldProps { + doctype: string; + filters?: Filter[]; + label?: string, + placeholder?: string, + value: string, + setValue: (value: string) => void, + disabled?: boolean, + autofocus?: boolean +} + + +const LinkField = ({ doctype, filters, label, placeholder, value, setValue, disabled, autofocus }: LinkFieldProps) => { + + const [searchText, setSearchText] = useState('') + + const isDesktop = useIsDesktop() + + const { data } = useSearch(doctype, searchText, filters) + + const items: SearchResult[] = data?.message ?? [] + + const { + isOpen, + // getToggleButtonProps, + getLabelProps, + getMenuProps, + getInputProps, + highlightedIndex, + getItemProps, + selectedItem, + } = useCombobox({ + onInputValueChange({ inputValue }) { + setSearchText(inputValue ?? '') + }, + items: items, + itemToString(item) { + return item ? item.value : '' + }, + selectedItem: items.find(item => item.value === value), + onSelectedItemChange({ selectedItem }) { + setValue(selectedItem?.value ?? '') + }, + }) + + return
+
+ + + + +
+ +
+} + +export default LinkField \ No newline at end of file diff --git a/frontend/src/components/common/LinkField/LinkFormField.tsx b/frontend/src/components/common/LinkField/LinkFormField.tsx new file mode 100644 index 000000000..3d1fc386c --- /dev/null +++ b/frontend/src/components/common/LinkField/LinkFormField.tsx @@ -0,0 +1,30 @@ +import { Controller, ControllerProps, useFormContext } from 'react-hook-form' +import LinkField, { LinkFieldProps } from './LinkField' + +interface LinkFormFieldProps extends Omit { + name: string, + rules: ControllerProps['rules'], + disabled?: boolean +} + +const LinkFormField = ({ name, rules, ...linkFieldProps }: LinkFormFieldProps) => { + + const { control } = useFormContext() + return ( + ( + + )} + /> + ) +} + +export default LinkFormField \ No newline at end of file diff --git a/frontend/src/components/feature/GlobalSearch/ChannelSearch.tsx b/frontend/src/components/feature/GlobalSearch/ChannelSearch.tsx index e36e7f76e..d175e409d 100644 --- a/frontend/src/components/feature/GlobalSearch/ChannelSearch.tsx +++ b/frontend/src/components/feature/GlobalSearch/ChannelSearch.tsx @@ -11,12 +11,12 @@ import { Flex, Select, TextField, Box, Checkbox, ScrollArea, Text, Badge } from import { Loader } from '@/components/common/Loader' interface Props { onToggleMyChannels: () => void, - isOpenMyChannels: boolean, + isOnlyInMyChannels: boolean, input: string, onClose: () => void } -export const ChannelSearch = ({ onToggleMyChannels, isOpenMyChannels, input, onClose }: Props) => { +export const ChannelSearch = ({ onToggleMyChannels, isOnlyInMyChannels, input, onClose }: Props) => { const [searchText, setSearchText] = useState(input) const debouncedText = useDebounce(searchText) @@ -31,10 +31,9 @@ export const ChannelSearch = ({ onToggleMyChannels, isOpenMyChannels, input, onC const { data, error, isLoading } = useFrappeGetCall<{ message: GetChannelSearchResult[] }>("raven.api.search.get_search_result", { filter_type: 'Channel', - doctype: 'Raven Channel', search_text: debouncedText, channel_type: channelType === 'any' ? undefined : channelType, - my_channel_only: isOpenMyChannels, + my_channel_only: isOnlyInMyChannels, }, undefined, { revalidateOnFocus: false }) @@ -94,22 +93,19 @@ export const ChannelSearch = ({ onToggleMyChannels, isOpenMyChannels, input, onC - - + - Only in my channels + Only in my channels + {data?.message?.length === 0 && } {data?.message && data.message.length > 0 ? - - + {data.message.map((channel: GetChannelSearchResult) => { return ( - + - {channel.channel_name} + {channel.channel_name} {channel.is_archived ? Archived : null} ) - } - )} + })} : null} diff --git a/frontend/src/components/feature/GlobalSearch/FileSearch.tsx b/frontend/src/components/feature/GlobalSearch/FileSearch.tsx index 23ef74882..393d4b89e 100644 --- a/frontend/src/components/feature/GlobalSearch/FileSearch.tsx +++ b/frontend/src/components/feature/GlobalSearch/FileSearch.tsx @@ -19,7 +19,7 @@ import { dateOption } from './GlobalSearch' interface Props { onToggleMyChannels: () => void, - isOpenMyChannels: boolean, + isOnlyInMyChannels: boolean, input: string, fromFilter?: string, withFilter?: string, @@ -36,7 +36,7 @@ export interface FileSearchResult { message_type: string } -export const FileSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSaved, isSaved, input, fromFilter, withFilter, inFilter }: Props) => { +export const FileSearch = ({ onToggleMyChannels, isOnlyInMyChannels, onToggleSaved, isSaved, input, fromFilter, withFilter, inFilter }: Props) => { const [searchText, setSearchText] = useState(input) const debouncedText = useDebounce(searchText) @@ -54,23 +54,20 @@ export const FileSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSaved const users = useGetUserRecords() - const handleChange = (e: React.ChangeEvent) => { setSearchText(e.target.value) } - const { data, error, isLoading } = useFrappeGetCall<{ message: GetFileSearchResult[] }>("raven.api.search.get_search_result", { filter_type: 'File', - doctype: 'Raven Message', search_text: debouncedText, - message_type: fileType === 'any' || fileType === undefined ? undefined : fileType === 'image' ? 'Image' : 'File', - file_type: fileType === 'any' ? undefined : fileType, - in_channel: channelFilter, - from_user: userFilter, + from_user: userFilter === 'any' ? undefined : userFilter, + in_channel: channelFilter === 'any' ? undefined : channelFilter, saved: isSaved, date: dateFilter, - my_channel_only: isOpenMyChannels, + file_type: fileType === 'any' ? undefined : fileType, + message_type: fileType === 'any' || fileType === undefined ? undefined : fileType === 'image' ? 'Image' : 'File', + my_channel_only: isOnlyInMyChannels, }, undefined, { revalidateOnFocus: false }) @@ -215,7 +212,7 @@ export const FileSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSaved - Only in my channels + Only in my channels @@ -244,7 +241,7 @@ export const FileSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSaved />} - {f.file && {getFileName(f.file)} {f.file}} + {f.file && {getFileName(f.file)}} {users && Shared by {Object.values(users).find((user: UserFields) => user.name === f.owner)?.full_name} on } diff --git a/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx b/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx index 5ceca6e0a..38f5db99e 100644 --- a/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx +++ b/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx @@ -19,7 +19,6 @@ interface GlobalSearchModalProps { export default function GlobalSearch(props: GlobalSearchModalProps) { - const { isOpen, onClose } = props const onOpenChange = (open: boolean) => { @@ -56,7 +55,7 @@ export default function GlobalSearch(props: GlobalSearchModalProps) { const GlobalSearchContent = (props: GlobalSearchModalProps) => { - const [isOpenMyChannels, { toggle: onToggleMyChannels }] = useBoolean() + const [isOnlyInMyChannels, { toggle: onToggleMyChannels }] = useBoolean() const [isSaved, { toggle: onToggleSaved }] = useBoolean() const { tabIndex, input, fromFilter, withFilter, inFilter, onClose } = props @@ -71,13 +70,13 @@ const GlobalSearchContent = (props: GlobalSearchModalProps) => { - + - + - + diff --git a/frontend/src/components/feature/GlobalSearch/MessageSearch.tsx b/frontend/src/components/feature/GlobalSearch/MessageSearch.tsx index 267a0bb5d..ce9caece6 100644 --- a/frontend/src/components/feature/GlobalSearch/MessageSearch.tsx +++ b/frontend/src/components/feature/GlobalSearch/MessageSearch.tsx @@ -2,7 +2,6 @@ import { BiSearch } from 'react-icons/bi' import { useFrappeGetCall } from 'frappe-react-sdk' import { useContext, useState, useMemo } from 'react' import { useDebounce } from '../../../hooks/useDebounce' -import { GetMessageSearchResult } from '../../../../../types/Search/Search' import { ErrorBanner } from '../../layout/AlertBanner' import { EmptyStateForSearch } from '../../layout/EmptyState/EmptyState' import { useNavigate } from 'react-router-dom' @@ -14,10 +13,11 @@ import { Box, Checkbox, Flex, Select, TextField, Text, Grid, ScrollArea } from ' import { UserAvatar } from '@/components/common/UserAvatar' import { dateOption } from './GlobalSearch' import { Loader } from '@/components/common/Loader' +import { Message } from '../../../../../types/Messaging/Message' interface Props { onToggleMyChannels: () => void, - isOpenMyChannels: boolean, + isOnlyInMyChannels: boolean, input: string, fromFilter?: string, inFilter?: string, @@ -27,15 +27,7 @@ interface Props { isSaved: boolean } -interface MessageSearchResult { - channel_id: string - name: string, - owner: string, - creation: string, - text: string, -} - -export const MessageSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSaved, isSaved, input, fromFilter, inFilter, withFilter, onClose }: Props) => { +export const MessageSearch = ({ onToggleMyChannels, isOnlyInMyChannels, onToggleSaved, isSaved, input, fromFilter, inFilter, withFilter, onClose }: Props) => { const [searchText, setSearchText] = useState(input) const debouncedText = useDebounce(searchText) @@ -70,23 +62,21 @@ export const MessageSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSa setSearchText(e.target.value) } - const showResults = useMemo(() => { - const isChannelFilterApplied = channelFilter !== 'any' && channelFilter !== undefined + const isChannelFilterApplied = channelFilter !== undefined const isUserFilterApplied = userFilter !== 'any' && userFilter !== undefined const isDateFilterApplied = dateFilter !== 'any' && dateFilter !== undefined - return (debouncedText.length > 2 || isChannelFilterApplied || isUserFilterApplied || isDateFilterApplied || isOpenMyChannels === true) - }, [debouncedText, channelFilter, userFilter, isOpenMyChannels, dateFilter]) + return (debouncedText.length > 2 || isChannelFilterApplied || isUserFilterApplied || isDateFilterApplied || isOnlyInMyChannels === true) + }, [debouncedText, channelFilter, userFilter, isOnlyInMyChannels, dateFilter]) - const { data, error, isLoading } = useFrappeGetCall<{ message: GetMessageSearchResult[] }>("raven.api.search.get_search_result", { + const { data, error, isLoading } = useFrappeGetCall<{ message: Message[] }>("raven.api.search.get_search_result", { filter_type: 'Message', - doctype: 'Raven Message', search_text: debouncedText, - in_channel: channelFilter === 'any' ? undefined : channelFilter, from_user: userFilter === 'any' ? undefined : userFilter, - date: dateFilter === 'any' ? undefined : dateFilter, - my_channel_only: isOpenMyChannels, + in_channel: channelFilter === 'any' ? undefined : channelFilter, saved: isSaved, + date: dateFilter === 'any' ? undefined : dateFilter, + my_channel_only: isOnlyInMyChannels, }, showResults ? undefined : null, { revalidateOnFocus: false, }) @@ -180,7 +170,7 @@ export const MessageSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSa - Only in my channels + Only in my channels @@ -195,7 +185,7 @@ export const MessageSearch = ({ onToggleMyChannels, isOpenMyChannels, onToggleSa {data?.message?.length === 0 && } {data?.message?.length && data?.message.length > 0 ? - {data.message.map((message: MessageSearchResult) => { + {data.message.map((message: Message) => { return ( void, + message: Message +} + +interface AttachFileToDocumentForm { + doctype: string, + docname: string, +} + +const AttachFileToDocumentModal = ({ onClose, message }: AttachFileToDocumentModalProps) => { + + const methods = useForm({ + defaultValues: { + } + }) + + const { handleSubmit, reset, watch } = methods + + const { call } = useContext(FrappeContext) as FrappeConfig + + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const onSubmit = (data: AttachFileToDocumentForm) => { + if ((message as FileMessage).file) { + setLoading(true) + call.get('frappe.client.get_value', { + doctype: 'File', + filters: { + file_url: (message as FileMessage).file, + attached_to_doctype: "Raven Message", + attached_to_name: message.name, + attached_to_field: "file" + }, + fieldname: ['name'] + }).then((res) => { + if (res.message) { + call.post('upload_file', { + doctype: data.doctype, + docname: data.docname, + library_file_name: res.message.name, + }) + } + }).then(() => { + toast.success(`File attached to ${data.doctype} - ${data.docname}`) + handleClose() + }).catch((err) => { + setError(err) + toast.error('Failed to attach file') + }).finally(() => { + setLoading(false) + }) + } + } + + const handleClose = () => { + reset() + onClose() + } + + const doctype = watch('doctype') + + const onDoctypeChange = () => { + // Reset docname when doctype changes + methods.setValue('docname', '') + } + + return ( + +
+ + Attach File to Document + + + + + + + + + + + + + {} + + + + {getFileName((message as FileMessage).file)} + + + + + + + {methods.formState.errors.doctype?.message} + + + {doctype && + + + + {methods.formState.errors.docname?.message} + + + } + + + + + + + + + +
+
+ ) +} + +export default AttachFileToDocumentModal \ No newline at end of file diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageActions/AttachFileToDocument.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageActions/AttachFileToDocument.tsx new file mode 100644 index 000000000..2beea7c08 --- /dev/null +++ b/frontend/src/components/feature/chat/ChatMessage/MessageActions/AttachFileToDocument.tsx @@ -0,0 +1,66 @@ +import { Dialog } from '@radix-ui/themes' +import { useCallback, useState } from 'react' +import { Message } from '../../../../../../../types/Messaging/Message' +import { useIsDesktop } from '@/hooks/useMediaQuery' +import { Drawer, DrawerContent } from '@/components/layout/Drawer' +import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' +import clsx from 'clsx' +import AttachFileToDocumentModal from '../ActionModals/AttachFileToDocumentModal' + +type Props = { + file: string +} + +export const useAttachFileToDocument = () => { + + const [message, setMessage] = useState(null) + + const onClose = useCallback(() => { + setMessage(null) + }, []) + + return { + message, + setAttachDocument: setMessage, + isOpen: message !== null, + onClose + } + +} + + +interface AttacFileToDocumentDialogProps { + message: Message | null, + isOpen: boolean, + onClose: () => void +} +const AttachFileToDocumentDialog = ({ message, isOpen, onClose }: AttacFileToDocumentDialogProps) => { + + const isDesktop = useIsDesktop() + + if (isDesktop) { + return + + {message && + + } + + + } else { + return + +
+ {message && + + } +
+
+
+ } +} + +export default AttachFileToDocumentDialog \ No newline at end of file diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx index 28d18fc49..806c119db 100644 --- a/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx @@ -2,7 +2,7 @@ import { ContextMenu, Flex } from '@radix-ui/themes' import { FileMessage, Message } from '../../../../../../../types/Messaging/Message' import { useContext } from 'react' import { UserContext } from '@/utils/auth/UserProvider' -import { BiBookmarkMinus, BiBookmarkPlus, BiCopy, BiDownload, BiLink, BiTrash } from 'react-icons/bi' +import { BiBookmarkMinus, BiBookmarkPlus, BiCopy, BiDownload, BiLink, BiPaperclip, BiTrash } from 'react-icons/bi' import { FrappeConfig, FrappeContext } from 'frappe-react-sdk' import { useMessageCopy } from './useMessageCopy' import { RetractVote } from './RetractVote' @@ -10,16 +10,18 @@ import { toast } from 'sonner' import { getErrorMessage } from '@/components/layout/AlertBanner/ErrorBanner' import { AiOutlineEdit } from 'react-icons/ai' import { LuForward, LuReply } from 'react-icons/lu' +import AttachFileToDocument from './AttachFileToDocument' export interface MessageContextMenuProps { message?: Message | null, onDelete: VoidFunction onEdit: VoidFunction, onReply: VoidFunction, - onForward: VoidFunction + onForward: VoidFunction, + onAttachDocument: VoidFunction } -export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForward }: MessageContextMenuProps) => { +export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForward, onAttachDocument }: MessageContextMenuProps) => { const copy = useMessageCopy(message) const { currentUser } = useContext(UserContext) @@ -72,6 +74,13 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForwa
+ + + + + Attach File to Document + + } diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageItem.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageItem.tsx index 6dbd65826..f376ba7dd 100644 --- a/frontend/src/components/feature/chat/ChatMessage/MessageItem.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/MessageItem.tsx @@ -33,10 +33,11 @@ interface MessageBlockProps { replyToMessage: (message: Message) => void, forwardMessage: (message: Message) => void, onReplyMessageClick: (messageID: string) => void, + onAttachDocument: (message: Message) => void, isHighlighted?: boolean } -export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyMessageClick, setEditMessage, replyToMessage, forwardMessage }: MessageBlockProps) => { +export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyMessageClick, setEditMessage, replyToMessage, forwardMessage, onAttachDocument }: MessageBlockProps) => { const { name, owner: userID, is_bot_message, bot, creation: timestamp, message_reactions, is_continuation, linked_message, replied_message_details } = message @@ -58,6 +59,10 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM forwardMessage(message) } + const onAttachToDocument = () => { + onAttachDocument(message) + } + const isDesktop = useIsDesktop() const [isHovered, setIsHovered] = useState(false) @@ -170,6 +175,7 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM onEdit={onEdit} onReply={onReply} onForward={onForward} + onAttachDocument={onAttachToDocument} /> } @@ -182,6 +188,7 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM onEdit={onEdit} onReply={onReply} onForward={onForward} + onAttachDocument={onAttachToDocument} /> diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx index 2405543af..8267ba0ee 100644 --- a/frontend/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/ImageMessage.tsx @@ -1,13 +1,13 @@ import { getFileName } from '@/utils/operations' import { ImageMessage } from '../../../../../../../types/Messaging/Message' -import { Box, Button, Dialog, Flex, Link } from '@radix-ui/themes' -import { Suspense, lazy, memo, useState } from 'react' +import { Box, Button, Dialog, Flex, IconButton, Link, Text } from '@radix-ui/themes' +import { Suspense, lazy, memo, useState, useRef, useEffect } from 'react' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' -import { BiDownload } from 'react-icons/bi' +import { BiDownload, BiChevronDown, BiChevronRight } from 'react-icons/bi' import { UserFields } from '@/utils/users/UserListProvider' import { DateMonthAtHourMinuteAmPm } from '@/utils/dateConversions' import { clsx } from 'clsx' -import { useIsDesktop, useIsMobile } from '@/hooks/useMediaQuery' +import { useIsMobile } from '@/hooks/useMediaQuery' const ImageViewer = lazy(() => import('@/components/common/ImageViewer')) @@ -25,20 +25,49 @@ export const ImageMessageBlock = memo(({ message, isScrolling = false, user }: I const isMobile = useIsMobile() - const height = message.thumbnail_height ? isMobile ? message.thumbnail_height / 2 : message.thumbnail_height : '200' + const fileName = getFileName(message.file) + + const [isVisible, setIsVisible] = useState(true) + + const height = isVisible ? (message.thumbnail_height ? isMobile ? message.thumbnail_height / 2 : message.thumbnail_height : '200') : '0' const width = message.thumbnail_width ? isMobile ? message.thumbnail_width / 2 : message.thumbnail_width : '300' + const contentRef = useRef(null); - const fileName = getFileName(message.file) + useEffect(() => { + if (isVisible && contentRef.current) { + setTimeout(() => { + if (contentRef.current) { + contentRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 200); + } + }, [isVisible]); return ( - {fileName} + + setIsVisible(prev => !prev)} + > + {isVisible ? : } + + {fileName} + + setIsOpen(!isScrolling && true)} style={{ @@ -118,4 +147,4 @@ export const ImageMessageBlock = memo(({ message, isScrolling = false, user }: I } ) -}) \ No newline at end of file +}) diff --git a/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx b/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx index c3578c1ae..2c68fe3d1 100644 --- a/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx +++ b/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx @@ -15,6 +15,7 @@ import { Button } from '@radix-ui/themes' import { FiArrowDown } from 'react-icons/fi' import { ErrorBanner } from '@/components/layout/AlertBanner' import { ForwardMessageDialog, useForwardMessage } from '../ChatMessage/MessageActions/ForwardMessage' +import AttachFileToDocumentDialog, { useAttachFileToDocument } from '../ChatMessage/MessageActions/AttachFileToDocument' /** * Anatomy of a message @@ -76,6 +77,7 @@ const ChatStream = ({ replyToMessage }: Props) => { const { setEditMessage, ...editProps } = useEditMessage() const { setForwardMessage, ...forwardProps } = useForwardMessage() + const { setAttachDocument, ...attachDocProps } = useAttachFileToDocument() const onReplyMessageClick = (messageID: string) => { scrollToMessage(messageID) @@ -137,6 +139,7 @@ const ChatStream = ({ replyToMessage }: Props) => { setEditMessage={setEditMessage} replyToMessage={replyToMessage} forwardMessage={setForwardMessage} + onAttachDocument={setAttachDocument} setDeleteMessage={setDeleteMessage} /> @@ -161,6 +164,7 @@ const ChatStream = ({ replyToMessage }: Props) => { + ) diff --git a/frontend/src/components/feature/polls/CreatePoll.tsx b/frontend/src/components/feature/polls/CreatePoll.tsx index a9a8e1e57..54d9bfd67 100644 --- a/frontend/src/components/feature/polls/CreatePoll.tsx +++ b/frontend/src/components/feature/polls/CreatePoll.tsx @@ -41,8 +41,6 @@ const CreatePollContent = ({ setIsOpen }: { setIsOpen: (open: boolean) => void } name: 'options' }) - const optionPlaceholders = ['Cersei Lannister', 'Jon Snow', 'Daenerys Targaryen', 'Tyrion Lannister', 'Night King', 'Arya Stark', 'Sansa Stark', 'Jaime Lannister', 'Bran Stark', 'The Hound'] - const handleAddOption = () => { // limit the number of options to 10 if (fields.length >= 10) { @@ -104,7 +102,7 @@ const CreatePollContent = ({ setIsOpen }: { setIsOpen: (open: boolean) => void }