Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@wireapp/avs": "10.2.19",
"@wireapp/avs-debugger": "0.0.7",
"@wireapp/commons": "5.4.9",
"@wireapp/core": "^46.46.6-beta.9.457c85ad6",
"@wireapp/core": "46.46.7",
"@wireapp/kalium-backup": "0.0.4",
"@wireapp/promise-queue": "2.4.9",
"@wireapp/react-ui-kit": "9.69.6",
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -2047,4 +2050,4 @@
"wireMacos": "{brandName} for macOS",
"wireWindows": "{brandName} for Windows",
"wire_for_web": "{brandName} for Web"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -49,7 +49,7 @@ export const CellsFilePreviewModal = () => {

return (
<FileFullscreenModal
id={id}
id={selectedFile.id}
isOpen={!!selectedFile}
onClose={handleCloseFile}
filePreviewUrl={getFileUrl()}
Expand All @@ -60,6 +60,7 @@ export const CellsFilePreviewModal = () => {
senderName={owner}
timestamp={uploadedAtTimestamp}
badges={tags}
isEditMode={isEditMode}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {DropdownMenu, MoreIcon} from '@wireapp/react-ui-kit';

import {openFolder} from 'Components/CellsGlobalView/common/openFolder/openFolder';
import {CellsRepository} from 'Repositories/cells/CellsRepository';
import {isFileEditable} from 'Util/FileTypeUtil';
import {t} from 'Util/LocalizerUtil';
import {forcedDownloadFile} from 'Util/util';

Expand All @@ -41,6 +42,7 @@ export const CellsTableRowOptions = ({node, cellsRepository}: CellsTableRowOptio

const url = node.url;
const name = node.type === 'folder' ? `${node.name}.zip` : node.name;
Copy link
Contributor

@zskhan zskhan Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving these folder and file into enum would be better
This can be tackled in another PR.

const isEditable = node.type === 'file' && isFileEditable(node.extension);

return (
<DropdownMenu>
Expand All @@ -59,6 +61,9 @@ export const CellsTableRowOptions = ({node, cellsRepository}: CellsTableRowOptio
<DropdownMenu.Item onClick={() => showShareModal({type: node.type, uuid: node.id, cellsRepository})}>
{t('cells.options.share')}
</DropdownMenu.Item>
{isEditable && (
<DropdownMenu.Item onClick={() => handleOpenFile(node, true)}>{t('cells.options.edit')}</DropdownMenu.Item>
)}
{!!url && (
<DropdownMenu.Item
onClick={() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -36,17 +37,22 @@ interface FilePreviewProviderProps {

export const FilePreviewProvider = ({children}: FilePreviewProviderProps) => {
const [selectedFile, setSelectedFile] = useState<CellFile | null>(null);
const [isEditMode, setIsEditMode] = useState(false);

const id = useId();

const value = useMemo(
() => ({
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 <CellsFilePreviewModalContext.Provider value={value}>{children}</CellsFilePreviewModalContext.Provider>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -49,7 +49,7 @@ export const CellsFilePreviewModal = () => {

return (
<FileFullscreenModal
id={id}
id={selectedFile.id}
isOpen={!!selectedFile}
onClose={handleCloseFile}
filePreviewUrl={getFileUrl()}
Expand All @@ -60,6 +60,7 @@ export const CellsFilePreviewModal = () => {
senderName={owner}
timestamp={uploadedAtTimestamp}
badges={tags}
isEditMode={isEditMode}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
isRootRecycleBinPath,
} from 'Components/Conversation/ConversationCells/common/recycleBin/recycleBin';
import {CellsRepository} from 'Repositories/cells/CellsRepository';
import {isFileEditable} from 'Util/FileTypeUtil';
import {t} from 'Util/LocalizerUtil';
import {forcedDownloadFile} from 'Util/util';

Expand Down Expand Up @@ -124,6 +125,8 @@ const CellsTableRowOptionsContent = ({
const isRootRecycleBin = isRootRecycleBinPath();
const isNestedRecycleBin = isInRecycleBin();

const isEditable = node.type === 'file' && isFileEditable(node.extension);

if (isRootRecycleBin || isNestedRecycleBin) {
return (
<DropdownMenu.Content>
Expand Down Expand Up @@ -190,6 +193,9 @@ const CellsTableRowOptionsContent = ({
</DropdownMenu.Item>
<DropdownMenu.Item onClick={() => setIsMoveNodeModalOpen(true)}>{t('cells.options.move')}</DropdownMenu.Item>
<DropdownMenu.Item onClick={() => setIsTagsModalOpen(true)}>{t('cells.options.tags')}</DropdownMenu.Item>
{isEditable && (
<DropdownMenu.Item onClick={() => handleOpenFile(node, true)}>{t('cells.options.edit')}</DropdownMenu.Item>
)}
<DropdownMenu.Item
onClick={() =>
showMoveToRecycleBinModal({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -36,17 +37,22 @@ interface FilePreviewProviderProps {

export const CellsFilePreviewModalProvider = ({children}: FilePreviewProviderProps) => {
const [selectedFile, setSelectedFile] = useState<CellFile | null>(null);
const [isEditMode, setIsEditMode] = useState(false);

const id = useId();

const value = useMemo(
() => ({
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 <CellsFilePreviewModalContext.Provider value={value}>{children}</CellsFilePreviewModalContext.Provider>;
Expand Down
Original file line number Diff line number Diff line change
@@ -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%',
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The iframe should have a border of 0 to avoid rendering a default border in some browsers:

export const editorIframe: CSSObject = {
  width: '100%',
  height: '100%',
  border: '0',
};
Suggested change
height: '100%',
height: '100%',
border: '0',

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] [Suggestion] The border styling appears to be missing from the iframe. For better visual consistency and to clearly separate the embedded content from the parent UI, consider adding a border property:

export const editorIframe: CSSObject = {
  width: '100%',
  height: '100%',
  border: 'none', // or '1px solid #ccc' if a border is desired
};
Suggested change
height: '100%',
height: '100%',
border: '1px solid #ccc', // Ensures visual separation of iframe content

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] [Suggestion] The iframe styling should include border: 'none' to remove the default iframe border, which is inconsistent with modern UI design.

Recommendation:

export const editorIframe: CSSObject = {
  width: '100%',
  height: '100%',
  border: 'none',
};
Suggested change
height: '100%',
height: '100%',
border: 'none',

Copilot uses AI. Check for mistakes.
};
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use TIME_IN_MILLIS.SECOND instead

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<Node | null>(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) {
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] [Suggestion] Error handling is incomplete - the err is caught but not logged. This makes debugging production issues difficult.

Add error logging:

} catch (err) {
  console.error('Failed to fetch node with editor URLs:', err);
  setIsError(true);
}
Suggested change
} catch (err) {
} catch (err) {
console.error('Failed to fetch node with editor URLs:', err);

Copilot uses AI. Check for mistakes.
setIsError(true);
} finally {

Check warning on line 53 in src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrLA7xPcXMBJo7p8zdZ&open=AZrLA7xPcXMBJo7p8zdZ&pullRequest=19813
setIsLoading(false);
}
}, [id, cellsRepository]);
Comment on lines +45 to +56
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The error handling doesn't capture or log the error details, making debugging difficult. When the API call fails, the error is silently swallowed.

Recommendation: Add proper error logging:

const fetchNode = useCallback(async () => {
  try {
    setIsLoading(true);
    setIsError(false);
    const fetchedNode = await cellsRepository.getNode({uuid: id, flags: ['WithEditorURLs']});
    setNode(fetchedNode);
  } catch (err) {
    console.error('Failed to fetch editor URLs:', err);
    setIsError(true);
  } finally {
    setIsLoading(false);
  }
}, [id, cellsRepository]);

Copilot uses AI. Check for mistakes.

// Initial fetch
useEffect(() => {
void fetchNode();

Check failure on line 60 in src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of the "void" operator.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrLA7xPcXMBJo7p8zda&open=AZrLA7xPcXMBJo7p8zda&pullRequest=19813
}, [id, cellsRepository, fetchNode]);
Comment on lines +56 to +61
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] The useEffect dependency array is incomplete. The fetchNode callback is included, but it depends on cellsRepository, which is resolved inside the component. Since cellsRepository comes from a DI container, it's stable, but for correctness and to avoid future issues, consider one of these approaches:

  1. Move cellsRepository outside the callback:
const cellsRepository = container.resolve(CellsRepository);

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]);
  1. Or simplify by removing cellsRepository from the dependency list since it's stable from the container.
Suggested change
}, [id, cellsRepository]);
// Initial fetch
useEffect(() => {
void fetchNode();
}, [id, cellsRepository, fetchNode]);
}, [id]);
// Initial fetch
useEffect(() => {
void fetchNode();
}, [id, fetchNode]);

Copilot uses AI. Check for mistakes.

// Auto-refresh mechanism before expiry
useEffect(() => {
if (!node?.EditorURLs?.collabora.ExpiresAt) {
return undefined;
}

const expiresInSeconds = Number(node.EditorURLs.collabora.ExpiresAt);
const refreshInSeconds = expiresInSeconds - REFRESH_BUFFER_SECONDS;

// Set timeout to refresh before expiry
const timeoutId = setTimeout(() => {
void fetchNode();

Check failure on line 74 in src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of the "void" operator.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrLA7xPcXMBJo7p8zdb&open=AZrLA7xPcXMBJo7p8zdb&pullRequest=19813
}, refreshInSeconds * MILLISECONDS_IN_SECOND);
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Important] Potential for extremely large or negative timeout values. If ExpiresAt is very large or refreshInSeconds is negative, this could cause issues.

Add validation:

const expiresInSeconds = Number(node.EditorURLs.collabora.ExpiresAt);

// Validate the expiry time is reasonable
if (isNaN(expiresInSeconds) || expiresInSeconds <= 0 || expiresInSeconds > 86400) {
  // Invalid or unreasonable expiry (> 24 hours)
  return undefined;
}

const refreshInSeconds = expiresInSeconds - REFRESH_BUFFER_SECONDS;

// Don't set timeout if already expired or refresh time is negative
if (refreshInSeconds <= 0) {
  return undefined;
}

const timeoutId = setTimeout(() => {
  void fetchNode();
}, refreshInSeconds * MILLISECONDS_IN_SECOND);

This prevents setting invalid timeouts that could cause unexpected behavior.

Copilot uses AI. Check for mistakes.

return () => {
clearTimeout(timeoutId);
};
Comment on lines 72 to 79
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] The timeout calculation can create negative or zero timeouts when expiresInSeconds is less than or equal to REFRESH_BUFFER_SECONDS (10 seconds). This will cause immediate re-fetching in a loop.

Add validation:

const expiresInSeconds = Number(node.EditorURLs.collabora.ExpiresAt);
const refreshInSeconds = Math.max(0, expiresInSeconds - REFRESH_BUFFER_SECONDS);

// Only set timeout if there's meaningful time left
if (refreshInSeconds > 0) {
  const timeoutId = setTimeout(() => {
    void fetchNode();
  }, refreshInSeconds * MILLISECONDS_IN_SECOND);
  
  return () => clearTimeout(timeoutId);
}
Suggested change
// Set timeout to refresh before expiry
const timeoutId = setTimeout(() => {
void fetchNode();
}, refreshInSeconds * MILLISECONDS_IN_SECOND);
return () => {
clearTimeout(timeoutId);
};
// Only set timeout if there's meaningful time left
if (refreshInSeconds > 0) {
const timeoutId = setTimeout(() => {
void fetchNode();
}, refreshInSeconds * MILLISECONDS_IN_SECOND);
return () => {
clearTimeout(timeoutId);
};
}

Copilot uses AI. Check for mistakes.
Comment on lines 70 to 79
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] Using Number() on ExpiresAt can produce NaN if the value is not a valid number. This would make refreshInSeconds also NaN, leading to unexpected behavior. Add validation:

const expiresInSeconds = Number(node.EditorURLs.collabora.ExpiresAt);

if (isNaN(expiresInSeconds) || expiresInSeconds <= 0) {
  return;
}

const refreshInSeconds = Math.max(0, expiresInSeconds - REFRESH_BUFFER_SECONDS);

if (refreshInSeconds > 0) {
  const timeoutId = setTimeout(() => {
    void fetchNode();
  }, refreshInSeconds * MILLISECONDS_IN_SECOND);

  return () => clearTimeout(timeoutId);
}
Suggested change
const refreshInSeconds = expiresInSeconds - REFRESH_BUFFER_SECONDS;
// Set timeout to refresh before expiry
const timeoutId = setTimeout(() => {
void fetchNode();
}, refreshInSeconds * MILLISECONDS_IN_SECOND);
return () => {
clearTimeout(timeoutId);
};
if (isNaN(expiresInSeconds) || expiresInSeconds <= 0) {
return;
}
const refreshInSeconds = Math.max(0, expiresInSeconds - REFRESH_BUFFER_SECONDS);
// Set timeout to refresh before expiry
if (refreshInSeconds > 0) {
const timeoutId = setTimeout(() => {
void fetchNode();
}, refreshInSeconds * MILLISECONDS_IN_SECOND);
return () => {
clearTimeout(timeoutId);
};
}

Copilot uses AI. Check for mistakes.
}, [node, fetchNode]);
Comment on lines 64 to 80
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] [Suggestion] The auto-refresh effect could cause memory leaks if fetchNode changes frequently. The dependency array includes fetchNode, which is memoized with useCallback, but if the component re-renders and fetchNode is recreated, a new timeout will be set without clearing the old one properly.

Consider removing fetchNode from the dependency array and using a ref pattern:

useEffect(() => {
  if (!node?.EditorURLs?.collabora.ExpiresAt) {
    return undefined;
  }

  const expiresInSeconds = Number(node.EditorURLs.collabora.ExpiresAt);
  const refreshInSeconds = Math.max(0, expiresInSeconds - REFRESH_BUFFER_SECONDS);

  if (refreshInSeconds <= 0) {
    return undefined;
  }

  const timeoutId = setTimeout(() => {
    void fetchNode();
  }, refreshInSeconds * MILLISECONDS_IN_SECOND);

  return () => {
    clearTimeout(timeoutId);
  };
}, [node]); // Remove fetchNode from dependencies

Copilot uses AI. Check for mistakes.

if (isLoading) {
return <FileLoader />;
}

if (isError || !node) {
return <div>{t('fileFullscreenModal.editor.error')}</div>;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] [Suggestion] The error display lacks proper styling and structure. Consider using a proper error component with consistent styling:

if (isError || !node) {
  return (
    <div css={{
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      height: '100%',
      padding: '2rem',
      textAlign: 'center'
    }}>
      {t('fileFullscreenModal.editor.error')}
    </div>
  );
}
Suggested change
return <div>{t('fileFullscreenModal.editor.error')}</div>;
return (
<div css={styles.errorContainer}>
{t('fileFullscreenModal.editor.error')}
</div>
);

Copilot uses AI. Check for mistakes.
}
Comment on lines +86 to +88
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] [Suggestion] The error state renders a plain <div> with text, which is inconsistent with how errors are handled elsewhere in FileFullscreenModal (e.g., NoPreviewAvailable uses FilePlaceholder).

For consistency, use a proper error UI component:

import {FilePlaceholder} from '../common/FilePlaceholder/FilePlaceholder';

if (isError || !node) {
  return (
    <FilePlaceholder
      title={t('fileFullscreenModal.editor.error')}
      description={t('fileFullscreenModal.editor.errorDescription')}
    />
  );
}

This provides a better user experience with properly styled error messaging.

Copilot uses AI. Check for mistakes.

return (
<iframe
css={styles.editorIframe}

Check warning on line 92 in src/script/components/FileFullscreenModal/FileEditor/FileEditor.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrLA7xPcXMBJo7p8zdc&open=AZrLA7xPcXMBJo7p8zdc&pullRequest=19813
src={node.EditorURLs?.collabora.Url}
Comment on lines +86 to +93
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Important] There's no validation that node.EditorURLs?.collabora.Url is actually a string before using it as the iframe src. Add validation:

if (isError || !node || !node.EditorURLs?.collabora?.Url) {
  return <div>{t('fileFullscreenModal.editor.error')}</div>;
}

const editorUrl = node.EditorURLs.collabora.Url;

return (
  <iframe
    css={styles.editorIframe}
    src={editorUrl}
    title={t('fileFullscreenModal.editor.iframeTitle')}
  />
);
Suggested change
if (isError || !node) {
return <div>{t('fileFullscreenModal.editor.error')}</div>;
}
return (
<iframe
css={styles.editorIframe}
src={node.EditorURLs?.collabora.Url}
const editorUrl = node?.EditorURLs?.collabora?.Url;
if (
isError ||
!node ||
typeof editorUrl !== 'string' ||
editorUrl.trim() === ''
) {
return <div>{t('fileFullscreenModal.editor.error')}</div>;
}
return (
<iframe
css={styles.editorIframe}
src={editorUrl}

Copilot uses AI. Check for mistakes.
title={t('fileFullscreenModal.editor.iframeTitle')}
/>
Comment on lines +91 to +95
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Important] The iframe lacks security attributes recommended for embedding external content. Add sandbox and other security attributes:

<iframe
  css={styles.editorIframe}
  src={node.EditorURLs?.collabora.Url}
  title={t('fileFullscreenModal.editor.iframeTitle')}
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
  referrerPolicy="no-referrer"
/>

Note: Adjust the sandbox permissions based on actual Collabora requirements - use the most restrictive set that still allows the editor to function.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +91 to +95
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Important] The iframe src attribute uses an unsanitized URL from the API response (node.EditorURLs?.collabora.Url). While this URL comes from a trusted backend, it should still be validated to ensure it uses HTTPS protocol and doesn't contain javascript: or data: schemes.

Consider adding URL validation:

const validateEditorUrl = (url: string): boolean => {
  try {
    const parsed = new URL(url);
    return parsed.protocol === 'https:';
  } catch {
    return false;
  }
};

// Then in the render:
if (!node.EditorURLs?.collabora.Url || !validateEditorUrl(node.EditorURLs.collabora.Url)) {
  return <div>{t('fileFullscreenModal.editor.error')}</div>;
}

Copilot generated this review using guidance from repository custom instructions.
);
};
Comment on lines 39 to 97
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new FileEditor component lacks test coverage. Given that other components in the codebase have comprehensive tests (e.g., FileTypeUtil.test.ts, FileAssetOptions.test.tsx), this critical component handling iframe integration should also have tests.

Recommendation: Add tests covering:

  • Loading state display
  • Error state display
  • Successful iframe rendering
  • URL refresh logic and timer handling
  • Handling of missing or invalid EditorURLs

Copilot uses AI. Check for mistakes.
Loading
Loading