mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 19:30:04 +08:00
fix: extract the ChatContext
This commit is contained in:
parent
d7aedbf14e
commit
bd9fe847ab
@ -5,20 +5,15 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
|
|||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { RootState } from '@renderer/store'
|
|
||||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
|
||||||
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { Flex, Modal } from 'antd'
|
import { Flex } from 'antd'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { FC, useEffect, useMemo, useState } from 'react'
|
import React, { FC, useMemo, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Inputbar from './Inputbar/Inputbar'
|
import Inputbar from './Inputbar/Inputbar'
|
||||||
|
import { ChatProvider, useChatContext } from './Messages/ChatContext'
|
||||||
import Messages from './Messages/Messages'
|
import Messages from './Messages/Messages'
|
||||||
import Tabs from './Tabs'
|
import Tabs from './Tabs'
|
||||||
|
|
||||||
@ -29,20 +24,11 @@ interface Props {
|
|||||||
setActiveAssistant: (assistant: Assistant) => void
|
setActiveAssistant: (assistant: Assistant) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<Props> = (props) => {
|
const ChatContent: FC<Props> = (props) => {
|
||||||
const { assistant } = useAssistant(props.assistant.id)
|
const { assistant } = useAssistant(props.assistant.id)
|
||||||
const { topicPosition, messageStyle, showAssistants } = useSettings()
|
const { topicPosition, messageStyle, showAssistants } = useSettings()
|
||||||
const { showTopics } = useShowTopics()
|
const { showTopics } = useShowTopics()
|
||||||
const { t } = useTranslation()
|
const { isMultiSelectMode, toggleMultiSelectMode, handleMultiSelectAction } = useChatContext()
|
||||||
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
|
||||||
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false)
|
|
||||||
const [messagesToDelete, setMessagesToDelete] = useState<string[]>([])
|
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
// 从 Redux 中获取当前主题的消息
|
|
||||||
const messages = useSelector((state: RootState) => selectMessagesForTopic(state, props.activeTopic.id))
|
|
||||||
// 获取所有消息块
|
|
||||||
const messageBlocks = useSelector(messageBlocksSelectors.selectEntities)
|
|
||||||
|
|
||||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||||
@ -119,106 +105,6 @@ const Chat: FC<Props> = (props) => {
|
|||||||
firstUpdateOrNoFirstUpdateHandler()
|
firstUpdateOrNoFirstUpdateHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleToggleMultiSelect = (value: boolean) => {
|
|
||||||
setIsMultiSelectMode(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.on(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventEmitter.off(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleMultiSelectAction = (actionType: string, messageIds: string[]) => {
|
|
||||||
if (messageIds.length === 0) {
|
|
||||||
window.message.warning(t('chat.multiple.select.empty'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch (actionType) {
|
|
||||||
case 'delete':
|
|
||||||
setMessagesToDelete(messageIds)
|
|
||||||
setConfirmDeleteVisible(true)
|
|
||||||
break
|
|
||||||
case 'save': {
|
|
||||||
const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id))
|
|
||||||
if (assistantMessages.length > 0) {
|
|
||||||
const contentToSave = assistantMessages
|
|
||||||
.map((msg) => {
|
|
||||||
return msg.blocks
|
|
||||||
.map((blockId) => {
|
|
||||||
const block = messageBlocks[blockId]
|
|
||||||
return block && 'content' in block ? block.content : ''
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n')
|
|
||||||
.trim()
|
|
||||||
})
|
|
||||||
.join('\n\n---\n\n')
|
|
||||||
const fileName = `chat_export_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.md`
|
|
||||||
window.api.file.save(fileName, contentToSave)
|
|
||||||
window.message.success({ content: t('message.save.success.title'), key: 'save-messages' })
|
|
||||||
EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, false)
|
|
||||||
} else {
|
|
||||||
window.message.warning(t('message.save.no.assistant'))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'copy': {
|
|
||||||
const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id))
|
|
||||||
if (assistantMessages.length > 0) {
|
|
||||||
const contentToCopy = assistantMessages
|
|
||||||
.map((msg) => {
|
|
||||||
return msg.blocks
|
|
||||||
.map((blockId) => {
|
|
||||||
const block = messageBlocks[blockId]
|
|
||||||
return block && 'content' in block ? block.content : ''
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n')
|
|
||||||
.trim()
|
|
||||||
})
|
|
||||||
.join('\n\n---\n\n')
|
|
||||||
navigator.clipboard.writeText(contentToCopy)
|
|
||||||
window.message.success({ content: t('message.copied'), key: 'copy-messages' })
|
|
||||||
EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, false)
|
|
||||||
} else {
|
|
||||||
window.message.warning(t('message.copy.no.assistant'))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
|
||||||
try {
|
|
||||||
dispatch(
|
|
||||||
newMessagesActions.removeMessages({
|
|
||||||
topicId: props.activeTopic.id,
|
|
||||||
messageIds: messagesToDelete
|
|
||||||
})
|
|
||||||
)
|
|
||||||
window.message.success(t('message.delete.success'))
|
|
||||||
setMessagesToDelete([])
|
|
||||||
setIsMultiSelectMode(false)
|
|
||||||
EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, false)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete messages:', error)
|
|
||||||
window.message.error(t('message.delete.failed'))
|
|
||||||
} finally {
|
|
||||||
setConfirmDeleteVisible(false)
|
|
||||||
setIsMultiSelectMode(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelDelete = () => {
|
|
||||||
setConfirmDeleteVisible(false)
|
|
||||||
setMessagesToDelete([])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="chat" className={messageStyle}>
|
<Container id="chat" className={messageStyle}>
|
||||||
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}>
|
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}>
|
||||||
@ -244,7 +130,7 @@ const Chat: FC<Props> = (props) => {
|
|||||||
{isMultiSelectMode && (
|
{isMultiSelectMode && (
|
||||||
<MultiSelectActionPopup
|
<MultiSelectActionPopup
|
||||||
visible={isMultiSelectMode}
|
visible={isMultiSelectMode}
|
||||||
onClose={() => setIsMultiSelectMode(false)}
|
onClose={() => toggleMultiSelectMode(false)}
|
||||||
onAction={handleMultiSelectAction}
|
onAction={handleMultiSelectAction}
|
||||||
topic={props.activeTopic}
|
topic={props.activeTopic}
|
||||||
/>
|
/>
|
||||||
@ -260,21 +146,18 @@ const Chat: FC<Props> = (props) => {
|
|||||||
position="right"
|
position="right"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Modal
|
|
||||||
title={t('message.delete.confirm.title')}
|
|
||||||
open={confirmDeleteVisible}
|
|
||||||
onOk={confirmDelete}
|
|
||||||
onCancel={cancelDelete}
|
|
||||||
okText={t('common.confirm')}
|
|
||||||
cancelText={t('common.cancel')}
|
|
||||||
okButtonProps={{ danger: true }}
|
|
||||||
centered={true}>
|
|
||||||
<p>{t('message.delete.confirm.content', { count: messagesToDelete.length })}</p>
|
|
||||||
</Modal>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Chat: FC<Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<ChatProvider activeTopic={props.activeTopic}>
|
||||||
|
<ChatContent {...props} />
|
||||||
|
</ChatProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const MessagesContainer = styled.div`
|
const MessagesContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
156
src/renderer/src/pages/home/Messages/ChatContext.tsx
Normal file
156
src/renderer/src/pages/home/Messages/ChatContext.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { RootState } from '@renderer/store'
|
||||||
|
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||||
|
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||||
|
import { Topic } from '@renderer/types'
|
||||||
|
import { Modal } from 'antd'
|
||||||
|
import { createContext, FC, ReactNode, use, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
interface ChatContextProps {
|
||||||
|
isMultiSelectMode: boolean
|
||||||
|
toggleMultiSelectMode: (value: boolean) => void
|
||||||
|
handleMultiSelectAction: (actionType: string, messageIds: string[]) => void
|
||||||
|
activeTopic: Topic
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatContext = createContext<ChatContextProps | undefined>(undefined)
|
||||||
|
|
||||||
|
export const useChatContext = () => {
|
||||||
|
const context = use(ChatContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useChatContext 必须在 ChatProvider 内使用')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatProviderProps {
|
||||||
|
children: ReactNode
|
||||||
|
activeTopic: Topic
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
||||||
|
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false)
|
||||||
|
const [messagesToDelete, setMessagesToDelete] = useState<string[]>([])
|
||||||
|
|
||||||
|
const messages = useSelector((state: RootState) => selectMessagesForTopic(state, activeTopic.id))
|
||||||
|
const messageBlocks = useSelector(messageBlocksSelectors.selectEntities)
|
||||||
|
|
||||||
|
const toggleMultiSelectMode = (value: boolean) => {
|
||||||
|
setIsMultiSelectMode(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMultiSelectAction = (actionType: string, messageIds: string[]) => {
|
||||||
|
if (messageIds.length === 0) {
|
||||||
|
window.message.warning(t('chat.multiple.select.empty'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (actionType) {
|
||||||
|
case 'delete':
|
||||||
|
setMessagesToDelete(messageIds)
|
||||||
|
setConfirmDeleteVisible(true)
|
||||||
|
break
|
||||||
|
case 'save': {
|
||||||
|
const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id))
|
||||||
|
if (assistantMessages.length > 0) {
|
||||||
|
const contentToSave = assistantMessages
|
||||||
|
.map((msg) => {
|
||||||
|
return msg.blocks
|
||||||
|
.map((blockId) => {
|
||||||
|
const block = messageBlocks[blockId]
|
||||||
|
return block && 'content' in block ? block.content : ''
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
.trim()
|
||||||
|
})
|
||||||
|
.join('\n\n---\n\n')
|
||||||
|
const fileName = `chat_export_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.md`
|
||||||
|
window.api.file.save(fileName, contentToSave)
|
||||||
|
window.message.success({ content: t('message.save.success.title'), key: 'save-messages' })
|
||||||
|
toggleMultiSelectMode(false)
|
||||||
|
} else {
|
||||||
|
window.message.warning(t('message.save.no.assistant'))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'copy': {
|
||||||
|
const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id))
|
||||||
|
if (assistantMessages.length > 0) {
|
||||||
|
const contentToCopy = assistantMessages
|
||||||
|
.map((msg) => {
|
||||||
|
return msg.blocks
|
||||||
|
.map((blockId) => {
|
||||||
|
const block = messageBlocks[blockId]
|
||||||
|
return block && 'content' in block ? block.content : ''
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
.trim()
|
||||||
|
})
|
||||||
|
.join('\n\n---\n\n')
|
||||||
|
navigator.clipboard.writeText(contentToCopy)
|
||||||
|
window.message.success({ content: t('message.copied'), key: 'copy-messages' })
|
||||||
|
toggleMultiSelectMode(false)
|
||||||
|
} else {
|
||||||
|
window.message.warning(t('message.copy.no.assistant'))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
try {
|
||||||
|
dispatch(
|
||||||
|
newMessagesActions.removeMessages({
|
||||||
|
topicId: activeTopic.id,
|
||||||
|
messageIds: messagesToDelete
|
||||||
|
})
|
||||||
|
)
|
||||||
|
window.message.success(t('message.delete.success'))
|
||||||
|
setMessagesToDelete([])
|
||||||
|
toggleMultiSelectMode(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete messages:', error)
|
||||||
|
window.message.error(t('message.delete.failed'))
|
||||||
|
} finally {
|
||||||
|
setConfirmDeleteVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelDelete = () => {
|
||||||
|
setConfirmDeleteVisible(false)
|
||||||
|
setMessagesToDelete([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
isMultiSelectMode,
|
||||||
|
toggleMultiSelectMode,
|
||||||
|
handleMultiSelectAction,
|
||||||
|
activeTopic
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatContext value={value}>
|
||||||
|
{children}
|
||||||
|
<Modal
|
||||||
|
title={t('message.delete.confirm.title')}
|
||||||
|
open={confirmDeleteVisible}
|
||||||
|
onOk={confirmDelete}
|
||||||
|
onCancel={cancelDelete}
|
||||||
|
okText={t('common.confirm')}
|
||||||
|
cancelText={t('common.cancel')}
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
centered={true}>
|
||||||
|
<p>{t('message.delete.confirm.content', { count: messagesToDelete.length })}</p>
|
||||||
|
</Modal>
|
||||||
|
</ChatContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ import { FC, memo, useCallback, useMemo, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
import { useChatContext } from './ChatContext'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -55,6 +56,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const { message, index, isGrouped, isLastMessage, isAssistantMessage, assistant, topic, model, messageContainerRef } =
|
const { message, index, isGrouped, isLastMessage, isAssistantMessage, assistant, topic, model, messageContainerRef } =
|
||||||
props
|
props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { toggleMultiSelectMode } = useChatContext()
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
|
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
|
||||||
@ -264,7 +266,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
key: 'multi-select',
|
key: 'multi-select',
|
||||||
icon: <MenuOutlined size={16} />,
|
icon: <MenuOutlined size={16} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, true)
|
toggleMultiSelectMode(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { useChatContext } from './ChatContext'
|
||||||
import ChatNavigation from './ChatNavigation'
|
import ChatNavigation from './ChatNavigation'
|
||||||
import MessageAnchorLine from './MessageAnchorLine'
|
import MessageAnchorLine from './MessageAnchorLine'
|
||||||
import MessageGroup from './MessageGroup'
|
import MessageGroup from './MessageGroup'
|
||||||
@ -55,8 +56,10 @@ const Messages: FC<MessagesProps> = ({ assistant, topic, setActiveTopic, onCompo
|
|||||||
const [hasMore, setHasMore] = useState(false)
|
const [hasMore, setHasMore] = useState(false)
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||||
const [isProcessingContext, setIsProcessingContext] = useState(false)
|
const [isProcessingContext, setIsProcessingContext] = useState(false)
|
||||||
|
|
||||||
|
const { isMultiSelectMode } = useChatContext()
|
||||||
|
|
||||||
const [selectedMessages, setSelectedMessages] = useState<Set<string>>(new Set())
|
const [selectedMessages, setSelectedMessages] = useState<Set<string>>(new Set())
|
||||||
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||||
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
|
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
|
||||||
@ -155,19 +158,10 @@ const Messages: FC<MessagesProps> = ({ assistant, topic, setActiveTopic, onCompo
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleToggleMultiSelect = (value: boolean) => {
|
if (!isMultiSelectMode) {
|
||||||
setIsMultiSelectMode(value)
|
setSelectedMessages(new Set())
|
||||||
if (!value) {
|
|
||||||
setSelectedMessages(new Set())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [isMultiSelectMode])
|
||||||
EventEmitter.on(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventEmitter.off(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRequestSelectedMessageDetails = (messageIds: string[]) => {
|
const handleRequestSelectedMessageDetails = (messageIds: string[]) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user