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 %1$s block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,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 @@
+ 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 = '';
+}
+$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',
+ )
+);
+
+?>
+
+
+>
+
+
+
['url'] ); ?>)
+
+
get_name() ); ?>
+
get_webfinger() ); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '4e430690e282cfe013db');
+ 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 @yourusername@example.com
)","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=n)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(l=!1,n0&&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);uo(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=r)&&Object.keys(l.O).every((e=>l.O[e](o[s])))?o.splice(s--,1):(c=!1,r0&&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 @@
- 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');
+ 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\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t${e}\n\t\t\t\n\t\t\t\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=a)&&Object.keys(n.O).every((e=>n.O[e](r[c])))?r.splice(c--,1):(l=!1,a0&&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);sn(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\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t${e}\n\t\t\t\n\t\t\t\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=a)&&Object.keys(n.O).every((e=>n.O[e](r[c])))?r.splice(c--,1):(l=!1,a0&&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);sn(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 '';
- } else {
- // If the user is a specific ID and we couldn't find it, render an error message.
- return '';
- }
- }
-
- $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 '';
- }
-
/**
* 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 (
-
- );
-}
\ 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 (
+
+ );
+ }
+
+ return (
+
+

+
+
{ name }
+
+ { webfinger }
+
+
+
+
+ );
+}
/**
* 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' ) }
/>
- setAttributes( { buttonText: value } ) }
- />
- setAttributes( { buttonSize: value } ) }
- help={ __( 'Choose the size of the follow button', 'activitypub' ) }
- />
- { isInheritMode ?
- authorId ? (
-
- ) : (
-
- )
- : (
-
- ) }
+
+ { isInheritMode && ! authorId ? (
+
+ ) : (
+
+ ) }
);
}
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 (
-
-
-
- );
- }
-
- return (
-
-

-
-
{ name }
-
{ webfingerWithAt }
-
-
-
- );
-}
-
-/**
- * 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 (
- <>
-
- { isOpen && (
- setIsOpen( false ) }
- title={ title }
- aria-label={ title }
- role="dialog"
- >
-
-
-
- ) }
- >
- );
-}
-
-/**
- * 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 (
-
- );
-}
-
-/**
- * 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 (
-
- );
-}
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 @@
+ 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 = '';
+}
+$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',
+ )
+);
+
+?>
+
+
+>
+
+
+
['url'] ); ?>)
+
+
get_name() ); ?>
+
get_webfinger() ); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+;
+}
+
+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( );
- } );
-} );
\ 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();
+ }
+ }
+ } );
+ },
+} );