Skip to content

Commit 7203b33

Browse files
katinthehatsiteKateryna Kodonenko
andauthored
Experiment to speed up deeplink connection from WP.com (#2375)
* Proof of concept * Fetch for one site * Clean up placeholder * Cleanup * Cleanup * Fix implementation * Cleanup and remove unused query * Remove siteName and siteUrl * Reuse and extract fields * Move unsubscribe call * Create Tailwind utility class for skeleton animation * Update refetch logic * Fix issue with the site switching * Use set * Fix linter --------- Co-authored-by: Kateryna Kodonenko <kateryna@automattic.com>
1 parent 6101db6 commit 7203b33

10 files changed

Lines changed: 358 additions & 165 deletions

File tree

‎apps/studio/src/components/content-tab-overview.tsx‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,11 @@ interface ContentTabOverviewProps {
3030
selectedSite: SiteDetails;
3131
}
3232

33-
const skeletonBg = 'animate-pulse bg-gradient-to-r from-[#F6F7F7] via-[#DCDCDE] to-[#F6F7F7]';
34-
3533
const ButtonSectionSkeleton = ( { title }: { title: string } ) => {
3634
return (
3735
<div className="w-full max-w-96">
3836
<h2 className="a8c-subtitle-small mb-3">{ title }</h2>
39-
<div className={ `w-full h-20 my-1 ${ skeletonBg }` }></div>
37+
<div className="w-full h-20 my-1 skeleton-bg"></div>
4038
</div>
4139
);
4240
};
@@ -222,7 +220,7 @@ export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps )
222220
<div
223221
className={ cx(
224222
'w-full min-h-40 max-h-64 rounded-sm border border-a8c-gray-5 bg-a8c-gray-0 mb-2 flex justify-center',
225-
loading && `h-64 ${ skeletonBg }`,
223+
loading && 'h-64 skeleton-bg',
226224
isThumbnailError && 'border-none',
227225
! loading && 'hover:border-a8c-blue-50 duration-300'
228226
) }
@@ -259,7 +257,7 @@ export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps )
259257
) }
260258
</div>
261259
<div className="flex justify-between items-center w-full">
262-
{ loading && <div className={ `w-[100px] min-h-4 ${ skeletonBg }` }></div> }
260+
{ loading && <div className="w-[100px] min-h-4 skeleton-bg"></div> }
263261
{ ! loading && ! isThumbnailError && <p>{ themeDetails?.name }</p> }
264262
</div>
265263
</div>

‎apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts‎

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { useAuth } from 'src/hooks/use-auth';
22
import { useContentTabs } from 'src/hooks/use-content-tabs';
33
import { useIpcListener } from 'src/hooks/use-ipc-listener';
44
import { useSiteDetails } from 'src/hooks/use-site-details';
5+
import { getIpcApi } from 'src/lib/get-ipc-api';
6+
import { SyncSite } from 'src/modules/sync/types';
57
import { useAppDispatch } from 'src/stores';
68
import {
79
connectedSitesActions,
10+
connectedSitesApi,
811
useConnectSiteMutation,
912
useGetConnectedSitesForLocalSiteQuery,
1013
} from 'src/stores/sync/connected-sites';
11-
import { useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites';
14+
import { wpcomSitesApi, useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites';
1215

1316
export function useListenDeepLinkConnection() {
1417
const dispatch = useAppDispatch();
@@ -28,26 +31,91 @@ export function useListenDeepLinkConnection() {
2831

2932
useIpcListener(
3033
'sync-connect-site',
31-
async ( _event, { remoteSiteId, studioSiteId, autoOpenPush } ) => {
32-
// Fetch latest sites from network before checking
33-
const result = await refetchWpComSites();
34-
const latestSites = result.data ?? [];
35-
const newConnectedSite = latestSites.find( ( site ) => site.id === remoteSiteId );
36-
if ( newConnectedSite ) {
37-
if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
38-
// Select studio site that started the sync
39-
setSelectedSiteId( studioSiteId );
40-
}
41-
await connectSite( { site: newConnectedSite, localSiteId: studioSiteId } );
42-
if ( selectedTab !== 'sync' ) {
43-
// Switch to sync tab
44-
setSelectedTab( 'sync' );
45-
}
46-
// Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button)
47-
if ( autoOpenPush ) {
48-
dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) );
34+
async (
35+
_event,
36+
{
37+
remoteSiteId,
38+
studioSiteId,
39+
autoOpenPush,
40+
}: {
41+
remoteSiteId: number;
42+
studioSiteId: string;
43+
autoOpenPush?: boolean;
44+
}
45+
) => {
46+
// Create minimal site object optimistically to connect immediately
47+
const minimalSite: SyncSite = {
48+
id: remoteSiteId,
49+
localSiteId: studioSiteId,
50+
name: '',
51+
url: '',
52+
isStaging: false,
53+
isPressable: false,
54+
environmentType: null,
55+
syncSupport: 'already-connected',
56+
lastPullTimestamp: null,
57+
lastPushTimestamp: null,
58+
};
59+
60+
// Switch to the site that initiated the connection if needed
61+
if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
62+
setSelectedSiteId( studioSiteId );
63+
}
64+
65+
// Switch to sync tab
66+
if ( selectedTab !== 'sync' ) {
67+
setSelectedTab( 'sync' );
68+
}
69+
70+
// Mark site as loading in ephemeral Redux state (not persisted to storage)
71+
dispatch( connectedSitesActions.addLoadingSiteId( remoteSiteId ) );
72+
73+
const connectPromise = connectSite( { site: minimalSite, localSiteId: studioSiteId } );
74+
75+
// Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button)
76+
if ( autoOpenPush ) {
77+
dispatch(
78+
connectedSitesActions.setSelectedRemoteSiteId( {
79+
remoteSiteId,
80+
localSiteId: studioSiteId,
81+
} )
82+
);
83+
}
84+
85+
const fetchSingleSitePromise = dispatch(
86+
wpcomSitesApi.endpoints.getSingleWpComSite.initiate( {
87+
siteId: remoteSiteId,
88+
userId: user?.id,
89+
} )
90+
);
91+
92+
// Wait for both operations to complete
93+
try {
94+
const [ , singleSiteResult ] = await Promise.all( [
95+
connectPromise,
96+
fetchSingleSitePromise,
97+
] );
98+
99+
if ( singleSiteResult.data ) {
100+
const fullSiteData: SyncSite = {
101+
...singleSiteResult.data,
102+
localSiteId: studioSiteId,
103+
syncSupport: 'already-connected',
104+
};
105+
await getIpcApi().updateSingleConnectedWpcomSite( fullSiteData );
106+
dispatch( connectedSitesApi.util.invalidateTags( [ 'ConnectedSites' ] ) );
49107
}
108+
} catch ( error ) {
109+
console.error( 'Error during site connection:', error );
110+
} finally {
111+
fetchSingleSitePromise.unsubscribe();
112+
dispatch( connectedSitesActions.removeLoadingSiteId( remoteSiteId ) );
50113
}
114+
115+
// Refetch all sites to update syncSites (used by the push/pull modal).
116+
// Fired after clearing the loading state to avoid stale closure issues
117+
// where the captured refetch references an outdated query subscription.
118+
void refetchWpComSites();
51119
}
52120
);
53121
}

‎apps/studio/src/index.css‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
.interpolate-size-allow-keywords {
88
interpolate-size: allow-keywords;
99
}
10+
11+
.skeleton-bg {
12+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
13+
background-image: linear-gradient(to right, #f6f7f7, #dcdcde, #f6f7f7);
14+
}
1015
}
1116

1217
body {

‎apps/studio/src/ipc-utils.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ export interface IpcEvents {
4343
'snapshot-key-value': [ { operationId: crypto.UUID; data: SnapshotKeyValueEventData } ];
4444
'snapshot-success': [ { operationId: crypto.UUID } ];
4545
'show-whats-new': [ void ];
46-
'sync-connect-site': [ { remoteSiteId: number; studioSiteId: string; autoOpenPush?: boolean } ];
46+
'sync-connect-site': [
47+
{
48+
remoteSiteId: number;
49+
studioSiteId: string;
50+
autoOpenPush?: boolean;
51+
},
52+
];
4753
'test-render-failure': [ void ];
4854
'theme-details-loading': [ { id: string } ];
4955
'theme-details-loaded': [ { id: string; details: StartedSiteDetails[ 'themeDetails' ] } ];

‎apps/studio/src/modules/sync/components/sync-connected-sites.tsx‎

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
} from 'src/stores/sync';
4444
import {
4545
connectedSitesActions,
46+
connectedSitesSelectors,
4647
useGetConnectedSitesForLocalSiteQuery,
4748
} from 'src/stores/sync/connected-sites';
4849
import type { SyncSite } from 'src/modules/sync/types';
@@ -214,6 +215,9 @@ const SyncConnectedSitesSectionItem = ( {
214215
const { __ } = useI18n();
215216
const dispatch = useAppDispatch();
216217
const isOffline = useOffline();
218+
const isSiteLoading = useRootSelector(
219+
connectedSitesSelectors.selectIsLoadingSiteId( connectedSite.id )
220+
);
217221
const getLastSyncTimeText = useLastSyncTimeText();
218222
const { importState, clearImportState } = useImportExport();
219223
const { getPushUploadPercentage, getPushUploadMessage } = useSyncStatesProgressInfo();
@@ -297,19 +301,30 @@ const SyncConnectedSitesSectionItem = ( {
297301
key={ connectedSite.id }
298302
>
299303
<div className="shrink-0">
300-
<EnvironmentBadge type={ getSiteEnvironment( connectedSite ) } />
304+
{ isSiteLoading ? (
305+
<div
306+
className="h-5 w-20 rounded skeleton-bg"
307+
aria-label={ __( 'Loading environment' ) }
308+
/>
309+
) : (
310+
<EnvironmentBadge type={ getSiteEnvironment( connectedSite ) } />
311+
) }
301312
</div>
302313

303-
<Button
304-
variant="link"
305-
className="!text-a8c-gray-70 hover:!text-a8c-blue-50 max-w-full overflow-hidden"
306-
onClick={ () => {
307-
getIpcApi().openURL( connectedSite.url );
308-
} }
309-
>
310-
<span className="truncate">{ connectedSite.url.replace( /^https?:\/\//, '' ) }</span>{ ' ' }
311-
<ArrowIcon />
312-
</Button>
314+
{ isSiteLoading ? (
315+
<div className="h-5 w-48 rounded skeleton-bg" aria-label={ __( 'Loading site URL' ) } />
316+
) : (
317+
<Button
318+
variant="link"
319+
className="!text-a8c-gray-70 hover:!text-a8c-blue-50 max-w-full overflow-hidden"
320+
onClick={ () => {
321+
getIpcApi().openURL( connectedSite.url );
322+
} }
323+
>
324+
<span className="truncate">{ connectedSite.url.replace( /^https?:\/\//, '' ) }</span>{ ' ' }
325+
<ArrowIcon />
326+
</Button>
327+
) }
313328

314329
<div className="flex shrink-0 justify-self-end justify-end items-center min-h-[26px] w-80">
315330
{ isPulling && (
@@ -643,6 +658,9 @@ const SyncConnectedSiteSection = ( {
643658
}
644659
};
645660

661+
const isSiteLoading = useRootSelector(
662+
connectedSitesSelectors.selectIsLoadingSiteId( connectedSite.id )
663+
);
646664
const hasConnectionErrors = connectedSite?.syncSupport !== 'already-connected';
647665
const isPulling = useRootSelector(
648666
syncOperationsSelectors.selectIsSiteIdPulling( selectedSite.id, connectedSite.id )
@@ -652,7 +670,9 @@ const SyncConnectedSiteSection = ( {
652670
);
653671

654672
let logo = <WordPressLogoCircle />;
655-
if ( hasConnectionErrors ) {
673+
if ( isSiteLoading ) {
674+
logo = <div className="w-5 h-5 rounded-full skeleton-bg" aria-label={ __( 'Loading' ) } />;
675+
} else if ( hasConnectionErrors ) {
656676
logo = <CircleRedCrossIcon />;
657677
} else if ( connectedSite.isPressable ) {
658678
logo = <PressableLogo />;
@@ -662,9 +682,13 @@ const SyncConnectedSiteSection = ( {
662682
<div key={ connectedSite.id } className="flex flex-col gap-2 border-b border-a8c-gray-0 py-5">
663683
<div className="flex items-center gap-2 ps-8 pe-5">
664684
{ logo }
665-
<div className={ cx( 'a8c-label-semibold', hasConnectionErrors && 'error-message' ) }>
666-
{ connectedSite.name }
667-
</div>
685+
{ isSiteLoading ? (
686+
<div className="h-5 w-40 rounded skeleton-bg" aria-label={ __( 'Loading site name' ) } />
687+
) : (
688+
<div className={ cx( 'a8c-label-semibold', hasConnectionErrors && 'error-message' ) }>
689+
{ connectedSite.name }
690+
</div>
691+
) }
668692
<div className="ms-auto">
669693
<Tooltip
670694
text={ __(

0 commit comments

Comments
 (0)