diff --git a/src/hooks/sync-sites/use-sync-push.ts b/src/hooks/sync-sites/use-sync-push.ts index 6552b0afe3..3b56df0c0a 100644 --- a/src/hooks/sync-sites/use-sync-push.ts +++ b/src/hooks/sync-sites/use-sync-push.ts @@ -27,6 +27,7 @@ export type SyncPushState = { status: PushStateProgressInfo; selectedSite: SiteDetails; remoteSiteUrl: string; + uploadProgress?: number; }; type PushSiteOptions = { @@ -99,11 +100,13 @@ export function useSyncPush( { const { pushStatesProgressInfo, isKeyPushing, + isKeyUploading, isKeyImporting, isKeyFinished, isKeyFailed, isKeyCancelled, getPushStatusWithProgress, + mapUploadProgressToOverallProgress, } = useSyncStatesProgressInfo(); const updatePushState = useCallback< UpdateState< SyncPushState > >( @@ -324,6 +327,7 @@ export function useSyncPush( { if ( response.success ) { updatePushState( selectedSite.id, remoteSiteId, { status: pushStatesProgressInfo.creatingRemoteBackup, + uploadProgress: undefined, // Clear upload progress when transitioning to next state } ); } else { throw response; @@ -392,12 +396,32 @@ export function useSyncPush( { useIpcListener( 'sync-upload-resumed', ( _event, payload: { selectedSiteId: string; remoteSiteId: number } ) => { + const currentState = getPushState( payload.selectedSiteId, payload.remoteSiteId ); updatePushState( payload.selectedSiteId, payload.remoteSiteId, { status: pushStatesProgressInfo.uploading, + uploadProgress: currentState?.uploadProgress, } ); } ); + useIpcListener( + 'sync-upload-progress', + ( _event, payload: { selectedSiteId: string; remoteSiteId: number; progress: number } ) => { + const currentState = getPushState( payload.selectedSiteId, payload.remoteSiteId ); + if ( currentState && isKeyUploading( currentState.status.key ) ) { + const mappedProgress = mapUploadProgressToOverallProgress( payload.progress ); + + updatePushState( payload.selectedSiteId, payload.remoteSiteId, { + status: { + ...currentState.status, + progress: mappedProgress, + }, + uploadProgress: payload.progress, + } ); + } + } + ); + const isAnySitePushing = useMemo< boolean >( () => { return Object.values( pushStates ).some( ( state ) => isKeyPushing( state.status.key ) ); }, [ pushStates, isKeyPushing ] ); diff --git a/src/hooks/use-sync-states-progress-info.ts b/src/hooks/use-sync-states-progress-info.ts index 4864b40b0c..313cec11a4 100644 --- a/src/hooks/use-sync-states-progress-info.ts +++ b/src/hooks/use-sync-states-progress-info.ts @@ -1,3 +1,4 @@ +import { sprintf } from '@wordpress/i18n'; import { useI18n } from '@wordpress/react-i18n'; import { useCallback, useMemo } from 'react'; import { ImportProgressState } from './use-import-export'; @@ -56,6 +57,80 @@ const DOWNLOADING_INITIAL_VALUE = 60; const IN_PROGRESS_TO_DOWNLOADING_STEP = DOWNLOADING_INITIAL_VALUE - IN_PROGRESS_INITIAL_VALUE; const PULL_IMPORTING_INITIAL_VALUE = 80; +function isKeyPulling( key: PullStateProgressInfo[ 'key' ] | undefined ): boolean { + const pullingStateKeys: PullStateProgressInfo[ 'key' ][] = [ + 'in-progress', + 'downloading', + 'importing', + ]; + if ( ! key ) { + return false; + } + return pullingStateKeys.includes( key ); +} + +function isKeyPushing( key: PushStateProgressInfo[ 'key' ] | undefined ): boolean { + const pushingStateKeys: PushStateProgressInfo[ 'key' ][] = [ + 'creatingBackup', + 'uploading', + 'creatingRemoteBackup', + 'applyingChanges', + 'finishing', + ]; + if ( ! key ) { + return false; + } + return pushingStateKeys.includes( key ); +} + +function isKeyUploadingPaused( key: PushStateProgressInfo[ 'key' ] | undefined ): boolean { + return key === 'uploadingPaused'; +} + +function isKeyUploading( key: PushStateProgressInfo[ 'key' ] | undefined ): boolean { + return key === 'uploading'; +} + +function isKeyImporting( key: PushStateProgressInfo[ 'key' ] | undefined ): boolean { + const pushingStateKeys: PushStateProgressInfo[ 'key' ][] = [ + 'creatingRemoteBackup', + 'applyingChanges', + 'finishing', + ]; + if ( ! key ) { + return false; + } + return pushingStateKeys.includes( key ); +} + +function isKeyFinished( + key: PullStateProgressInfo[ 'key' ] | PushStateProgressInfo[ 'key' ] | undefined +): boolean { + return key === 'finished'; +} + +function isKeyFailed( + key: PullStateProgressInfo[ 'key' ] | PushStateProgressInfo[ 'key' ] | undefined +): boolean { + return key === 'failed'; +} + +function isKeyCancelled( + key: PullStateProgressInfo[ 'key' ] | PushStateProgressInfo[ 'key' ] | undefined +): boolean { + return key === 'cancelled'; +} + +function getPushUploadPercentage( + statusKey: PushStateProgressInfo[ 'key' ] | undefined, + uploadProgress: number | undefined +): number | null { + if ( isKeyUploading( statusKey ) && uploadProgress !== undefined ) { + return Math.round( uploadProgress ); + } + return null; +} + export function useSyncStatesProgressInfo() { const { __ } = useI18n(); const pullStatesProgressInfo = useMemo( () => { @@ -104,7 +179,7 @@ export function useSyncStatesProgressInfo() { uploading: { key: 'uploading', progress: 40, - message: __( 'Uploading Studio site…' ), + message: __( 'Uploading site…' ), }, uploadingPaused: { key: 'uploadingPaused', @@ -144,67 +219,7 @@ export function useSyncStatesProgressInfo() { } as const satisfies PushStateProgressInfoValues; }, [ __ ] ); - const isKeyPulling = ( key: PullStateProgressInfo[ 'key' ] | undefined ) => { - const pullingStateKeys: PullStateProgressInfo[ 'key' ][] = [ - 'in-progress', - 'downloading', - 'importing', - ]; - if ( ! key ) { - return false; - } - return pullingStateKeys.includes( key ); - }; - - const isKeyPushing = ( key: PushStateProgressInfo[ 'key' ] | undefined ) => { - const pushingStateKeys: PushStateProgressInfo[ 'key' ][] = [ - 'creatingBackup', - 'uploading', - 'creatingRemoteBackup', - 'applyingChanges', - 'finishing', - ]; - if ( ! key ) { - return false; - } - return pushingStateKeys.includes( key ); - }; - - const isKeyUploadingPaused = ( key: PushStateProgressInfo[ 'key' ] | undefined ) => { - return key === 'uploadingPaused'; - }; - - const isKeyImporting = ( key: PushStateProgressInfo[ 'key' ] | undefined ) => { - const pushingStateKeys: PushStateProgressInfo[ 'key' ][] = [ - 'creatingRemoteBackup', - 'applyingChanges', - 'finishing', - ]; - if ( ! key ) { - return false; - } - return pushingStateKeys.includes( key ); - }; - const isKeyFinished = useCallback( - ( key: PullStateProgressInfo[ 'key' ] | PushStateProgressInfo[ 'key' ] | undefined ) => { - return key === 'finished'; - }, - [] - ); - - const isKeyFailed = useCallback( - ( key: PullStateProgressInfo[ 'key' ] | PushStateProgressInfo[ 'key' ] | undefined ) => { - return key === 'failed'; - }, - [] - ); - - const isKeyCancelled = useCallback( - ( key: PullStateProgressInfo[ 'key' ] | PushStateProgressInfo[ 'key' ] | undefined ) => { - return key === 'cancelled'; - }, - [] - ); + const uploadingProgressMessageTemplate = useMemo( () => __( 'Uploading site (%d%%)…' ), [ __ ] ); const getBackupStatusWithProgress = useCallback( ( @@ -294,6 +309,34 @@ export function useSyncStatesProgressInfo() { ] ); + const getPushUploadMessage = useCallback( + ( message: string, uploadPercentage: number | null ): string => { + if ( uploadPercentage !== null ) { + // translators: %d is the upload progress percentage + return sprintf( uploadingProgressMessageTemplate, uploadPercentage ); + } + return message; + }, + [ uploadingProgressMessageTemplate ] + ); + + const mapUploadProgressToOverallProgress = useCallback( + ( uploadProgress: number ): number => { + // Map upload progress (0-100%) to the uploading state range (40-50%) + const uploadingProgressRange = + pushStatesProgressInfo.creatingRemoteBackup.progress - + pushStatesProgressInfo.uploading.progress; + return ( + pushStatesProgressInfo.uploading.progress + + ( uploadProgress / 100 ) * uploadingProgressRange + ); + }, + [ + pushStatesProgressInfo.creatingRemoteBackup.progress, + pushStatesProgressInfo.uploading.progress, + ] + ); + return { pullStatesProgressInfo, pushStatesProgressInfo, @@ -303,9 +346,13 @@ export function useSyncStatesProgressInfo() { isKeyFinished, isKeyFailed, isKeyCancelled, + isKeyUploading, getBackupStatusWithProgress, getPullStatusWithProgress, getPushStatusWithProgress, + getPushUploadPercentage, + getPushUploadMessage, + mapUploadProgressToOverallProgress, isKeyUploadingPaused, }; } diff --git a/src/ipc-utils.ts b/src/ipc-utils.ts index 9893ac9934..e5e8de413b 100644 --- a/src/ipc-utils.ts +++ b/src/ipc-utils.ts @@ -40,6 +40,7 @@ export interface IpcEvents { 'site-context-menu-action': [ { action: string; siteId: string } ]; 'sync-upload-paused': [ { error: string; selectedSiteId: string; remoteSiteId: number } ]; 'sync-upload-resumed': [ { selectedSiteId: string; remoteSiteId: number } ]; + 'sync-upload-progress': [ { selectedSiteId: string; remoteSiteId: number; progress: number } ]; 'snapshot-error': [ { operationId: crypto.UUID; data: SnapshotEventData } ]; 'snapshot-fatal-error': [ { operationId: crypto.UUID; data: { message: string } } ]; 'snapshot-output': [ { operationId: crypto.UUID; data: SnapshotEventData } ]; diff --git a/src/modules/sync/components/sync-connected-sites.tsx b/src/modules/sync/components/sync-connected-sites.tsx index 2a6a2f48aa..40af65606a 100644 --- a/src/modules/sync/components/sync-connected-sites.tsx +++ b/src/modules/sync/components/sync-connected-sites.tsx @@ -199,6 +199,8 @@ const SyncConnectedSitesSectionItem = ( { isKeyFailed, isKeyCancelled, getPullStatusWithProgress, + getPushUploadPercentage, + getPushUploadMessage, isKeyUploadingPaused, } = useSyncStatesProgressInfo(); @@ -217,6 +219,11 @@ const SyncConnectedSitesSectionItem = ( { const hasPushFinished = pushState && isKeyFinished( pushState.status.key ); const hasPushCancelled = pushState && isKeyCancelled( pushState.status.key ); + const uploadPercentage = getPushUploadPercentage( + pushState?.status.key, + pushState?.uploadProgress + ); + return (
- { pushState.status.message } + { getPushUploadMessage( pushState.status.message, uploadPercentage ) }
diff --git a/src/modules/sync/lib/ipc-handlers.ts b/src/modules/sync/lib/ipc-handlers.ts index d38a936187..f30c5fb90a 100644 --- a/src/modules/sync/lib/ipc-handlers.ts +++ b/src/modules/sync/lib/ipc-handlers.ts @@ -194,7 +194,7 @@ export async function pushArchive( console.error( '[TUS] Upload error', error ); reject( error ); }, - onProgress: () => { + onProgress: ( bytesSent: number, bytesTotal: number ) => { if ( isUploadingPaused ) { isUploadingPaused = false; void sendIpcEventToRenderer( 'sync-upload-resumed', { @@ -207,6 +207,14 @@ export async function pushArchive( if ( ! hasUploadStarted ) { hasUploadStarted = true; } + + // Calculate upload progress percentage (0-100) + const uploadProgress = bytesTotal > 0 ? ( bytesSent / bytesTotal ) * 100 : 0; + void sendIpcEventToRenderer( 'sync-upload-progress', { + selectedSiteId: selectedSiteId, + remoteSiteId: remoteSiteId, + progress: uploadProgress, + } ); }, onSuccess: ( payload ) => { if ( ! payload.lastResponse ) {