11import apiFetch from '@wordpress/api-fetch' ;
2- import { SelectControl , RangeControl , PanelBody } from '@wordpress/components' ;
2+ import { SelectControl , RangeControl , PanelBody , Notice } from '@wordpress/components' ;
33import { InspectorControls , useBlockProps , InnerBlocks } from '@wordpress/block-editor' ;
44import { store as coreStore } from '@wordpress/core-data' ;
55import { useSelect } from '@wordpress/data' ;
6- import { useState , useEffect } from '@wordpress/element' ;
6+ import { useState , useEffect , useMemo , createInterpolateElement } from '@wordpress/element' ;
77import { addQueryArgs } from '@wordpress/url' ;
88import { __ } from '@wordpress/i18n' ;
99import { useOptions } from '../shared/use-options' ;
1010import { useUserOptions } from '../shared/use-user-options' ;
1111import { InheritModeBlockFallback } from '../shared/inherit-block-fallback' ;
1212
13+ /**
14+ * Check if a user has their social graph hidden based on user meta.
15+ *
16+ * @param {Object } userMeta The user's metadata.
17+ * @return {boolean } True if social graph is hidden.
18+ */
19+ function hasSocialGraphHidden ( userMeta ) {
20+ if ( ! userMeta ) {
21+ return false ;
22+ }
23+
24+ return Object . entries ( userMeta ) . some (
25+ ( [ key , value ] ) => key . endsWith ( 'activitypub_hide_social_graph' ) && value
26+ ) ;
27+ }
28+
1329/**
1430 * Edit component.
1531 *
16- * @param {Object } props Component props.
17- * @param {Object } props.attributes Block attributes.
18- * @param {Function } props.setAttributes Set block attributes.
19- * @param {Object } props.context Block context.
20- * @param {string } props.context.postType Post type.
21- * @param {number } props.context.postId Post ID.
32+ * @param {Object } props Component props.
33+ * @param {Object } props.attributes Block attributes.
34+ * @param {Function } props.setAttributes Set block attributes.
35+ * @param {Object } props.context Block context.
36+ * @param {string } props.context.postType Post type.
37+ * @param {number } props.context.postId Post ID.
2238 *
2339 * @return {JSX.Element } Edit component.
2440 */
@@ -35,6 +51,22 @@ export default function Edit( { attributes, setAttributes, context: { postType,
3551 setPage ( 1 ) ;
3652 setAttributes ( { [ key ] : value } ) ;
3753 } ;
54+
55+ // Get site settings to check blog social graph visibility.
56+ const { blogSocialGraphHidden, currentUser, usersWithMeta, siteUrl, canManageOptions } = useSelect ( ( select ) => {
57+ const { getCurrentUser, getUsers, getEntityRecord, canUser } = select ( coreStore ) ;
58+ const siteSettings = getEntityRecord ( 'root' , 'site' ) ;
59+ const siteData = getEntityRecord ( 'root' , '__unstableBase' ) ;
60+
61+ return {
62+ blogSocialGraphHidden : ! ! siteSettings ?. activitypub_hide_social_graph ,
63+ currentUser : getCurrentUser ( ) ,
64+ usersWithMeta : getUsers ( { capabilities : 'activitypub' , context : 'edit' } ) ,
65+ siteUrl : siteData ?. home ,
66+ canManageOptions : canUser ( 'update' , { kind : 'root' , name : 'site' } ) ,
67+ } ;
68+ } , [ ] ) ;
69+
3870 const authorId = useSelect (
3971 ( select ) => {
4072 const { getEditedEntityRecord } = select ( coreStore ) ;
@@ -45,16 +77,92 @@ export default function Edit( { attributes, setAttributes, context: { postType,
4577 [ postType , postId ]
4678 ) ;
4779
80+ // Filter user options based on social graph visibility.
81+ const filteredUsersOptions = useMemo ( ( ) => {
82+ if ( ! usersOptions . length || ! usersWithMeta ) {
83+ return [ ] ;
84+ }
85+
86+ return usersOptions . filter ( ( { value } ) => {
87+ // Always keep 'inherit' (Dynamic User) option.
88+ if ( value === 'inherit' ) {
89+ return true ;
90+ }
91+ // Check blog social graph visibility.
92+ if ( value === 'blog' ) {
93+ return ! blogSocialGraphHidden ;
94+ }
95+ // Check individual user social graph visibility.
96+ const user = usersWithMeta ?. find ( ( u ) => String ( u . id ) === value ) ;
97+ return ! hasSocialGraphHidden ( user ?. meta ) ;
98+ } ) ;
99+ } , [ usersOptions , blogSocialGraphHidden , usersWithMeta ] ) ;
100+
101+ // Determine if we should show a notice for hidden social graph.
102+ const showHiddenNotice = useMemo ( ( ) => {
103+ if ( ! usersWithMeta ) {
104+ return false ;
105+ }
106+
107+ // Check blog social graph visibility.
108+ if ( selectedUser === 'blog' ) {
109+ return blogSocialGraphHidden ;
110+ }
111+
112+ // For 'inherit' mode, check if the resolved author has hidden social graph.
113+ if ( selectedUser === 'inherit' ) {
114+ if ( ! authorId ) {
115+ return false ;
116+ }
117+ const author = usersWithMeta . find ( ( u ) => u . id === authorId ) ;
118+ return author ? hasSocialGraphHidden ( author . meta ) : false ;
119+ }
120+
121+ return false ;
122+ } , [ selectedUser , authorId , usersWithMeta , blogSocialGraphHidden ] ) ;
123+
124+ // Determine if current user can edit the settings for the selected user.
125+ const canEditSettings = useMemo ( ( ) => {
126+ if ( ! showHiddenNotice || ! currentUser ) {
127+ return false ;
128+ }
129+
130+ if ( selectedUser === 'blog' ) {
131+ return canManageOptions ;
132+ }
133+
134+ return currentUser . id === authorId ;
135+ } , [ showHiddenNotice , currentUser , selectedUser , authorId , canManageOptions ] ) ;
136+
137+ // Get the settings URL for the notice.
138+ const settingsUrl = useMemo ( ( ) => {
139+ if ( ! canEditSettings || ! siteUrl ) {
140+ return null ;
141+ }
142+
143+ if ( selectedUser === 'blog' ) {
144+ return siteUrl + '/wp-admin/options-general.php?page=activitypub&tab=blog-profile' ;
145+ }
146+
147+ return siteUrl + '/wp-admin/profile.php#activitypub' ;
148+ } , [ canEditSettings , siteUrl , selectedUser ] ) ;
149+
48150 useEffect ( ( ) => {
49151 // if there are no users yet, do nothing
50- if ( ! usersOptions . length ) {
152+ if ( ! filteredUsersOptions . length ) {
51153 return ;
52154 }
53- // ensure that the selected user is in the list of options, if not, select the first available user
54- if ( ! usersOptions . find ( ( { value } ) => value === selectedUser ) ) {
55- setAttributes ( { selectedUser : usersOptions [ 0 ] . value } ) ;
155+
156+ // If selected user is not in the filtered options, auto-switch to first available.
157+ // Exception: 'blog' and 'inherit' show a notice instead of auto-switching.
158+ if (
159+ selectedUser !== 'blog' &&
160+ selectedUser !== 'inherit' &&
161+ ! filteredUsersOptions . find ( ( { value } ) => value === selectedUser )
162+ ) {
163+ setAttributes ( { selectedUser : filteredUsersOptions [ 0 ] . value } ) ;
56164 }
57- } , [ selectedUser , usersOptions ] ) ;
165+ } , [ selectedUser , filteredUsersOptions , setAttributes ] ) ;
58166
59167 // Template for InnerBlocks - allows only a heading block.
60168 const TEMPLATE = [
@@ -72,11 +180,11 @@ export default function Edit( { attributes, setAttributes, context: { postType,
72180 < div { ...blockProps } >
73181 < InspectorControls key = "setting" >
74182 < PanelBody title = { __ ( 'Followers Options' , 'activitypub' ) } >
75- { usersOptions . length > 1 && (
183+ { filteredUsersOptions . length > 1 && (
76184 < SelectControl
77185 label = { __ ( 'Select User' , 'activitypub' ) }
78186 value = { selectedUser }
79- options = { usersOptions }
187+ options = { filteredUsersOptions }
80188 onChange = { setAttributeWithPageReset ( 'selectedUser' ) }
81189 __next40pxDefaultSize
82190 __nextHasNoMarginBottom
@@ -110,7 +218,25 @@ export default function Edit( { attributes, setAttributes, context: { postType,
110218 renderAppender = { false }
111219 />
112220
113- { selectedUser === 'inherit' ? (
221+ { showHiddenNotice ? (
222+ < Notice status = "warning" isDismissible = { false } >
223+ { settingsUrl
224+ ? createInterpolateElement (
225+ /* translators: <a> is a link to the profile settings page. */
226+ __ (
227+ 'The selected user has their social graph hidden. This block will not display followers on the frontend. <a>Edit privacy settings</a>' ,
228+ 'activitypub'
229+ ) ,
230+ {
231+ a : < a href = { settingsUrl } target = "_blank" rel = "noopener noreferrer" /> ,
232+ }
233+ )
234+ : __ (
235+ 'The selected user has their social graph hidden. This block will not display followers on the frontend.' ,
236+ 'activitypub'
237+ ) }
238+ </ Notice >
239+ ) : selectedUser === 'inherit' ? (
114240 authorId ? (
115241 < Followers { ...attributes } page = { page } setPage = { setPage } selectedUser = { authorId } />
116242 ) : (
@@ -124,33 +250,17 @@ export default function Edit( { attributes, setAttributes, context: { postType,
124250 ) ;
125251}
126252
127- /**
128- * Builds the API path for fetching followers.
129- *
130- * @param {number } userId - The ID of the user whose followers are being fetched.
131- * @param {number } per_page - The number of followers to fetch per page.
132- * @param {string } order - The order in which to fetch followers ('asc' or 'desc').
133- * @param {number } page - The page number to fetch.
134- * @return {string } The API path with query arguments for fetching followers.
135- */
136- function getPath ( userId , per_page , order , page ) {
137- const { namespace } = useOptions ( ) ;
138- const path = `/${ namespace } /actors/${ userId } /followers` ;
139- const args = { per_page, order, page, context : 'full' } ;
140-
141- return addQueryArgs ( path , args ) ;
142- }
143-
144253/**
145254 * Component to display followers of a user.
146255 *
147- * @param {Object } props - The component props.
148- * @param {String } props.selectedUser - The ID of the user whose followers are being fetched.
149- * @param {number } props.per_page - The number of followers to fetch per page.
150- * @param {string } props.order - The order in which to fetch followers ('asc' or 'desc').
151- * @param {number } props.page - The page number to fetch.
152- * @param {function } props.setPage - The function to set the page number.
153- * @param {Object } props.followerData - Optional pre-fetched follower data.
256+ * @param {Object } props The component props.
257+ * @param {string } props.selectedUser The ID of the user whose followers are being fetched.
258+ * @param {number } props.per_page The number of followers to fetch per page.
259+ * @param {string } props.order The order in which to fetch followers ('asc' or 'desc').
260+ * @param {number } props.page The page number to fetch.
261+ * @param {Function } props.setPage The function to set the page number.
262+ * @param {Object } props.followerData Optional pre-fetched follower data.
263+ * @return {JSX.Element } The followers list component.
154264 */
155265function Followers ( {
156266 selectedUser,
@@ -160,17 +270,16 @@ function Followers( {
160270 setPage : passedSetPage ,
161271 followerData = false ,
162272} ) {
273+ const { namespace } = useOptions ( ) ;
163274 const userId = selectedUser === 'blog' ? 0 : selectedUser ;
164275 const [ followers , setFollowers ] = useState ( [ ] ) ;
165276 const [ pages , setPages ] = useState ( 0 ) ;
166- const [ total , setTotal ] = useState ( 0 ) ;
167277 const [ localPage , setLocalPage ] = useState ( 1 ) ;
168278 const page = passedPage || localPage ;
169279 const setPage = passedSetPage || setLocalPage ;
170280
171281 const setData = ( followers , total ) => {
172282 setFollowers ( followers ) ;
173- setTotal ( total ) ;
174283 setPages ( Math . ceil ( total / per_page ) ) ;
175284 } ;
176285
@@ -179,11 +288,16 @@ function Followers( {
179288 return setData ( followerData . followers , followerData . total ) ;
180289 }
181290
182- const path = getPath ( userId , per_page , order , page ) ;
291+ const path = addQueryArgs ( `/${ namespace } /actors/${ userId } /followers` , {
292+ per_page,
293+ order,
294+ page,
295+ context : 'full' ,
296+ } ) ;
183297 apiFetch ( { path } )
184- . then ( ( { orderedItems, totalItems } ) => setData ( orderedItems , totalItems ) )
298+ . then ( ( { orderedItems = [ ] , totalItems = 0 } ) => setData ( orderedItems , totalItems ) )
185299 . catch ( ( ) => setData ( [ ] , 0 ) ) ;
186- } , [ userId , per_page , order , page , followerData ] ) ;
300+ } , [ namespace , userId , per_page , order , page , followerData ] ) ;
187301
188302 return (
189303 < div className = "followers-container" >
@@ -207,10 +321,11 @@ function Followers( {
207321/**
208322 * Component to display pagination navigation.
209323 *
210- * @param {Object } props - The component props.
211- * @param {number } props.page - The current page number.
212- * @param {number } props.pages - The total number of pages.
213- * @param {function } props.setPage - The function to set the page number.
324+ * @param {Object } props The component props.
325+ * @param {number } props.page The current page number.
326+ * @param {number } props.pages The total number of pages.
327+ * @param {Function } props.setPage The function to set the page number.
328+ * @return {JSX.Element|null } The pagination component or null if not needed.
214329 */
215330function Pagination ( { page, pages, setPage } ) {
216331 if ( pages <= 1 ) {
@@ -255,11 +370,12 @@ function Pagination( { page, pages, setPage } ) {
255370/**
256371 * Component to display a single follower.
257372 *
258- * @param {Object } props - The component props.
259- * @param {string } props.name - The name of the follower.
260- * @param {Object } props.icon - The icon of the follower.
261- * @param {string } props.url - The URL of the follower.
262- * @param {string } props.preferredUsername - The preferred username of the follower.
373+ * @param {Object } props The component props.
374+ * @param {string } props.name The name of the follower.
375+ * @param {Object } props.icon The icon of the follower.
376+ * @param {string } props.url The URL of the follower.
377+ * @param {string } props.preferredUsername The preferred username of the follower.
378+ * @return {JSX.Element } The follower component.
263379 */
264380function Follower ( { name, icon, url, preferredUsername } ) {
265381 const handle = `@${ preferredUsername } ` ;
0 commit comments