From 4390446e1c5b2dcf8d4b18057b51d66b9f04afc6 Mon Sep 17 00:00:00 2001 From: Immad Abdul Jabbar Date: Thu, 27 Nov 2025 11:37:56 +0100 Subject: [PATCH 01/12] initial commit --- src/i18n/en-US.json | 5 +- .../CellsFilePreviewModal.tsx | 5 +- .../CellsTableRowOptions.tsx | 6 ++ .../CellsFilePreviewModalContext.tsx | 12 ++- .../CellsFilePreviewModal.tsx | 5 +- .../CellsTableRowOptions.tsx | 6 ++ .../CellsFilePreviewModalContext.tsx | 12 ++- .../FileEditor/FileEditor.styles.ts | 25 +++++ .../FileEditor/FileEditor.tsx | 97 +++++++++++++++++++ .../FileFullscreenModal.tsx | 35 +++++-- .../FileHeader/FileHeader.styles.ts | 30 ++++++ .../FileHeader/FileHeader.tsx | 32 +++++- .../FileAssetCard/FileAssetCard.tsx | 4 + .../FileAssetSmall/FileAssetSmall.tsx | 5 +- .../FileAssetWithPreview.tsx | 10 +- .../FilePreviewModal/FilePreviewModal.tsx | 3 + .../ImageAssetCard/ImageAssetCard.tsx | 4 + .../ImageAssetLarge/ImageAssetLarge.tsx | 5 +- .../ImageAssetSmall/ImageAssetSmall.tsx | 5 +- .../asset/MultipartAssets/MultipartAssets.tsx | 3 + .../VideoAssetCard/VideoAssetCard.tsx | 4 + .../VideoAssetCard/VideoAssetCard.tsx | 6 +- .../VideoAssetPlayer/VideoAssetPlayer.tsx | 5 + .../VideoAssetSmall/VideoAssetSmall.tsx | 5 +- .../repositories/cells/CellsRepository.ts | 5 +- .../getFileTypeFromExtension.ts | 2 +- src/types/i18n.d.ts | 3 + 27 files changed, 301 insertions(+), 38 deletions(-) create mode 100644 src/script/components/FileFullscreenModal/FileEditor/FileEditor.styles.ts create mode 100644 src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index 45f60d255d5..e5715368a63 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -437,6 +437,7 @@ "cells.options.restore": "Restore", "cells.options.share": "Share", "cells.options.tags": "Add or Remove Tags", + "cells.options.edit": "Edit", "cells.pagination.loadMoreResults": "Load More Items", "cells.pagination.nextPage": "Next Page", "cells.pagination.previousPage": "Previous Page", @@ -981,6 +982,8 @@ "federationConnectionRemove": "The backends [bold]{backendUrlOne}[/bold] and [bold]{backendUrlTwo}[/bold] stopped federating.", "federationDelete": "[bold]Your backend[/bold] stopped federating with [bold]{backendUrl}.[/bold]", "fileCardDefaultCloseButtonLabel": "Close", + "fileFullscreenModal.editor.error": "Failed to load edit preview", + "fileFullscreenModal.editor.iframeTitle": "Document editor", "fileFullscreenModal.noPreviewAvailable.callToAction": "Download File", "fileFullscreenModal.noPreviewAvailable.description": "There is no preview available for this file. Download the file instead.", "fileFullscreenModal.noPreviewAvailable.title": "File without preview", @@ -2047,4 +2050,4 @@ "wireMacos": "{brandName} for macOS", "wireWindows": "{brandName} for Windows", "wire_for_web": "{brandName} for Web" -} +} \ No newline at end of file diff --git a/src/script/components/CellsGlobalView/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx b/src/script/components/CellsGlobalView/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx index ad1f31294b3..097bf8eba2b 100644 --- a/src/script/components/CellsGlobalView/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx +++ b/src/script/components/CellsGlobalView/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx @@ -25,7 +25,7 @@ import {useCellsFilePreviewModal} from '../common/CellsFilePreviewModalContext/C // This component is duplicated across global view and conversation view // TODO: Abstract when it starts to grow / feels right export const CellsFilePreviewModal = () => { - const {id, selectedFile, handleCloseFile} = useCellsFilePreviewModal(); + const {selectedFile, handleCloseFile, isEditMode} = useCellsFilePreviewModal(); if (!selectedFile) { return null; @@ -49,7 +49,7 @@ export const CellsFilePreviewModal = () => { return ( { senderName={owner} timestamp={uploadedAtTimestamp} badges={tags} + isEditMode={isEditMode} /> ); }; diff --git a/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx b/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx index 60a7d0fb81e..74f4029c72b 100644 --- a/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx +++ b/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx @@ -36,11 +36,14 @@ interface CellsTableRowOptionsProps { cellsRepository: CellsRepository; } +const EDITABLE_FILE_EXTENSIONS = ['odf', 'docx', 'xlsx', 'pptx']; + export const CellsTableRowOptions = ({node, cellsRepository}: CellsTableRowOptionsProps) => { const {handleOpenFile} = useCellsFilePreviewModal(); const url = node.url; const name = node.type === 'folder' ? `${node.name}.zip` : node.name; + const isEditable = node.type === 'file' && EDITABLE_FILE_EXTENSIONS.includes(node.extension.toLowerCase()); return ( @@ -59,6 +62,9 @@ export const CellsTableRowOptions = ({node, cellsRepository}: CellsTableRowOptio showShareModal({type: node.type, uuid: node.id, cellsRepository})}> {t('cells.options.share')} + {isEditable && ( + handleOpenFile(node, true)}>{t('cells.options.edit')} + )} {!!url && ( diff --git a/src/script/components/CellsGlobalView/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx b/src/script/components/CellsGlobalView/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx index 4528ea6630f..bbd041ac385 100644 --- a/src/script/components/CellsGlobalView/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx +++ b/src/script/components/CellsGlobalView/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx @@ -24,7 +24,8 @@ import {CellFile} from '../../../common/cellNode/cellNode'; interface CellsFilePreviewModalContextValue { id: string; selectedFile: CellFile | null; - handleOpenFile: (file: CellFile) => void; + isEditMode: boolean; + handleOpenFile: (file: CellFile, isEditMode?: boolean) => void; handleCloseFile: () => void; } @@ -36,6 +37,7 @@ interface FilePreviewProviderProps { export const FilePreviewProvider = ({children}: FilePreviewProviderProps) => { const [selectedFile, setSelectedFile] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); const id = useId(); @@ -43,10 +45,14 @@ export const FilePreviewProvider = ({children}: FilePreviewProviderProps) => { () => ({ id, selectedFile, - handleOpenFile: (file: CellFile) => setSelectedFile(file), + isEditMode, + handleOpenFile: (file: CellFile, isEditMode?: boolean) => { + setSelectedFile(file); + setIsEditMode(!!isEditMode); + }, handleCloseFile: () => setSelectedFile(null), }), - [id, selectedFile], + [id, selectedFile, isEditMode], ); return {children}; diff --git a/src/script/components/Conversation/ConversationCells/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx b/src/script/components/Conversation/ConversationCells/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx index e55987699ca..2d86f2bb595 100644 --- a/src/script/components/Conversation/ConversationCells/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx +++ b/src/script/components/Conversation/ConversationCells/CellsTable/CellsFilePreviewModal/CellsFilePreviewModal.tsx @@ -25,7 +25,7 @@ import {useCellsFilePreviewModal} from '../common/CellsFilePreviewModalContext/C // This component is duplicated across global view and conversation view // TODO: Abstract when it starts to grow / feels right export const CellsFilePreviewModal = () => { - const {id, selectedFile, handleCloseFile} = useCellsFilePreviewModal(); + const {selectedFile, handleCloseFile, isEditMode} = useCellsFilePreviewModal(); if (!selectedFile) { return null; @@ -49,7 +49,7 @@ export const CellsFilePreviewModal = () => { return ( { senderName={owner} timestamp={uploadedAtTimestamp} badges={tags} + isEditMode={isEditMode} /> ); }; diff --git a/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx b/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx index 4264f5fb2c5..c51cd208519 100644 --- a/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx +++ b/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsTableRowOptions/CellsTableRowOptions.tsx @@ -82,6 +82,7 @@ export const CellsTableRowOptions = ({ ); }; +const EDITABLE_FILE_EXTENSIONS = ['odf', 'docx', 'xlsx', 'pptx']; const CellsTableRowOptionsContent = ({ node, @@ -124,6 +125,8 @@ const CellsTableRowOptionsContent = ({ const isRootRecycleBin = isRootRecycleBinPath(); const isNestedRecycleBin = isInRecycleBin(); + const isEditable = node.type === 'file' && EDITABLE_FILE_EXTENSIONS.includes(node.extension.toLowerCase()); + if (isRootRecycleBin || isNestedRecycleBin) { return ( @@ -190,6 +193,9 @@ const CellsTableRowOptionsContent = ({ setIsMoveNodeModalOpen(true)}>{t('cells.options.move')} setIsTagsModalOpen(true)}>{t('cells.options.tags')} + {isEditable && ( + handleOpenFile(node, true)}>{t('cells.options.edit')} + )} showMoveToRecycleBinModal({ diff --git a/src/script/components/Conversation/ConversationCells/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx b/src/script/components/Conversation/ConversationCells/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx index e25c554d728..d37783b6089 100644 --- a/src/script/components/Conversation/ConversationCells/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx +++ b/src/script/components/Conversation/ConversationCells/CellsTable/common/CellsFilePreviewModalContext/CellsFilePreviewModalContext.tsx @@ -23,8 +23,9 @@ import {CellFile} from '../../../common/cellNode/cellNode'; interface CellsFilePreviewModalContextValue { id: string; + isEditMode: boolean; selectedFile: CellFile | null; - handleOpenFile: (file: CellFile) => void; + handleOpenFile: (file: CellFile, isEditMode?: boolean) => void; handleCloseFile: () => void; } @@ -36,6 +37,7 @@ interface FilePreviewProviderProps { export const CellsFilePreviewModalProvider = ({children}: FilePreviewProviderProps) => { const [selectedFile, setSelectedFile] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); const id = useId(); @@ -43,10 +45,14 @@ export const CellsFilePreviewModalProvider = ({children}: FilePreviewProviderPro () => ({ id, selectedFile, - handleOpenFile: (file: CellFile) => setSelectedFile(file), + isEditMode, + handleOpenFile: (file: CellFile, isEditMode?: boolean) => { + setSelectedFile(file); + setIsEditMode(!!isEditMode); + }, handleCloseFile: () => setSelectedFile(null), }), - [id, selectedFile], + [id, selectedFile, isEditMode], ); return {children}; diff --git a/src/script/components/FileFullscreenModal/FileEditor/FileEditor.styles.ts b/src/script/components/FileFullscreenModal/FileEditor/FileEditor.styles.ts new file mode 100644 index 00000000000..a9e384faca2 --- /dev/null +++ b/src/script/components/FileFullscreenModal/FileEditor/FileEditor.styles.ts @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +export const editorIframe: CSSObject = { + width: '100%', + height: '100%', +}; diff --git a/src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx b/src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx new file mode 100644 index 00000000000..d1c31ddb85f --- /dev/null +++ b/src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx @@ -0,0 +1,97 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useEffect, useState} from 'react'; + +import {Node} from '@wireapp/api-client/lib/cells'; +import {container} from 'tsyringe'; + +import {CellsRepository} from 'Repositories/cells/CellsRepository'; +import {t} from 'Util/LocalizerUtil'; + +import * as styles from './FileEditor.styles'; + +import {FileLoader} from '../FileLoader/FileLoader'; + +const MILLISECONDS_IN_SECOND = 1000; +const REFRESH_BUFFER_SECONDS = 10; // Refresh 10 seconds before expiry for safety + +interface FileEditorProps { + id: string; +} + +export const FileEditor = ({id}: FileEditorProps) => { + const cellsRepository = container.resolve(CellsRepository); + const [node, setNode] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + + const fetchNode = useCallback(async () => { + try { + setIsLoading(true); + setIsError(false); + const fetchedNode = await cellsRepository.getNode({uuid: id, flags: ['WithEditorURLs']}); + setNode(fetchedNode); + } catch (err) { + setIsError(true); + } finally { + setIsLoading(false); + } + }, [id, cellsRepository]); + + // Initial fetch + useEffect(() => { + void fetchNode(); + }, [id, cellsRepository, fetchNode]); + + // Auto-refresh mechanism before expiry + useEffect(() => { + if (!node?.EditorURLs?.collabora.ExpiresAt) { + return; + } + + const expiresInSeconds = Number(node.EditorURLs.collabora.ExpiresAt); + const refreshInSeconds = expiresInSeconds - REFRESH_BUFFER_SECONDS; + + // Set timeout to refresh before expiry + const timeoutId = setTimeout(() => { + void fetchNode(); + }, refreshInSeconds * MILLISECONDS_IN_SECOND); + + return () => { + clearTimeout(timeoutId); + }; + }, [node, fetchNode]); + + if (isLoading) { + return ; + } + + if (isError || !node) { + return
{t('fileFullscreenModal.editor.error')}
; + } + + return ( +