diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 35180b38b24..501c5ddd2ba 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Publish on: push: - branches: [master, dev, release/*, new-navigation] + branches: [master, dev, release/*, new-navigation, feat/cells-service-api-deploy-to-imai] tags: - '*q1-2024*' - '*staging*' @@ -196,6 +196,9 @@ jobs: }, "q1-2024": { "targets": "[\"q1-2024\"]" + }, + "feat/cells-service-api": { + "targets": "[\"wire-cells-dev\"]" } } export_to: log,output diff --git a/package.json b/package.json index 70fb2f3e586..bceb94342b2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@wireapp/avs": "9.10.25", "@wireapp/avs-debugger": "0.0.7", "@wireapp/commons": "5.4.2", - "@wireapp/core": "46.19.5", + "@wireapp/core": "46.19.6", "@wireapp/react-ui-kit": "9.38.0", "@wireapp/store-engine-dexie": "2.1.15", "@wireapp/telemetry": "0.3.1", diff --git a/server/config/client.config.ts b/server/config/client.config.ts index 328e60172de..f72685b4dcc 100644 --- a/server/config/client.config.ts +++ b/server/config/client.config.ts @@ -30,6 +30,13 @@ export function generateConfig(params: ConfigGeneratorParams, env: Env) { BACKEND_REST: urls.api ?? '', BACKEND_WS: urls.ws ?? '', BRAND_NAME: env.BRAND_NAME, + CELLS_PYDIO_API_KEY: env.CELLS_PYDIO_API_KEY, + CELLS_PYDIO_SEGMENT: env.CELLS_PYDIO_SEGMENT, + CELLS_PYDIO_URL: env.CELLS_PYDIO_URL, + CELLS_S3_API_KEY: env.CELLS_S3_API_KEY, + CELLS_S3_BUCKET: env.CELLS_S3_BUCKET, + CELLS_S3_REGION: env.CELLS_S3_REGION, + CELLS_S3_ENDPOINT: env.CELLS_S3_ENDPOINT, COUNTLY_API_KEY: env.COUNTLY_API_KEY, COUNTLY_ENABLE_LOGGING: env.COUNTLY_ENABLE_LOGGING == 'true', COUNTLY_FORCE_REPORTING: env.COUNTLY_FORCE_REPORTING == 'true', diff --git a/server/config/env.ts b/server/config/env.ts index 8b08dbc751f..f2d23a6973f 100644 --- a/server/config/env.ts +++ b/server/config/env.ts @@ -46,6 +46,15 @@ export type Env = { /** Specifies the name of the application, e.g. Webapp */ APP_NAME: string; + /** Specifies configuration for Cells */ + CELLS_PYDIO_API_KEY: string; + CELLS_PYDIO_SEGMENT: string; + CELLS_PYDIO_URL: string; + CELLS_S3_API_KEY: string; + CELLS_S3_BUCKET: string; + CELLS_S3_REGION: string; + CELLS_S3_ENDPOINT: string; + /** Specifies the name of the backend, e.g. Wire */ BACKEND_NAME: string; diff --git a/server/config/server.config.ts b/server/config/server.config.ts index cf6dfa07940..7c677c25950 100644 --- a/server/config/server.config.ts +++ b/server/config/server.config.ts @@ -30,7 +30,7 @@ const ROBOTS_ALLOW_FILE = path.join(ROBOTS_DIR, 'robots.txt'); const ROBOTS_DISALLOW_FILE = path.join(ROBOTS_DIR, 'robots-disallow.txt'); const defaultCSP = { - connectSrc: ["'self'", 'blob:', 'data:', 'https://*.giphy.com'], + connectSrc: ["'self'", 'blob:', 'data:', 'https://*.giphy.com', 'https://service.zeta.pydiocells.com'], defaultSrc: ["'self'"], fontSrc: ["'self'", 'data:'], frameSrc: [ diff --git a/setupTests.js b/setupTests.js index 82188f92064..c8fa1a17c99 100644 --- a/setupTests.js +++ b/setupTests.js @@ -69,3 +69,7 @@ Object.defineProperty(document, 'elementFromPoint', { const testLib = require('@testing-library/react'); testLib.configure({testIdAttribute: 'data-uie-name'}); + +jest.mock('@formkit/auto-animate/react', () => ({ + useAutoAnimate: () => [null, () => {}], +})); diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index b82a32af798..9eadc3bfad0 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -502,6 +502,9 @@ "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", @@ -1754,4 +1757,4 @@ "paginationLeftArrowAriaLabel": "Go to previous page", "paginationRightArrowAriaLabel": "Go to next page", "paginationDotAriaLabel": "Go to page {page}" -} \ No newline at end of file +} diff --git a/src/script/cells/CellsRepository.ts b/src/script/cells/CellsRepository.ts new file mode 100644 index 00000000000..19adc4d5684 --- /dev/null +++ b/src/script/cells/CellsRepository.ts @@ -0,0 +1,47 @@ +/* + * 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 {container} from 'tsyringe'; + +import {createUuid} from 'Util/uuid'; + +import {APIClient} from '../service/APIClientSingleton'; + +export class CellsRepository { + private readonly basePath = 'wire-cells-web'; + constructor(private readonly apiClient = container.resolve(APIClient)) {} + + async uploadFile(file: File): Promise<{uuid: string; versionId: string}> { + const path = `${this.basePath}/${encodeURIComponent(file.name)}`; + + const uuid = createUuid(); + const versionId = createUuid(); + + await this.apiClient.api.cells.uploadFileDraft({filePath: path, file, uuid, versionId}); + + return { + uuid, + versionId, + }; + } + + async deleteFileDraft({uuid, versionId}: {uuid: string; versionId: string}) { + return this.apiClient.api.cells.deleteFileDraft({uuid, versionId}); + } +} diff --git a/src/script/components/Conversation/Conversation.tsx b/src/script/components/Conversation/Conversation.tsx index e8eeafc98d0..9ddc45d35fc 100644 --- a/src/script/components/Conversation/Conversation.tsx +++ b/src/script/components/Conversation/Conversation.tsx @@ -19,13 +19,11 @@ import {UIEvent, useCallback, useEffect, useState} from 'react'; -import cx from 'classnames'; import {container} from 'tsyringe'; import {useMatchMedia} from '@wireapp/react-ui-kit'; import {CallingCell} from 'Components/calling/CallingCell'; -import {DropFileArea} from 'Components/DropFileArea'; import {Giphy} from 'Components/Giphy'; import {InputBar} from 'Components/InputBar'; import {MessagesList} from 'Components/MessagesList'; @@ -42,8 +40,9 @@ import {isHittingUploadLimit} from 'Util/isHittingUploadLimit'; import {t} from 'Util/LocalizerUtil'; import {getLogger} from 'Util/Logger'; import {safeMailOpen, safeWindowOpen} from 'Util/SanitizationUtil'; -import {formatBytes, incomingCssClass, removeAnimationsClass} from 'Util/util'; +import {formatBytes} from 'Util/util'; +import {ConversationFileDropzone} from './ConversationFileDropzone/ConversationFileDropzone'; import {useReadReceiptSender} from './hooks/useReadReceipt'; import {ReadOnlyConversationMessage} from './ReadOnlyConversationMessage'; import {checkFileSharingPermission} from './utils/checkFileSharingPermission'; @@ -463,12 +462,12 @@ export const Conversation = ({ ); return ( - {activeConversation && ( <> @@ -561,6 +560,6 @@ export const Conversation = ({ {isGiphyModalOpen && inputValue && ( )} - + ); }; diff --git a/src/script/components/Conversation/ConversationFileDropzone/ConversationFileDropzone.tsx b/src/script/components/Conversation/ConversationFileDropzone/ConversationFileDropzone.tsx new file mode 100644 index 00000000000..f5b84dbf663 --- /dev/null +++ b/src/script/components/Conversation/ConversationFileDropzone/ConversationFileDropzone.tsx @@ -0,0 +1,72 @@ +/* + * 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 {ReactNode} from 'react'; + +import cx from 'classnames'; + +import {DropFileArea} from 'Components/DropFileArea'; +import {incomingCssClass, removeAnimationsClass} from 'Util/util'; + +import {FileDropzone} from './FileDropzone/FileDropzone'; + +interface ConversationFileDropzoneProps { + inTeam: boolean; + isCellsEnabled: boolean; + isConversationLoaded: boolean; + activeConversationId?: string; + onFileDropped: (files: File[]) => void; + children: ReactNode; +} + +export const ConversationFileDropzone = ({ + inTeam, + isCellsEnabled, + isConversationLoaded, + activeConversationId, + onFileDropped, + children, +}: ConversationFileDropzoneProps) => { + if (isCellsEnabled) { + return ( + +
+ {children} +
+
+ ); + } + + return ( + + {children} + + ); +}; diff --git a/src/script/components/Conversation/FileDropzone/FileDropzone.styles.ts b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzone.styles.ts similarity index 100% rename from src/script/components/Conversation/FileDropzone/FileDropzone.styles.ts rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzone.styles.ts diff --git a/src/script/components/Conversation/FileDropzone/FileDropzone.tsx b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzone.tsx similarity index 72% rename from src/script/components/Conversation/FileDropzone/FileDropzone.tsx rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzone.tsx index c4ef68c9cea..9cbb1323276 100644 --- a/src/script/components/Conversation/FileDropzone/FileDropzone.tsx +++ b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzone.tsx @@ -20,9 +20,12 @@ import {ReactNode, useEffect} from 'react'; import {FileRejection, useDropzone} from 'react-dropzone'; +import {container} from 'tsyringe'; +import {CellsRepository} from 'src/script/cells/CellsRepository'; import {Config} from 'src/script/Config'; import {t} from 'Util/LocalizerUtil'; +import {getLogger} from 'Util/Logger'; import {createUuid} from 'Util/uuid'; import {wrapperStyles} from './FileDropzone.styles'; @@ -31,25 +34,42 @@ import {FileDropzoneOverlay} from './FileDropzoneOverlay/FileDropzoneOverlay'; import {validateFiles, ValidationResult} from './fileValidation/fileValidation'; import {useIsDragging} from './useIsDragging/useIsDragging'; -import {useFileUploadState} from '../useFiles/useFiles'; -import {checkFileSharingPermission} from '../utils/checkFileSharingPermission'; +import {FileWithPreview, useFileUploadState} from '../../useFiles/useFiles'; +import {checkFileSharingPermission} from '../../utils/checkFileSharingPermission'; interface FileDropzoneProps { children: ReactNode; isTeam: boolean; + cellsRepository?: CellsRepository; } const MAX_FILES = 10; const CONFIG = Config.getConfig(); -export const FileDropzone = ({isTeam, children}: FileDropzoneProps) => { +const logger = getLogger('FileDropzone'); + +export const FileDropzone = ({ + isTeam, + cellsRepository = container.resolve(CellsRepository), + children, +}: FileDropzoneProps) => { const {isDragging, wrapperRef} = useIsDragging(); - const {addFiles, files} = useFileUploadState(); + const {addFiles, files, updateFile} = useFileUploadState(); const MAX_SIZE = isTeam ? CONFIG.MAXIMUM_ASSET_FILE_SIZE_TEAM : CONFIG.MAXIMUM_ASSET_FILE_SIZE_PERSONAL; + const uploadFile = async (file: FileWithPreview) => { + try { + const {uuid, versionId} = await cellsRepository.uploadFile(file); + updateFile(file.id, {remoteUuid: uuid, remoteVersionId: versionId, uploadStatus: 'success'}); + } catch (error) { + logger.error('Uploading file failed', error); + updateFile(file.id, {uploadStatus: 'error'}); + } + }; + const {getRootProps, getInputProps, isDragAccept} = useDropzone({ maxSize: MAX_SIZE, noClick: true, @@ -78,12 +98,20 @@ export const FileDropzone = ({isTeam, children}: FileDropzoneProps) => { return Object.assign(file, { id: createUuid(), preview: URL.createObjectURL(file), + remoteUuid: '', + remoteVersionId: '', + uploadStatus: 'uploading' as const, }); }); addFiles(acceptedFilesWithPreview); + + acceptedFilesWithPreview.forEach(file => { + void uploadFile(file); + }); }), - onError: () => { + onError: (error: Error) => { + logger.error('Dropping files failed', error); showFileDropzoneErrorModal({ title: t('conversationFileUploadFailedHeading'), message: t('conversationFileUploadFailedMessage'), diff --git a/src/script/components/Conversation/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.styles.ts b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.styles.ts similarity index 100% rename from src/script/components/Conversation/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.styles.ts rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.styles.ts diff --git a/src/script/components/Conversation/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.tsx b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.tsx similarity index 100% rename from src/script/components/Conversation/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.tsx rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneErrorModal/FileDropzoneErrorModal.tsx diff --git a/src/script/components/Conversation/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.styles.ts b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.styles.ts similarity index 100% rename from src/script/components/Conversation/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.styles.ts rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.styles.ts diff --git a/src/script/components/Conversation/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.tsx b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.tsx similarity index 100% rename from src/script/components/Conversation/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.tsx rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/FileDropzoneOverlay/FileDropzoneOverlay.tsx diff --git a/src/script/components/Conversation/FileDropzone/fileValidation/fileValidation.ts b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/fileValidation/fileValidation.ts similarity index 100% rename from src/script/components/Conversation/FileDropzone/fileValidation/fileValidation.ts rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/fileValidation/fileValidation.ts diff --git a/src/script/components/Conversation/FileDropzone/useIsDragging/useIsDragging.ts b/src/script/components/Conversation/ConversationFileDropzone/FileDropzone/useIsDragging/useIsDragging.ts similarity index 100% rename from src/script/components/Conversation/FileDropzone/useIsDragging/useIsDragging.ts rename to src/script/components/Conversation/ConversationFileDropzone/FileDropzone/useIsDragging/useIsDragging.ts diff --git a/src/script/components/Conversation/useFiles/useFiles.ts b/src/script/components/Conversation/useFiles/useFiles.ts index edd71999f45..92b41efe452 100644 --- a/src/script/components/Conversation/useFiles/useFiles.ts +++ b/src/script/components/Conversation/useFiles/useFiles.ts @@ -19,15 +19,32 @@ import {create} from 'zustand'; +export type FileUploadStatus = 'pending' | 'uploading' | 'success' | 'error'; + export interface FileWithPreview extends File { id: string; preview: string; + remoteUuid: string; + remoteVersionId: string; + uploadStatus: FileUploadStatus; } interface FileUploadState { files: FileWithPreview[]; addFiles: (files: FileWithPreview[]) => void; deleteFile: (fileId: string) => void; + updateFile: ( + fileId: string, + { + remoteUuid, + remoteVersionId, + uploadStatus, + }: { + remoteUuid?: string; + remoteVersionId?: string; + uploadStatus?: FileUploadStatus; + }, + ) => void; } export const useFileUploadState = create(set => ({ @@ -40,4 +57,15 @@ export const useFileUploadState = create(set => ({ set(state => ({ files: state.files.filter(file => file.id !== fileId), })), + updateFile: (fileId, {remoteUuid, remoteVersionId, uploadStatus}) => + set(state => ({ + files: state.files.map(file => { + if (file.id === fileId) { + file.remoteUuid = remoteUuid ?? file.remoteUuid; + file.remoteVersionId = remoteVersionId ?? file.remoteVersionId; + file.uploadStatus = uploadStatus ?? file.uploadStatus; + } + return file; + }), + })), })); diff --git a/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.styles.ts b/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.styles.ts index 92f14ec7af2..ca577b0010f 100644 --- a/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.styles.ts +++ b/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.styles.ts @@ -37,3 +37,16 @@ export const playerWrapperStyles: CSSObject = { gap: '4px', width: '100%', }; + +export const loaderWrapperStyles: CSSObject = { + width: '32px', + height: '32px', + backgroundColor: 'var(--white)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +export const loadingStyles: CSSObject = { + color: 'var(--foreground)', +}; diff --git a/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.tsx b/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.tsx index d3ed1fe3a93..9f1fcda4d43 100644 --- a/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.tsx +++ b/src/script/components/InputBar/FilePreviews/AudioPreviewCard/AudioPreviewCard.tsx @@ -24,19 +24,21 @@ import { } from 'Components/MessagesList/Message/ContentMessage/asset/AudioAsset/AudioAssetV2.styles'; import {AudioEmptySeekBar} from './AudioEmptySeekBar/AudioEmptySeekBar'; -import {wrapperStyles} from './AudioPreviewCard.styles'; +import {loaderWrapperStyles, wrapperStyles} from './AudioPreviewCard.styles'; import {FilePreviewDeleteButton} from '../common/FilePreviewDeleteButton/FilePreviewDeleteButton'; +import {FilePreviewErrorMoreButton} from '../common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton'; import {FilePreviewPlayButton} from '../common/FilePreviewPlayButton/FilePreviewPlayButton'; +import {FilePreviewSpinner} from '../common/FilePreviewSpinner/FilePreviewSpinner'; interface AudioPreviewCardProps { extension: string; name: string; size: string; - isError?: boolean; - isLoading?: boolean; - loadingProgress?: number; + isError: boolean; + isLoading: boolean; onDelete: () => void; + onRetry: () => void; } export const AudioPreviewCard = ({ @@ -45,8 +47,8 @@ export const AudioPreviewCard = ({ size, isError, isLoading, - loadingProgress, onDelete, + onRetry, }: AudioPreviewCardProps) => { return (
@@ -56,15 +58,25 @@ export const AudioPreviewCard = ({ -
- + {isLoading ? ( +
+ +
+ ) : ( + + )}
- {isError && } - {isLoading && } + {isError && ( + <> + + + + )} + {!isError && !isLoading && }
); diff --git a/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.styles.ts b/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.styles.ts index 283c06e99d7..1a106f214c2 100644 --- a/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.styles.ts +++ b/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.styles.ts @@ -22,3 +22,10 @@ import {CSSObject} from '@emotion/react'; export const wrapperStyles: CSSObject = { gridColumn: 'span 3', }; + +export const loadingWrapperStyles: CSSObject = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginLeft: 'auto', +}; diff --git a/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.tsx b/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.tsx index 23177e40662..492eca61f21 100644 --- a/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.tsx +++ b/src/script/components/InputBar/FilePreviews/FilePreviewCard/FilePreviewCard.tsx @@ -17,23 +17,22 @@ * */ -import {ReactNode} from 'react'; - import {FileCard} from 'Components/FileCard/FileCard'; -import {wrapperStyles} from './FilePreviewCard.styles'; +import {loadingWrapperStyles, wrapperStyles} from './FilePreviewCard.styles'; import {FilePreviewDeleteButton} from '../common/FilePreviewDeleteButton/FilePreviewDeleteButton'; +import {FilePreviewErrorMoreButton} from '../common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton'; +import {FilePreviewSpinner} from '../common/FilePreviewSpinner/FilePreviewSpinner'; interface FilePreviewCardProps { extension: string; name: string; size: string; - isError?: boolean; - isLoading?: boolean; - loadingProgress?: number; + isError: boolean; + isLoading: boolean; onDelete: () => void; - children?: ReactNode; + onRetry: () => void; } export const FilePreviewCard = ({ @@ -42,9 +41,8 @@ export const FilePreviewCard = ({ size, isError, isLoading, - loadingProgress, onDelete, - children, + onRetry, }: FilePreviewCardProps) => { return (
@@ -52,12 +50,20 @@ export const FilePreviewCard = ({ + {isLoading && ( +
+ +
+ )}
- - {children} - {isError && } - {isLoading && } + {isError && ( + <> + + + + )} + {!isError && !isLoading && }
); diff --git a/src/script/components/InputBar/FilePreviews/FilePreviews.tsx b/src/script/components/InputBar/FilePreviews/FilePreviews.tsx index b562d6935d3..150e1f9d996 100644 --- a/src/script/components/InputBar/FilePreviews/FilePreviews.tsx +++ b/src/script/components/InputBar/FilePreviews/FilePreviews.tsx @@ -18,15 +18,17 @@ */ import {useAutoAnimate} from '@formkit/auto-animate/react'; +import {container} from 'tsyringe'; -import {FileWithPreview, useFileUploadState} from 'Components/Conversation/useFiles/useFiles'; -import {isAudio, isImage, isVideo} from 'src/script/assets/AssetMetaDataBuilder'; -import {formatBytes, getFileExtension, trimFileExtension} from 'Util/util'; +import {FileWithPreview} from 'Components/Conversation/useFiles/useFiles'; +import {isAudio, isVideo, isImage} from 'src/script/assets/AssetMetaDataBuilder'; +import {CellsRepository} from 'src/script/cells/CellsRepository'; import {AudioPreviewCard} from './AudioPreviewCard/AudioPreviewCard'; import {FilePreviewCard} from './FilePreviewCard/FilePreviewCard'; import {wrapperStyles} from './FilePreviews.styles'; import {ImagePreviewCard} from './ImagePreviewCard/ImagePreviewCard'; +import {useFilePreview} from './useFilePreview/useFilePreview'; import {VideoPreviewCard} from './VideoPreviewCard/VideoPreviewCard'; interface FilePreviewsProps { @@ -36,10 +38,6 @@ interface FilePreviewsProps { export const FilePreviews = ({files}: FilePreviewsProps) => { const [wrapperRef] = useAutoAnimate(); - if (files.length === 0) { - return null; - } - return (
{files.map(file => ( @@ -49,28 +47,64 @@ export const FilePreviews = ({files}: FilePreviewsProps) => { ); }; -const FilePreview = ({file}: {file: FileWithPreview}) => { - const name = trimFileExtension(file.name); - const extension = getFileExtension(file.name); - const size = formatBytes(file.size); - - const {deleteFile} = useFileUploadState(); +interface FilePreviewProps { + file: FileWithPreview; + cellsRepository?: CellsRepository; +} - const handleDelete = () => { - deleteFile(file.id); - }; +const FilePreview = ({file, cellsRepository = container.resolve(CellsRepository)}: FilePreviewProps) => { + const {name, extension, size, isLoading, isError, handleDelete, handleRetry} = useFilePreview({ + file, + cellsRepository, + }); if (isImage(file)) { - return ; + return ( + + ); } if (isAudio(file)) { - return ; + return ( + + ); } if (isVideo(file)) { - return ; + return ( + + ); } - return ; + return ( + + ); }; diff --git a/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.styles.ts b/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.styles.ts index 41bd0c1e159..34bc5415298 100644 --- a/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.styles.ts +++ b/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.styles.ts @@ -33,3 +33,21 @@ export const imageStyles: CSSObject = { objectFit: 'cover', borderRadius: '10px', }; + +export const iconWrapperStyles: CSSObject = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '32px', + height: '32px', + backgroundColor: 'var(--white)', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +export const alertIconStyles: CSSObject = { + fill: 'var(--danger-color)', +}; diff --git a/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.tsx b/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.tsx index fecc7ba2305..5b40390bc5a 100644 --- a/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.tsx +++ b/src/script/components/InputBar/FilePreviews/ImagePreviewCard/ImagePreviewCard.tsx @@ -17,17 +17,25 @@ * */ +import {AlertIcon} from '@wireapp/react-ui-kit'; + import {t} from 'Util/LocalizerUtil'; -import {imageStyles, wrapperStyles} from './ImagePreviewCard.styles'; +import {alertIconStyles, iconWrapperStyles, imageStyles, wrapperStyles} from './ImagePreviewCard.styles'; import {FilePreviewDeleteButton} from '../common/FilePreviewDeleteButton/FilePreviewDeleteButton'; +import {FilePreviewErrorMoreButton} from '../common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton'; +import {FilePreviewSpinner} from '../common/FilePreviewSpinner/FilePreviewSpinner'; + interface ImagePreviewCardProps { src: string; onDelete: () => void; + onRetry: () => void; + isLoading: boolean; + isError: boolean; } -export const ImagePreviewCard = ({src, onDelete}: ImagePreviewCardProps) => { +export const ImagePreviewCard = ({src, onDelete, onRetry, isLoading, isError}: ImagePreviewCardProps) => { return (
{ URL.revokeObjectURL(src); }} /> - + {isLoading && ( +
+ +
+ )} + {isError && ( + <> +
+ +
+ + + )} + {!isLoading && !isError && }
); }; diff --git a/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.styles.ts b/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.styles.ts index 34a22185f90..910c0137b15 100644 --- a/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.styles.ts +++ b/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.styles.ts @@ -40,3 +40,21 @@ export const controlStyles: CSSObject = { left: '50%', transform: 'translate(-50%, -50%)', }; + +export const iconWrapperStyles: CSSObject = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '32px', + height: '32px', + backgroundColor: 'var(--white)', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +export const alertIconStyles: CSSObject = { + fill: 'var(--danger-color)', +}; diff --git a/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.tsx b/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.tsx index 6b63efad541..a5aff26662e 100644 --- a/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.tsx +++ b/src/script/components/InputBar/FilePreviews/VideoPreviewCard/VideoPreviewCard.tsx @@ -17,25 +17,46 @@ * */ +import {AlertIcon} from '@wireapp/react-ui-kit'; + import {t} from 'Util/LocalizerUtil'; -import {controlStyles, imageStyles, wrapperStyles} from './VideoPreviewCard.styles'; +import {controlStyles, imageStyles, iconWrapperStyles, wrapperStyles, alertIconStyles} from './VideoPreviewCard.styles'; import {FilePreviewDeleteButton} from '../common/FilePreviewDeleteButton/FilePreviewDeleteButton'; +import {FilePreviewErrorMoreButton} from '../common/FilePreviewErrorMoreButton/FilePreviewErrorMoreButton'; import {FilePreviewPlayButton} from '../common/FilePreviewPlayButton/FilePreviewPlayButton'; +import {FilePreviewSpinner} from '../common/FilePreviewSpinner/FilePreviewSpinner'; + interface VideoPreviewCardProps { src: string; onDelete: () => void; + onRetry: () => void; + isLoading: boolean; + isError: boolean; } -export const VideoPreviewCard = ({src, onDelete}: VideoPreviewCardProps) => { +export const VideoPreviewCard = ({src, onDelete, onRetry, isLoading, isError}: VideoPreviewCardProps) => { return (
-
); }; 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"