Repository: Automattic/wordpress-activitypub
Pull request #1691: Follow Me: Make it interactive!
Author: login: obenland
Labels: name: [Block] Follow Me
name: [Focus] Editor
description: Changes to the ActivityPub experience in the block editor
Created: 2025-05-11T14:54:05Z | Merged: 2025-05-21T13:35:39Z

Moves the Follow Me block to using the Interactivity API for front-end rendering. Removes the block's dependency on React in the frontend.

Requires a major version release, updates minimum required WordPress version to 6.5.


## Proposed changes:
<!--- Explain what functional changes your PR includes -->
* Replaces React frontend of Follow ME block with Interactivity API.

### Other information:

- [ ] Have you written new tests for your changes, if applicable?

## Testing instructions:
<!-- If you were reviewing this PR, how would you like the instructions to be presented? -->
<!-- Please include detailed testing steps, explaining how to test your change. -->
<!-- Bear in mind that context you working on is not obvious for everyone.  -->
<!-- Adding "simple" configuration steps will help reviewers to get to your PR as quickly as possible. -->
<!-- "Before / After" screenshots can also be very helpful when the change is visual. -->

* Go to the Editor and add a Follow Me block.
* Test it in the Editor and frontend and make sure it still works as expected.
* It will no-longer open the modal, when in the Editor.

### Changelog entry

<!-- You can optionally choose to enter a changelog entry by checking the box below and supplying data. -->
<!-- It will trigger a GitHub workflow that will create and push the entry into the branch. -->

<!-- Due to org permissions, the job may fail for PRs crated from a fork under GitHub organizations. -->
<!-- In this case, you can create entry manually with `composer changelog:add` and push it into the branch. -->

<!-- If no changelog entry is required for this PR, please add the "Skip Changelog" label. -->

-   [x] Automatically create a changelog entry from the details below.

<details>

<summary>Changelog Entry Details</summary>

#### Significance

<!-- Choose only one -->

-   [ ] Patch
-   [ ] Minor
-   [x] Major

#### Type

<!-- Choose only one -->

-   [ ] Added - for new features
-   [x] Changed - for changes in existing functionality
-   [ ] Deprecated - for soon-to-be removed features
-   [ ] Removed - for now removed features
-   [ ] Fixed - for any bug fixes
-   [ ] Security - in case of vulnerabilities

#### Message

The Follow Me block now uses the latest Block Editor technology for display on the frontend.

</details>
Server-side rendering what we canServer-side rendering what we can


Diff:
diff --git a/.github/changelog/1691-from-description b/.github/changelog/1691-from-description
new file mode 100644
index 000000000..129285f9f
--- /dev/null
+++ b/.github/changelog/1691-from-description
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+The Follow Me block now uses the latest Block Editor technology for display on the frontend.
diff --git a/.prettierignore b/.prettierignore
index 6e56f88ee..0121f2e2d 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -5,7 +5,6 @@ vendor
 
 # Temporary ignores while breaking out each component.
 assets
-src/follow-me
 src/followers
 src/reactions
 src/remote-reply
diff --git a/build/follow-me/block.json b/build/follow-me/block.json
index e799fbb58..14f41256a 100644
--- a/build/follow-me/block.json
+++ b/build/follow-me/block.json
@@ -2,14 +2,20 @@
   "$schema": "https://schemas.wp.org/trunk/block.json",
   "name": "activitypub/follow-me",
   "apiVersion": 3,
-  "version": "1.0.0",
+  "version": "2.0.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": {
+      "buttonOnly": false
+    }
+  },
   "supports": {
     "html": false,
+    "interactivity": true,
     "color": {
       "gradients": true,
       "link": true,
@@ -30,6 +36,11 @@
       "__experimentalDefaultControls": {
         "fontSize": true
       }
+    },
+    "innerBlocks": {
+      "allowedBlocks": [
+        "core/button"
+      ]
     }
   },
   "attributes": {
@@ -40,19 +51,6 @@
     "buttonOnly": {
       "type": "boolean",
       "default": false
-    },
-    "buttonText": {
-      "type": "string",
-      "default": "Follow"
-    },
-    "buttonSize": {
-      "type": "string",
-      "default": "default",
-      "enum": [
-        "small",
-        "default",
-        "compact"
-      ]
     }
   },
   "usesContext": [
@@ -60,9 +58,8 @@
     "postId"
   ],
   "editorScript": "file:./index.js",
-  "viewScript": "file:./view.js",
-  "style": [
-    "file:./style-view.css",
-    "wp-components"
-  ]
+  "viewScriptModule": "file:./view.js",
+  "viewScript": "wp-api-fetch",
+  "style": "file:./style-view.css",
+  "render": "file:./render.php"
 }
\ No newline at end of file
diff --git a/build/follow-me/index.asset.php b/build/follow-me/index.asset.php
index 59e5f6f9f..a40896d0e 100644
--- a/build/follow-me/index.asset.php
+++ b/build/follow-me/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'd69f8905fbe5ea6410fa');
+<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '49624f0cc66c91bddf95');
diff --git a/build/follow-me/index.js b/build/follow-me/index.js
index a0131f1b8..7b72534da 100644
--- a/build/follow-me/index.js
+++ b/build/follow-me/index.js
@@ -1,3 +1,2 @@
-(()=>{"use strict";var e,t={20:(e,t,o)=>{var r=o(609),l=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),a=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,o){var r,c={},u=null,s=null;for(r in void 0!==o&&(u=""+o),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=t.ref),t)n.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:l,type:e,key:u,ref:s,props:c,_owner:a.current}}},609:e=>{e.exports=window.React},848:(e,t,o)=>{e.exports=o(20)},919:(e,t,o)=>{const r=window.wp.blocks,l=window.wp.primitives;var n=o(848);const a=(0,n.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,n.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"})});var i=o(609);const c=window.wp.blockEditor,u=window.wp.i18n,s=window.wp.data,p=window.wp.coreData,d=window.wp.components,m=window.wp.element;function v(){return window._activityPubOptions||{}}const b=window.wp.apiFetch;var f=o.n(b);function y(e){return`var(--wp--preset--color--${e})`}function _(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|");return y(t)}function h(e,t,o=null,r=""){return o?`${e}${r} { ${t}: ${o}; }\n`:""}function w(e,t,o,r){return h(e,"background-color",t)+h(e,"color",o)+h(e,"background-color",r,":hover")+h(e,"background-color",r,":focus")}function g({selector:e,style:t,backgroundColor:o}){const r=function(e,t,o){const r=`${e} .components-button`,l=("string"==typeof(n=o)?y(n):n?.color?.background||null)||t?.color?.background;var n;return w(r,_(t?.elements?.link?.color?.text),l,_(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,o);return(0,i.createElement)("style",null,r)}const E=(0,n.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,n.jsx)(l.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),x=(0,n.jsx)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,n.jsx)(l.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),S=(0,m.forwardRef)((function({icon:e,size:t=24,...o},r){return(0,m.cloneElement)(e,{width:t,height:t,...o,ref:r})})),k=window.wp.compose,C="fediverse-remote-user";function O(e){try{return new URL(e),!0}catch(e){return!1}}function T({actionText:e,copyDescription:t,handle:o,resourceUrl:r,myProfile:l="",rememberProfile:n=!1}){const c=(0,u.__)("Loading...","activitypub"),s=(0,u.__)("Opening...","activitypub"),p=(0,u.__)("Error","activitypub"),v=(0,u.__)("Invalid","activitypub"),b=l||(0,u.__)("My Profile","activitypub"),[y,_]=(0,m.useState)(e),[h,w]=(0,m.useState)(E),g=(0,k.useCopyToClipboard)(o,(()=>{w(x),setTimeout((()=>w(E)),1e3)})),[T,N]=(0,m.useState)(""),[I,R]=(0,m.useState)(!0),{setRemoteUser:U}=function(){const[e,t]=(0,m.useState)(function(){const e=localStorage.getItem(C);return e?JSON.parse(e):{}}()),o=(0,m.useCallback)((e=>{!function(e){localStorage.setItem(C,JSON.stringify(e))}(e),t(e)}),[]),r=(0,m.useCallback)((()=>{localStorage.removeItem(C),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:o,deleteRemoteUser:r}}(),z=(0,m.useCallback)((()=>{let t;if(!O(T)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&O(`https://${t[1]}`)}(T))return _(v),t=setTimeout((()=>_(e)),2e3),()=>clearTimeout(t);const o=r+T;_(c),f()({path:o}).then((({url:t,template:o})=>{I&&U({profileURL:T,template:o}),_(s),setTimeout((()=>{window.open(t,"_blank"),_(e)}),200)})).catch((()=>{_(p),setTimeout((()=>_(e)),2e3)}))}),[T]);return(0,i.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",{id:"dialog-title"},b),(0,i.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,i.createElement)("input",{type:"text",id:"profile-handle",value:o,readOnly:!0}),(0,i.createElement)(d.Button,{ref:g,"aria-label":(0,u.__)("Copy handle to clipboard","activitypub")},(0,i.createElement)(S,{icon:h}),(0,u.__)("Copy","activitypub")))),(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",{id:"remote-profile-title"},(0,u.__)("Your Profile","activitypub")),(0,i.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,m.createInterpolateElement)((0,u.__)("Or, if you know your own profile, we can start things that way! (eg <code>@yourusername@example.com</code>)","activitypub"),{code:(0,i.createElement)("code",null)})),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,u.__)("Enter your ActivityPub profile","activitypub")),(0,i.createElement)("input",{type:"text",id:"remote-profile",value:T,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>N(e.target.value),"aria-invalid":y===v}),(0,i.createElement)(d.Button,{onClick:z,"aria-label":(0,u.__)("Submit profile","activitypub")},(0,i.createElement)(S,{icon:a}),y)),n&&(0,i.createElement)("div",{className:"activitypub-dialog__remember"},(0,i.createElement)(d.CheckboxControl,{checked:I,label:(0,u.__)("Remember me for easier comments","activitypub"),onChange:()=>{R(!I)}}))))}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function I(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:o,buttonText:r,buttonOnly:l,buttonSize:n}){const{webfinger:a,avatar:c,name:u}=e,s=a.startsWith("@")?a:`@${a}`;return l?(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:n})):(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,i.createElement)("div",{className:"activitypub-profile__content"},(0,i.createElement)("div",{className:"activitypub-profile__name"},u),(0,i.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:n}))}function U({profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:l}){const[n,a]=(0,m.useState)(!1),c=(0,u.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */
-(0,u.__)("Follow %s","activitypub"),e?.name);return(0,i.createElement)(i.Fragment,null,(0,i.createElement)(d.Button,{className:"activitypub-profile__follow",onClick:()=>a(!0),"aria-haspopup":"dialog","aria-expanded":n,"aria-label":(0,u.__)("Follow me on the Fediverse","activitypub"),size:l},r),n&&(0,i.createElement)(d.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>a(!1),title:c,"aria-label":c,role:"dialog"},(0,i.createElement)(z,{profile:e,userId:o}),(0,i.createElement)("style",null,t)))}function z({profile:e,userId:t}){const{namespace:o}=v(),{webfinger:r}=e,l=(0,u.__)("Follow","activitypub"),n=`/${o}/actors/${t}/remote-follow?resource=`,a=(0,u.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=r.startsWith("@")?r:`@${r}`;return(0,i.createElement)(T,{actionText:l,copyDescription:a,handle:c,resourceUrl:n})}function $({selectedUser:e,style:t,backgroundColor:o,id:r,useId:l=!1,profileData:n=!1,buttonOnly:a=!1,buttonText:c=(0,u.__)("Follow","activitypub"),buttonSize:s="default"}){const[p,d]=(0,m.useState)(I()),b="site"===e?0:e,y=function(e){return w(".apfmd__button-group .components-button",_(e?.elements?.link?.color?.text)||"#111","#fff",_(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=l?{id:r}:{};return(0,m.useEffect)((()=>{n?d(I(n)):function(e){const{namespace:t}=v(),o={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return f()(o)}(b).then((e=>{d(I(e))}))}),[b,n]),(0,i.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,i.createElement)(g,{selector:`#${r}`,style:t,backgroundColor:o}),(0,i.createElement)(R,{profile:p,userId:b,popupStyles:y,buttonText:c,buttonOnly:a,buttonSize:s}))}function P({name:e}){const{enabled:t}=v(),o=t?.site?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),r=(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 <strong>%1$s</strong> 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,o).trim();return(0,i.createElement)(d.Card,null,(0,i.createElement)(d.CardBody,null,(0,m.createInterpolateElement)(r,{strong:(0,i.createElement)("strong",null)})))}(0,r.registerBlockType)("activitypub/follow-me",{edit:function({attributes:e,setAttributes:t,context:{postType:o,postId:r}}){const l=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),n=function({withInherit:e=!1}){const{enabled:t}=v(),o=t?.users?(0,s.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,m.useMemo)((()=>{if(!o)return[];const r=[];return t?.site&&r.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&t?.users&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),o.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[o])}({withInherit:!0}),{selectedUser:a,buttonOnly:b,buttonText:f,buttonSize:y}=e,_="inherit"===a,h=(0,s.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),l=t("postType",o,r)?.author;return null!=l?l:null}),[o,r]);return(0,m.useEffect)((()=>{n.length&&(n.find((({value:e})=>e===a))||t({selectedUser:n[0].value}))}),[a,n]),(0,i.createElement)("div",{...l},(0,i.createElement)(c.InspectorControls,{key:"activitypub-follow-me"},(0,i.createElement)(d.PanelBody,{title:(0,u.__)("Follow Me Options","activitypub")},n.length>1&&(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:e.selectedUser,options:n,onChange:e=>t({selectedUser:e})}),(0,i.createElement)(d.ToggleControl,{label:(0,u.__)("Button Only Mode","activitypub"),checked:b,onChange:e=>t({buttonOnly:e}),help:(0,u.__)("Only show the follow button without profile information","activitypub")}),(0,i.createElement)(d.TextControl,{label:(0,u.__)("Button Text","activitypub"),value:f,onChange:e=>t({buttonText:e})}),(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Button Size","activitypub"),value:y,options:[{label:(0,u.__)("Default","activitypub"),value:"default"},{label:(0,u.__)("Compact","activitypub"),value:"compact"},{label:(0,u.__)("Small","activitypub"),value:"small"}],onChange:e=>t({buttonSize:e}),help:(0,u.__)("Choose the size of the follow button","activitypub")}))),_?h?(0,i.createElement)($,{...e,id:l.id,selectedUser:h}):(0,i.createElement)(P,{name:(0,u.__)("Follow Me","activitypub")}):(0,i.createElement)($,{...e,id:l.id}))},save:()=>null,icon:a})}},o={};function r(e){var l=o[e];if(void 0!==l)return l.exports;var n=o[e]={exports:{}};return t[e](n,n.exports,r),n.exports}r.m=t,e=[],r.O=(t,o,l,n)=>{if(!o){var a=1/0;for(s=0;s<e.length;s++){for(var[o,l,n]=e[s],i=!0,c=0;c<o.length;c++)(!1&n||a>=n)&&Object.keys(r.O).every((e=>r.O[e](o[c])))?o.splice(c--,1):(i=!1,n<a&&(a=n));if(i){e.splice(s--,1);var u=l();void 0!==u&&(t=u)}}return t}n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[o,l,n]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={338:0,301:0};r.O.j=t=>0===e[t];var t=(t,o)=>{var l,n,[a,i,c]=o,u=0;if(a.some((t=>0!==e[t]))){for(l in i)r.o(i,l)&&(r.m[l]=i[l]);if(c)var s=c(r)}for(t&&t(o);u<a.length;u++)n=a[u],r.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return r.O(s)},o=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];o.forEach(t.bind(null,0)),o.push=t.bind(null,o.push.bind(o))})();var l=r.O(void 0,[301],(()=>r(919)));l=r.O(l)})();
\ No newline at end of file
+(()=>{"use strict";var e={20:(e,t,n)=>{var r=n(609),o=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,n){var r,s={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,r)&&!i.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:o,type:e,key:c,ref:u,props:s,_owner:l.current}}},609:e=>{e.exports=window.React},848:(e,t,n)=>{e.exports=n(20)}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var a=t[r]={exports:{}};return e[r](a,a.exports,n),a.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);const r=window.wp.blocks,o=window.wp.primitives;var a=n(848);const l=(0,a.jsx)(o.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,a.jsx)(o.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"})}),i=[{attributes:{buttonOnly:{type:"boolean",default:!1},buttonText:{type:"string",default:"Follow"},selectedUser:{type:"string",default:"site"}},supports:{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}}},isEligible:e=>!!e.buttonText||!!e.buttonOnly,migrate(e){const{buttonText:t,...n}=e;return[n,[(0,r.createBlock)("core/button",{tagName:"button",text:t})]]}}];var s=n(609);const c=window.wp.apiFetch;var u=n.n(c);const p=window.wp.blockEditor,d=window.wp.i18n,v=window.wp.data,w=window.wp.coreData,b=window.wp.components,f=window.wp.element;function m(){return window._activityPubOptions||{}}function y({name:e}){const{enabled:t}=m(),n=t?.site?"":(0,d.__)("It will be empty in other non-author contexts.","activitypub"),r=(0,d.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,d.__)("This <strong>%1$s</strong> 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,n).trim();return(0,s.createElement)(b.Card,null,(0,s.createElement)(b.CardBody,null,(0,f.createInterpolateElement)(r,{strong:(0,s.createElement)("strong",null)})))}const _={avatar:"https://secure.gravatar.com/avatar/default?s=120",webfinger:"@well@hello.dolly",name:(0,d.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function h(e){if(!e)return _;const t={..._,...e};return t.avatar=t?.icon?.url,t.webfinger&&!t.webfinger.startsWith("@")&&(t.webfinger="@"+t.webfinger),t}function g({profile:e,buttonOnly:t,innerBlocksProps:n}){const{webfinger:r,avatar:o,name:a}=e;return t?(0,s.createElement)("div",{className:"activitypub-profile"},(0,s.createElement)("div",{...n})):(0,s.createElement)("div",{className:"activitypub-profile"},(0,s.createElement)("img",{className:"activitypub-profile__avatar",src:o,alt:a}),(0,s.createElement)("div",{className:"activitypub-profile__content"},(0,s.createElement)("div",{className:"activitypub-profile__name"},a),(0,s.createElement)("div",{className:"activitypub-profile__handle",title:r},r)),(0,s.createElement)("div",{...n}))}(0,r.registerBlockType)("activitypub/follow-me",{deprecated:i,edit:function({attributes:e,setAttributes:t,context:{postType:n,postId:r}}){const o=(0,p.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),a=function({withInherit:e=!1}){const{enabled:t}=m(),n=t?.users?(0,v.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,f.useMemo)((()=>{if(!n)return[];const r=[];return t?.site&&r.push({label:(0,d.__)("Site","activitypub"),value:"site"}),e&&t?.users&&r.push({label:(0,d.__)("Dynamic User","activitypub"),value:"inherit"}),n.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[n])}({withInherit:!0}),{selectedUser:l,buttonOnly:i}=e,c="inherit"===l,[E,k]=(0,f.useState)(h(_)),x="site"===l?0:l,O=[["core/button",{text:(0,d.__)("Follow","activitypub"),tagName:"button"}]],B=(0,p.useInnerBlocksProps)({},{allowedBlocks:["core/button"],template:O,templateLock:!1,renderAppender:!1}),P=(0,v.useSelect)((e=>{const{getEditedEntityRecord:t}=e(w.store),o=t("postType",n,r)?.author;return null!=o?o:null}),[n,r]);return(0,f.useEffect)((()=>{c&&!P||function(e){const{namespace:t}=m(),n={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return u()(n)}(c?P:x).then((e=>{k(h(e))}))}),[x,P,c]),(0,f.useEffect)((()=>{a.length&&(a.find((({value:e})=>e===l))||t({selectedUser:a[0].value}))}),[l,a]),(0,s.createElement)("div",{...o},(0,s.createElement)(p.InspectorControls,{key:"activitypub-follow-me"},(0,s.createElement)(b.PanelBody,{title:(0,d.__)("Follow Me Options","activitypub")},a.length>1&&(0,s.createElement)(b.SelectControl,{label:(0,d.__)("Select User","activitypub"),value:e.selectedUser,options:a,onChange:e=>t({selectedUser:e})}),(0,s.createElement)(b.ToggleControl,{label:(0,d.__)("Button Only Mode","activitypub"),checked:i,onChange:e=>t({buttonOnly:e}),help:(0,d.__)("Only show the follow button without profile information","activitypub")}))),c&&!P?(0,s.createElement)(y,{name:(0,d.__)("Follow Me","activitypub")}):(0,s.createElement)(g,{profile:E,userId:c?P:x,buttonOnly:i,innerBlocksProps:B}))},icon:l,save:function(){const e=p.useBlockProps.save(),t=p.useInnerBlocksProps.save(e);return(0,s.createElement)("div",{...t})}})})();
\ No newline at end of file
diff --git a/build/follow-me/render.php b/build/follow-me/render.php
new file mode 100644
index 000000000..36bd24531
--- /dev/null
+++ b/build/follow-me/render.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Server-side rendering of the `activitypub/follow-me` block.
+ *
+ * @package ActivityPub
+ */
+
+use Activitypub\Blocks;
+use Activitypub\Collection\Actors;
+
+/* @var array $attributes Block attributes. */
+$attributes = wp_parse_args( $attributes );
+
+// Get the user ID from the selected user attribute.
+$selected_user = $attributes['selectedUser'] ?? 'site';
+$user_id       = Blocks::get_user_id( $selected_user );
+$button_only   = $attributes['buttonOnly'] ?? false;
+
+// Generate a unique ID for the block.
+$block_id = 'activitypub-follow-me-block-' . wp_unique_id();
+
+// Get block style information.
+$style            = wp_get_global_styles();
+$background_color = $attributes['backgroundColor'] ?? $style['color']['background'] ?? '';
+
+// Get button style from block attributes.
+$button_style = $attributes['style'] ?? array();
+
+$actor = Actors::get_by_id( $user_id );
+if ( is_wp_error( $actor ) ) {
+	return;
+}
+
+// Set up the Interactivity API state.
+$state = wp_interactivity_state(
+	'activitypub/follow-me',
+	array(
+		'namespace' => ACTIVITYPUB_REST_NAMESPACE,
+		'i18n'      => array(
+			'copied'              => __( 'Copied!', 'activitypub' ),
+			'copy'                => __( 'Copy', 'activitypub' ),
+			'emptyProfileError'   => __( 'Please enter a profile URL or handle.', 'activitypub' ),
+			'invalidProfileError' => __( 'Please enter a valid URL or handle.', 'activitypub' ),
+			'genericError'        => __( 'An error occurred. Please try again.', 'activitypub' ),
+		),
+	)
+);
+
+// Add the block wrapper attributes.
+$wrapper_attributes = get_block_wrapper_attributes(
+	array(
+		'id'                           => $block_id,
+		'class'                        => 'activitypub-follow-me-block-wrapper',
+		'data-wp-interactive'          => 'activitypub/follow-me',
+		'data-wp-init'                 => 'callbacks.initButtonStyles',
+		'data-wp-on-document--keydown' => 'callbacks.documentKeydown',
+		'data-wp-on-document--click'   => 'callbacks.documentClick',
+	)
+);
+
+$wrapper_context = wp_interactivity_data_wp_context(
+	array(
+		'blockId'         => $block_id,
+		'isModalOpen'     => false,
+		'remoteProfile'   => '',
+		'isLoading'       => false,
+		'isError'         => false,
+		'errorMessage'    => '',
+		'copyButtonText'  => $state['i18n']['copy'],
+		'userId'          => $user_id,
+		'buttonOnly'      => $button_only,
+		'buttonStyle'     => $button_style,
+		'backgroundColor' => $background_color,
+		'webfinger'       => '@' . $actor->get_webfinger(),
+	)
+);
+
+/* @var string $content Inner blocks content. */
+if ( empty( $content ) ) {
+	$button_text = $attributes['buttonText'] ?? __( 'Follow', 'activitypub' );
+	$content     = '<div class="wp-block-button"><button class="wp-block-button__link wp-element-button">' . esc_html( $button_text ) . '</button></div>';
+}
+$content = Blocks::add_directions(
+	$content,
+	array( 'class_name' => 'wp-element-button' ),
+	array(
+		'data-wp-on--click'           => 'actions.toggleModal',
+		'data-wp-bind--aria-expanded' => 'context.isModalOpen',
+		'aria-label'                  => __( 'Follow me on the Fediverse', 'activitypub' ),
+		'aria-haspopup'               => 'dialog',
+		'aria-controls'               => 'modal-heading',
+	)
+);
+
+?>
+<div
+	<?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput ?>
+	<?php echo $wrapper_context; // phpcs:ignore WordPress.Security.EscapeOutput ?>
+>
+	<div class="activitypub-profile">
+		<?php if ( ! $button_only ) : ?>
+			<img
+				class="activitypub-profile__avatar"
+				src="<?php echo esc_url( $actor->get_icon()['url'] ); ?>"
+				alt="<?php echo esc_attr( $actor->get_name() ); ?>"
+			/>
+			<div class="activitypub-profile__content">
+				<div class="activitypub-profile__name"><?php echo esc_html( $actor->get_name() ); ?></div>
+				<div class="activitypub-profile__handle"><?php echo esc_html( '@' . $actor->get_webfinger() ); ?></div>
+			</div>
+		<?php endif; ?>
+
+		<?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput ?>
+	</div>
+
+	<div
+		class="activitypub-modal__overlay"
+		data-wp-bind--hidden="!context.isModalOpen"
+		role="dialog"
+		aria-modal="true"
+		aria-labelledby="modal-heading"
+	>
+		<div class="activitypub-modal__frame">
+			<div class="activitypub-modal__header">
+				<h2 id="modal-heading" class="activitypub-modal__title">
+					<?php
+					printf(
+						/* translators: %s: Profile name. */
+						esc_html__( 'Follow %s', 'activitypub' ),
+						esc_html( $actor->get_name() )
+					);
+					?>
+				</h2>
+				<button
+					type="button"
+					class="activitypub-modal__close wp-element-button wp-block-button__link"
+					data-wp-on--click="actions.closeModal"
+					aria-label="<?php echo esc_attr__( 'Close dialog', 'activitypub' ); ?>"
+				>
+					<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" role="img" aria-hidden="true" focusable="false">
+						<path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path>
+					</svg>
+				</button>
+			</div>
+			<div class="activitypub-modal__content">
+				<div class="activitypub-dialog__section">
+					<h4><?php echo esc_html__( 'My Profile', 'activitypub' ); ?></h4>
+					<div class="activitypub-dialog__description">
+						<?php echo esc_html__( 'Copy and paste my profile into the search field of your favorite fediverse app or server.', 'activitypub' ); ?>
+					</div>
+					<div class="activitypub-dialog__button-group">
+						<input
+							type="text"
+							id="profile-handle"
+							value="<?php echo esc_attr( '@' . $actor->get_webfinger() ); ?>"
+							tabindex="-1"
+							readonly
+							aria-readonly="true"
+						/>
+						<button
+							type="button"
+							class="wp-element-button wp-block-button__link"
+							data-wp-on--click="actions.copyToClipboard"
+							aria-label="<?php echo esc_attr__( 'Copy handle to clipboard', 'activitypub' ); ?>"
+						>
+							<span data-wp-text="context.copyButtonText"></span>
+						</button>
+					</div>
+				</div>
+				<div class="activitypub-dialog__section">
+					<h4><?php echo esc_html__( 'Your Profile', 'activitypub' ); ?></h4>
+					<div class="activitypub-dialog__description">
+						<?php echo esc_html__( 'Or, if you know your own profile, we can start things that way!', 'activitypub' ); ?>
+					</div>
+					<div class="activitypub-dialog__button-group">
+						<input
+							type="text"
+							id="remote-profile"
+							placeholder="<?php echo esc_attr__( '@username@example.com', 'activitypub' ); ?>"
+							data-wp-bind--value="context.remoteProfile"
+							data-wp-on--input="actions.updateRemoteProfile"
+							data-wp-on--keydown="actions.handleKeyDown"
+							data-wp-bind--aria-invalid="context.isError"
+						/>
+						<button
+							type="button"
+							class="wp-element-button wp-block-button__link"
+							data-wp-on--click="actions.submitRemoteProfile"
+							aria-label="<?php echo esc_attr__( 'Follow', 'activitypub' ); ?>"
+							data-wp-bind--disabled="context.isLoading"
+						>
+							<span data-wp-bind--hidden="context.isLoading">
+								<?php echo esc_html__( 'Follow', 'activitypub' ); ?>
+							</span>
+							<span data-wp-bind--hidden="!context.isLoading">
+								<?php echo esc_html__( 'Loading...', 'activitypub' ); ?>
+							</span>
+						</button>
+					</div>
+					<div
+						class="activitypub-dialog__error"
+						data-wp-bind--hidden="!context.isError"
+						data-wp-text="context.errorMessage"
+					></div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<?php
diff --git a/build/follow-me/style-view-rtl.css b/build/follow-me/style-view-rtl.css
deleted file mode 100644
index eb3c85089..000000000
--- a/build/follow-me/style-view-rtl.css
+++ /dev/null
@@ -1 +0,0 @@
-.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-right:1rem}
diff --git a/build/follow-me/style-view.css b/build/follow-me/style-view.css
index 1c28cab0d..7b057ab96 100644
--- a/build/follow-me/style-view.css
+++ b/build/follow-me/style-view.css
@@ -1 +1 @@
-.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-left:1rem}
+.activitypub-follow-me-block-wrapper{display:block;position:relative}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;-o-object-fit:cover;object-fit:cover;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile__content{flex:1;margin-right:1rem;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__name{font-size:1.25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile__name{color:inherit;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button{align-items:center;display:flex;margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button:not(:only-child){margin-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button__link{margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .is-small{font-size:.8rem;padding:.25rem .5rem}.activitypub-follow-me-block-wrapper .activitypub-profile .is-compact{font-size:.9rem;padding:.4rem .8rem}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border .activitypub-profile{padding-left:1rem;padding-right:1rem}body.modal-open{overflow:hidden}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;left:0;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white);border-radius:4px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.activitypub-dialog__section{border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;font-size:95%;margin-bottom:1rem}.activitypub-dialog__button-group{display:flex;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{border:1px solid var(--wp--preset--color--gray,#e2e4e7);border-radius:4px 0 0 4px;flex:1;line-height:1;margin:0}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red)}.activitypub-dialog__button-group button{border-radius:0 4px 4px 0!important;margin-left:-1px!important;min-width:22.5%;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red);font-size:90%;margin-top:.5rem}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
diff --git a/build/follow-me/view.asset.php b/build/follow-me/view.asset.php
index b2a00fcd5..e05081792 100644
--- a/build/follow-me/view.asset.php
+++ b/build/follow-me/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '4e430690e282cfe013db');
+<?php return array('dependencies' => array('@wordpress/interactivity'), 'version' => '66f75ff35a5e5e082c49', 'type' => 'module');
diff --git a/build/follow-me/view.js b/build/follow-me/view.js
index 7d4de0582..4862dc41e 100644
--- a/build/follow-me/view.js
+++ b/build/follow-me/view.js
@@ -1,2 +1 @@
-(()=>{"use strict";var e,t={5:(e,t,r)=>{var o=r(609);const a=window.wp.element,n=window.wp.domReady;var i=r.n(n);const l=window.wp.apiFetch;var c=r.n(l);const u=window.wp.components,s=window.wp.i18n;function p(e){return`var(--wp--preset--color--${e})`}function m(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|");return p(t)}function d(e,t,r=null,o=""){return r?`${e}${o} { ${t}: ${r}; }\n`:""}function v(e,t,r,o){return d(e,"background-color",t)+d(e,"color",r)+d(e,"background-color",o,":hover")+d(e,"background-color",o,":focus")}function f({selector:e,style:t,backgroundColor:r}){const a=function(e,t,r){const o=`${e} .components-button`,a=("string"==typeof(n=r)?p(n):n?.color?.background||null)||t?.color?.background;var n;return v(o,m(t?.elements?.link?.color?.text),a,m(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,r);return(0,o.createElement)("style",null,a)}const b=window.wp.primitives;var y=r(848);const _=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),w=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),h=(0,a.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,a.cloneElement)(e,{width:t,height:t,...r,ref:o})})),g=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.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"})}),E=window.wp.compose,x="fediverse-remote-user";function S(e){try{return new URL(e),!0}catch(e){return!1}}function k({actionText:e,copyDescription:t,handle:r,resourceUrl:n,myProfile:i="",rememberProfile:l=!1}){const p=(0,s.__)("Loading...","activitypub"),m=(0,s.__)("Opening...","activitypub"),d=(0,s.__)("Error","activitypub"),v=(0,s.__)("Invalid","activitypub"),f=i||(0,s.__)("My Profile","activitypub"),[b,y]=(0,a.useState)(e),[k,O]=(0,a.useState)(_),N=(0,E.useCopyToClipboard)(r,(()=>{O(w),setTimeout((()=>O(_)),1e3)})),[C,R]=(0,a.useState)(""),[T,I]=(0,a.useState)(!0),{setRemoteUser:$}=function(){const[e,t]=(0,a.useState)(function(){const e=localStorage.getItem(x);return e?JSON.parse(e):{}}()),r=(0,a.useCallback)((e=>{!function(e){localStorage.setItem(x,JSON.stringify(e))}(e),t(e)}),[]),o=(0,a.useCallback)((()=>{localStorage.removeItem(x),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}(),z=(0,a.useCallback)((()=>{let t;if(!S(C)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&S(`https://${t[1]}`)}(C))return y(v),t=setTimeout((()=>y(e)),2e3),()=>clearTimeout(t);const r=n+C;y(p),c()({path:r}).then((({url:t,template:r})=>{T&&$({profileURL:C,template:r}),y(m),setTimeout((()=>{window.open(t,"_blank"),y(e)}),200)})).catch((()=>{y(d),setTimeout((()=>y(e)),2e3)}))}),[C]);return(0,o.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"dialog-title"},f),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,o.createElement)("input",{type:"text",id:"profile-handle",value:r,readOnly:!0}),(0,o.createElement)(u.Button,{ref:N,"aria-label":(0,s.__)("Copy handle to clipboard","activitypub")},(0,o.createElement)(h,{icon:k}),(0,s.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"remote-profile-title"},(0,s.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,a.createInterpolateElement)((0,s.__)("Or, if you know your own profile, we can start things that way! (eg <code>@yourusername@example.com</code>)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,s.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:C,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>R(e.target.value),"aria-invalid":b===v}),(0,o.createElement)(u.Button,{onClick:z,"aria-label":(0,s.__)("Submit profile","activitypub")},(0,o.createElement)(h,{icon:g}),b)),l&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(u.CheckboxControl,{checked:T,label:(0,s.__)("Remember me for easier comments","activitypub"),onChange:()=>{I(!T)}}))))}function O(){return window._activityPubOptions||{}}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,s.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function C(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:r,buttonText:a,buttonOnly:n,buttonSize:i}){const{webfinger:l,avatar:c,name:u}=e,s=l.startsWith("@")?l:`@${l}`;return n?(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)(T,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i})):(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,o.createElement)("div",{className:"activitypub-profile__content"},(0,o.createElement)("div",{className:"activitypub-profile__name"},u),(0,o.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,o.createElement)(T,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i}))}function T({profile:e,popupStyles:t,userId:r,buttonText:n,buttonSize:i}){const[l,c]=(0,a.useState)(!1),p=(0,s.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */
-(0,s.__)("Follow %s","activitypub"),e?.name);return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(u.Button,{className:"activitypub-profile__follow",onClick:()=>c(!0),"aria-haspopup":"dialog","aria-expanded":l,"aria-label":(0,s.__)("Follow me on the Fediverse","activitypub"),size:i},n),l&&(0,o.createElement)(u.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>c(!1),title:p,"aria-label":p,role:"dialog"},(0,o.createElement)(I,{profile:e,userId:r}),(0,o.createElement)("style",null,t)))}function I({profile:e,userId:t}){const{namespace:r}=O(),{webfinger:a}=e,n=(0,s.__)("Follow","activitypub"),i=`/${r}/actors/${t}/remote-follow?resource=`,l=(0,s.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=a.startsWith("@")?a:`@${a}`;return(0,o.createElement)(k,{actionText:n,copyDescription:l,handle:c,resourceUrl:i})}function $({selectedUser:e,style:t,backgroundColor:r,id:n,useId:i=!1,profileData:l=!1,buttonOnly:u=!1,buttonText:p=(0,s.__)("Follow","activitypub"),buttonSize:d="default"}){const[b,y]=(0,a.useState)(C()),_="site"===e?0:e,w=function(e){return v(".apfmd__button-group .components-button",m(e?.elements?.link?.color?.text)||"#111","#fff",m(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=i?{id:n}:{};return(0,a.useEffect)((()=>{l?y(C(l)):function(e){const{namespace:t}=O(),r={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return c()(r)}(_).then((e=>{y(C(e))}))}),[_,l]),(0,o.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,o.createElement)(f,{selector:`#${n}`,style:t,backgroundColor:r}),(0,o.createElement)(R,{profile:b,userId:_,popupStyles:w,buttonText:p,buttonOnly:u,buttonSize:d}))}let z=1;i()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follow-me-block-wrapper"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)($,{...t,id:"activitypub-follow-me-block-"+z++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},u=null,s=null;for(o in void 0!==r&&(u=""+r),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=t.ref),t)n.call(t,o)&&!l.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:u,ref:s,props:c,_owner:i.current}}},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var n=r[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,r,a,n)=>{if(!r){var i=1/0;for(s=0;s<e.length;s++){for(var[r,a,n]=e[s],l=!0,c=0;c<r.length;c++)(!1&n||i>=n)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(l=!1,n<i&&(i=n));if(l){e.splice(s--,1);var u=a();void 0!==u&&(t=u)}}return t}n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[r,a,n]},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={41:0,301:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var a,n,[i,l,c]=r,u=0;if(i.some((t=>0!==e[t]))){for(a in l)o.o(l,a)&&(o.m[a]=l[a]);if(c)var s=c(o)}for(t&&t(r);u<i.length;u++)n=i[u],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(s)},r=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var a=o.O(void 0,[301],(()=>o(5)));a=o.O(a)})();
\ No newline at end of file
+import*as e from"@wordpress/interactivity";var t,o,n={825:(t,o,n)=>{const r=(i={getContext:()=>e.getContext,store:()=>e.store},c={},n.d(c,i),c),l={computedStyles:null,variables:{}};var i,c;function s(e){if("undefined"==typeof window||!window.getComputedStyle)return!1;if(l.variables.hasOwnProperty(e))return l.variables[e];l.computedStyles||(l.computedStyles=window.getComputedStyle(document.documentElement));const t=l.computedStyles.getPropertyValue(e).trim();return l.variables[e]=""!==t,l.variables[e]}function a(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|"),o=`--wp--preset--color--${t}`;return s(o)?`var(${o})`:null}function d(e,t,o=null,n=""){return o?`${e}${n} { ${t}: ${o}; }\n`:""}function u(e,t,o,n){return d(e,"background-color",t)+d(e,"color",o)+d(e,"background-color",n,":hover")+d(e,"background-color",n,":focus")}const{apiFetch:p}=window.wp,{state:m,actions:f,utils:y}=(0,r.store)("activitypub/follow-me",{actions:{openModal(){const e=(0,r.getContext)();e.isModalOpen=!0,document.body.classList.add("modal-open"),setTimeout((()=>{const t=document.getElementById(e.blockId);if(t){const e=t.querySelector(".activitypub-modal__frame");e&&y.trapFocus(e)}}),50)},closeModal(){const e=(0,r.getContext)();e.isModalOpen=!1,e.isError=!1,document.body.classList.remove("modal-open");const t=document.getElementById(e.blockId);if(t){const e=t.querySelector(".wp-block-button__link");e&&e.focus()}},toggleModal(){const{isModalOpen:e}=(0,r.getContext)();e?f.closeModal():f.openModal()},copyToClipboard(){const e=(0,r.getContext)();navigator.clipboard.writeText(e.webfinger).then((()=>{e.copyButtonText=m.i18n.copied,setTimeout((()=>{e.copyButtonText=m.i18n.copy}),1e3)}),(e=>{console.error("Could not copy text: ",e)}))},updateRemoteProfile(e){const t=(0,r.getContext)();t.remoteProfile=e.target.value,t.isError=!1,t.errorMessage=""},handleKeyDown(e){"Enter"===e.key&&(e.preventDefault(),f.submitRemoteProfile())},submitRemoteProfile:function*(){const e=(0,r.getContext)(),{namespace:t}=m,o=e.remoteProfile.trim();if(!o)return e.isError=!0,void(e.errorMessage=m.i18n.emptyProfileError);if(!y.isHandle(o))return e.isError=!0,void(e.errorMessage=m.i18n.invalidProfileError);e.isLoading=!0,e.isError=!1;const n=`/${t}/actors/${e.userId}/remote-follow?resource=${encodeURIComponent(o)}`;try{const t=yield p({path:n});e.isLoading=!1,window.open(t.url,"_blank"),f.closeModal()}catch(t){console.error("Error submitting profile:",t),e.isLoading=!1,e.isError=!0,e.errorMessage=t.message||m.i18n.genericError}}},callbacks:{initButtonStyles:()=>{const{buttonStyle:e,backgroundColor:t,blockId:o}=(0,r.getContext)();if(o&&e){const r=document.createElement("style"),l=`#${o}`;r.textContent=function(e,t,o){const n=`${e} .wp-block-button__link`,r=function(e){if("string"==typeof e){const t=`--wp--preset--color--${e}`;return s(t)?`var(${t})`:null}return e?.color?.background||null}(o)||t?.color?.background;return u(n,a(t?.elements?.link?.color?.text),r,a(t?.elements?.link?.[":hover"]?.color?.text))}(l,e,t),document.head.appendChild(r);const i=document.createElement("style");i.textContent=(n=e,u(".activitypub-dialog__button-group .wp-block-button",a(n?.elements?.link?.color?.text)||"#111","#fff",a(n?.elements?.link?.[":hover"]?.color?.text)||"#333")),document.head.appendChild(i)}var n},documentKeydown(e){const{isModalOpen:t}=(0,r.getContext)();t&&"Escape"===e.key&&f.closeModal()},documentClick(e){const{blockId:t,isModalOpen:o}=(0,r.getContext)();if(!o)return;const n=document.getElementById(t);if(!n)return;const l=n.querySelector('.wp-element-button[data-wp-on--click="actions.toggleModal"]');if(l&&(l===e.target||l.contains(e.target)))return;const i=n.querySelector(".activitypub-modal__frame");i&&!i.contains(e.target)&&f.closeModal()}},utils:{isHandle(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&y.isUrl(`https://${t[1]}`)},isUrl(e){try{return new URL(e),!0}catch(e){return!1}}},trapFocus(e){const t=e.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),o=t[0],n=t[t.length-1];o&&o.classList.contains("activitypub-modal__close")&&t.length>1?t[1].focus():o.focus(),e.addEventListener("keydown",(function(e){"Tab"!==e.key&&9!==e.keyCode||(e.shiftKey?document.activeElement===o&&(n.focus(),e.preventDefault()):document.activeElement===n&&(o.focus(),e.preventDefault()))}))}})}},r={};function l(e){var t=r[e];if(void 0!==t)return t.exports;var o=r[e]={exports:{}};return n[e](o,o.exports,l),o.exports}l.m=n,t=[],l.O=(e,o,n,r)=>{if(!o){var i=1/0;for(d=0;d<t.length;d++){for(var[o,n,r]=t[d],c=!0,s=0;s<o.length;s++)(!1&r||i>=r)&&Object.keys(l.O).every((e=>l.O[e](o[s])))?o.splice(s--,1):(c=!1,r<i&&(i=r));if(c){t.splice(d--,1);var a=n();void 0!==a&&(e=a)}}return e}r=r||0;for(var d=t.length;d>0&&t[d-1][2]>r;d--)t[d]=t[d-1];t[d]=[o,n,r]},l.d=(e,t)=>{for(var o in t)l.o(t,o)&&!l.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},l.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o={41:0,301:0},l.O.j=e=>0===o[e];var i=l.O(void 0,[301],(()=>l(825)));i=l.O(i);
\ No newline at end of file
diff --git a/build/reply/index.asset.php b/build/reply/index.asset.php
index 9f4117fee..302b07b9f 100644
--- a/build/reply/index.asset.php
+++ b/build/reply/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '78dbc26b5e405051df4a');
+<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'e4cc0f6ffd681f4d678d');
diff --git a/build/reply/index.js b/build/reply/index.js
index c8ee59e4e..c9b6b8200 100644
--- a/build/reply/index.js
+++ b/build/reply/index.js
@@ -1 +1 @@
-(()=>{"use strict";var e,t={20:(e,t,r)=>{var n=r(609),o=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var n,c={},s=null,d=null;for(n in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(d=t.ref),t)a.call(t,n)&&!l.hasOwnProperty(n)&&(c[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===c[n]&&(c[n]=t[n]);return{$$typeof:o,type:e,key:s,ref:d,props:c,_owner:i.current}}},238:(e,t,r)=>{const n=window.wp.blocks,o=window.wp.primitives;var a=r(848);const i=(0,a.jsx)(o.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,a.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"})});var l=r(609);const c=window.wp.blockEditor,s=window.wp.components,d=window.wp.i18n,u=window.wp.element,m=window.wp.compose,p=window.wp.apiFetch;var f=r.n(p);const h=window.wp.url,w=window.wp.data;function b({html:e}){const t=(0,u.useRef)(null),[r,n]=(0,u.useState)(300),o=(0,u.useRef)(300),a=(0,u.useCallback)((()=>{if(t.current)try{const e=t.current;let r=300;try{e.contentDocument&&e.contentDocument.body?r=e.contentDocument.body.scrollHeight:e.contentWindow&&e.contentWindow.document&&e.contentWindow.document.body&&(r=e.contentWindow.document.body.scrollHeight)}catch(e){console.log("Could not access iframe content document:",e)}r+=5,Math.abs(r-o.current)>5&&(o.current=r,n(r))}catch(e){console.error("Error adjusting iframe height:",e)}}),[]),i=(0,u.useCallback)((()=>{if(t.current)try{a()}catch(e){console.error("Error setting up iframe height adjustment:",e)}}),[a]);return(0,u.useEffect)((()=>{t.current&&t.current.addEventListener("load",i);const e=setInterval(a,1e3);return()=>{clearInterval(e),t.current&&t.current.removeEventListener("load",i)}}),[i,a]),(0,u.useEffect)((()=>{if(t.current){const e=setTimeout((()=>{a()}),100);return()=>clearTimeout(e)}}),[e,a]),{iframeRef:t,iframeHeight:r,adjustIframeHeight:a,handleIframeLoad:i}}const v={class:"className",frameborder:"frameBorder",allowfullscreen:"allowFullScreen",allowtransparency:"allowTransparency",marginheight:"marginHeight",marginwidth:"marginWidth"};function y({onClick:e}){return(0,l.createElement)("div",{className:"activitypub-embed-overlay",onClick:e,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1}})}function g({html:e,onSelectBlock:t}){const r=(0,u.useRef)(),[n,o]=(0,u.useState)(282),[a,i]=(0,u.useState)(!1),c=(0,u.useCallback)((()=>{const t=(new window.DOMParser).parseFromString(e,"text/html").querySelector("iframe"),r={};return t?(Array.from(t.attributes).forEach((({name:e,value:t})=>{"style"!==e&&(r[v[e]||e]=t)})),r):r}),[e]),s=c();return(0,u.useEffect)((()=>{if(!r.current)return;const{ownerDocument:e}=r.current,{defaultView:t}=e;function n({data:{secret:e,message:t,value:r}={}}){"height"===t&&e===s["data-secret"]&&o(r)}return t.addEventListener("message",n),()=>{t.removeEventListener("message",n)}}),[s]),s.src?(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:r,title:s.title||(0,d.__)("Embedded WordPress content","activitypub"),...s,height:n,style:{width:"100%",maxWidth:"100%"}}),!a&&(0,l.createElement)(y,{onClick:t})):(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("div",{dangerouslySetInnerHTML:{__html:e}}),(0,l.createElement)(y,{onClick:t}))}function _({html:e,onClick:t,isSelected:r}){const{iframeRef:n,iframeHeight:o,adjustIframeHeight:a,handleIframeLoad:i}=b({html:e}),c=(0,u.useCallback)((()=>`\n\t\t\t<!DOCTYPE html>\n\t\t\t<html>\n\t\t\t<head>\n\t\t\t\t<meta charset="UTF-8">\n\t\t\t\t<meta name="viewport" content="width=device-width, initial-scale=1.0">\n\t\t\t\t<style>\n\t\t\t\t\tbody { margin: 0; padding: 0; overflow-x: hidden; }\n\t\t\t\t</style>\n\t\t\t</head>\n\t\t\t<body>\n\t\t\t\t${e}\n\t\t\t</body>\n\t\t\t</html>\n\t\t`),[e]);return(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:n,srcDoc:c(),sandbox:"allow-scripts allow-same-origin allow-popups allow-forms",style:{width:"100%",height:`${o}px`,border:"none",overflow:"hidden"},onLoad:i}),r&&(0,l.createElement)("div",{onClick:t,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1,display:r?"block":"none"}}))}const E={default:(0,d.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,l.createElement)(l.Fragment,null,(0,l.createElement)(s.Spinner,null)," "+(0,d.__)("Checking if this URL supports ActivityPub replies...","activitypub")),valid:(0,d.__)("The author will be notified of your response.","activitypub"),error:(0,d.__)("This URL probably won't receive your reply. We'll still try.","activitypub")},k={valid:(0,d.__)("This post can be embedded with your reply.","activitypub"),invalid:(0,d.__)("This post cannot be embedded.","activitypub")};(0,n.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:t,clientId:r,isSelected:n}){const{url:o}=e,{namespace:a}=window._activityPubOptions||{},[i,p]=(0,u.useState)(E.default),[v,y]=(0,u.useState)(!1),[C,L]=(0,u.useState)(!1),[S,O]=(0,u.useState)(!1),[P,x]=(0,u.useState)(!0===e.embedPost||!o),[R,T]=(0,u.useState)(null),{iframeRef:H,iframeHeight:I,adjustIframeHeight:j,handleIframeLoad:D}=b({html:R}),{insertAfterBlock:B,removeBlock:N}=(0,w.useDispatch)("core/block-editor"),W=(0,c.useBlockProps)(),F=(0,u.useRef)(),M=((0,u.useRef)(),(0,u.useRef)(P)),U=()=>{setTimeout((()=>F.current?.focus()),50)};(0,u.useEffect)((()=>{M.current=P}),[P]);const A=(0,u.useCallback)((e=>{y(e),M.current&&e&&t({embedPost:!0})}),[t]),V=(e=!1)=>{O(e),y(!1),L(!1),T("")},$=(0,m.useDebounce)((async e=>{if(e)try{V(!0),p(E.checking());const t=await f()({path:(0,h.addQueryArgs)(`${a}/url/validate`,{url:e})});A(t.is_activitypub),L(t.is_real_oembed),T(t.html||""),p(E.valid)}catch(e){V(),p(E.error)}finally{O(!1)}else V()}),250);return(0,u.useEffect)((()=>{o&&$(o)}),[o]),(0,l.createElement)(l.Fragment,null,(0,l.createElement)(c.InspectorControls,null,(0,l.createElement)(s.PanelBody,{title:(0,d.__)("Settings","activitypub")},(0,l.createElement)(s.ToggleControl,{label:(0,d.__)("Embed Post","activitypub"),checked:e.embedPost,onChange:e=>{t({embedPost:e}),x(e)},disabled:!v,help:v?k.valid:k.invalid}))),(0,l.createElement)("div",{...W},n&&(0,l.createElement)(s.TextControl,{label:(0,d.__)("Your post is a reply to the following URL","activitypub"),value:o,onChange:e=>t({url:e}),help:i,onKeyDown:t=>{"Enter"===t.key&&B(r),!e.url&&["Backspace","Delete"].includes(t.key)&&N(r)},ref:F}),v&&e.embedPost&&R&&(0,l.createElement)("div",{className:"activitypub-embed-container"},C&&(Y=R)&&(Y.includes("wp-embedded-content")||Y.includes("wp-embed/")||Y.includes('class="wp-embed"'))?(0,l.createElement)(g,{html:R,onSelectBlock:U}):(0,l.createElement)(_,{html:R,onClick:U,isSelected:n})),o&&(!e.embedPost||!R)&&(0,l.createElement)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:U,style:{cursor:"pointer"}},(0,l.createElement)("a",{href:o,className:"u-in-reply-to",target:"_blank",rel:"noreferrer"},"↬"+o.replace(/^https?:\/\//,"")))));var Y},save:()=>null,icon:i})},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function n(e){var o=r[e];if(void 0!==o)return o.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.m=t,e=[],n.O=(t,r,o,a)=>{if(!r){var i=1/0;for(d=0;d<e.length;d++){for(var[r,o,a]=e[d],l=!0,c=0;c<r.length;c++)(!1&a||i>=a)&&Object.keys(n.O).every((e=>n.O[e](r[c])))?r.splice(c--,1):(l=!1,a<i&&(i=a));if(l){e.splice(d--,1);var s=o();void 0!==s&&(t=s)}}return t}a=a||0;for(var d=e.length;d>0&&e[d-1][2]>a;d--)e[d]=e[d-1];e[d]=[r,o,a]},n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={780:0,356:0};n.O.j=t=>0===e[t];var t=(t,r)=>{var o,a,[i,l,c]=r,s=0;if(i.some((t=>0!==e[t]))){for(o in l)n.o(l,o)&&(n.m[o]=l[o]);if(c)var d=c(n)}for(t&&t(r);s<i.length;s++)a=i[s],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(d)},r=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var o=n.O(void 0,[356],(()=>n(238)));o=n.O(o)})();
\ No newline at end of file
+(()=>{"use strict";var e,t={20:(e,t,r)=>{var n=r(609),o=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var n,c={},s=null,u=null;for(n in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,n)&&!l.hasOwnProperty(n)&&(c[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===c[n]&&(c[n]=t[n]);return{$$typeof:o,type:e,key:s,ref:u,props:c,_owner:i.current}}},238:(e,t,r)=>{const n=window.wp.blocks,o=window.wp.primitives;var a=r(848);const i=(0,a.jsx)(o.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,a.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"})});var l=r(609);const c=window.wp.blockEditor,s=window.wp.components,u=window.wp.i18n,d=window.wp.element,m=window.wp.compose,p=window.wp.apiFetch;var f=r.n(p);const h=window.wp.url,b=window.wp.data;function w({html:e}){const t=(0,d.useRef)(null),[r,n]=(0,d.useState)(300),o=(0,d.useRef)(300),a=(0,d.useCallback)((()=>{if(t.current)try{const e=t.current;let r=300;try{e.contentDocument&&e.contentDocument.body?r=e.contentDocument.body.scrollHeight:e.contentWindow&&e.contentWindow.document&&e.contentWindow.document.body&&(r=e.contentWindow.document.body.scrollHeight)}catch(e){console.log("Could not access iframe content document:",e)}r+=5,Math.abs(r-o.current)>5&&(o.current=r,n(r))}catch(e){console.error("Error adjusting iframe height:",e)}}),[]),i=(0,d.useCallback)((()=>{if(t.current)try{a()}catch(e){console.error("Error setting up iframe height adjustment:",e)}}),[a]);return(0,d.useEffect)((()=>{t.current&&t.current.addEventListener("load",i);const e=setInterval(a,1e3);return()=>{clearInterval(e),t.current&&t.current.removeEventListener("load",i)}}),[i,a]),(0,d.useEffect)((()=>{if(t.current){const e=setTimeout((()=>{a()}),100);return()=>clearTimeout(e)}}),[e,a]),{iframeRef:t,iframeHeight:r,adjustIframeHeight:a,handleIframeLoad:i}}const v={class:"className",frameborder:"frameBorder",allowfullscreen:"allowFullScreen",allowtransparency:"allowTransparency",marginheight:"marginHeight",marginwidth:"marginWidth"};function y({onClick:e}){return(0,l.createElement)("div",{className:"activitypub-embed-overlay",onClick:e,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1}})}function g({html:e,onSelectBlock:t}){const r=(0,d.useRef)(),[n,o]=(0,d.useState)(282),[a,i]=(0,d.useState)(!1),c=(0,d.useCallback)((()=>{const t=(new window.DOMParser).parseFromString(e,"text/html").querySelector("iframe"),r={};return t?(Array.from(t.attributes).forEach((({name:e,value:t})=>{"style"!==e&&(r[v[e]||e]=t)})),r):r}),[e]),s=c();return(0,d.useEffect)((()=>{if(!r.current)return;const{ownerDocument:e}=r.current,{defaultView:t}=e;function n({data:{secret:e,message:t,value:r}={}}){"height"===t&&e===s["data-secret"]&&o(r)}return t.addEventListener("message",n),()=>{t.removeEventListener("message",n)}}),[s]),s.src?(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:r,title:s.title||(0,u.__)("Embedded WordPress content","activitypub"),...s,height:n,style:{width:"100%",maxWidth:"100%"}}),!a&&(0,l.createElement)(y,{onClick:t})):(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("div",{dangerouslySetInnerHTML:{__html:e}}),(0,l.createElement)(y,{onClick:t}))}function _({html:e,onClick:t,isSelected:r}){const{iframeRef:n,iframeHeight:o,adjustIframeHeight:a,handleIframeLoad:i}=w({html:e}),c=(0,d.useCallback)((()=>`\n\t\t\t<!DOCTYPE html>\n\t\t\t<html>\n\t\t\t<head>\n\t\t\t\t<meta charset="UTF-8">\n\t\t\t\t<meta name="viewport" content="width=device-width, initial-scale=1.0">\n\t\t\t\t<style>\n\t\t\t\t\tbody { margin: 0; padding: 0; overflow-x: hidden; }\n\t\t\t\t</style>\n\t\t\t</head>\n\t\t\t<body>\n\t\t\t\t${e}\n\t\t\t</body>\n\t\t\t</html>\n\t\t`),[e]);return(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:n,srcDoc:c(),sandbox:"allow-scripts allow-same-origin allow-popups allow-forms",style:{width:"100%",height:`${o}px`,border:"none",overflow:"hidden"},onLoad:i}),r&&(0,l.createElement)("div",{onClick:t,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1,display:r?"block":"none"}}))}const E={default:(0,u.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,l.createElement)(l.Fragment,null,(0,l.createElement)(s.Spinner,null)," "+(0,u.__)("Checking if this URL supports ActivityPub replies...","activitypub")),valid:(0,u.__)("The author will be notified of your response.","activitypub"),error:(0,u.__)("This URL probably won't receive your reply. We'll still try.","activitypub")},k={valid:(0,u.__)("This post can be embedded with your reply.","activitypub"),invalid:(0,u.__)("This post cannot be embedded.","activitypub")};(0,n.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:t,clientId:r,isSelected:n}){const{url:o}=e,{namespace:a}=window._activityPubOptions||{},[i,p]=(0,d.useState)(E.default),[v,y]=(0,d.useState)(!1),[C,L]=(0,d.useState)(!1),[S,O]=(0,d.useState)(!1),[P,x]=(0,d.useState)(!0===e.embedPost||!o),[R,T]=(0,d.useState)(null),{iframeRef:H,iframeHeight:I,adjustIframeHeight:j,handleIframeLoad:D}=w({html:R}),{insertAfterBlock:B,removeBlock:N}=(0,b.useDispatch)("core/block-editor"),W=(0,c.useBlockProps)(),F=(0,d.useRef)(),M=((0,d.useRef)(),(0,d.useRef)(P)),U=()=>{setTimeout((()=>F.current?.focus()),50)};(0,d.useEffect)((()=>{M.current=P}),[P]);const A=(0,d.useCallback)((e=>{y(e),M.current&&e&&t({embedPost:!0})}),[t]),V=(e=!1)=>{O(e),y(!1),L(!1),T("")},$=(0,m.useDebounce)((async e=>{if(e)try{V(!0),p(E.checking());const t=await f()({path:(0,h.addQueryArgs)(`${a}/url/validate`,{url:e})});A(t.is_activitypub),L(t.is_real_oembed),T(t.html||""),p(E.valid)}catch(e){V(),p(E.error)}finally{O(!1)}else V()}),250);return(0,d.useEffect)((()=>{o&&$(o)}),[o]),(0,l.createElement)(l.Fragment,null,(0,l.createElement)(c.InspectorControls,null,(0,l.createElement)(s.PanelBody,{title:(0,u.__)("Settings","activitypub")},(0,l.createElement)(s.ToggleControl,{label:(0,u.__)("Embed Post","activitypub"),checked:e.embedPost,onChange:e=>{t({embedPost:e}),x(e)},disabled:!v,help:v?k.valid:k.invalid}))),(0,l.createElement)("div",{...W},n&&(0,l.createElement)(s.TextControl,{label:(0,u.__)("Your post is a reply to the following URL","activitypub"),value:o,onChange:e=>t({url:e}),help:i,onKeyDown:t=>{"Enter"===t.key&&B(r),!e.url&&["Backspace","Delete"].includes(t.key)&&N(r)},ref:F}),v&&e.embedPost&&R&&(0,l.createElement)("div",{className:"activitypub-embed-container"},C&&(Y=R)&&(Y.includes("wp-embedded-content")||Y.includes("wp-embed/")||Y.includes('class="wp-embed"'))?(0,l.createElement)(g,{html:R,onSelectBlock:U}):(0,l.createElement)(_,{html:R,onClick:U,isSelected:n})),o&&(!e.embedPost||!R)&&(0,l.createElement)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:U,style:{cursor:"pointer"}},(0,l.createElement)("a",{href:o,className:"u-in-reply-to",target:"_blank",rel:"noreferrer"},"↬"+o.replace(/^https?:\/\//,"")))));var Y},save:()=>null,icon:i})},609:e=>{e.exports=window.React},848:(e,t,r)=>{e.exports=r(20)}},r={};function n(e){var o=r[e];if(void 0!==o)return o.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.m=t,e=[],n.O=(t,r,o,a)=>{if(!r){var i=1/0;for(u=0;u<e.length;u++){for(var[r,o,a]=e[u],l=!0,c=0;c<r.length;c++)(!1&a||i>=a)&&Object.keys(n.O).every((e=>n.O[e](r[c])))?r.splice(c--,1):(l=!1,a<i&&(i=a));if(l){e.splice(u--,1);var s=o();void 0!==s&&(t=s)}}return t}a=a||0;for(var u=e.length;u>0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={780:0,356:0};n.O.j=t=>0===e[t];var t=(t,r)=>{var o,a,[i,l,c]=r,s=0;if(i.some((t=>0!==e[t]))){for(o in l)n.o(l,o)&&(n.m[o]=l[o]);if(c)var u=c(n)}for(t&&t(r);s<i.length;s++)a=i[s],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(u)},r=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var o=n.O(void 0,[356],(()=>n(238)));o=n.O(o)})();
\ No newline at end of file
diff --git a/includes/class-blocks.php b/includes/class-blocks.php
index f09e206c9..216e380e3 100644
--- a/includes/class-blocks.php
+++ b/includes/class-blocks.php
@@ -151,12 +151,9 @@ public static function register_blocks() {
 				'render_callback' => array( self::class, 'render_follower_block' ),
 			)
 		);
-		\register_block_type_from_metadata(
-			ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me',
-			array(
-				'render_callback' => array( self::class, 'render_follow_me_block' ),
-			)
-		);
+
+		\register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me' );
+
 		\register_block_type_from_metadata(
 			ACTIVITYPUB_PLUGIN_DIR . '/build/reply',
 			array(
@@ -224,7 +221,7 @@ public static function render_post_reactions_block( $attrs, $content ) {
 	 * @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'.
 	 * @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
 	 */
-	private static function get_user_id( $user_string ) {
+	public static function get_user_id( $user_string ) {
 		if ( is_numeric( $user_string ) ) {
 			return absint( $user_string );
 		}
@@ -281,40 +278,6 @@ protected static function filter_array_by_keys( $data, $keys ) {
 		return array_intersect_key( $data, array_flip( $keys ) );
 	}
 
-	/**
-	 * Render the follow me block.
-	 *
-	 * @param array $attrs The block attributes.
-	 * @return string The HTML to render.
-	 */
-	public static function render_follow_me_block( $attrs ) {
-		$user_id = self::get_user_id( $attrs['selectedUser'] );
-		$user    = Actors::get_by_id( $user_id );
-		if ( is_wp_error( $user ) ) {
-			if ( 'inherit' === $attrs['selectedUser'] ) {
-				// If the user is 'inherit' and we couldn't determine the user, don't render anything.
-				return '<!-- Follow Me block: `inherit` mode does not display on this type of page -->';
-			} else {
-				// If the user is a specific ID and we couldn't find it, render an error message.
-				return '<!-- Follow Me block: user not found -->';
-			}
-		}
-
-		$attrs['profileData'] = self::filter_array_by_keys(
-			$user->to_array(),
-			array( 'icon', 'name', 'webfinger' )
-		);
-
-		$wrapper_attributes = get_block_wrapper_attributes(
-			array(
-				'class'      => 'activitypub-follow-me-block-wrapper',
-				'data-attrs' => wp_json_encode( $attrs ),
-			)
-		);
-		// todo: render more than an empty div?
-		return '<div ' . $wrapper_attributes . '></div>';
-	}
-
 	/**
 	 * Render the follower block.
 	 *
@@ -477,4 +440,30 @@ function ( $paragraph ) {
 
 		return $data;
 	}
+
+	/**
+	 * Add Interactivity directions to the specified element.
+	 *
+	 * @param string   $content    The block content.
+	 * @param string[] $selector   The selector for the element to add directions to.
+	 * @param string[] $attributes The attributes to add to the element.
+	 *
+	 * @return string The updated content.
+	 */
+	public static function add_directions( $content, $selector, $attributes ) {
+		$tags = new \WP_HTML_Tag_Processor( $content );
+
+		while ( $tags->next_tag( $selector ) ) {
+			foreach ( $attributes as $key => $value ) {
+				if ( 'class' === $key ) {
+					$tags->add_class( $value );
+					continue;
+				}
+
+				$tags->set_attribute( $key, $value );
+			}
+		}
+
+		return $tags->get_updated_html();
+	}
 }
diff --git a/package.json b/package.json
index 58c9ec9a4..c753376c6 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,8 @@
 		"web": "https://notiz.blog"
 	},
 	"scripts": {
-		"dev": "wp-scripts start",
-		"build": "wp-scripts format && wp-scripts build",
+		"dev": "wp-scripts start --experimental-modules",
+		"build": "wp-scripts format && wp-scripts build --experimental-modules",
 		"format": "wp-scripts format",
 		"lint:css": "wp-scripts lint-style",
 		"lint:js": "wp-scripts lint-js",
@@ -27,7 +27,7 @@
 	},
 	"homepage": "https://github.com/automattic/wordpress-activitypub#readme",
 	"devDependencies": {
-		"@wordpress/api-fetch": "^7.22.0",
+		"@wordpress/api-fetch": "^7.23.0",
 		"@wordpress/block-editor": "^14.17.0",
 		"@wordpress/blocks": "^14.0.0",
 		"@wordpress/components": "^29.1.1",
@@ -40,6 +40,7 @@
 		"@wordpress/env": "^10.10.0",
 		"@wordpress/i18n": "^5.22.0",
 		"@wordpress/icons": "^10.10.0",
+		"@wordpress/interactivity": "^6.23.0",
 		"@wordpress/plugins": "^7.22.0",
 		"@wordpress/prettier-config": "^4.23.0",
 		"@wordpress/primitives": "^4.22.0",
diff --git a/src/follow-me/block.json b/src/follow-me/block.json
index 2abe9de5d..e19607945 100644
--- a/src/follow-me/block.json
+++ b/src/follow-me/block.json
@@ -2,14 +2,20 @@
 	"$schema": "https://schemas.wp.org/trunk/block.json",
 	"name": "activitypub/follow-me",
 	"apiVersion": 3,
-	"version": "1.0.0",
+	"version": "2.0.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": {
+			"buttonOnly": false
+		}
+	},
 	"supports": {
 		"html": false,
+		"interactivity": true,
 		"color": {
 			"gradients": true,
 			"link": true,
@@ -30,6 +36,9 @@
 			"__experimentalDefaultControls": {
 				"fontSize": true
 			}
+		},
+		"innerBlocks": {
+			"allowedBlocks": [ "core/button" ]
 		}
 	},
 	"attributes": {
@@ -40,19 +49,12 @@
 		"buttonOnly": {
 			"type": "boolean",
 			"default": false
-		},
-		"buttonText": {
-			"type": "string",
-			"default": "Follow"
-		},
-		"buttonSize": {
-			"type": "string",
-			"default": "default",
-			"enum": ["small", "default", "compact"]
 		}
 	},
 	"usesContext": [ "postType", "postId" ],
 	"editorScript": "file:./index.js",
-	"viewScript": "file:./view.js",
-	"style": ["file:./style-view.css", "wp-components"]
-}
\ No newline at end of file
+	"viewScriptModule": "file:./view.js",
+	"viewScript": "wp-api-fetch",
+	"style": "file:./style-view.css",
+	"render": "file:./render.php"
+}
diff --git a/src/follow-me/button-style.js b/src/follow-me/button-style.js
index 0c33ca2a3..b022cd08e 100644
--- a/src/follow-me/button-style.js
+++ b/src/follow-me/button-style.js
@@ -1,32 +1,103 @@
-function presetVarColorCss( color ) {
-	return `var(--wp--preset--color--${ color })`;
+/**
+ * Cache for computed styles and CSS variable checks.
+ */
+const cssCache = {
+	computedStyles: null,
+	variables: {},
+};
+
+/**
+ * Checks if a CSS variable is defined.
+ *
+ * Uses a caching mechanism to avoid frequent getComputedStyle calls,
+ * which can cause layout thrashing when called repeatedly.
+ *
+ * @param {string} variableName The CSS variable name to check.
+ * @return {boolean} Whether the variable is defined.
+ */
+function isCssVariableDefined( variableName ) {
+	// Return false if we're in a server-side context.
+	if ( typeof window === 'undefined' || ! window.getComputedStyle ) {
+		return false;
+	}
+
+	// Check if we've already cached this variable.
+	if ( cssCache.variables.hasOwnProperty( variableName ) ) {
+		return cssCache.variables[ variableName ];
+	}
+
+	// Get the computed style of the root element (cached).
+	if ( ! cssCache.computedStyles ) {
+		cssCache.computedStyles = window.getComputedStyle( document.documentElement );
+	}
+
+	// Get the value of the CSS variable.
+	const value = cssCache.computedStyles.getPropertyValue( variableName ).trim();
+
+	// Cache the result.
+	cssCache.variables[ variableName ] = value !== '';
+
+	// If the value is empty, the variable is not defined or is set to an empty value.
+	return cssCache.variables[ variableName ];
 }
 
+/**
+ * Gets the background color from a style object.
+ *
+ * @param {Object|string} color Color object or string.
+ * @return {string|null} Background color.
+ */
 function getBackgroundColor( color ) {
-	// if color is a string, it's a var like this.
+	// If color is a string, it's a var like this.
 	if ( typeof color === 'string' ) {
-		return presetVarColorCss( color );
+		const varName = `--wp--preset--color--${ color }`;
+		if ( ! isCssVariableDefined( varName ) ) {
+			return null;
+		}
+		return `var(${ varName })`;
 	}
 
 	return color?.color?.background || null;
 }
 
+/**
+ * Gets the link color from a style object.
+ *
+ * @param {string} text Text color.
+ * @return {string|null} Link color.
+ */
 function getLinkColor( text ) {
 	if ( typeof text !== 'string' ) {
 		return null;
 	}
-	// if it starts with a hash, leave it be
+	// If it starts with a hash, leave it be.
 	if ( text.match( /^#/ ) ) {
-		// we don't handle the alpha channel if present.
+		// We don't handle the alpha channel if present.
 		return text.substring( 0, 7 );
 	}
 	// var:preset|color|luminous-vivid-amber
 	// var(--wp--preset--color--luminous-vivid-amber)
-	// we will receive the top format, we need to output the bottom format
+	// We will receive the top format, we need to output the bottom format.
 	const [ , , color ] = text.split( '|' );
-	return presetVarColorCss( color );
+	const varName = `--wp--preset--color--${ color }`;
+
+	// Check if the CSS variable is defined before using it.
+	if ( ! isCssVariableDefined( varName ) ) {
+		return null;
+	}
+
+	return `var(${ varName })`;
 }
 
+/**
+ * Generates a CSS selector.
+ *
+ * @param {string} selector CSS selector.
+ * @param {string} prop CSS property.
+ * @param {string|null} value CSS value.
+ * @param {string} pseudo Pseudo-selector.
+ * @return {string} CSS selector.
+ */
 function generateSelector( selector, prop, value = null, pseudo = '' ) {
 	if ( ! value ) {
 		return '';
@@ -34,42 +105,60 @@ function generateSelector( selector, prop, value = null, pseudo = '' ) {
 	return `${ selector }${ pseudo } { ${ prop }: ${ value }; }\n`;
 }
 
+/**
+ * Gets styles for a button.
+ *
+ * @param {string} selector CSS selector.
+ * @param {string} button Button color.
+ * @param {string} text Text color.
+ * @param {string} hover Hover color.
+ * @return {string} CSS styles.
+ */
 function getStyles( selector, button, text, hover ) {
-	return generateSelector( selector, 'background-color', button )
-	+ generateSelector( selector, 'color', text )
-	+ generateSelector( selector, 'background-color', hover, ':hover' )
-	+ generateSelector( selector, 'background-color', hover, ':focus' );
+	return (
+		generateSelector( selector, 'background-color', button ) +
+		generateSelector( selector, 'color', text ) +
+		generateSelector( selector, 'background-color', hover, ':hover' ) +
+		generateSelector( selector, 'background-color', hover, ':focus' )
+	);
 }
 
-function getBlockStyles( base, style, backgroundColor ) {
-	const selector = `${ base } .components-button`;
-	// we grab the background color if set as a good color for our button text
-	const buttonTextColor = getBackgroundColor( backgroundColor )
-		// bg might be in this form.
-		|| style?.color?.background;
-	// we misuse the link color for the button background
+/**
+ * Gets block styles.
+ *
+ * @param {string} base Base selector.
+ * @param {Object} style Style object.
+ * @param {Object|string} backgroundColor Background color.
+ * @return {string} CSS styles.
+ */
+export function getBlockStyles( base, style, backgroundColor ) {
+	const selector = `${ base } .wp-block-button__link`;
+
+	// We grab the background color if set as a good color for our button text.
+	const buttonTextColor =
+		getBackgroundColor( backgroundColor ) ||
+		// Background might be in this form.
+		style?.color?.background;
+
+	// We misuse the link color for the button background.
 	const buttonColor = getLinkColor( style?.elements?.link?.color?.text );
-	// hover!
-	const buttonHoverColor = getLinkColor( style?.elements?.link?.[':hover']?.color?.text );
+	const buttonHoverColor = getLinkColor( style?.elements?.link?.[ ':hover' ]?.color?.text );
 
 	return getStyles( selector, buttonColor, buttonTextColor, buttonHoverColor );
 }
 
+/**
+ * Gets popup styles.
+ *
+ * @param {Object} style Style object.
+ * @return {string} CSS styles.
+ */
 export function getPopupStyles( style ) {
-	// we don't acept backgroundColor because the popup is always white (right?)
-	const buttonColor = getLinkColor( style?.elements?.link?.color?.text )
-		|| '#111';
+	// We don't accept backgroundColor because the popup is always white (right?).
+	const buttonColor = getLinkColor( style?.elements?.link?.color?.text ) || '#111';
 	const buttonTextColor = '#fff';
-	const buttonHoverColor = getLinkColor( style?.elements?.link?.[':hover']?.color?.text )
-		|| '#333';
-	const selector = '.apfmd__button-group .components-button';
+	const buttonHoverColor = getLinkColor( style?.elements?.link?.[ ':hover' ]?.color?.text ) || '#333';
+	const selector = '.activitypub-dialog__button-group .wp-block-button';
 
 	return getStyles( selector, buttonColor, buttonTextColor, buttonHoverColor );
 }
-
-export function ButtonStyle( { selector, style, backgroundColor } ) {
-	const css = getBlockStyles( selector, style, backgroundColor );
-	return (
-		<style>{ css }</style>
-	);
-}
\ No newline at end of file
diff --git a/src/follow-me/deprecation.js b/src/follow-me/deprecation.js
new file mode 100644
index 000000000..30b44acd6
--- /dev/null
+++ b/src/follow-me/deprecation.js
@@ -0,0 +1,79 @@
+import { createBlock } from '@wordpress/blocks';
+
+/**
+ * Deprecation for the Follow Me block to use a core button block instead of the custom button.
+ * This handles the migration of the buttonText and buttonSize attributes to the innerBlock.
+ */
+const v1 = {
+	attributes: {
+		buttonOnly: {
+			type: 'boolean',
+			default: false,
+		},
+		buttonText: {
+			type: 'string',
+			default: 'Follow',
+		},
+		selectedUser: {
+			type: 'string',
+			default: 'site',
+		},
+	},
+
+	supports: {
+		html: false,
+		color: {
+			gradients: true,
+			link: true,
+			__experimentalDefaultControls: {
+				background: true,
+				text: true,
+				link: true,
+			},
+		},
+		__experimentalBorder: {
+			radius: true,
+			width: true,
+			color: true,
+			style: true,
+		},
+		typography: {
+			fontSize: true,
+			__experimentalDefaultControls: {
+				fontSize: true,
+			},
+		},
+	},
+
+	/**
+	 * Checks if the block is eligible for migration.
+	 *
+	 * @param {Object} attributes The block attributes.
+	 *
+	 * @return {boolean} Whether the block is eligible for migration.
+	 */
+	isEligible( attributes ) {
+		// Run migration if buttonText or buttonOnly is set.
+		return !! attributes.buttonText || !! attributes.buttonOnly;
+	},
+
+	/**
+	 * Migrates the Follow Me block to use a core button block instead of the custom button.
+	 *
+	 * @param {Object} attributes The block attributes.
+	 *
+	 * @return {[Object, Array]} An array with the new block attributes and inner blocks.
+	 */
+	migrate( attributes ) {
+		const { buttonText, ...newAttributes } = attributes;
+
+		const buttonBlock = createBlock( 'core/button', {
+			tagName: 'button',
+			text: buttonText,
+		} );
+
+		return [ newAttributes, [ buttonBlock ] ];
+	},
+};
+
+export default [ v1 ];
diff --git a/src/follow-me/edit.js b/src/follow-me/edit.js
index b41d0f0fb..c33e0bb5b 100644
--- a/src/follow-me/edit.js
+++ b/src/follow-me/edit.js
@@ -1,12 +1,93 @@
-import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
+import apiFetch from '@wordpress/api-fetch';
+import { InspectorControls, useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
 import { __ } from '@wordpress/i18n';
 import { useSelect } from '@wordpress/data';
 import { store as coreStore } from '@wordpress/core-data';
-import { SelectControl, PanelBody, ToggleControl, TextControl } from '@wordpress/components';
+import { SelectControl, PanelBody, ToggleControl } from '@wordpress/components';
+import { useEffect, useState } from '@wordpress/element';
 import { useUserOptions } from '../shared/use-user-options';
-import FollowMe from './follow-me';
-import { useEffect } from '@wordpress/element';
 import { InheritModeBlockFallback } from '../shared/inherit-block-fallback';
+import { useOptions } from '../shared/use-options';
+
+/**
+ * Default profile data.
+ *
+ * @type {Object}
+ */
+const DEFAULT_PROFILE_DATA = {
+	avatar: 'https://secure.gravatar.com/avatar/default?s=120',
+	webfinger: '@well@hello.dolly',
+	name: __( 'Hello Dolly Fan Account', 'activitypub' ),
+	url: '#',
+};
+
+/**
+ * Get normalized profile data.
+ *
+ * @param {Object} profile Profile data.
+ * @return {Object} Normalized profile data.
+ */
+function getNormalizedProfile( profile ) {
+	if ( ! profile ) {
+		return DEFAULT_PROFILE_DATA;
+	}
+
+	const data = { ...DEFAULT_PROFILE_DATA, ...profile };
+	data.avatar = data?.icon?.url;
+
+	// Ensure webfinger always has the @ prefix.
+	if ( data.webfinger && ! data.webfinger.startsWith( '@' ) ) {
+		data.webfinger = '@' + data.webfinger;
+	}
+
+	return data;
+}
+
+/**
+ * Fetch profile data.
+ *
+ * @param {number} userId User ID.
+ * @return {Promise} Promise resolving with profile data.
+ */
+function fetchProfile( userId ) {
+	const { namespace } = useOptions();
+	const fetchOptions = {
+		headers: { Accept: 'application/activity+json' },
+		path: `/${ namespace }/actors/${ userId }`,
+	};
+	return apiFetch( fetchOptions );
+}
+
+/**
+ * Profile component for the editor.
+ *
+ * @param {Object} props Component props.
+ * @return {JSX.Element} Profile component.
+ */
+function EditorProfile( { profile, buttonOnly, innerBlocksProps } ) {
+	const { webfinger, avatar, name } = profile;
+
+	if ( buttonOnly ) {
+		return (
+			<div className="activitypub-profile">
+				<div { ...innerBlocksProps } />
+			</div>
+		);
+	}
+
+	return (
+		<div className="activitypub-profile">
+			<img className="activitypub-profile__avatar" src={ avatar } alt={ name } />
+			<div className="activitypub-profile__content">
+				<div className="activitypub-profile__name">{ name }</div>
+				<div className="activitypub-profile__handle" title={ webfinger }>
+					{ webfinger }
+				</div>
+			</div>
+			<div { ...innerBlocksProps } />
+		</div>
+	);
+}
 
 /**
  * Edit component.
@@ -19,29 +100,32 @@ import { InheritModeBlockFallback } from '../shared/inherit-block-fallback';
  * @param {number} props.context.postId Post ID.
  * @return {JSX.Element} Edit component.
  */
-export default function Edit( {
-	attributes,
-	setAttributes,
-	context: {
-		postType,
-		postId,
-	},
-} ) {
+export default function Edit( { attributes, setAttributes, context: { postType, postId } } ) {
 	const blockProps = useBlockProps( {
 		className: 'activitypub-follow-me-block-wrapper',
 	} );
 	const usersOptions = useUserOptions( { withInherit: true } );
-	const { selectedUser, buttonOnly, buttonText, buttonSize } = attributes;
+	const { selectedUser, buttonOnly } = attributes;
 	const isInheritMode = selectedUser === 'inherit';
+	const [ profile, setProfile ] = useState( getNormalizedProfile( DEFAULT_PROFILE_DATA ) );
+	const userId = selectedUser === 'site' ? 0 : selectedUser;
+
+	const TEMPLATE = [ [ 'core/button', { text: __( 'Follow', 'activitypub' ), tagName: 'button' } ] ];
+
+	const innerBlocksProps = useInnerBlocksProps(
+		{},
+		{
+			allowedBlocks: [ 'core/button' ],
+			template: TEMPLATE,
+			templateLock: false,
+			renderAppender: false,
+		}
+	);
 
 	const authorId = useSelect(
 		( select ) => {
 			const { getEditedEntityRecord } = select( coreStore );
-			const _authorId = getEditedEntityRecord(
-				'postType',
-				postType,
-				postId
-			)?.author;
+			const _authorId = getEditedEntityRecord( 'postType', postType, postId )?.author;
 
 			return _authorId ?? null;
 		},
@@ -49,11 +133,23 @@ export default function Edit( {
 	);
 
 	useEffect( () => {
-		// if there are no users yet, do nothing
+		// Fetch profile data when userId changes.
+		if ( isInheritMode && ! authorId ) {
+			return;
+		}
+
+		const effectiveUserId = isInheritMode ? authorId : userId;
+		fetchProfile( effectiveUserId ).then( ( data ) => {
+			setProfile( getNormalizedProfile( data ) );
+		} );
+	}, [ userId, authorId, isInheritMode ] );
+
+	useEffect( () => {
+		// If there are no users yet, do nothing.
 		if ( ! usersOptions.length ) {
 			return;
 		}
-		// ensure that the selected user is in the list of options, if not, select the first available user
+		// Ensure that the selected user is in the list of options, if not, select the first available user.
 		if ( ! usersOptions.find( ( { value } ) => value === selectedUser ) ) {
 			setAttributes( { selectedUser: usersOptions[ 0 ].value } );
 		}
@@ -77,33 +173,19 @@ export default function Edit( {
 						onChange={ ( value ) => setAttributes( { buttonOnly: value } ) }
 						help={ __( 'Only show the follow button without profile information', 'activitypub' ) }
 					/>
-					<TextControl
-						label={ __( 'Button Text', 'activitypub' ) }
-						value={ buttonText }
-						onChange={ ( value ) => setAttributes( { buttonText: value } ) }
-					/>
-					<SelectControl
-						label={ __( 'Button Size', 'activitypub' ) }
-						value={ buttonSize }
-						options={ [
-							{ label: __( 'Default', 'activitypub' ), value: 'default' },
-							{ label: __( 'Compact', 'activitypub' ), value: 'compact' },
-							{ label: __( 'Small', 'activitypub' ), value: 'small' },
-						] }
-						onChange={ ( value ) => setAttributes( { buttonSize: value } ) }
-						help={ __( 'Choose the size of the follow button', 'activitypub' ) }
-					/>
 				</PanelBody>
 			</InspectorControls>
-			{ isInheritMode ?
-				authorId ? (
-					<FollowMe { ...attributes } id={ blockProps.id } selectedUser={ authorId } />
-				) : (
-					<InheritModeBlockFallback name={ __( 'Follow Me', 'activitypub' ) } />
-				)
-				: (
-					<FollowMe { ...attributes } id={ blockProps.id } />
-				) }
+
+			{ isInheritMode && ! authorId ? (
+				<InheritModeBlockFallback name={ __( 'Follow Me', 'activitypub' ) } />
+			) : (
+				<EditorProfile
+					profile={ profile }
+					userId={ isInheritMode ? authorId : userId }
+					buttonOnly={ buttonOnly }
+					innerBlocksProps={ innerBlocksProps }
+				/>
+			) }
 		</div>
 	);
 }
diff --git a/src/follow-me/follow-me.js b/src/follow-me/follow-me.js
deleted file mode 100644
index aba7e0bc4..000000000
--- a/src/follow-me/follow-me.js
+++ /dev/null
@@ -1,242 +0,0 @@
-import apiFetch from '@wordpress/api-fetch';
-import { useEffect, useState } from '@wordpress/element';
-import { Button, Modal } from '@wordpress/components';
-import { __, sprintf } from '@wordpress/i18n';
-import { ButtonStyle, getPopupStyles } from './button-style';
-import { Dialog } from '../shared/dialog';
-import { useOptions } from '../shared/use-options';
-import './style.scss';
-
-/**
- * Default profile data.
- *
- * @type {Object}
- */
-const DEFAULT_PROFILE_DATA = {
-	avatar: '',
-	webfinger: '@well@hello.dolly',
-	name: __( 'Hello Dolly Fan Account', 'activitypub' ),
-	url: '#',
-};
-
-/**
- * Get normalized profile data.
- *
- * @param {Object} profile Profile data.
- * @return {Object} Normalized profile data.
- */
-function getNormalizedProfile( profile ) {
-	if ( ! profile ) {
-		return DEFAULT_PROFILE_DATA;
-	}
-	const data = { ...DEFAULT_PROFILE_DATA, ...profile };
-	data.avatar = data?.icon?.url;
-	return data;
-}
-
-/**
- * Fetch profile data.
- *
- * @param {number} userId User ID.
- * @return {Promise} Promise resolving with profile data.
- */
-function fetchProfile( userId ) {
-	const { namespace } = useOptions();
-	const fetchOptions = {
-		headers: { Accept: 'application/activity+json' },
-		path: `/${ namespace }/actors/${ userId }`,
-	};
-	return apiFetch( fetchOptions );
-}
-
-/**
- * Profile component.
- *
- * @param {Object} props Component props.
- * @param {Object} props.profile Profile data.
- * @param {string} props.popupStyles Popup styles.
- * @param {number} props.userId User ID.
- * @param {string} props.buttonText Button text.
- * @param {boolean} props.buttonOnly Whether to render only the button.
- * @param {string} props.buttonSize Button size.
- * @return {JSX.Element} Profile component.
- */
-function Profile( {
-	profile,
-	popupStyles,
-	userId,
-	buttonText,
-	buttonOnly,
-	buttonSize,
-} ) {
-	const { webfinger, avatar, name } = profile;
-	// check if webfinger starts with @ and add it if it doesn't
-	const webfingerWithAt = webfinger.startsWith( '@' ) ? webfinger : `@${ webfinger }`;
-
-	if ( buttonOnly ) {
-		return (
-			<div className="activitypub-profile">
-				<Follow
-					profile={ profile }
-					popupStyles={ popupStyles }
-					userId={ userId }
-					buttonText={ buttonText }
-					buttonSize={ buttonSize }
-				/>
-			</div>
-		);
-	}
-
-	return (
-		<div className="activitypub-profile">
-			<img className="activitypub-profile__avatar" src={ avatar } alt={ name } />
-			<div className="activitypub-profile__content">
-				<div className="activitypub-profile__name">{ name }</div>
-				<div className="activitypub-profile__handle" title={ webfingerWithAt }>{ webfingerWithAt }</div>
-			</div>
-			<Follow
-				profile={ profile }
-				popupStyles={ popupStyles }
-				userId={ userId }
-				buttonText={ buttonText }
-				buttonSize={ buttonSize }
-			/>
-		</div>
-	);
-}
-
-/**
- * Follow component.
- *
- * @param {Object} props Component props.
- * @param {Object} props.profile Profile data.
- * @param {string} props.popupStyles Popup styles.
- * @param {number} props.userId User ID.
- * @param {string} props.buttonText Button text.
- * @param {string} props.buttonSize Button size.
- * @return {JSX.Element} Follow component.
- */
-function Follow( {
-	profile,
-	popupStyles,
-	userId,
-	buttonText,
-	buttonSize,
-} ) {
-	const [ isOpen, setIsOpen ] = useState( false );
-	const title = sprintf(
-		/* translators: %s: profile name */
-		__( 'Follow %s', 'activitypub' ),
-		profile?.name
-	);
-
-	return (
-		<>
-			<Button
-				className="activitypub-profile__follow"
-				onClick={ () => setIsOpen( true ) }
-				aria-haspopup="dialog"
-				aria-expanded={ isOpen }
-				aria-label={ __( 'Follow me on the Fediverse', 'activitypub' ) }
-				size={ buttonSize }
-			>
-				{ buttonText }
-			</Button>
-			{ isOpen && (
-				<Modal
-					className="activitypub-profile__confirm activitypub__modal"
-					onRequestClose={ () => setIsOpen( false ) }
-					title={ title }
-					aria-label={ title }
-					role="dialog"
-				>
-					<DialogFollow profile={ profile } userId={ userId } />
-					<style>{ popupStyles }</style>
-				</Modal>
-			) }
-		</>
-	);
-}
-
-/**
- * Dialog follow component.
- *
- * @param {Object} props Component props.
- * @param {Object} props.profile Profile data.
- * @param {number} props.userId User ID.
- * @return {JSX.Element} Dialog follow component.
- */
-function DialogFollow( { profile, userId } ) {
-	const { namespace } = useOptions();
-	const { webfinger } = profile;
-	const actionText = __( 'Follow', 'activitypub' );
-	const resourceUrl = `/${ namespace }/actors/${ userId }/remote-follow?resource=`;
-	const copyDescription = __( 'Copy and paste my profile into the search field of your favorite fediverse app or server.', 'activitypub' );
-	const webfingerWithAt = webfinger.startsWith( '@' ) ? webfinger : `@${ webfinger }`;
-
-	return (
-		<Dialog
-			actionText={ actionText }
-			copyDescription={ copyDescription }
-			handle={ webfingerWithAt }
-			resourceUrl={ resourceUrl }
-		/>
-	);
-}
-
-/**
- * Follow me component.
- *
- * @param {Object} props Component props.
- * @param {number|string} props.selectedUser Selected user ID or 'site'.
- * @param {Object} props.style Style object.
- * @param {string} props.backgroundColor Background color.
- * @param {string} props.id Component ID.
- * @param {boolean} props.useId Whether to use the ID.
- * @param {Object} props.profileData Profile data.
- * @param {boolean} props.buttonOnly Whether to render only the button.
- * @param {string} props.buttonText Button text.
- * @param {string} props.buttonSize Button size.
- * @return {JSX.Element} Follow me component.
- */
-export default function FollowMe( {
-	selectedUser,
-	style,
-	backgroundColor,
-	id,
-	useId = false,
-	profileData = false,
-	buttonOnly = false,
-	buttonText = __( 'Follow', 'activitypub' ),
-	buttonSize = 'default',
-} ) {
-	const [ profile, setProfile ] = useState( getNormalizedProfile() );
-	const userId = selectedUser === 'site' ? 0 : selectedUser;
-	const popupStyles = getPopupStyles( style );
-	const wrapperProps = useId ? { id } : {};
-
-	useEffect( () => {
-		if ( profileData ) {
-			setProfile( getNormalizedProfile( profileData ) );
-			return;
-		}
-
-		fetchProfile( userId ).then( ( data ) => {
-			setProfile( getNormalizedProfile( data ) );
-		} );
-	}, [ userId, profileData ] );
-
-	return (
-		<div { ...wrapperProps } className="activitypub-follow-me-block-wrapper">
-			<ButtonStyle selector={ `#${ id }` } style={ style } backgroundColor={ backgroundColor } />
-			<Profile
-				profile={ profile }
-				userId={ userId }
-				popupStyles={ popupStyles }
-				buttonText={ buttonText }
-				buttonOnly={ buttonOnly }
-				buttonSize={ buttonSize }
-			/>
-		</div>
-	);
-}
diff --git a/src/follow-me/index.js b/src/follow-me/index.js
index 550af6078..fd6263ec0 100644
--- a/src/follow-me/index.js
+++ b/src/follow-me/index.js
@@ -1,5 +1,13 @@
 import { registerBlockType } from '@wordpress/blocks';
 import { people } from '@wordpress/icons';
+import deprecated from './deprecation';
 import edit from './edit';
-const save = () => null;
-registerBlockType( 'activitypub/follow-me', { edit, save, icon: people } );
\ No newline at end of file
+import save from './save';
+
+// Register the block.
+registerBlockType( 'activitypub/follow-me', {
+	deprecated,
+	edit,
+	icon: people,
+	save,
+} );
diff --git a/src/follow-me/render.php b/src/follow-me/render.php
new file mode 100644
index 000000000..36bd24531
--- /dev/null
+++ b/src/follow-me/render.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Server-side rendering of the `activitypub/follow-me` block.
+ *
+ * @package ActivityPub
+ */
+
+use Activitypub\Blocks;
+use Activitypub\Collection\Actors;
+
+/* @var array $attributes Block attributes. */
+$attributes = wp_parse_args( $attributes );
+
+// Get the user ID from the selected user attribute.
+$selected_user = $attributes['selectedUser'] ?? 'site';
+$user_id       = Blocks::get_user_id( $selected_user );
+$button_only   = $attributes['buttonOnly'] ?? false;
+
+// Generate a unique ID for the block.
+$block_id = 'activitypub-follow-me-block-' . wp_unique_id();
+
+// Get block style information.
+$style            = wp_get_global_styles();
+$background_color = $attributes['backgroundColor'] ?? $style['color']['background'] ?? '';
+
+// Get button style from block attributes.
+$button_style = $attributes['style'] ?? array();
+
+$actor = Actors::get_by_id( $user_id );
+if ( is_wp_error( $actor ) ) {
+	return;
+}
+
+// Set up the Interactivity API state.
+$state = wp_interactivity_state(
+	'activitypub/follow-me',
+	array(
+		'namespace' => ACTIVITYPUB_REST_NAMESPACE,
+		'i18n'      => array(
+			'copied'              => __( 'Copied!', 'activitypub' ),
+			'copy'                => __( 'Copy', 'activitypub' ),
+			'emptyProfileError'   => __( 'Please enter a profile URL or handle.', 'activitypub' ),
+			'invalidProfileError' => __( 'Please enter a valid URL or handle.', 'activitypub' ),
+			'genericError'        => __( 'An error occurred. Please try again.', 'activitypub' ),
+		),
+	)
+);
+
+// Add the block wrapper attributes.
+$wrapper_attributes = get_block_wrapper_attributes(
+	array(
+		'id'                           => $block_id,
+		'class'                        => 'activitypub-follow-me-block-wrapper',
+		'data-wp-interactive'          => 'activitypub/follow-me',
+		'data-wp-init'                 => 'callbacks.initButtonStyles',
+		'data-wp-on-document--keydown' => 'callbacks.documentKeydown',
+		'data-wp-on-document--click'   => 'callbacks.documentClick',
+	)
+);
+
+$wrapper_context = wp_interactivity_data_wp_context(
+	array(
+		'blockId'         => $block_id,
+		'isModalOpen'     => false,
+		'remoteProfile'   => '',
+		'isLoading'       => false,
+		'isError'         => false,
+		'errorMessage'    => '',
+		'copyButtonText'  => $state['i18n']['copy'],
+		'userId'          => $user_id,
+		'buttonOnly'      => $button_only,
+		'buttonStyle'     => $button_style,
+		'backgroundColor' => $background_color,
+		'webfinger'       => '@' . $actor->get_webfinger(),
+	)
+);
+
+/* @var string $content Inner blocks content. */
+if ( empty( $content ) ) {
+	$button_text = $attributes['buttonText'] ?? __( 'Follow', 'activitypub' );
+	$content     = '<div class="wp-block-button"><button class="wp-block-button__link wp-element-button">' . esc_html( $button_text ) . '</button></div>';
+}
+$content = Blocks::add_directions(
+	$content,
+	array( 'class_name' => 'wp-element-button' ),
+	array(
+		'data-wp-on--click'           => 'actions.toggleModal',
+		'data-wp-bind--aria-expanded' => 'context.isModalOpen',
+		'aria-label'                  => __( 'Follow me on the Fediverse', 'activitypub' ),
+		'aria-haspopup'               => 'dialog',
+		'aria-controls'               => 'modal-heading',
+	)
+);
+
+?>
+<div
+	<?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput ?>
+	<?php echo $wrapper_context; // phpcs:ignore WordPress.Security.EscapeOutput ?>
+>
+	<div class="activitypub-profile">
+		<?php if ( ! $button_only ) : ?>
+			<img
+				class="activitypub-profile__avatar"
+				src="<?php echo esc_url( $actor->get_icon()['url'] ); ?>"
+				alt="<?php echo esc_attr( $actor->get_name() ); ?>"
+			/>
+			<div class="activitypub-profile__content">
+				<div class="activitypub-profile__name"><?php echo esc_html( $actor->get_name() ); ?></div>
+				<div class="activitypub-profile__handle"><?php echo esc_html( '@' . $actor->get_webfinger() ); ?></div>
+			</div>
+		<?php endif; ?>
+
+		<?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput ?>
+	</div>
+
+	<div
+		class="activitypub-modal__overlay"
+		data-wp-bind--hidden="!context.isModalOpen"
+		role="dialog"
+		aria-modal="true"
+		aria-labelledby="modal-heading"
+	>
+		<div class="activitypub-modal__frame">
+			<div class="activitypub-modal__header">
+				<h2 id="modal-heading" class="activitypub-modal__title">
+					<?php
+					printf(
+						/* translators: %s: Profile name. */
+						esc_html__( 'Follow %s', 'activitypub' ),
+						esc_html( $actor->get_name() )
+					);
+					?>
+				</h2>
+				<button
+					type="button"
+					class="activitypub-modal__close wp-element-button wp-block-button__link"
+					data-wp-on--click="actions.closeModal"
+					aria-label="<?php echo esc_attr__( 'Close dialog', 'activitypub' ); ?>"
+				>
+					<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" role="img" aria-hidden="true" focusable="false">
+						<path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path>
+					</svg>
+				</button>
+			</div>
+			<div class="activitypub-modal__content">
+				<div class="activitypub-dialog__section">
+					<h4><?php echo esc_html__( 'My Profile', 'activitypub' ); ?></h4>
+					<div class="activitypub-dialog__description">
+						<?php echo esc_html__( 'Copy and paste my profile into the search field of your favorite fediverse app or server.', 'activitypub' ); ?>
+					</div>
+					<div class="activitypub-dialog__button-group">
+						<input
+							type="text"
+							id="profile-handle"
+							value="<?php echo esc_attr( '@' . $actor->get_webfinger() ); ?>"
+							tabindex="-1"
+							readonly
+							aria-readonly="true"
+						/>
+						<button
+							type="button"
+							class="wp-element-button wp-block-button__link"
+							data-wp-on--click="actions.copyToClipboard"
+							aria-label="<?php echo esc_attr__( 'Copy handle to clipboard', 'activitypub' ); ?>"
+						>
+							<span data-wp-text="context.copyButtonText"></span>
+						</button>
+					</div>
+				</div>
+				<div class="activitypub-dialog__section">
+					<h4><?php echo esc_html__( 'Your Profile', 'activitypub' ); ?></h4>
+					<div class="activitypub-dialog__description">
+						<?php echo esc_html__( 'Or, if you know your own profile, we can start things that way!', 'activitypub' ); ?>
+					</div>
+					<div class="activitypub-dialog__button-group">
+						<input
+							type="text"
+							id="remote-profile"
+							placeholder="<?php echo esc_attr__( '@username@example.com', 'activitypub' ); ?>"
+							data-wp-bind--value="context.remoteProfile"
+							data-wp-on--input="actions.updateRemoteProfile"
+							data-wp-on--keydown="actions.handleKeyDown"
+							data-wp-bind--aria-invalid="context.isError"
+						/>
+						<button
+							type="button"
+							class="wp-element-button wp-block-button__link"
+							data-wp-on--click="actions.submitRemoteProfile"
+							aria-label="<?php echo esc_attr__( 'Follow', 'activitypub' ); ?>"
+							data-wp-bind--disabled="context.isLoading"
+						>
+							<span data-wp-bind--hidden="context.isLoading">
+								<?php echo esc_html__( 'Follow', 'activitypub' ); ?>
+							</span>
+							<span data-wp-bind--hidden="!context.isLoading">
+								<?php echo esc_html__( 'Loading...', 'activitypub' ); ?>
+							</span>
+						</button>
+					</div>
+					<div
+						class="activitypub-dialog__error"
+						data-wp-bind--hidden="!context.isError"
+						data-wp-text="context.errorMessage"
+					></div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<?php
diff --git a/src/follow-me/save.js b/src/follow-me/save.js
new file mode 100644
index 000000000..48c46b4aa
--- /dev/null
+++ b/src/follow-me/save.js
@@ -0,0 +1,17 @@
+import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
+
+/**
+ * Save component for the Follow Me block.
+ *
+ * This component ensures that inner blocks (the button) are properly saved.
+ *
+ * @return {JSX.Element|null} Save component.
+ */
+function save() {
+	const blockProps = useBlockProps.save();
+	const innerBlocksProps = useInnerBlocksProps.save( blockProps );
+
+	return <div { ...innerBlocksProps } />;
+}
+
+export default save;
diff --git a/src/follow-me/style.scss b/src/follow-me/style.scss
index 8a70618e5..54457d580 100644
--- a/src/follow-me/style.scss
+++ b/src/follow-me/style.scss
@@ -1,52 +1,214 @@
-@use '../shared/lightbox';
-
 .activitypub-follow-me-block-wrapper {
-	width: 100%;
-
-	// extra side padding for border/background colors
-	&.has-border-color, &.has-background {
-		.activitypub-profile {
-			padding-left: 1rem;
-			padding-right: 1rem;
-		}
-	}
+	display: block;
+	position: relative;
 
 	.activitypub-profile {
-
 		display: flex;
 		align-items: center;
-		// right/left padding overridden above for border/background colors
 		padding: 1rem 0;
 
-		.activitypub-profile__avatar {
-			height: 75px;
+		&__avatar {
 			width: 75px;
-			margin-right: 1rem;
+			height: 75px;
 			border-radius: 50%;
+			margin-right: 1rem;
+			object-fit: cover;
 		}
-		.activitypub-profile__content {
+
+		&__content {
 			flex: 1;
+			margin-right: 1rem;
 			min-width: 0;
 		}
-		.activitypub-profile__name, .activitypub-profile__handle {
-			margin: 0;
-			line-height: 1.2;
+
+		&__name {
+			font-size: 1.25em;
 			white-space: nowrap;
 			overflow: hidden;
 			text-overflow: ellipsis;
 		}
-		.activitypub-profile__name {
-			font-size: 1.25em;
+
+		&__name,
+		&__handle {
+			color: inherit;
+			line-height: 1.2;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
 		}
-		.activitypub-profile__follow {
-			align-self: center;
-			background-color: var(--wp--preset--color--black);
-			color: var(--wp--preset--color--white);
 
-			// Only add left margin to the button when there's profile content.
+		.wp-block-button {
+			margin: 0;
+			display: flex;
+			align-items: center;
+
 			&:not(:only-child) {
 				margin-left: 1rem;
 			}
 		}
+
+		.wp-block-button__link {
+			margin: 0;
+		}
+
+		.is-small {
+			padding: 0.25rem 0.5rem;
+			font-size: 0.8rem;
+		}
+
+		.is-compact {
+			padding: 0.4rem 0.8rem;
+			font-size: 0.9rem;
+		}
+	}
+
+	&.has-background .activitypub-profile,
+	&.has-border .activitypub-profile {
+		padding-left: 1rem;
+		padding-right: 1rem;
+	}
+}
+
+body.modal-open {
+	overflow: hidden;
+}
+
+.activitypub-modal {
+	&__overlay {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: rgba(0, 0, 0, 0.5);
+		color: initial;
+		z-index: 100000;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 1rem;
+
+		&[hidden] {
+			display: none;
+		}
+	}
+
+	&__frame {
+		max-width: 660px;
+		width: 100%;
+		background-color: var(--wp--preset--color--white);
+		border-radius: 4px;
+		box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+		overflow: hidden;
+		display: flex;
+		flex-direction: column;
+		max-height: calc(100vh - 2rem);
+		animation: activitypub-modal-appear 0.2s ease-out;
+	}
+
+	&__header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		padding: 2rem 2rem 1.5rem 2rem;
+		border-bottom: 1px solid var(--wp--preset--color--light-gray, #f0f0f0);
+		flex-shrink: 0;
+
+		.activitypub-modal__close {
+			border: none;
+			padding: 0.5rem;
+			cursor: pointer;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			width: auto;
+
+			&:active {
+				border: none;
+				padding: 0.5rem;
+			}
+		}
+	}
+
+	&__title {
+		margin: 0 !important;
+		font-size: 130%;
+		font-weight: 600;
+		line-height: 1.4;
+	}
+
+	&__content {
+		overflow-y: auto;
+	}
+}
+
+.activitypub-dialog {
+	&__section {
+		padding: 1.5rem 2rem;
+		border-bottom: 1px solid var(--wp--preset--color--light-gray, #f0f0f0);
+
+		&:last-child {
+			border-bottom: none;
+			padding-bottom: 2rem;
+		}
+
+		h4 {
+			margin-top: 0;
+			margin-bottom: 0.5rem;
+			font-size: 110%;
+		}
+	}
+
+	&__description {
+		margin-bottom: 1rem;
+		color: inherit;
+		font-size: 95%;
+	}
+
+	&__button-group {
+		display: flex;
+		width: 100%;
+		margin-bottom: 0.5rem;
+
+		input[type] {
+			flex: 1;
+			border: 1px solid var(--wp--preset--color--gray, #e2e4e7);
+			border-radius: 4px 0 0 4px;
+			line-height: 1;
+			margin: 0;
+
+			&::placeholder {
+				opacity: 0.5;
+			}
+
+			&[aria-invalid="true"] {
+				border-color: var(--wp--preset--color--vivid-red);
+			}
+		}
+
+		button {
+			border-radius: 0 4px 4px 0 !important;
+			margin-left: -1px !important;
+			min-width: 22.5%;
+			width: auto;
+		}
+	}
+
+	&__error {
+		color: var(--wp--preset--color--vivid-red);
+		font-size: 90%;
+		margin-top: 0.5rem;
+	}
+}
+
+/* Animation for modal */
+@keyframes activitypub-modal-appear {
+	from {
+		opacity: 0;
+		transform: translateY(20px);
+	}
+	to {
+		opacity: 1;
+		transform: translateY(0);
 	}
 }
diff --git a/src/follow-me/view.js b/src/follow-me/view.js
index c1b0cd1bc..b1d9b0e4c 100644
--- a/src/follow-me/view.js
+++ b/src/follow-me/view.js
@@ -1,16 +1,301 @@
-import { createRoot } from '@wordpress/element';
-import domReady from '@wordpress/dom-ready';
-import FollowMe from './follow-me';
-
-let id = 1;
-function getUniqueId() {
-	return `activitypub-follow-me-block-${ id++ }`;
-}
-
-domReady( () => {
-	// iterate over a nodelist
-	[].forEach.call( document.querySelectorAll( '.activitypub-follow-me-block-wrapper' ), ( element ) => {
-		const attrs = JSON.parse( element.dataset.attrs );
-		createRoot( element ).render( <FollowMe { ...attrs } id={ getUniqueId() } useId={ true } /> );
-	} );
-} );
\ No newline at end of file
+import { store, getContext } from '@wordpress/interactivity';
+import { getBlockStyles, getPopupStyles } from './button-style';
+import './style.scss';
+
+/** @var {object} wp WordPress global. */
+const { apiFetch } = window.wp;
+
+const { state, actions, utils } = store( 'activitypub/follow-me', {
+	actions: {
+		/**
+		 * Open the modal.
+		 */
+		openModal() {
+			const context = getContext();
+			context.isModalOpen = true;
+			document.body.classList.add( 'modal-open' );
+
+			// Set up the focus trap after modal is open.
+			setTimeout( () => {
+				// Use the blockId to find the specific modal frame for this block.
+				const blockWrapper = document.getElementById( context.blockId );
+				if ( blockWrapper ) {
+					const modalFrame = blockWrapper.querySelector( '.activitypub-modal__frame' );
+					if ( modalFrame ) {
+						utils.trapFocus( modalFrame );
+					}
+				}
+			}, 50 );
+		},
+
+		/**
+		 * Close the modal.
+		 */
+		closeModal() {
+			const context = getContext();
+			context.isModalOpen = false;
+			context.isError = false;
+			document.body.classList.remove( 'modal-open' );
+
+			// Return focus to the button that opened the modal.
+			const blockWrapper = document.getElementById( context.blockId );
+			if ( blockWrapper ) {
+				const openButton = blockWrapper.querySelector( '.wp-block-button__link' );
+				if ( openButton ) {
+					openButton.focus();
+				}
+			}
+		},
+
+		toggleModal() {
+			const { isModalOpen } = getContext();
+
+			isModalOpen ? actions.closeModal() : actions.openModal();
+		},
+
+		/**
+		 * Copy the webfinger to clipboard.
+		 */
+		copyToClipboard() {
+			const context = getContext();
+
+			// Use the Clipboard API to copy text.
+			navigator.clipboard.writeText( context.webfinger ).then(
+				() => {
+					// Update button text to show success.
+					context.copyButtonText = state.i18n.copied;
+
+					// Reset button text after 1 second.
+					setTimeout( () => {
+						context.copyButtonText = state.i18n.copy;
+					}, 1000 );
+				},
+				( error ) => {
+					// Log error if copying fails.
+					console.error( 'Could not copy text: ', error );
+				}
+			);
+		},
+
+		/**
+		 * Update the remote profile value.
+		 *
+		 * @param {Event} event Input event.
+		 */
+		updateRemoteProfile( event ) {
+			const context = getContext();
+			context.remoteProfile = event.target.value;
+			// Reset error state when input changes.
+			context.isError = false;
+			context.errorMessage = '';
+		},
+
+		/**
+		 * Handle keydown event for remote profile input.
+		 *
+		 * @param {Event} event Keydown event.
+		 */
+		handleKeyDown( event ) {
+			if ( event.key === 'Enter' ) {
+				event.preventDefault();
+				actions.submitRemoteProfile();
+			}
+		},
+
+		/**
+		 * Submit the remote profile.
+		 */
+		submitRemoteProfile: function* () {
+			const context = getContext();
+			const { namespace } = state;
+			const input = context.remoteProfile.trim();
+
+			// Validate input.
+			if ( ! input ) {
+				context.isError = true;
+				context.errorMessage = state.i18n.emptyProfileError;
+				return;
+			}
+
+			if ( ! utils.isHandle( input ) ) {
+				context.isError = true;
+				context.errorMessage = state.i18n.invalidProfileError;
+				return;
+			}
+
+			// Set loading state.
+			context.isLoading = true;
+			context.isError = false;
+
+			// Construct the API path.
+			const path = `/${ namespace }/actors/${ context.userId }/remote-follow?resource=${ encodeURIComponent(
+				input
+			) }`;
+
+			try {
+				// Make the API request.
+				const response = yield apiFetch( { path } );
+
+				// Set opening state.
+				context.isLoading = false;
+
+				// Open the remote follow URL in a new tab.
+				window.open( response.url, '_blank' );
+
+				// Close the modal after opening the URL.
+				actions.closeModal();
+			} catch ( error ) {
+				// Handle error.
+				console.error( 'Error submitting profile:', error );
+				context.isLoading = false;
+				context.isError = true;
+				context.errorMessage = error.message || state.i18n.genericError;
+			}
+		},
+	},
+	callbacks: {
+		/**
+		 * Initialize button styles.
+		 */
+		initButtonStyles: () => {
+			const { buttonStyle, backgroundColor, blockId } = getContext();
+
+			// Add dynamic button styles to the document.
+			if ( blockId && buttonStyle ) {
+				const styleElement = document.createElement( 'style' );
+				const selector = `#${ blockId }`;
+
+				// Use getBlockStyles from button-style.js to get the CSS string.
+				styleElement.textContent = getBlockStyles( selector, buttonStyle, backgroundColor );
+
+				document.head.appendChild( styleElement );
+
+				// Add popup styles.
+				const popupStyleElement = document.createElement( 'style' );
+				popupStyleElement.textContent = getPopupStyles( buttonStyle );
+				document.head.appendChild( popupStyleElement );
+			}
+		},
+
+		/**
+		 * Close modal when pressing ESC key.
+		 *
+		 * @param {Event} event Keyboard event.
+		 */
+		documentKeydown( event ) {
+			const { isModalOpen } = getContext();
+
+			if ( isModalOpen && event.key === 'Escape' ) {
+				actions.closeModal();
+			}
+		},
+
+		/**
+		 * Close modal when clicking outside.
+		 *
+		 * @param {Event} event Click event.
+		 */
+		documentClick( event ) {
+			const { blockId, isModalOpen } = getContext();
+			if ( ! isModalOpen ) {
+				return;
+			}
+
+			// Get the block wrapper element.
+			const blockWrapper = document.getElementById( blockId );
+			if ( ! blockWrapper ) {
+				return;
+			}
+
+			// If the click was on the button or its children, we should not close the modal.
+			const toggleButton = blockWrapper.querySelector(
+				'.wp-element-button[data-wp-on--click="actions.toggleModal"]'
+			);
+			if ( toggleButton && ( toggleButton === event.target || toggleButton.contains( event.target ) ) ) {
+				return;
+			}
+
+			// Check if the click was inside the modal frame.
+			const modalFrame = blockWrapper.querySelector( '.activitypub-modal__frame' );
+			if ( ! modalFrame || modalFrame.contains( event.target ) ) {
+				return;
+			}
+
+			actions.closeModal();
+		},
+	},
+	utils: {
+		/**
+		 * Best guess whether a string is a valid ActivityPub handle.
+		 *
+		 * @param {string} string - String to check.
+		 * @returns {boolean} True if string is a valid handle, false otherwise.
+		 */
+		isHandle( string ) {
+			// Check if the string starts with '@' and contains a valid URL.
+			const parts = string.replace( /^@/, '' ).split( '@' );
+
+			return parts.length === 2 && utils.isUrl( `https://${ parts[ 1 ] }` );
+		},
+
+		/**
+		 * Checks if a string is a valid URL.
+		 *
+		 * @param {string} string - String to check.
+		 * @returns {boolean} True if string is a valid URL, false otherwise.
+		 */
+		isUrl( string ) {
+			try {
+				new URL( string );
+				return true;
+			} catch ( _ ) {
+				return false;
+			}
+		},
+	},
+
+	/**
+	 * Traps focus within the specified element.
+	 *
+	 * @param {Element} element The element to trap focus within.
+	 */
+	trapFocus( element ) {
+		const focusableElements = element.querySelectorAll(
+			'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'
+		);
+		const firstFocusableElement = focusableElements[ 0 ];
+		const lastFocusableElement = focusableElements[ focusableElements.length - 1 ];
+
+		// If the first focusable element is the close button, set initial focus to the next element instead.
+		if (
+			firstFocusableElement &&
+			firstFocusableElement.classList.contains( 'activitypub-modal__close' ) &&
+			focusableElements.length > 1
+		) {
+			// Set initial focus to the second element, but keep firstFocusableElement as is for tab trapping.
+			focusableElements[ 1 ].focus();
+		} else {
+			// Otherwise focus the first element as usual.
+			firstFocusableElement.focus();
+		}
+
+		element.addEventListener( 'keydown', function ( event ) {
+			if ( event.key !== 'Tab' && event.keyCode !== 9 /* KEYCODE_TAB */ ) {
+				return;
+			}
+
+			if ( event.shiftKey ) {
+				/* shift + tab */
+				if ( document.activeElement === firstFocusableElement ) {
+					lastFocusableElement.focus();
+					event.preventDefault();
+				}
+			} /* tab */ else {
+				if ( document.activeElement === lastFocusableElement ) {
+					firstFocusableElement.focus();
+					event.preventDefault();
+				}
+			}
+		} );
+	},
+} );


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates the Follow Me block to use the new WordPress Interactivity API for front-end rendering while removing its dependency on React for the interactive behavior. It also bumps the version to 2.0.0, updates block metadata and style files, removes the old follow-me.js legacy code, and adjusts build/package configurations for experimental modules.

### Reviewed Changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                  | Description                                                                                          |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| src/follow-me/view.js                 | Replaces React DOM rendering with Interactivity API state and adds focus trapping for modals.        |
| src/follow-me/style.scss              | Updates the component’s styling for improved layout and consistent spacing.                          |
| src/follow-me/render.php              | Implements server‑side rendering using updated block attributes and state from Interactivity API.      |
| src/follow-me/index.js                | Adjusts block registration to align with experimental module flags and updated dependencies.         |
| src/follow-me/follow-me.js            | Entirely removed as it’s replaced by the new interactive implementation.                             |
| src/follow-me/edit.js                 | Updates the editor preview using new profile fetching methods and improved user option handling.       |
| src/follow-me/button-style.js         | Refactors style generation functions for the block and popup buttons.                                |
| src/follow-me/block.json              | Updates version, adds an example definition, enables interactivity, and switches to module formats.   |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/follow-me/view.js:18**
* [nitpick] Consider removing or cleaning up the keydown event listener when the modal is closed to prevent potential memory leaks.
```
element.addEventListener( 'keydown', function ( event ) { ... } );
```
**src/follow-me/edit.js:130**
* [nitpick] Consider adding error handling for the profile fetch to update the UI in case the API request fails, ensuring that the editor remains in a consistent state.
```
fetchProfile( effectiveUserId ).then( ( data ) => { setProfile( getNormalizedProfile( data ) ); } );
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-11T14:54:22Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request migrates the Follow Me block to use the new Interactivity API for front‑end rendering and removes its dependency on React. Key changes include switching the block’s save mechanism to a proper server‑side render with a new render.php file, deprecating and removing legacy React‑based code (including the old follow‑me.js file), and updating block metadata and build configurations for a major version bump.

### Reviewed Changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                    | Description                                                                       |
| --------------------------------------- | --------------------------------------------------------------------------------- |
| src/follow-me/index.js                  | Refactored to import a proper save function and include deprecated support.       |
| src/follow-me/follow-me.js              | Entirely removed legacy React code that is replaced by the Interactivity API.     |
| src/follow-me/edit.js                    | Updated the editor component by removing custom button text/size controls in favor of inner blocks. |
| src/follow-me/deprecation.js             | Added migration support for converting attribute values to a core button block.   |
| src/follow-me/button-style.js            | Improved CSS variable handling and style generation.                            |
| src/follow-me/block.json                 | Updated version and block supports including inner blocks and interactivity.      |
| package.json                             | Updated build scripts and dependencies to use experimental modules.               |
| includes/class-blocks.php                | Removed a custom render callback for follow‑me and changed get_user_id visibility. |
| build/follow-me/render.php               | New server‑side render file implementing the Interactivity API state and markup.  |
| (Other build and config files)           | Adjusted build artifacts and changelog entry to align with the major update.      |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**includes/class-blocks.php:216**
* Changing get_user_id() from private to public may affect encapsulation; ensure that any external dependencies or calls are updated accordingly and consider updating the PHPDoc to clearly reflect its new visibility.
```
private static function get_user_id( $user_string ) {
```
**src/follow-me/edit.js:173**
* The removal of the Button Text and Button Size controls aligns with the migration to the core button block. Please ensure that the attribute definitions and related documentation or migration notes are updated to reflect that these properties are now managed via inner blocks.
```
TextControl and SelectControl blocks for buttonText and buttonSize (removed)
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T21:23:48Z
user: login: obenland
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T02:29:32Z
user: login: pfefferle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T16:38:51Z
user: login: pfefferle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T16:39:50Z
user: login: pfefferle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T17:22:55Z
user: login: obenland
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T18:03:34Z
user: login: obenland
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T18:03:58Z
user: login: obenland
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T18:50:57Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates the Follow Me block to use the new Interactivity API for frontend rendering, removes its React dependency, and requires a major version bump with WordPress 6.5+.

- Replaces the React-based frontend (`follow-me.js`) with an Interactivity API–driven `view.js` and server-side `render.php`.
- Refactors editor code (`edit.js`) to use `InnerBlocks` for the button and normalizes profile data.
- Updates `block.json`, build scripts, and PHP block registration to enable interactivity and bump package versions.

### Reviewed Changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                 | Description                                                  |
|--------------------------------------|--------------------------------------------------------------|
| src/follow-me/index.js               | Added `deprecated` and `save`, registered block with interactivity. |
| src/follow-me/follow-me.js           | Removed legacy React component.                              |
| src/follow-me/edit.js                | Refactored editor to use `useInnerBlocksProps` and normalized data. |
| src/follow-me/deprecation.js         | New migration logic to convert old attributes to `core/button`. |
| src/follow-me/button-style.js        | Implemented helpers to generate CSS selectors and var resolution. |
| src/follow-me/block.json             | Bumped version, added `interactivity` support and innerBlocks config. |
| package.json                         | Updated scripts for experimental modules, bumped dependencies. |
| includes/class-blocks.php            | Switched to metadata registration, added interactivity callbacks. |
| build/follow-me/* & build/reply/*    | Regenerated assets for interactivity and removed React bundles. |
| .prettierignore                      | Removed `src/follow-me` from ignore so it’s formatted.       |
| .github/changelog/1691-from-description | Added changelog entry.                                    |
</details>



<details>
<summary>Comments suppressed due to low confidence (4)</summary>

**src/follow-me/deprecation.js:7**
* The deprecation migration logic for v1 should have accompanying unit tests to verify that attributes (e.g., buttonText, buttonOnly) are correctly migrated to the new innerBlock format.
```
const v1 = {
```
**src/follow-me/button-style.js:7**
* [nitpick] isCssVariableDefined invokes getComputedStyle on each call, which may run frequently and cause layout thrashing; consider caching the root styles or memoizing checks for known variables.
```
function isCssVariableDefined( variableName ) {
```
**src/follow-me/button-style.js:29**
* The helper functions getBackgroundColor, getLinkColor, getBlockStyles, and getPopupStyles involve nontrivial string logic for CSS variables—adding unit tests can help catch formatting or resolution regressions.
```
function getBackgroundColor( color ) {
```
**src/follow-me/edit.js:17**
* [nitpick] The DEFAULT_PROFILE_DATA and getNormalizedProfile logic appear duplicated across modules; consider extracting these into a shared utility to reduce duplication and ease future updates.
```
const DEFAULT_PROFILE_DATA = {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T20:27:50Z
user: login: pfefferle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T08:04:41Z
user: login: pfefferle
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-21T06:33:09Z

Review comments:
diff_hunk: @@ -469,4 +432,37 @@ function ( $paragraph ) {
 
 		return $data;
 	}
+
+	/**
+	 * Add directions to the button.
+	 *
+	 * @param string $content    The block content.
+	 * @param array  $attributes The block attributes.
+	 *
+	 * @return string The updated content.
+	 */
+	public static function add_directions_to_button( $content, $attributes ) {
+		$tags = new \WP_HTML_Tag_Processor( $content );
+
+		$button_class = '';
+		if ( isset( $attributes['buttonSize'] ) && 'small' === $attributes['buttonSize'] ) {
+			$button_class = 'is-small';
+		} elseif ( isset( $attributes['buttonSize'] ) && 'compact' === $attributes['buttonSize'] ) {
+			$button_class = 'is-compact';
+		}
path: includes/class-blocks.php
user: login: obenland
body: I'm leaning towards removing this functionality and just keep customization options to whatever the core button block offers. What do you think?
created_at: 2025-05-16T02:29:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,195 @@
+<?php
+/**
+ * Server-side rendering of the `activitypub/follow-me` block.
+ *
+ * @package ActivityPub
+ */
+
+use Activitypub\Blocks;
+
+/* @var array $attributes Block attributes. */
+$attributes = wp_parse_args( $attributes );
+
+// Get the user ID from the selected user attribute.
+$selected_user = $attributes['selectedUser'] ?? 'site';
+$user_id       = Blocks::get_user_id( $selected_user );
+$button_only   = $attributes['buttonOnly'] ?? false;
+
+// Generate a unique ID for the block.
+$block_id = 'activitypub-follow-me-block-' . wp_unique_id();
+
+// Get block style information.
+$style            = wp_get_global_styles();
+$background_color = $attributes['backgroundColor'] ?? $style['color']['background'] ?? '';
+
+// Get button style from block attributes.
+$button_style = $attributes['style'] ?? array();
+
+$actor = \Activitypub\Collection\Actors::get_by_id( $user_id );
path: build/follow-me/render.php
user: login: pfefferle
body: we should stick with either full path (including Namespace) or define it as `use`, but not mix it.

See `use Activitypub\Blocks;`
created_at: 2025-05-19T16:38:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1 @@
+{"version":3,"file":"follow-me/view.js","mappings":"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","sources":["webpack://wordpress-activitypub/./src/follow-me/button-style.js","webpack://wordpress-activitypub/./src/follow-me/style.scss?8459","webpack://wordpress-activitypub/./src/follow-me/view.js","webpack://wordpress-activitypub/external module \"@wordpress/interactivity\"","webpack://wordpress-activitypub/webpack/bootstrap","webpack://wordpress-activitypub/webpack/runtime/chunk loaded","webpack://wordpress-activitypub/webpack/runtime/define property getters","webpack://wordpress-activitypub/webpack/runtime/hasOwnProperty shorthand","webpack://wordpress-activitypub/webpack/runtime/make namespace object","webpack://wordpress-activitypub/webpack/runtime/import chunk loading","webpack://wordpress-activitypub/webpack/before-startup","webpack://wordpress-activitypub/webpack/startup","webpack://wordpress-activitypub/webpack/after-startup"],"sourcesContent":["/**\n * Checks if a CSS variable is defined.\n *\n * @param {string} variableName The CSS variable name to check.\n * @return {boolean} Whether the variable is defined.\n */\nfunction isCssVariableDefined( variableName ) {\n\t// Return false if we're in a server-side context.\n\tif ( typeof window === 'undefined' || ! window.getComputedStyle ) {\n\t\treturn false;\n\t}\n\n\t// Get the computed style of the root element.\n\tconst styles = window.getComputedStyle( document.documentElement );\n\t\n\t// Get the value of the CSS variable.\n\tconst value = styles.getPropertyValue( variableName ).trim();\n\t\n\t// If the value is empty, the variable is not defined or is set to an empty value.\n\treturn value !== '';\n}\n\n/**\n * Gets the background color from a style object.\n *\n * @param {Object|string} color Color object or string.\n * @return {string|null} Background color.\n */\nfunction getBackgroundColor( color ) {\n\t// If color is a string, it's a var like this.\n\tif ( typeof color === 'string' ) {\n\t\tconst varName = `--wp--preset--color--${ color }`;\n\t\tif ( ! isCssVariableDefined( varName ) ) {\n\t\t\treturn null;\n\t\t}\n\t\treturn `var(${ varName })`;\n\t}\n\n\treturn color?.color?.background || null;\n}\n\n/**\n * Gets the link color from a style object.\n *\n * @param {string} text Text color.\n * @return {string|null} Link color.\n */\nfunction getLinkColor( text ) {\n\tif ( typeof text !== 'string' ) {\n\t\treturn null;\n\t}\n\t// If it starts with a hash, leave it be.\n\tif ( text.match( /^#/ ) ) {\n\t\t// We don't handle the alpha channel if present.\n\t\treturn text.substring( 0, 7 );\n\t}\n\t// var:preset|color|luminous-vivid-amber\n\t// var(--wp--preset--color--luminous-vivid-amber)\n\t// We will receive the top format, we need to output the bottom format.\n\tconst [ , , color ] = text.split( '|' );\n\tconst varName = `--wp--preset--color--${ color }`;\n\t\n\t// Check if the CSS variable is defined before using it.\n\tif ( ! isCssVariableDefined( varName ) ) {\n\t\treturn null;\n\t}\n\t\n\treturn `var(${ varName })`;\n}\n\n/**\n * Generates a CSS selector.\n *\n * @param {string} selector CSS selector.\n * @param {string} prop CSS property.\n * @param {string|null} value CSS value.\n * @param {string} pseudo Pseudo-selector.\n * @return {string} CSS selector.\n */\nfunction generateSelector( selector, prop, value = null, pseudo = '' ) {\n\tif ( ! value ) {\n\t\treturn '';\n\t}\n\treturn `${ selector }${ pseudo } { ${ prop }: ${ value }; }\\n`;\n}\n\n/**\n * Gets styles for a button.\n *\n * @param {string} selector CSS selector.\n * @param {string} button Button color.\n * @param {string} text Text color.\n * @param {string} hover Hover color.\n * @return {string} CSS styles.\n */\nfunction getStyles( selector, button, text, hover ) {\n\treturn (\n\t\tgenerateSelector( selector, 'background-color', button ) +\n\t\tgenerateSelector( selector, 'color', text ) +\n\t\tgenerateSelector( selector, 'background-color', hover, ':hover' ) +\n\t\tgenerateSelector( selector, 'background-color', hover, ':focus' )\n\t);\n}\n\n/**\n * Gets block styles.\n *\n * @param {string} base Base selector.\n * @param {Object} style Style object.\n * @param {Object|string} backgroundColor Background color.\n * @return {string} CSS styles.\n */\nexport function getBlockStyles( base, style, backgroundColor ) {\n\tconst selector = `${ base } .wp-block-button__link`;\n\n\t// We grab the background color if set as a good color for our button text.\n\tconst buttonTextColor =\n\t\tgetBackgroundColor( backgroundColor ) ||\n\t\t// Background might be in this form.\n\t\tstyle?.color?.background;\n\n\t// We misuse the link color for the button background.\n\tconst buttonColor = getLinkColor( style?.elements?.link?.color?.text );\n\tconst buttonHoverColor = getLinkColor( style?.elements?.link?.[ ':hover' ]?.color?.text );\n\n\treturn getStyles( selector, buttonColor, buttonTextColor, buttonHoverColor );\n}\n\n/**\n * Gets popup styles.\n *\n * @param {Object} style Style object.\n * @return {string} CSS styles.\n */\nexport function getPopupStyles( style ) {\n\t// We don't accept backgroundColor because the popup is always white (right?).\n\tconst buttonColor = getLinkColor( style?.elements?.link?.color?.text ) || '#111';\n\tconst buttonTextColor = '#fff';\n\tconst buttonHoverColor = getLinkColor( style?.elements?.link?.[ ':hover' ]?.color?.text ) || '#333';\n\tconst selector = '.activitypub-dialog__button-group .wp-block-button';\n\n\treturn getStyles( selector, buttonColor, buttonTextColor, buttonHoverColor );\n}\n","// extracted by mini-css-extract-plugin\nexport {};","import { store, getContext } from '@wordpress/interactivity';\nimport { getBlockStyles, getPopupStyles } from './button-style';\nimport './style.scss';\n\n/** @var {object} wp WordPress global. */\nconst { apiFetch } = window.wp;\n\n/**\n * Traps focus within the specified element.\n *\n * @param {Element} element The element to trap focus within.\n */\nfunction trapFocus( element ) {\n\tvar focusableElements = element.querySelectorAll(\n\t\t'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type=\"text\"]:not([disabled]), input[type=\"radio\"]:not([disabled]), input[type=\"checkbox\"]:not([disabled]), select:not([disabled])'\n\t);\n\tvar firstFocusableElement = focusableElements[ 0 ];\n\tvar lastFocusableElement = focusableElements[ focusableElements.length - 1 ];\n\tfirstFocusableElement.focus();\n\n\telement.addEventListener( 'keydown', function ( event ) {\n\t\tif ( event.key !== 'Tab' && event.keyCode !== 9 /* KEYCODE_TAB */ ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( event.shiftKey ) {\n\t\t\t/* shift + tab */\n\t\t\tif ( document.activeElement === firstFocusableElement ) {\n\t\t\t\tlastFocusableElement.focus();\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\t\t} /* tab */ else {\n\t\t\tif ( document.activeElement === lastFocusableElement ) {\n\t\t\t\tfirstFocusableElement.focus();\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\t\t}\n\t} );\n}\n\nconst { state, actions } = store( 'activitypub/follow-me', {\n\tactions: {\n\t\t/**\n\t\t * Open the modal.\n\t\t */\n\t\topenModal() {\n\t\t\tconst context = getContext();\n\t\t\tcontext.isModalOpen = true;\n\t\t\tdocument.body.classList.add( 'modal-open' );\n\n\t\t\t// Set up the focus trap after modal is open.\n\t\t\tsetTimeout( () => {\n\t\t\t\t// Use the blockId to find the specific modal frame for this block.\n\t\t\t\tconst blockWrapper = document.getElementById( context.blockId );\n\t\t\t\tif ( blockWrapper ) {\n\t\t\t\t\tconst modalFrame = blockWrapper.querySelector( '.activitypub-modal__frame' );\n\t\t\t\t\tif ( modalFrame ) {\n\t\t\t\t\t\ttrapFocus( modalFrame );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, 50 );\n\t\t},\n\n\t\t/**\n\t\t * Close the modal.\n\t\t */\n\t\tcloseModal() {\n\t\t\tconst context = getContext();\n\t\t\tcontext.isModalOpen = false;\n\t\t\tdocument.body.classList.remove( 'modal-open' );\n\n\t\t\t// Return focus to the button that opened the modal.\n\t\t\tconst blockWrapper = document.getElementById( context.blockId );\n\t\t\tif ( blockWrapper ) {\n\t\t\t\tconst openButton = blockWrapper.querySelector( '.wp-block-button__link' );\n\t\t\t\tif ( openButton ) {\n\t\t\t\t\topenButton.focus();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\ttoggleModal() {\n\t\t\tconst { isModalOpen } = getContext();\n\n\t\t\tisModalOpen ? actions.closeModal() : actions.openModal();\n\t\t},\n\n\t\t/**\n\t\t * Copy the webfinger to clipboard.\n\t\t */\n\t\tcopyToClipboard() {\n\t\t\tconst context = getContext();\n\n\t\t\t// Use the Clipboard API to copy text.\n\t\t\tnavigator.clipboard.writeText( context.webfinger ).then(\n\t\t\t\t() => {\n\t\t\t\t\t// Update button text to show success.\n\t\t\t\t\tcontext.copyButtonText = state.i18n.copied;\n\n\t\t\t\t\t// Reset button text after 1 second.\n\t\t\t\t\tsetTimeout( () => {\n\t\t\t\t\t\tcontext.copyButtonText = state.i18n.copy;\n\t\t\t\t\t}, 1000 );\n\t\t\t\t},\n\t\t\t\t( error ) => {\n\t\t\t\t\t// Log error if copying fails.\n\t\t\t\t\tconsole.error( 'Could not copy text: ', error );\n\t\t\t\t}\n\t\t\t);\n\t\t},\n\n\t\t/**\n\t\t * Update the remote profile value.\n\t\t *\n\t\t * @param {Event} event Input event.\n\t\t */\n\t\tupdateRemoteProfile( event ) {\n\t\t\tconst context = getContext();\n\t\t\tcontext.remoteProfile = event.target.value;\n\t\t\t// Reset error state when input changes.\n\t\t\tcontext.isError = false;\n\t\t\tcontext.errorMessage = '';\n\t\t},\n\n\t\t/**\n\t\t * Handle keydown event for remote profile input.\n\t\t *\n\t\t * @param {Event} event Keydown event.\n\t\t */\n\t\thandleKeyDown( event ) {\n\t\t\tif ( event.key === 'Enter' ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tactions.submitRemoteProfile();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Submit the remote profile.\n\t\t */\n\t\tsubmitRemoteProfile: function* () {\n\t\t\tconst context = getContext();\n\t\t\tconst { namespace } = state;\n\t\t\tconst input = context.remoteProfile.trim();\n\n\t\t\t// Validate input.\n\t\t\tif ( ! input ) {\n\t\t\t\tcontext.isError = true;\n\t\t\t\tcontext.errorMessage = state.i18n.emptyProfileError;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( ! /^(https?:\\/\\/|@)/.test( input ) ) {\n\t\t\t\tcontext.isError = true;\n\t\t\t\tcontext.errorMessage = state.i18n.invalidProfileError;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set loading state.\n\t\t\tcontext.isLoading = true;\n\t\t\tcontext.isError = false;\n\n\t\t\t// Construct the API path.\n\t\t\tconst path = `/${ namespace }/actors/${ context.userId }/remote-follow?resource=${ encodeURIComponent(\n\t\t\t\tinput\n\t\t\t) }`;\n\n\t\t\ttry {\n\t\t\t\t// Make the API request.\n\t\t\t\tconst response = yield apiFetch( { path } );\n\n\t\t\t\t// Set opening state.\n\t\t\t\tcontext.isLoading = false;\n\n\t\t\t\t// Open the remote follow URL in a new tab.\n\t\t\t\twindow.open( response.url, '_blank' );\n\n\t\t\t\t// Close the modal after opening the URL.\n\t\t\t\tactions.closeModal();\n\t\t\t} catch ( error ) {\n\t\t\t\t// Handle error.\n\t\t\t\tconsole.error( 'Error submitting profile:', error );\n\t\t\t\tcontext.isLoading = false;\n\t\t\t\tcontext.isError = true;\n\t\t\t\tcontext.errorMessage = error.message || state.i18n.genericError;\n\t\t\t}\n\t\t},\n\t},\n\tcallbacks: {\n\t\t/**\n\t\t * Initialize button styles.\n\t\t */\n\t\tinitButtonStyles: () => {\n\t\t\tconst { buttonStyle, backgroundColor, blockId } = getContext();\n\n\t\t\t// Add dynamic button styles to the document.\n\t\t\tif ( blockId && buttonStyle ) {\n\t\t\t\tconst styleElement = document.createElement( 'style' );\n\t\t\t\tconst selector = `#${ blockId }`;\n\n\t\t\t\t// Use getBlockStyles from button-style.js to get the CSS string.\n\t\t\t\tstyleElement.textContent = getBlockStyles( selector, buttonStyle, backgroundColor );\n\n\t\t\t\tdocument.head.appendChild( styleElement );\n\n\t\t\t\t// Add popup styles.\n\t\t\t\tconst popupStyleElement = document.createElement( 'style' );\n\t\t\t\tpopupStyleElement.textContent = getPopupStyles( buttonStyle );\n\t\t\t\tdocument.head.appendChild( popupStyleElement );\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Close modal when pressing ESC key.\n\t\t *\n\t\t * @param {Event} event Keyboard event.\n\t\t */\n\t\tdocumentKeydown( event ) {\n\t\t\tconst { isModalOpen } = getContext();\n\n\t\t\tif ( isModalOpen && event.key === 'Escape' ) {\n\t\t\t\tactions.closeModal();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Close modal when clicking outside.\n\t\t *\n\t\t * @param {Event} event Click event.\n\t\t */\n\t\tdocumentClick( event ) {\n\t\t\tconst { blockId, isModalOpen } = getContext();\n\t\t\tif ( ! isModalOpen ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Get the block wrapper element.\n\t\t\tconst blockWrapper = document.getElementById( blockId );\n\t\t\tif ( ! blockWrapper ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If the click was on the button or its children, we should not close the modal.\n\t\t\tconst toggleButton = blockWrapper.querySelector(\n\t\t\t\t'.wp-element-button[data-wp-on--click=\"actions.toggleModal\"]'\n\t\t\t);\n\t\t\tif ( toggleButton && ( toggleButton === event.target || toggleButton.contains( event.target ) ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check if the click was inside the modal frame.\n\t\t\tconst modalFrame = blockWrapper.querySelector( '.activitypub-modal__frame' );\n\t\t\tif ( ! modalFrame || modalFrame.contains( event.target ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tactions.closeModal();\n\t\t},\n\t},\n} );\n","module.exports = __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","var deferred = [];\n__webpack_require__.O = (result, chunkIds, fn, priority) => {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar [chunkIds, fn, priority] = deferred[i];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t\"follow-me/view\": 0,\n\t\"follow-me/style-view\": 0\n};\n\n// no install chunk\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no external install chunk\n\n__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);","","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [\"follow-me/style-view\"], () => (__webpack_require__(\"./src/follow-me/view.js\")))\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n",""],"names":["isCssVariableDefined","variableName","window","getComputedStyle","styles","document","documentElement","value","getPropertyValue","trim","getBackgroundColor","color","varName","background","getLinkColor","text","match","substring","split","generateSelector","selector","prop","pseudo","getStyles","button","hover","getBlockStyles","base","style","backgroundColor","buttonTextColor","buttonColor","elements","link","buttonHoverColor","getPopupStyles","store","getContext","apiFetch","wp","trapFocus","element","focusableElements","querySelectorAll","firstFocusableElement","lastFocusableElement","length","focus","addEventListener","event","key","keyCode","shiftKey","activeElement","preventDefault","state","actions","openModal","context","isModalOpen","body","classList","add","setTimeout","blockWrapper","getElementById","blockId","modalFrame","querySelector","closeModal","remove","openButton","toggleModal","copyToClipboard","navigator","clipboard","writeText","webfinger","then","copyButtonText","i18n","copied","copy","error","console","updateRemoteProfile","remoteProfile","target","isError","errorMessage","handleKeyDown","submitRemoteProfile","namespace","input","emptyProfileError","test","invalidProfileError","isLoading","path","userId","encodeURIComponent","response","open","url","message","genericError","callbacks","initButtonStyles","buttonStyle","styleElement","createElement","textContent","head","appendChild","popupStyleElement","documentKeydown","documentClick","toggleButton","contains"],"sourceRoot":""}
path: build/follow-me/view.js.map
user: login: pfefferle
body: where does the map files come from? is that a config change?
created_at: 2025-05-19T16:39:49Z
author_association: MEMBER
diff_hunk: @@ -469,4 +432,37 @@ function ( $paragraph ) {
 
 		return $data;
 	}
+
+	/**
+	 * Add directions to the button.
+	 *
+	 * @param string $content    The block content.
+	 * @param array  $attributes The block attributes.
+	 *
+	 * @return string The updated content.
+	 */
+	public static function add_directions_to_button( $content, $attributes ) {
+		$tags = new \WP_HTML_Tag_Processor( $content );
+
+		$button_class = '';
+		if ( isset( $attributes['buttonSize'] ) && 'small' === $attributes['buttonSize'] ) {
+			$button_class = 'is-small';
+		} elseif ( isset( $attributes['buttonSize'] ) && 'compact' === $attributes['buttonSize'] ) {
+			$button_class = 'is-compact';
+		}
path: includes/class-blocks.php
user: login: pfefferle
body: I think it is important to have a "button" only mode, but let's remove the sizing for now.
created_at: 2025-05-19T17:22:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,195 @@
+<?php
+/**
+ * Server-side rendering of the `activitypub/follow-me` block.
+ *
+ * @package ActivityPub
+ */
+
+use Activitypub\Blocks;
+
+/* @var array $attributes Block attributes. */
+$attributes = wp_parse_args( $attributes );
+
+// Get the user ID from the selected user attribute.
+$selected_user = $attributes['selectedUser'] ?? 'site';
+$user_id       = Blocks::get_user_id( $selected_user );
+$button_only   = $attributes['buttonOnly'] ?? false;
+
+// Generate a unique ID for the block.
+$block_id = 'activitypub-follow-me-block-' . wp_unique_id();
+
+// Get block style information.
+$style            = wp_get_global_styles();
+$background_color = $attributes['backgroundColor'] ?? $style['color']['background'] ?? '';
+
+// Get button style from block attributes.
+$button_style = $attributes['style'] ?? array();
+
+$actor = \Activitypub\Collection\Actors::get_by_id( $user_id );
path: build/follow-me/render.php
user: login: obenland
body: Fixed in f86bc55b4a794930d687c3520c05de9515b41ae8
created_at: 2025-05-19T18:03:34Z
author_association: MEMBER
diff_hunk: @@ -469,4 +432,37 @@ function ( $paragraph ) {
 
 		return $data;
 	}
+
+	/**
+	 * Add directions to the button.
+	 *
+	 * @param string $content    The block content.
+	 * @param array  $attributes The block attributes.
+	 *
+	 * @return string The updated content.
+	 */
+	public static function add_directions_to_button( $content, $attributes ) {
+		$tags = new \WP_HTML_Tag_Processor( $content );
+
+		$button_class = '';
+		if ( isset( $attributes['buttonSize'] ) && 'small' === $attributes['buttonSize'] ) {
+			$button_class = 'is-small';
+		} elseif ( isset( $attributes['buttonSize'] ) && 'compact' === $attributes['buttonSize'] ) {
+			$button_class = 'is-compact';
+		}
path: includes/class-blocks.php
user: login: obenland
body: Fixed in f86bc55b4a794930d687c3520c05de9515b41ae8
created_at: 2025-05-19T18:03:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1 @@
+{"version":3,"file":"follow-me/view.js","mappings":"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","sources":["webpack://wordpress-activitypub/./src/follow-me/button-style.js","webpack://wordpress-activitypub/./src/follow-me/style.scss?8459","webpack://wordpress-activitypub/./src/follow-me/view.js","webpack://wordpress-activitypub/external module \"@wordpress/interactivity\"","webpack://wordpress-activitypub/webpack/bootstrap","webpack://wordpress-activitypub/webpack/runtime/chunk loaded","webpack://wordpress-activitypub/webpack/runtime/define property getters","webpack://wordpress-activitypub/webpack/runtime/hasOwnProperty shorthand","webpack://wordpress-activitypub/webpack/runtime/make namespace object","webpack://wordpress-activitypub/webpack/runtime/import chunk loading","webpack://wordpress-activitypub/webpack/before-startup","webpack://wordpress-activitypub/webpack/startup","webpack://wordpress-activitypub/webpack/after-startup"],"sourcesContent":["/**\n * Checks if a CSS variable is defined.\n *\n * @param {string} variableName The CSS variable name to check.\n * @return {boolean} Whether the variable is defined.\n */\nfunction isCssVariableDefined( variableName ) {\n\t// Return false if we're in a server-side context.\n\tif ( typeof window === 'undefined' || ! window.getComputedStyle ) {\n\t\treturn false;\n\t}\n\n\t// Get the computed style of the root element.\n\tconst styles = window.getComputedStyle( document.documentElement );\n\t\n\t// Get the value of the CSS variable.\n\tconst value = styles.getPropertyValue( variableName ).trim();\n\t\n\t// If the value is empty, the variable is not defined or is set to an empty value.\n\treturn value !== '';\n}\n\n/**\n * Gets the background color from a style object.\n *\n * @param {Object|string} color Color object or string.\n * @return {string|null} Background color.\n */\nfunction getBackgroundColor( color ) {\n\t// If color is a string, it's a var like this.\n\tif ( typeof color === 'string' ) {\n\t\tconst varName = `--wp--preset--color--${ color }`;\n\t\tif ( ! isCssVariableDefined( varName ) ) {\n\t\t\treturn null;\n\t\t}\n\t\treturn `var(${ varName })`;\n\t}\n\n\treturn color?.color?.background || null;\n}\n\n/**\n * Gets the link color from a style object.\n *\n * @param {string} text Text color.\n * @return {string|null} Link color.\n */\nfunction getLinkColor( text ) {\n\tif ( typeof text !== 'string' ) {\n\t\treturn null;\n\t}\n\t// If it starts with a hash, leave it be.\n\tif ( text.match( /^#/ ) ) {\n\t\t// We don't handle the alpha channel if present.\n\t\treturn text.substring( 0, 7 );\n\t}\n\t// var:preset|color|luminous-vivid-amber\n\t// var(--wp--preset--color--luminous-vivid-amber)\n\t// We will receive the top format, we need to output the bottom format.\n\tconst [ , , color ] = text.split( '|' );\n\tconst varName = `--wp--preset--color--${ color }`;\n\t\n\t// Check if the CSS variable is defined before using it.\n\tif ( ! isCssVariableDefined( varName ) ) {\n\t\treturn null;\n\t}\n\t\n\treturn `var(${ varName })`;\n}\n\n/**\n * Generates a CSS selector.\n *\n * @param {string} selector CSS selector.\n * @param {string} prop CSS property.\n * @param {string|null} value CSS value.\n * @param {string} pseudo Pseudo-selector.\n * @return {string} CSS selector.\n */\nfunction generateSelector( selector, prop, value = null, pseudo = '' ) {\n\tif ( ! value ) {\n\t\treturn '';\n\t}\n\treturn `${ selector }${ pseudo } { ${ prop }: ${ value }; }\\n`;\n}\n\n/**\n * Gets styles for a button.\n *\n * @param {string} selector CSS selector.\n * @param {string} button Button color.\n * @param {string} text Text color.\n * @param {string} hover Hover color.\n * @return {string} CSS styles.\n */\nfunction getStyles( selector, button, text, hover ) {\n\treturn (\n\t\tgenerateSelector( selector, 'background-color', button ) +\n\t\tgenerateSelector( selector, 'color', text ) +\n\t\tgenerateSelector( selector, 'background-color', hover, ':hover' ) +\n\t\tgenerateSelector( selector, 'background-color', hover, ':focus' )\n\t);\n}\n\n/**\n * Gets block styles.\n *\n * @param {string} base Base selector.\n * @param {Object} style Style object.\n * @param {Object|string} backgroundColor Background color.\n * @return {string} CSS styles.\n */\nexport function getBlockStyles( base, style, backgroundColor ) {\n\tconst selector = `${ base } .wp-block-button__link`;\n\n\t// We grab the background color if set as a good color for our button text.\n\tconst buttonTextColor =\n\t\tgetBackgroundColor( backgroundColor ) ||\n\t\t// Background might be in this form.\n\t\tstyle?.color?.background;\n\n\t// We misuse the link color for the button background.\n\tconst buttonColor = getLinkColor( style?.elements?.link?.color?.text );\n\tconst buttonHoverColor = getLinkColor( style?.elements?.link?.[ ':hover' ]?.color?.text );\n\n\treturn getStyles( selector, buttonColor, buttonTextColor, buttonHoverColor );\n}\n\n/**\n * Gets popup styles.\n *\n * @param {Object} style Style object.\n * @return {string} CSS styles.\n */\nexport function getPopupStyles( style ) {\n\t// We don't accept backgroundColor because the popup is always white (right?).\n\tconst buttonColor = getLinkColor( style?.elements?.link?.color?.text ) || '#111';\n\tconst buttonTextColor = '#fff';\n\tconst buttonHoverColor = getLinkColor( style?.elements?.link?.[ ':hover' ]?.color?.text ) || '#333';\n\tconst selector = '.activitypub-dialog__button-group .wp-block-button';\n\n\treturn getStyles( selector, buttonColor, buttonTextColor, buttonHoverColor );\n}\n","// extracted by mini-css-extract-plugin\nexport {};","import { store, getContext } from '@wordpress/interactivity';\nimport { getBlockStyles, getPopupStyles } from './button-style';\nimport './style.scss';\n\n/** @var {object} wp WordPress global. */\nconst { apiFetch } = window.wp;\n\n/**\n * Traps focus within the specified element.\n *\n * @param {Element} element The element to trap focus within.\n */\nfunction trapFocus( element ) {\n\tvar focusableElements = element.querySelectorAll(\n\t\t'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type=\"text\"]:not([disabled]), input[type=\"radio\"]:not([disabled]), input[type=\"checkbox\"]:not([disabled]), select:not([disabled])'\n\t);\n\tvar firstFocusableElement = focusableElements[ 0 ];\n\tvar lastFocusableElement = focusableElements[ focusableElements.length - 1 ];\n\tfirstFocusableElement.focus();\n\n\telement.addEventListener( 'keydown', function ( event ) {\n\t\tif ( event.key !== 'Tab' && event.keyCode !== 9 /* KEYCODE_TAB */ ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( event.shiftKey ) {\n\t\t\t/* shift + tab */\n\t\t\tif ( document.activeElement === firstFocusableElement ) {\n\t\t\t\tlastFocusableElement.focus();\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\t\t} /* tab */ else {\n\t\t\tif ( document.activeElement === lastFocusableElement ) {\n\t\t\t\tfirstFocusableElement.focus();\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\t\t}\n\t} );\n}\n\nconst { state, actions } = store( 'activitypub/follow-me', {\n\tactions: {\n\t\t/**\n\t\t * Open the modal.\n\t\t */\n\t\topenModal() {\n\t\t\tconst context = getContext();\n\t\t\tcontext.isModalOpen = true;\n\t\t\tdocument.body.classList.add( 'modal-open' );\n\n\t\t\t// Set up the focus trap after modal is open.\n\t\t\tsetTimeout( () => {\n\t\t\t\t// Use the blockId to find the specific modal frame for this block.\n\t\t\t\tconst blockWrapper = document.getElementById( context.blockId );\n\t\t\t\tif ( blockWrapper ) {\n\t\t\t\t\tconst modalFrame = blockWrapper.querySelector( '.activitypub-modal__frame' );\n\t\t\t\t\tif ( modalFrame ) {\n\t\t\t\t\t\ttrapFocus( modalFrame );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, 50 );\n\t\t},\n\n\t\t/**\n\t\t * Close the modal.\n\t\t */\n\t\tcloseModal() {\n\t\t\tconst context = getContext();\n\t\t\tcontext.isModalOpen = false;\n\t\t\tdocument.body.classList.remove( 'modal-open' );\n\n\t\t\t// Return focus to the button that opened the modal.\n\t\t\tconst blockWrapper = document.getElementById( context.blockId );\n\t\t\tif ( blockWrapper ) {\n\t\t\t\tconst openButton = blockWrapper.querySelector( '.wp-block-button__link' );\n\t\t\t\tif ( openButton ) {\n\t\t\t\t\topenButton.focus();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\ttoggleModal() {\n\t\t\tconst { isModalOpen } = getContext();\n\n\t\t\tisModalOpen ? actions.closeModal() : actions.openModal();\n\t\t},\n\n\t\t/**\n\t\t * Copy the webfinger to clipboard.\n\t\t */\n\t\tcopyToClipboard() {\n\t\t\tconst context = getContext();\n\n\t\t\t// Use the Clipboard API to copy text.\n\t\t\tnavigator.clipboard.writeText( context.webfinger ).then(\n\t\t\t\t() => {\n\t\t\t\t\t// Update button text to show success.\n\t\t\t\t\tcontext.copyButtonText = state.i18n.copied;\n\n\t\t\t\t\t// Reset button text after 1 second.\n\t\t\t\t\tsetTimeout( () => {\n\t\t\t\t\t\tcontext.copyButtonText = state.i18n.copy;\n\t\t\t\t\t}, 1000 );\n\t\t\t\t},\n\t\t\t\t( error ) => {\n\t\t\t\t\t// Log error if copying fails.\n\t\t\t\t\tconsole.error( 'Could not copy text: ', error );\n\t\t\t\t}\n\t\t\t);\n\t\t},\n\n\t\t/**\n\t\t * Update the remote profile value.\n\t\t *\n\t\t * @param {Event} event Input event.\n\t\t */\n\t\tupdateRemoteProfile( event ) {\n\t\t\tconst context = getContext();\n\t\t\tcontext.remoteProfile = event.target.value;\n\t\t\t// Reset error state when input changes.\n\t\t\tcontext.isError = false;\n\t\t\tcontext.errorMessage = '';\n\t\t},\n\n\t\t/**\n\t\t * Handle keydown event for remote profile input.\n\t\t *\n\t\t * @param {Event} event Keydown event.\n\t\t */\n\t\thandleKeyDown( event ) {\n\t\t\tif ( event.key === 'Enter' ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tactions.submitRemoteProfile();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Submit the remote profile.\n\t\t */\n\t\tsubmitRemoteProfile: function* () {\n\t\t\tconst context = getContext();\n\t\t\tconst { namespace } = state;\n\t\t\tconst input = context.remoteProfile.trim();\n\n\t\t\t// Validate input.\n\t\t\tif ( ! input ) {\n\t\t\t\tcontext.isError = true;\n\t\t\t\tcontext.errorMessage = state.i18n.emptyProfileError;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( ! /^(https?:\\/\\/|@)/.test( input ) ) {\n\t\t\t\tcontext.isError = true;\n\t\t\t\tcontext.errorMessage = state.i18n.invalidProfileError;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set loading state.\n\t\t\tcontext.isLoading = true;\n\t\t\tcontext.isError = false;\n\n\t\t\t// Construct the API path.\n\t\t\tconst path = `/${ namespace }/actors/${ context.userId }/remote-follow?resource=${ encodeURIComponent(\n\t\t\t\tinput\n\t\t\t) }`;\n\n\t\t\ttry {\n\t\t\t\t// Make the API request.\n\t\t\t\tconst response = yield apiFetch( { path } );\n\n\t\t\t\t// Set opening state.\n\t\t\t\tcontext.isLoading = false;\n\n\t\t\t\t// Open the remote follow URL in a new tab.\n\t\t\t\twindow.open( response.url, '_blank' );\n\n\t\t\t\t// Close the modal after opening the URL.\n\t\t\t\tactions.closeModal();\n\t\t\t} catch ( error ) {\n\t\t\t\t// Handle error.\n\t\t\t\tconsole.error( 'Error submitting profile:', error );\n\t\t\t\tcontext.isLoading = false;\n\t\t\t\tcontext.isError = true;\n\t\t\t\tcontext.errorMessage = error.message || state.i18n.genericError;\n\t\t\t}\n\t\t},\n\t},\n\tcallbacks: {\n\t\t/**\n\t\t * Initialize button styles.\n\t\t */\n\t\tinitButtonStyles: () => {\n\t\t\tconst { buttonStyle, backgroundColor, blockId } = getContext();\n\n\t\t\t// Add dynamic button styles to the document.\n\t\t\tif ( blockId && buttonStyle ) {\n\t\t\t\tconst styleElement = document.createElement( 'style' );\n\t\t\t\tconst selector = `#${ blockId }`;\n\n\t\t\t\t// Use getBlockStyles from button-style.js to get the CSS string.\n\t\t\t\tstyleElement.textContent = getBlockStyles( selector, buttonStyle, backgroundColor );\n\n\t\t\t\tdocument.head.appendChild( styleElement );\n\n\t\t\t\t// Add popup styles.\n\t\t\t\tconst popupStyleElement = document.createElement( 'style' );\n\t\t\t\tpopupStyleElement.textContent = getPopupStyles( buttonStyle );\n\t\t\t\tdocument.head.appendChild( popupStyleElement );\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Close modal when pressing ESC key.\n\t\t *\n\t\t * @param {Event} event Keyboard event.\n\t\t */\n\t\tdocumentKeydown( event ) {\n\t\t\tconst { isModalOpen } = getContext();\n\n\t\t\tif ( isModalOpen && event.key === 'Escape' ) {\n\t\t\t\tactions.closeModal();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Close modal when clicking outside.\n\t\t *\n\t\t * @param {Event} event Click event.\n\t\t */\n\t\tdocumentClick( event ) {\n\t\t\tconst { blockId, isModalOpen } = getContext();\n\t\t\tif ( ! isModalOpen ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Get the block wrapper element.\n\t\t\tconst blockWrapper = document.getElementById( blockId );\n\t\t\tif ( ! blockWrapper ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If the click was on the button or its children, we should not close the modal.\n\t\t\tconst toggleButton = blockWrapper.querySelector(\n\t\t\t\t'.wp-element-button[data-wp-on--click=\"actions.toggleModal\"]'\n\t\t\t);\n\t\t\tif ( toggleButton && ( toggleButton === event.target || toggleButton.contains( event.target ) ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check if the click was inside the modal frame.\n\t\t\tconst modalFrame = blockWrapper.querySelector( '.activitypub-modal__frame' );\n\t\t\tif ( ! modalFrame || modalFrame.contains( event.target ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tactions.closeModal();\n\t\t},\n\t},\n} );\n","module.exports = __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","var deferred = [];\n__webpack_require__.O = (result, chunkIds, fn, priority) => {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar [chunkIds, fn, priority] = deferred[i];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t\"follow-me/view\": 0,\n\t\"follow-me/style-view\": 0\n};\n\n// no install chunk\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no external install chunk\n\n__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);","","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [\"follow-me/style-view\"], () => (__webpack_require__(\"./src/follow-me/view.js\")))\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n",""],"names":["isCssVariableDefined","variableName","window","getComputedStyle","styles","document","documentElement","value","getPropertyValue","trim","getBackgroundColor","color","varName","background","getLinkColor","text","match","substring","split","generateSelector","selector","prop","pseudo","getStyles","button","hover","getBlockStyles","base","style","backgroundColor","buttonTextColor","buttonColor","elements","link","buttonHoverColor","getPopupStyles","store","getContext","apiFetch","wp","trapFocus","element","focusableElements","querySelectorAll","firstFocusableElement","lastFocusableElement","length","focus","addEventListener","event","key","keyCode","shiftKey","activeElement","preventDefault","state","actions","openModal","context","isModalOpen","body","classList","add","setTimeout","blockWrapper","getElementById","blockId","modalFrame","querySelector","closeModal","remove","openButton","toggleModal","copyToClipboard","navigator","clipboard","writeText","webfinger","then","copyButtonText","i18n","copied","copy","error","console","updateRemoteProfile","remoteProfile","target","isError","errorMessage","handleKeyDown","submitRemoteProfile","namespace","input","emptyProfileError","test","invalidProfileError","isLoading","path","userId","encodeURIComponent","response","open","url","message","genericError","callbacks","initButtonStyles","buttonStyle","styleElement","createElement","textContent","head","appendChild","popupStyleElement","documentKeydown","documentClick","toggleButton","contains"],"sourceRoot":""}
path: build/follow-me/view.js.map
user: login: obenland
body: I think they're just left-overs from early dev mode. Good eye!
Removed in [c6b54f2](https://github.com/Automattic/wordpress-activitypub/pull/1691/commits/c6b54f247e171b4b2087b6bf04f9fdb7bb3fc980).
created_at: 2025-05-19T18:50:57Z
author_association: MEMBER
diff_hunk: @@ -224,7 +221,7 @@ public static function render_post_reactions_block( $attrs, $content ) {
 	 * @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'.
 	 * @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
path: includes/class-blocks.php
user: login: Copilot
body: [nitpick] get_user_id visibility changed from private to public; consider updating the docblock or adding an @access tag to reflect its new public status.
```suggestion
	 * @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
	 * @access public
```
created_at: 2025-05-19T20:27:50Z
author_association: NONE
diff_hunk: @@ -213,6 +213,81 @@ body.modal-open {
 	overflow: hidden;
 }
 
+/* Dark mode styles. */
+@media (prefers-color-scheme: dark) {
path: src/follow-me/style.scss
user: login: pfefferle
body: I think the current code simply switches the global foreground color with the background and vice verse. This way we would not have to be so specific?
created_at: 2025-05-20T08:04:40Z
author_association: MEMBER

PR comments:
user: login: obenland
created_at: 2025-05-15T21:23:09Z
body: @pfefferle This should be ready for testing
author_association: MEMBER
user: login: pfefferle
created_at: 2025-05-19T15:47:26Z
body: Dark mode is not properly supported by the new overlay:

<img width="1021" alt="Screenshot 2025-05-19 at 17 46 03" src="https://github.com/user-attachments/assets/77886367-40b9-49f9-bec9-915983158826" />

You can test it for example with: https://github.com/pfefferle/autonomie
author_association: MEMBER
user: login: pfefferle
created_at: 2025-05-19T15:49:28Z
body: The button design will not be re-used in classic themes:

<img width="831" alt="Screenshot 2025-05-19 at 17 49 15" src="https://github.com/user-attachments/assets/bef3168c-d282-4a8c-8703-88c194de7e2d" />

author_association: MEMBER
user: login: pfefferle
created_at: 2025-05-19T16:09:10Z
body: If you migrate from old "only button mode" to new button mode, it falls back to the full block
author_association: MEMBER
user: login: obenland
created_at: 2025-05-19T18:45:18Z
body: > Dark mode is not properly supported by the new overlay:

I added styles for dark mode in [15c0875](https://github.com/Automattic/wordpress-activitypub/pull/1691/commits/15c0875967d75d11820b0f6d73b37dee4fce4598). FWIW, it looks quite out of place and unexpected in any theme that doesn't have separate dark mode styles, which I assume is the vast majority. Not sure what we want to do there. Would you mind testing it out and see what you think?
author_association: MEMBER
user: login: obenland
created_at: 2025-05-19T20:24:19Z
body: > If you migrate from old "only button mode" to new button mode, it falls back to the full block

Fixed in dfee0d2b2c9ba06e2eb7f6672156889d48274f81.
author_association: MEMBER
user: login: obenland
created_at: 2025-05-19T20:25:21Z
body: @pfefferle Thanks for giving this a look, I think all your feedback should be addressed. Could you give it another run, when you have a chance?
author_association: MEMBER
user: login: pfefferle
created_at: 2025-05-20T08:00:53Z
body: The Button on the archive page looks different to the button on the single view (Twenty Twenty-Five)

| Archive | Single |
| --- | --- |
| <img width="1148" alt="Screenshot 2025-05-20 at 09 56 02" src="https://github.com/user-attachments/assets/7b62fa5f-4009-41a8-84fa-3d0f3711f8af" /> | <img width="1123" alt="Screenshot 2025-05-20 at 09 56 07" src="https://github.com/user-attachments/assets/83dc79a8-28a8-4986-be7c-cdf37dccbb69" /> |


author_association: MEMBER
user: login: pfefferle
created_at: 2025-05-20T08:02:40Z
body: Buttons still look weird in FSE themes (Twenty Twenty-Five and Twenty Twenty-Four):

<img width="739" alt="Screenshot 2025-05-20 at 09 56 26" src="https://github.com/user-attachments/assets/5e0e3003-a2fa-4a39-94e2-f1f84ad544b8" />

author_association: MEMBER
user: login: pfefferle
created_at: 2025-05-20T08:03:22Z
body: Dark-Mode on FSE themes does not properly switch colors (see the headlines):

<img width="737" alt="Screenshot 2025-05-20 at 10 01 46" src="https://github.com/user-attachments/assets/afe5529f-936a-4c90-a155-fc94cb41c433" />

author_association: MEMBER
user: login: obenland
created_at: 2025-05-20T13:22:09Z
body: > The Button on the archive page looks different to the button on the single view (Twenty Twenty-Five)

That seems to be a result of how Core enqueues inline styles for the button block. They're enqueued in Single view, but not in Archive.
author_association: MEMBER
user: login: obenland
created_at: 2025-05-20T18:44:21Z
body: @pfefferle If you agree, I'd love to move on with this one. I think the styles are in a pretty good place now, thank you for all your feedback on that. Any additional nitpicks we have we could address in follow-up PRs?
author_association: MEMBER
user: login: jeherve
created_at: 2025-05-30T12:12:32Z
body: @obenland Now that this is merged, do you think we could close #426? I haven't tested the block on `trunk` yet.
author_association: MEMBER
user: login: obenland
created_at: 2025-05-30T13:20:32Z
body: It's getting close, yeah! It's fixed for the Follow Me block and I think #1747 removes the React dependencies for the final block. Thanks for the nudge!
author_association: MEMBER