diff --git a/frontend/constants/messages/de.ts b/frontend/constants/messages/de.ts index 45d3d05..957c4c6 100644 --- a/frontend/constants/messages/de.ts +++ b/frontend/constants/messages/de.ts @@ -255,8 +255,10 @@ export const de: Messages = { "Speichern Sie große Dateien oder Ordner direkt in einem ausgewählten Verzeichnis. 👉", chooseSavePath_dis: "Speicherort auswählen", safeSave_dis: "Sicheres Speichern", - safeSave_tooltip: "Keine Angst vor Verbindungsunterbrechung, klicken Sie hier, um Dateien sicher zu speichern für die nächste Fortsetzung", - safeSaveSuccessMsg: "Dateien wurden sicher auf der Festplatte gespeichert, sicher die Seite zu schließen, unterstützt Wiederaufnahme der Übertragung!", + safeSave_tooltip: + "Keine Angst vor Verbindungsunterbrechung, klicken Sie hier, um Dateien sicher zu speichern für die nächste Fortsetzung", + safeSaveSuccessMsg: + "Dateien wurden sicher auf der Festplatte gespeichert, sicher die Seite zu schließen, unterstützt Wiederaufnahme der Übertragung!", }, RetrieveMethod: { P: "Glückwunsch 🎉 Freigegebene Inhalte warten darauf, abgerufen zu werden:", @@ -305,6 +307,10 @@ export const de: Messages = { leftRoomMsg: "Sie haben den Raum verlassen.", leaveRoomBtn: "Raum Verlassen", }, + fileExistMsg: "Einige Dateien wurden bereits hinzugefügt.", + noFilesForFolderMsg: "Keine Dateien im Ordner '{folderName}' gefunden.", + zipError: "Fehler beim Erstellen der ZIP-Datei.", + fileNotFoundMsg: "Datei '{fileName}' zum Herunterladen nicht gefunden.", html: { senderTab: "Senden", retrieveTab: "Abrufen", diff --git a/frontend/constants/messages/en.ts b/frontend/constants/messages/en.ts index c4fdf39..7ec1861 100644 --- a/frontend/constants/messages/en.ts +++ b/frontend/constants/messages/en.ts @@ -252,8 +252,10 @@ export const en: Messages = { "Save large files or folders directly to a selected directory. 👉", chooseSavePath_dis: "Choose save location", safeSave_dis: "Safe Save", - safeSave_tooltip: "Don't worry about connection interruption, click here to safely save files for next resume", - safeSaveSuccessMsg: "Files have been safely saved to disk, safe to close page, supports resume transfer!", + safeSave_tooltip: + "Don't worry about connection interruption, click here to safely save files for next resume", + safeSaveSuccessMsg: + "Files have been safely saved to disk, safe to close page, supports resume transfer!", }, RetrieveMethod: { P: "Congrats 🎉 Share content is waiting to be retrieved:", @@ -297,6 +299,10 @@ export const en: Messages = { leftRoomMsg: "You have left the room.", leaveRoomBtn: "Leave Room", }, + fileExistMsg: "Some files were already added.", + noFilesForFolderMsg: "No files found for folder '{folderName}'.", + zipError: "Error creating ZIP.", + fileNotFoundMsg: "File '{fileName}' not found for download.", html: { senderTab: "Send", retrieveTab: "Retrieve", diff --git a/frontend/constants/messages/es.ts b/frontend/constants/messages/es.ts index 40214c7..18e3073 100644 --- a/frontend/constants/messages/es.ts +++ b/frontend/constants/messages/es.ts @@ -253,8 +253,10 @@ export const es: Messages = { "Guarda archivos grandes o carpetas directamente en un directorio seleccionado. 👉", chooseSavePath_dis: "Elegir ubicación de guardado", safeSave_dis: "Guardar Seguro", - safeSave_tooltip: "No te preocupes por la interrupción de la conexión, haz clic aquí para guardar archivos de forma segura para la próxima reanudación", - safeSaveSuccessMsg: "Los archivos se han guardado de forma segura en el disco, es seguro cerrar la página, ¡admite transferencia de reanudación!", + safeSave_tooltip: + "No te preocupes por la interrupción de la conexión, haz clic aquí para guardar archivos de forma segura para la próxima reanudación", + safeSaveSuccessMsg: + "Los archivos se han guardado de forma segura en el disco, es seguro cerrar la página, ¡admite transferencia de reanudación!", }, RetrieveMethod: { P: "¡Felicitaciones 🎉 El contenido compartido está esperando ser recuperado:", @@ -298,6 +300,11 @@ export const es: Messages = { leftRoomMsg: "Has salido de la sala.", leaveRoomBtn: "Salir de la Sala", }, + fileExistMsg: "Algunos archivos ya han sido añadidos.", + noFilesForFolderMsg: + "No se encontraron archivos en la carpeta '{folderName}'.", + zipError: "Error al crear el archivo ZIP.", + fileNotFoundMsg: "Archivo '{fileName}' no encontrado para descargar.", html: { senderTab: "Enviar", retrieveTab: "Recuperar", diff --git a/frontend/constants/messages/fr.ts b/frontend/constants/messages/fr.ts index 3a516ec..3020d52 100644 --- a/frontend/constants/messages/fr.ts +++ b/frontend/constants/messages/fr.ts @@ -256,8 +256,10 @@ export const fr: Messages = { "Enregistrez des fichiers volumineux ou des dossiers directement dans un répertoire sélectionné. 👉", chooseSavePath_dis: "Choisir l'emplacement de sauvegarde", safeSave_dis: "Sauvegarde Sécurisée", - safeSave_tooltip: "N'ayez pas peur de l'interruption de connexion, cliquez ici pour sauvegarder les fichiers en toute sécurité pour la prochaine reprise", - safeSaveSuccessMsg: "Les fichiers ont été sauvegardés en toute sécurité sur le disque, sûr de fermer la page, prend en charge la reprise du transfert !", + safeSave_tooltip: + "N'ayez pas peur de l'interruption de connexion, cliquez ici pour sauvegarder les fichiers en toute sécurité pour la prochaine reprise", + safeSaveSuccessMsg: + "Les fichiers ont été sauvegardés en toute sécurité sur le disque, sûr de fermer la page, prend en charge la reprise du transfert !", }, RetrieveMethod: { P: "Félicitations 🎉 Le contenu partagé attend d'être récupéré :", @@ -305,6 +307,12 @@ export const fr: Messages = { leftRoomMsg: "Vous avez quitté la salle.", leaveRoomBtn: "Quitter la Salle", }, + fileExistMsg: "Certains fichiers ont déjà été ajoutés.", + noFilesForFolderMsg: + "Aucun fichier trouvé dans le dossier '{folderName}'.", + zipError: "Erreur lors de la création du fichier ZIP.", + fileNotFoundMsg: + "Fichier '{fileName}' introuvable pour le téléchargement.", html: { senderTab: "Envoyer", retrieveTab: "Récupérer", diff --git a/frontend/constants/messages/ja.ts b/frontend/constants/messages/ja.ts index b95d523..206fa07 100644 --- a/frontend/constants/messages/ja.ts +++ b/frontend/constants/messages/ja.ts @@ -293,6 +293,10 @@ export const ja: Messages = { leftRoomMsg: "ルームを離れました。", leaveRoomBtn: "ルームを離れる", }, + fileExistMsg: "一部のファイルは既に追加されています。", + noFilesForFolderMsg: "フォルダ '{folderName}' にファイルが見つかりません。", + zipError: "ZIP の作成中にエラーが発生しました。", + fileNotFoundMsg: "ダウンロードするファイル '{fileName}' が見つかりません。", html: { senderTab: "送信", retrieveTab: "取得", diff --git a/frontend/constants/messages/ko.ts b/frontend/constants/messages/ko.ts index 7569c44..4c946f6 100644 --- a/frontend/constants/messages/ko.ts +++ b/frontend/constants/messages/ko.ts @@ -291,6 +291,10 @@ export const ko: Messages = { leftRoomMsg: "방을 나갔습니다.", leaveRoomBtn: "방 나가기", }, + fileExistMsg: "일부 파일이 이미 추가되었습니다.", + noFilesForFolderMsg: "폴더 '{folderName}'에서 파일을 찾을 수 없습니다.", + zipError: "ZIP 파일 생성 중 오류가 발생했습니다.", + fileNotFoundMsg: "다운로드할 파일 '{fileName}'을(를) 찾을 수 없습니다.", html: { senderTab: "보내기", retrieveTab: "검색", diff --git a/frontend/constants/messages/zh.ts b/frontend/constants/messages/zh.ts index 07ce4d6..a231087 100644 --- a/frontend/constants/messages/zh.ts +++ b/frontend/constants/messages/zh.ts @@ -278,6 +278,10 @@ export const zh: Messages = { leftRoomMsg: "您已离开房间。", leaveRoomBtn: "离开房间", }, + fileExistMsg: "某些文件已添加。", + noFilesForFolderMsg: "在文件夹 '{folderName}' 中未找到文件。", + zipError: "创建 ZIP 文件时出错。", + fileNotFoundMsg: "未找到要下载的文件 '{fileName}'。", html: { senderTab: "发送", retrieveTab: "接收", diff --git a/frontend/hooks/useFileTransferHandler.ts b/frontend/hooks/useFileTransferHandler.ts index f4c181b..ba3bf52 100644 --- a/frontend/hooks/useFileTransferHandler.ts +++ b/frontend/hooks/useFileTransferHandler.ts @@ -1,8 +1,9 @@ -import { useState, useCallback } from "react"; +import { useCallback } from "react"; import { CustomFile, FileMeta, fileMetadata } from "@/types/webrtc"; import { Messages } from "@/types/messages"; import JSZip from "jszip"; import { downloadAs } from "@/lib/fileUtils"; +import { useFileTransferStore } from "@/stores/fileTransferStore"; interface UseFileTransferHandlerProps { messages: Messages | null; @@ -17,98 +18,76 @@ export function useFileTransferHandler({ messages, putMessageInMs, }: UseFileTransferHandlerProps) { - const [shareContent, setShareContent] = useState(""); - const [sendFiles, setSendFiles] = useState([]); - const [retrievedContent, setRetrievedContent] = useState(""); - const [retrievedFiles, setRetrievedFiles] = useState([]); - const [retrievedFileMetas, setRetrievedFileMetas] = useState([]); + // 从 store 中获取状态 + const { + shareContent, + sendFiles, + retrievedContent, + retrievedFiles, + retrievedFileMetas, + setShareContent, + setSendFiles, + addSendFiles, + removeSendFile, + setRetrievedContent, + setRetrievedFiles, + setRetrievedFileMetas, + } = useFileTransferStore(); const updateShareContent = useCallback((content: string) => { setShareContent(content); - }, []); + }, [setShareContent]); const addFilesToSend = useCallback( (pickedFiles: CustomFile[]) => { - setSendFiles((prevFiles) => { - const newFiles = pickedFiles.filter( - (pf) => - !prevFiles.some((ef) => ef.name === pf.name && ef.size === pf.size) + const newFiles = pickedFiles.filter( + (pf) => + !sendFiles.some((ef) => ef.name === pf.name && ef.size === pf.size) + ); + if (newFiles.length < pickedFiles.length && messages) { + putMessageInMs( + messages.text.ClipboardApp.fileExistMsg || + "Some files were already added.", + true ); - if (newFiles.length < pickedFiles.length && messages) { - putMessageInMs( - // messages.text.ClipboardApp.fileExistMsg || - "Some files were already added.", - true - ); - } - return [...prevFiles, ...newFiles]; - }); + } + addSendFiles(newFiles); }, - [messages, putMessageInMs] + [sendFiles, messages, putMessageInMs, addSendFiles] ); const removeFileToSend = useCallback((metaToRemove: FileMeta) => { - setSendFiles((prevFiles) => { - if (metaToRemove.folderName && metaToRemove.folderName !== "") { - return prevFiles.filter( - (file) => file.folderName !== metaToRemove.folderName - ); - } else { - return prevFiles.filter((file) => file.name !== metaToRemove.name); - } - }); - }, []); - - const clearSentItems = useCallback(() => { - setShareContent(""); - setSendFiles([]); - }, []); - - const clearRetrievedItems = useCallback(() => { - setRetrievedContent(""); - setRetrievedFiles([]); - setRetrievedFileMetas([]); - }, []); - - // Reset function specifically for receiver state (for leave room functionality) - const resetReceiverState = useCallback(() => { - setRetrievedContent(""); - setRetrievedFiles([]); - setRetrievedFileMetas([]); - }, []); + removeSendFile(metaToRemove); + }, [removeSendFile]); // Callbacks for useWebRTCConnection const onStringDataReceived = useCallback((data: string, peerId: string) => { - // console.log(`FileTransferHandler received string from ${peerId}`); setRetrievedContent(data); - }, []); + }, [setRetrievedContent]); const onFileMetadataReceived = useCallback( (meta: fileMetadata, peerId: string) => { - // console.log(`FileTransferHandler received file meta from ${peerId}: ${meta.name}`); - const { type, ...metaWithoutType } = meta; // Assuming 'type' is not part of FileMeta - setRetrievedFileMetas((prev) => { - const DPrev = prev.filter( - (existingFile) => existingFile.fileId !== metaWithoutType.fileId - ); - return [...DPrev, metaWithoutType]; - }); + const { type, ...metaWithoutType } = meta; + // Filter out existing file with same ID and add the new one + const DPrev = retrievedFileMetas.filter( + (existingFile: FileMeta) => existingFile.fileId !== metaWithoutType.fileId + ); + setRetrievedFileMetas([...DPrev, metaWithoutType]); }, - [] + [retrievedFileMetas, setRetrievedFileMetas] ); - const onFileFullyReceived = async (file: CustomFile, peerId: string) => { - // console.log(`FileTransferHandler received file from ${peerId}: ${file.name}`); - setRetrievedFiles((prev) => { - const isDuplicate = prev.some( - (existingFile) => - existingFile.fullName === file.fullName && - existingFile.size === file.size - ); - if (isDuplicate) return prev; - return [...prev, file]; - }); - }; + const onFileFullyReceived = useCallback((file: CustomFile, peerId: string) => { + // Check if file already exists + const isDuplicate = retrievedFiles.some( + (existingFile: CustomFile) => + existingFile.fullName === file.fullName && + existingFile.size === file.size + ); + if (!isDuplicate) { + setRetrievedFiles([...retrievedFiles, file]); + } + }, [retrievedFiles, setRetrievedFiles]); const handleDownloadFile = useCallback( async (meta: FileMeta) => { @@ -120,7 +99,7 @@ export function useFileTransferHandler({ ); if (filesToZip.length === 0) { putMessageInMs( - // messages.text.ClipboardApp.noFilesForFolderMsg || + messages.text.ClipboardApp.noFilesForFolderMsg || "No files found for folder '{folderName}'.".replace( "{folderName}", meta.folderName @@ -139,7 +118,7 @@ export function useFileTransferHandler({ } catch (error) { console.error("Error creating zip file:", error); putMessageInMs( - // messages.text.ClipboardApp.zipError || + messages.text.ClipboardApp.zipError || "Error creating ZIP.", false ); @@ -150,7 +129,7 @@ export function useFileTransferHandler({ downloadAs(fileToDownload, fileToDownload.name); } else { putMessageInMs( - // messages.text.ClipboardApp.fileNotFoundMsg || + messages.text.ClipboardApp.fileNotFoundMsg || "File '{fileName}' not found for download.".replace( "{fileName}", meta.name @@ -163,6 +142,13 @@ export function useFileTransferHandler({ [retrievedFiles, messages, putMessageInMs] ); + // Reset function specifically for receiver state (for leave room functionality) + const resetReceiverState = useCallback(() => { + setRetrievedContent(""); + setRetrievedFiles([]); + setRetrievedFileMetas([]); + }, [setRetrievedContent, setRetrievedFiles, setRetrievedFileMetas]); + return { shareContent, sendFiles, @@ -172,14 +158,10 @@ export function useFileTransferHandler({ updateShareContent, addFilesToSend, removeFileToSend, - clearSentItems, - clearRetrievedItems, - resetReceiverState, // Export the new reset function - // Callbacks to provide to useWebRTCConnection + resetReceiverState, // Export the reset function onStringDataReceived, onFileMetadataReceived, onFileFullyReceived, - // Download function handleDownloadFile, }; -} +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 8756faa..a0cc13e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,7 +48,8 @@ "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "unified": "^11.0.5", - "unist-util-visit": "^5.0.0" + "unist-util-visit": "^5.0.0", + "zustand": "^5.0.7" }, "devDependencies": { "@types/react": "^18.3.18", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c183266..ce0056b 100755 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -128,6 +128,9 @@ importers: unist-util-visit: specifier: ^5.0.0 version: 5.0.0 + zustand: + specifier: ^5.0.7 + version: 5.0.7(@types/react@18.3.22)(react@18.3.1) devDependencies: '@types/react': specifier: ^18.3.18 @@ -3465,6 +3468,24 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.7: + resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -7504,4 +7525,9 @@ snapshots: yocto-queue@0.1.0: {} + zustand@5.0.7(@types/react@18.3.22)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.22 + react: 18.3.1 + zwitch@2.0.4: {} diff --git a/frontend/stores/fileTransferStore.ts b/frontend/stores/fileTransferStore.ts new file mode 100644 index 0000000..a3cca42 --- /dev/null +++ b/frontend/stores/fileTransferStore.ts @@ -0,0 +1,159 @@ +import { create } from 'zustand'; +import { CustomFile, FileMeta } from '@/types/webrtc'; + +interface FileTransferState { + // 房间相关状态 + shareRoomId: string; + initShareRoomId: string; + shareLink: string; + shareRoomStatusText: string; + retrieveRoomStatusText: string; + + // WebRTC 连接状态 + sharePeerCount: number; + retrievePeerCount: number; + senderDisconnected: boolean; + + // 文件传输状态 + shareContent: string; + sendFiles: CustomFile[]; + retrievedContent: string; + retrievedFiles: CustomFile[]; + retrievedFileMetas: FileMeta[]; + + // 传输进度状态 + sendProgress: Record; + receiveProgress: Record; + isAnyFileTransferring: boolean; + + // UI 状态 + activeTab: 'send' | 'receive'; + retrieveRoomIdInput: string; + isDragging: boolean; + + // 消息状态 + shareMessage: string; + retrieveMessage: string; + + // Actions + // 房间相关 actions + setShareRoomId: (id: string) => void; + setInitShareRoomId: (id: string) => void; + setShareLink: (link: string) => void; + setShareRoomStatusText: (text: string) => void; + setRetrieveRoomStatusText: (text: string) => void; + + // WebRTC 连接相关 actions + setSharePeerCount: (count: number) => void; + setRetrievePeerCount: (count: number) => void; + setSenderDisconnected: (disconnected: boolean) => void; + + // 文件传输相关 actions + setShareContent: (content: string) => void; + setSendFiles: (files: CustomFile[]) => void; + addSendFiles: (files: CustomFile[]) => void; + removeSendFile: (meta: FileMeta) => void; + setRetrievedContent: (content: string) => void; + setRetrievedFiles: (files: CustomFile[]) => void; + setRetrievedFileMetas: (metas: FileMeta[]) => void; + + // 传输进度相关 actions + setSendProgress: (progress: Record) => void; + setReceiveProgress: (progress: Record) => void; + setIsAnyFileTransferring: (transferring: boolean) => void; + + // UI 状态相关 actions + setActiveTab: (tab: 'send' | 'receive') => void; + setRetrieveRoomIdInput: (input: string) => void; + setIsDragging: (dragging: boolean) => void; + + // 消息相关 actions + setShareMessage: (message: string) => void; + setRetrieveMessage: (message: string) => void; + setRetrieveRoomId: (input: string) => void; + + // 重置相关 actions + resetReceiverState: () => void; + resetSenderApp: () => void; +} + +export const useFileTransferStore = create()((set, get) => ({ + // 初始状态 + shareRoomId: '', + initShareRoomId: '', + shareLink: '', + shareRoomStatusText: '', + retrieveRoomStatusText: '', + sharePeerCount: 0, + retrievePeerCount: 0, + senderDisconnected: false, + shareContent: '', + sendFiles: [], + retrievedContent: '', + retrievedFiles: [], + retrievedFileMetas: [], + sendProgress: {}, + receiveProgress: {}, + isAnyFileTransferring: false, + activeTab: 'send', + retrieveRoomIdInput: '', + isDragging: false, + shareMessage: '', + retrieveMessage: '', + + // Actions 实现 + setShareRoomId: (id) => set({ shareRoomId: id }), + setInitShareRoomId: (id) => set({ initShareRoomId: id }), + setShareLink: (link) => set({ shareLink: link }), + setShareRoomStatusText: (text) => set({ shareRoomStatusText: text }), + setRetrieveRoomStatusText: (text) => set({ retrieveRoomStatusText: text }), + + setSharePeerCount: (count) => set({ sharePeerCount: count }), + setRetrievePeerCount: (count) => set({ retrievePeerCount: count }), + setSenderDisconnected: (disconnected) => set({ senderDisconnected: disconnected }), + + setShareContent: (content) => set({ shareContent: content }), + setSendFiles: (files) => set({ sendFiles: files }), + addSendFiles: (files) => set((state) => ({ sendFiles: [...state.sendFiles, ...files] })), + removeSendFile: (meta) => set((state) => { + if (meta.folderName && meta.folderName !== '') { + return { + sendFiles: state.sendFiles.filter( + (file) => file.folderName !== meta.folderName + ) + }; + } else { + return { + sendFiles: state.sendFiles.filter((file) => file.name !== meta.name) + }; + } + }), + setRetrievedContent: (content) => set({ retrievedContent: content }), + setRetrievedFiles: (files) => set({ retrievedFiles: files }), + setRetrievedFileMetas: (metas) => set({ retrievedFileMetas: metas }), + + setSendProgress: (progress) => set({ sendProgress: progress }), + setReceiveProgress: (progress) => set({ receiveProgress: progress }), + setIsAnyFileTransferring: (transferring) => set({ isAnyFileTransferring: transferring }), + + setActiveTab: (tab) => set({ activeTab: tab }), + setRetrieveRoomIdInput: (input) => set({ retrieveRoomIdInput: input }), + setRetrieveRoomId: (input) => set({ retrieveRoomIdInput: input }), + setIsDragging: (dragging) => set({ isDragging: dragging }), + + setShareMessage: (message) => set({ shareMessage: message }), + setRetrieveMessage: (message) => set({ retrieveMessage: message }), + + resetReceiverState: () => set({ + retrievedContent: '', + retrievedFiles: [], + retrievedFileMetas: [] + }), + + resetSenderApp: () => set({ + shareLink: '', + sendProgress: {}, + receiveProgress: {}, + isAnyFileTransferring: false + }) +})); \ No newline at end of file diff --git a/frontend/types/messages.ts b/frontend/types/messages.ts index 5844609..0a253dd 100644 --- a/frontend/types/messages.ts +++ b/frontend/types/messages.ts @@ -264,6 +264,10 @@ export type ClipboardApp = { pickSaveMsg: string; roomStatus: RoomStatus; html: ClipboardAppHtml; + fileExistMsg?: string; + noFilesForFolderMsg?: string; + zipError?: string; + fileNotFoundMsg?: string; }; export type Home = { @@ -300,4 +304,4 @@ export type Text = { export type Messages = { meta: Meta; text: Text; -}; +}; \ No newline at end of file