Skip to content

Commit 32b9c67

Browse files
authored
Social: Default per-network mode from connection templates (#48721)
* Social: default per-network mode from connection templates * Social: Respect saved per-network choice
1 parent 9c0dfce commit 32b9c67

5 files changed

Lines changed: 233 additions & 25 deletions

File tree

projects/packages/publicize/_inc/hooks/use-per-network-customization/index.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { siteHasFeature } from '@automattic/jetpack-script-data';
22
import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
33
import { useDispatch, useSelect } from '@wordpress/data';
44
import { store as editorStore } from '@wordpress/editor';
5-
import { useCallback, useMemo } from '@wordpress/element';
5+
import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
66
import { store as socialStore } from '../../social-store';
77
import { CUSTOMIZE_PER_NETWORK_KEY } from '../../social-store/constants';
88
import { features, hasSocialPaidFeatures } from '../../utils';
@@ -23,24 +23,53 @@ export function usePerNetworkCustomization() {
2323
const { editPost } = useDispatch( editorStore );
2424
const { customizeConnectionById } = useDispatch( socialStore );
2525
const connections = useSelect( select => select( socialStore ).getConnections(), [] );
26+
const [ templateDefaultDisabled, setTemplateDefaultDisabled ] = useState( false );
2627

2728
// Get featured image details for syncing to connections
2829
const featuredImageId = useFeaturedImage();
2930
const [ featuredImageDetails ] = useMediaDetails( featuredImageId );
3031
const featuredImageUrl = featuredImageDetails?.mediaData?.sourceUrl;
3132
const featuredImageMime = featuredImageDetails?.metaData?.mime ?? 'image/jpeg';
33+
const templatesEnabled = siteHasFeature( features.MESSAGE_TEMPLATES );
34+
const hasConnectionTemplate = connections.some(
35+
connection => typeof connection.template === 'string' && connection.template.trim() !== ''
36+
);
37+
const hasUserSetCustomizePerNetwork = Boolean(
38+
postMeta.jetpackSocialOptions.customize_per_network_user_set
39+
);
3240

33-
const isEnabled = useSelect( select => {
41+
const isMetaEnabled = useSelect( select => {
3442
const meta = select( editorStore ).getEditedPostAttribute( 'meta' );
3543

3644
return Boolean( meta?.[ CUSTOMIZE_PER_NETWORK_KEY ] );
3745
}, [] );
46+
const shouldUseTemplateDefault =
47+
templatesEnabled &&
48+
hasConnectionTemplate &&
49+
! isMetaEnabled &&
50+
! hasUserSetCustomizePerNetwork &&
51+
! templateDefaultDisabled;
52+
const isEnabled = hasUserSetCustomizePerNetwork
53+
? isMetaEnabled
54+
: isMetaEnabled || shouldUseTemplateDefault;
55+
56+
useEffect( () => {
57+
if ( ! shouldUseTemplateDefault || ! hasSocialPaidFeatures() ) {
58+
return;
59+
}
60+
61+
editPost( {
62+
meta: {
63+
[ CUSTOMIZE_PER_NETWORK_KEY ]: true,
64+
},
65+
} );
66+
}, [ shouldUseTemplateDefault, editPost ] );
3867

3968
const syncConnections = useCallback( () => {
4069
/*
4170
* Don't sync when the message-templates feature is on. Server-side defaults
4271
*/
43-
if ( siteHasFeature( features.MESSAGE_TEMPLATES ) ) {
72+
if ( templatesEnabled ) {
4473
return;
4574
}
4675

@@ -72,6 +101,7 @@ export function usePerNetworkCustomization() {
72101
featuredImageId,
73102
featuredImageUrl,
74103
featuredImageMime,
104+
templatesEnabled,
75105
] );
76106

77107
const toggle = useCallback( () => {
@@ -81,17 +111,32 @@ export function usePerNetworkCustomization() {
81111
enabled: isNowEnabled,
82112
} );
83113

114+
setTemplateDefaultDisabled( ! isNowEnabled && templatesEnabled && hasConnectionTemplate );
115+
84116
// Update post metadata.
85117
editPost( {
86118
meta: {
87119
[ CUSTOMIZE_PER_NETWORK_KEY ]: isNowEnabled,
120+
jetpack_social_options: {
121+
...postMeta.jetpackSocialOptions,
122+
customize_per_network_user_set: true,
123+
version: 2,
124+
},
88125
},
89126
} );
90127

91128
if ( isNowEnabled ) {
92129
syncConnections();
93130
}
94-
}, [ isEnabled, recordEvent, editPost, syncConnections ] );
131+
}, [
132+
isEnabled,
133+
recordEvent,
134+
templatesEnabled,
135+
hasConnectionTemplate,
136+
editPost,
137+
postMeta.jetpackSocialOptions,
138+
syncConnections,
139+
] );
95140

96141
return useMemo(
97142
() => ( {

projects/packages/publicize/_inc/hooks/use-per-network-customization/test/index.test.ts

Lines changed: 172 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import { siteHasFeature } from '@automattic/jetpack-script-data';
12
import { act, renderHook } from '@testing-library/react';
23
import { useDispatch, useSelect } from '@wordpress/data';
34
import { usePerNetworkCustomization } from '../';
45

6+
jest.mock( '@automattic/jetpack-script-data', () => {
7+
const actual = jest.requireActual( '@automattic/jetpack-script-data' );
8+
return {
9+
...actual,
10+
siteHasFeature: jest.fn(),
11+
};
12+
} );
13+
514
jest.mock( '@wordpress/data', () => {
615
const actual = jest.requireActual( '@wordpress/data' );
716
const mocks = {
@@ -21,37 +30,29 @@ jest.mock( '../../use-featured-image', () => jest.fn( () => null ) );
2130
// Mock useMediaDetails to avoid nested useSelect calls
2231
jest.mock( '../../use-media-details', () => jest.fn( () => [ null ] ) );
2332

33+
const mockUsePostMeta = jest.fn();
34+
2435
// Mock usePostMeta to avoid nested hook calls
2536
jest.mock( '../../use-post-meta', () => ( {
26-
usePostMeta: jest.fn( () => ( {
27-
attachedMedia: [],
28-
imageGeneratorSettings: { enabled: false },
29-
mediaSource: undefined,
30-
shareMessage: '',
31-
} ) ),
37+
usePostMeta: () => mockUsePostMeta(),
3238
} ) );
3339

3440
// Mock useAnalytics to avoid deep dependency chain
3541
jest.mock( '@automattic/jetpack-shared-extension-utils', () => ( {
3642
useAnalytics: jest.fn( () => ( { recordEvent: jest.fn() } ) ),
3743
} ) );
3844

39-
// Mock hasSocialPaidFeatures to return true by default
40-
jest.mock( '../../../utils', () => {
41-
const actual = jest.requireActual( '../../../utils' );
42-
return {
43-
...actual,
44-
hasSocialPaidFeatures: jest.fn( () => true ),
45-
};
46-
} );
47-
4845
const mockUseDispatch = useDispatch as jest.Mock;
4946
const mockUseSelect = useSelect as jest.Mock;
47+
const mockSiteHasFeature = siteHasFeature as jest.Mock;
5048

51-
const createMockSelect = ( meta: Record< string, unknown > = {} ) => {
49+
const createMockSelect = (
50+
meta: Record< string, unknown > = {},
51+
connections: Array< Record< string, unknown > > = []
52+
) => {
5253
return () => ( {
5354
getEditedPostAttribute: jest.fn().mockReturnValue( meta ),
54-
getConnections: jest.fn().mockReturnValue( [] ),
55+
getConnections: jest.fn().mockReturnValue( connections ),
5556
getEnabledConnections: jest.fn().mockReturnValue( [] ),
5657
getDisabledConnections: jest.fn().mockReturnValue( [] ),
5758
} );
@@ -68,6 +69,18 @@ describe( 'usePerNetworkCustomization', () => {
6869
editPost: mockEditPost,
6970
customizeConnectionById: mockCustomizeConnectionById,
7071
} );
72+
73+
mockUsePostMeta.mockReturnValue( {
74+
attachedMedia: [],
75+
imageGeneratorSettings: { enabled: false },
76+
jetpackSocialOptions: {},
77+
mediaSource: undefined,
78+
shareMessage: '',
79+
} );
80+
81+
mockSiteHasFeature.mockImplementation( feature =>
82+
[ 'social-message-templates', 'social-enhanced-publishing' ].includes( feature )
83+
);
7184
} );
7285

7386
it( 'should return isEnabled as false when meta key is not set', () => {
@@ -78,6 +91,7 @@ describe( 'usePerNetworkCustomization', () => {
7891
const { result } = renderHook( () => usePerNetworkCustomization() );
7992

8093
expect( result.current.isEnabled ).toBe( false );
94+
expect( mockEditPost ).not.toHaveBeenCalled();
8195
} );
8296

8397
it( 'should return isEnabled as true when meta key is true', () => {
@@ -92,6 +106,143 @@ describe( 'usePerNetworkCustomization', () => {
92106
const { result } = renderHook( () => usePerNetworkCustomization() );
93107

94108
expect( result.current.isEnabled ).toBe( true );
109+
expect( mockEditPost ).not.toHaveBeenCalled();
110+
} );
111+
112+
it( 'should default to enabled when message templates are enabled and a connection has a custom template', () => {
113+
mockUseSelect.mockImplementation( ( selector: ( select: unknown ) => unknown ) => {
114+
return selector(
115+
createMockSelect(
116+
{
117+
_wpas_customize_per_network: false,
118+
},
119+
[
120+
{
121+
connection_id: 'connection-1',
122+
template: 'Custom template',
123+
},
124+
]
125+
)
126+
);
127+
} );
128+
129+
const { result } = renderHook( () => usePerNetworkCustomization() );
130+
131+
expect( result.current.isEnabled ).toBe( true );
132+
expect( mockEditPost ).toHaveBeenCalledWith( {
133+
meta: {
134+
_wpas_customize_per_network: true,
135+
},
136+
} );
137+
} );
138+
139+
it( 'should respect an explicitly saved global mode when a connection has a custom template', () => {
140+
mockUsePostMeta.mockReturnValue( {
141+
attachedMedia: [],
142+
imageGeneratorSettings: { enabled: false },
143+
jetpackSocialOptions: {
144+
customize_per_network_user_set: true,
145+
},
146+
mediaSource: undefined,
147+
shareMessage: '',
148+
} );
149+
mockUseSelect.mockImplementation( ( selector: ( select: unknown ) => unknown ) => {
150+
return selector(
151+
createMockSelect(
152+
{
153+
_wpas_customize_per_network: false,
154+
},
155+
[
156+
{
157+
connection_id: 'connection-1',
158+
template: 'Custom template',
159+
},
160+
]
161+
)
162+
);
163+
} );
164+
165+
const { result } = renderHook( () => usePerNetworkCustomization() );
166+
167+
expect( result.current.isEnabled ).toBe( false );
168+
expect( mockEditPost ).not.toHaveBeenCalled();
169+
} );
170+
171+
it( 'should not default to enabled when the connection template is blank', () => {
172+
mockUseSelect.mockImplementation( ( selector: ( select: unknown ) => unknown ) => {
173+
return selector(
174+
createMockSelect( {}, [
175+
{
176+
connection_id: 'connection-1',
177+
template: ' ',
178+
},
179+
] )
180+
);
181+
} );
182+
183+
const { result } = renderHook( () => usePerNetworkCustomization() );
184+
185+
expect( result.current.isEnabled ).toBe( false );
186+
expect( mockEditPost ).not.toHaveBeenCalled();
187+
} );
188+
189+
it( 'should not default to enabled when message templates are disabled', () => {
190+
mockSiteHasFeature.mockReturnValue( false );
191+
mockUseSelect.mockImplementation( ( selector: ( select: unknown ) => unknown ) => {
192+
return selector(
193+
createMockSelect( {}, [
194+
{
195+
connection_id: 'connection-1',
196+
template: 'Custom template',
197+
},
198+
] )
199+
);
200+
} );
201+
202+
const { result } = renderHook( () => usePerNetworkCustomization() );
203+
204+
expect( result.current.isEnabled ).toBe( false );
205+
expect( mockEditPost ).not.toHaveBeenCalled();
206+
} );
207+
208+
it( 'should allow turning off the template-based default for the current editor session', () => {
209+
let meta = {};
210+
const connections = [
211+
{
212+
connection_id: 'connection-1',
213+
template: 'Custom template',
214+
},
215+
];
216+
217+
mockEditPost.mockImplementation( ( update: { meta: Record< string, unknown > } ) => {
218+
meta = {
219+
...meta,
220+
...update.meta,
221+
};
222+
} );
223+
mockUseSelect.mockImplementation( ( selector: ( select: unknown ) => unknown ) => {
224+
return selector( createMockSelect( meta, connections ) );
225+
} );
226+
227+
const { result, rerender } = renderHook( () => usePerNetworkCustomization() );
228+
229+
expect( result.current.isEnabled ).toBe( true );
230+
231+
act( () => {
232+
result.current.toggle();
233+
} );
234+
rerender();
235+
236+
expect( result.current.isEnabled ).toBe( false );
237+
expect( mockEditPost ).toHaveBeenLastCalledWith( {
238+
meta: {
239+
_wpas_customize_per_network: false,
240+
jetpack_social_options: {
241+
customize_per_network_user_set: true,
242+
version: 2,
243+
},
244+
},
245+
} );
95246
} );
96247

97248
it( 'should toggle the meta value when toggle is called', () => {
@@ -112,6 +263,10 @@ describe( 'usePerNetworkCustomization', () => {
112263
expect( mockEditPost ).toHaveBeenCalledWith( {
113264
meta: {
114265
_wpas_customize_per_network: true,
266+
jetpack_social_options: {
267+
customize_per_network_user_set: true,
268+
version: 2,
269+
},
115270
},
116271
} );
117272
} );

projects/packages/publicize/_inc/utils/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type MediaSourceValue = 'featured-image' | 'sig' | 'media-library' | 'upl
1818

1919
export type JetpackSocialOptions = {
2020
attached_media?: Array< AttachedMedia >;
21+
customize_per_network_user_set?: boolean;
2122
image_generator_settings?: SIGSettings;
2223
media_source?: MediaSourceValue;
2324
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Social: Default per-network customization to enabled only when a custom connection template exists.

projects/packages/publicize/src/class-publicize-base.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,10 +1231,10 @@ public function register_post_meta() {
12311231
'schema' => array(
12321232
'type' => 'object',
12331233
'properties' => array(
1234-
'version' => array(
1234+
'version' => array(
12351235
'type' => 'number',
12361236
),
1237-
'attached_media' => array(
1237+
'attached_media' => array(
12381238
'type' => 'array',
12391239
'items' => array(
12401240
'type' => 'object',
@@ -1251,7 +1251,10 @@ public function register_post_meta() {
12511251
),
12521252
),
12531253
),
1254-
'image_generator_settings' => array(
1254+
'customize_per_network_user_set' => array(
1255+
'type' => 'boolean',
1256+
),
1257+
'image_generator_settings' => array(
12551258
'type' => 'object',
12561259
'properties' => array(
12571260
'enabled' => array(
@@ -1280,7 +1283,7 @@ public function register_post_meta() {
12801283
),
12811284
),
12821285
),
1283-
'media_source' => array(
1286+
'media_source' => array(
12841287
'type' => 'string',
12851288
'enum' => array( 'featured-image', 'sig', 'media-library', 'upload-video', 'none' ),
12861289
),

0 commit comments

Comments
 (0)