diff --git a/projects/js-packages/publicize-components/changelog/add-social-new-media-selector-ui b/projects/js-packages/publicize-components/changelog/add-social-new-media-selector-ui
new file mode 100644
index 0000000000000..a50d0ab3c66c5
--- /dev/null
+++ b/projects/js-packages/publicize-components/changelog/add-social-new-media-selector-ui
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Add the new media selection UI for Social
diff --git a/projects/js-packages/publicize-components/src/components/form/share-post-form.tsx b/projects/js-packages/publicize-components/src/components/form/share-post-form.tsx
index 5a05467bd5b2f..219abb58404a2 100644
--- a/projects/js-packages/publicize-components/src/components/form/share-post-form.tsx
+++ b/projects/js-packages/publicize-components/src/components/form/share-post-form.tsx
@@ -5,6 +5,7 @@ import useSocialMediaMessage from '../../hooks/use-social-media-message';
import { features } from '../../utils/constants';
import { useIsSocialNote } from '../../utils/use-is-social-note';
import MediaSection from '../media-section';
+import MediaSectionV2 from '../media-section-v2';
import MessageBoxControl from '../message-box-control';
import SocialImageGeneratorPanel from '../social-image-generator/panel';
import styles from './styles.module.scss';
@@ -41,15 +42,20 @@ export const SharePostForm: FC< SharePostFormProps > = ( { analyticsData = null
/>
) }
{ siteHasFeature( features.UNIFIED_UI_V1 ) ? (
-
HERE GOES THE NEW MEDIA SECTION
- ) : null }
- { siteHasFeature( features.ENHANCED_PUBLISHING ) && (
-
+
+ ) : (
+ <>
+ { siteHasFeature( features.ENHANCED_PUBLISHING ) && (
+
+
+
+ ) }
+ { /* Social Image Generator panel - only shown when not using unified UI */ }
+ { postCanUseSig && }
+ >
) }
- { /* Social Image Generator panel */ }
- { postCanUseSig && }
>
);
};
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/custom-media-toggle.tsx b/projects/js-packages/publicize-components/src/components/media-section-v2/custom-media-toggle.tsx
new file mode 100644
index 0000000000000..892f8120af2ed
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/custom-media-toggle.tsx
@@ -0,0 +1,65 @@
+/**
+ * CustomMediaToggle component
+ * Toggle for sharing image as attachment instead of link preview
+ */
+
+import { ToggleControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { getAttachmentDescription } from './media-source-menu';
+import styles from './styles.module.scss';
+import { MediaSourceType } from './types';
+
+interface CustomMediaToggleProps {
+ /**
+ * Current media source type
+ */
+ source: MediaSourceType;
+
+ /**
+ * Whether the toggle is checked (attached media is set)
+ */
+ checked: boolean;
+
+ /**
+ * Callback when toggle changes
+ */
+ onChange: ( checked: boolean ) => void;
+
+ /**
+ * Whether the toggle is disabled
+ */
+ disabled?: boolean;
+}
+
+/**
+ * Toggle for sharing image as attachment
+ *
+ * @param {CustomMediaToggleProps} props - Component props
+ * @return {JSX.Element|null} CustomMediaToggle component
+ */
+export default function CustomMediaToggle( {
+ source,
+ checked,
+ onChange,
+ disabled = false,
+}: CustomMediaToggleProps ) {
+ const description = getAttachmentDescription( source );
+
+ // Only show for sources that have an attachment description (featured-image, sig)
+ if ( ! description ) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/index.tsx b/projects/js-packages/publicize-components/src/components/media-section-v2/index.tsx
new file mode 100644
index 0000000000000..c0a22db7b2757
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/index.tsx
@@ -0,0 +1,332 @@
+/**
+ * MediaSectionV2 component
+ * Unified media selection interface for social posts
+ */
+
+import { ThemeProvider } from '@automattic/jetpack-components';
+import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
+import { MediaUpload } from '@wordpress/block-editor';
+import { BaseControl, Button, Notice } from '@wordpress/components';
+import { useCallback, useMemo, useRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import useFeaturedImage from '../../hooks/use-featured-image';
+import useImageGeneratorConfig from '../../hooks/use-image-generator-config';
+import useMediaDetails from '../../hooks/use-media-details';
+import { SELECTABLE_MEDIA_TYPES } from '../../hooks/use-media-restrictions/restrictions';
+import { usePostMeta } from '../../hooks/use-post-meta';
+import useSigPreview from '../../hooks/use-sig-preview';
+import CustomMediaToggle from './custom-media-toggle';
+import MediaPreview from './media-preview';
+import MediaSourceMenu, { getMediaSourceDescription } from './media-source-menu';
+import styles from './styles.module.scss';
+import { MediaSourceType, MediaSectionV2Props, MediaPreviewData, WPMediaObject } from './types';
+
+/**
+ * Detect the current media source based on existing data (for backward compatibility)
+ *
+ * @param {Array} attachedMedia - Attached media array
+ * @param {number} featuredImageId - Featured image ID
+ * @param {boolean} sigEnabled - Whether SIG is enabled
+ * @return {string|null} Current media source type
+ */
+function detectMediaSource(
+ attachedMedia: Array< { id: number; url: string; type: string } >,
+ featuredImageId: number | null,
+ sigEnabled: boolean
+): MediaSourceType {
+ // Priority 1: Attached media (uploaded content)
+ if ( attachedMedia && attachedMedia.length > 0 ) {
+ // Check if attached media is the featured image (shared as attachment)
+ if ( featuredImageId && attachedMedia[ 0 ].id === featuredImageId ) {
+ return 'featured-image';
+ }
+ // Check if it's SIG in attachment mode (id=0 with SIG enabled)
+ if ( sigEnabled && attachedMedia[ 0 ].id === 0 ) {
+ return 'sig';
+ }
+ return attachedMedia[ 0 ].type?.startsWith( 'video/' ) ? 'upload-video' : 'media-library';
+ }
+
+ // Priority 2: Social Image Generator
+ if ( sigEnabled ) {
+ return 'sig';
+ }
+
+ // Priority 3: Featured Image
+ if ( featuredImageId ) {
+ return 'featured-image';
+ }
+
+ // No media selected
+ return null;
+}
+
+/**
+ * MediaSectionV2 component
+ *
+ * @param {object} props - Component props
+ * @param {object} props.analyticsData - Analytics data
+ * @param {boolean} props.disabled - Whether the section is disabled
+ * @return {object} MediaSectionV2 component
+ */
+export default function MediaSectionV2( {
+ analyticsData = {},
+ disabled = false,
+}: MediaSectionV2Props ) {
+ const { recordEvent } = useAnalytics();
+ const featuredImageId = useFeaturedImage();
+ const { isEnabled: sigEnabled } = useImageGeneratorConfig();
+ const { attachedMedia, imageGeneratorSettings, mediaSource, updateJetpackSocialOptions } =
+ usePostMeta();
+
+ // Get SIG preview URL when SIG is enabled
+ const { url: sigPreviewUrl, isLoading: sigIsLoading } = useSigPreview( sigEnabled );
+
+ // Ref to store the MediaUpload open function
+ const openMediaLibraryRef = useRef< () => void >( () => {} );
+
+ // Determine current media source
+ // Priority 1: Explicit user choice (if media_source is set)
+ // Priority 2: Detect from existing data (backward compatibility)
+ const currentSource = useMemo( () => {
+ if ( mediaSource !== undefined ) {
+ return mediaSource === 'none' ? null : ( mediaSource as MediaSourceType );
+ }
+ return detectMediaSource( attachedMedia, featuredImageId, sigEnabled );
+ }, [ mediaSource, attachedMedia, featuredImageId, sigEnabled ] );
+
+ // Attachment mode: check if attached_media has items (matches backend is_social_post())
+ const isShareAsAttachment = attachedMedia?.length > 0;
+
+ // Get media ID for preview
+ const mediaId = useMemo( () => {
+ if ( currentSource === 'featured-image' ) {
+ return featuredImageId;
+ }
+ if ( currentSource === 'media-library' || currentSource === 'upload-video' ) {
+ return attachedMedia?.[ 0 ]?.id;
+ }
+ return null;
+ }, [ currentSource, featuredImageId, attachedMedia ] );
+
+ const [ mediaDetails ] = useMediaDetails( mediaId );
+
+ const previewData: MediaPreviewData | null = useMemo( () => {
+ // Use SIG preview URL when SIG is selected
+ // Always return an object (even with empty URL) so the loading spinner can show
+ if ( currentSource === 'sig' ) {
+ return {
+ id: 0,
+ url: sigPreviewUrl || '',
+ type: 'image',
+ };
+ }
+
+ if ( ! mediaId || ! mediaDetails?.mediaData ) {
+ return null;
+ }
+
+ const { sourceUrl } = mediaDetails.mediaData;
+ const { mime } = mediaDetails.metaData || {};
+
+ return {
+ id: mediaId,
+ url: sourceUrl,
+ type: mime?.startsWith( 'video/' ) ? 'video' : 'image',
+ };
+ }, [ currentSource, mediaId, mediaDetails, sigPreviewUrl ] );
+
+ // Handle media source selection from dropdown
+ const handleSourceSelect = useCallback(
+ ( source: MediaSourceType ) => {
+ recordEvent( 'jetpack_social_media_source_changed', {
+ ...analyticsData,
+ source,
+ } );
+
+ // Single batch update with explicit media_source and all related fields
+ updateJetpackSocialOptions( {
+ media_source: source || 'none',
+ attached_media: [], // Reset attachment when changing source
+ image_generator_settings: {
+ ...imageGeneratorSettings,
+ enabled: source === 'sig',
+ },
+ } );
+ },
+ [ recordEvent, analyticsData, updateJetpackSocialOptions, imageGeneratorSettings ]
+ );
+
+ // Handle media selection from Media Library
+ const handleMediaLibrarySelect = useCallback(
+ ( media: WPMediaObject ) => {
+ const { id, url, mime: type } = media;
+
+ // Single batch update with explicit media_source
+ updateJetpackSocialOptions( {
+ media_source: 'media-library',
+ attached_media: [ { id, url, type } ],
+ image_generator_settings: { ...imageGeneratorSettings, enabled: false },
+ } );
+
+ recordEvent( 'jetpack_social_media_source_changed', {
+ ...analyticsData,
+ source: 'media-library',
+ } );
+ },
+ [ updateJetpackSocialOptions, imageGeneratorSettings, recordEvent, analyticsData ]
+ );
+
+ const handleMediaLibraryClick = useCallback( () => {
+ setTimeout( () => {
+ openMediaLibraryRef.current();
+ }, 0 );
+ }, [] );
+
+ const renderMediaUpload = useCallback( ( { open }: { open: () => void } ) => {
+ openMediaLibraryRef.current = open;
+ return null;
+ }, [] );
+
+ // Handle remove - go to "no image" state
+ const handleRemove = useCallback( () => {
+ // Single batch update with explicit 'none' source
+ updateJetpackSocialOptions( {
+ media_source: 'none',
+ attached_media: [],
+ image_generator_settings: { ...imageGeneratorSettings, enabled: false },
+ } );
+
+ recordEvent( 'jetpack_social_media_removed', {
+ ...analyticsData,
+ source: currentSource,
+ } );
+ }, [
+ updateJetpackSocialOptions,
+ imageGeneratorSettings,
+ recordEvent,
+ analyticsData,
+ currentSource,
+ ] );
+
+ // Handle attachment toggle change
+ const handleAttachmentToggle = useCallback(
+ ( checked: boolean ) => {
+ if ( currentSource === 'featured-image' && previewData ) {
+ // Featured image: toggle attachment mode
+ updateJetpackSocialOptions( {
+ media_source: 'featured-image',
+ attached_media: checked
+ ? [ { id: previewData.id, url: previewData.url, type: 'image/jpeg' } ]
+ : [],
+ } );
+ } else if ( currentSource === 'sig' && sigPreviewUrl ) {
+ // SIG: toggle attachment mode (add SIG URL to attached_media)
+ updateJetpackSocialOptions( {
+ media_source: 'sig',
+ attached_media: checked ? [ { id: 0, url: sigPreviewUrl, type: 'image/jpeg' } ] : [],
+ // Keep SIG enabled regardless
+ image_generator_settings: { ...imageGeneratorSettings, enabled: true },
+ } );
+ }
+
+ recordEvent(
+ checked
+ ? 'jetpack_social_share_as_attachment_enabled'
+ : 'jetpack_social_share_as_attachment_disabled',
+ {
+ ...analyticsData,
+ source: currentSource,
+ }
+ );
+ },
+ [
+ currentSource,
+ previewData,
+ sigPreviewUrl,
+ updateJetpackSocialOptions,
+ imageGeneratorSettings,
+ recordEvent,
+ analyticsData,
+ ]
+ );
+
+ return (
+
+
+
+
+ { __( 'Media', 'jetpack-publicize-components' ) }
+
+ { getMediaSourceDescription( currentSource ) }
+
+ { /* MediaUpload component - rendered once, open function stored in ref */ }
+
+
+ { /* Show dropdown + preview when there's media */ }
+ { previewData && (
+ <>
+
+ { ( { open } ) => (
+
+ ) }
+
+ { currentSource === 'sig' && (
+
+ ) }
+
+ >
+ ) }
+
+ { /* Show dropdown when no media */ }
+ { ! previewData && (
+ <>
+
+ { currentSource === 'featured-image' && ! featuredImageId && (
+
+ { __(
+ 'Your post does not have a featured image.',
+ 'jetpack-publicize-components'
+ ) }
+
+ ) }
+ >
+ ) }
+
+
+
+ );
+}
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/media-preview.tsx b/projects/js-packages/publicize-components/src/components/media-section-v2/media-preview.tsx
new file mode 100644
index 0000000000000..23496076a6b5b
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/media-preview.tsx
@@ -0,0 +1,72 @@
+/**
+ * MediaPreview component
+ * Displays media preview
+ */
+
+import {
+ Button,
+ Spinner,
+ __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import styles from './styles.module.scss';
+import { MediaPreviewProps } from './types';
+
+/**
+ * MediaPreview component matching WordPress core featured image pattern
+ *
+ * @param {MediaPreviewProps} props - Component props
+ * @return {JSX.Element|null} MediaPreview component
+ */
+export default function MediaPreview( {
+ media,
+ isLoading = false,
+ onReplace,
+ onRemove,
+ disabled = false,
+}: MediaPreviewProps ) {
+ if ( ! media && ! isLoading ) {
+ return null;
+ }
+
+ return (
+
+
+ { media &&
+ ! isLoading &&
+ ( media.type === 'video' ? (
+
+ ) : (
+

+ ) ) }
+ { isLoading &&
}
+
+ { media && ! isLoading && (
+
+
+
+
+ ) }
+
+ );
+}
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/media-source-menu.tsx b/projects/js-packages/publicize-components/src/components/media-section-v2/media-source-menu.tsx
new file mode 100644
index 0000000000000..30a78ac6b4871
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/media-source-menu.tsx
@@ -0,0 +1,216 @@
+/**
+ * MediaSourceMenu component
+ * Displays a dropdown menu with grouped media source options
+ */
+
+import { Button, Dropdown, MenuGroup, MenuItem } from '@wordpress/components';
+import { useCallback } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { image, video, starEmpty, media as mediaIcon } from '@wordpress/icons';
+import styles from './styles.module.scss';
+import { MediaSourceMenuProps, MediaSourceOption, MediaSourceType } from './types';
+
+/**
+ * Available media source options with their metadata
+ */
+const MEDIA_SOURCE_OPTIONS: MediaSourceOption[] = [
+ {
+ id: 'featured-image',
+ label: __( 'Featured Image', 'jetpack-publicize-components' ),
+ description: __( 'You are using your post featured image', 'jetpack-publicize-components' ),
+ icon: image,
+ group: 'link-preview',
+ attachmentDescription: __(
+ 'Shares your image as a regular post, without a link preview card, for higher engagement.',
+ 'jetpack-publicize-components'
+ ),
+ },
+ {
+ id: 'sig',
+ label: __( 'Social Image Template', 'jetpack-publicize-components' ),
+ description: __( 'You are using the template', 'jetpack-publicize-components' ),
+ icon: starEmpty,
+ group: 'link-preview',
+ attachmentDescription: __(
+ 'Shares your template as an attached image, without a link preview card, for higher engagement.',
+ 'jetpack-publicize-components'
+ ),
+ },
+ {
+ id: 'media-library',
+ label: __( 'Media Library', 'jetpack-publicize-components' ),
+ description: __( 'You are using a custom image.', 'jetpack-publicize-components' ),
+ icon: mediaIcon,
+ group: 'attachment',
+ },
+ {
+ id: 'upload-video',
+ label: __( 'Upload video', 'jetpack-publicize-components' ),
+ description: __( 'Upload a video file', 'jetpack-publicize-components' ),
+ icon: video,
+ group: 'attachment',
+ },
+];
+
+/**
+ * Get the description for a media source
+ *
+ * @param {string} sourceType - Media source type
+ * @return {string} Description for the media source
+ */
+export function getMediaSourceDescription( sourceType: MediaSourceType ): string {
+ if ( ! sourceType ) {
+ return __( "Your post won't show an image.", 'jetpack-publicize-components' );
+ }
+ const option = MEDIA_SOURCE_OPTIONS.find( opt => opt.id === sourceType );
+ return (
+ option?.description || __( "Your post won't show an image.", 'jetpack-publicize-components' )
+ );
+}
+
+/**
+ * Get the attachment toggle description for a media source
+ *
+ * @param {string} sourceType - Media source type
+ * @return {string|undefined} Attachment description for the media source
+ */
+export function getAttachmentDescription( sourceType: MediaSourceType ): string | undefined {
+ if ( ! sourceType ) {
+ return undefined;
+ }
+ const option = MEDIA_SOURCE_OPTIONS.find( opt => opt.id === sourceType );
+ return option?.attachmentDescription;
+}
+
+/**
+ * Props for MediaSourceMenuItem component
+ */
+interface MediaSourceMenuItemProps {
+ option: MediaSourceOption;
+ isSelected: boolean;
+ onSelect: ( optionId: MediaSourceType ) => void;
+ onClose: () => void;
+ onMediaLibraryClick?: () => void;
+}
+
+/**
+ * MediaSourceMenuItem component
+ *
+ * @param {object} props - Component props
+ * @param {object} props.option - Menu option data
+ * @param {boolean} props.isSelected - Whether this option is selected
+ * @param {Function} props.onSelect - Selection handler
+ * @param {Function} props.onClose - Close dropdown handler
+ * @param {Function} props.onMediaLibraryClick - Media library click handler
+ * @return {object} MediaSourceMenuItem component
+ */
+function MediaSourceMenuItem( {
+ option,
+ isSelected,
+ onSelect,
+ onClose,
+ onMediaLibraryClick,
+}: MediaSourceMenuItemProps ) {
+ const handleClick = useCallback( () => {
+ if ( option.id === 'media-library' ) {
+ onMediaLibraryClick?.();
+ } else {
+ onSelect( option.id );
+ }
+ onClose();
+ }, [ option.id, onSelect, onClose, onMediaLibraryClick ] );
+
+ return (
+
+ );
+}
+
+/**
+ * MediaSourceMenu component
+ *
+ * @param {object} props - Component props
+ * @param {string} props.currentSource - Currently selected media source
+ * @param {Function} props.onSelect - Callback when a source is selected
+ * @param {Function} props.onMediaLibraryClick - Callback when Media Library option is clicked
+ * @param {boolean} props.disabled - Whether the menu is disabled
+ * @param {Function} props.children - Optional children render function that receives open function
+ * @return {object} MediaSourceMenu component
+ */
+export default function MediaSourceMenu( {
+ currentSource,
+ onSelect,
+ onMediaLibraryClick,
+ disabled = false,
+ children,
+}: MediaSourceMenuProps ) {
+ // Group options by category
+ const linkPreviewOptions = MEDIA_SOURCE_OPTIONS.filter( opt => opt.group === 'link-preview' );
+ const attachmentOptions = MEDIA_SOURCE_OPTIONS.filter( opt => opt.group === 'attachment' );
+
+ const renderToggle = useCallback(
+ ( { isOpen, onToggle }: { isOpen: boolean; onToggle: () => void } ) => (
+ <>
+ { ! children && (
+
+ ) }
+ { children && children( { open: onToggle } ) }
+ >
+ ),
+ [ children, disabled ]
+ );
+
+ const renderContent = useCallback(
+ ( { onClose }: { onClose: () => void } ) => (
+ <>
+
+ { linkPreviewOptions.map( option => (
+
+ ) ) }
+
+
+ { attachmentOptions.map( option => (
+
+ ) ) }
+
+ >
+ ),
+ [ linkPreviewOptions, attachmentOptions, currentSource, onSelect, onMediaLibraryClick ]
+ );
+
+ return (
+
+ );
+}
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/styles.module.scss b/projects/js-packages/publicize-components/src/components/media-section-v2/styles.module.scss
new file mode 100644
index 0000000000000..c61fd7a35e009
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/styles.module.scss
@@ -0,0 +1,86 @@
+/**
+ * Styles for the unified media section
+ * Featured image styles adapted from @wordpress/editor post-featured-image
+ */
+
+.media-section {
+ margin-bottom: 16px;
+}
+
+.description {
+ color: #757575;
+ margin: 8px 0 8px;
+ font-size: 13px;
+ line-height: 1.4;
+}
+
+.selectButton {
+ width: 100%;
+ justify-content: center;
+ margin-top: 6px;
+}
+
+.dropdownMenu {
+ width: 100%;
+}
+
+// Featured image styles (copied from Gutenberg for stability)
+.container {
+ position: relative;
+
+ &:hover,
+ &:focus,
+ &:focus-within {
+
+ .actions {
+ opacity: 1;
+ }
+ }
+}
+
+.preview {
+ width: 100%;
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ outline: 1px solid rgba(0, 0, 0, 0.1);
+ position: relative;
+ aspect-ratio: 2/1;
+
+ .components-spinner {
+ position: absolute;
+ }
+}
+
+.previewImage {
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ object-position: 50% 50%;
+}
+
+.actions {
+ bottom: 0;
+ opacity: 0;
+ padding: 8px;
+ position: absolute;
+ transition: opacity 50ms ease-out;
+ width: 100%;
+
+ .action {
+ flex-grow: 1;
+ justify-content: center;
+ backdrop-filter: blur(16px) saturate(180%);
+ background: rgba(255, 255, 255, 0.75);
+ }
+}
+
+.custom-media-toggle {
+ margin-top: 16px;
+}
+
+.notice {
+ margin-top: 12px;
+}
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/test/index.test.tsx b/projects/js-packages/publicize-components/src/components/media-section-v2/test/index.test.tsx
new file mode 100644
index 0000000000000..0bc0f537dfbf1
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/test/index.test.tsx
@@ -0,0 +1,309 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import MediaSectionV2 from '..';
+import useFeaturedImage from '../../../hooks/use-featured-image';
+import useImageGeneratorConfig from '../../../hooks/use-image-generator-config';
+import useMediaDetails from '../../../hooks/use-media-details';
+import { usePostMeta } from '../../../hooks/use-post-meta';
+import useSigPreview from '../../../hooks/use-sig-preview';
+
+// Mock functions
+const mockUpdateJetpackSocialOptions = jest.fn();
+const mockRecordEvent = jest.fn();
+
+jest.mock( '../../../hooks/use-featured-image', () => {
+ return jest.fn( () => 123 );
+} );
+
+jest.mock( '../../../hooks/use-image-generator-config', () => {
+ return jest.fn( () => ( {
+ isEnabled: false,
+ setIsEnabled: jest.fn(),
+ } ) );
+} );
+
+jest.mock( '../../../hooks/use-media-details', () => {
+ return jest.fn( () => [
+ {
+ mediaData: {
+ sourceUrl: 'https://example.com/featured.jpg',
+ },
+ metaData: {
+ mime: 'image/jpeg',
+ },
+ },
+ ] );
+} );
+
+jest.mock( '../../../hooks/use-post-meta', () => ( {
+ usePostMeta: jest.fn( () => ( {
+ attachedMedia: [],
+ imageGeneratorSettings: { enabled: false },
+ mediaSource: undefined,
+ updateJetpackSocialOptions: mockUpdateJetpackSocialOptions,
+ } ) ),
+} ) );
+
+jest.mock( '../../../hooks/use-sig-preview', () => {
+ return jest.fn( () => ( {
+ url: 'https://example.com/sig-preview.jpg',
+ isLoading: false,
+ } ) );
+} );
+
+jest.mock( '@automattic/jetpack-shared-extension-utils', () => ( {
+ useAnalytics: () => ( {
+ recordEvent: mockRecordEvent,
+ } ),
+} ) );
+
+jest.mock( '@wordpress/block-editor', () => ( {
+ MediaUpload: ( {
+ render: renderProp,
+ onSelect,
+ }: {
+ render: ( props: { open: () => void } ) => React.ReactNode;
+ onSelect: ( media: unknown ) => void;
+ } ) => {
+ const open = () => {
+ onSelect( {
+ id: 456,
+ url: 'https://example.com/selected.jpg',
+ mime: 'image/jpeg',
+ } );
+ };
+ return renderProp( { open } );
+ },
+} ) );
+
+describe( 'MediaSectionV2', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ describe( 'Initial rendering', () => {
+ it( 'should render the Media label', () => {
+ render( );
+
+ expect( screen.getByText( 'Media' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show featured image description when featured image is detected', () => {
+ render( );
+
+ expect( screen.getByText( 'You are using your post featured image' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show featured image preview', () => {
+ render( );
+
+ const img = screen.getByRole( 'img' );
+ expect( img ).toBeInTheDocument();
+ expect( img ).toHaveAttribute( 'src', 'https://example.com/featured.jpg' );
+ } );
+ } );
+
+ describe( 'No media state', () => {
+ beforeEach( () => {
+ ( useFeaturedImage as jest.Mock ).mockReturnValue( null );
+ ( useMediaDetails as jest.Mock ).mockReturnValue( [ null ] );
+ } );
+
+ afterEach( () => {
+ ( useFeaturedImage as jest.Mock ).mockReturnValue( 123 );
+ ( useMediaDetails as jest.Mock ).mockReturnValue( [
+ {
+ mediaData: { sourceUrl: 'https://example.com/featured.jpg' },
+ metaData: { mime: 'image/jpeg' },
+ },
+ ] );
+ } );
+
+ it( 'should show "no image" description when no media source is selected', () => {
+ render( );
+
+ expect( screen.getByText( "Your post won't show an image." ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show Select button when no media', () => {
+ render( );
+
+ expect( screen.getByRole( 'button', { name: 'Select' } ) ).toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'Attached media state', () => {
+ beforeEach( () => {
+ ( usePostMeta as jest.Mock ).mockReturnValue( {
+ attachedMedia: [ { id: 789, url: 'https://example.com/attached.jpg', type: 'image/jpeg' } ],
+ imageGeneratorSettings: { enabled: false },
+ mediaSource: 'media-library',
+ updateJetpackSocialOptions: mockUpdateJetpackSocialOptions,
+ } );
+ ( useMediaDetails as jest.Mock ).mockReturnValue( [
+ {
+ mediaData: { sourceUrl: 'https://example.com/attached.jpg' },
+ metaData: { mime: 'image/jpeg' },
+ },
+ ] );
+ } );
+
+ afterEach( () => {
+ ( usePostMeta as jest.Mock ).mockReturnValue( {
+ attachedMedia: [],
+ imageGeneratorSettings: { enabled: false },
+ mediaSource: undefined,
+ updateJetpackSocialOptions: mockUpdateJetpackSocialOptions,
+ } );
+ } );
+
+ it( 'should show custom image description when attached media exists', () => {
+ render( );
+
+ expect( screen.getByText( 'You are using a custom image.' ) ).toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'SIG enabled state', () => {
+ beforeEach( () => {
+ ( useImageGeneratorConfig as jest.Mock ).mockReturnValue( {
+ isEnabled: true,
+ setIsEnabled: jest.fn(),
+ } );
+ } );
+
+ afterEach( () => {
+ ( useImageGeneratorConfig as jest.Mock ).mockReturnValue( {
+ isEnabled: false,
+ setIsEnabled: jest.fn(),
+ } );
+ } );
+
+ it( 'should show SIG description when SIG is enabled', () => {
+ render( );
+
+ expect( screen.getByText( 'You are using the template' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should show SIG preview image', () => {
+ render( );
+
+ const img = screen.getByRole( 'img' );
+ expect( img ).toHaveAttribute( 'src', 'https://example.com/sig-preview.jpg' );
+ } );
+
+ it( 'should not show SIG preview image when loading', () => {
+ ( useSigPreview as jest.Mock ).mockReturnValue( {
+ url: '',
+ isLoading: true,
+ } );
+
+ render( );
+
+ // When SIG is loading, the preview image should not be visible yet
+ expect( screen.queryByRole( 'img' ) ).not.toBeInTheDocument();
+
+ // Reset
+ ( useSigPreview as jest.Mock ).mockReturnValue( {
+ url: 'https://example.com/sig-preview.jpg',
+ isLoading: false,
+ } );
+ } );
+ } );
+
+ describe( 'Source selection', () => {
+ it( 'should call updateJetpackSocialOptions when selecting SIG', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ // Open dropdown
+ await user.click( screen.getByRole( 'button', { name: 'Replace' } ) );
+
+ // Select SIG
+ await user.click( screen.getByRole( 'menuitem', { name: 'Social Image Template' } ) );
+
+ expect( mockUpdateJetpackSocialOptions ).toHaveBeenCalledWith( {
+ media_source: 'sig',
+ attached_media: [],
+ image_generator_settings: { enabled: true },
+ } );
+ } );
+
+ it( 'should call updateJetpackSocialOptions when selecting Featured Image', async () => {
+ const user = userEvent.setup();
+
+ // Start with SIG enabled
+ ( useImageGeneratorConfig as jest.Mock ).mockReturnValue( {
+ isEnabled: true,
+ setIsEnabled: jest.fn(),
+ } );
+
+ render( );
+
+ // Open dropdown
+ await user.click( screen.getByRole( 'button', { name: 'Replace' } ) );
+
+ // Select Featured Image
+ await user.click( screen.getByRole( 'menuitem', { name: 'Featured Image' } ) );
+
+ expect( mockUpdateJetpackSocialOptions ).toHaveBeenCalledWith( {
+ media_source: 'featured-image',
+ attached_media: [],
+ image_generator_settings: { enabled: false },
+ } );
+
+ // Reset
+ ( useImageGeneratorConfig as jest.Mock ).mockReturnValue( {
+ isEnabled: false,
+ setIsEnabled: jest.fn(),
+ } );
+ } );
+
+ it( 'should record analytics event when source is changed', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ // Open dropdown
+ await user.click( screen.getByRole( 'button', { name: 'Replace' } ) );
+
+ // Select SIG
+ await user.click( screen.getByRole( 'menuitem', { name: 'Social Image Template' } ) );
+
+ expect( mockRecordEvent ).toHaveBeenCalledWith( 'jetpack_social_media_source_changed', {
+ test: 'data',
+ source: 'sig',
+ } );
+ } );
+ } );
+
+ describe( 'Remove media', () => {
+ it( 'should clear media and record event when Remove is clicked', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ await user.click( screen.getByRole( 'button', { name: 'Remove' } ) );
+
+ expect( mockUpdateJetpackSocialOptions ).toHaveBeenCalledWith( {
+ media_source: 'none',
+ attached_media: [],
+ image_generator_settings: { enabled: false },
+ } );
+ expect( mockRecordEvent ).toHaveBeenCalledWith( 'jetpack_social_media_removed', {
+ test: 'data',
+ source: 'featured-image',
+ } );
+ } );
+ } );
+
+ describe( 'Disabled state', () => {
+ it( 'should disable buttons when disabled prop is true', () => {
+ render( );
+
+ expect( screen.getByRole( 'button', { name: 'Replace' } ) ).toBeDisabled();
+ expect( screen.getByRole( 'button', { name: 'Remove' } ) ).toBeDisabled();
+ } );
+ } );
+} );
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/test/media-preview.test.tsx b/projects/js-packages/publicize-components/src/components/media-section-v2/test/media-preview.test.tsx
new file mode 100644
index 0000000000000..9867738085af9
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/test/media-preview.test.tsx
@@ -0,0 +1,133 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import MediaPreview from '../media-preview';
+
+describe( 'MediaPreview', () => {
+ const defaultMedia = {
+ id: 123,
+ url: 'https://example.com/image.jpg',
+ type: 'image' as const,
+ };
+
+ const mockOnReplace = jest.fn();
+ const mockOnRemove = jest.fn();
+
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ it( 'should return null when no media and not loading', () => {
+ const { container } = render(
+
+ );
+
+ expect( container ).toBeEmptyDOMElement();
+ } );
+
+ it( 'should render image preview for image media', () => {
+ render(
+
+ );
+
+ const img = screen.getByRole( 'img' );
+ expect( img ).toBeInTheDocument();
+ expect( img ).toHaveAttribute( 'src', defaultMedia.url );
+ } );
+
+ it( 'should render video preview for video media', () => {
+ const videoMedia = {
+ id: 456,
+ url: 'https://example.com/video.mp4',
+ type: 'video' as const,
+ };
+
+ render(
+
+ );
+
+ // When video media is passed, no img element should be rendered
+ expect( screen.queryByRole( 'img' ) ).not.toBeInTheDocument();
+
+ // But the action buttons should still be available
+ expect( screen.getByRole( 'button', { name: 'Replace' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Remove' } ) ).toBeInTheDocument();
+ } );
+
+ it( 'should not show media preview when loading', () => {
+ render(
+
+ );
+
+ // When loading, the image should not be visible
+ expect( screen.queryByRole( 'img' ) ).not.toBeInTheDocument();
+ // And the action buttons should not be shown
+ expect( screen.queryByRole( 'button', { name: 'Replace' } ) ).not.toBeInTheDocument();
+ expect( screen.queryByRole( 'button', { name: 'Remove' } ) ).not.toBeInTheDocument();
+ } );
+
+ it( 'should render Replace and Remove buttons', () => {
+ render(
+
+ );
+
+ expect( screen.getByRole( 'button', { name: 'Replace' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Remove' } ) ).toBeInTheDocument();
+ } );
+
+ it( 'should call onReplace when Replace button is clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click( screen.getByRole( 'button', { name: 'Replace' } ) );
+
+ expect( mockOnReplace ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'should call onRemove when Remove button is clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click( screen.getByRole( 'button', { name: 'Remove' } ) );
+
+ expect( mockOnRemove ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'should disable buttons when disabled prop is true', () => {
+ render(
+
+ );
+
+ expect( screen.getByRole( 'button', { name: 'Replace' } ) ).toBeDisabled();
+ expect( screen.getByRole( 'button', { name: 'Remove' } ) ).toBeDisabled();
+ } );
+
+ it( 'should not show buttons when loading', () => {
+ render(
+
+ );
+
+ expect( screen.queryByRole( 'button', { name: 'Replace' } ) ).not.toBeInTheDocument();
+ expect( screen.queryByRole( 'button', { name: 'Remove' } ) ).not.toBeInTheDocument();
+ } );
+} );
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/test/media-source-menu.test.tsx b/projects/js-packages/publicize-components/src/components/media-section-v2/test/media-source-menu.test.tsx
new file mode 100644
index 0000000000000..d986c60b49d89
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/test/media-source-menu.test.tsx
@@ -0,0 +1,191 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import MediaSourceMenu, { getMediaSourceDescription } from '../media-source-menu';
+
+describe( 'getMediaSourceDescription', () => {
+ it( 'should return default message when sourceType is null', () => {
+ expect( getMediaSourceDescription( null ) ).toBe( "Your post won't show an image." );
+ } );
+
+ it( 'should return featured image description', () => {
+ expect( getMediaSourceDescription( 'featured-image' ) ).toBe(
+ 'You are using your post featured image'
+ );
+ } );
+
+ it( 'should return SIG description', () => {
+ expect( getMediaSourceDescription( 'sig' ) ).toBe( 'You are using the template' );
+ } );
+
+ it( 'should return media library description', () => {
+ expect( getMediaSourceDescription( 'media-library' ) ).toBe( 'You are using a custom image.' );
+ } );
+
+ it( 'should return upload video description', () => {
+ expect( getMediaSourceDescription( 'upload-video' ) ).toBe( 'Upload a video file' );
+ } );
+} );
+
+describe( 'MediaSourceMenu', () => {
+ const mockOnSelect = jest.fn();
+ const mockOnMediaLibraryClick = jest.fn();
+
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ it( 'should render Select button when no children provided', () => {
+ render(
+
+ );
+
+ expect( screen.getByRole( 'button', { name: 'Select' } ) ).toBeInTheDocument();
+ } );
+
+ it( 'should disable Select button when disabled prop is true', () => {
+ render(
+
+ );
+
+ expect( screen.getByRole( 'button', { name: 'Select' } ) ).toBeDisabled();
+ } );
+
+ it( 'should open dropdown menu when Select button is clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click( screen.getByRole( 'button', { name: 'Select' } ) );
+
+ // Check that menu groups are rendered
+ expect( screen.getByText( 'Link Preview' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Attachment' ) ).toBeInTheDocument();
+
+ // Check that menu items are rendered
+ expect( screen.getByRole( 'menuitem', { name: 'Featured Image' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'menuitem', { name: 'Social Image Template' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'menuitem', { name: 'Media Library' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'menuitem', { name: 'Upload video' } ) ).toBeInTheDocument();
+ } );
+
+ it( 'should call onSelect when Featured Image is clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click( screen.getByRole( 'button', { name: 'Select' } ) );
+ await user.click( screen.getByRole( 'menuitem', { name: 'Featured Image' } ) );
+
+ expect( mockOnSelect ).toHaveBeenCalledWith( 'featured-image' );
+ } );
+
+ it( 'should call onSelect when Social Image Template is clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click( screen.getByRole( 'button', { name: 'Select' } ) );
+ await user.click( screen.getByRole( 'menuitem', { name: 'Social Image Template' } ) );
+
+ expect( mockOnSelect ).toHaveBeenCalledWith( 'sig' );
+ } );
+
+ it( 'should call onMediaLibraryClick when Media Library is clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click( screen.getByRole( 'button', { name: 'Select' } ) );
+ await user.click( screen.getByRole( 'menuitem', { name: 'Media Library' } ) );
+
+ expect( mockOnMediaLibraryClick ).toHaveBeenCalledTimes( 1 );
+ expect( mockOnSelect ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should render children with open function when provided', async () => {
+ const user = userEvent.setup();
+ const mockChildren = jest.fn( ( { open } ) => (
+
+ ) );
+
+ render(
+
+ { mockChildren }
+
+ );
+
+ // Select button should not be rendered
+ expect( screen.queryByRole( 'button', { name: 'Select' } ) ).not.toBeInTheDocument();
+
+ // Custom trigger should be rendered
+ expect( screen.getByRole( 'button', { name: 'Custom Trigger' } ) ).toBeInTheDocument();
+
+ // Children should receive open function
+ expect( mockChildren ).toHaveBeenCalledWith(
+ expect.objectContaining( { open: expect.any( Function ) } )
+ );
+
+ // Clicking custom trigger should open dropdown
+ await user.click( screen.getByRole( 'button', { name: 'Custom Trigger' } ) );
+ expect( screen.getByText( 'Link Preview' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should render menu with current source item', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ await user.click( screen.getByRole( 'button', { name: 'Select' } ) );
+
+ // Verify the menu renders with all options including the current source
+ const featuredImageItem = screen.getByRole( 'menuitem', { name: 'Featured Image' } );
+ expect( featuredImageItem ).toBeInTheDocument();
+
+ // Verify clicking the current source still triggers onSelect
+ await user.click( featuredImageItem );
+ expect( mockOnSelect ).toHaveBeenCalledWith( 'featured-image' );
+ } );
+} );
diff --git a/projects/js-packages/publicize-components/src/components/media-section-v2/types.ts b/projects/js-packages/publicize-components/src/components/media-section-v2/types.ts
new file mode 100644
index 0000000000000..6598b13f36c06
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/components/media-section-v2/types.ts
@@ -0,0 +1,120 @@
+/**
+ * Types for the unified media section component
+ */
+
+/**
+ * Media source types
+ */
+export type MediaSourceType = 'featured-image' | 'media-library' | 'upload-video' | 'sig' | null;
+
+/**
+ * WordPress media object from MediaUpload
+ */
+export interface WPMediaObject {
+ id: number;
+ url: string;
+ mime: string;
+}
+
+/**
+ * Menu group types for categorizing media sources
+ */
+export type MenuGroupType = 'link-preview' | 'attachment';
+
+/**
+ * Media source option definition
+ */
+export interface MediaSourceOption {
+ id: MediaSourceType;
+ label: string;
+ description: string;
+ icon: JSX.Element;
+ group: MenuGroupType;
+ attachmentDescription?: string;
+}
+
+/**
+ * Media preview data structure
+ */
+export interface MediaPreviewData {
+ id: number;
+ url: string;
+ type: 'image' | 'video';
+ width?: number;
+ height?: number;
+}
+
+/**
+ * Props for MediaSectionV2 component
+ */
+export interface MediaSectionV2Props {
+ /**
+ * Analytics data to be passed to tracking events
+ */
+ analyticsData?: Record< string, unknown >;
+
+ /**
+ * Whether the section is disabled
+ */
+ disabled?: boolean;
+}
+
+/**
+ * Props for MediaSourceMenu component
+ */
+export interface MediaSourceMenuProps {
+ /**
+ * Currently selected media source
+ */
+ currentSource: MediaSourceType;
+
+ /**
+ * Callback when a media source is selected
+ */
+ onSelect: ( source: MediaSourceType ) => void;
+
+ /**
+ * Callback when Media Library option is clicked
+ */
+ onMediaLibraryClick?: () => void;
+
+ /**
+ * Whether the menu is disabled
+ */
+ disabled?: boolean;
+
+ /**
+ * Optional children render function that receives open function
+ */
+ children?: ( { open }: { open: () => void } ) => React.ReactNode;
+}
+
+/**
+ * Props for MediaPreview component
+ */
+export interface MediaPreviewProps {
+ /**
+ * Media preview data
+ */
+ media: MediaPreviewData | null;
+
+ /**
+ * Whether the preview is in loading state
+ */
+ isLoading?: boolean;
+
+ /**
+ * Callback to replace the media
+ */
+ onReplace?: () => void;
+
+ /**
+ * Callback to remove the media
+ */
+ onRemove?: () => void;
+
+ /**
+ * Whether the actions are disabled
+ */
+ disabled?: boolean;
+}
diff --git a/projects/js-packages/publicize-components/src/hooks/use-post-meta/index.js b/projects/js-packages/publicize-components/src/hooks/use-post-meta/index.js
index ea6fe4416fc35..04c162e5e8251 100644
--- a/projects/js-packages/publicize-components/src/hooks/use-post-meta/index.js
+++ b/projects/js-packages/publicize-components/src/hooks/use-post-meta/index.js
@@ -30,6 +30,7 @@ export function usePostMeta() {
const attachedMedia = jetpackSocialOptions.attached_media || DEFAULT_ATTACHED_MEDIA;
const imageGeneratorSettings =
jetpackSocialOptions.image_generator_settings ?? DEFAULT_IMAGE_GENERATOR_SETTINGS;
+ const mediaSource = jetpackSocialOptions.media_source;
const isPostAlreadyShared = meta.jetpack_social_post_already_shared ?? false;
const shareMessage = `${ meta.jetpack_publicize_message || '' }`.substring(
@@ -42,6 +43,7 @@ export function usePostMeta() {
jetpackSocialOptions,
attachedMedia,
imageGeneratorSettings,
+ mediaSource,
isPostAlreadyShared,
shareMessage,
};
@@ -65,10 +67,13 @@ export function usePostMeta() {
}, [ metaValues.isPublicizeEnabled, updateMeta ] );
const updateJetpackSocialOptions = useCallback(
- ( key, value ) => {
+ ( keyOrUpdates, value ) => {
+ // Support both single key-value and object of updates
+ const updates = typeof keyOrUpdates === 'string' ? { [ keyOrUpdates ]: value } : keyOrUpdates;
+
updateMeta( 'jetpack_social_options', {
...metaValues.jetpackSocialOptions,
- [ key ]: value,
+ ...updates,
version: 2,
} );
},
diff --git a/projects/js-packages/publicize-components/src/utils/types.ts b/projects/js-packages/publicize-components/src/utils/types.ts
index 71a5b8f1dc034..1ec711ff58a20 100644
--- a/projects/js-packages/publicize-components/src/utils/types.ts
+++ b/projects/js-packages/publicize-components/src/utils/types.ts
@@ -14,9 +14,12 @@ export type AttachedMedia = {
url: string;
};
+export type MediaSourceValue = 'featured-image' | 'sig' | 'media-library' | 'upload-video' | 'none';
+
export type JetpackSocialOptions = {
attached_media?: Array< AttachedMedia >;
image_generator_settings?: SIGSettings;
+ media_source?: MediaSourceValue;
};
export type JetpackSocialPostMeta = {
@@ -32,14 +35,17 @@ export type UsePostMeta = {
isPostAlreadyShared: boolean;
isPublicizeEnabled: boolean;
jetpackSocialOptions: JetpackSocialOptions;
+ mediaSource: MediaSourceValue | undefined;
shareMessage: string;
togglePublicizeFeature: VoidFunction;
updateMeta: < K extends keyof JetpackSocialPostMeta >(
metaKey: K,
metaValue: JetpackSocialPostMeta[ K ]
) => void;
- updateJetpackSocialOptions: < K extends keyof JetpackSocialOptions >(
- key: K,
- value: JetpackSocialOptions[ K ]
- ) => void;
+ updateJetpackSocialOptions: {
+ // Single key-value update
+ < K extends keyof JetpackSocialOptions >( key: K, value: JetpackSocialOptions[ K ] ): void;
+ // Batch update with object
+ ( updates: Partial< JetpackSocialOptions > ): void;
+ };
};
diff --git a/projects/packages/publicize/changelog/add-social-new-media-selector-ui b/projects/packages/publicize/changelog/add-social-new-media-selector-ui
new file mode 100644
index 0000000000000..cf502cf278d39
--- /dev/null
+++ b/projects/packages/publicize/changelog/add-social-new-media-selector-ui
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Added new media selector UI.
diff --git a/projects/packages/publicize/src/class-publicize-base.php b/projects/packages/publicize/src/class-publicize-base.php
index c182a41995534..575ebb1ce6381 100644
--- a/projects/packages/publicize/src/class-publicize-base.php
+++ b/projects/packages/publicize/src/class-publicize-base.php
@@ -1202,6 +1202,10 @@ public function register_post_meta() {
),
),
),
+ 'media_source' => array(
+ 'type' => 'string',
+ 'enum' => array( 'featured-image', 'sig', 'media-library', 'upload-video', 'none' ),
+ ),
),
),
),
diff --git a/projects/plugins/jetpack/changelog/add-social-new-media-selector-ui b/projects/plugins/jetpack/changelog/add-social-new-media-selector-ui
new file mode 100644
index 0000000000000..34fb0082fd3fc
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-social-new-media-selector-ui
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Add the new media selection UI for Social.
diff --git a/projects/plugins/social/changelog/add-social-new-media-selector-ui b/projects/plugins/social/changelog/add-social-new-media-selector-ui
new file mode 100644
index 0000000000000..bc731a24e862f
--- /dev/null
+++ b/projects/plugins/social/changelog/add-social-new-media-selector-ui
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Add the new media selection UI for Social.