diff --git a/ACTORS_ENTITY_IMPLEMENTATION.md b/ACTORS_ENTITY_IMPLEMENTATION.md new file mode 100644 index 0000000000..7100bb17a2 --- /dev/null +++ b/ACTORS_ENTITY_IMPLEMENTATION.md @@ -0,0 +1,267 @@ +# Actors Entity Implementation + +This document describes the implementation of ActivityPub actors as a WordPress Core Data entity. + +## Overview + +The actors entity allows block editor components to fetch and display ActivityPub actor information using WordPress Core Data API hooks like `useEntityRecords()` and `useEntityRecord()`. + +## What Was Created + +### 1. REST API Controller (PHP) + +**File**: `includes/rest/class-internal-actors-controller.php` + +A REST API controller that provides internal endpoints for fetching actor data: + +- **GET** `/wp-json/activitypub/v1/internal/actors` - Get all actors +- **GET** `/wp-json/activitypub/v1/internal/actors/{id}` - Get single actor + +**Features**: +- Read-only access to local actors (users, blog, application) +- Requires authentication (logged-in users only) +- Returns actor data in a standardized format +- Includes proper WordPress coding standards + +**Actor Data Structure**: +```php +array( + 'id' => int, // WordPress user ID (0 for blog, -1 for application) + 'type' => string, // 'user', 'blog', or 'application' + 'name' => string, // Display name + 'preferred_username' => string, // Username/identifier + 'url' => string, // Profile URL + 'icon' => object, // Avatar/icon info + 'summary' => string, // Bio/description + 'activitypub_id' => string, // ActivityPub URI +) +``` + +### 2. Entity Registration (JavaScript) + +**File**: `src/actors-entity/index.js` + +Registers the actor entity with WordPress Core Data API: + +```javascript +registerEntityType( { + kind: 'activitypub/v1', + name: 'actor', + baseURL: '/wp-json/activitypub/v1/internal/actors', + // ... configuration +} ); +``` + +### 3. Build Configuration + +**File**: `src/actors-entity/block.json` + +Minimal block.json to enable wp-scripts build process: + +```json +{ + "name": "actors-entity", + "title": "Actors Entity: Registers actors as WordPress Core Data entities", + "editorScript": "file:./index.js" +} +``` + +### 4. Integration + +**Modified Files**: + +1. `activitypub.php` - Added REST route registration: + ```php + ( new Rest\Internal_Actors_Controller() )->register_routes(); + ``` + +2. `includes/class-blocks.php` - Added script enqueuing: + ```php + // Register the actors entity with WordPress Core Data API. + $entity_asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/actors-entity/index.asset.php'; + if ( file_exists( $entity_asset_file ) ) { + $entity_asset_data = include $entity_asset_file; + $entity_url = plugins_url( 'build/actors-entity/index.js', ACTIVITYPUB_PLUGIN_FILE ); + wp_enqueue_script( + 'activitypub-actors-entity', + $entity_url, + $entity_asset_data['dependencies'], + $entity_asset_data['version'], + true + ); + } + ``` + +### 5. Documentation + +**File**: `src/actors-entity/README.md` + +Comprehensive documentation including: +- What WordPress entities are +- How to use the actors entity +- 5 detailed usage examples +- REST API endpoint documentation +- Actor schema reference +- Testing instructions +- Extension guidelines + +### 6. Example Code + +**File**: `src/actors-entity/example.js` + +Complete example components demonstrating: +- `ActorsList` - Display all actors +- `ActorProfile` - Display single actor details +- `ActorSelector` - Dropdown for selecting actors +- `ActorsByType` - Filter actors by type +- `ActorBrowser` - Complete interactive browser + +## Usage in Block Editor + +### Basic Example: Fetch All Actors + +```javascript +import { useEntityRecords } from '@wordpress/core-data'; + +function MyComponent() { + const { records: actors, isResolving } = useEntityRecords( + 'activitypub/v1', + 'actor' + ); + + if ( isResolving ) { + return

Loading...

; + } + + return ( + + ); +} +``` + +### Fetch Single Actor + +```javascript +import { useEntityRecord } from '@wordpress/core-data'; + +function ActorProfile( { actorId } ) { + const { record: actor, isResolving } = useEntityRecord( + 'activitypub/v1', + 'actor', + actorId + ); + + if ( isResolving ) return

Loading...

; + if ( ! actor ) return

Not found

; + + return ( +
+

{ actor.name }

+

@{ actor.preferred_username }

+

Type: { actor.type }

+
+ ); +} +``` + +## Testing + +### Test REST API Endpoint + +Start the development environment: + +```bash +npm run env-start +``` + +Test the endpoints (replace cookie with actual logged-in cookie): + +```bash +# Get all actors +curl -X GET "http://localhost/wp-json/activitypub/v1/internal/actors" \ + --cookie "wordpress_logged_in_..." + +# Get specific actor (blog actor) +curl -X GET "http://localhost/wp-json/activitypub/v1/internal/actors/0" \ + --cookie "wordpress_logged_in_..." + +# Get user actor +curl -X GET "http://localhost/wp-json/activitypub/v1/internal/actors/1" \ + --cookie "wordpress_logged_in_..." +``` + +### Test in Block Editor + +1. Create a new post +2. Open browser console +3. Test the entity: + +```javascript +// Fetch all actors +wp.data.select('core').getEntityRecords('activitypub/v1', 'actor'); + +// Fetch specific actor +wp.data.select('core').getEntityRecord('activitypub/v1', 'actor', 0); +``` + +## File Structure + +``` +includes/ +└── rest/ + └── class-internal-actors-controller.php # REST API controller + +src/ +└── actors-entity/ + ├── index.js # Entity registration + ├── block.json # Build configuration + ├── README.md # Documentation + └── example.js # Example components + +build/ +└── actors-entity/ + ├── index.js # Built entity registration + ├── index.asset.php # Asset dependencies + └── block.json # Copied block.json +``` + +## Benefits + +1. **Standard WordPress Pattern**: Uses WordPress Core Data API +2. **Type Safety**: Returns consistent data structure +3. **Automatic Caching**: Core Data handles caching automatically +4. **Easy to Use**: Simple hooks interface +5. **Flexible**: Can be extended with filters +6. **Documented**: Comprehensive documentation and examples + +## Future Enhancements + +Possible improvements: + +1. Add support for remote actors (not just local) +2. Add pagination for large actor lists +3. Add search/filter parameters +4. Add write operations (if needed) +5. Add real-time updates via WebSocket or polling + +## Related Resources + +- [WordPress Core Data Package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/) +- [WordPress REST API Handbook](https://developer.wordpress.org/rest-api/) +- [ActivityPub Actors Collection](includes/collection/class-actors.php) + +## Changelog + +### v1.0.0 - Initial Implementation + +- Created Internal_Actors_Controller REST API endpoint +- Registered actor entity with WordPress Core Data +- Added comprehensive documentation +- Added example components +- Integrated with block editor assets diff --git a/INTERNAL_API_IMPLEMENTATION.md b/INTERNAL_API_IMPLEMENTATION.md new file mode 100644 index 0000000000..62696299a2 --- /dev/null +++ b/INTERNAL_API_IMPLEMENTATION.md @@ -0,0 +1,383 @@ +# Internal API Implementation + +This document describes the implementation of the internal REST API and entity system for the ActivityPub plugin, specifically organized under the `/internal` namespace. + +## Overview + +The internal API provides WordPress Core Data entities for ActivityPub actors with support for filtering by relationships (followers/following). This allows block editor components to easily fetch and display actor data using standard WordPress hooks. + +## Structure + +### PHP (REST API) + +**Location**: `includes/rest/internal/` + +- **`class-actors-controller.php`** - Internal actors REST controller + +**Namespace**: `Activitypub\Rest\Internal` + +### JavaScript (Entities) + +**Location**: `src/internal/` + +- **`actors-entity/`** - Actors entity registration and examples + +**Build Output**: `build/internal/` + +## REST API Endpoint + +### Base URL + +``` +/wp-json/activitypub/v1/internal/actors +``` + +### Endpoints + +#### 1. Get All Actors (Local) + +``` +GET /wp-json/activitypub/v1/internal/actors +``` + +Returns all local actors (users, blog, application). + +**Query Parameters**: +- `context` (string): Response context (`view`, `edit`, `embed`) +- `type` (string): Filter by actor type (`user`, `blog`, `application`, `remote`) +- `page` (integer): Page number (default: 1) +- `per_page` (integer): Items per page (default: 10, max: 100) +- `order` (string): Sort order (`asc`, `desc`, default: `desc`) +- `search` (string): Search term + +**Example**: +```bash +curl "http://localhost/wp-json/activitypub/v1/internal/actors?type=user" +``` + +#### 2. Get Single Actor + +``` +GET /wp-json/activitypub/v1/internal/actors/{id} +``` + +Returns a single actor by ID. + +**Example**: +```bash +curl "http://localhost/wp-json/activitypub/v1/internal/actors/0" # Blog actor +curl "http://localhost/wp-json/activitypub/v1/internal/actors/1" # User with ID 1 +``` + +#### 3. Get Followers (NEW!) + +``` +GET /wp-json/activitypub/v1/internal/actors?relationship=followers&user_id={id} +``` + +Returns followers for a specific user. + +**Query Parameters**: +- `relationship` (string): **Required**. Must be `followers` or `following` +- `user_id` (integer): **Required**. Actor ID to get relationships for +- `page` (integer): Page number (default: 1) +- `per_page` (integer): Items per page (default: 10, max: 100) +- `order` (string): Sort order (`asc`, `desc`, default: `desc`) +- `search` (string): Search followers by name + +**Example**: +```bash +# Get followers for blog actor (ID: 0) +curl "http://localhost/wp-json/activitypub/v1/internal/actors?relationship=followers&user_id=0&per_page=10" + +# Get followers for user ID 1 +curl "http://localhost/wp-json/activitypub/v1/internal/actors?relationship=followers&user_id=1" + +# Search followers +curl "http://localhost/wp-json/activitypub/v1/internal/actors?relationship=followers&user_id=0&search=mastodon" +``` + +#### 4. Get Following + +``` +GET /wp-json/activitypub/v1/internal/actors?relationship=following&user_id={id} +``` + +Returns actors that a user is following. + +**Parameters**: Same as followers endpoint, but with `relationship=following` + +### Response Format + +#### Local Actor Response + +```json +{ + "id": 0, + "type": "blog", + "name": "My WordPress Site", + "preferred_username": "blog", + "url": "https://example.com", + "icon": { + "type": "Image", + "url": "https://example.com/avatar.jpg" + }, + "summary": "A WordPress blog about technology", + "activitypub_id": "https://example.com" +} +``` + +#### Remote Actor Response (Followers/Following) + +```json +{ + "id": 12345, + "type": "remote", + "name": "John Doe", + "preferred_username": "johndoe", + "url": "https://mastodon.social/@johndoe", + "icon": { + "type": "Image", + "url": "https://mastodon.social/avatar.jpg" + }, + "summary": "Software developer from Berlin", + "activitypub_id": "https://mastodon.social/users/johndoe" +} +``` + +### Pagination + +Paginated responses include headers: +- `X-WP-Total`: Total number of items +- `X-WP-TotalPages`: Total number of pages + +## JavaScript Entity + +### Registration + +The actors entity is registered in `src/internal/actors-entity/index.js`: + +```javascript +registerEntityType( { + kind: 'activitypub/v1', + name: 'actor', + baseURL: '/wp-json/activitypub/v1/internal/actors', + // ... configuration +} ); +``` + +### Usage Examples + +#### 1. Fetch Followers + +```javascript +import { useEntityRecords } from '@wordpress/core-data'; + +function UserFollowers( { userId } ) { + const { records: followers, isResolving, totalItems } = useEntityRecords( + 'activitypub/v1', + 'actor', + { + relationship: 'followers', + user_id: userId, + per_page: 10, + } + ); + + if ( isResolving ) { + return

Loading...

; + } + + return ( +
+

Followers ({ totalItems })

+ +
+ ); +} +``` + +#### 2. Fetch Following + +```javascript +const { records: following } = useEntityRecords( + 'activitypub/v1', + 'actor', + { + relationship: 'following', + user_id: userId, + } +); +``` + +#### 3. Fetch Local Actors Only + +```javascript +const { records: localActors } = useEntityRecords( + 'activitypub/v1', + 'actor', + { + type: 'user', // Only user actors + } +); +``` + +#### 4. Search Followers + +```javascript +const { records: searchResults } = useEntityRecords( + 'activitypub/v1', + 'actor', + { + relationship: 'followers', + user_id: userId, + search: 'mastodon', + } +); +``` + +#### 5. Paginated Followers + +```javascript +const [ page, setPage ] = useState( 1 ); + +const { records, totalPages } = useEntityRecords( + 'activitypub/v1', + 'actor', + { + relationship: 'followers', + user_id: userId, + page, + per_page: 20, + } +); +``` + +## Using in Followers Block + +The followers block can be updated to use the entity API like this: + +```javascript +// Instead of apiFetch +const { records: followers, isResolving, totalItems, totalPages } = useEntityRecords( + 'activitypub/v1', + 'actor', + { + relationship: 'followers', + user_id: userId, + per_page, + page, + order, + } +); +``` + +**Benefits**: +1. Automatic caching through WordPress Core Data +2. Consistent API across all components +3. Built-in pagination support +4. Standard WordPress patterns +5. Easy to extend with additional filters + +## Files Created/Modified + +### Created + +1. `includes/rest/internal/class-actors-controller.php` - Internal actors REST controller +2. `src/internal/actors-entity/index.js` - Entity registration +3. `src/internal/actors-entity/block.json` - Build configuration +4. `src/internal/actors-entity/README.md` - Documentation +5. `src/internal/actors-entity/example.js` - Usage examples +6. `src/internal/actors-entity/example-followers.js` - Followers-specific examples + +### Modified + +1. `activitypub.php` - Added REST route registration +2. `includes/class-blocks.php` - Added entity script enqueuing + +## Features + +### Actor Types + +- `user` - WordPress users with ActivityPub enabled +- `blog` - Blog actor (ID: 0) +- `application` - Application actor (ID: -1) +- `remote` - Remote actors (followers/following) + +### Relationships + +- `followers` - Get followers for a user +- `following` - Get users/actors that a user is following + +### Filtering + +- By type +- By relationship +- By search term +- With pagination +- With custom sort order + +## Authentication + +All endpoints require authentication: +- User must be logged in +- Suitable for block editor and admin interfaces +- Not for public API consumption + +## Testing + +### Test REST API + +```bash +# Start development environment +npm run env-start + +# Test endpoints (with valid WordPress cookie) +curl -X GET "http://localhost/wp-json/activitypub/v1/internal/actors" \ + --cookie "wordpress_logged_in_..." + +# Test followers endpoint +curl -X GET "http://localhost/wp-json/activitypub/v1/internal/actors?relationship=followers&user_id=0" \ + --cookie "wordpress_logged_in_..." +``` + +### Test in Browser Console + +```javascript +// Fetch all actors +wp.data.select('core').getEntityRecords('activitypub/v1', 'actor'); + +// Fetch followers for blog actor +wp.data.select('core').getEntityRecords('activitypub/v1', 'actor', { + relationship: 'followers', + user_id: 0 +}); + +// Fetch following +wp.data.select('core').getEntityRecords('activitypub/v1', 'actor', { + relationship: 'following', + user_id: 1 +}); +``` + +## Next Steps + +1. Update the followers block to use the entity API +2. Update the following block (if exists) to use the entity API +3. Add unit tests for the REST controller +4. Add E2E tests for the entity integration +5. Consider adding write operations (follow/unfollow) + +## Related Documentation + +- [WordPress Core Data API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/) +- [WordPress REST API](https://developer.wordpress.org/rest-api/) +- [ActivityPub Actors Collection](../../includes/collection/class-actors.php) +- [ActivityPub Followers Collection](../../includes/collection/class-followers.php) +- [ActivityPub Following Collection](../../includes/collection/class-following.php) diff --git a/activitypub.php b/activitypub.php index be75b770de..e09e5f78a0 100644 --- a/activitypub.php +++ b/activitypub.php @@ -51,6 +51,8 @@ function rest_init() { ( new Rest\Following_Controller() )->register_routes(); ( new Rest\Inbox_Controller() )->register_routes(); ( new Rest\Interaction_Controller() )->register_routes(); + ( new Rest\Internal\Actors_Controller() )->register_routes(); + ( new Rest\Internal\Posts_Controller() )->register_routes(); ( new Rest\Moderators_Controller() )->register_routes(); ( new Rest\Outbox_Controller() )->register_routes(); ( new Rest\Post_Controller() )->register_routes(); diff --git a/build/actors-entity/block.json b/build/actors-entity/block.json new file mode 100644 index 0000000000..a04705d027 --- /dev/null +++ b/build/actors-entity/block.json @@ -0,0 +1,8 @@ +{ + "name": "actors-entity", + "title": "Actors Entity: Registers actors as WordPress Core Data entities", + "category": "widgets", + "icon": "admin-users", + "keywords": [], + "editorScript": "file:./index.js" +} \ No newline at end of file diff --git a/build/actors-entity/index.asset.php b/build/actors-entity/index.asset.php new file mode 100644 index 0000000000..1dcb903101 --- /dev/null +++ b/build/actors-entity/index.asset.php @@ -0,0 +1 @@ + array('wp-core-data', 'wp-data'), 'version' => 'ca8f4a84f3b3a3dfff48'); diff --git a/build/actors-entity/index.js b/build/actors-entity/index.js new file mode 100644 index 0000000000..1577ba7df0 --- /dev/null +++ b/build/actors-entity/index.js @@ -0,0 +1 @@ +(()=>{"use strict";const t=window.wp.data,a=window.wp.coreData;(()=>{const{registerEntityType:i}=(0,t.dispatch)(a.store);i({kind:"activitypub/v1",name:"actor",label:"Actor",plural:"Actors",baseURL:"/wp-json/activitypub/v1/internal/actors",key:"id",transientEdits:{name:!0,preferred_username:!0,url:!0,icon:!0,summary:!0,activitypub_id:!0},supportsPagination:!1})})()})(); \ No newline at end of file diff --git a/build/editor-plugin/plugin.asset.php b/build/editor-plugin/plugin.asset.php index 4ebfedf087..2bc42a8252 100644 --- a/build/editor-plugin/plugin.asset.php +++ b/build/editor-plugin/plugin.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-components', 'wp-core-data', 'wp-data', 'wp-edit-post', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'c0707a36ca43854f47b7'); + array('react-jsx-runtime', 'wp-components', 'wp-core-data', 'wp-data', 'wp-edit-post', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '4371fba4c154ea1d6303'); diff --git a/build/editor-plugin/plugin.js b/build/editor-plugin/plugin.js index 0175d6f6cf..b6ce964b64 100644 --- a/build/editor-plugin/plugin.js +++ b/build/editor-plugin/plugin.js @@ -1 +1 @@ -(()=>{"use strict";const t=window.wp.editor,e=window.wp.editPost,i=window.wp.plugins,n=window.wp.components,a=window.wp.element;var o=(0,a.forwardRef)(({icon:t,size:e=24,...i},n)=>(0,a.cloneElement)(t,{width:e,height:e,...i,ref:n}));const l=window.ReactJSXRuntime,c=window.wp.primitives;var s=(0,l.jsx)(c.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(c.Path,{d:"M12 4c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8Zm6.5 8c0 .6 0 1.2-.2 1.8h-2.7c0-.6.2-1.1.2-1.8s0-1.2-.2-1.8h2.7c.2.6.2 1.1.2 1.8Zm-.9-3.2h-2.4c-.3-.9-.7-1.8-1.1-2.4-.1-.2-.2-.4-.3-.5 1.6.5 3 1.6 3.8 3ZM12.8 17c-.3.5-.6 1-.8 1.3-.2-.3-.5-.8-.8-1.3-.3-.5-.6-1.1-.8-1.7h3.3c-.2.6-.5 1.2-.8 1.7Zm-2.9-3.2c-.1-.6-.2-1.1-.2-1.8s0-1.2.2-1.8H14c.1.6.2 1.1.2 1.8s0 1.2-.2 1.8H9.9ZM11.2 7c.3-.5.6-1 .8-1.3.2.3.5.8.8 1.3.3.5.6 1.1.8 1.7h-3.3c.2-.6.5-1.2.8-1.7Zm-1-1.2c-.1.2-.2.3-.3.5-.4.7-.8 1.5-1.1 2.4H6.4c.8-1.4 2.2-2.5 3.8-3Zm-1.8 8H5.7c-.2-.6-.2-1.1-.2-1.8s0-1.2.2-1.8h2.7c0 .6-.2 1.1-.2 1.8s0 1.2.2 1.8Zm-2 1.4h2.4c.3.9.7 1.8 1.1 2.4.1.2.2.4.3.5-1.6-.5-3-1.6-3.8-3Zm7.4 3c.1-.2.2-.3.3-.5.4-.7.8-1.5 1.1-2.4h2.4c-.8 1.4-2.2 2.5-3.8 3Z"})}),u=(0,l.jsx)(c.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(c.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),r=(0,l.jsx)(c.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(c.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})});const p=window.wp.data,v=window.wp.coreData,w=window.wp.url,_=window.wp.i18n,h=(t,e)=>t?.activitypub_content_visibility?t.activitypub_content_visibility:"federated"===t?.activitypub_status?"public":e&&new Date(e).getTime(){const i=(0,p.useSelect)(e=>e(t.store).getCurrentPostType(),[]),[a,r]=(0,v.useEntityProp)("postType",i,"meta"),w=(0,p.useSelect)(e=>e(t.store).getCurrentPost().date,[]);if("wp_block"===i)return null;const b=(0,l.jsx)(c.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(c.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 18.5A6.5 6.5 0 0 1 6.93 7.931l9.139 9.138A6.473 6.473 0 0 1 12 18.5Zm5.123-2.498a6.5 6.5 0 0 0-9.124-9.124l9.124 9.124ZM4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z"})}),d={verticalAlign:"middle",gap:"4px",justifyContent:"start",display:"inline-flex",alignItems:"center"},m=(t,e,i)=>(0,l.jsx)(n.Tooltip,{text:i,children:(0,l.jsxs)(n.__experimentalText,{style:d,children:[(0,l.jsx)(o,{icon:t}),e]})}),y=t.PluginDocumentSettingPanel||e.PluginDocumentSettingPanel;return(0,l.jsxs)(y,{name:"activitypub",className:"block-editor-block-inspector",title:(0,_.__)("Fediverse ⁂","activitypub"),children:[(0,l.jsx)(n.TextControl,{label:(0,_.__)("Content Warning","activitypub"),value:a?.activitypub_content_warning,onChange:t=>{r({...a,activitypub_content_warning:t})},placeholder:(0,_.__)("Optional content warning","activitypub"),help:(0,_.__)("Content warnings do not change the content on your site, only in the fediverse.","activitypub"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,l.jsx)(n.RangeControl,{label:(0,_.__)("Maximum Image Attachments","activitypub"),value:a?.activitypub_max_image_attachments,onChange:t=>{r({...a,activitypub_max_image_attachments:t})},min:0,max:10,help:(0,_.__)("Maximum number of image attachments to include when sharing to the fediverse.","activitypub"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,l.jsx)(n.RadioControl,{label:(0,_.__)("Visibility","activitypub"),help:(0,_.__)("This adjusts the visibility of a post in the fediverse, but note that it won't affect how the post appears on the blog.","activitypub"),selected:h(a,w),options:[{label:m(s,(0,_.__)("Public","activitypub"),(0,_.__)("Post will be visible to everyone and appear in public timelines.","activitypub")),value:"public"},{label:m(u,(0,_.__)("Quiet public","activitypub"),(0,_.__)("Post will be visible to everyone but will not appear in public timelines.","activitypub")),value:"quiet_public"},{label:m(b,(0,_.__)("Do not federate","activitypub"),(0,_.__)("Post will not be shared to the Fediverse.","activitypub")),value:"local"}],onChange:t=>{r({...a,activitypub_content_visibility:t})},className:"activitypub-visibility"}),(0,l.jsx)(n.SelectControl,{label:(0,_.__)("Who can quote this post?","activitypub"),help:(0,_.__)("Quoting allows others to cite your post while adding their own commentary.","activitypub"),value:a?.activitypub_interaction_policy_quote,options:[{label:(0,_.__)("Anyone","activitypub"),value:"anyone"},{label:(0,_.__)("Followers only","activitypub"),value:"followers"},{label:(0,_.__)("Just me","activitypub"),value:"me"}],onChange:t=>{r({...a,activitypub_interaction_policy_quote:t})},__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0})]})}}),(0,i.registerPlugin)("activitypub-editor-preview",{render:()=>{const e=(0,p.useSelect)(e=>e(t.store).getCurrentPost().status,[]);return(0,l.jsx)(l.Fragment,{children:t.PluginPreviewMenuItem?(0,l.jsx)(t.PluginPreviewMenuItem,{onClick:()=>{const e=(0,p.select)(t.store).getEditedPostPreviewLink(),i=(0,w.addQueryArgs)(e,{activitypub:"true"});window.open(i,"_blank")},icon:r,disabled:"auto-draft"===e,children:(0,_.__)("Fediverse preview ⁂","activitypub")}):null})}})})(); \ No newline at end of file +(()=>{"use strict";const t=window.wp.editor,e=window.wp.editPost,i=window.wp.plugins,n=window.wp.components,a=window.wp.element;var o=(0,a.forwardRef)(({icon:t,size:e=24,...i},n)=>(0,a.cloneElement)(t,{width:e,height:e,...i,ref:n}));const l=window.wp.primitives,c=window.ReactJSXRuntime;var s=(0,c.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,c.jsx)(l.Path,{d:"M12 4c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8Zm6.5 8c0 .6 0 1.2-.2 1.8h-2.7c0-.6.2-1.1.2-1.8s0-1.2-.2-1.8h2.7c.2.6.2 1.1.2 1.8Zm-.9-3.2h-2.4c-.3-.9-.7-1.8-1.1-2.4-.1-.2-.2-.4-.3-.5 1.6.5 3 1.6 3.8 3ZM12.8 17c-.3.5-.6 1-.8 1.3-.2-.3-.5-.8-.8-1.3-.3-.5-.6-1.1-.8-1.7h3.3c-.2.6-.5 1.2-.8 1.7Zm-2.9-3.2c-.1-.6-.2-1.1-.2-1.8s0-1.2.2-1.8H14c.1.6.2 1.1.2 1.8s0 1.2-.2 1.8H9.9ZM11.2 7c.3-.5.6-1 .8-1.3.2.3.5.8.8 1.3.3.5.6 1.1.8 1.7h-3.3c.2-.6.5-1.2.8-1.7Zm-1-1.2c-.1.2-.2.3-.3.5-.4.7-.8 1.5-1.1 2.4H6.4c.8-1.4 2.2-2.5 3.8-3Zm-1.8 8H5.7c-.2-.6-.2-1.1-.2-1.8s0-1.2.2-1.8h2.7c0 .6-.2 1.1-.2 1.8s0 1.2.2 1.8Zm-2 1.4h2.4c.3.9.7 1.8 1.1 2.4.1.2.2.4.3.5-1.6-.5-3-1.6-3.8-3Zm7.4 3c.1-.2.2-.3.3-.5.4-.7.8-1.5 1.1-2.4h2.4c-.8 1.4-2.2 2.5-3.8 3Z"})}),u=(0,c.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,c.jsx)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),r=(0,c.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,c.jsx)(l.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})});const p=window.wp.data,v=window.wp.coreData,w=window.wp.url,_=window.wp.i18n,h=(t,e)=>t?.activitypub_content_visibility?t.activitypub_content_visibility:"federated"===t?.activitypub_status?"public":e&&new Date(e).getTime(){const i=(0,p.useSelect)(e=>e(t.store).getCurrentPostType(),[]),[a,r]=(0,v.useEntityProp)("postType",i,"meta"),w=(0,p.useSelect)(e=>e(t.store).getCurrentPost().date,[]);if("wp_block"===i)return null;const b=(0,c.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,c.jsx)(l.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 18.5A6.5 6.5 0 0 1 6.93 7.931l9.139 9.138A6.473 6.473 0 0 1 12 18.5Zm5.123-2.498a6.5 6.5 0 0 0-9.124-9.124l9.124 9.124ZM4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z"})}),d={verticalAlign:"middle",gap:"4px",justifyContent:"start",display:"inline-flex",alignItems:"center"},m=(t,e,i)=>(0,c.jsx)(n.Tooltip,{text:i,children:(0,c.jsxs)(n.__experimentalText,{style:d,children:[(0,c.jsx)(o,{icon:t}),e]})}),y=t.PluginDocumentSettingPanel||e.PluginDocumentSettingPanel;return(0,c.jsxs)(y,{name:"activitypub",className:"block-editor-block-inspector",title:(0,_.__)("Fediverse ⁂","activitypub"),children:[(0,c.jsx)(n.TextControl,{label:(0,_.__)("Content Warning","activitypub"),value:a?.activitypub_content_warning,onChange:t=>{r({...a,activitypub_content_warning:t})},placeholder:(0,_.__)("Optional content warning","activitypub"),help:(0,_.__)("Content warnings do not change the content on your site, only in the fediverse.","activitypub"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,c.jsx)(n.RangeControl,{label:(0,_.__)("Maximum Image Attachments","activitypub"),value:a?.activitypub_max_image_attachments,onChange:t=>{r({...a,activitypub_max_image_attachments:t})},min:0,max:10,help:(0,_.__)("Maximum number of image attachments to include when sharing to the fediverse.","activitypub"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,c.jsx)(n.RadioControl,{label:(0,_.__)("Visibility","activitypub"),help:(0,_.__)("This adjusts the visibility of a post in the fediverse, but note that it won't affect how the post appears on the blog.","activitypub"),selected:h(a,w),options:[{label:m(s,(0,_.__)("Public","activitypub"),(0,_.__)("Post will be visible to everyone and appear in public timelines.","activitypub")),value:"public"},{label:m(u,(0,_.__)("Quiet public","activitypub"),(0,_.__)("Post will be visible to everyone but will not appear in public timelines.","activitypub")),value:"quiet_public"},{label:m(b,(0,_.__)("Do not federate","activitypub"),(0,_.__)("Post will not be shared to the Fediverse.","activitypub")),value:"local"}],onChange:t=>{r({...a,activitypub_content_visibility:t})},className:"activitypub-visibility"}),(0,c.jsx)(n.SelectControl,{label:(0,_.__)("Who can quote this post?","activitypub"),help:(0,_.__)("Quoting allows others to cite your post while adding their own commentary.","activitypub"),value:a?.activitypub_interaction_policy_quote,options:[{label:(0,_.__)("Anyone","activitypub"),value:"anyone"},{label:(0,_.__)("Followers only","activitypub"),value:"followers"},{label:(0,_.__)("Just me","activitypub"),value:"me"}],onChange:t=>{r({...a,activitypub_interaction_policy_quote:t})},__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0})]})}}),(0,i.registerPlugin)("activitypub-editor-preview",{render:()=>{const e=(0,p.useSelect)(e=>e(t.store).getCurrentPost().status,[]);return(0,c.jsx)(c.Fragment,{children:t.PluginPreviewMenuItem?(0,c.jsx)(t.PluginPreviewMenuItem,{onClick:()=>{const e=(0,p.select)(t.store).getEditedPostPreviewLink(),i=(0,w.addQueryArgs)(e,{activitypub:"true"});window.open(i,"_blank")},icon:r,disabled:"auto-draft"===e,children:(0,_.__)("Fediverse preview ⁂","activitypub")}):null})}})})(); \ No newline at end of file diff --git a/build/follow-me/index.asset.php b/build/follow-me/index.asset.php index ea94ba2682..8c30a0aa73 100644 --- a/build/follow-me/index.asset.php +++ b/build/follow-me/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '71359709005ebbf77285'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '3c6293a99ee2025acf0a'); diff --git a/build/follow-me/index.js b/build/follow-me/index.js index 0b841d7e3e..47ff3ffb4f 100644 --- a/build/follow-me/index.js +++ b/build/follow-me/index.js @@ -1,2 +1,2 @@ -(()=>{var e,t={768:(e,t,r)=>{"use strict";const o=window.wp.blocks,s=window.ReactJSXRuntime,i=window.wp.primitives;var n=(0,s.jsx)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,s.jsx)(i.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),a=r(942),l=r.n(a);const c=window.wp.blockEditor,u=window.wp.i18n,p={html:!1,color:{gradients:!0,link:!0,__experimentalDefaultControls:{background:!0,text:!0,link:!0}},__experimentalBorder:{radius:!0,width:!0,color:!0,style:!0},typography:{fontSize:!0,__experimentalDefaultControls:{fontSize:!0}}},d=p;function v({buttonOnly:e=!1,className:t="",...r}){return r.className=l()(t,e?"is-style-button-only":"is-style-default"),r}const f={attributes:{buttonOnly:{type:"boolean",default:!1},buttonText:{type:"string",default:"Follow"},selectedUser:{type:"string",default:"blog"}},supports:p,isEligible:({buttonText:e,buttonOnly:t})=>!!e||!!t,migrate({buttonText:e,...t}){const r=(0,o.createBlock)("core/button",{text:e});return[v(t),[r]]}},b={attributes:{selectedUser:{type:"string",default:"blog"},buttonOnly:{type:"boolean",default:!1}},supports:d,isEligible:({buttonOnly:e})=>!!e,migrate:v,save(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,s.jsx)("div",{...t})}},h=[{attributes:{selectedUser:{type:"string",default:"blog"}},supports:d,isEligible:(e,t)=>1===t.length&&"button"===t[0].attributes.tagName,migrate(e,t){var r;const{tagName:s,...i}=t[0].attributes,n=null!==(r=t[0].originalContent.replace(/<[^>]*>/g,""))&&void 0!==r?r:(0,u.__)("Follow","activitypub");return[e,[(0,o.createBlock)("core/button",{...i,text:n})]]},save(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,s.jsx)("div",{...t})}},b,f],y=window.wp.apiFetch;var w=r.n(y);const m=window.wp.data,g=window.wp.coreData,_=window.wp.components,x=window.wp.element;function j(){return window._activityPubOptions||{}}function k({name:e}){const{enabled:t}=j(),r=t?.blog?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),o=(0,u.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ -(0,u.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,r).trim();return(0,s.jsx)(_.Card,{children:(0,s.jsx)(_.CardBody,{children:(0,x.createInterpolateElement)(o,{strong:(0,s.jsx)("strong",{})})})})}const B={avatar:"https://secure.gravatar.com/avatar/default?s=120",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#",image:{url:""},summary:""};function S(e){if(!e)return B;const t={...B,...e};return t.avatar=t?.icon?.url,t.webfinger&&!t.webfinger.startsWith("@")&&(t.webfinger="@"+t.webfinger),t}function O({profile:e,className:t,innerBlocksProps:r}){const{webfinger:o,avatar:i,name:n,image:a,summary:l,followersCount:c,postsCount:p}=e,d=t&&t.includes("is-style-button-only"),v={posts:p||0,followers:c||0};return(0,s.jsxs)("div",{className:"activitypub-profile",children:[!d&&a?.url&&(0,s.jsx)("div",{className:"activitypub-profile__header",style:{backgroundImage:`url(${a.url})`}}),(0,s.jsxs)("div",{className:"activitypub-profile__body",children:[!d&&(0,s.jsx)("img",{className:"activitypub-profile__avatar",src:i,alt:n}),(0,s.jsxs)("div",{className:"activitypub-profile__content",children:[!d&&(0,s.jsxs)("div",{className:"activitypub-profile__info",children:[(0,s.jsx)("div",{className:"activitypub-profile__name",children:n}),(0,s.jsx)("div",{className:"activitypub-profile__handle",children:o})]}),(0,s.jsx)("div",{...r}),!d&&(0,s.jsx)("div",{className:"activitypub-profile__bio",dangerouslySetInnerHTML:{__html:l}}),!d&&(0,s.jsx)("div",{className:"activitypub-profile__stats",children:Object.entries(v).map(([e,t])=>(0,s.jsxs)("div",{children:[(0,s.jsx)("strong",{children:t})," ","posts"===e?(0,u._n)("post","posts",t,"activitypub"):"followers"===e?(0,u._n)("follower","followers",t,"activitypub"):(0,u._n)("following","following",t,"activitypub")]},e))})]})]})]})}const N=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/follow-me","apiVersion":3,"version":"2.2.0","title":"Follow me on the Fediverse","category":"widgets","description":"Display your Fediverse profile so that visitors can follow you.","textdomain":"activitypub","icon":"groups","example":{"attributes":{"className":"is-style-default"}},"supports":{"html":false,"interactivity":true,"color":{"gradients":true,"link":true,"__experimentalDefaultControls":{"background":true,"text":true,"link":true}},"__experimentalBorder":{"radius":true,"width":true,"color":true,"style":true},"shadow":true,"typography":{"fontSize":true,"__experimentalDefaultControls":{"fontSize":true}},"innerBlocks":{"allowedBlocks":["core/button"]}},"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"button-only","label":"Button"},{"name":"profile","label":"Profile"}],"attributes":{"selectedUser":{"type":"string","default":"blog"}},"usesContext":["postType","postId"],"editorScript":"file:./index.js","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":"file:./style-index.css","render":"file:./render.php"}');(0,o.registerBlockType)(N,{deprecated:h,edit:function({attributes:e,setAttributes:t,context:{postType:r,postId:o}}){const i=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),n=function({withInherit:e=!1}){const{enabled:t,namespace:r}=j(),[o,s]=(0,x.useState)(!1),{fetchedUsers:i,isLoadingUsers:n}=(0,m.useSelect)(e=>{const{getUsers:r,getIsResolving:o}=e("core");return{fetchedUsers:t?.users?r({capabilities:"activitypub"}):null,isLoadingUsers:!!t?.users&&o("getUsers",[{capabilities:"activitypub"}])}},[]),a=(0,m.useSelect)(e=>i||n?null:e("core").getCurrentUser(),[i,n]);(0,x.useEffect)(()=>{i||n||!a||w()({path:`/${r}/actors/${a.id}`,method:"HEAD",headers:{Accept:"application/activity+json"},parse:!1}).then(()=>s(!0)).catch(()=>s(!1))},[i,n,a]);const l=i||(a&&o?[{id:a.id,name:a.name}]:[]);return(0,x.useMemo)(()=>{if(!l.length)return[];const r=[];return t?.blog&&i&&r.push({label:(0,u.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&i&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),l.reduce((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e),r)},[l])}({withInherit:!0}),{selectedUser:a,className:l="is-style-default"}=e,p="inherit"===a,[d,v]=(0,x.useState)(S(B)),f="blog"===a?0:a,b=[["core/button",{text:(0,u.__)("Follow","activitypub")}]],h=(0,c.useInnerBlocksProps)({},{allowedBlocks:["core/button"],template:b,templateLock:!1,renderAppender:!1}),y=(0,m.useSelect)(e=>{const{getEditedEntityRecord:t}=e(g.store),s=t("postType",r,o)?.author;return null!=s?s:null},[r,o]);return(0,x.useEffect)(()=>{if(p&&!y)return;const e=p?y:f;(function(e){const{namespace:t}=j(),r={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return w()(r)})(e).then(t=>{if(v(S(t)),t.followers)try{const{pathname:e}=new URL(t.followers);w()({path:e.replace("wp-json/","")}).then(({totalItems:e=0})=>{v(t=>({...t,followersCount:e}))}).catch(()=>{})}catch(e){}e?w()({path:`/wp/v2/users/${e}/?context=activitypub`}).then(({post_count:e})=>{v(t=>({...t,postsCount:e}))}).catch(()=>{}):w()({path:"/wp/v2/posts",method:"HEAD",parse:!1}).then(e=>{const t=e.headers.get("X-WP-Total");v(e=>({...e,postsCount:t}))}).catch(()=>{})}).catch(()=>{})},[f,y,p]),(0,x.useEffect)(()=>{n.length&&(n.find(({value:e})=>e===a)||t({selectedUser:n[0].value}))},[a,n]),(0,s.jsxs)("div",{...i,children:[(0,s.jsx)(c.InspectorControls,{children:n.length>1&&(0,s.jsx)(_.PanelBody,{title:(0,u.__)("Follow Me Options","activitypub"),children:(0,s.jsx)(_.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:e.selectedUser,options:n,onChange:e=>t({selectedUser:e}),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0})})},"activitypub-follow-me"),p&&!y?(0,s.jsx)(k,{name:(0,u.__)("Follow Me","activitypub")}):(0,s.jsx)(O,{profile:d,className:l,innerBlocksProps:h})]})},icon:n,save:function(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,s.jsx)("div",{...t})}})},942:(e,t)=>{var r;!function(){"use strict";var o={}.hasOwnProperty;function s(){for(var e="",t=0;t{if(!r){var n=1/0;for(u=0;u=i)&&Object.keys(o.O).every(e=>o.O[e](r[l]))?r.splice(l--,1):(a=!1,i0&&e[u-1][2]>i;u--)e[u]=e[u-1];e[u]=[r,s,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={338:0,870:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var s,i,[n,a,l]=r,c=0;if(n.some(t=>0!==e[t])){for(s in a)o.o(a,s)&&(o.m[s]=a[s]);if(l)var u=l(o)}for(t&&t(r);co(768));s=o.O(s)})(); \ No newline at end of file +(()=>{var e,t={768:(e,t,r)=>{"use strict";const o=window.wp.blocks,s=window.wp.primitives,i=window.ReactJSXRuntime;var n=(0,i.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,i.jsx)(s.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),a=r(942),l=r.n(a);const c=window.wp.blockEditor,u=window.wp.i18n,p={html:!1,color:{gradients:!0,link:!0,__experimentalDefaultControls:{background:!0,text:!0,link:!0}},__experimentalBorder:{radius:!0,width:!0,color:!0,style:!0},typography:{fontSize:!0,__experimentalDefaultControls:{fontSize:!0}}},d=p;function v({buttonOnly:e=!1,className:t="",...r}){return r.className=l()(t,e?"is-style-button-only":"is-style-default"),r}const f={attributes:{buttonOnly:{type:"boolean",default:!1},buttonText:{type:"string",default:"Follow"},selectedUser:{type:"string",default:"blog"}},supports:p,isEligible:({buttonText:e,buttonOnly:t})=>!!e||!!t,migrate({buttonText:e,...t}){const r=(0,o.createBlock)("core/button",{text:e});return[v(t),[r]]}},b={attributes:{selectedUser:{type:"string",default:"blog"},buttonOnly:{type:"boolean",default:!1}},supports:d,isEligible:({buttonOnly:e})=>!!e,migrate:v,save(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,i.jsx)("div",{...t})}},h=[{attributes:{selectedUser:{type:"string",default:"blog"}},supports:d,isEligible:(e,t)=>1===t.length&&"button"===t[0].attributes.tagName,migrate(e,t){var r;const{tagName:s,...i}=t[0].attributes,n=null!==(r=t[0].originalContent.replace(/<[^>]*>/g,""))&&void 0!==r?r:(0,u.__)("Follow","activitypub");return[e,[(0,o.createBlock)("core/button",{...i,text:n})]]},save(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,i.jsx)("div",{...t})}},b,f],y=window.wp.apiFetch;var w=r.n(y);const m=window.wp.data,g=window.wp.coreData,_=window.wp.components,x=window.wp.element;function j(){return window._activityPubOptions||{}}function k({name:e}){const{enabled:t}=j(),r=t?.blog?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),o=(0,u.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ +(0,u.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,r).trim();return(0,i.jsx)(_.Card,{children:(0,i.jsx)(_.CardBody,{children:(0,x.createInterpolateElement)(o,{strong:(0,i.jsx)("strong",{})})})})}const B={avatar:"https://secure.gravatar.com/avatar/default?s=120",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#",image:{url:""},summary:""};function S(e){if(!e)return B;const t={...B,...e};return t.avatar=t?.icon?.url,t.webfinger&&!t.webfinger.startsWith("@")&&(t.webfinger="@"+t.webfinger),t}function O({profile:e,className:t,innerBlocksProps:r}){const{webfinger:o,avatar:s,name:n,image:a,summary:l,followersCount:c,postsCount:p}=e,d=t&&t.includes("is-style-button-only"),v={posts:p||0,followers:c||0};return(0,i.jsxs)("div",{className:"activitypub-profile",children:[!d&&a?.url&&(0,i.jsx)("div",{className:"activitypub-profile__header",style:{backgroundImage:`url(${a.url})`}}),(0,i.jsxs)("div",{className:"activitypub-profile__body",children:[!d&&(0,i.jsx)("img",{className:"activitypub-profile__avatar",src:s,alt:n}),(0,i.jsxs)("div",{className:"activitypub-profile__content",children:[!d&&(0,i.jsxs)("div",{className:"activitypub-profile__info",children:[(0,i.jsx)("div",{className:"activitypub-profile__name",children:n}),(0,i.jsx)("div",{className:"activitypub-profile__handle",children:o})]}),(0,i.jsx)("div",{...r}),!d&&(0,i.jsx)("div",{className:"activitypub-profile__bio",dangerouslySetInnerHTML:{__html:l}}),!d&&(0,i.jsx)("div",{className:"activitypub-profile__stats",children:Object.entries(v).map(([e,t])=>(0,i.jsxs)("div",{children:[(0,i.jsx)("strong",{children:t})," ","posts"===e?(0,u._n)("post","posts",t,"activitypub"):"followers"===e?(0,u._n)("follower","followers",t,"activitypub"):(0,u._n)("following","following",t,"activitypub")]},e))})]})]})]})}const N=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/follow-me","apiVersion":3,"version":"2.2.0","title":"Follow me on the Fediverse","category":"widgets","description":"Display your Fediverse profile so that visitors can follow you.","textdomain":"activitypub","icon":"groups","example":{"attributes":{"className":"is-style-default"}},"supports":{"html":false,"interactivity":true,"color":{"gradients":true,"link":true,"__experimentalDefaultControls":{"background":true,"text":true,"link":true}},"__experimentalBorder":{"radius":true,"width":true,"color":true,"style":true},"shadow":true,"typography":{"fontSize":true,"__experimentalDefaultControls":{"fontSize":true}},"innerBlocks":{"allowedBlocks":["core/button"]}},"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"button-only","label":"Button"},{"name":"profile","label":"Profile"}],"attributes":{"selectedUser":{"type":"string","default":"blog"}},"usesContext":["postType","postId"],"editorScript":"file:./index.js","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":"file:./style-index.css","render":"file:./render.php"}');(0,o.registerBlockType)(N,{deprecated:h,edit:function({attributes:e,setAttributes:t,context:{postType:r,postId:o}}){const s=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),n=function({withInherit:e=!1}){const{enabled:t,namespace:r}=j(),[o,s]=(0,x.useState)(!1),{fetchedUsers:i,isLoadingUsers:n}=(0,m.useSelect)(e=>{const{getUsers:r,getIsResolving:o}=e("core");return{fetchedUsers:t?.users?r({capabilities:"activitypub"}):null,isLoadingUsers:!!t?.users&&o("getUsers",[{capabilities:"activitypub"}])}},[]),a=(0,m.useSelect)(e=>i||n?null:e("core").getCurrentUser(),[i,n]);(0,x.useEffect)(()=>{i||n||!a||w()({path:`/${r}/actors/${a.id}`,method:"HEAD",headers:{Accept:"application/activity+json"},parse:!1}).then(()=>s(!0)).catch(()=>s(!1))},[i,n,a]);const l=i||(a&&o?[{id:a.id,name:a.name}]:[]);return(0,x.useMemo)(()=>{if(!l.length)return[];const r=[];return t?.blog&&i&&r.push({label:(0,u.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&i&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),l.reduce((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e),r)},[l])}({withInherit:!0}),{selectedUser:a,className:l="is-style-default"}=e,p="inherit"===a,[d,v]=(0,x.useState)(S(B)),f="blog"===a?0:a,b=[["core/button",{text:(0,u.__)("Follow","activitypub")}]],h=(0,c.useInnerBlocksProps)({},{allowedBlocks:["core/button"],template:b,templateLock:!1,renderAppender:!1}),y=(0,m.useSelect)(e=>{const{getEditedEntityRecord:t}=e(g.store),s=t("postType",r,o)?.author;return null!=s?s:null},[r,o]);return(0,x.useEffect)(()=>{if(p&&!y)return;const e=p?y:f;(function(e){const{namespace:t}=j(),r={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return w()(r)})(e).then(t=>{if(v(S(t)),t.followers)try{const{pathname:e}=new URL(t.followers);w()({path:e.replace("wp-json/","")}).then(({totalItems:e=0})=>{v(t=>({...t,followersCount:e}))}).catch(()=>{})}catch(e){}e?w()({path:`/wp/v2/users/${e}/?context=activitypub`}).then(({post_count:e})=>{v(t=>({...t,postsCount:e}))}).catch(()=>{}):w()({path:"/wp/v2/posts",method:"HEAD",parse:!1}).then(e=>{const t=e.headers.get("X-WP-Total");v(e=>({...e,postsCount:t}))}).catch(()=>{})}).catch(()=>{})},[f,y,p]),(0,x.useEffect)(()=>{n.length&&(n.find(({value:e})=>e===a)||t({selectedUser:n[0].value}))},[a,n]),(0,i.jsxs)("div",{...s,children:[(0,i.jsx)(c.InspectorControls,{children:n.length>1&&(0,i.jsx)(_.PanelBody,{title:(0,u.__)("Follow Me Options","activitypub"),children:(0,i.jsx)(_.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:e.selectedUser,options:n,onChange:e=>t({selectedUser:e}),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0})})},"activitypub-follow-me"),p&&!y?(0,i.jsx)(k,{name:(0,u.__)("Follow Me","activitypub")}):(0,i.jsx)(O,{profile:d,className:l,innerBlocksProps:h})]})},icon:n,save:function(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,i.jsx)("div",{...t})}})},942:(e,t)=>{var r;!function(){"use strict";var o={}.hasOwnProperty;function s(){for(var e="",t=0;t{if(!r){var n=1/0;for(u=0;u=i)&&Object.keys(o.O).every(e=>o.O[e](r[l]))?r.splice(l--,1):(a=!1,i0&&e[u-1][2]>i;u--)e[u]=e[u-1];e[u]=[r,s,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={338:0,870:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var s,i,[n,a,l]=r,c=0;if(n.some(t=>0!==e[t])){for(s in a)o.o(a,s)&&(o.m[s]=a[s]);if(l)var u=l(o)}for(t&&t(r);co(768));s=o.O(s)})(); \ No newline at end of file diff --git a/build/followers/index.asset.php b/build/followers/index.asset.php index 479c32f366..1b7f5f7e96 100644 --- a/build/followers/index.asset.php +++ b/build/followers/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '809dcb63fca0c8ee2302'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'd864454e562ad019ef94'); diff --git a/build/followers/index.js b/build/followers/index.js index 6769cfcf09..ac81f553f6 100644 --- a/build/followers/index.js +++ b/build/followers/index.js @@ -1,2 +1,2 @@ -(()=>{"use strict";var e,t={454:(e,t,s)=>{const a=window.wp.blocks,r=window.ReactJSXRuntime,l=window.wp.primitives;var i=(0,r.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});const o=[{attributes:{title:{type:"string",default:"Fediverse Followers"},selectedUser:{type:"string",default:"blog"},per_page:{type:"number",default:10},order:{type:"string",default:"desc",enum:["asc","desc"]}},supports:{html:!1},isEligible:({title:e})=>!!e,migrate:({title:e,...t})=>[t,[(0,a.createBlock)("core/heading",{content:e,level:3})]]}],n=window.wp.apiFetch;var c=s.n(n);const p=window.wp.components,u=window.wp.blockEditor,d=window.wp.coreData,v=window.wp.data,h=window.wp.element,g=window.wp.url,w=window.wp.i18n;function f(){return window._activityPubOptions||{}}function b({name:e}){const{enabled:t}=f(),s=t?.blog?"":(0,w.__)("It will be empty in other non-author contexts.","activitypub"),a=(0,w.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ -(0,w.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,s).trim();return(0,r.jsx)(p.Card,{children:(0,r.jsx)(p.CardBody,{children:(0,h.createInterpolateElement)(a,{strong:(0,r.jsx)("strong",{})})})})}function m({selectedUser:e,per_page:t,order:s,page:a,setPage:l,followerData:i=!1}){const o="blog"===e?0:e,[n,p]=(0,h.useState)([]),[u,d]=(0,h.useState)(0),[v,b]=(0,h.useState)(0),[m,y]=(0,h.useState)(1),j=a||m,N=l||y,k=(e,s)=>{p(e),b(s),d(Math.ceil(s/t))};return(0,h.useEffect)(()=>{if(i&&1===j)return k(i.followers,i.total);const e=function(e,t,s,a){const{namespace:r}=f(),l=`/${r}/actors/${e}/followers`,i={per_page:t,order:s,page:a,context:"full"};return(0,g.addQueryArgs)(l,i)}(o,t,s,j);c()({path:e}).then(({orderedItems:e,totalItems:t})=>k(e,t)).catch(()=>k([],0))},[o,t,s,j,i]),(0,r.jsxs)("div",{className:"followers-container",children:[n.length?(0,r.jsx)("ul",{className:"followers-list",children:n.map(e=>(0,r.jsx)("li",{className:"follower-item",children:(0,r.jsx)(_,{...e})},e.url))}):(0,r.jsx)("p",{className:"followers-placeholder",children:(0,w.__)("No followers found.","activitypub")}),(0,r.jsx)(x,{page:j,pages:u,setPage:N})]})}function x({page:e,pages:t,setPage:s}){if(t<=1)return null;const a=e<=1,l=e>=t;return(0,r.jsxs)("nav",{className:"followers-pagination",role:"navigation",children:[(0,r.jsx)("h1",{className:"screen-reader-text",children:(0,w.__)("Follower navigation","activitypub")}),(0,r.jsx)("a",{className:"pagination-previous","aria-disabled":a,"aria-label":(0,w.__)("Previous page","activitypub"),onClick:t=>{t.preventDefault(),s(e-1)},children:(0,w.__)("Previous","activitypub")}),(0,r.jsx)("div",{className:"pagination-info",children:`${e} / ${t}`}),(0,r.jsx)("a",{className:"pagination-next","aria-disabled":l,"aria-label":(0,w.__)("Next page","activitypub"),onClick:t=>{t.preventDefault(),s(e+1)},children:(0,w.__)("Next","activitypub")})]})}function _({name:e,icon:t,url:s,preferredUsername:a}){const l=`@${a}`,{defaultAvatarUrl:i}=f(),o=t?.url||i;return(0,r.jsxs)("a",{className:"follower-link",href:s,title:l,onClick:e=>e.preventDefault(),children:[(0,r.jsx)("img",{width:"48",height:"48",src:o,className:"follower-avatar",alt:e,onError:e=>{e.target.src=i}}),(0,r.jsxs)("div",{className:"follower-info",children:[(0,r.jsx)("span",{className:"follower-name",children:e}),(0,r.jsx)("span",{className:"follower-username",children:l})]}),(0,r.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24",className:"external-link-icon","aria-hidden":"true",focusable:"false",fill:"currentColor",children:(0,r.jsx)("path",{d:"M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"})})]})}const y=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/followers","apiVersion":3,"version":"2.0.1","title":"Fediverse Followers","category":"widgets","description":"Display your followers from the Fediverse on your website.","textdomain":"activitypub","icon":"groups","supports":{"html":false,"interactivity":true},"attributes":{"selectedUser":{"type":"string","default":"blog"},"per_page":{"type":"number","default":10},"order":{"type":"string","default":"desc","enum":["asc","desc"]}},"usesContext":["postType","postId"],"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"card","label":"Card"},{"name":"compact","label":"Compact"}],"editorScript":"file:./index.js","editorStyle":"file:./index.css","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":["file:./style-index.css"],"render":"file:./render.php"}');(0,a.registerBlockType)(y,{deprecated:o,edit:function({attributes:e,setAttributes:t,context:{postType:s,postId:a}}){const{className:l="",order:i,per_page:o,selectedUser:n}=e,g=(0,u.useBlockProps)(),[x,_]=(0,h.useState)(1),y=[{label:(0,w.__)("New to old","activitypub"),value:"desc"},{label:(0,w.__)("Old to new","activitypub"),value:"asc"}],j=function({withInherit:e=!1}){const{enabled:t,namespace:s}=f(),[a,r]=(0,h.useState)(!1),{fetchedUsers:l,isLoadingUsers:i}=(0,v.useSelect)(e=>{const{getUsers:s,getIsResolving:a}=e("core");return{fetchedUsers:t?.users?s({capabilities:"activitypub"}):null,isLoadingUsers:!!t?.users&&a("getUsers",[{capabilities:"activitypub"}])}},[]),o=(0,v.useSelect)(e=>l||i?null:e("core").getCurrentUser(),[l,i]);(0,h.useEffect)(()=>{l||i||!o||c()({path:`/${s}/actors/${o.id}`,method:"HEAD",headers:{Accept:"application/activity+json"},parse:!1}).then(()=>r(!0)).catch(()=>r(!1))},[l,i,o]);const n=l||(o&&a?[{id:o.id,name:o.name}]:[]);return(0,h.useMemo)(()=>{if(!n.length)return[];const s=[];return t?.blog&&l&&s.push({label:(0,w.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&l&&s.push({label:(0,w.__)("Dynamic User","activitypub"),value:"inherit"}),n.reduce((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e),s)},[n])}({withInherit:!0}),N=e=>s=>{_(1),t({[e]:s})},k=(0,v.useSelect)(e=>{const{getEditedEntityRecord:t}=e(d.store),r=t("postType",s,a)?.author;return null!=r?r:null},[s,a]);(0,h.useEffect)(()=>{j.length&&(j.find(({value:e})=>e===n)||t({selectedUser:j[0].value}))},[n,j]);const S=[["core/heading",{level:3,placeholder:(0,w.__)("Fediverse Followers","activitypub"),content:(0,w.__)("Fediverse Followers","activitypub")}]];return(0,r.jsxs)("div",{...g,children:[(0,r.jsx)(u.InspectorControls,{children:(0,r.jsxs)(p.PanelBody,{title:(0,w.__)("Followers Options","activitypub"),children:[j.length>1&&(0,r.jsx)(p.SelectControl,{label:(0,w.__)("Select User","activitypub"),value:n,options:j,onChange:N("selectedUser"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,r.jsx)(p.SelectControl,{label:(0,w.__)("Sort","activitypub"),value:i,options:y,onChange:N("order"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,r.jsx)(p.RangeControl,{label:(0,w.__)("Number of Followers","activitypub"),value:o,onChange:N("per_page"),min:1,max:10,__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0})]})},"setting"),(0,r.jsxs)("div",{className:"wp-block-activitypub-followers "+l,children:[(0,r.jsx)(u.InnerBlocks,{template:S,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),"inherit"===n?k?(0,r.jsx)(m,{...e,page:x,setPage:_,selectedUser:k}):(0,r.jsx)(b,{name:(0,w.__)("Followers","activitypub")}):(0,r.jsx)(m,{...e,page:x,setPage:_})]})]})},save:function(){const e=u.useBlockProps.save(),t=u.useInnerBlocksProps.save(e);return(0,r.jsx)("div",{...t})},icon:i})}},s={};function a(e){var r=s[e];if(void 0!==r)return r.exports;var l=s[e]={exports:{}};return t[e](l,l.exports,a),l.exports}a.m=t,e=[],a.O=(t,s,r,l)=>{if(!s){var i=1/0;for(p=0;p=l)&&Object.keys(a.O).every(e=>a.O[e](s[n]))?s.splice(n--,1):(o=!1,l0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[s,r,l]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var s in t)a.o(t,s)&&!a.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={149:0,17:0};a.O.j=t=>0===e[t];var t=(t,s)=>{var r,l,[i,o,n]=s,c=0;if(i.some(t=>0!==e[t])){for(r in o)a.o(o,r)&&(a.m[r]=o[r]);if(n)var p=n(a)}for(t&&t(s);ca(454));r=a.O(r)})(); \ No newline at end of file +(()=>{"use strict";var e,t={454:(e,t,s)=>{const a=window.wp.blocks,r=window.wp.primitives,l=window.ReactJSXRuntime;var i=(0,l.jsx)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(r.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});const o=[{attributes:{title:{type:"string",default:"Fediverse Followers"},selectedUser:{type:"string",default:"blog"},per_page:{type:"number",default:10},order:{type:"string",default:"desc",enum:["asc","desc"]}},supports:{html:!1},isEligible:({title:e})=>!!e,migrate:({title:e,...t})=>[t,[(0,a.createBlock)("core/heading",{content:e,level:3})]]}],n=window.wp.apiFetch;var c=s.n(n);const p=window.wp.components,u=window.wp.blockEditor,d=window.wp.coreData,v=window.wp.data,h=window.wp.element,g=window.wp.url,w=window.wp.i18n;function f(){return window._activityPubOptions||{}}function b({name:e}){const{enabled:t}=f(),s=t?.blog?"":(0,w.__)("It will be empty in other non-author contexts.","activitypub"),a=(0,w.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */ +(0,w.__)("This %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,s).trim();return(0,l.jsx)(p.Card,{children:(0,l.jsx)(p.CardBody,{children:(0,h.createInterpolateElement)(a,{strong:(0,l.jsx)("strong",{})})})})}function m({selectedUser:e,per_page:t,order:s,page:a,setPage:r,followerData:i=!1}){const o="blog"===e?0:e,[n,p]=(0,h.useState)([]),[u,d]=(0,h.useState)(0),[v,b]=(0,h.useState)(0),[m,y]=(0,h.useState)(1),j=a||m,N=r||y,k=(e,s)=>{p(e),b(s),d(Math.ceil(s/t))};return(0,h.useEffect)(()=>{if(i&&1===j)return k(i.followers,i.total);const e=function(e,t,s,a){const{namespace:r}=f(),l=`/${r}/actors/${e}/followers`,i={per_page:t,order:s,page:a,context:"full"};return(0,g.addQueryArgs)(l,i)}(o,t,s,j);c()({path:e}).then(({orderedItems:e,totalItems:t})=>k(e,t)).catch(()=>k([],0))},[o,t,s,j,i]),(0,l.jsxs)("div",{className:"followers-container",children:[n.length?(0,l.jsx)("ul",{className:"followers-list",children:n.map(e=>(0,l.jsx)("li",{className:"follower-item",children:(0,l.jsx)(_,{...e})},e.url))}):(0,l.jsx)("p",{className:"followers-placeholder",children:(0,w.__)("No followers found.","activitypub")}),(0,l.jsx)(x,{page:j,pages:u,setPage:N})]})}function x({page:e,pages:t,setPage:s}){if(t<=1)return null;const a=e<=1,r=e>=t;return(0,l.jsxs)("nav",{className:"followers-pagination",role:"navigation",children:[(0,l.jsx)("h1",{className:"screen-reader-text",children:(0,w.__)("Follower navigation","activitypub")}),(0,l.jsx)("a",{className:"pagination-previous","aria-disabled":a,"aria-label":(0,w.__)("Previous page","activitypub"),onClick:t=>{t.preventDefault(),s(e-1)},children:(0,w.__)("Previous","activitypub")}),(0,l.jsx)("div",{className:"pagination-info",children:`${e} / ${t}`}),(0,l.jsx)("a",{className:"pagination-next","aria-disabled":r,"aria-label":(0,w.__)("Next page","activitypub"),onClick:t=>{t.preventDefault(),s(e+1)},children:(0,w.__)("Next","activitypub")})]})}function _({name:e,icon:t,url:s,preferredUsername:a}){const r=`@${a}`,{defaultAvatarUrl:i}=f(),o=t?.url||i;return(0,l.jsxs)("a",{className:"follower-link",href:s,title:r,onClick:e=>e.preventDefault(),children:[(0,l.jsx)("img",{width:"48",height:"48",src:o,className:"follower-avatar",alt:e,onError:e=>{e.target.src=i}}),(0,l.jsxs)("div",{className:"follower-info",children:[(0,l.jsx)("span",{className:"follower-name",children:e}),(0,l.jsx)("span",{className:"follower-username",children:r})]}),(0,l.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24",className:"external-link-icon","aria-hidden":"true",focusable:"false",fill:"currentColor",children:(0,l.jsx)("path",{d:"M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"})})]})}const y=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/followers","apiVersion":3,"version":"2.0.1","title":"Fediverse Followers","category":"widgets","description":"Display your followers from the Fediverse on your website.","textdomain":"activitypub","icon":"groups","supports":{"html":false,"interactivity":true},"attributes":{"selectedUser":{"type":"string","default":"blog"},"per_page":{"type":"number","default":10},"order":{"type":"string","default":"desc","enum":["asc","desc"]}},"usesContext":["postType","postId"],"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"card","label":"Card"},{"name":"compact","label":"Compact"}],"editorScript":"file:./index.js","editorStyle":"file:./index.css","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":["file:./style-index.css"],"render":"file:./render.php"}');(0,a.registerBlockType)(y,{deprecated:o,edit:function({attributes:e,setAttributes:t,context:{postType:s,postId:a}}){const{className:r="",order:i,per_page:o,selectedUser:n}=e,g=(0,u.useBlockProps)(),[x,_]=(0,h.useState)(1),y=[{label:(0,w.__)("New to old","activitypub"),value:"desc"},{label:(0,w.__)("Old to new","activitypub"),value:"asc"}],j=function({withInherit:e=!1}){const{enabled:t,namespace:s}=f(),[a,r]=(0,h.useState)(!1),{fetchedUsers:l,isLoadingUsers:i}=(0,v.useSelect)(e=>{const{getUsers:s,getIsResolving:a}=e("core");return{fetchedUsers:t?.users?s({capabilities:"activitypub"}):null,isLoadingUsers:!!t?.users&&a("getUsers",[{capabilities:"activitypub"}])}},[]),o=(0,v.useSelect)(e=>l||i?null:e("core").getCurrentUser(),[l,i]);(0,h.useEffect)(()=>{l||i||!o||c()({path:`/${s}/actors/${o.id}`,method:"HEAD",headers:{Accept:"application/activity+json"},parse:!1}).then(()=>r(!0)).catch(()=>r(!1))},[l,i,o]);const n=l||(o&&a?[{id:o.id,name:o.name}]:[]);return(0,h.useMemo)(()=>{if(!n.length)return[];const s=[];return t?.blog&&l&&s.push({label:(0,w.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&l&&s.push({label:(0,w.__)("Dynamic User","activitypub"),value:"inherit"}),n.reduce((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e),s)},[n])}({withInherit:!0}),N=e=>s=>{_(1),t({[e]:s})},k=(0,v.useSelect)(e=>{const{getEditedEntityRecord:t}=e(d.store),r=t("postType",s,a)?.author;return null!=r?r:null},[s,a]);(0,h.useEffect)(()=>{j.length&&(j.find(({value:e})=>e===n)||t({selectedUser:j[0].value}))},[n,j]);const S=[["core/heading",{level:3,placeholder:(0,w.__)("Fediverse Followers","activitypub"),content:(0,w.__)("Fediverse Followers","activitypub")}]];return(0,l.jsxs)("div",{...g,children:[(0,l.jsx)(u.InspectorControls,{children:(0,l.jsxs)(p.PanelBody,{title:(0,w.__)("Followers Options","activitypub"),children:[j.length>1&&(0,l.jsx)(p.SelectControl,{label:(0,w.__)("Select User","activitypub"),value:n,options:j,onChange:N("selectedUser"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,l.jsx)(p.SelectControl,{label:(0,w.__)("Sort","activitypub"),value:i,options:y,onChange:N("order"),__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),(0,l.jsx)(p.RangeControl,{label:(0,w.__)("Number of Followers","activitypub"),value:o,onChange:N("per_page"),min:1,max:10,__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0})]})},"setting"),(0,l.jsxs)("div",{className:"wp-block-activitypub-followers "+r,children:[(0,l.jsx)(u.InnerBlocks,{template:S,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),"inherit"===n?k?(0,l.jsx)(m,{...e,page:x,setPage:_,selectedUser:k}):(0,l.jsx)(b,{name:(0,w.__)("Followers","activitypub")}):(0,l.jsx)(m,{...e,page:x,setPage:_})]})]})},save:function(){const e=u.useBlockProps.save(),t=u.useInnerBlocksProps.save(e);return(0,l.jsx)("div",{...t})},icon:i})}},s={};function a(e){var r=s[e];if(void 0!==r)return r.exports;var l=s[e]={exports:{}};return t[e](l,l.exports,a),l.exports}a.m=t,e=[],a.O=(t,s,r,l)=>{if(!s){var i=1/0;for(p=0;p=l)&&Object.keys(a.O).every(e=>a.O[e](s[n]))?s.splice(n--,1):(o=!1,l0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[s,r,l]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var s in t)a.o(t,s)&&!a.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={149:0,17:0};a.O.j=t=>0===e[t];var t=(t,s)=>{var r,l,[i,o,n]=s,c=0;if(i.some(t=>0!==e[t])){for(r in o)a.o(o,r)&&(a.m[r]=o[r]);if(n)var p=n(a)}for(t&&t(s);ca(454));r=a.O(r)})(); \ No newline at end of file diff --git a/build/internal/actors-entity/block.json b/build/internal/actors-entity/block.json new file mode 100644 index 0000000000..a04705d027 --- /dev/null +++ b/build/internal/actors-entity/block.json @@ -0,0 +1,8 @@ +{ + "name": "actors-entity", + "title": "Actors Entity: Registers actors as WordPress Core Data entities", + "category": "widgets", + "icon": "admin-users", + "keywords": [], + "editorScript": "file:./index.js" +} \ No newline at end of file diff --git a/build/internal/actors-entity/index.asset.php b/build/internal/actors-entity/index.asset.php new file mode 100644 index 0000000000..61c0a29e35 --- /dev/null +++ b/build/internal/actors-entity/index.asset.php @@ -0,0 +1 @@ + array('wp-core-data', 'wp-data'), 'version' => '4415cff498283c5086fd'); diff --git a/build/internal/actors-entity/index.js b/build/internal/actors-entity/index.js new file mode 100644 index 0000000000..416ef3060f --- /dev/null +++ b/build/internal/actors-entity/index.js @@ -0,0 +1 @@ +(()=>{"use strict";const t=window.wp.data,a=window.wp.coreData;(()=>{const{registerEntityType:i}=(0,t.dispatch)(a.store);i({kind:"activitypub/1.0",name:"actor",label:"Actor",plural:"Actors",baseURL:"/wp-json/activitypub/1.0/internal/actors",key:"id",transientEdits:{name:!0,preferred_username:!0,url:!0,icon:!0,summary:!0,activitypub_id:!0},supportsPagination:!1})})()})(); \ No newline at end of file diff --git a/build/internal/posts-entity/block.json b/build/internal/posts-entity/block.json new file mode 100644 index 0000000000..b7fb81610e --- /dev/null +++ b/build/internal/posts-entity/block.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "activitypub/posts-entity", + "title": "ActivityPub Posts Entity", + "category": "widgets", + "description": "Registers ActivityPub posts as a WordPress Core Data entity.", + "textdomain": "activitypub", + "editorScript": "file:./index.js" +} \ No newline at end of file diff --git a/build/internal/posts-entity/index.asset.php b/build/internal/posts-entity/index.asset.php new file mode 100644 index 0000000000..38189711ce --- /dev/null +++ b/build/internal/posts-entity/index.asset.php @@ -0,0 +1 @@ + array('wp-core-data', 'wp-data'), 'version' => '5665619fcc026a423a8b'); diff --git a/build/internal/posts-entity/index.js b/build/internal/posts-entity/index.js new file mode 100644 index 0000000000..110542d5d7 --- /dev/null +++ b/build/internal/posts-entity/index.js @@ -0,0 +1 @@ +(()=>{"use strict";const t=window.wp.data,s=window.wp.coreData;(()=>{const{registerEntityType:i}=(0,t.dispatch)(s.store);i({kind:"activitypub/1.0",name:"post",label:"Post",plural:"Posts",baseURL:"/wp-json/activitypub/1.0/internal/posts",key:"wp_id",transientEdits:{id:!0,type:!0,name:!0,content:!0,summary:!0,published:!0,wp_status:!0},supportsPagination:!1})})()})(); \ No newline at end of file diff --git a/build/reply/index.asset.php b/build/reply/index.asset.php index 9657d90a6f..e40056b538 100644 --- a/build/reply/index.asset.php +++ b/build/reply/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'f5e41001f58596092a8f'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '81c54476cf58b65cc4fd'); diff --git a/build/reply/index.js b/build/reply/index.js index b0bab10583..d7dbd5e256 100644 --- a/build/reply/index.js +++ b/build/reply/index.js @@ -1 +1 @@ -(()=>{"use strict";var e={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return e.d(o,{a:o}),o},d:(t,o)=>{for(var r in o)e.o(o,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:o[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.blocks,o=window.ReactJSXRuntime,r=window.wp.primitives;var i=(0,o.jsx)(r.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,o.jsx)(r.Path,{d:"M6.68822 10.625L6.24878 11.0649L5.5 11.8145L5.5 5.5L12.5 5.5V8L14 6.5V5C14 4.44772 13.5523 4 13 4H5C4.44772 4 4 4.44771 4 5V13.5247C4 13.8173 4.16123 14.086 4.41935 14.2237C4.72711 14.3878 5.10601 14.3313 5.35252 14.0845L7.31 12.125H8.375L9.875 10.625H7.31H6.68822ZM14.5605 10.4983L11.6701 13.75H16.9975C17.9963 13.75 18.7796 14.1104 19.3553 14.7048C19.9095 15.2771 20.2299 16.0224 20.4224 16.7443C20.7645 18.0276 20.7543 19.4618 20.7487 20.2544C20.7481 20.345 20.7475 20.4272 20.7475 20.4999L19.2475 20.5001C19.2475 20.4191 19.248 20.3319 19.2484 20.2394V20.2394C19.2526 19.4274 19.259 18.2035 18.973 17.1307C18.8156 16.5401 18.586 16.0666 18.2778 15.7483C17.9909 15.4521 17.5991 15.25 16.9975 15.25H11.8106L14.5303 17.9697L13.4696 19.0303L8.96956 14.5303L13.4394 9.50171L14.5605 10.4983Z"})});const c=window.wp.blockEditor,l=window.wp.components,n=window.wp.i18n,s=window.wp.element,a=window.wp.compose,d=window.wp.data,u=window.wp.apiFetch;var p=e.n(u);const b=window.wp.url,w={default:(0,n.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(l.Spinner,{})," "+(0,n.__)("Checking URL...","activitypub")]}),valid:(0,n.__)("The author will be notified of your response.","activitypub"),error:(0,n.__)("This site doesn’t have ActivityPub enabled and won’t receive your reply.","activitypub")};(0,t.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:r,clientId:i,isSelected:u}){const{url:y="",embedPost:m=!1}=e,[v,h]=(0,s.useState)(w.default),[f,k]=(0,s.useState)(!1),[_,x]=(0,s.useState)(!1),C=(0,s.useRef)(),{insertAfterBlock:g,removeBlock:L,replaceInnerBlocks:P}=(0,d.useDispatch)("core/block-editor"),j=m&&!_&&f,B=(0,c.useInnerBlocksProps)({className:"activitypub-embed-container"},{allowedBlocks:["core/embed"],template:y&&j?[["core/embed",{url:y}]]:[],templateLock:"all"});(0,s.useEffect)(()=>{P(i,y&&j?[(0,t.createBlock)("core/embed",{url:y})]:[])},[y,j,i,P]),(0,s.useEffect)(()=>{h(y?_?w.checking():f?w.valid:w.error:w.default)},[y,_,f]);const S=()=>{setTimeout(()=>C.current?.focus(),50)},V=(0,s.useCallback)(async e=>{if(e)try{x(!0),new URL(e);try{const t=await p()({path:(0,b.addQueryArgs)("/oembed/1.0/proxy",{url:e,activitypub:!0})});t&&t.provider_name?(r({embedPost:!0,isValidActivityPub:!0}),k(!0)):(r({isValidActivityPub:!1}),k(!1))}catch(e){console.log("Could not fetch embed:",e),r({isValidActivityPub:!1}),k(!1)}}catch(e){r({isValidActivityPub:!1}),k(!1)}finally{x(!1)}else k(!1)},[m,r]),A=(0,a.useDebounce)(V,250);return(0,s.useEffect)(()=>{y&&A(y)},[y]),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(c.InspectorControls,{children:(0,o.jsx)(l.PanelBody,{title:(0,n.__)("Settings","activitypub"),children:(0,o.jsx)(l.ToggleControl,{label:(0,n.__)("Embed Post","activitypub"),checked:!!m,onChange:e=>r({embedPost:e}),disabled:!f,help:(0,n.__)("Show embedded content from the URL.","activitypub"),__nextHasNoMarginBottom:!0})})}),(0,o.jsxs)("div",{onClick:S,...(0,c.useBlockProps)(),children:[u&&(0,o.jsx)(l.TextControl,{label:(0,n.__)("Your post is a reply to the following URL","activitypub"),value:y,onChange:e=>r({url:e}),help:v,onKeyDown:e=>{"Enter"===e.key&&g(i),!y&&["Backspace","Delete"].includes(e.key)&&L(i)},ref:C,__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),j&&(0,o.jsx)("div",{...B}),y&&!j&&!u&&(0,o.jsx)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:S,style:{cursor:"pointer"},children:(0,o.jsx)("a",{href:y,className:"u-in-reply-to",target:"_blank",rel:"noreferrer",children:"↬"+y.replace(/^https?:\/\//,"")})})]})]})},save:()=>null,icon:i,transforms:{from:[{type:"block",blocks:["core/embed"],transform:e=>(0,t.createBlock)("activitypub/reply",{url:e.url||"",embedPost:!0})}],to:[{type:"block",blocks:["core/embed"],transform:e=>(0,t.createBlock)("core/embed",{url:e.url||""})}]}})})(); \ No newline at end of file +(()=>{"use strict";var e={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return e.d(o,{a:o}),o},d:(t,o)=>{for(var r in o)e.o(o,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:o[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.blocks,o=window.wp.primitives,r=window.ReactJSXRuntime;var i=(0,r.jsx)(o.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,r.jsx)(o.Path,{d:"M6.68822 10.625L6.24878 11.0649L5.5 11.8145L5.5 5.5L12.5 5.5V8L14 6.5V5C14 4.44772 13.5523 4 13 4H5C4.44772 4 4 4.44771 4 5V13.5247C4 13.8173 4.16123 14.086 4.41935 14.2237C4.72711 14.3878 5.10601 14.3313 5.35252 14.0845L7.31 12.125H8.375L9.875 10.625H7.31H6.68822ZM14.5605 10.4983L11.6701 13.75H16.9975C17.9963 13.75 18.7796 14.1104 19.3553 14.7048C19.9095 15.2771 20.2299 16.0224 20.4224 16.7443C20.7645 18.0276 20.7543 19.4618 20.7487 20.2544C20.7481 20.345 20.7475 20.4272 20.7475 20.4999L19.2475 20.5001C19.2475 20.4191 19.248 20.3319 19.2484 20.2394V20.2394C19.2526 19.4274 19.259 18.2035 18.973 17.1307C18.8156 16.5401 18.586 16.0666 18.2778 15.7483C17.9909 15.4521 17.5991 15.25 16.9975 15.25H11.8106L14.5303 17.9697L13.4696 19.0303L8.96956 14.5303L13.4394 9.50171L14.5605 10.4983Z"})});const c=window.wp.blockEditor,l=window.wp.components,n=window.wp.i18n,s=window.wp.element,a=window.wp.compose,d=window.wp.data,u=window.wp.apiFetch;var p=e.n(u);const b=window.wp.url,w={default:(0,n.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(l.Spinner,{})," "+(0,n.__)("Checking URL...","activitypub")]}),valid:(0,n.__)("The author will be notified of your response.","activitypub"),error:(0,n.__)("This site doesn’t have ActivityPub enabled and won’t receive your reply.","activitypub")};(0,t.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:o,clientId:i,isSelected:u}){const{url:y="",embedPost:m=!1}=e,[v,h]=(0,s.useState)(w.default),[f,k]=(0,s.useState)(!1),[_,x]=(0,s.useState)(!1),C=(0,s.useRef)(),{insertAfterBlock:g,removeBlock:L,replaceInnerBlocks:P}=(0,d.useDispatch)("core/block-editor"),j=m&&!_&&f,B=(0,c.useInnerBlocksProps)({className:"activitypub-embed-container"},{allowedBlocks:["core/embed"],template:y&&j?[["core/embed",{url:y}]]:[],templateLock:"all"});(0,s.useEffect)(()=>{P(i,y&&j?[(0,t.createBlock)("core/embed",{url:y})]:[])},[y,j,i,P]),(0,s.useEffect)(()=>{h(y?_?w.checking():f?w.valid:w.error:w.default)},[y,_,f]);const S=()=>{setTimeout(()=>C.current?.focus(),50)},V=(0,s.useCallback)(async e=>{if(e)try{x(!0),new URL(e);try{const t=await p()({path:(0,b.addQueryArgs)("/oembed/1.0/proxy",{url:e,activitypub:!0})});t&&t.provider_name?(o({embedPost:!0,isValidActivityPub:!0}),k(!0)):(o({isValidActivityPub:!1}),k(!1))}catch(e){console.log("Could not fetch embed:",e),o({isValidActivityPub:!1}),k(!1)}}catch(e){o({isValidActivityPub:!1}),k(!1)}finally{x(!1)}else k(!1)},[m,o]),A=(0,a.useDebounce)(V,250);return(0,s.useEffect)(()=>{y&&A(y)},[y]),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(c.InspectorControls,{children:(0,r.jsx)(l.PanelBody,{title:(0,n.__)("Settings","activitypub"),children:(0,r.jsx)(l.ToggleControl,{label:(0,n.__)("Embed Post","activitypub"),checked:!!m,onChange:e=>o({embedPost:e}),disabled:!f,help:(0,n.__)("Show embedded content from the URL.","activitypub"),__nextHasNoMarginBottom:!0})})}),(0,r.jsxs)("div",{onClick:S,...(0,c.useBlockProps)(),children:[u&&(0,r.jsx)(l.TextControl,{label:(0,n.__)("Your post is a reply to the following URL","activitypub"),value:y,onChange:e=>o({url:e}),help:v,onKeyDown:e=>{"Enter"===e.key&&g(i),!y&&["Backspace","Delete"].includes(e.key)&&L(i)},ref:C,__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0}),j&&(0,r.jsx)("div",{...B}),y&&!j&&!u&&(0,r.jsx)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:S,style:{cursor:"pointer"},children:(0,r.jsx)("a",{href:y,className:"u-in-reply-to",target:"_blank",rel:"noreferrer",children:"↬"+y.replace(/^https?:\/\//,"")})})]})]})},save:()=>null,icon:i,transforms:{from:[{type:"block",blocks:["core/embed"],transform:e=>(0,t.createBlock)("activitypub/reply",{url:e.url||"",embedPost:!0})}],to:[{type:"block",blocks:["core/embed"],transform:e=>(0,t.createBlock)("core/embed",{url:e.url||""})}]}})})(); \ No newline at end of file diff --git a/includes/class-blocks.php b/includes/class-blocks.php index 640867399f..1f11bc9407 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -49,6 +49,34 @@ public static function enqueue_editor_assets() { ); wp_localize_script( 'wp-editor', '_activityPubOptions', $data ); + // Register the actors entity with WordPress Core Data API. + $entity_asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/internal/actors-entity/index.asset.php'; + if ( file_exists( $entity_asset_file ) ) { + $entity_asset_data = include $entity_asset_file; + $entity_url = plugins_url( 'build/internal/actors-entity/index.js', ACTIVITYPUB_PLUGIN_FILE ); + wp_enqueue_script( + 'activitypub-actors-entity', + $entity_url, + $entity_asset_data['dependencies'], + $entity_asset_data['version'], + true + ); + } + + // Register the posts entity with WordPress Core Data API. + $posts_entity_asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/internal/posts-entity/index.asset.php'; + if ( file_exists( $posts_entity_asset_file ) ) { + $posts_entity_asset_data = include $posts_entity_asset_file; + $posts_entity_url = plugins_url( 'build/internal/posts-entity/index.js', ACTIVITYPUB_PLUGIN_FILE ); + wp_enqueue_script( + 'activitypub-posts-entity', + $posts_entity_url, + $posts_entity_asset_data['dependencies'], + $posts_entity_asset_data['version'], + true + ); + } + // Check for our supported post types. $current_screen = \get_current_screen(); $ap_post_types = \get_post_types_by_support( 'activitypub' ); diff --git a/includes/functions.php b/includes/functions.php index 1bfcc9b881..886e494a2b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -760,6 +760,11 @@ function object_to_uri( $data ) { return $data; } + // Return scalar values as-is (bool, int, float). + if ( is_bool( $data ) || is_int( $data ) || is_float( $data ) ) { + return $data; + } + if ( is_object( $data ) ) { $data = $data->to_array(); } @@ -792,7 +797,7 @@ function object_to_uri( $data ) { $data = $data['href']; break; default: - $data = $data['id']; + $data = $data['id'] ?? null; break; } diff --git a/includes/rest/internal/class-actors-controller.php b/includes/rest/internal/class-actors-controller.php new file mode 100644 index 0000000000..2e123029ab --- /dev/null +++ b/includes/rest/internal/class-actors-controller.php @@ -0,0 +1,449 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + // Single item endpoint. + \register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d-]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the actor.', 'activitypub' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves a collection of actors. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $type = $request->get_param( 'type' ); + $relationship = $request->get_param( 'relationship' ); + $user_id = $request->get_param( 'user_id' ); + $per_page = (int) $request->get_param( 'per_page' ); + $page = (int) $request->get_param( 'page' ); + $order = $request->get_param( 'order' ); + $search = $request->get_param( 'search' ); + + // If requesting followers or following, use appropriate collection. + if ( $relationship && null !== $user_id ) { + return $this->get_relationship_actors( $relationship, $user_id, $per_page, $page, $order, $search, $request ); + } + + // Get local actors. + $actors = Actor_Collection::get_all(); + + // Filter by type if specified. + if ( $type ) { + $actors = array_filter( + $actors, + function ( $actor ) use ( $type ) { + return Actor_Collection::get_type_by_id( $actor->get_user_id() ) === $type; + } + ); + } + + $data = array(); + foreach ( $actors as $actor ) { + $item_data = $this->prepare_item_for_response( $actor, $request ); + $data[] = $this->prepare_response_for_collection( $item_data ); + } + + return \rest_ensure_response( $data ); + } + + /** + * Retrieves a single actor. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $actor_id = (int) $request->get_param( 'id' ); + $actor = Actor_Collection::get_by_id( $actor_id ); + + if ( \is_wp_error( $actor ) ) { + return $actor; + } + + $data = $this->prepare_item_for_response( $actor, $request ); + + return \rest_ensure_response( $data ); + } + + /** + * Retrieves actors based on relationship (followers or following). + * + * @param string $relationship Relationship type ('followers' or 'following'). + * @param int $user_id User ID for the relationship. + * @param int $per_page Number of items per page. + * @param int $page Current page. + * @param string $order Order direction. + * @param string $search Search term. + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + protected function get_relationship_actors( $relationship, $user_id, $per_page, $page, $order, $search, $request ) { + // Validate actor exists. + $actor = Actor_Collection::get_by_id( $user_id ); + if ( \is_wp_error( $actor ) ) { + return $actor; + } + + $args = array( + 'order' => 'desc' === $order ? 'DESC' : 'ASC', + ); + + // Add search if provided. + if ( ! empty( $search ) ) { + $args['s'] = $search; + } + + // Query the appropriate collection. + if ( 'followers' === $relationship ) { + $collection_class = 'Activitypub\Collection\Followers'; + } elseif ( 'following' === $relationship ) { + $collection_class = 'Activitypub\Collection\Following'; + } else { + return new WP_Error( + 'invalid_relationship', + __( 'Invalid relationship type.', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + $result = $collection_class::query( $user_id, $per_page, $page, $args ); + + $total_items = $result['total'] ?? 0; + $total_pages = $result['pages'] ?? 0; + $actor_ids = $result['followers'] ?? $result['following'] ?? array(); + + // Prepare response data. + $data = array(); + foreach ( $actor_ids as $actor_id ) { + $post = \get_post( $actor_id ); + if ( ! $post ) { + continue; + } + + $item_data = $this->prepare_remote_actor_for_response( $post, $request ); + $data[] = $this->prepare_response_for_collection( $item_data ); + } + + $response = \rest_ensure_response( $data ); + + // Add pagination headers. + $response->header( 'X-WP-Total', $total_items ); + $response->header( 'X-WP-TotalPages', $total_pages ); + + return $response; + } + + /** + * Checks if a given request has access to read actors. + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Allow any logged-in user to read actors. + if ( ! \is_user_logged_in() ) { + return new WP_Error( + 'rest_forbidden', + __( 'Sorry, you are not allowed to view actors.', 'activitypub' ), + array( 'status' => \rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks if a given request has access to read a specific actor. + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + return $this->get_items_permissions_check( $request ); + } + + /** + * Prepares an actor for the REST response. + * + * @param object $actor Actor object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $actor, $request ) { + $actor_array = $actor->to_array(); + + // Remove context. + unset( $actor_array['@context'] ); + + // Map all values through object_to_uri and filter out empty ones. + $data = array_filter( array_map( '\Activitypub\object_to_uri', $actor_array ) ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = \rest_ensure_response( $data ); + + /** + * Filters the actor data for a REST API response. + * + * @param WP_REST_Response $response The response object. + * @param object|array $actor Actor object or array. + * @param WP_REST_Request $request Request object. + */ + return \apply_filters( 'activitypub_rest_prepare_actor', $response, $actor, $request ); + } + + /** + * Prepares links for the request. + * + * @param object $actor Actor object. + * @return array Links for the given actor. + */ + protected function prepare_links( $actor ) { + $actor_id = $actor->get_user_id(); + + $links = array( + 'self' => array( + 'href' => \rest_url( \sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $actor_id ) ), + ), + 'collection' => array( + 'href' => \rest_url( \sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Prepares a remote actor (from post) for the REST response. + * + * @param \WP_Post $post Post object representing remote actor. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + protected function prepare_remote_actor_for_response( $post, $request ) { + // Get actor data from post content or meta. + $actor_data = \json_decode( $post->post_content, true ); + + if ( empty( $actor_data ) || ! is_array( $actor_data ) ) { + $actor_data = array(); + } + + // Remove context. + unset( $actor_data['@context'] ); + + // Map all values through object_to_uri and filter out empty ones. + $data = array_filter( array_map( '\Activitypub\object_to_uri', $actor_data ) ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = \rest_ensure_response( $data ); + + /** + * Filters the remote actor data for a REST API response. + * + * @param WP_REST_Response $response The response object. + * @param \WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return \apply_filters( 'activitypub_rest_prepare_remote_actor', $response, $post, $request ); + } + + /** + * Retrieves the actor schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'activitypub-actor', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the actor.', 'activitypub' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'The type of actor (user, blog, application).', 'activitypub' ), + 'type' => 'string', + 'enum' => array( 'user', 'blog', 'application' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'The display name of the actor.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'preferred_username' => array( + 'description' => __( 'The preferred username of the actor.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'url' => array( + 'description' => __( 'The URL of the actor profile.', 'activitypub' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'icon' => array( + 'description' => __( 'The icon/avatar of the actor.', 'activitypub' ), + 'type' => array( 'object', 'null' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'The biography/summary of the actor.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'activitypub_id' => array( + 'description' => __( 'The ActivityPub ID (URI) of the actor.', 'activitypub' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the query params for the collection. + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $params = array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'type' => array( + 'description' => __( 'Limit results to actors of a specific type.', 'activitypub' ), + 'type' => 'string', + 'enum' => array( 'user', 'blog', 'application', 'remote' ), + ), + 'relationship' => array( + 'description' => __( 'Filter actors by relationship to a user.', 'activitypub' ), + 'type' => 'string', + 'enum' => array( 'followers', 'following' ), + ), + 'user_id' => array( + 'description' => __( 'User ID for relationship filtering.', 'activitypub' ), + 'type' => 'integer', + ), + 'page' => array( + 'description' => __( 'Current page of the collection.', 'activitypub' ), + 'type' => 'integer', + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'activitypub' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'order' => array( + 'description' => __( 'Order sort attribute ascending or descending.', 'activitypub' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + ), + 'search' => array( + 'description' => __( 'Limit results to those matching a string.', 'activitypub' ), + 'type' => 'string', + ), + ); + + return $params; + } +} diff --git a/includes/rest/internal/class-posts-controller.php b/includes/rest/internal/class-posts-controller.php new file mode 100644 index 0000000000..62b95bbed1 --- /dev/null +++ b/includes/rest/internal/class-posts-controller.php @@ -0,0 +1,356 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + // Single item endpoint. + \register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the post.', 'activitypub' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves a collection of ActivityPub posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $user_id = $request->get_param( 'user_id' ); + $actor_id = $request->get_param( 'actor_id' ); + $per_page = (int) $request->get_param( 'per_page' ); + $page = (int) $request->get_param( 'page' ); + $order = $request->get_param( 'order' ); + $search = $request->get_param( 'search' ); + + $args = array( + 'post_type' => Posts_Collection::POST_TYPE, + 'posts_per_page' => $per_page, + 'paged' => $page, + 'order' => 'desc' === $order ? 'DESC' : 'ASC', + 'post_status' => 'publish', + ); + + // Filter by recipient user. + if ( null !== $user_id ) { + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $args['meta_query'] = array( + array( + 'key' => '_activitypub_user_id', + 'value' => (int) $user_id, + ), + ); + } + + // Filter by remote actor. + if ( $actor_id ) { + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $args['meta_query'] = array( + array( + 'key' => '_activitypub_remote_actor_id', + 'value' => (int) $actor_id, + ), + ); + } + + // Add search if provided. + if ( ! empty( $search ) ) { + $args['s'] = $search; + } + + $query = new \WP_Query( $args ); + + $total_items = $query->found_posts; + $total_pages = $query->max_num_pages; + + $data = array(); + foreach ( $query->posts as $post ) { + $item_data = $this->prepare_item_for_response( $post, $request ); + $data[] = $this->prepare_response_for_collection( $item_data ); + } + + $response = \rest_ensure_response( $data ); + + // Add pagination headers. + $response->header( 'X-WP-Total', $total_items ); + $response->header( 'X-WP-TotalPages', $total_pages ); + + return $response; + } + + /** + * Retrieves a single ActivityPub post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $post_id = (int) $request->get_param( 'id' ); + $post = Posts_Collection::get( $post_id ); + + if ( ! $post || Posts_Collection::POST_TYPE !== $post->post_type ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $data = $this->prepare_item_for_response( $post, $request ); + + return \rest_ensure_response( $data ); + } + + /** + * Checks if a given request has access to read posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Allow any logged-in user to read posts. + if ( ! \is_user_logged_in() ) { + return new WP_Error( + 'rest_forbidden', + __( 'Sorry, you are not allowed to view posts.', 'activitypub' ), + array( 'status' => \rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks if a given request has access to read a specific post. + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + return $this->get_items_permissions_check( $request ); + } + + /** + * Prepares an ActivityPub post for the REST response. + * + * @param \WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $post, $request ) { + // Get ActivityPub data from post content. + $post_data = \json_decode( $post->post_content, true ); + + if ( empty( $post_data ) || ! is_array( $post_data ) ) { + $post_data = array(); + } + + // Remove context. + unset( $post_data['@context'] ); + + // Map all values through object_to_uri and filter out empty ones. + $data = array_filter( array_map( '\Activitypub\object_to_uri', $post_data ) ); + + // Add WordPress post data. + $data['wp_id'] = $post->ID; + $data['wp_status'] = $post->post_status; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = \rest_ensure_response( $data ); + + /** + * Filters the post data for a REST API response. + * + * @param WP_REST_Response $response The response object. + * @param \WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return \apply_filters( 'activitypub_rest_prepare_post', $response, $post, $request ); + } + + /** + * Retrieves the post schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'activitypub-post', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'The ActivityPub ID (URI) of the post.', 'activitypub' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'The ActivityPub object type.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'The title of the post.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'content' => array( + 'description' => __( 'The content of the post.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'The summary/excerpt of the post.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'published' => array( + 'description' => __( 'The publication date.', 'activitypub' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'wp_id' => array( + 'description' => __( 'The WordPress post ID.', 'activitypub' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'wp_status' => array( + 'description' => __( 'The WordPress post status.', 'activitypub' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the query params for the collection. + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $params = array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'user_id' => array( + 'description' => __( 'Limit results to posts for a specific user (recipient).', 'activitypub' ), + 'type' => 'integer', + ), + 'actor_id' => array( + 'description' => __( 'Limit results to posts from a specific remote actor.', 'activitypub' ), + 'type' => 'integer', + ), + 'page' => array( + 'description' => __( 'Current page of the collection.', 'activitypub' ), + 'type' => 'integer', + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'activitypub' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'order' => array( + 'description' => __( 'Order sort attribute ascending or descending.', 'activitypub' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + ), + 'search' => array( + 'description' => __( 'Limit results to those matching a string.', 'activitypub' ), + 'type' => 'string', + ), + ); + + return $params; + } +} diff --git a/src/internal/actors-entity/README.md b/src/internal/actors-entity/README.md new file mode 100644 index 0000000000..5896d974ba --- /dev/null +++ b/src/internal/actors-entity/README.md @@ -0,0 +1,231 @@ +# Actors Entity + +This module registers ActivityPub actors as a WordPress Core Data entity, making it easy to fetch and display actor information in the block editor. + +## What is a WordPress Entity? + +WordPress entities are data types that can be accessed through the `@wordpress/core-data` package. They provide a standardized way to interact with WordPress data using hooks like `useEntityRecords` and `useEntityRecord`. + +Reference: [WordPress Core Data Package Documentation](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/#whats-an-entity) + +## Features + +- **Read-only access** to local ActivityPub actors (users, blog, application) +- **REST API integration** via internal endpoint +- **Core Data hooks** for easy data fetching +- **Type-safe** actor information + +## REST API Endpoint + +The entity is backed by the following internal REST API endpoint: + +- **Base URL**: `/wp-json/activitypub/v1/internal/actors` +- **Single Actor**: `/wp-json/activitypub/v1/internal/actors/{id}` +- **Authentication**: Requires logged-in user + +### Actor Schema + +Each actor has the following properties: + +| Property | Type | Description | +|----------|------|-------------| +| `id` | integer | WordPress user ID (0 for blog, -1 for application) | +| `type` | string | Actor type: `user`, `blog`, or `application` | +| `name` | string | Display name of the actor | +| `preferred_username` | string | Username/identifier | +| `url` | string | Profile URL | +| `icon` | object/null | Avatar/icon information | +| `summary` | string | Biography/description | +| `activitypub_id` | string | ActivityPub URI | + +## Usage Examples + +### 1. Fetch All Actors + +```javascript +import { useEntityRecords } from '@wordpress/core-data'; + +function ActorsList() { + const { records: actors, isResolving } = useEntityRecords( + 'activitypub/v1', + 'actor' + ); + + if ( isResolving ) { + return

Loading actors...

; + } + + return ( +
    + { actors?.map( ( actor ) => ( +
  • + { actor.name } (@{ actor.preferred_username }) +
  • + ) ) } +
+ ); +} +``` + +### 2. Fetch a Single Actor + +```javascript +import { useEntityRecord } from '@wordpress/core-data'; + +function ActorProfile( { actorId } ) { + const { record: actor, isResolving } = useEntityRecord( + 'activitypub/v1', + 'actor', + actorId + ); + + if ( isResolving ) { + return

Loading...

; + } + + if ( ! actor ) { + return

Actor not found

; + } + + return ( +
+

{ actor.name }

+

@{ actor.preferred_username }

+

Type: { actor.type }

+ { actor.summary &&

{ actor.summary }

} + View Profile +
+ ); +} +``` + +### 3. Use with Select/Dispatch + +```javascript +import { useSelect } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; + +function MyComponent() { + const actors = useSelect( ( select ) => { + return select( coreDataStore ).getEntityRecords( + 'activitypub/v1', + 'actor' + ); + }, [] ); + + // Your component logic here +} +``` + +### 4. Actor Type Filter + +```javascript +import { useEntityRecords } from '@wordpress/core-data'; + +function UserActorsList() { + const { records: actors } = useEntityRecords( + 'activitypub/v1', + 'actor' + ); + + // Filter to show only user actors (not blog or application) + const userActors = actors?.filter( ( actor ) => actor.type === 'user' ); + + return ( +
    + { userActors?.map( ( actor ) => ( +
  • { actor.name }
  • + ) ) } +
+ ); +} +``` + +### 5. Actor Selector Component + +```javascript +import { SelectControl } from '@wordpress/components'; +import { useEntityRecords } from '@wordpress/core-data'; + +function ActorSelector( { value, onChange } ) { + const { records: actors, isResolving } = useEntityRecords( + 'activitypub/v1', + 'actor' + ); + + if ( isResolving ) { + return ; + } + + const options = actors?.map( ( actor ) => ( { + label: `${ actor.name } (@${ actor.preferred_username })`, + value: actor.id, + } ) ) || []; + + return ( + + ); +} +``` + +## Actor IDs + +The plugin uses special IDs for non-user actors: + +- **User actors**: Positive integers (1, 2, 3, ...) +- **Blog actor**: `0` +- **Application actor**: `-1` + +## Permissions + +The internal endpoint requires authentication. Users must be logged in to fetch actor data. This is enforced by the REST API controller. + +## Implementation Details + +### PHP Side + +- **Controller**: `Activitypub\Rest\Internal_Actors_Controller` +- **Collection**: `Activitypub\Collection\Actors` +- **Route Registration**: `activitypub.php:rest_init()` + +### JavaScript Side + +- **Entity Registration**: `src/actors-entity/index.js` +- **Enqueuing**: `includes/class-blocks.php:enqueue_editor_assets()` + +## Testing + +You can test the REST API endpoint directly: + +```bash +# Get all actors +curl -X GET "http://localhost/wp-json/activitypub/v1/internal/actors" \ + --cookie "wordpress_logged_in_..." + +# Get specific actor +curl -X GET "http://localhost/wp-json/activitypub/v1/internal/actors/0" \ + --cookie "wordpress_logged_in_..." +``` + +## Extending the Entity + +If you need to add custom fields to actors, you can use WordPress filters: + +```php +// Add custom field to REST response +add_filter( 'activitypub_rest_prepare_actor', function( $response, $actor, $request ) { + $response->data['custom_field'] = get_user_meta( $actor->get_user_id(), 'custom_field', true ); + return $response; +}, 10, 3 ); +``` + +## Related + +- [WordPress Core Data Package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/) +- [ActivityPub Actors Collection](../../includes/collection/class-actors.php) +- [Internal Actors REST Controller](../../includes/rest/class-internal-actors-controller.php) diff --git a/src/internal/actors-entity/block.json b/src/internal/actors-entity/block.json new file mode 100644 index 0000000000..138795b72f --- /dev/null +++ b/src/internal/actors-entity/block.json @@ -0,0 +1,8 @@ +{ + "name": "actors-entity", + "title": "Actors Entity: Registers actors as WordPress Core Data entities", + "category": "widgets", + "icon": "admin-users", + "keywords": [], + "editorScript": "file:./index.js" +} diff --git a/src/internal/actors-entity/example-followers.js b/src/internal/actors-entity/example-followers.js new file mode 100644 index 0000000000..32d71e760e --- /dev/null +++ b/src/internal/actors-entity/example-followers.js @@ -0,0 +1,271 @@ +/** + * Example: Using the Actors Entity for Followers + * + * This demonstrates how to fetch followers using the actors entity + * with relationship filtering. + * + * NOTE: This is an example/documentation file only. + * + * @package Activitypub + */ + +/* eslint-disable */ + +/** + * WordPress dependencies + */ +import { useEntityRecords } from '@wordpress/core-data'; +import { Spinner } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +/** + * Example 1: Fetch followers for a user + * + * @param {Object} props Component props + * @param {number} props.userId User ID to fetch followers for + * @return {Element} Component displaying followers + */ +export function UserFollowers( { userId } ) { + const { + records: followers, + isResolving, + totalItems, + totalPages, + } = useEntityRecords( 'activitypub/v1', 'actor', { + relationship: 'followers', + user_id: userId, + per_page: 10, + page: 1, + order: 'desc', + } ); + + if ( isResolving ) { + return ; + } + + return ( +
+

Followers ({ totalItems })

+ { followers?.length ? ( +
    + { followers.map( ( follower ) => ( +
  • + { follower.icon?.url && } + { follower.name } @{ follower.preferred_username } +
  • + ) ) } +
+ ) : ( +

No followers found.

+ ) } +
+ ); +} + +/** + * Example 2: Followers list with pagination + * + * @param {Object} props Component props + * @param {number} props.userId User ID to fetch followers for + * @return {Element} Component with paginated followers + */ +export function PaginatedFollowers( { userId } ) { + const [ page, setPage ] = useState( 1 ); + const perPage = 10; + + const { + records: followers, + isResolving, + totalPages, + } = useEntityRecords( 'activitypub/v1', 'actor', { + relationship: 'followers', + user_id: userId, + per_page: perPage, + page, + order: 'desc', + } ); + + if ( isResolving ) { + return ; + } + + return ( +
+ + + { totalPages > 1 && ( +
+ + + Page { page } of { totalPages } + + +
+ ) } +
+ ); +} + +/** + * Example 3: Followers with search + * + * @param {Object} props Component props + * @param {number} props.userId User ID to fetch followers for + * @return {Element} Component with searchable followers + */ +export function SearchableFollowers( { userId } ) { + const [ searchTerm, setSearchTerm ] = useState( '' ); + const [ debouncedSearch, setDebouncedSearch ] = useState( '' ); + + // Debounce search + useEffect( () => { + const timer = setTimeout( () => { + setDebouncedSearch( searchTerm ); + }, 300 ); + return () => clearTimeout( timer ); + }, [ searchTerm ] ); + + const queryArgs = { + relationship: 'followers', + user_id: userId, + per_page: 20, + }; + + if ( debouncedSearch ) { + queryArgs.search = debouncedSearch; + } + + const { records: followers, isResolving } = useEntityRecords( 'activitypub/v1', 'actor', queryArgs ); + + return ( +
+ setSearchTerm( e.target.value ) } + /> + + { isResolving ? ( + + ) : ( +
    + { followers?.map( ( follower ) => ( +
  • + { follower.name } (@{ follower.preferred_username }) +
  • + ) ) } +
+ ) } +
+ ); +} + +/** + * Example 4: Followers and Following tabs + * + * @param {Object} props Component props + * @param {number} props.userId User ID + * @return {Element} Component with tabs for followers/following + */ +export function FollowersAndFollowing( { userId } ) { + const [ activeTab, setActiveTab ] = useState( 'followers' ); + + const { records, isResolving, totalItems } = useEntityRecords( 'activitypub/v1', 'actor', { + relationship: activeTab, + user_id: userId, + per_page: 20, + } ); + + return ( +
+
+ + +
+ + { isResolving ? ( + + ) : ( +
    + { records?.map( ( actor ) => ( +
  • + { actor.icon?.url && } +
    + { actor.name } +
    @{ actor.preferred_username } +
    +
  • + ) ) } +
+ ) } +
+ ); +} + +/** + * Example 5: Using with useSelect + * + * For more control over the data fetching + */ +import { useSelect } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; + +export function AdvancedFollowers( { userId } ) { + const { followers, isResolving, hasResolved } = useSelect( + ( select ) => { + const { + getEntityRecords, + isResolving: isResolvingSelector, + hasFinishedResolution, + } = select( coreDataStore ); + + const queryArgs = { + relationship: 'followers', + user_id: userId, + per_page: 10, + }; + + return { + followers: getEntityRecords( 'activitypub/v1', 'actor', queryArgs ), + isResolving: isResolvingSelector( 'getEntityRecords', [ 'activitypub/v1', 'actor', queryArgs ] ), + hasResolved: hasFinishedResolution( 'getEntityRecords', [ 'activitypub/v1', 'actor', queryArgs ] ), + }; + }, + [ userId ] + ); + + if ( isResolving && ! hasResolved ) { + return ; + } + + return ( +
    + { followers?.map( ( follower ) => ( +
  • { follower.name }
  • + ) ) } +
+ ); +} diff --git a/src/internal/actors-entity/example.js b/src/internal/actors-entity/example.js new file mode 100644 index 0000000000..5744946704 --- /dev/null +++ b/src/internal/actors-entity/example.js @@ -0,0 +1,185 @@ +/** + * Example: How to use the Actors Entity + * + * This file demonstrates various ways to use the actors entity + * registered with WordPress Core Data API. + * + * NOTE: This is an example/documentation file only. It is not built or included + * in the plugin. The examples below show how to use the actors entity in your + * own code. + * + * @package Activitypub + */ + +/* eslint-disable */ + +/** + * WordPress dependencies + */ +import { useEntityRecords, useEntityRecord } from '@wordpress/core-data'; +import { SelectControl, Spinner } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +/** + * Example 1: Display all actors in a list + * + * @return {Element} Component displaying all actors + */ +export function ActorsList() { + const { records: actors, isResolving } = useEntityRecords( 'activitypub/v1', 'actor' ); + + if ( isResolving ) { + return ; + } + + if ( ! actors || actors.length === 0 ) { + return

No actors found.

; + } + + return ( +
    + { actors.map( ( actor ) => ( +
  • + { actor.name } (@{ actor.preferred_username }){ ' ' } + ({ actor.type }) +
  • + ) ) } +
+ ); +} + +/** + * Example 2: Display a single actor with full details + * + * @param {Object} props Component props + * @param {number} props.actorId Actor ID to display + * @return {Element} Component displaying actor details + */ +export function ActorProfile( { actorId } ) { + const { record: actor, isResolving } = useEntityRecord( 'activitypub/v1', 'actor', actorId ); + + if ( isResolving ) { + return ; + } + + if ( ! actor ) { + return

Actor not found.

; + } + + return ( +
+ { actor.icon?.url && { } +

{ actor.name }

+

+ Username: @{ actor.preferred_username } +

+

+ Type: { actor.type } +

+ { actor.summary && ( +
+ ) } +

+ + View Profile + +

+

+ + ActivityPub ID: { actor.activitypub_id } + +

+
+ ); +} + +/** + * Example 3: Actor selector dropdown + * + * @param {Object} props Component props + * @param {number} props.value Selected actor ID + * @param {Function} props.onChange Callback when selection changes + * @return {Element} Select control for choosing an actor + */ +export function ActorSelector( { value, onChange } ) { + const { records: actors, isResolving } = useEntityRecords( 'activitypub/v1', 'actor' ); + + if ( isResolving ) { + return ; + } + + if ( ! actors || actors.length === 0 ) { + return ; + } + + const options = [ + { label: 'Select an actor...', value: '' }, + ...actors.map( ( actor ) => ( { + label: `${ actor.name } (@${ actor.preferred_username }) [${ actor.type }]`, + value: actor.id, + } ) ), + ]; + + return ; +} + +/** + * Example 4: Filter actors by type + * + * @param {Object} props Component props + * @param {string} props.type Actor type to filter ('user', 'blog', 'application') + * @return {Element} Component displaying filtered actors + */ +export function ActorsByType( { type } ) { + const { records: actors, isResolving } = useEntityRecords( 'activitypub/v1', 'actor' ); + + if ( isResolving ) { + return ; + } + + const filteredActors = actors?.filter( ( actor ) => actor.type === type ); + + if ( ! filteredActors || filteredActors.length === 0 ) { + return

No { type } actors found.

; + } + + return ( +
+

{ type.charAt( 0 ).toUpperCase() + type.slice( 1 ) } Actors

+
    + { filteredActors.map( ( actor ) => ( +
  • + { actor.name } (@{ actor.preferred_username }) +
  • + ) ) } +
+
+ ); +} + +/** + * Example 5: Complete example with state management + * + * @return {Element} Interactive component with actor selection + */ +export function ActorBrowser() { + const [ selectedActorId, setSelectedActorId ] = useState( '' ); + + return ( +
+

Actor Browser

+ + + + { selectedActorId && } + +
+ +

All Actors

+ +
+ ); +} diff --git a/src/internal/actors-entity/index.js b/src/internal/actors-entity/index.js new file mode 100644 index 0000000000..cb21e140f9 --- /dev/null +++ b/src/internal/actors-entity/index.js @@ -0,0 +1,54 @@ +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; + +/** + * Register the ActivityPub actors entity with WordPress Core Data API. + * + * This allows components to interact with actors using hooks like: + * - useEntityRecords( 'activitypub/1.0', 'actor' ) + * - useEntityRecord( 'activitypub/1.0', 'actor', id ) + */ +const registerActorEntity = () => { + const { registerEntityType } = dispatch( coreDataStore ); + + // Register the actor entity + registerEntityType( { + // The kind is typically the namespace of your REST endpoint + kind: 'activitypub/1.0', + + // The name is the entity type name + name: 'actor', + + // The label used in the UI + label: 'Actor', + + // The plural label + plural: 'Actors', + + // The base URL for the REST API endpoint + baseURL: '/wp-json/activitypub/1.0/internal/actors', + + // The key to use as the unique identifier + key: 'id', + + // Whether this is a transient entity (not saved to the database directly) + transientEdits: { + // Since this is read-only, mark all fields as transient + name: true, + preferred_username: true, + url: true, + icon: true, + summary: true, + activitypub_id: true, + }, + + // Define which methods are supported + supportsPagination: false, + } ); +}; + +// Register the entity when the script loads +registerActorEntity(); diff --git a/src/internal/posts-entity/block.json b/src/internal/posts-entity/block.json new file mode 100644 index 0000000000..d2f94d8a49 --- /dev/null +++ b/src/internal/posts-entity/block.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "activitypub/posts-entity", + "title": "ActivityPub Posts Entity", + "category": "widgets", + "description": "Registers ActivityPub posts as a WordPress Core Data entity.", + "textdomain": "activitypub", + "editorScript": "file:./index.js" +} diff --git a/src/internal/posts-entity/index.js b/src/internal/posts-entity/index.js new file mode 100644 index 0000000000..c682781f1d --- /dev/null +++ b/src/internal/posts-entity/index.js @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; + +/** + * Register the ActivityPub posts entity with WordPress Core Data API. + * + * This allows components to interact with ActivityPub posts using hooks like: + * - useEntityRecords( 'activitypub/1.0', 'post' ) + * - useEntityRecord( 'activitypub/1.0', 'post', id ) + */ +const registerPostEntity = () => { + const { registerEntityType } = dispatch( coreDataStore ); + + // Register the post entity + registerEntityType( { + // The kind is typically the namespace of your REST endpoint + kind: 'activitypub/1.0', + + // The name is the entity type name + name: 'post', + + // The label used in the UI + label: 'Post', + + // The plural label + plural: 'Posts', + + // The base URL for the REST API endpoint + baseURL: '/wp-json/activitypub/1.0/internal/posts', + + // The key to use as the unique identifier + key: 'wp_id', + + // Whether this is a transient entity (not saved to the database directly) + transientEdits: { + // Since this is read-only, mark all fields as transient + id: true, + type: true, + name: true, + content: true, + summary: true, + published: true, + wp_status: true, + }, + + // Define which methods are supported + supportsPagination: false, + } ); +}; + +// Register the entity when the script loads +registerPostEntity();