+
+ >
+ )}
+ {!isLoading && !isError && }
);
};
diff --git a/src/script/components/InputBar/FilePreviews/common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton.styles.ts b/src/script/components/InputBar/FilePreviews/common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton.styles.ts
new file mode 100644
index 00000000000..5d4aa9e1f4e
--- /dev/null
+++ b/src/script/components/InputBar/FilePreviews/common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton.styles.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 buttonStyles: CSSObject = {
+ position: 'absolute',
+ top: '-8px',
+ right: '-12px',
+ padding: '0',
+ margin: '0',
+ cursor: 'pointer',
+ width: '24px',
+ height: '24px',
+ background: 'var(--icon-button-primary-enabled-bg)',
+ border: '1px solid var(--icon-button-primary-border)',
+ borderRadius: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+};
+
+export const iconStyles: CSSObject = {
+ fill: 'var(--main-color)',
+};
diff --git a/src/script/components/InputBar/FilePreviews/common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton.tsx b/src/script/components/InputBar/FilePreviews/common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton.tsx
new file mode 100644
index 00000000000..3120f6a5763
--- /dev/null
+++ b/src/script/components/InputBar/FilePreviews/common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 {KeyboardEvent, MouseEvent as ReactMouseEvent} from 'react';
+
+import {MoreIcon} from '@wireapp/react-ui-kit';
+
+import {showContextMenu} from 'src/script/ui/ContextMenu';
+import {isSpaceOrEnterKey} from 'Util/KeyboardUtil';
+import {t} from 'Util/LocalizerUtil';
+import {setContextMenuPosition} from 'Util/util';
+
+import {buttonStyles, iconStyles} from './FilePreviewErrorMoreButton.styles';
+
+interface FilePreviewErrorMoreButtonProps {
+ onDelete: () => void;
+ onRetry: () => void;
+}
+
+export const FilePreviewErrorMoreButton = ({onDelete, onRetry}: FilePreviewErrorMoreButtonProps) => {
+ const showOptionsMenu = (event: ReactMouseEvent | MouseEvent) => {
+ const retryLabel = t('conversationFilePreviewErrorRetry');
+ const removeLabel = t('conversationFilePreviewErrorRemove');
+
+ showContextMenu({
+ event,
+ entries: [
+ {title: retryLabel, label: retryLabel, click: onRetry},
+ {title: removeLabel, label: removeLabel, click: onDelete},
+ ],
+ identifier: 'file-preview-error-more-button',
+ });
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (isSpaceOrEnterKey(event.key)) {
+ const newEvent = setContextMenuPosition(event);
+ showOptionsMenu(newEvent);
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/src/script/components/InputBar/FilePreviews/common/FilePreviewSpinner/FilePreviewSpinner.styles.ts b/src/script/components/InputBar/FilePreviews/common/FilePreviewSpinner/FilePreviewSpinner.styles.ts
new file mode 100644
index 00000000000..8356601ba34
--- /dev/null
+++ b/src/script/components/InputBar/FilePreviews/common/FilePreviewSpinner/FilePreviewSpinner.styles.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 spinnerStyles: CSSObject = {
+ color: 'var(--foreground)',
+};
diff --git a/src/script/components/InputBar/FilePreviews/common/FilePreviewSpinner/FilePreviewSpinner.tsx b/src/script/components/InputBar/FilePreviews/common/FilePreviewSpinner/FilePreviewSpinner.tsx
new file mode 100644
index 00000000000..fb68cc8688f
--- /dev/null
+++ b/src/script/components/InputBar/FilePreviews/common/FilePreviewSpinner/FilePreviewSpinner.tsx
@@ -0,0 +1,24 @@
+/*
+ * 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 {spinnerStyles} from './FilePreviewSpinner.styles';
+
+export const FilePreviewSpinner = () => {
+ return ;
+};
diff --git a/src/script/components/InputBar/FilePreviews/useFilePreview/useFilePreview.ts b/src/script/components/InputBar/FilePreviews/useFilePreview/useFilePreview.ts
new file mode 100644
index 00000000000..8a43e5a2755
--- /dev/null
+++ b/src/script/components/InputBar/FilePreviews/useFilePreview/useFilePreview.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 {FileWithPreview, useFileUploadState} from 'Components/Conversation/useFiles/useFiles';
+import {CellsRepository} from 'src/script/cells/CellsRepository';
+import {getFileExtension, trimFileExtension, formatBytes} from 'Util/util';
+
+interface FilePreviewParams {
+ file: FileWithPreview;
+ cellsRepository: CellsRepository;
+}
+
+export const useFilePreview = ({file, cellsRepository}: FilePreviewParams) => {
+ const {deleteFile, updateFile} = useFileUploadState();
+
+ const name = trimFileExtension(file.name);
+ const extension = getFileExtension(file.name);
+ const size = formatBytes(file.size);
+
+ const isLoading = file.uploadStatus === 'uploading';
+ const isError = file.uploadStatus === 'error';
+
+ const transformedName = isError ? `Upload failed: ${name}` : name;
+
+ const handleDelete = () => {
+ deleteFile(file.id);
+ void cellsRepository.deleteFileDraft({uuid: file.remoteUuid, versionId: file.remoteVersionId});
+ };
+
+ const handleRetry = async () => {
+ try {
+ updateFile(file.id, {uploadStatus: 'uploading'});
+ const {uuid, versionId} = await cellsRepository.uploadFile(file);
+ updateFile(file.id, {remoteUuid: uuid, remoteVersionId: versionId, uploadStatus: 'success'});
+ } catch (error) {
+ updateFile(file.id, {uploadStatus: 'error'});
+ }
+ };
+
+ return {
+ name: transformedName,
+ extension,
+ size,
+ isLoading,
+ isError,
+ handleDelete,
+ handleRetry,
+ };
+};
diff --git a/src/script/components/InputBar/InputBar.tsx b/src/script/components/InputBar/InputBar.tsx
index e42be86a6a5..f64272949bb 100644
--- a/src/script/components/InputBar/InputBar.tsx
+++ b/src/script/components/InputBar/InputBar.tsx
@@ -28,6 +28,7 @@ import {WebAppEvents} from '@wireapp/webapp-events';
import {Avatar, AVATAR_SIZE} from 'Components/Avatar';
import {ConversationClassifiedBar} from 'Components/ClassifiedBar/ClassifiedBar';
+import {useFileUploadState} from 'Components/Conversation/useFiles/useFiles';
import {EmojiPicker} from 'Components/EmojiPicker/EmojiPicker';
import {useUserPropertyValue} from 'src/script/hooks/useUserProperty';
import {PROPERTIES_TYPE} from 'src/script/properties/PropertiesType';
@@ -38,6 +39,7 @@ import {t} from 'Util/LocalizerUtil';
import {TIME_IN_MILLIS} from 'Util/TimeUtil';
import {MessageContent} from './common/messageContent/messageContent';
+import {FilePreviews} from './FilePreviews/FilePreviews';
import {InputBarContainer} from './InputBarContainer/InputBarContainer';
import {InputBarControls} from './InputBarControls/InputBarControls';
import {InputBarEditor} from './InputBarEditor/InputBarEditor';
@@ -120,6 +122,8 @@ export const InputBar = ({
'isIncomingRequest',
]);
+ const {files} = useFileUploadState();
+
const wrapperRef = useRef(null);
const editorRef = useRef(null);
@@ -248,7 +252,7 @@ export const InputBar = ({
className={cx(`conversation-input-bar__input input-bar-container`, {
[`conversation-input-bar__input--editing`]: isEditing,
'input-bar-container--with-toolbar': formatToolbar.open && showMarkdownPreview,
- 'input-bar-container--with-files': false,
+ 'input-bar-container--with-files': !!files.length,
})}
>
{!isOutgoingRequest && (
@@ -318,6 +322,8 @@ export const InputBar = ({
onSend={fileHandling.sendPastedFile}
/>
)}
+
+ {!!files.length && }
{emojiPicker.open ? (
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index 1592565fb58..0703f8c94a1 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -52,6 +52,7 @@ import {BackupRepository} from '../backup/BackupRepository';
import {BackupService} from '../backup/BackupService';
import {CacheRepository} from '../cache/CacheRepository';
import {CallingRepository} from '../calling/CallingRepository';
+import {CellsRepository} from '../cells/CellsRepository';
import {ClientRepository, ClientService} from '../client';
import {getClientMLSConfig} from '../client/clientMLSConfig';
import {Configuration} from '../Config';
@@ -307,6 +308,8 @@ export class App {
);
repositories.preferenceNotification = new PreferenceNotificationRepository(repositories.user['userState'].self);
+ repositories.cells = new CellsRepository();
+
return repositories;
}
diff --git a/src/script/service/APIClientSingleton.ts b/src/script/service/APIClientSingleton.ts
index e3ac0c790c2..11ad139bded 100644
--- a/src/script/service/APIClientSingleton.ts
+++ b/src/script/service/APIClientSingleton.ts
@@ -32,6 +32,19 @@ export class APIClient extends APIClientUnconfigured {
rest: Config.getConfig().BACKEND_REST,
ws: Config.getConfig().BACKEND_WS,
},
+ cells: {
+ pydio: {
+ apiKey: Config.getConfig().CELLS_PYDIO_API_KEY,
+ segment: Config.getConfig().CELLS_PYDIO_SEGMENT,
+ url: Config.getConfig().CELLS_PYDIO_URL,
+ },
+ s3: {
+ apiKey: Config.getConfig().CELLS_S3_API_KEY,
+ bucket: Config.getConfig().CELLS_S3_BUCKET,
+ endpoint: Config.getConfig().CELLS_S3_ENDPOINT,
+ region: Config.getConfig().CELLS_S3_REGION,
+ },
+ },
});
}
}
diff --git a/src/script/view_model/MainViewModel.ts b/src/script/view_model/MainViewModel.ts
index d12dcce9884..1be894869ea 100644
--- a/src/script/view_model/MainViewModel.ts
+++ b/src/script/view_model/MainViewModel.ts
@@ -28,6 +28,7 @@ import type {AssetRepository} from '../assets/AssetRepository';
import type {AudioRepository} from '../audio/AudioRepository';
import type {BackupRepository} from '../backup/BackupRepository';
import type {CallingRepository} from '../calling/CallingRepository';
+import {CellsRepository} from '../cells/CellsRepository';
import type {ClientRepository} from '../client';
import type {ConnectionRepository} from '../connection/ConnectionRepository';
import type {ConversationRepository} from '../conversation/ConversationRepository';
@@ -55,6 +56,7 @@ export interface ViewModelRepositories {
asset: AssetRepository;
audio: AudioRepository;
backup: BackupRepository;
+ cells: CellsRepository;
calling: CallingRepository;
client: ClientRepository;
connection: ConnectionRepository;
diff --git a/src/types/i18n.d.ts b/src/types/i18n.d.ts
index 63094a435bb..0cb466a5f5d 100644
--- a/src/types/i18n.d.ts
+++ b/src/types/i18n.d.ts
@@ -506,6 +506,9 @@ declare module 'I18n/en-US.json' {
'conversationFileUploadFailedTooLargeImagesMessage': `Please select images smaller than {maxSize}MB.`;
'conversationFileUploadFailedTooLargeFilesAndImagesMessage': `Please select files smaller than {maxImageSize}MB and images smaller than {maxFileSize}MB.`;
'conversationFileUploadOverlayTitle': `Upload files`;
+ 'conversationFilePreviewErrorMoreOptions': `More options`;
+ 'conversationFilePreviewErrorRetry': `Retry`;
+ 'conversationFilePreviewErrorRemove': `Remove`;
'conversationFileUploadOverlayDescription': `Drag & drop to add files`;
'conversationFoldersEmptyText': `Add your conversations to folders to stay organized.`;
'conversationFoldersEmptyTextLearnMore': `Learn more`;
diff --git a/yarn.lock b/yarn.lock
index 152e0d054da..62ad9779bcf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7398,9 +7398,9 @@ __metadata:
languageName: node
linkType: hard
-"@wireapp/api-client@npm:^27.20.1":
- version: 27.20.1
- resolution: "@wireapp/api-client@npm:27.20.1"
+"@wireapp/api-client@npm:^27.20.2":
+ version: 27.20.2
+ resolution: "@wireapp/api-client@npm:27.20.2"
dependencies:
"@aws-sdk/client-s3": "npm:3.750.0"
"@wireapp/commons": "npm:^5.4.2"
@@ -7418,7 +7418,7 @@ __metadata:
uuid: "npm:11.1.0"
ws: "npm:8.18.1"
zod: "npm:3.24.2"
- checksum: 10/9a92728cd1200d37c985b5f866e69b71a7f40026da5683f885643075eb89df49bf64ee096851e5d11773faa90ea39600651919016b8748f8fa18a764d2c8b938
+ checksum: 10/b3ab201477faa41fbe231e40feca069632cbdafca9f840a6dc4b67f0db31b9c5f723070968f83ca4307584426a7d7c43911dd40e0f8e6d756ac406bf180d7e52
languageName: node
linkType: hard
@@ -7483,11 +7483,11 @@ __metadata:
languageName: node
linkType: hard
-"@wireapp/core@npm:46.19.5":
- version: 46.19.5
- resolution: "@wireapp/core@npm:46.19.5"
+"@wireapp/core@npm:46.19.6":
+ version: 46.19.6
+ resolution: "@wireapp/core@npm:46.19.6"
dependencies:
- "@wireapp/api-client": "npm:^27.20.1"
+ "@wireapp/api-client": "npm:^27.20.2"
"@wireapp/commons": "npm:^5.4.2"
"@wireapp/core-crypto": "npm:3.1.0"
"@wireapp/cryptobox": "npm:12.8.0"
@@ -7505,7 +7505,7 @@ __metadata:
long: "npm:^5.2.0"
uuid: "npm:9.0.1"
zod: "npm:3.24.2"
- checksum: 10/7246eb2eb6ea4ee50df3606265e60d8348b8909b5ad615b40c32ccece0e3379541aee91cd9181b3bbe3675ab8d85c76d2190ddc1b2895a1fefa57ecab0386db8
+ checksum: 10/6bbe6ad73a3235b163d949c2af23efa599c5313aeb3087b904257d38508caa607349546d51e92242e2c63ad4fa370bf16ce9e41ce76f13f8f627e4cb2e16a7f2
languageName: node
linkType: hard
@@ -20677,7 +20677,7 @@ __metadata:
"@wireapp/avs-debugger": "npm:0.0.7"
"@wireapp/commons": "npm:5.4.2"
"@wireapp/copy-config": "npm:2.3.0"
- "@wireapp/core": "npm:46.19.5"
+ "@wireapp/core": "npm:46.19.6"
"@wireapp/eslint-config": "npm:3.0.7"
"@wireapp/prettier-config": "npm:0.6.4"
"@wireapp/react-ui-kit": "npm:9.38.0"