Skip to content

Commit bd7f10b

Browse files
obenlandpfefferle
andauthored
Fix Followers block error when social graph is hidden (#2625)
Co-authored-by: Matthias Pfefferle <[email protected]>
1 parent 899db61 commit bd7f10b

File tree

10 files changed

+190
-57
lines changed

10 files changed

+190
-57
lines changed
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+
Fediverse Followers block now works correctly when the "Hide Social Graph" privacy option is enabled.

build/followers/index.asset.php

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/followers/index.js

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/followers/render.php

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

includes/class-activitypub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ public static function register_user_meta() {
194194
'single' => true,
195195
'default' => 0,
196196
'sanitize_callback' => 'absint',
197+
'show_in_rest' => true,
197198
)
198199
);
199200

includes/class-options.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ public static function register_settings() {
393393
'description' => 'Hide Followers and Followings on Profile.',
394394
'default' => 0,
395395
'sanitize_callback' => 'absint',
396+
'show_in_rest' => true,
396397
)
397398
);
398399
}

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@wordpress/editor": "^14.33.2",
5151
"@wordpress/element": "^6.0.0",
5252
"@wordpress/env": "^10.36.0",
53+
"@wordpress/html-entities": "^4.36.0",
5354
"@wordpress/i18n": "^6.0.0",
5455
"@wordpress/icons": "^11.0.0",
5556
"@wordpress/interactivity": "^6.33.0",

src/followers/edit.js

Lines changed: 170 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
11
import apiFetch from '@wordpress/api-fetch';
2-
import { SelectControl, RangeControl, PanelBody } from '@wordpress/components';
2+
import { SelectControl, RangeControl, PanelBody, Notice } from '@wordpress/components';
33
import { InspectorControls, useBlockProps, InnerBlocks } from '@wordpress/block-editor';
44
import { store as coreStore } from '@wordpress/core-data';
55
import { useSelect } from '@wordpress/data';
6-
import { useState, useEffect } from '@wordpress/element';
6+
import { useState, useEffect, useMemo, createInterpolateElement } from '@wordpress/element';
77
import { addQueryArgs } from '@wordpress/url';
88
import { __ } from '@wordpress/i18n';
99
import { useOptions } from '../shared/use-options';
1010
import { useUserOptions } from '../shared/use-user-options';
1111
import { 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
*/
155265
function 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
*/
215330
function 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
*/
264380
function Follower( { name, icon, url, preferredUsername } ) {
265381
const handle = `@${ preferredUsername }`;

src/followers/render.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545
return '<!-- Followers block: `' . $user_id . '` not an active ActivityPub user -->';
4646
}
4747

48+
if ( ! Actors::show_social_graph( $user_id ) ) {
49+
return '<!-- Followers block: social graph is hidden for this user -->';
50+
}
51+
4852
$_per_page = absint( $attributes['per_page'] );
4953
$_show_avatars = (bool) \get_option( 'show_avatars' );
5054
$follower_data = Followers::query( $user_id, $_per_page );

0 commit comments

Comments
 (0)