Followers Block (#344)

Introduces a new Followers block. Proudly display your Fediverse followers to the world!

---------

Co-authored-by: Matthias Pfefferle <pfefferle@users.noreply.github.com>
This commit is contained in:
Matt Wiebe 2023-07-26 15:05:41 -05:00 committed by GitHub
parent 608d50e475
commit 5b9dadd6fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 948 additions and 26 deletions

View file

@ -36,3 +36,4 @@ phpunit.xml.dist
tests tests
node_modules node_modules
vendor vendor
src

View file

@ -32,6 +32,7 @@ function init() {
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
Migration::init(); Migration::init();
Activitypub::init(); Activitypub::init();
@ -50,6 +51,7 @@ function init() {
Admin::init(); Admin::init();
Hashtag::init(); Hashtag::init();
Shortcodes::init(); Shortcodes::init();
Blocks::init();
Mention::init(); Mention::init();
Health_Check::init(); Health_Check::init();
Scheduler::init(); Scheduler::init();

BIN
assets/img/mp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,57 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"name": "activitypub/followers",
"apiVersion": 3,
"version": "1.0.0",
"title": "Fediverse Followers",
"category": "widgets",
"description": "Display your followers from the Fediverse on your website.",
"textdomain": "activitypub",
"icon": "groups",
"supports": {
"html": false
},
"attributes": {
"title": {
"type": "string",
"default": "Fediverse Followers"
},
"selectedUser": {
"type": "string",
"default": "site"
},
"per_page": {
"type": "number",
"default": 10
},
"order": {
"type": "string",
"default": "desc",
"enum": [
"asc",
"desc"
]
}
},
"styles": [
{
"name": "default",
"label": "No Lines",
"isDefault": true
},
{
"name": "with-lines",
"label": "Lines"
},
{
"name": "compact",
"label": "Compact"
}
],
"editorScript": "file:./index.js",
"viewScript": "file:./view.js",
"style": [
"file:./style-view.css",
"wp-block-query-pagination"
]
}

View file

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '2879986bf189fb73965e');

3
build/followers/index.js Normal file
View file

@ -0,0 +1,3 @@
(()=>{var e={184:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var l=typeof a;if("string"===l||"number"===l)e.push(a);else if(Array.isArray(a)){if(a.length){var o=n.apply(null,a);o&&e.push(o)}}else if("object"===l){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)r.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},t={};function a(r){var n=t[r];if(void 0!==n)return n.exports;var l=t[r]={exports:{}};return e[r](l,l.exports,a),l.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.element,r=window.wp.primitives,n=(0,t.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,t.createElement)(r.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"}));function l(){return l=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var r in a)Object.prototype.hasOwnProperty.call(a,r)&&(e[r]=a[r])}return e},l.apply(this,arguments)}const o=window.wp.components,i=window.wp.data,c=window.wp.blockEditor,s=window.wp.i18n,p=window.React,u=window.wp.apiFetch;var m=a.n(u);const v=window.wp.url;var b=a(184),w=a.n(b);function d(e){let{active:a,children:r,page:n,pageClick:l,className:o}=e;const i=w()("wp-block activitypub-pager",o,{current:a});return(0,t.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!a&&l(n)}},r)}const g={outlined:"outlined",minimal:"minimal"};function y(e){let{compact:a,nextLabel:r,page:n,pageClick:l,perPage:o,prevLabel:i,total:c,variant:s=g.outlined}=e;const p=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(n,Math.ceil(c/o)),u=w()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":a});return(0,t.createElement)("nav",{className:u},i&&(0,t.createElement)(d,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!a&&(0,t.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,t.createElement)(d,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),r&&(0,t.createElement)(d,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/o),"aria-label":r,className:"wp-block-query-pagination-next block-editor-block-list__block"},r))}const{namespace:f}=window._activityPubOptions;function h(e){let{selectedUser:a,per_page:r,order:n,title:l,page:o,setPage:i,className:c=""}=e;const u="site"===a?0:a,[b,w]=(0,p.useState)([]),[d,g]=(0,p.useState)(0),[h,E]=(0,p.useState)(0),[_,x]=function(){const[e,t]=(0,p.useState)(1);return[e,t]}(),S=o||_,C=i||x,N=(0,t.createInterpolateElement)(/* translators: arrow for previous followers link */
(0,s.__)("<span>←</span> Less","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),O=(0,t.createInterpolateElement)(/* translators: arrow for next followers link */
(0,s.__)("More <span>→</span>","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})});return(0,p.useEffect)((()=>{const e=function(e,t,a,r){const n=`/${f}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,v.addQueryArgs)(n,l)}(u,r,n,S);m()({path:e}).then((e=>{g(Math.ceil(e.totalItems/r)),E(e.totalItems),w(e.orderedItems)})).catch((e=>console.error(e)))}),[u,r,n,S]),(0,t.createElement)("div",{className:"activitypub-follower-block "+c},(0,t.createElement)("h3",null,l),(0,t.createElement)("ul",null,b&&b.map((e=>(0,t.createElement)("li",{key:e.url},(0,t.createElement)(k,e))))),d>1&&(0,t.createElement)(y,{page:S,perPage:r,total:h,pageClick:C,nextLabel:O,prevLabel:N,compact:"is-style-compact"===c}))}function k(e){let{name:a,icon:r,url:n,preferredUsername:l}=e;const i=`@${l}`;return(0,t.createElement)(o.ExternalLink,{className:"activitypub-link",href:n,title:i,onClick:e=>e.preventDefault()},(0,t.createElement)("img",{width:"40",height:"40",src:r.url,class:"avatar activitypub-avatar"}),(0,t.createElement)("span",{class:"activitypub-actor"},(0,t.createElement)("strong",{className:"activitypub-name"},a),(0,t.createElement)("span",{class:"sep"},"/"),(0,t.createElement)("span",{class:"activitypub-handle"},i)))}(0,e.registerBlockType)("activitypub/followers",{edit:function(e){let{attributes:a,setAttributes:r}=e;const{order:n,per_page:p,selectedUser:u,className:m}=a,v=(0,c.useBlockProps)(),[b,w]=(0,t.useState)(1),d=[{label:(0,s.__)("New to old","activitypub"),value:"desc"},{label:(0,s.__)("Old to new","activitypub"),value:"asc"}],g=(0,i.useSelect)((e=>e("core").getUsers({who:"authors"}))),y=(0,t.useMemo)((()=>{if(!g)return[];const e=[{label:(0,s.__)("Whole Site","activitypub"),value:"site"}];return g.reduce(((e,t)=>(e.push({label:t.name,value:t.id}),e)),e)}),[g]),f=e=>t=>{w(1),r({[e]:t})};return(0,t.createElement)("div",v,(0,t.createElement)(c.InspectorControls,{key:"setting"},(0,t.createElement)(o.PanelBody,{title:(0,s.__)("Followers Options","activitypub")},(0,t.createElement)(o.SelectControl,{label:(0,s.__)("Select User","activitypub"),value:u,options:y,onChange:f("selectedUser")}),(0,t.createElement)(o.SelectControl,{label:(0,s.__)("Sort","activitypub"),value:n,options:d,onChange:f("order")}),(0,t.createElement)(o.RangeControl,{label:(0,s.__)("Number of Followers","activitypub"),value:p,onChange:f("per_page"),min:1,max:10}))),(0,t.createElement)(h,l({},a,{page:b,setPage:w})))},save:()=>null,icon:n})})()})();

View file

@ -0,0 +1 @@
.activitypub-follower-block.is-style-compact .activitypub-handle,.activitypub-follower-block.is-style-compact .sep{display:none}.activitypub-follower-block.is-style-with-lines ul li{border-bottom:.5px solid;margin-bottom:.5rem;padding-bottom:.5rem}.activitypub-follower-block.is-style-with-lines ul li:last-child{border-bottom:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle,.activitypub-follower-block.is-style-with-lines .activitypub-name{text-decoration:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle:hover,.activitypub-follower-block.is-style-with-lines .activitypub-name:hover{text-decoration:underline}.activitypub-follower-block ul{margin:0!important;padding:0!important}.activitypub-follower-block li{display:flex;margin-bottom:1rem}.activitypub-follower-block img{border-radius:50%;height:40px;margin-right:var(--wp--preset--spacing--20,.5rem);width:40px}.activitypub-follower-block .activitypub-link{align-items:center;color:inherit!important;display:flex;flex-flow:row nowrap;max-width:100%;text-decoration:none!important}.activitypub-follower-block .activitypub-handle,.activitypub-follower-block .activitypub-name{text-decoration:underline;text-decoration-thickness:.8px;text-underline-position:under}.activitypub-follower-block .activitypub-handle:hover,.activitypub-follower-block .activitypub-name:hover{text-decoration:none}.activitypub-follower-block .activitypub-name{font-size:var(--wp--preset--font-size--normal,16px)}.activitypub-follower-block .activitypub-actor{font-size:var(--wp--preset--font-size--small,13px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follower-block .sep{padding:0 .2rem}.activitypub-follower-block .wp-block-query-pagination{margin-top:1.5rem}.activitypub-follower-block .activitypub-pager{cursor:default}.activitypub-follower-block .activitypub-pager.current{opacity:.33}.activitypub-follower-block .page-numbers{padding:0 .2rem}.activitypub-follower-block .page-numbers.current{font-weight:700;opacity:1}

View file

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'aa55e14b87c4b4e1c1a3');

3
build/followers/view.js Normal file
View file

@ -0,0 +1,3 @@
(()=>{var e,t={142:(e,t,a)=>{"use strict";const r=window.wp.element,n=window.React,l=window.wp.apiFetch;var i=a.n(l);const o=window.wp.url,c=window.wp.i18n;var s=a(184),p=a.n(s);function u(e){let{active:t,children:a,page:n,pageClick:l,className:i}=e;const o=p()("wp-block activitypub-pager",i,{current:t});return(0,r.createElement)("a",{className:o,onClick:e=>{e.preventDefault(),!t&&l(n)}},a)}const m={outlined:"outlined",minimal:"minimal"};function v(e){let{compact:t,nextLabel:a,page:n,pageClick:l,perPage:i,prevLabel:o,total:c,variant:s=m.outlined}=e;const v=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(n,Math.ceil(c/i)),b=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":t});return(0,r.createElement)("nav",{className:b},o&&(0,r.createElement)(u,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!t&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},v.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),a&&(0,r.createElement)(u,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/i),"aria-label":a,className:"wp-block-query-pagination-next block-editor-block-list__block"},a))}const b=window.wp.components,{namespace:d}=window._activityPubOptions;function f(e){let{selectedUser:t,per_page:a,order:l,title:s,page:p,setPage:u,className:m=""}=e;const b="site"===t?0:t,[f,w]=(0,n.useState)([]),[y,k]=(0,n.useState)(0),[h,E]=(0,n.useState)(0),[x,_]=function(){const[e,t]=(0,n.useState)(1);return[e,t]}(),O=p||x,N=u||_,S=(0,r.createInterpolateElement)(/* translators: arrow for previous followers link */
(0,c.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),C=(0,r.createInterpolateElement)(/* translators: arrow for next followers link */
(0,c.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})});return(0,n.useEffect)((()=>{const e=function(e,t,a,r){const n=`/${d}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(n,l)}(b,a,l,O);i()({path:e}).then((e=>{k(Math.ceil(e.totalItems/a)),E(e.totalItems),w(e.orderedItems)})).catch((e=>console.error(e)))}),[b,a,l,O]),(0,r.createElement)("div",{className:"activitypub-follower-block "+m},(0,r.createElement)("h3",null,s),(0,r.createElement)("ul",null,f&&f.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(g,e))))),y>1&&(0,r.createElement)(v,{page:O,perPage:a,total:h,pageClick:N,nextLabel:C,prevLabel:S,compact:"is-style-compact"===m}))}function g(e){let{name:t,icon:a,url:n,preferredUsername:l}=e;const i=`@${l}`;return(0,r.createElement)(b.ExternalLink,{className:"activitypub-link",href:n,title:i,onClick:e=>e.preventDefault()},(0,r.createElement)("img",{width:"40",height:"40",src:a.url,class:"avatar activitypub-avatar"}),(0,r.createElement)("span",{class:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},t),(0,r.createElement)("span",{class:"sep"},"/"),(0,r.createElement)("span",{class:"activitypub-handle"},i)))}const w=window.wp.domReady;a.n(w)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,r.render)((0,r.createElement)(f,t),e)}))}))},184:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var l=typeof a;if("string"===l||"number"===l)e.push(a);else if(Array.isArray(a)){if(a.length){var i=n.apply(null,a);i&&e.push(i)}}else if("object"===l){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var o in a)r.call(a,o)&&a[o]&&e.push(o)}}}return e.join(" ")}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var i=1/0;for(p=0;p<e.length;p++){for(var[a,n,l]=e[p],o=!0,c=0;c<a.length;c++)(!1&l||i>=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(o=!1,l<i&&(i=l));if(o){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={638:0,962:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[i,o,c]=a,s=0;if(i.some((t=>0!==e[t]))){for(n in o)r.o(o,n)&&(r.m[n]=o[n]);if(c)var p=c(r)}for(t&&t(a);s<i.length;s++)l=i[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[962],(()=>r(142)));n=r.O(n)})();

View file

@ -8,6 +8,7 @@
namespace Activitypub\Activity; namespace Activitypub\Activity;
use WP_Error; use WP_Error;
use ReflectionClass;
use function Activitypub\camel_to_snake_case; use function Activitypub\camel_to_snake_case;
use function Activitypub\snake_to_camel_case; use function Activitypub\snake_to_camel_case;
@ -448,9 +449,10 @@ class Base_Object {
$var = \strtolower( \substr( $method, 4 ) ); $var = \strtolower( \substr( $method, 4 ) );
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
if ( $this->has( $var ) ) { if ( ! $this->has( $var ) ) {
return $this->get( $var ); return new WP_Error( 'invalid_key', 'Invalid key' );
} }
return $this->$var; return $this->$var;
} }
@ -493,7 +495,7 @@ class Base_Object {
return new WP_Error( 'invalid_key', 'Invalid key' ); return new WP_Error( 'invalid_key', 'Invalid key' );
} }
return $this->$key; return call_user_func( array( $this, 'get_' . $key ) );
} }
/** /**
@ -644,7 +646,7 @@ class Base_Object {
$array = array_merge( array( '@context' => $context ), $array ); $array = array_merge( array( '@context' => $context ), $array );
} }
$class = new \ReflectionClass( $this ); $class = new ReflectionClass( $this );
$class = strtolower( $class->getShortName() ); $class = strtolower( $class->getShortName() );
$array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this ); $array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this );

90
includes/class-blocks.php Normal file
View file

@ -0,0 +1,90 @@
<?php
namespace Activitypub;
use Activitypub\Collection\Followers;
class Blocks {
public static function init() {
\add_action( 'init', array( self::class, 'register_blocks' ) );
\add_action( 'wp_enqueue_scripts', array( self::class, 'add_data' ) );
\add_action( 'enqueue_block_editor_assets', array( self::class, 'add_data' ) );
}
public static function add_data() {
$handle = is_admin() ? 'activitypub-followers-editor-script' : 'activitypub-followers-view-script';
$data = array(
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
);
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
\wp_add_inline_script( $handle, $js, 'before' );
}
public static function register_blocks() {
\register_block_type_from_metadata(
ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
array(
'render_callback' => array( self::class, 'render_follower_block' ),
)
);
}
private static function get_user_id( $user_string ) {
if ( is_numeric( $user_string ) ) {
return absint( $user_string );
}
// any other non-numeric falls back to 0, including the `site` string used in the UI
return 0;
}
public static function render_follower_block( $attrs, $content, $block ) {
$followee_user_id = self::get_user_id( $attrs['selectedUser'] );
$per_page = absint( $attrs['per_page'] );
$followers = Followers::get_followers( $followee_user_id, $per_page );
$title = $attrs['title'];
$wrapper_attributes = get_block_wrapper_attributes(
array(
'aria-label' => __( 'Fediverse Followers', 'activitypub' ),
'class' => 'activitypub-follower-block',
'data-attrs' => wp_json_encode( $attrs ),
)
);
$html = '<div class="activitypub-follower-block" ' . $wrapper_attributes . '>';
if ( $title ) {
$html .= '<h3>' . $title . '</h3>';
}
$html .= '<ul>';
foreach ( $followers as $follower ) {
$html .= '<li>' . self::render_follower( $follower ) . '</li>';
}
// We are only pagination on the JS side. Could be revisited but we gotta ship!
$html .= '</ul></div>';
return $html;
}
public static function render_follower( $follower ) {
$external_svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="components-external-link__icon css-rvs7bx esh4a730" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg>';
$template =
'<a href="%s" title="%s" class="components-external-link activitypub-link" target="_blank" rel="external noreferrer noopener">
<img width="40" height="40" src="%s" class="avatar activitypub-avatar" />
<span class="activitypub-actor">
<strong class="activitypub-name">%s</strong>
<span class="sep">/</span>
<span class="activitypub-handle">@%s</span>
</span>
%s
</a>';
$data = $follower->to_array();
return sprintf(
$template,
esc_url( $data['url'] ),
esc_attr( $data['name'] ),
esc_attr( $data['icon']['url'] ),
esc_html( $data['name'] ),
esc_html( $data['preferredUsername'] ),
$external_svg
);
}
}

View file

@ -292,14 +292,30 @@ class Followers {
/** /**
* Get the Followers of a given user * Get the Followers of a given user
* *
* @param int $user_id The ID of the WordPress User * @param int $user_id The ID of the WordPress User.
* @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT * @param int $number Maximum number of results to return.
* @param int $number Limts the result * @param int $page Page number.
* @param int $offset Offset * @param array $args The WP_Query arguments.
* * @return array List of `Follower` objects.
* @return array The Term list of Followers, the format depends on $output
*/ */
public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) {
$data = self::get_followers_with_count( $user_id, $number, $page, $args );
return $data['followers'];
}
/**
* Get the Followers of a given user, along with a total count for pagination purposes.
*
* @param int $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array
* followers List of `Follower` objects.
* total Total number of followers.
*/
public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
$defaults = array( $defaults = array(
'post_type' => self::POST_TYPE, 'post_type' => self::POST_TYPE,
'posts_per_page' => $number, 'posts_per_page' => $number,
@ -314,16 +330,16 @@ class Followers {
), ),
); );
$args = wp_parse_args( $args, $defaults ); $args = wp_parse_args( $args, $defaults );
$query = new WP_Query( $args ); $query = new WP_Query( $args );
$posts = $query->get_posts(); $total = $query->found_posts;
$items = array(); $followers = array_map(
function( $post ) {
foreach ( $posts as $post ) { return Follower::init_from_cpt( $post );
$items[] = Follower::init_from_cpt( $post ); // phpcs:ignore },
} $query->get_posts()
);
return $items; return compact( 'followers', 'total' );
} }
/** /**

View file

@ -171,6 +171,55 @@ class Follower extends Actor {
return $meta_input; return $meta_input;
} }
/**
* Get the icon.
*
* Sets a fallback to better handle API and HTML outputs.
*
* @return array The icon.
*/
public function get_icon() {
if ( isset( $this->icon['url'] ) ) {
return $this->icon;
}
return array(
'type' => 'Image',
'mediaType' => 'image/jpeg',
'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
);
}
/**
* Get Name.
*
* Tries to extract a name from the URL or ID if not set.
*
* @return string The name.
*/
public function get_name() {
if ( isset( $this->name ) ) {
return $this->name;
}
return $this->extract_name_from_uri();
}
/**
* The preferred Username.
*
* Tries to extract a name from the URL or ID if not set.
*
* @return string The preferred Username.
*/
public function get_preferred_username() {
if ( isset( $this->name ) ) {
return $this->name;
}
return $this->extract_name_from_uri();
}
/** /**
* Get the Icon URL (Avatar) * Get the Icon URL (Avatar)
* *
@ -222,4 +271,47 @@ class Follower extends Actor {
return $object; return $object;
} }
/**
* Infer a shortname from the Actor ID or URL. Used only for fallbacks,
* we will try to use what's supplied.
*
* @return string Hopefully the name of the Follower.
*/
protected function extract_name_from_uri() {
// prefer the URL, but fall back to the ID.
if ( $this->url ) {
$name = $this->url;
} else {
$name = $this->id;
}
if ( \filter_var( $name, FILTER_VALIDATE_URL ) ) {
$name = \rtrim( $name, '/' );
$path = \wp_parse_url( $name, PHP_URL_PATH );
if ( $path ) {
if ( \strpos( $name, '@' ) !== false ) {
// expected: https://example.com/@user (default URL pattern)
$name = \preg_replace( '|^/@?|', '', $path );
} else {
// expected: https://example.com/users/user (default ID pattern)
$parts = \explode( '/', $path );
$name = \array_pop( $parts );
}
}
} elseif (
\is_email( $name ) ||
\strpos( $name, 'acct' ) === 0 ||
\strpos( $name, '@' ) === 0
) {
// expected: user@example.com or acct:user@example (WebFinger)
$name = \ltrim( $name, '@' );
$name = \ltrim( $name, 'acct:' );
$parts = \explode( '@', $name );
$name = $parts[0];
}
return $name;
}
} }

View file

@ -58,13 +58,17 @@ class Followers {
return $user; return $user;
} }
$page = $request->get_param( 'page', 1 ); $order = $request->get_param( 'order' );
$per_page = (int) $request->get_param( 'per_page' );
$page = (int) $request->get_param( 'page' );
$context = $request->get_param( 'context' );
/* /*
* Action triggerd prior to the ActivityPub profile being created and sent to the client * Action triggerd prior to the ActivityPub profile being created and sent to the client
*/ */
\do_action( 'activitypub_rest_followers_pre' ); \do_action( 'activitypub_rest_followers_pre' );
$data = Follower_Collection::get_followers_with_count( $user_id, $per_page, $page, array( 'order' => ucwords( $order ) ) );
$json = new stdClass(); $json = new stdClass();
$json->{'@context'} = \Activitypub\get_context(); $json->{'@context'} = \Activitypub\get_context();
@ -74,13 +78,13 @@ class Followers {
$json->actor = $user->get_id(); $json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage'; $json->type = 'OrderedCollectionPage';
$json->totalItems = Follower_Collection::count_followers( $user->get__id() ); // phpcs:ignore $json->totalItems = $data['total']; // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore
$json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore
$json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 20 ), $json->partOf ); // phpcs:ignore $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / $per_page ), $json->partOf ); // phpcs:ignore
if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) > $page ) ) { // phpcs:ignore if ( $page && ( ( \ceil ( $json->totalItems / $per_page ) ) > $page ) ) { // phpcs:ignore
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore
} }
@ -90,10 +94,13 @@ class Followers {
// phpcs:ignore // phpcs:ignore
$json->orderedItems = array_map( $json->orderedItems = array_map(
function( $item ) { function( $item ) use ( $context ) {
if ( 'full' === $context ) {
return $item->to_array();
}
return $item->get_url(); return $item->get_url();
}, },
Follower_Collection::get_followers( $user->get__id(), 20, $page ) $data['followers']
); );
$response = new WP_REST_Response( $json, 200 ); $response = new WP_REST_Response( $json, 200 );
@ -115,11 +122,28 @@ class Followers {
'default' => 1, 'default' => 1,
); );
$params['per_page'] = array(
'type' => 'integer',
'default' => 20,
);
$params['order'] = array(
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
);
$params['user_id'] = array( $params['user_id'] = array(
'required' => true, 'required' => true,
'type' => 'string', 'type' => 'string',
); );
$params['context'] = array(
'type' => 'string',
'default' => 'simple',
'enum' => array( 'simple', 'full' ),
);
return $params; return $params;
} }
} }

View file

@ -9,12 +9,23 @@
"name": "Matthias Pfefferle", "name": "Matthias Pfefferle",
"web": "https://notiz.blog" "web": "https://notiz.blog"
}, },
"scripts": {
"dev": "wp-scripts start",
"build": "wp-scripts build"
},
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/pfefferle/wordpress-activitypub/issues" "url": "https://github.com/pfefferle/wordpress-activitypub/issues"
}, },
"homepage": "https://github.com/pfefferle/wordpress-activitypub#readme", "homepage": "https://github.com/pfefferle/wordpress-activitypub#readme",
"devDependencies": { "devDependencies": {
"@wordpress/blocks": "^12.11.0",
"@wordpress/components": "^25.0.0",
"@wordpress/data": "^9.4.0",
"@wordpress/dom-ready": "^3.36.0",
"@wordpress/element": "^5.11.0",
"@wordpress/scripts": "^26.5.0",
"classnames": "^2.3.2",
"grunt": "^1.1.0", "grunt": "^1.1.0",
"grunt-checktextdomain": "^1.0.1", "grunt-checktextdomain": "^1.0.1",
"grunt-wp-i18n": "^1.0.3", "grunt-wp-i18n": "^1.0.3",

41
src/followers/block.json Normal file
View file

@ -0,0 +1,41 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"name": "activitypub/followers",
"apiVersion": 3,
"version": "1.0.0",
"title": "Fediverse Followers",
"category": "widgets",
"description": "Display your followers from the Fediverse on your website.",
"textdomain": "activitypub",
"icon": "groups",
"supports": {
"html": false
},
"attributes": {
"title": {
"type": "string",
"default": "Fediverse Followers"
},
"selectedUser": {
"type": "string",
"default": "site"
},
"per_page": {
"type": "number",
"default": 10
},
"order": {
"type": "string",
"default": "desc",
"enum": [ "asc", "desc" ]
}
},
"styles": [
{ "name": "default", "label": "No Lines", "isDefault": true },
{ "name": "with-lines", "label": "Lines" },
{ "name": "compact", "label": "Compact" }
],
"editorScript": "file:./index.js",
"viewScript": "file:./view.js",
"style": ["file:./style-view.css","wp-block-query-pagination"]
}

68
src/followers/edit.js Normal file
View file

@ -0,0 +1,68 @@
import { SelectControl, RangeControl, PanelBody } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useMemo, useState } from '@wordpress/element';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { Followers } from './followers';
export default function Edit( { attributes, setAttributes } ) {
const { order, per_page, selectedUser, className } = attributes;
const blockProps = useBlockProps();
const [ page, setPage ] = useState( 1 );
const orderOptions = [
{ label: __( 'New to old', 'activitypub' ), value: 'desc' },
{ label: __( 'Old to new', 'activitypub' ), value: 'asc' },
];
const users = useSelect( ( select ) => select( 'core' ).getUsers( { who: 'authors' } ) );
const usersOptions = useMemo( () => {
if ( ! users ) {
return [];
}
const withBlogUser =[ {
label: __( 'Whole Site', 'activitypub' ),
value: 'site'
} ];
return users.reduce( ( acc, user ) => {
acc.push({
label: user.name,
value: user.id
} );
return acc;
}, withBlogUser );
}, [ users ] );
const setAttributestAndResetPage = ( key ) => {
return ( value ) => {
setPage( 1 );
setAttributes( { [ key ]: value } );
};
}
return (
<div { ...blockProps }>
<InspectorControls key="setting">
<PanelBody title={ __( 'Followers Options', 'activitypub' ) }>
<SelectControl
label= { __( 'Select User', 'activitypub' ) }
value={ selectedUser }
options={ usersOptions }
onChange={ setAttributestAndResetPage( 'selectedUser' ) }
/>
<SelectControl
label={ __( 'Sort', 'activitypub' ) }
value={ order }
options={ orderOptions }
onChange={ setAttributestAndResetPage( 'order' ) }
/>
<RangeControl
label={ __( 'Number of Followers', 'activitypub' ) }
value={ per_page }
onChange={ setAttributestAndResetPage( 'per_page' ) }
min={ 1 }
max={ 10 }
/>
</PanelBody>
</InspectorControls>
<Followers { ...attributes } page={ page } setPage={ setPage } />
</div>
);
}

105
src/followers/followers.js Normal file
View file

@ -0,0 +1,105 @@
import { useState, useEffect } from 'react';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Pagination } from './pagination';
import { ExternalLink } from '@wordpress/components';
const { namespace } = window._activityPubOptions;
function getPath( userId, per_page, order, page ) {
const path = `/${ namespace }/users/${ userId }/followers`;
const args = {
per_page,
order,
page,
context: 'full'
};
return addQueryArgs( path, args );
}
function usePage() {
const [ page, setPage ] = useState( 1 );
return [ page, setPage ];
}
export function Followers( {
selectedUser,
per_page,
order,
title,
page: passedPage,
setPage: passedSetPage,
className = ''
} ) {
const userId = selectedUser === 'site' ? 0 : selectedUser;
const [ followers, setFollowers ] = useState( [] );
const [ pages, setPages ] = useState( 0 );
const [ total, setTotal ] = useState( 0 );
const [ localPage, setLocalPage ] = usePage();
const page = passedPage || localPage;
const setPage = passedSetPage || setLocalPage;
const prevLabel = createInterpolateElement(
/* translators: arrow for previous followers link */
__( '<span>←</span> Less', 'activitypub' ),
{
span: <span class="wp-block-query-pagination-previous-arrow is-arrow-arrow" aria-hidden="true" />,
}
);
const nextLabel = createInterpolateElement(
/* translators: arrow for next followers link */
__( 'More <span>→</span>', 'activitypub' ),
{
span: <span class="wp-block-query-pagination-next-arrow is-arrow-arrow" aria-hidden="true" />,
}
);
useEffect( () => {
const path = getPath( userId, per_page, order, page );
apiFetch( { path } )
.then( ( data ) => {
setPages( Math.ceil( data.totalItems / per_page ) );
setTotal( data.totalItems );
setFollowers( data.orderedItems );
} )
.catch( ( error ) => console.error( error ) );
}, [ userId, per_page, order, page ] );
return (
<div className={ "activitypub-follower-block " + className }>
<h3>{ title }</h3>
<ul>
{ followers && followers.map( ( follower ) => (
<li key={ follower.url }>
<Follower { ...follower } />
</li>
) ) }
</ul>
{ pages > 1 && (
<Pagination
page={ page }
perPage={ per_page }
total={ total }
pageClick={ setPage }
nextLabel={ nextLabel }
prevLabel={ prevLabel }
compact={ className === 'is-style-compact' }
/>
) }
</div>
);
}
function Follower( { name, icon, url, preferredUsername } ) {
const handle = `@${ preferredUsername }`;
return (
<ExternalLink className="activitypub-link" href={ url } title={ handle } onClick={ event => event.preventDefault() }>
<img width="40" height="40" src={ icon.url } class="avatar activitypub-avatar" />
<span class="activitypub-actor">
<strong className="activitypub-name">{ name }</strong>
<span class="sep">/</span>
<span class="activitypub-handle">{ handle }</span>
</span>
</ExternalLink>
)
}

5
src/followers/index.js Normal file
View file

@ -0,0 +1,5 @@
import { registerBlockType } from '@wordpress/blocks';
import { people } from '@wordpress/icons';
import edit from './edit';
const save = () => null;
registerBlockType( 'activitypub/followers', { edit, save, icon: people } );

View file

@ -0,0 +1,18 @@
import classNames from 'classnames';
export function PaginationPage( { active, children, page, pageClick, className } ) {
const handleClick = event => {
event.preventDefault();
! active && pageClick( page );
};
const classes = classNames( 'wp-block activitypub-pager', className , {
'current': active,
} );
return (
<a className={ classes }onClick={ handleClick }>
{ children }
</a>
);
}

View file

@ -0,0 +1,82 @@
// Adapted from: https://github.com/Automattic/wp-calypso/tree/trunk/client/components/pagination
// Markup adapted to imitate the core query-pagination component so we can inherit those styles.
import classnames from 'classnames';
import { PaginationPage } from './pagination-page';
const PaginationVariant = {
outlined: 'outlined',
minimal: 'minimal',
};
export function Pagination( { compact, nextLabel, page, pageClick, perPage, prevLabel, total, variant = PaginationVariant.outlined } ) {
const getPageList = ( page, pageCount ) => {
let pageList = [ 1, page - 2, page - 1, page, page + 1, page + 2, pageCount ];
pageList.sort( ( a, b ) => a - b );
// Remove pages less than 1, or greater than total number of pages, and remove duplicates
pageList = pageList.filter( ( pageNumber, index, originalPageList ) => {
return (
pageNumber >= 1 &&
pageNumber <= pageCount &&
originalPageList.lastIndexOf( pageNumber ) === index
);
} );
for ( let i = pageList.length - 2; i >= 0; i-- ) {
if ( pageList[ i ] === pageList[ i + 1 ] ) {
pageList.splice( i + 1, 1 );
}
}
return pageList;
};
const pageList = getPageList( page, Math.ceil( total / perPage ) );
const className = classnames( 'alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex', `is-${ variant }`, {
'is-compact': compact,
} );
return (
<nav className={ className }>
{ prevLabel && (
<PaginationPage
key="prev"
page={ page - 1 }
pageClick={ pageClick }
active={ page === 1 }
aria-label={ prevLabel }
className="wp-block-query-pagination-previous block-editor-block-list__block"
>
{ prevLabel }
</PaginationPage>
) }
{ ! compact && (
<div className="block-editor-block-list__block wp-block wp-block-query-pagination-numbers">
{ pageList.map( pageNumber => (
<PaginationPage
key={ pageNumber }
page={ pageNumber }
pageClick={ pageClick }
active={ pageNumber === page }
className="page-numbers"
>
{ pageNumber }
</PaginationPage>
) ) }
</div>
) }
{ nextLabel && (
<PaginationPage
key="next"
page={ page + 1 }
pageClick={ pageClick }
active={ page === Math.ceil( total / perPage ) }
aria-label={ nextLabel }
className="wp-block-query-pagination-next block-editor-block-list__block"
>
{ nextLabel }
</PaginationPage>
) }
</nav>
);
}

View file

@ -0,0 +1,179 @@
// I like lamp.
$minimal-inactive: #999;
$font-body-small: 12px;
.followers-pagination {
width: 100%;
&.is-compact .pagination__list-item {
.pagination__list-button.button.is-borderless {
padding: 5px 8px;
}
&.pagination__ellipsis span {
padding: 5px 6px;
}
&.pagination__arrow .pagination__list-button.button.is-borderless {
padding: 6px;
}
}
&.is-minimal {
.pagination__list-item,
.pagination__list-item:first-child,
.pagination__list-item:last-child {
.pagination__list-button.button {
padding: 8px 16px;
background-color: transparent;
border: none;
color: $minimal-inactive;
&:hover {
color: var( --wp--preset--color--contrast, #000 );
}
&:disabled {
visibility: hidden;
}
}
&.pagination__ellipsis span {
padding: 8px 16px;
background-color: transparent;
border: none;
color: $minimal-inactive;
}
&.is-selected {
.pagination__list-button.button {
background-color: transparent;
color: var( --wp--preset--color--contrast, #000 );
&:hover {
color: var( --wp--preset--color--contrast, #000 );
}
}
}
&.pagination__arrow {
.pagination__list-button {
color: var( --wp--preset--color--contrast, #000 );
line-height: calc( var( --wp--preset--color--contrast, #000 ) * 1.5 );
}
.gridicon {
width: calc( var( --wp--preset--color--contrast, #000 ) * 1.5 );
height: calc( var( --wp--preset--color--contrast, #000 ) * 1.5 );
}
}
}
}
}
.pagination__list {
display: flex;
margin: 0;
list-style: none;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
// List item styles for all links
.pagination__list-item {
.pagination__list-button.button.is-borderless,
&.pagination__ellipsis span {
padding: 8px 12px;
background-color: var(--color-surface);
border: solid 1px var(--color-neutral-10);
border-right: none;
font-size: $font-body-small;
line-height: 18px;
color: var(--color-text-subtle);
text-align: center;
border-radius: 0;
display: flex;
flex-wrap: nowrap;
.gridicon {
top: 1px;
.pagination.is-compact & {
top: 0;
}
}
}
&:first-child .pagination__list-button.button.is-borderless {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
&:last-child .pagination__list-button.button.is-borderless {
border-right: solid 1px var(--color-neutral-10);
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
}
// Left/right navigation arrows
.pagination__list-item.pagination__arrow {
display: flex;
align-items: center;
&.is-left {
padding-left: 14px;
float: left;
.gridicon {
margin-right: 2px;
}
}
&.is-right {
padding-right: 14px;
float: right;
.gridicon {
margin-left: 2px;
}
}
.gridicon {
vertical-align: middle;
width: 18px;
height: 18px;
}
}
// // Hover/focus states
.pagination__list-item .pagination__list-button.button.is-borderless:not([disabled]):hover,
.pagination__list-item .pagination__list-button.button.is-borderless:focus {
color: var(--color-neutral-70);
outline: none;
}
// Selected state
.pagination__list-item.is-selected .pagination__list-button.button.is-borderless {
border-color: var(--color-primary);
background-color: var(--color-primary);
color: var(--color-text-inverted);
}
.pagination__list-item.is-selected .pagination__list-button.button.is-borderless:hover {
color: var(--color-text-inverted);
}
.pagination__list-item.is-selected + .pagination__list-item
.pagination__list-button.button.is-borderless {
border-left: 0;
}
// Abridgement indication
.pagination__ellipsis,
.pagination__ellipsis:hover {
color: var(--color-neutral-light);
}

82
src/followers/style.scss Normal file
View file

@ -0,0 +1,82 @@
.activitypub-follower-block {
&.is-style-compact {
.activitypub-handle, .sep {
display: none;
}
}
&.is-style-with-lines {
ul li {
border-bottom: .5px solid;
padding-bottom: .5rem;
margin-bottom: .5rem;
&:last-child {
border-bottom: none;
}
}
.activitypub-name, .activitypub-handle {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
ul {
margin: 0 !important;
padding: 0 !important;
}
li {
display: flex;
margin-bottom: 1rem;
}
img {
border-radius: 50%;
width: 40px;
height: 40px;
margin-right: var( --wp--preset--spacing--20, .5rem );
}
.activitypub-link {
display: flex;
flex-flow: row nowrap;
align-items: center;
max-width: 100%;
color: inherit !important;
text-decoration: none !important; // overrides core styles for external links
}
.activitypub-name, .activitypub-handle {
text-decoration: underline;
text-decoration-thickness: .8px;
text-underline-position: under;
&:hover {
text-decoration: none;
}
}
.activitypub-name {
font-size: var( --wp--preset--font-size--normal, 16px );
}
.activitypub-actor {
font-size: var( --wp--preset--font-size--small, 13px );
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sep {
padding: 0 .2rem;
}
.wp-block-query-pagination {
margin-top: 1.5rem;
}
.activitypub-pager {
cursor: default;
&.current {
opacity: .33;
}
}
.page-numbers {
padding: 0 .2rem;
&.current {
opacity: 1;
font-weight: bold;
}
}
}

12
src/followers/view.js Normal file
View file

@ -0,0 +1,12 @@
import './style.scss';
import { Followers } from './followers';
import { render } from '@wordpress/element';
import domReady from '@wordpress/dom-ready';
domReady( () => {
// iterate over a nodelist
[].forEach.call( document.querySelectorAll( '.activitypub-follower-block' ), ( element ) => {
const attrs = JSON.parse( element.dataset.attrs );
render( <Followers { ...attrs } />, element );
} );
} );

View file

@ -1,6 +1,6 @@
<div class="wrap"> <div class="wrap">
<h1><?php \esc_html_e( 'Followers', 'activitypub' ); ?></h1> <h1><?php \esc_html_e( 'Followers', 'activitypub' ); ?></h1>
<?php // translators: ?> <?php // translators: The follower count. ?>
<p><?php \printf( \esc_html__( 'You currently have %s followers.', 'activitypub' ), \esc_attr( \Activitypub\Collection\Followers::count_followers( \get_current_user_id() ) ) ); ?></p> <p><?php \printf( \esc_html__( 'You currently have %s followers.', 'activitypub' ), \esc_attr( \Activitypub\Collection\Followers::count_followers( \get_current_user_id() ) ) ); ?></p>
<?php $table = new \Activitypub\Table\Followers(); ?> <?php $table = new \Activitypub\Table\Followers(); ?>

View file

@ -267,6 +267,16 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
$this->assertCount( 1, $meta ); $this->assertCount( 1, $meta );
} }
/**
* @dataProvider extract_name_from_uri_content_provider
*/
public function test_extract_name_from_uri( $uri, $name ) {
$follower = new \Activitypub\Model\Follower();
$follower->set_id( $uri );
$this->assertEquals( $name, $follower->get_name() );
}
public static function http_request_host_is_external( $in, $host ) { public static function http_request_host_is_external( $in, $host ) {
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
@ -274,6 +284,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
} }
return $in; return $in;
} }
public static function http_request_args( $args, $url ) { public static function http_request_args( $args, $url ) {
if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'example.com', 'example.org' ), true ) ) { if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'example.com', 'example.org' ), true ) ) {
$args['reject_unsafe_urls'] = false; $args['reject_unsafe_urls'] = false;
@ -308,4 +319,19 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
} }
return $pre; return $pre;
} }
public function extract_name_from_uri_content_provider() {
return array(
array( 'https://example.com/@user', 'user' ),
array( 'https://example.com/@user/', 'user' ),
array( 'https://example.com/users/user', 'user' ),
array( 'https://example.com/users/user/', 'user' ),
array( 'https://example.com/@user?as=asasas', 'user' ),
array( 'https://example.com/@user#asass', 'user' ),
array( '@user@example.com', 'user' ),
array( 'acct:user@example.com', 'user' ),
array( 'user@example.com', 'user' ),
array( 'https://example.com', 'https://example.com' ),
);
}
} }