diff --git a/.github/changelog/1722-from-description b/.github/changelog/1722-from-description new file mode 100644 index 000000000..dde9a07ff --- /dev/null +++ b/.github/changelog/1722-from-description @@ -0,0 +1,4 @@ +Significance: major +Type: changed + +The Reactions block now uses the latest Block Editor technology for display on the frontend. diff --git a/.prettierignore b/.prettierignore index 0121f2e2d..b35ffe6c4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,7 +6,6 @@ vendor # Temporary ignores while breaking out each component. assets src/followers -src/reactions src/remote-reply src/reply src/reply-intent diff --git a/build/follow-me/style-view.css b/build/follow-me/style-view.css index 7b057ab96..4f53275d1 100644 --- a/build/follow-me/style-view.css +++ b/build/follow-me/style-view.css @@ -1 +1 @@ -.activitypub-follow-me-block-wrapper{display:block;position:relative}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;-o-object-fit:cover;object-fit:cover;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile__content{flex:1;margin-right:1rem;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__name{font-size:1.25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile__name{color:inherit;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button{align-items:center;display:flex;margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button:not(:only-child){margin-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button__link{margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .is-small{font-size:.8rem;padding:.25rem .5rem}.activitypub-follow-me-block-wrapper .activitypub-profile .is-compact{font-size:.9rem;padding:.4rem .8rem}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border .activitypub-profile{padding-left:1rem;padding-right:1rem}body.modal-open{overflow:hidden}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;left:0;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white);border-radius:4px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.activitypub-dialog__section{border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;font-size:95%;margin-bottom:1rem}.activitypub-dialog__button-group{display:flex;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{border:1px solid var(--wp--preset--color--gray,#e2e4e7);border-radius:4px 0 0 4px;flex:1;line-height:1;margin:0}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red)}.activitypub-dialog__button-group button{border-radius:0 4px 4px 0!important;margin-left:-1px!important;min-width:22.5%;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red);font-size:90%;margin-top:.5rem}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}} +.activitypub-follow-me-block-wrapper{display:block;position:relative}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;-o-object-fit:cover;object-fit:cover;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile__content{flex:1;margin-right:1rem;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__name{font-size:1.25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile__name{color:inherit;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button{align-items:center;display:flex;margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button:not(:only-child){margin-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button__link{margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .is-small{font-size:.8rem;padding:.25rem .5rem}.activitypub-follow-me-block-wrapper .activitypub-profile .is-compact{font-size:.9rem;padding:.4rem .8rem}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border .activitypub-profile{padding-left:1rem;padding-right:1rem}body.modal-open{overflow:hidden}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;left:0;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.activitypub-dialog__section{border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;font-size:95%;margin-bottom:1rem}.activitypub-dialog__button-group{display:flex;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{border:1px solid var(--wp--preset--color--gray,#e2e4e7);border-radius:4px 0 0 4px;flex:1;line-height:1;margin:0}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red)}.activitypub-dialog__button-group button{border-radius:0 4px 4px 0!important;margin-left:-1px!important;min-width:22.5%;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red);font-size:90%;margin-top:.5rem}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}} diff --git a/build/reactions/block.json b/build/reactions/block.json index 4d2ab185e..91ed1585d 100644 --- a/build/reactions/block.json +++ b/build/reactions/block.json @@ -1,32 +1,27 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", "name": "activitypub/reactions", - "apiVersion": 2, - "version": "2.0.0", + "apiVersion": 3, + "version": "3.0.0", "title": "Fediverse Reactions", "category": "widgets", "icon": "heart", "description": "Display Fediverse likes and reposts", "supports": { "html": false, - "align": true, - "layout": { - "default": { - "type": "constrained", - "orientation": "vertical", - "justifyContent": "center" - } - } + "align": [ + "wide", + "full" + ], + "interactivity": true }, - "attributes": {}, "blockHooks": { "core/post-content": "after" }, "textdomain": "activitypub", "editorScript": "file:./index.js", - "style": [ - "file:./style-index.css", - "wp-components" - ], - "viewScript": "file:./view.js" + "style": "file:./style-index.css", + "viewScriptModule": "file:./view.js", + "viewScript": "wp-api-fetch", + "render": "file:./render.php" } \ No newline at end of file diff --git a/build/reactions/index.asset.php b/build/reactions/index.asset.php index 24cc64a55..d80457c91 100644 --- a/build/reactions/index.asset.php +++ b/build/reactions/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '71a9b97929d16c6932ad'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '8cb5b7894afa6bbe1c96'); diff --git a/build/reactions/index.js b/build/reactions/index.js index 2c4ef6561..dd0db18a9 100644 --- a/build/reactions/index.js +++ b/build/reactions/index.js @@ -1,3 +1,3 @@ -(()=>{"use strict";var e,t={29:(e,t,r)=>{const n=window.wp.blocks,a=[{attributes:{title:{type:"string",default:"Fediverse reactions"}},supports:{html:!1,align:!0,layout:{default:{type:"constrained",orientation:"vertical",justifyContent:"center"}}},isEligible:e=>!!e.title,migrate(e){const{title:t,...r}=e;return[r,[(0,n.createBlock)("core/heading",{content:t,level:6})]]}}],l=window.React,o=window.wp.blockEditor,i=window.wp.element,s=window.wp.i18n,c=window.wp.components,u=window.wp.apiFetch;var m=r.n(u);function p(){return window._activityPubOptions||{}}const d=({reactions:e})=>{const{defaultAvatarUrl:t}=p();return(0,l.createElement)("ul",{className:"reaction-avatars"},e.map(((e,r)=>{const n=["reaction-avatar"].filter(Boolean).join(" "),a=e.avatar||t;return(0,l.createElement)("li",{key:r},(0,l.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer"},(0,l.createElement)("img",{src:a,alt:e.name,className:n,width:"32",height:"32",onError:e=>{e.target.src=t}})))})))},f=({reactions:e,type:t})=>{const{defaultAvatarUrl:r}=p();return(0,l.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,t)=>{const n=e.avatar||r;return(0,l.createElement)("li",{key:t},(0,l.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,l.createElement)("img",{src:n,alt:e.name,width:"32",height:"32",onError:e=>{e.target.src=r}}),(0,l.createElement)("span",null,e.name)))})))},h=({items:e,label:t})=>{const[r,n]=(0,i.useState)(!1),[a,o]=(0,i.useState)(null),[s,u]=(0,i.useState)(e.length),m=(0,i.useRef)(null);(0,i.useEffect)((()=>{if(!m.current)return;const t=()=>{const t=m.current;if(!t)return;const r=t.offsetWidth-(a?.offsetWidth||0)-12,n=Math.max(1,Math.floor((r-32)/22));u(Math.min(n,e.length))};t();const r=new ResizeObserver(t);return r.observe(m.current),()=>{r.disconnect()}}),[a,e.length]);const p=e.slice(0,s);return(0,l.createElement)("div",{className:"reaction-group",ref:m},(0,l.createElement)(d,{reactions:p}),(0,l.createElement)(c.Button,{ref:o,className:"reaction-label is-link",onClick:()=>n(!r),"aria-expanded":r},t),r&&a&&(0,l.createElement)(c.Popover,{anchor:a,onClose:()=>n(!1)},(0,l.createElement)(f,{reactions:e})))};function g({postId:e=null,reactions:t=null}){const{namespace:r}=p(),[n,a]=(0,i.useState)(t),[o,s]=(0,i.useState)(!t);return(0,i.useEffect)((()=>{if(t)return a(t),void s(!1);e?(s(!0),m()({path:`/${r}/posts/${e}/reactions`}).then((e=>{a(e),s(!1)})).catch((()=>s(!1)))):s(!1)}),[e,t]),o?null:n&&Object.values(n).some((e=>e.items?.length>0))?(0,l.createElement)(l.Fragment,null,Object.entries(n).map((([e,t])=>t.items?.length?(0,l.createElement)(h,{key:e,items:t.items,label:t.label}):null))):null}const v=e=>{const t=["#FF6B6B","#4ECDC4","#45B7D1","#96CEB4","#FFEEAD","#D4A5A5","#9B59B6","#3498DB","#E67E22"],r=(()=>{const e=["Bouncy","Cosmic","Dancing","Fluffy","Giggly","Hoppy","Jazzy","Magical","Nifty","Perky","Quirky","Sparkly","Twirly","Wiggly","Zippy"],t=["Badger","Capybara","Dolphin","Echidna","Flamingo","Giraffe","Hedgehog","Iguana","Jellyfish","Koala","Lemur","Manatee","Narwhal","Octopus","Penguin"];return`${e[Math.floor(Math.random()*e.length)]} ${t[Math.floor(Math.random()*t.length)]}`})(),n=t[Math.floor(Math.random()*t.length)],a=r.charAt(0),l=document.createElement("canvas");l.width=64,l.height=64;const o=l.getContext("2d");return o.fillStyle=n,o.beginPath(),o.arc(32,32,32,0,2*Math.PI),o.fill(),o.fillStyle="#FFFFFF",o.font="32px sans-serif",o.textAlign="center",o.textBaseline="middle",o.fillText(a,32,32),{name:r,url:"#",avatar:l.toDataURL()}},b=JSON.parse('{"UU":"activitypub/reactions"}');(0,n.registerBlockType)(b.UU,{deprecated:a,edit:function({attributes:e,__unstableLayoutClassNames:t}){const r=(0,o.useBlockProps)({className:t}),[n]=(0,i.useState)({likes:{label:(0,s.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */ -(0,s._x)("%d likes","number of likes","activitypub"),9),items:Array.from({length:9},((e,t)=>v()))},reposts:{label:(0,s.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */ -(0,s._x)("%d reposts","number of reposts","activitypub"),6),items:Array.from({length:6},((e,t)=>v()))}}),a=[["core/heading",{level:6,placeholder:(0,s.__)("Fediverse Reactions","activitypub"),content:(0,s.__)("Fediverse Reactions","activitypub")}]];return(0,l.createElement)("div",{...r},(0,l.createElement)(o.InnerBlocks,{template:a,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),(0,l.createElement)(g,{reactions:n}))},save:function(){return(0,l.createElement)(l.Fragment,null,(0,l.createElement)(o.InnerBlocks.Content,null),(0,l.createElement)("div",{className:"activitypub-reactions-block"}))}})}},r={};function n(e){var a=r[e];if(void 0!==a)return a.exports;var l=r[e]={exports:{}};return t[e](l,l.exports,n),l.exports}n.m=t,e=[],n.O=(t,r,a,l)=>{if(!r){var o=1/0;for(u=0;u=l)&&Object.keys(n.O).every((e=>n.O[e](r[s])))?r.splice(s--,1):(i=!1,l0&&e[u-1][2]>l;u--)e[u]=e[u-1];e[u]=[r,a,l]},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={608:0,104:0};n.O.j=t=>0===e[t];var t=(t,r)=>{var a,l,[o,i,s]=r,c=0;if(o.some((t=>0!==e[t]))){for(a in i)n.o(i,a)&&(n.m[a]=i[a]);if(s)var u=s(n)}for(t&&t(r);cn(29)));a=n.O(a)})(); \ No newline at end of file +(()=>{"use strict";var e,t={646:(e,t,r)=>{const a=window.wp.blocks,n=[{attributes:{title:{type:"string",default:"Fediverse reactions"}},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.title,migrate(e){const{title:t,...r}=e;return[r,[(0,a.createBlock)("core/heading",{content:t,level:6})]]}}],l=window.React,o=window.wp.blockEditor,s=window.wp.i18n,i=window.wp.data,c=window.wp.element,u=window.wp.components,m=window.wp.apiFetch;var p=r.n(m);function d(){return window._activityPubOptions||{}}const v=({reactions:e})=>{const{defaultAvatarUrl:t}=d();return(0,l.createElement)("ul",{className:"reaction-avatars"},e.map(((e,r)=>{const a=["reaction-avatar"].filter(Boolean).join(" "),n=e.avatar||t;return(0,l.createElement)("li",{key:r},(0,l.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer"},(0,l.createElement)("img",{src:n,alt:e.name,className:a,width:"32",height:"32",onError:e=>{e.target.src=t}})))})))},f=({reactions:e})=>{const{defaultAvatarUrl:t}=d();return(0,l.createElement)("ul",{className:"reactions-list"},e.map(((e,r)=>{const a=e.avatar||t;return(0,l.createElement)("li",{key:r,className:"reaction-item"},(0,l.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,l.createElement)("img",{src:a,alt:e.name,width:"32",height:"32",onError:e=>{e.target.src=t}}),(0,l.createElement)("span",{className:"reaction-name"},e.name)))})))},h=({items:e,label:t})=>{const[r,a]=(0,c.useState)(!1),[n,o]=(0,c.useState)(null),s=(0,c.useRef)(null),i=e.slice(0,20);return(0,l.createElement)("div",{className:"reaction-group",ref:s},(0,l.createElement)(v,{reactions:i}),(0,l.createElement)(u.Button,{ref:o,className:"reaction-label is-link",onClick:()=>a(!r),"aria-expanded":r},t),r&&n&&(0,l.createElement)(u.Popover,{anchor:n,onClose:()=>a(!1)},(0,l.createElement)(f,{reactions:e})))};function b({postId:e=null,reactions:t=null,fallbackReactions:r=null}){const{namespace:a}=d(),[n,o]=(0,c.useState)(t),[s,i]=(0,c.useState)(!t);return(0,c.useEffect)((()=>{if(t)return o(t),void i(!1);e?(i(!0),p()({path:`/${a}/posts/${e}/reactions`}).then((e=>{const t=Object.values(e).some((e=>e.items?.length>0));o(!t&&r?r:e),i(!1)})).catch((()=>{r&&o(r),i(!1)}))):i(!1)}),[e,t,r,a]),s?null:n&&Object.values(n).some((e=>e.items?.length>0))?(0,l.createElement)(l.Fragment,null,Object.entries(n).map((([e,t])=>t.items?.length?(0,l.createElement)(h,{key:e,items:t.items,label:t.label}):null))):null}const g=(e,t,r,a)=>Array.from({length:e},((e,n)=>({name:`${t} ${n+1}`,url:"#",avatar:`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='32' fill='%23${a[n%a.length]}'/%3E%3Ctext x='32' y='38' font-family='sans-serif' font-size='24' fill='white' text-anchor='middle'%3E${String.fromCharCode(r+n)}%3C/text%3E%3C/svg%3E`}))),w=["FF6B6B","4ECDC4","45B7D1","96CEB4","D4A5A5","9B59B6","3498DB","E67E22"],E={likes:{label:(0,s.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */ +(0,s._x)("%d likes","number of likes","activitypub"),9),items:g(9,"User",65,w)},reposts:{label:(0,s.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */ +(0,s._x)("%d reposts","number of reposts","activitypub"),6),items:g(6,"Reposter",82,w)}},k=JSON.parse('{"UU":"activitypub/reactions"}');(0,a.registerBlockType)(k.UU,{deprecated:n,edit:function({__unstableLayoutClassNames:e}){const t=(0,o.useBlockProps)({className:e}),{getCurrentPostId:r}=(0,i.select)("core/editor"),a=[["core/heading",{level:6,placeholder:(0,s.__)("Fediverse Reactions","activitypub"),content:(0,s.__)("Fediverse Reactions","activitypub")}]];return(0,l.createElement)("div",{...t},(0,l.createElement)(o.InnerBlocks,{template:a,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),(0,l.createElement)(b,{postId:r(),fallbackReactions:E}))},save:function(){return(0,l.createElement)("div",{...o.useBlockProps.save()},(0,l.createElement)(o.InnerBlocks.Content,null))}})}},r={};function a(e){var n=r[e];if(void 0!==n)return n.exports;var l=r[e]={exports:{}};return t[e](l,l.exports,a),l.exports}a.m=t,e=[],a.O=(t,r,n,l)=>{if(!r){var o=1/0;for(u=0;u=l)&&Object.keys(a.O).every((e=>a.O[e](r[i])))?r.splice(i--,1):(s=!1,l0&&e[u-1][2]>l;u--)e[u]=e[u-1];e[u]=[r,n,l]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var 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),(()=>{var e={608:0,104:0};a.O.j=t=>0===e[t];var t=(t,r)=>{var n,l,[o,s,i]=r,c=0;if(o.some((t=>0!==e[t]))){for(n in s)a.o(s,n)&&(a.m[n]=s[n]);if(i)var u=i(a)}for(t&&t(r);ca(646)));n=a.O(n)})(); \ No newline at end of file diff --git a/build/reactions/render.php b/build/reactions/render.php new file mode 100644 index 000000000..e4c4ca1bc --- /dev/null +++ b/build/reactions/render.php @@ -0,0 +1,216 @@ + null ) ); + +/* @var string $content Inner blocks content. */ +if ( empty( $content ) ) { + // Fallback for v1.0.0 blocks. + $_title = $attributes['title'] ?? __( 'Fediverse Reactions', 'activitypub' ); + $content = '
' . esc_html( $_title ) . '
'; + unset( $attributes['title'], $attributes['className'] ); +} + +// Get the Post ID from attributes or use the current post. +$_post_id = $attributes['postId'] ?? get_the_ID(); + +// Generate a unique ID for the block. +$block_id = 'activitypub-reactions-block-' . wp_unique_id(); + +$reactions = array(); + +foreach ( Comment::get_comment_types() as $_type => $type_object ) { + $_comments = get_comments( + array( + 'post_id' => $_post_id, + 'type' => $_type, + 'status' => 'approve', + ) + ); + + if ( empty( $_comments ) ) { + continue; + } + + $count = count( $_comments ); + // phpcs:disable WordPress.WP.I18n + $label = sprintf( + _n( + $type_object['count_single'], + $type_object['count_plural'], + $count, + 'activitypub' + ), + number_format_i18n( $count ) + ); + // phpcs:enable WordPress.WP.I18n + + $reactions[ $_type ] = array( + 'label' => $label, + 'count' => $count, + 'items' => array_map( + function ( $comment ) { + return array( + 'id' => $comment->comment_ID, + 'name' => $comment->comment_author, + 'url' => $comment->comment_author_url, + 'avatar' => get_comment_meta( $comment->comment_ID, 'avatar_url', true ), + ); + }, + $_comments + ), + ); +} + +// Set up the Interactivity API state. +wp_interactivity_state( + 'activitypub/reactions', + array( + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'reactions' => array( + $_post_id => $reactions, + ), + ) +); + +// Render a subset of the most recent reactions. +$reactions = array_map( + function ( $reaction ) use ( $attributes ) { + $count = 20; + if ( 'wide' === $attributes['align'] ) { + $count = 40; + } elseif ( 'full' === $attributes['align'] ) { + $count = 60; + } + + $reaction['items'] = array_slice( array_reverse( $reaction['items'] ), 0, $count ); + + return $reaction; + }, + $reactions +); + +// Initialize the context for the block. +$context = array( + 'blockId' => $block_id, + 'hasReactions' => ! empty( $reactions ), + 'reactions' => $reactions, + 'postId' => $_post_id, + 'isModalOpen' => false, + 'modal' => array( + 'items' => array(), + ), +); + +// Add the block wrapper attributes. +$wrapper_attributes = get_block_wrapper_attributes( + array( + 'id' => $block_id, + 'data-wp-interactive' => 'activitypub/reactions', + 'data-wp-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wp-init' => 'callbacks.initReactions', + 'data-wp-on-document--keydown' => 'callbacks.documentKeydown', + 'data-wp-on-document--click' => 'callbacks.documentClick', + 'data-wp-bind--hidden' => '!context.hasReactions', + ) +); +?> + +
> + + +
+ $reaction ) : + /* translators: %s: reaction type. */ + $aria_label = sprintf( __( 'View all %s', 'activitypub' ), Comment::get_comment_type_attr( $_type, 'label' ) ); + ?> +
+
    + +
+ +
+ +
+ + +
diff --git a/build/reactions/style-index-rtl.css b/build/reactions/style-index-rtl.css index 3260c3349..871d27339 100644 --- a/build/reactions/style-index-rtl.css +++ b/build/reactions/style-index-rtl.css @@ -1 +1 @@ -.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.wp-block-activitypub-reactions .reaction-avatars li{margin:0 0 0 -10px;padding:0}.wp-block-activitypub-reactions .reaction-avatars li:last-child{margin-left:0}.wp-block-activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.wp-block-activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.wp-block-activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em .7em .25em 1.3em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px} +.wp-block-activitypub-reactions{margin-bottom:2rem;margin-top:2rem;position:relative}.wp-block-activitypub-reactions .activitypub-reactions{display:flex;flex-direction:column;flex-wrap:wrap;gap:1rem;margin-top:16px}.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.5rem;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0!important;padding:0}.wp-block-activitypub-reactions .reaction-avatars li{margin:0 0 0 -10px;padding:0;transition:transform .2s ease}.wp-block-activitypub-reactions .reaction-avatars li:last-child{margin-left:0}.wp-block-activitypub-reactions .reaction-avatars li:hover{transform:translateY(-2px);z-index:2}.wp-block-activitypub-reactions .reaction-avatars li a{border-radius:50%;box-shadow:none;display:block;line-height:1;text-decoration:none}.wp-block-activitypub-reactions .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-avatar:focus-visible,.wp-block-activitypub-reactions .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-label{align-items:center;background:none;border:none;border-radius:4px;color:var(--wp--preset--color--contrast,#2271b1);display:flex;flex:0 0 auto;font-size:70%;gap:.25rem;margin-right:12px;padding:.25rem .5rem;text-decoration:none;transition:background-color .2s ease;white-space:nowrap}.wp-block-activitypub-reactions .reaction-label:hover{background-color:rgba(0,0,0,.05);color:var(--wp--preset--color--contrast,#135e96)}.wp-block-activitypub-reactions .reaction-label:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;right:0;padding:1rem;position:fixed;left:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;right:auto;padding:0;position:absolute;left:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.reactions-list{list-style:none;margin:0!important;padding:.5rem}.reactions-list .reaction-item{margin:0 0 .5rem}.reactions-list .reaction-item:last-child{margin-bottom:0}.reactions-list .reaction-item a{align-items:center;border-radius:4px;box-shadow:none;color:inherit;display:flex;gap:.75rem;padding:.5rem;text-decoration:none;transition:background-color .2s ease}.reactions-list .reaction-item a:hover{background-color:rgba(0,0,0,.03)}.reactions-list .reaction-item img{border:1px solid #f0f0f0;border-radius:50%;box-shadow:none;height:36px;width:36px}.reactions-list .reaction-item .reaction-name{font-size:75%}body.modal-open{overflow:hidden}.components-popover__content{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;padding:.5rem;width:auto}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}} diff --git a/build/reactions/style-index.css b/build/reactions/style-index.css index 4cb5a88b2..f3054205e 100644 --- a/build/reactions/style-index.css +++ b/build/reactions/style-index.css @@ -1 +1 @@ -.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.wp-block-activitypub-reactions .reaction-avatars li{margin:0 -10px 0 0;padding:0}.wp-block-activitypub-reactions .reaction-avatars li:last-child{margin-right:0}.wp-block-activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.wp-block-activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.wp-block-activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em 1.3em .25em .7em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px} +.wp-block-activitypub-reactions{margin-bottom:2rem;margin-top:2rem;position:relative}.wp-block-activitypub-reactions .activitypub-reactions{display:flex;flex-direction:column;flex-wrap:wrap;gap:1rem;margin-top:16px}.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.5rem;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.wp-block-activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.wp-block-activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0!important;padding:0}.wp-block-activitypub-reactions .reaction-avatars li{margin:0 -10px 0 0;padding:0;transition:transform .2s ease}.wp-block-activitypub-reactions .reaction-avatars li:last-child{margin-right:0}.wp-block-activitypub-reactions .reaction-avatars li:hover{transform:translateY(-2px);z-index:2}.wp-block-activitypub-reactions .reaction-avatars li a{border-radius:50%;box-shadow:none;display:block;line-height:1;text-decoration:none}.wp-block-activitypub-reactions .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-avatar:focus-visible,.wp-block-activitypub-reactions .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-label{align-items:center;background:none;border:none;border-radius:4px;color:var(--wp--preset--color--contrast,#2271b1);display:flex;flex:0 0 auto;font-size:70%;gap:.25rem;margin-left:12px;padding:.25rem .5rem;text-decoration:none;transition:background-color .2s ease;white-space:nowrap}.wp-block-activitypub-reactions .reaction-label:hover{background-color:rgba(0,0,0,.05);color:var(--wp--preset--color--contrast,#135e96)}.wp-block-activitypub-reactions .reaction-label:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-modal__overlay{align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:initial;display:flex;justify-content:center;left:0;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;left:auto;padding:0;position:absolute;right:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--wp--preset--color--white,#fff);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,.3);display:flex;flex-direction:column;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--wp--preset--color--light-gray,#f0f0f0);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;border:none;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.reactions-list{list-style:none;margin:0!important;padding:.5rem}.reactions-list .reaction-item{margin:0 0 .5rem}.reactions-list .reaction-item:last-child{margin-bottom:0}.reactions-list .reaction-item a{align-items:center;border-radius:4px;box-shadow:none;color:inherit;display:flex;gap:.75rem;padding:.5rem;text-decoration:none;transition:background-color .2s ease}.reactions-list .reaction-item a:hover{background-color:rgba(0,0,0,.03)}.reactions-list .reaction-item img{border:1px solid #f0f0f0;border-radius:50%;box-shadow:none;height:36px;width:36px}.reactions-list .reaction-item .reaction-name{font-size:75%}body.modal-open{overflow:hidden}.components-popover__content{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;padding:.5rem;width:auto}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}} diff --git a/build/reactions/view.asset.php b/build/reactions/view.asset.php index e74a40674..cb2c46c11 100644 --- a/build/reactions/view.asset.php +++ b/build/reactions/view.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => '8709d255c0c51cead818'); + array('@wordpress/interactivity'), 'version' => '9e4598b81d1765ad8fed', 'type' => 'module'); diff --git a/build/reactions/view.js b/build/reactions/view.js index 3fc7a56ea..a6c8cb030 100644 --- a/build/reactions/view.js +++ b/build/reactions/view.js @@ -1 +1 @@ -(()=>{"use strict";var e={n:t=>{var r=t&&t.__esModule?()=>t.default:()=>t;return e.d(r,{a:r}),r},d:(t,r)=>{for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,r=window.wp.element,n=window.wp.domReady;var a=e.n(n);const l=window.wp.components,c=window.wp.apiFetch;var o=e.n(c);function s(){return window._activityPubOptions||{}}window.wp.i18n;const i=({reactions:e})=>{const{defaultAvatarUrl:r}=s();return(0,t.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const a=["reaction-avatar"].filter(Boolean).join(" "),l=e.avatar||r;return(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer"},(0,t.createElement)("img",{src:l,alt:e.name,className:a,width:"32",height:"32",onError:e=>{e.target.src=r}})))})))},u=({reactions:e,type:r})=>{const{defaultAvatarUrl:n}=s();return(0,t.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,r)=>{const a=e.avatar||n;return(0,t.createElement)("li",{key:r},(0,t.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,t.createElement)("img",{src:a,alt:e.name,width:"32",height:"32",onError:e=>{e.target.src=n}}),(0,t.createElement)("span",null,e.name)))})))},m=({items:e,label:n})=>{const[a,c]=(0,r.useState)(!1),[o,s]=(0,r.useState)(null),[m,d]=(0,r.useState)(e.length),p=(0,r.useRef)(null);(0,r.useEffect)((()=>{if(!p.current)return;const t=()=>{const t=p.current;if(!t)return;const r=t.offsetWidth-(o?.offsetWidth||0)-12,n=Math.max(1,Math.floor((r-32)/22));d(Math.min(n,e.length))};t();const r=new ResizeObserver(t);return r.observe(p.current),()=>{r.disconnect()}}),[o,e.length]);const f=e.slice(0,m);return(0,t.createElement)("div",{className:"reaction-group",ref:p},(0,t.createElement)(i,{reactions:f}),(0,t.createElement)(l.Button,{ref:s,className:"reaction-label is-link",onClick:()=>c(!a),"aria-expanded":a},n),a&&o&&(0,t.createElement)(l.Popover,{anchor:o,onClose:()=>c(!1)},(0,t.createElement)(u,{reactions:e})))};function d({postId:e=null,reactions:n=null}){const{namespace:a}=s(),[l,c]=(0,r.useState)(n),[i,u]=(0,r.useState)(!n);return(0,r.useEffect)((()=>{if(n)return c(n),void u(!1);e?(u(!0),o()({path:`/${a}/posts/${e}/reactions`}).then((e=>{c(e),u(!1)})).catch((()=>u(!1)))):u(!1)}),[e,n]),i?null:l&&Object.values(l).some((e=>e.items?.length>0))?(0,t.createElement)(t.Fragment,null,Object.entries(l).map((([e,r])=>r.items?.length?(0,t.createElement)(m,{key:e,items:r.items,label:r.label}):null))):null}a()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-reactions-block"),(e=>{const n=JSON.parse(e.dataset.attrs||e.parentElement.dataset.attrs);(0,r.createRoot)(e).render((0,t.createElement)(d,{...n}))}))}))})(); \ No newline at end of file +import*as t from"@wordpress/interactivity";var e={d:(t,o)=>{for(var n in o)e.o(o,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:o[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)};const o=(i={getContext:()=>t.getContext,store:()=>t.store,withScope:()=>t.withScope},s={},e.d(s,i),s),{apiFetch:n}=window.wp,{actions:c,callbacks:a,state:r}=(0,o.store)("activitypub/reactions",{actions:{async fetchReactions(){const t=(0,o.getContext)(),{namespace:e}=r;if(t.postId)try{t.reactions=await n({path:`/${e}/posts/${t.postId}/reactions`})}catch(t){console.error("Error fetching reactions:",t)}},openModal({target:t}){const e=(0,o.getContext)(),n=t.closest("[data-reaction-type]").getAttribute("data-reaction-type");e.isModalOpen=!0,e.modal.items=r.reactions[e.postId][n].items,setTimeout(a.positionModal,0)},closeModal(){const t=(0,o.getContext)();t.isModalOpen=!1;const e=document.getElementById(t.blockId);if(e){const t=e.querySelector(".reaction-label");t&&t.focus()}},toggleModal(t){const{isModalOpen:e}=(0,o.getContext)();e?c.closeModal():c.openModal(t)}},callbacks:{calculateVisibleAvatars(){const{blockId:t,postId:e}=(0,o.getContext)();(r.reactions&&r.reactions[e]?Object.keys(r.reactions[e]):[]).forEach((o=>{r.reactions?.[e][o]?.items?.length&&document.getElementById(t).querySelectorAll(".reaction-group").forEach((t=>{const n=t.querySelector(".reaction-label").offsetWidth||0,c=t.offsetWidth-n-12;let a=1;c>32&&(a+=Math.floor((c-32)/22));const i=r.reactions[e][o].items,s=Math.min(a,i.length),l=t.querySelector(".reaction-avatars");l&&l.querySelectorAll("li").forEach(((t,e)=>{e{const{blockId:t}=(0,o.getContext)(),e=new ResizeObserver((0,o.withScope)(a.calculateVisibleAvatars)),n=document.getElementById(t);n&&n.querySelectorAll(".reaction-group").forEach((t=>{e.observe(t)}))})),10)},setDefaultAvatar(t){t.target.src=r.defaultAvatarUrl},documentKeydown({key:t}){const{isModalOpen:e}=(0,o.getContext)();e&&"Escape"===t&&c.closeModal()},documentClick(t){const{blockId:e,isModalOpen:n}=(0,o.getContext)();if(!n)return;const a=document.getElementById(e);if(!a)return;const r=a.querySelector('.wp-element-button[data-wp-on--click="actions.toggleModal"]');if(r&&(r===t.target||r.contains(t.target)))return;const i=a.querySelector(".activitypub-modal__frame");i&&!i.contains(t.target)&&c.closeModal()},positionModal(){const{blockId:t}=(0,o.getContext)(),e=document.getElementById(t);if(!e)return;const n=e.querySelector(".reaction-label");if(!n)return;const c=e.querySelector(".activitypub-modal__overlay");if(!c)return;c.style.top="",c.style.left="",c.style.right="",c.style.bottom="";const a=n.getBoundingClientRect(),r=window.innerWidth,i=e.getBoundingClientRect();let s={top:a.bottom-i.top+8+"px",left:a.left-i.left-2+"px"};r-a.right<250&&(s.left="auto",s.right=i.right-a.right+"px"),Object.assign(c.style,s)}}});var i,s; \ No newline at end of file diff --git a/includes/class-blocks.php b/includes/class-blocks.php index 216e380e3..a0d221621 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -161,12 +161,7 @@ public static function register_blocks() { ) ); - \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/reactions', - array( - 'render_callback' => array( self::class, 'render_post_reactions_block' ), - ) - ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/reactions' ); } /** diff --git a/src/follow-me/style.scss b/src/follow-me/style.scss index 54457d580..99c528988 100644 --- a/src/follow-me/style.scss +++ b/src/follow-me/style.scss @@ -97,7 +97,7 @@ body.modal-open { max-width: 660px; width: 100%; background-color: var(--wp--preset--color--white); - border-radius: 4px; + border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); overflow: hidden; display: flex; diff --git a/src/reactions/block.json b/src/reactions/block.json index 8b3f1f9ce..70bbb37a0 100644 --- a/src/reactions/block.json +++ b/src/reactions/block.json @@ -1,29 +1,24 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", "name": "activitypub/reactions", - "apiVersion": 2, - "version": "2.0.0", + "apiVersion": 3, + "version": "3.0.0", "title": "Fediverse Reactions", "category": "widgets", "icon": "heart", "description": "Display Fediverse likes and reposts", "supports": { "html": false, - "align": true, - "layout": { - "default": { - "type": "constrained", - "orientation": "vertical", - "justifyContent": "center" - } - } + "align": [ "wide", "full" ], + "interactivity": true }, - "attributes": {}, "blockHooks": { "core/post-content": "after" }, "textdomain": "activitypub", "editorScript": "file:./index.js", - "style": [ "file:./style-index.css", "wp-components" ], - "viewScript": "file:./view.js" + "style": "file:./style-index.css", + "viewScriptModule": "file:./view.js", + "viewScript": "wp-api-fetch", + "render": "file:./render.php" } diff --git a/src/reactions/deprecation.js b/src/reactions/deprecation.js index 7fcea84b2..dcf98606c 100644 --- a/src/reactions/deprecation.js +++ b/src/reactions/deprecation.js @@ -10,18 +10,31 @@ const v1 = { supports: { html: false, - align: true, - layout: { - default: { - type: 'constrained', - orientation: 'vertical', - justifyContent: 'center', + 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, }, }, }, isEligible( attributes ) { - // Run migration if title attribute exists. + // Run migration if the title attribute exists. return !! attributes.title; }, diff --git a/src/reactions/edit.js b/src/reactions/edit.js index 25c42d7c4..c0e5698a5 100644 --- a/src/reactions/edit.js +++ b/src/reactions/edit.js @@ -1,120 +1,33 @@ import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; -import { useState } from '@wordpress/element'; import { __, _x, sprintf } from '@wordpress/i18n'; +import { select } from '@wordpress/data'; import { Reactions } from './reactions'; +import './style.scss'; -/** - * Generate a whimsical name using an adjective and noun combination. - * - * @return {string} A whimsical name. - */ -const generateWhimsicalName = () => { - const adjectives = [ - 'Bouncy', - 'Cosmic', - 'Dancing', - 'Fluffy', - 'Giggly', - 'Hoppy', - 'Jazzy', - 'Magical', - 'Nifty', - 'Perky', - 'Quirky', - 'Sparkly', - 'Twirly', - 'Wiggly', - 'Zippy', - ]; - const nouns = [ - 'Badger', - 'Capybara', - 'Dolphin', - 'Echidna', - 'Flamingo', - 'Giraffe', - 'Hedgehog', - 'Iguana', - 'Jellyfish', - 'Koala', - 'Lemur', - 'Manatee', - 'Narwhal', - 'Octopus', - 'Penguin', - ]; - - const adjective = - adjectives[ Math.floor( Math.random() * adjectives.length ) ]; - const noun = nouns[ Math.floor( Math.random() * nouns.length ) ]; - - return `${ adjective } ${ noun }`; -}; - -/** - * Generate a dummy reaction with a random letter and color. - * - * @param {number} index Index for color selection. - * @return {Object} Reaction object. - */ -const generateDummyReaction = ( index ) => { - const colors = [ - '#FF6B6B', // Coral - '#4ECDC4', // Turquoise - '#45B7D1', // Sky Blue - '#96CEB4', // Sage - '#FFEEAD', // Cream - '#D4A5A5', // Dusty Rose - '#9B59B6', // Purple - '#3498DB', // Blue - '#E67E22', // Orange - ]; - - const name = generateWhimsicalName(); - const color = colors[ Math.floor( Math.random() * colors.length ) ]; - const letter = name.charAt( 0 ); - - // Create a data URL for a colored circle with a letter. - const canvas = document.createElement( 'canvas' ); - canvas.width = 64; - canvas.height = 64; - const ctx = canvas.getContext( '2d' ); - - // Draw colored circle. - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc( 32, 32, 32, 0, 2 * Math.PI ); - ctx.fill(); - - // Draw letter. - ctx.fillStyle = '#FFFFFF'; - ctx.font = '32px sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText( letter, 32, 32 ); - - return { - name, +// Generate reaction items with SVG avatars. +const generateReactionItems = ( count, prefix, startChar, colors ) => + Array.from( { length: count }, ( _, i ) => ( { + name: `${ prefix } ${ i + 1 }`, url: '#', - avatar: canvas.toDataURL(), - }; -}; + avatar: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='32' fill='%23${ + colors[ i % colors.length ] + }'/%3E%3Ctext x='32' y='38' font-family='sans-serif' font-size='24' fill='white' text-anchor='middle'%3E${ String.fromCharCode( + startChar + i + ) }%3C/text%3E%3C/svg%3E`, + } ) ); -/** - * Generate dummy reactions for editor preview. - * - * @return {Object} Reactions data. - */ -const generateDummyReactions = () => ( { +// Colors for avatars. +const COLORS = [ 'FF6B6B', '4ECDC4', '45B7D1', '96CEB4', 'D4A5A5', '9B59B6', '3498DB', 'E67E22' ]; + +// Simple predefined dummy Reactions data. +const DUMMY_REACTIONS = { likes: { label: sprintf( /* translators: %d: Number of likes */ _x( '%d likes', 'number of likes', 'activitypub' ), 9 ), - items: Array.from( { length: 9 }, ( _, i ) => - generateDummyReaction( i ) - ), + items: generateReactionItems( 9, 'User', 65, COLORS ), // 65 is ASCII for 'A' }, reposts: { label: sprintf( @@ -122,28 +35,24 @@ const generateDummyReactions = () => ( { _x( '%d reposts', 'number of reposts', 'activitypub' ), 6 ), - items: Array.from( { length: 6 }, ( _, i ) => - generateDummyReaction( i + 9 ) - ), + items: generateReactionItems( 6, 'Reposter', 82, COLORS ), // 82 is ASCII for 'R' }, -} ); +}; /** * Edit component for the Reactions block. * - * @param {Object} props Block props. - * @param {Object} props.attributes Block attributes. - * @param {Function} props.setAttributes Attribute update callback. - * @param props.__unstableLayoutClassNames - * @return {JSX.Element} Component to render. + * @param {Object} props Block props. + * @param props.__unstableLayoutClassNames Layout class names. + * @return {JSX.Element} Component to render. */ -export default function Edit( { attributes, __unstableLayoutClassNames } ) { +export default function Edit( { __unstableLayoutClassNames } ) { const blockProps = useBlockProps( { className: __unstableLayoutClassNames, } ); - const [ dummyReactions ] = useState( generateDummyReactions() ); + const { getCurrentPostId } = select( 'core/editor' ); - // Template for InnerBlocks - allows only a heading block + // Template for InnerBlocks - allows only a heading block. const TEMPLATE = [ [ 'core/heading', @@ -163,7 +72,7 @@ export default function Edit( { attributes, __unstableLayoutClassNames } ) { templateLock={ 'all' } renderAppender={ false } /> - + ); } diff --git a/src/reactions/editor.scss b/src/reactions/editor.scss deleted file mode 100644 index b4efee0ea..000000000 --- a/src/reactions/editor.scss +++ /dev/null @@ -1,4 +0,0 @@ -.wp-block-activitypub-reactions { - // Any editor-specific styles would go here - // For now, we'll inherit all styles from style.scss -} \ No newline at end of file diff --git a/src/reactions/reactions.js b/src/reactions/reactions.js index 4482804fb..cc604f4e0 100644 --- a/src/reactions/reactions.js +++ b/src/reactions/reactions.js @@ -1,12 +1,13 @@ -/** - * WordPress dependencies - */ import { useState, useEffect, useRef } from '@wordpress/element'; import { Popover, Button } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; -import { __ } from '@wordpress/i18n'; import { useOptions } from '../shared/use-options'; +/** + * @typedef {Object} JSX + * @typedef {import('react').ReactElement} JSX.Element + */ + /** * A component that renders a row of user avatars for a given set of reactions. * @@ -20,27 +21,21 @@ const FacepileRow = ( { reactions } ) => { return (
    { reactions.map( ( reaction, index ) => { - const classes = [ - 'reaction-avatar', - ] - .filter( Boolean ) - .join( ' ' ); + const classes = [ 'reaction-avatar' ].filter( Boolean ).join( ' ' ); const avatar = reaction.avatar || defaultAvatarUrl; return (
  • - + { { e.target.src = defaultAvatarUrl; } } + onError={ ( e ) => { + e.target.src = defaultAvatarUrl; + } } />
  • @@ -55,32 +50,28 @@ const FacepileRow = ( { reactions } ) => { * * @param {Object} props Component props. * @param {Array} props.reactions Array of reaction objects. - * @param {string} props.type Type of reaction (likes/reposts). - * @return {JSX.Element} The rendered component. + * @return {JSX.Element} The rendered component. */ -const ReactionList = ( { reactions, type } ) => { +const ReactionList = ( { reactions } ) => { const { defaultAvatarUrl } = useOptions(); return ( -
      +
        { reactions.map( ( reaction, index ) => { const avatar = reaction.avatar || defaultAvatarUrl; return ( -
      • - +
      • + { { e.target.src = defaultAvatarUrl; } } + onError={ ( e ) => { + e.target.src = defaultAvatarUrl; + } } /> - { reaction.name } + { reaction.name }
      • ); @@ -100,56 +91,9 @@ const ReactionList = ( { reactions, type } ) => { const ReactionGroup = ( { items, label } ) => { const [ isOpen, setIsOpen ] = useState( false ); const [ buttonRef, setButtonRef ] = useState( null ); - const [ visibleCount, setVisibleCount ] = useState( items.length ); const containerRef = useRef( null ); - // Constants for calculations - const AVATAR_WIDTH = 32; // Width of each avatar - const AVATAR_OVERLAP = 10; // How much each avatar overlaps - const EFFECTIVE_AVATAR_WIDTH = AVATAR_WIDTH - AVATAR_OVERLAP; // Width each additional avatar takes - const BUTTON_GAP = 12; // Gap between avatars and button (0.75em) - - useEffect( () => { - if ( ! containerRef.current ) { - return; - } - - const calculateVisibleAvatars = () => { - const container = containerRef.current; - if ( ! container ) { - return; - } - - const containerWidth = container.offsetWidth; - const labelWidth = buttonRef?.offsetWidth || 0; - const availableWidth = containerWidth - labelWidth - BUTTON_GAP; - - // Calculate how many avatars can fit - // First avatar takes full width, rest take effective width - const maxAvatars = Math.max( - 1, - Math.floor( - ( availableWidth - AVATAR_WIDTH ) / EFFECTIVE_AVATAR_WIDTH - ) - ); - - // Ensure we don't show more than we have - setVisibleCount( Math.min( maxAvatars, items.length ) ); - }; - - // Initial calculation - calculateVisibleAvatars(); - - // Setup resize observer - const resizeObserver = new ResizeObserver( calculateVisibleAvatars ); - resizeObserver.observe( containerRef.current ); - - return () => { - resizeObserver.disconnect(); - }; - }, [ buttonRef, items.length ] ); - - const visibleItems = items.slice( 0, visibleCount ); + const visibleItems = items.slice( 0, 20 ); return (
        @@ -163,10 +107,7 @@ const ReactionGroup = ( { items, label } ) => { { label } { isOpen && buttonRef && ( - setIsOpen( false ) } - > + setIsOpen( false ) }> ) } @@ -178,14 +119,12 @@ const ReactionGroup = ( { items, label } ) => { * The Reactions component. * * @param {Object} props Component props. - * @param {?number} props.postId The post ID. + * @param {?number} props.postId The Post ID. * @param {?Object} props.reactions Optional reactions data. + * @param {?Object} props.fallbackReactions Optional fallback reactions data to use if no real reactions are found. * @return {?JSX.Element} The rendered component. */ -export function Reactions( { - postId = null, - reactions: providedReactions = null, -} ) { +export function Reactions( { postId = null, reactions: providedReactions = null, fallbackReactions = null } ) { const { namespace } = useOptions(); const [ reactions, setReactions ] = useState( providedReactions ); const [ loading, setLoading ] = useState( ! providedReactions ); @@ -207,23 +146,32 @@ export function Reactions( { path: `/${ namespace }/posts/${ postId }/reactions`, } ) .then( ( response ) => { - setReactions( response ); + // Check if the response has any actual reactions + const hasReactions = Object.values( response ).some( ( group ) => group.items?.length > 0 ); + + // If there are no real reactions and fallback is provided, use the fallback + if ( ! hasReactions && fallbackReactions ) { + setReactions( fallbackReactions ); + } else { + setReactions( response ); + } setLoading( false ); } ) - .catch( () => setLoading( false ) ); - }, [ postId, providedReactions ] ); + .catch( () => { + // On error, use fallback reactions if provided + if ( fallbackReactions ) { + setReactions( fallbackReactions ); + } + setLoading( false ); + } ); + }, [ postId, providedReactions, fallbackReactions, namespace ] ); if ( loading ) { return null; } // Return null if there are no reactions - if ( - ! reactions || - ! Object.values( reactions ).some( - ( group ) => group.items?.length > 0 - ) - ) { + if ( ! reactions || ! Object.values( reactions ).some( ( group ) => group.items?.length > 0 ) ) { return null; } @@ -234,13 +182,7 @@ export function Reactions( { return null; } - return ( - - ); + return ; } ) } ); diff --git a/src/reactions/render.php b/src/reactions/render.php new file mode 100644 index 000000000..e4c4ca1bc --- /dev/null +++ b/src/reactions/render.php @@ -0,0 +1,216 @@ + null ) ); + +/* @var string $content Inner blocks content. */ +if ( empty( $content ) ) { + // Fallback for v1.0.0 blocks. + $_title = $attributes['title'] ?? __( 'Fediverse Reactions', 'activitypub' ); + $content = '
        ' . esc_html( $_title ) . '
        '; + unset( $attributes['title'], $attributes['className'] ); +} + +// Get the Post ID from attributes or use the current post. +$_post_id = $attributes['postId'] ?? get_the_ID(); + +// Generate a unique ID for the block. +$block_id = 'activitypub-reactions-block-' . wp_unique_id(); + +$reactions = array(); + +foreach ( Comment::get_comment_types() as $_type => $type_object ) { + $_comments = get_comments( + array( + 'post_id' => $_post_id, + 'type' => $_type, + 'status' => 'approve', + ) + ); + + if ( empty( $_comments ) ) { + continue; + } + + $count = count( $_comments ); + // phpcs:disable WordPress.WP.I18n + $label = sprintf( + _n( + $type_object['count_single'], + $type_object['count_plural'], + $count, + 'activitypub' + ), + number_format_i18n( $count ) + ); + // phpcs:enable WordPress.WP.I18n + + $reactions[ $_type ] = array( + 'label' => $label, + 'count' => $count, + 'items' => array_map( + function ( $comment ) { + return array( + 'id' => $comment->comment_ID, + 'name' => $comment->comment_author, + 'url' => $comment->comment_author_url, + 'avatar' => get_comment_meta( $comment->comment_ID, 'avatar_url', true ), + ); + }, + $_comments + ), + ); +} + +// Set up the Interactivity API state. +wp_interactivity_state( + 'activitypub/reactions', + array( + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'reactions' => array( + $_post_id => $reactions, + ), + ) +); + +// Render a subset of the most recent reactions. +$reactions = array_map( + function ( $reaction ) use ( $attributes ) { + $count = 20; + if ( 'wide' === $attributes['align'] ) { + $count = 40; + } elseif ( 'full' === $attributes['align'] ) { + $count = 60; + } + + $reaction['items'] = array_slice( array_reverse( $reaction['items'] ), 0, $count ); + + return $reaction; + }, + $reactions +); + +// Initialize the context for the block. +$context = array( + 'blockId' => $block_id, + 'hasReactions' => ! empty( $reactions ), + 'reactions' => $reactions, + 'postId' => $_post_id, + 'isModalOpen' => false, + 'modal' => array( + 'items' => array(), + ), +); + +// Add the block wrapper attributes. +$wrapper_attributes = get_block_wrapper_attributes( + array( + 'id' => $block_id, + 'data-wp-interactive' => 'activitypub/reactions', + 'data-wp-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wp-init' => 'callbacks.initReactions', + 'data-wp-on-document--keydown' => 'callbacks.documentKeydown', + 'data-wp-on-document--click' => 'callbacks.documentClick', + 'data-wp-bind--hidden' => '!context.hasReactions', + ) +); +?> + +
        > + + +
        + $reaction ) : + /* translators: %s: reaction type. */ + $aria_label = sprintf( __( 'View all %s', 'activitypub' ), Comment::get_comment_type_attr( $_type, 'label' ) ); + ?> +
        +
          + +
        + +
        + +
        + + +
        diff --git a/src/reactions/save.js b/src/reactions/save.js index 75a6b6401..3fb25192e 100644 --- a/src/reactions/save.js +++ b/src/reactions/save.js @@ -1,10 +1,22 @@ -import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; +/** + * @typedef {Object} InnerBlocks + * @property {function(): JSX.Element} Content - The InnerBlocks.Content component. + */ + +/** + * Save function for the reactions block. + * + * With server-side rendering via render.php, we only need to output + * the InnerBlocks content and a placeholder div. + * + * @return {JSX.Element} React element to save. + */ export default function save() { return ( - <> +
        -
        - +
        ); } diff --git a/src/reactions/style.scss b/src/reactions/style.scss index c43cdd5ce..942e87e54 100644 --- a/src/reactions/style.scss +++ b/src/reactions/style.scss @@ -1,14 +1,28 @@ .wp-block-activitypub-reactions { + margin-top: 2rem; + margin-bottom: 2rem; + position: relative; + + // Main container for reactions. + .activitypub-reactions { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 1rem; + margin-top: 16px; + } + + // Reaction group for each type (likes, reposts). .reaction-group { display: flex; align-items: center; margin: 0.5em 0; position: relative; width: 100%; - gap: 0.75em; + gap: 0.5rem; justify-content: flex-start; - // When content overflows, switch to space-between + // When content overflows, switch to space-between. &:has(.reaction-avatars:not(:empty)) { @media (max-width: 782px) { justify-content: space-between; @@ -16,110 +30,249 @@ } } + // Container for avatar images. .reaction-avatars { display: flex; flex-direction: row; align-items: center; list-style: none; - margin: 0; + margin: 0 !important; padding: 0; li { - margin: 0; padding: 0; - margin-right: -10px; + margin: 0 -10px 0 0; + transition: transform 0.2s ease; &:last-child { margin-right: 0; } + &:hover { + z-index: 2; + transform: translateY(-2px); + } + a { + box-shadow: none; + border-radius: 50%; display: block; + line-height: 1; text-decoration: none; } } + } - .reaction-avatar { - max-width: 32px; - max-height: 32px; - overflow: hidden; - -moz-force-broken-image-icon: 1; - border-radius: 50%; - border: 0.5px solid var( --wp--preset--color--contrast, rgba(255, 255, 255, 0.8) ); - box-shadow: - 0 0 0 0.5px rgba(255, 255, 255, 0.8), // Crisp white border - 0 1px 3px rgba(0, 0, 0, 0.2); // Soft drop shadow - transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); - will-change: transform; - - &:hover, - &:focus-visible { - z-index: 1; - position: relative; - transform: translateY(-5px); - } + .reaction-avatar { + max-width: 32px; + max-height: 32px; + overflow: hidden; + -moz-force-broken-image-icon: 1; + border-radius: 50%; + border: 0.5px solid var( --wp--preset--color--contrast, rgba(255, 255, 255, 0.8) ); + box-shadow: + 0 0 0 0.5px rgba(255, 255, 255, 0.8), // Crisp white border + 0 1px 3px rgba(0, 0, 0, 0.2); // Soft drop shadow + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + will-change: transform; + + &:hover, + &:focus-visible { + z-index: 1; + position: relative; + transform: translateY(-5px); } } - .reaction-label.components-button { + // Label showing count of reactions. + .reaction-label { + background: none; + border: none; + border-radius: 4px; + display: flex; + align-items: center; + gap: 0.25rem; + flex: 0 0 auto; + margin-left: 12px; + padding: 0.25rem 0.5rem; + font-size: 70%; + color: var(--wp--preset--color--contrast, #2271b1); + transition: background-color 0.2s ease; white-space: nowrap; - height: auto; - padding: 0; text-decoration: none; - color: var( --wp--preset--color--contrast, --wp--preset--color--secondary, #2271b1 ); - flex: 0 0 auto; &:hover { - color: var( --wp--preset--color--contrast, --wp--preset--color--secondary, #135e96 ); - text-decoration: underline; + background-color: rgba(0, 0, 0, 0.05); + color: var(--wp--preset--color--contrast, #135e96); } &:focus:not(:disabled) { box-shadow: none; - outline: 1px solid var( --wp--preset--color--contrast, #135e96 ); + outline: 1px solid var(--wp--preset--color--contrast, #135e96); outline-offset: 2px; } } } -.activitypub-reaction-list { - margin: 0; - padding: .25em 1.3em .25em 0.7em; - list-style: none; - width: max-content; - max-width: 300px; - background-color: var( --wp--preset--color--background, var( --wp--preset--color--custom-background, var( --wp--preset--color--base ) ) ); +/* Modal styles */ +.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; - ul { - margin: 0; - padding: 0; + &.compact { + position: absolute; + background-color: transparent; + padding: 0; + top: auto; + left: auto; + right: auto; + bottom: auto; + align-items: flex-start; + justify-content: flex-start; + z-index: 100; + } + + &[hidden] { + display: none; + } } - li { - margin: 0; - padding: 0; - font-size: var( --wp--preset--font-size--small ); + &__frame { + max-width: 660px; + width: 100%; + background-color: var(--wp--preset--color--white, #fff); + border-radius: 8px; + 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; + + .compact & { + width: auto; + max-width: min-content; + min-width: 250px; + max-height: 300px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } } - a { + &__header { display: flex; + justify-content: space-between; align-items: center; - justify-content: flex-start; - gap: .5em; - padding: .5em; - text-decoration: none; - font-size: var( --wp--preset--font-size--small, .75rem ); - color: var( --wp--preset--color--contrast, var( --wp--preset--color--secondary ) ); + padding: 2rem 2rem 1.5rem 2rem; + border-bottom: 1px solid var(--wp--preset--color--light-gray, #f0f0f0); + flex-shrink: 0; - &:hover { - text-decoration: underline; + .compact & { + display: none; + } + + .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; + } +} + +/* Reactions list styles */ +.reactions-list { + list-style: none; + margin: 0 !important; + padding: 0.5rem; + + .reaction-item { + margin: 0 0 0.5rem 0; + + &:last-child { + margin-bottom: 0; + } + + a { + box-shadow: none; + display: flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; + color: inherit; + transition: background-color 0.2s ease; + padding: 0.5rem; + border-radius: 4px; + + &:hover { + background-color: rgba(0, 0, 0, 0.03); + } } img { - width: 24px; - height: 24px; + box-shadow: none; + width: 36px; + height: 36px; border-radius: 50%; - flex: none; + border: 1px solid #f0f0f0; + } + + .reaction-name { + font-size: 75%; } } } + +body.modal-open { + overflow: hidden; +} + +.components-popover__content { + width: auto; + max-width: min-content; + min-width: 250px; + max-height: 300px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 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/reactions/view.js b/src/reactions/view.js index e191678bf..63dc58b80 100644 --- a/src/reactions/view.js +++ b/src/reactions/view.js @@ -1,14 +1,279 @@ -import { createRoot } from '@wordpress/element'; -import domReady from '@wordpress/dom-ready'; -import { Reactions } from './reactions'; - -domReady( () => { - // iterate over a nodelist - [].forEach.call( - document.querySelectorAll( '.activitypub-reactions-block' ), - ( element ) => { - const attrs = JSON.parse( element.dataset.attrs || element.parentElement.dataset.attrs ); - createRoot( element ).render( ); - } - ); +import { store, getContext, withScope } from '@wordpress/interactivity'; + +/** @var {Object} window.wp WordPress global object */ +const { apiFetch } = window.wp; + +/** + * @var {Object} state + * @var {Object} state.reactions Reactions data. + * @var {String} state.defaultAvatarUrl Default avatar URL. + */ +const { actions, callbacks, state } = store( 'activitypub/reactions', { + actions: { + /** + * Fetches reactions for a post. + */ + async fetchReactions() { + const context = getContext(); + const { namespace } = state; + + if ( ! context.postId ) return; + + try { + // Update the state with the new Reactions data. + context.reactions = await apiFetch( { + path: `/${ namespace }/posts/${ context.postId }/reactions`, + } ); + } catch ( error ) { + console.error( 'Error fetching reactions:', error ); + } + }, + + /** + * Opens the modal with the specified reaction type. + * + * @param {Object} event The click event. + */ + openModal( { target } ) { + const context = getContext(); + const button = target.closest( '[data-reaction-type]' ); + const reactionType = button.getAttribute( 'data-reaction-type' ); + + // Set modal properties. + context.isModalOpen = true; + context.modal.items = state.reactions[ context.postId ][ reactionType ].items; + + // Position the compact modal relative to the button. + setTimeout( callbacks.positionModal, 0 ); + }, + + /** + * Closes the reactions modal. + */ + closeModal() { + const context = getContext(); + + context.isModalOpen = false; + + // Return focus to the button that opened the modal. + const blockWrapper = document.getElementById( context.blockId ); + if ( blockWrapper ) { + const openButton = blockWrapper.querySelector( '.reaction-label' ); + if ( openButton ) { + openButton.focus(); + } + } + }, + + /** + * Toggles the modal open or closed based on its current state. + * + * @param {Object} event The click event. + */ + toggleModal( event ) { + const { isModalOpen } = getContext(); + + isModalOpen ? actions.closeModal() : actions.openModal( event ); + }, + }, + callbacks: { + /** + * Calculates and sets the number of visible avatars based on container width. + */ + calculateVisibleAvatars() { + const { blockId, postId } = getContext(); + + // Constants for calculations + const AVATAR_WIDTH = 32; // Width of each avatar + const AVATAR_OVERLAP = 10; // How much each avatar overlaps + const EFFECTIVE_AVATAR_WIDTH = AVATAR_WIDTH - AVATAR_OVERLAP; // Width each additional avatar takes + const BUTTON_GAP = 12; // Gap between avatars and button (0.75em) + + // Get all reaction types from the state. + const reactionTypes = + state.reactions && state.reactions[ postId ] ? Object.keys( state.reactions[ postId ] ) : []; + + // Process each reaction group. + reactionTypes.forEach( ( reactionType ) => { + if ( ! state.reactions?.[ postId ][ reactionType ]?.items?.length ) { + return; + } + + document + .getElementById( blockId ) + .querySelectorAll( '.reaction-group' ) + .forEach( ( container ) => { + const label = container.querySelector( '.reaction-label' ); + const labelWidth = label.offsetWidth || 0; + const availableWidth = container.offsetWidth - labelWidth - BUTTON_GAP; + + // Calculate how many avatars can fit. + // The first avatar takes full width, the rest take effective width. + let maxAvatars = 1; // Start with 1 for the first avatar. + + // If we have space for more than one avatar. + if ( availableWidth > AVATAR_WIDTH ) { + // Calculate how many additional avatars can fit in the remaining space. + maxAvatars += Math.floor( ( availableWidth - AVATAR_WIDTH ) / EFFECTIVE_AVATAR_WIDTH ); + } + + // Ensure we don't show more than we have. + const items = state.reactions[ postId ][ reactionType ].items; + const visibleCount = Math.min( maxAvatars, items.length ); + + // Update the DOM to show only the calculated number of avatars. + const avatarsList = container.querySelector( '.reaction-avatars' ); + if ( avatarsList ) { + const avatarItems = avatarsList.querySelectorAll( 'li' ); + avatarItems.forEach( ( item, index ) => { + if ( index < visibleCount ) { + item.removeAttribute( 'hidden' ); + } else { + item.setAttribute( 'hidden', 'hidden' ); + } + } ); + } + } ); + } ); + }, + + /** + * Initializes the Reactions component. + */ + initReactions() { + // Calculate visible avatars after the component is initialized. + setTimeout( + withScope( () => { + const { blockId } = getContext(); + + // Set up resize observer to recalculate on window resize. + const resizeObserver = new ResizeObserver( withScope( callbacks.calculateVisibleAvatars ) ); + + // Observe both reaction groups. + const blockWrapper = document.getElementById( blockId ); + if ( blockWrapper ) { + blockWrapper.querySelectorAll( '.reaction-group' ).forEach( ( group ) => { + resizeObserver.observe( group ); + } ); + } + } ), + 10 + ); + }, + + /** + * Sets the default avatar when the avatar image fails to load. + * + * @param {Object} event The error event. + */ + setDefaultAvatar( event ) { + event.target.src = state.defaultAvatarUrl; + }, + + /** + * Close modal when pressing the ESC key. + * + * @param {String} key Keyboard event key. + */ + documentKeydown( { key } ) { + const { isModalOpen } = getContext(); + + if ( isModalOpen && 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(); + }, + + /** + * Positions the modal relative to the button that opened it. + */ + positionModal() { + const { blockId } = getContext(); + + const blockWrapper = document.getElementById( blockId ); + if ( ! blockWrapper ) { + return; + } + + const button = blockWrapper.querySelector( '.reaction-label' ); + if ( ! button ) { + return; + } + + const modalOverlay = blockWrapper.querySelector( '.activitypub-modal__overlay' ); + if ( ! modalOverlay ) { + return; + } + + // Reset any previously set positioning. + modalOverlay.style.top = ''; + modalOverlay.style.left = ''; + modalOverlay.style.right = ''; + modalOverlay.style.bottom = ''; + + // Get button position relative to viewport. + const buttonRect = button.getBoundingClientRect(); + + // Get viewport dimensions. + const viewportWidth = window.innerWidth; + + // Get the block's position to calculate relative positioning. + const blockRect = blockWrapper.getBoundingClientRect(); + + // Calculate position relative to the block (our positioning context). + const relativeTop = buttonRect.bottom - blockRect.top; + const relativeLeft = buttonRect.left - blockRect.left; + + // Calculate available space. + const spaceRight = viewportWidth - buttonRect.right; + + // Default position (below button, relative to the block). + let position = { + top: `${ relativeTop + 8 }px`, + left: `${ relativeLeft - 2 }px`, // -2 px to account for the button border. + }; + + // If not enough space to the right, align with the right edge. + if ( spaceRight < 250 ) { + position.left = 'auto'; + position.right = `${ blockRect.right - buttonRect.right }px`; + } + + // Apply the position. + Object.assign( modalOverlay.style, position ); + }, + }, } ); diff --git a/tests/includes/class-test-blocks.php b/tests/includes/class-test-blocks.php index 8891b417c..6a0887909 100644 --- a/tests/includes/class-test-blocks.php +++ b/tests/includes/class-test-blocks.php @@ -202,49 +202,6 @@ public function test_filter_import_mastodon_post_data_with_reply() { $this->assertStringContainsString( "\n

        This is a reply

        \n", $result['post_content'] ); } - /** - * Test the reactions block with deprecated markup. - * - * @covers ::render_post_reactions_block - */ - public function test_render_reactions_block() { - $block_markup = ' - -

        Fediverse Custom

        -
        -'; - $output = do_blocks( $block_markup ); - $expected = '
        - - -
        -
        '; - - $this->assertSame( $expected, $output ); - - // Reactions block with reactions. - $post_id = $this->get_post_id_with_reactions(); - $block_markup = sprintf( - ' - -

        Fediverse Custom

        -
        -', - $post_id - ); - $output = do_blocks( $block_markup ); - $expected = sprintf( - '
        - -

        Fediverse Custom

        -
        -
        ', - $post_id - ); - - $this->assertSame( $expected, $output ); - } - /** * Test the reactions block with deprecated markup. * @@ -253,26 +210,22 @@ public function test_render_reactions_block() { public function test_render_reactions_block_with_deprecated_markup() { $block_markup = ''; $output = do_blocks( $block_markup ); - $expected = '
        -
        '; + $expected = '
        What people think about it on the Fediverse!
        '; - $this->assertSame( $expected, $output ); + $this->assertStringContainsString( $expected, $output ); $block_markup = ''; $output = do_blocks( $block_markup ); - $expected = '
        -
        '; + $expected = '
        Fediverse Reactions
        '; - $this->assertSame( $expected, $output ); + $this->assertStringContainsString( $expected, $output ); // Reactions block with reactions. $post_id = $this->get_post_id_with_reactions(); $block_markup = sprintf( '', $post_id ); $output = do_blocks( $block_markup ); - $expected = '
        Fediverse Reactions
        -
        '; - $this->assertSame( $expected, $output ); + $this->assertStringContainsString( $expected, $output ); } /**