├── .distignore ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── admin.css ├── common.js ├── location.css ├── location.js ├── mm.png ├── remove-extra-margin.css ├── webmention-icons.svg ├── webmention-legacy.js └── webmention.js ├── blocks ├── bookmark │ ├── block.asset.php │ ├── block.css │ ├── block.js │ └── block.json ├── context │ ├── block.asset.php │ ├── block.js │ ├── block.json │ └── editor.css ├── facepile-content │ ├── block.asset.php │ ├── block.css │ ├── block.js │ ├── block.json │ └── render.php ├── facepile │ ├── block.asset.php │ ├── block.js │ ├── block.json │ └── render.php ├── like │ ├── block.asset.php │ ├── block.css │ ├── block.js │ └── block.json ├── link-preview │ ├── block.asset.php │ ├── block.css │ ├── block.js │ ├── block.json │ └── render.php ├── location │ ├── block.asset.php │ ├── block.js │ ├── block.json │ └── render.php ├── reply │ ├── block.asset.php │ ├── block.css │ ├── block.js │ └── block.json ├── repost │ ├── block.asset.php │ ├── block.css │ ├── block.js │ └── block.json └── syndication │ ├── block.asset.php │ ├── block.js │ ├── block.json │ └── render.php ├── build └── vendor │ ├── autoload.php │ ├── composer │ ├── ClassLoader.php │ ├── InstalledVersions.php │ ├── LICENSE │ ├── autoload_classmap.php │ ├── autoload_files.php │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_real.php │ ├── autoload_static.php │ ├── installed.php │ └── platform_check.php │ ├── masterminds │ └── html5 │ │ ├── CREDITS │ │ └── src │ │ ├── HTML5.php │ │ └── HTML5 │ │ ├── Elements.php │ │ ├── Entities.php │ │ ├── Exception.php │ │ ├── InstructionProcessor.php │ │ ├── Parser │ │ ├── CharacterReference.php │ │ ├── DOMTreeBuilder.php │ │ ├── EventHandler.php │ │ ├── FileInputStream.php │ │ ├── InputStream.php │ │ ├── ParseError.php │ │ ├── Scanner.php │ │ ├── StringInputStream.php │ │ ├── Tokenizer.php │ │ ├── TreeBuildingRules.php │ │ └── UTF8Utils.php │ │ └── Serializer │ │ ├── HTML5Entities.php │ │ ├── OutputRules.php │ │ ├── RulesInterface.php │ │ └── Traverser.php │ ├── mf2 │ └── mf2 │ │ └── Mf2 │ │ └── Parser.php │ ├── michelf │ └── php-markdown │ │ ├── Michelf │ │ ├── Markdown.inc.php │ │ ├── Markdown.php │ │ ├── MarkdownExtra.inc.php │ │ ├── MarkdownExtra.php │ │ ├── MarkdownInterface.inc.php │ │ └── MarkdownInterface.php │ │ └── Readme.php │ ├── ralouphie │ └── mimey │ │ ├── license │ │ ├── mime.types │ │ ├── mime.types.php │ │ └── src │ │ ├── MimeMappingGenerator.php │ │ ├── MimeTypes.php │ │ └── MimeTypesInterface.php │ └── scoper-autoload.php ├── composer.json ├── composer.lock ├── includes ├── class-blocks.php ├── class-feeds.php ├── class-location.php ├── class-micropub-compat.php ├── class-options-handler.php ├── class-parser.php ├── class-plugin.php ├── class-post-types.php ├── class-preview-cards.php ├── class-theme-mf2.php ├── commands │ └── class-commands.php ├── functions.php └── webmention │ ├── class-webmention-parser.php │ ├── class-webmention-receiver.php │ ├── class-webmention-sender.php │ └── class-webmention.php ├── indieblocks.code-workspace ├── indieblocks.php ├── languages └── indieblocks.pot ├── phpcs.xml ├── readme.txt ├── scoper.inc.php ├── scoper.sh └── templates ├── feed-atom.php └── feed-rss2.php /.distignore: -------------------------------------------------------------------------------- 1 | /.wordpress-org 2 | /.git 3 | /.github 4 | /node_modules 5 | /tests 6 | /vendor 7 | /svn 8 | 9 | .distignore 10 | .gitignore 11 | .phpunit.result.cache 12 | .prettierrc 13 | README.md 14 | bootstrap.php 15 | composer.json 16 | composer.lock 17 | phpcs.xml 18 | phpunit.xml 19 | *.code-workspace 20 | scoper.* 21 | *.bak.* 22 | *.cache 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress.org 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | tag: 8 | name: New tag 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: WordPress Plugin Deploy 13 | uses: 10up/action-wordpress-plugin-deploy@stable 14 | env: 15 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 16 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all files that start with ~ 2 | ~* 3 | 4 | # ignore OS-generated files 5 | ehthumbs.db 6 | Thumbs.db 7 | 8 | # ignore Editor files 9 | *.sublime-project 10 | *.sublime-workspace 11 | *.komodoproject 12 | 13 | # ignore log files, databases and shell scripts 14 | *.log 15 | *.sql 16 | *.sqlite 17 | #*.sh 18 | 19 | # ignore compiled files 20 | *.com 21 | *.class 22 | *.dll 23 | *.exe 24 | *.o 25 | *.so 26 | 27 | # ignore packaged files 28 | *.7z 29 | *.dmg 30 | *.gz 31 | *.iso 32 | *.jar 33 | *.rar 34 | *.tar 35 | *.zip 36 | 37 | /svn/* 38 | /vendor/* 39 | *.bak.* 40 | *.cache 41 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 4, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "bracketSameLine": false, 8 | "bracketSpacing": true, 9 | "parenSpacing": true, 10 | "semi": true, 11 | "arrowParens": "always" 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IndieBlocks 2 | Use blocks, and, optionally, "short-form" post types to more easily "IndieWebify" your WordPress site. 3 | 4 | ## Why 5 | This plugin lets you: 6 | 7 | * Add a "microblog" to either a new or existing WordPress site 8 | * Add Webmention support to enable rich _cross-site_ conversations 9 | * Add "microformats" to your WordPress site's front end and posts, so that Webmention endpoints are able to correctly interpret your posts 10 | 11 | ## How 12 | IndieBlocks provides a number of blocks that can be used to add microformats to posts _without having to tweak or know HTML_. It also adds a number of "short-form" custom post types to more easily add a "microblog" to your site. (Both are optional, like, well, all of IndieBlocks features.) 13 | 14 | IndieBlocks also "hooks" into the [Micropub](https://wordpress.org/plugins/micropub/) plugin, so that Micropub posts are given the correct post type, and, when applicable, "microformats" blocks. 15 | 16 | If you're running a "block theme," it will attempt to add "microformats" to it. (If your theme already supports microformats, just leave this part disabled.) 17 | 18 | There's also Webmention, which can be enabled for posts, notes, and likes (and any other post types of your choosing). This, in combination with the aforementioned microformats and this plugin's blocks, enables richer _cross-site_ conversations. (If you're perfectly happy with your existing Webmention setup, here, too, you can simply leave this setting disabled.) 19 | 20 | ## Classic Editor and Themes 21 | IndieBlocks, despite its name, runs just fine on sites that have the Classic Editor plugin installed, or don't (yet) use a "block theme." The Custom Post Types and Webmention options will just work, as will most of its smaller "modules." Microformats, however, will need to be added some other way. (It's absolutely okay to run a "classic" theme with microformats support and add microformats to posts by hand or through a different plugin.) 22 | 23 | ## Modules 24 | * **Blocks:** Add microformats to posts without having to think about HTML. There's also a couple blocks for use in theme templates (like the "Facepile" block for webmentions, or the "Link Preview" block). 25 | * **Block theme "enhancements":** Automatic microformats support for any (!) "block theme." 26 | * **Webmention:** Send and receive webmentions, for _actual cross-site conversations_. 27 | * **Custom post types:** Add "note" (i.e., a "microblog") and "like" sections. 28 | * **Location**: Add basic location and weather info to posts. 29 | * **Link preview cards**: Grab basic metadata from linked pages (to be used with the "Link Preview" block). 30 | 31 | 32 | -------------------------------------------------------------------------------- /assets/admin.css: -------------------------------------------------------------------------------- 1 | .repost .dashboard-comment-wrap, 2 | .like .dashboard-comment-wrap, 3 | .favorite .dashboard-comment-wrap, 4 | .tag .dashboard-comment-wrap, 5 | .bookmark .dashboard-comment-wrap, 6 | .listen .dashboard-comment-wrap, 7 | .watch .dashboard-comment-wrap, 8 | .read .dashboard-comment-wrap, 9 | .follow .dashboard-comment-wrap, 10 | .mention .dashboard-comment-wrap, 11 | .reacji .dashboard-comment-wrap { 12 | padding-inline-start: 63px; 13 | } 14 | 15 | .repost .dashboard-comment-wrap .comment-author, 16 | .like .dashboard-comment-wrap .comment-author, 17 | .favorite .dashboard-comment-wrap .comment-author, 18 | .tag .dashboard-comment-wrap .comment-author, 19 | .bookmark .dashboard-comment-wrap .comment-author, 20 | .listen .dashboard-comment-wrap .comment-author, 21 | .watch .dashboard-comment-wrap .comment-author, 22 | .read .dashboard-comment-wrap .comment-author, 23 | .follow .dashboard-comment-wrap .comment-author, 24 | .mention .dashboard-comment-wrap .comment-author, 25 | .reacji .dashboard-comment-wrap .comment-author { 26 | margin-block: 0; 27 | } 28 | -------------------------------------------------------------------------------- /assets/common.js: -------------------------------------------------------------------------------- 1 | ( ( element, i18n, apiFetch ) => { 2 | // (Global) object; holds some helper functions. 3 | window.IndieBlocks = { 4 | hCite: ( className, attributes, innerBlocks = null ) => { 5 | const { createElement: el, createInterpolateElement: interpolate } = element; 6 | const { __, sprintf } = i18n; 7 | 8 | const messagesWithByline = { 9 | /* translators: %1$s: Link to the bookmarked page. %2$s: Author of the bookmarked page. */ 10 | 'u-bookmark-of': __( 'Bookmarked %1$s by %2$s.', 'indieblocks' ), 11 | /* translators: %1$s: Link to the "liked" page. %2$s: Author of the "liked" page. */ 12 | 'u-like-of': __( 'Likes %1$s by %2$s.', 'indieblocks' ), 13 | /* translators: %1$s: Link to the page being replied to. %2$s: Author of the page being replied to. */ 14 | 'u-in-reply-to': __( 'In reply to %1$s by %2$s.', 'indieblocks' ), 15 | /* translators: %1$s: Link to the "page" being reposted. %2$s: Author of the "page" being reposted. */ 16 | 'u-repost-of': __( 'Reposted %1$s by %2$s.', 'indieblocks' ), 17 | }; 18 | 19 | const messages = { 20 | /* translators: %s: Link to the bookmarked page. */ 21 | 'u-bookmark-of': __( 'Bookmarked %s.', 'indieblocks' ), 22 | /* translators: %s: Link to the "liked" page. */ 23 | 'u-like-of': __( 'Likes %s.', 'indieblocks' ), 24 | /* translators: %s: Link to the page being replied to. */ 25 | 'u-in-reply-to': __( 'In reply to %s.', 'indieblocks' ), 26 | /* translators: %s: Link to the "page" being reposted. */ 27 | 'u-repost-of': __( 'Reposted %s.', 'indieblocks' ), 28 | }; 29 | 30 | const message = 31 | ! attributes.author || 'undefined' === attributes.author 32 | ? messages[ className ] 33 | : messagesWithByline[ className ]; 34 | 35 | const name = attributes.title || attributes.url; 36 | 37 | return el( 38 | 'div', 39 | { className: className + ' h-cite' }, 40 | el( 41 | 'p', 42 | {}, // Adding paragraphs this time around. 43 | el( 44 | 'i', 45 | {}, // Could've been `span`, with a `className` or something, but works well enough. 46 | ! attributes.author || 'undefined' === attributes.author 47 | ? interpolate( sprintf( message, '' + name + '' ), { 48 | a: el( 'a', { 49 | className: 50 | attributes.title && attributes.url !== attributes.title 51 | ? 'u-url p-name' // No title means no `p-name`. 52 | : 'u-url', 53 | href: attributes.url, 54 | } ), 55 | } ) 56 | : interpolate( 57 | sprintf( message, '' + name + '', '' + attributes.author + '' ), 58 | { 59 | a: el( 'a', { 60 | className: 61 | attributes.title && attributes.url !== attributes.title 62 | ? 'u-url p-name' 63 | : 'u-url', 64 | href: attributes.url, 65 | } ), 66 | span: el( 'span', { 67 | className: 'p-author', 68 | } ), 69 | } 70 | ) 71 | ) 72 | ), 73 | 'u-repost-of' === className && innerBlocks && ! attributes.empty 74 | ? el( 'blockquote', { className: 'wp-block-quote e-content' }, el( innerBlocks ) ) 75 | : null 76 | ); 77 | }, 78 | /** 79 | * Calls a backend function that parses a URL for microformats and the like, 80 | * and sets attributes accordingly. 81 | */ 82 | updateMeta: ( props ) => { 83 | const url = props.attributes.url; 84 | 85 | if ( props.attributes.customTitle && props.attributes.customAuthor ) { 86 | // We're using custom values for both title and author; nothing 87 | // to do here. 88 | return; 89 | } 90 | 91 | if ( ! IndieBlocks.isValidUrl( url ) ) { 92 | return; 93 | } 94 | 95 | // Like a time-out. 96 | const controller = new AbortController(); 97 | const timeoutId = setTimeout( () => { 98 | controller.abort(); 99 | }, 6000 ); 100 | 101 | apiFetch( { 102 | path: '/indieblocks/v1/meta?url=' + encodeURIComponent( url ), 103 | signal: controller.signal, // That time-out thingy. 104 | } ) 105 | .then( ( response ) => { 106 | if ( ! props.attributes.customTitle && ( response.name || '' === response.name ) ) { 107 | // Got a, possibly empty, title. 108 | props.setAttributes( { title: response.name } ); 109 | } 110 | 111 | if ( ! props.attributes.customAuthor && ( response.author.name || '' === response.author.name ) ) { 112 | // Got a, possibly empty, name. 113 | props.setAttributes( { author: response.author.name } ); 114 | } 115 | 116 | clearTimeout( timeoutId ); 117 | } ) 118 | .catch( ( error ) => { 119 | // The request timed out or otherwise failed. Leave as is. 120 | } ); 121 | }, 122 | /** 123 | * Validates a URL. 124 | */ 125 | isValidUrl: ( string ) => { 126 | try { 127 | new URL( string ); 128 | } catch ( error ) { 129 | return false; 130 | } 131 | 132 | return true; 133 | }, 134 | }; 135 | } )( window.wp.element, window.wp.i18n, window.wp.apiFetch ); 136 | -------------------------------------------------------------------------------- /assets/location.css: -------------------------------------------------------------------------------- 1 | .indieblocks-location__fetch-button { 2 | height: 32px; 3 | } 4 | 5 | @media screen and (max-width: 782px) { 6 | .indieblocks-location__fetch-button { 7 | min-height: 40px; 8 | } 9 | } 10 | 11 | .indieblocks-location__address-field { 12 | width: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /assets/location.js: -------------------------------------------------------------------------------- 1 | ( ( element, components, i18n, data, coreData, apiFetch, plugins, editor ) => { 2 | const { createElement: el, useEffect, useState, useRef } = element; 3 | const { Button, Flex, FlexBlock, FlexItem, TextControl, PanelRow } = components; 4 | const { __ } = i18n; 5 | const { useSelect } = data; 6 | const { useEntityProp } = coreData; 7 | const { registerPlugin } = plugins; 8 | const { PluginDocumentSettingPanel } = editor; 9 | 10 | // @link https://wordpress.stackexchange.com/questions/362975/admin-notification-after-save-post-when-ajax-saving-in-gutenberg 11 | const doneSaving = () => { 12 | const { isSaving, isAutosaving, status } = useSelect( ( select ) => { 13 | return { 14 | isSaving: select( 'core/editor' ).isSavingPost(), 15 | isAutosaving: select( 'core/editor' ).isAutosavingPost(), 16 | status: select( 'core/editor' ).getEditedPostAttribute( 'status' ), 17 | }; 18 | } ); 19 | 20 | const [ wasSaving, setWasSaving ] = useState( isSaving && ! isAutosaving ); // Ignore autosaves. 21 | 22 | if ( wasSaving ) { 23 | if ( ! isSaving ) { 24 | setWasSaving( false ); 25 | return true; 26 | } 27 | } else if ( isSaving && ! isAutosaving ) { 28 | setWasSaving( true ); 29 | } 30 | 31 | return false; 32 | }; 33 | 34 | registerPlugin( 'indieblocks-location-panel', { 35 | render: ( props ) => { 36 | const { postId, postType } = useSelect( ( select ) => { 37 | return { 38 | postId: select( 'core/editor' ).getCurrentPostId(), 39 | postType: select( 'core/editor' ).getCurrentPostType(), 40 | }; 41 | }, [] ); 42 | 43 | const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' ); 44 | 45 | const latitude = meta?.geo_latitude ?? ''; 46 | const longitude = meta?.geo_longitude ?? ''; 47 | const geoAddress = meta?.geo_address ?? ''; 48 | 49 | const stateRef = useRef(); 50 | stateRef.current = meta; 51 | 52 | // This seems superfluous, but let's leave it in place, in case our 53 | // server's "too slow" to immediately return a location name. 54 | 55 | // The idea here was to fetch a location name after it is set in the 56 | // background. So that if the OpenStreetMap geolocation API is "too slow" 57 | // we'll still get an "up-to-date" location. 58 | const fetchLocation = () => { 59 | if ( ! postId ) { 60 | return; 61 | } 62 | 63 | if ( ! postType ) { 64 | return; 65 | } 66 | 67 | if ( stateRef.current.geo_address ) { 68 | return; 69 | } 70 | 71 | // Like a time-out. 72 | const controller = new AbortController(); 73 | const timeoutId = setTimeout( () => { 74 | controller.abort(); 75 | }, 6000 ); 76 | 77 | apiFetch( { 78 | path: '/wp/v2/' + postType + '/' + postId, 79 | signal: controller.signal, // That time-out thingy. 80 | } ) 81 | .then( ( response ) => { 82 | clearTimeout( timeoutId ); 83 | 84 | if ( response.indieblocks_location && response.indieblocks_location.geo_address ) { 85 | // This function does not do anything besides displaying a location name. 86 | setMeta( { 87 | ...stateRef.current, 88 | geo_address: response.indieblocks_location.geo_address, 89 | } ); 90 | } 91 | } ) 92 | .catch( ( error ) => { 93 | // The request timed out or otherwise failed. Leave as is. 94 | } ); 95 | }; 96 | 97 | if ( doneSaving() && ! geoAddress ) { 98 | setTimeout( () => { 99 | fetchLocation(); 100 | }, 1500 ); 101 | 102 | setTimeout( () => { 103 | fetchLocation(); 104 | }, 15000 ); 105 | } 106 | 107 | // `navigator.geolocation.getCurrentPosition` callback. 108 | const updatePosition = ( position ) => { 109 | setMeta( { 110 | ...stateRef.current, 111 | geo_latitude: position.coords.latitude.toString(), 112 | geo_longitude: position.coords.longitude.toString(), 113 | } ); 114 | }; 115 | 116 | // Runs once. 117 | useEffect( () => { 118 | if ( '' !== latitude ) { 119 | return; 120 | } 121 | 122 | if ( '' !== longitude ) { 123 | return; 124 | } 125 | 126 | const shouldUpdate = indieblocks_location_obj?.should_update ?? '0'; 127 | if ( '1' !== shouldUpdate ) { 128 | return; 129 | } 130 | 131 | if ( ! navigator.geolocation ) { 132 | return; 133 | } 134 | 135 | navigator.geolocation.getCurrentPosition( updatePosition, ( error ) => { 136 | // Do nothing. 137 | console.log( error ); 138 | } ); 139 | }, [ updatePosition ] ); 140 | 141 | return el( 142 | PluginDocumentSettingPanel, 143 | { 144 | name: 'indieblocks-location-panel', 145 | title: __( 'Location', 'indieblocks' ), 146 | }, 147 | el( 148 | PanelRow, 149 | {}, 150 | el( 151 | Flex, 152 | { align: 'end' }, 153 | el( 154 | FlexBlock, 155 | {}, 156 | el( TextControl, { 157 | label: __( 'Latitude', 'indieblocks' ), 158 | value: latitude, 159 | onChange: ( value ) => { 160 | setMeta( { ...meta, geo_latitude: value } ); 161 | }, 162 | } ) 163 | ), 164 | el( 165 | FlexBlock, 166 | {}, 167 | el( TextControl, { 168 | label: __( 'Longitude', 'indieblocks' ), 169 | value: longitude, 170 | onChange: ( value ) => { 171 | setMeta( { 172 | ...meta, 173 | geo_longitude: value, 174 | } ); 175 | }, 176 | } ) 177 | ), 178 | el( 179 | FlexItem, 180 | {}, 181 | el( 182 | Button, 183 | { 184 | className: 'indieblocks-location__fetch-button', 185 | onClick: () => { 186 | if ( ! navigator.geolocation ) { 187 | return; 188 | } 189 | 190 | navigator.geolocation.getCurrentPosition( updatePosition, ( error ) => { 191 | // Do nothing. 192 | console.log( error ); 193 | } ); 194 | }, 195 | variant: 'secondary', 196 | }, 197 | __( 'Fetch', 'indieblocks' ) 198 | ) 199 | ) 200 | ) 201 | ), 202 | el( 203 | PanelRow, 204 | {}, 205 | // To allow authors to manually override or pass on a location. 206 | el( TextControl, { 207 | className: 'indieblocks-location__address-field', 208 | label: __( 'Location', 'indieblocks' ), 209 | value: geoAddress, 210 | onChange: ( value ) => { 211 | setMeta( { ...meta, geo_address: value } ); 212 | }, 213 | } ) 214 | ) 215 | ); 216 | }, 217 | } ); 218 | } )( 219 | window.wp.element, 220 | window.wp.components, 221 | window.wp.i18n, 222 | window.wp.data, 223 | window.wp.coreData, 224 | window.wp.apiFetch, 225 | window.wp.plugins, 226 | window.wp.editor 227 | ); 228 | -------------------------------------------------------------------------------- /assets/mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janboddez/indieblocks/187985d5c505f4f5041dc391d498cd7f021e0eca/assets/mm.png -------------------------------------------------------------------------------- /assets/remove-extra-margin.css: -------------------------------------------------------------------------------- 1 | .wp-block-post-title.screen-reader-text + * { 2 | margin-block-start: 0 !important; 3 | } 4 | -------------------------------------------------------------------------------- /assets/webmention-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/webmention-legacy.js: -------------------------------------------------------------------------------- 1 | jQuery( document ).ready( function ( $ ) { 2 | $( '#indieblocks-webmention .indieblocks-resend-webmention' ).click( function () { 3 | var button = $( this ); 4 | var type = button.data( 'type' ); 5 | var data = { 6 | 'action': 'indieblocks_resend_webmention', 7 | 'type': type, // Post or comment. 8 | 'obj_id': 'post' === type ? $( '[name="post_ID"]' ).val() : $( '[name="comment_ID"]' ).val(), // Current post or comment ID. 9 | '_wp_nonce': button.data( 'nonce' ), // Nonce. 10 | }; 11 | 12 | $.post( ajaxurl, data, function ( response ) { 13 | button.parent().find( 'p' ).remove(); 14 | button.parent().append( '

' + indieblocks_webmention_legacy_obj.message + '

' ); 15 | button.remove(); 16 | } ); 17 | } ); 18 | 19 | $( '.indieblocks-delete-avatar' ).click( function () { 20 | var button = $( this ); 21 | var data = { 22 | 'action': 'indieblocks_delete_avatar', 23 | 'comment_id': $( '[name="comment_ID"]' ).val(), // Current comment ID. 24 | '_wp_nonce': button.data( 'nonce' ), // Nonce. 25 | }; 26 | 27 | $.post( ajaxurl, data, function ( response ) { 28 | // Bit lazy, but 'kay. 29 | location.reload(); 30 | } ); 31 | } ); 32 | } ); 33 | -------------------------------------------------------------------------------- /assets/webmention.js: -------------------------------------------------------------------------------- 1 | ( ( element, components, i18n, data, coreData, plugins, editPost ) => { 2 | const { createElement: el, createInterpolateElement: interpolate, useState } = element; 3 | const { Button, PanelBody } = components; 4 | const { __, sprintf } = i18n; 5 | const { useSelect } = data; 6 | const { useEntityRecord } = coreData; 7 | const { registerPlugin } = plugins; 8 | const { PluginSidebar, PluginSidebarMoreMenuItem } = editPost; 9 | 10 | const reschedule = ( postId, setWebmention ) => { 11 | if ( ! postId ) { 12 | return false; 13 | } 14 | 15 | // Like a time-out. 16 | const controller = new AbortController(); 17 | const timeoutId = setTimeout( () => { 18 | controller.abort(); 19 | }, 6000 ); 20 | 21 | try { 22 | fetch( indieblocks_webmention_obj.ajaxurl, { 23 | signal: controller.signal, // That time-out thingy. 24 | method: 'POST', 25 | body: new URLSearchParams( { 26 | action: 'indieblocks_resend_webmention', 27 | type: 'post', 28 | obj_id: postId, 29 | _wp_nonce: indieblocks_webmention_obj.nonce, 30 | } ), 31 | } ) 32 | .then( ( response ) => { 33 | clearTimeout( timeoutId ); 34 | setWebmention( 'scheduled' ); // So as to trigger a re-render. 35 | } ) 36 | .catch( ( error ) => { 37 | // The request timed out or otherwise failed. Leave as is. 38 | throw new Error( 'The "Resend" request failed.' ); 39 | } ); 40 | } catch ( error ) { 41 | return false; 42 | } 43 | 44 | return true; 45 | }; 46 | 47 | if ( '1' === indieblocks_webmention_obj.show_meta_box ) { 48 | // Gutenberg sidebar. 49 | registerPlugin( 'indieblocks-webmention-sidebar', { 50 | render: () => { 51 | const { postId, postType } = useSelect( ( select ) => { 52 | return { 53 | postId: select( 'core/editor' ).getCurrentPostId(), 54 | postType: select( 'core/editor' ).getCurrentPostType(), 55 | }; 56 | }, [] ); 57 | 58 | const { record, isResolving } = useEntityRecord( 'postType', postType, postId ); 59 | const [ webmention, setWebmention ] = useState( record?.indieblocks_webmention ?? null ); 60 | 61 | let output = []; 62 | 63 | // console.table( webmention ); 64 | 65 | if ( typeof webmention === 'object' ) { 66 | Object.keys( webmention ).forEach( ( key ) => { 67 | const value = webmention[ key ]; 68 | 69 | if ( ! value.endpoint ) { 70 | return; 71 | } 72 | 73 | let line = ''; 74 | 75 | if ( value.sent ) { 76 | line = sprintf( 77 | /* translators: %1$s: Webmention endpoint. %2$s: HTTP response code. */ 78 | __( 'Sent to %1$s: %2$d.', 'indieblocks' ), 79 | '' + value.endpoint + '', 80 | value.code 81 | ); 82 | line = el( 83 | 'p', 84 | {}, 85 | interpolate( line, { 86 | a: el( 'a', { 87 | href: encodeURI( value.endpoint ), 88 | title: value.sent, 89 | target: '_blank', 90 | rel: 'noreferrer noopener', 91 | } ), 92 | } ) 93 | ); 94 | output.push( line ); 95 | } else if ( value.retries ) { 96 | if ( value.retries >= 3 ) { 97 | line = sprintf( 98 | __( 'Could not send webmention to %s.', 'indieblocks' ), 99 | value.endpoint 100 | ); 101 | line = el( 102 | 'p', 103 | {}, 104 | interpolate( line, { 105 | a: el( 'a', { 106 | href: encodeURI( value.endpoint ), 107 | target: '_blank', 108 | rel: 'noreferrer noopener', 109 | } ), 110 | } ) 111 | ); 112 | output.push( line ); 113 | } else { 114 | line = sprintf( 115 | __( 'Could not send webmention to %s. Trying again soon.', 'indieblocks' ), 116 | value.endpoint 117 | ); 118 | line = el( 119 | 'p', 120 | {}, 121 | interpolate( line, { 122 | a: el( 'a', { 123 | href: encodeURI( value.endpoint ), 124 | target: '_blank', 125 | rel: 'noreferrer noopener', 126 | } ), 127 | } ) 128 | ); 129 | output.push( line ); 130 | } 131 | } 132 | } ); 133 | } else if ( 'scheduled' === webmention ) { 134 | line = el( 'p', {}, __( 'Webmention scheduled.', 'indieblocks' ) ); 135 | output.push( line ); 136 | } 137 | 138 | if ( ! output.length ) { 139 | // return; 140 | output.push( el( 'p', {}, __( 'No endpoints found.', 'indieblocks' ) ) ); 141 | } 142 | 143 | return [ 144 | el( 145 | PluginSidebarMoreMenuItem, 146 | { target: 'indieblocks-webmention-sidebar' }, 147 | __( 'Webmention', 'indieblocks' ) 148 | ), 149 | el( 150 | PluginSidebar, 151 | { 152 | icon: el( 153 | 'svg', 154 | { 155 | xmlns: 'http://www.w3.org/2000/svg', 156 | viewBox: '0 0 24 24', 157 | }, 158 | el( 'path', { 159 | d: 'm13.91 18.089-1.894-5.792h-.032l-1.863 5.792H7.633L4.674 6.905h2.458L8.9 14.518h.032l1.94-5.793h2.302l1.91 5.886h.031L16.48 8.73l-1.778-.004L18.387 5.3l2.287 3.43-1.81-.001-2.513 9.36z', 160 | } ) 161 | ), 162 | name: 'indieblocks-webmention-sidebar', 163 | title: __( 'Webmention', 'indieblocks' ), 164 | }, 165 | el( 166 | PanelBody, 167 | {}, 168 | output, 169 | el( 170 | 'p', 171 | {}, 172 | el( 173 | Button, 174 | { 175 | onClick: () => { 176 | if ( confirm( __( 'Reschedule webmentions?', 'indieblocks' ) ) ) { 177 | reschedule( postId, setWebmention ); 178 | } 179 | }, 180 | variant: 'secondary', 181 | }, 182 | __( 'Resend', 'indieblocks' ) 183 | ) 184 | ) 185 | ) 186 | ), 187 | ]; 188 | }, 189 | } ); 190 | } 191 | } )( 192 | window.wp.element, 193 | window.wp.components, 194 | window.wp.i18n, 195 | window.wp.data, 196 | window.wp.coreData, 197 | window.wp.plugins, 198 | window.wp.editPost 199 | ); 200 | -------------------------------------------------------------------------------- /blocks/bookmark/block.asset.php: -------------------------------------------------------------------------------- 1 | \IndieBlocks\Plugin::PLUGIN_VERSION, 5 | 'dependencies' => array( 6 | 'wp-blocks', 7 | 'wp-element', 8 | 'wp-block-editor', 9 | 'wp-components', 10 | 'wp-i18n', 11 | 'indieblocks-common', 12 | ), 13 | ); 14 | -------------------------------------------------------------------------------- /blocks/bookmark/block.css: -------------------------------------------------------------------------------- 1 | .wp-block-indieblocks-bookmark > .h-cite:first-child > p { 2 | margin-block: 0; 3 | } 4 | 5 | .wp-block-indieblocks-bookmark > .block-editor-inner-blocks { 6 | margin-block: 1em; 7 | min-height: 35px; 8 | } 9 | -------------------------------------------------------------------------------- /blocks/bookmark/block.js: -------------------------------------------------------------------------------- 1 | ( ( blocks, element, blockEditor, components, data, i18n, IndieBlocks ) => { 2 | const { createBlock, registerBlockType } = blocks; 3 | const { createElement: el, renderToString, useEffect } = element; 4 | const { BlockControls, InnerBlocks, InspectorControls, useBlockProps } = blockEditor; 5 | const { PanelBody, Placeholder, ToggleControl, TextControl } = components; 6 | const { useSelect } = data; 7 | const { __ } = i18n; 8 | 9 | registerBlockType( 'indieblocks/bookmark', { 10 | icon: el( 11 | 'svg', 12 | { 13 | xmlns: 'http://www.w3.org/2000/svg', 14 | viewBox: '0 0 24 24', 15 | }, 16 | el( 'path', { 17 | d: 'M8.1 5a2 2 0 0 0-2 2v12.1L12 15l5.9 4.1V7a2 2 0 0 0-2-2H8.1z', 18 | } ) 19 | ), 20 | edit: ( props ) => { 21 | const url = props.attributes.url; 22 | const customTitle = props.attributes.customTitle; 23 | const title = props.attributes.title || ''; // May not be present in the saved HTML, so we need a fallback value even when `block.json` contains a default. 24 | const customAuthor = props.attributes.customAuthor; 25 | const author = props.attributes.author || ''; 26 | 27 | const updateEmpty = ( empty ) => { 28 | props.setAttributes( { empty } ); 29 | }; 30 | 31 | const { parentClientId, innerBlocks } = useSelect( ( select ) => { 32 | const parentClientId = select( 'core/block-editor' ).getBlockHierarchyRootClientId( props.clientId ); 33 | 34 | return { 35 | parentClientId: parentClientId, 36 | innerBlocks: select( 'core/block-editor' ).getBlocks( parentClientId ), 37 | }; 38 | }, [] ); 39 | 40 | // To determine whether `.e-content` and `InnerBlocks.Content` 41 | // should be saved (and echoed). 42 | useEffect( () => { 43 | let empty = true; 44 | 45 | if ( innerBlocks.length > 1 ) { 46 | // More than one child block. 47 | empty = false; 48 | } 49 | 50 | if ( 51 | 'undefined' !== typeof innerBlocks[ 0 ] && 52 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.content && 53 | innerBlocks[ 0 ].attributes.content.length 54 | ) { 55 | // A non-empty paragraph or heading. Empty paragraphs are 56 | // almost unavoidable, so it's important to get this right. 57 | empty = false; 58 | } 59 | 60 | if ( 61 | 'undefined' !== typeof innerBlocks[ 0 ] && 62 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.href && 63 | innerBlocks[ 0 ].attributes.href.length 64 | ) { 65 | // A non-empty image. 66 | empty = false; 67 | } 68 | 69 | if ( 'undefined' !== typeof innerBlocks[ 0 ] && innerBlocks[ 0 ].innerBlocks.length ) { 70 | // A quote or gallery, empty or not. 71 | empty = false; 72 | } 73 | 74 | updateEmpty( empty ); 75 | }, [ innerBlocks, updateEmpty ] ); 76 | 77 | const placeholderProps = { 78 | icon: el( 79 | 'svg', 80 | { 81 | xmlns: 'http://www.w3.org/2000/svg', 82 | viewBox: '0 0 24 24', 83 | }, 84 | el( 'path', { 85 | d: 'M8.1 5a2 2 0 0 0-2 2v12.1L12 15l5.9 4.1V7a2 2 0 0 0-2-2H8.1z', 86 | } ) 87 | ), 88 | label: __( 'Bookmark', 'indieblocks' ), 89 | isColumnLayout: true, 90 | }; 91 | 92 | if ( ! url || 'undefined' === url ) { 93 | placeholderProps.instructions = __( 94 | 'Add a URL and have WordPress automatically generate a correctly microformatted introductory paragraph.', 95 | 'indieblocks' 96 | ); 97 | } 98 | 99 | const titleProps = { 100 | label: __( 'Title', 'indieblocks' ), 101 | value: title, 102 | onChange: ( value ) => { 103 | props.setAttributes( { title: value } ); 104 | }, 105 | }; 106 | 107 | if ( ! customTitle ) { 108 | titleProps.readOnly = 'readonly'; 109 | } 110 | 111 | const authorProps = { 112 | label: __( 'Author', 'indieblocks' ), 113 | value: author, 114 | onChange: ( value ) => { 115 | props.setAttributes( { author: value } ); 116 | }, 117 | }; 118 | 119 | if ( ! customAuthor ) { 120 | authorProps.readOnly = 'readonly'; 121 | } 122 | 123 | return el( 124 | 'div', 125 | useBlockProps(), 126 | el( BlockControls ), 127 | props.isSelected || ! url || 'undefined' === url 128 | ? el( 129 | Placeholder, 130 | placeholderProps, 131 | el( 132 | InspectorControls, 133 | { key: 'inspector' }, 134 | el( 135 | PanelBody, 136 | { 137 | title: __( 'Title and Author' ), 138 | initialOpen: true, 139 | }, 140 | el( TextControl, titleProps ), 141 | el( ToggleControl, { 142 | label: __( 'Customize title', 'indieblocks' ), 143 | checked: customTitle, 144 | onChange: ( value ) => { 145 | props.setAttributes( { 146 | customTitle: value, 147 | } ); 148 | }, 149 | } ), 150 | el( TextControl, authorProps ), 151 | el( ToggleControl, { 152 | label: __( 'Customize author', 'indieblocks' ), 153 | checked: customAuthor, 154 | onChange: ( value ) => { 155 | props.setAttributes( { 156 | customAuthor: value, 157 | } ); 158 | }, 159 | } ) 160 | ) 161 | ), 162 | el( TextControl, { 163 | label: __( 'URL', 'indieblocks' ), 164 | value: url, 165 | onChange: ( value ) => { 166 | props.setAttributes( { url: value } ); 167 | }, 168 | onKeyDown: ( event ) => { 169 | if ( 13 === event.keyCode ) { 170 | IndieBlocks.updateMeta( props ); 171 | } 172 | }, 173 | onBlur: () => { 174 | IndieBlocks.updateMeta( props ); 175 | }, 176 | } ) 177 | ) 178 | : IndieBlocks.hCite( 'u-bookmark-of', props.attributes ), 179 | el( InnerBlocks, { 180 | template: [ [ 'core/paragraph' ] ], 181 | templateLock: false, 182 | } ) // Always **show** (editable) `InnerBlocks`. 183 | ); 184 | }, 185 | save: ( props ) => 186 | el( 187 | 'div', 188 | useBlockProps.save(), 189 | ! props.attributes.url || 'undefined' === props.attributes.url 190 | ? null // Can't do much without a URL. 191 | : IndieBlocks.hCite( 'u-bookmark-of', props.attributes ), 192 | ! props.attributes.empty ? el( 'div', { className: 'e-content' }, el( InnerBlocks.Content ) ) : null 193 | ), 194 | transforms: { 195 | from: [ 196 | { 197 | type: 'block', 198 | blocks: [ 'indieblocks/context' ], 199 | transform: ( { url } ) => { 200 | return createBlock( 'indieblocks/bookmark', { url } ); 201 | }, 202 | }, 203 | ], 204 | to: [ 205 | { 206 | type: 'block', 207 | blocks: [ 'core/group' ], 208 | transform: ( attributes, innerBlocks ) => { 209 | return createBlock( 'core/group', attributes, [ 210 | createBlock( 'core/html', { 211 | content: renderToString( IndieBlocks.hCite( 'u-bookmark-of', attributes ) ), 212 | } ), 213 | createBlock( 'core/group', { className: 'e-content' }, innerBlocks ), 214 | ] ); 215 | }, 216 | }, 217 | { 218 | type: 'block', 219 | blocks: [ 'indieblocks/like' ], 220 | transform: ( attributes, innerBlocks ) => { 221 | return createBlock( 'indieblocks/like', attributes, innerBlocks ); 222 | }, 223 | }, 224 | ], 225 | }, 226 | } ); 227 | } )( 228 | window.wp.blocks, 229 | window.wp.element, 230 | window.wp.blockEditor, 231 | window.wp.components, 232 | window.wp.data, 233 | window.wp.i18n, 234 | window.IndieBlocks 235 | ); 236 | -------------------------------------------------------------------------------- /blocks/bookmark/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": 2, 3 | "title": "Bookmark", 4 | "description": "Bookmark and annotate web pages or posts.", 5 | "name": "indieblocks/bookmark", 6 | "category": "text", 7 | "editorScript": "file:./block.js", 8 | "style": "file:./block.css", 9 | "attributes": { 10 | "url": { 11 | "type": "string", 12 | "source": "attribute", 13 | "selector": ".u-url", 14 | "attribute": "href", 15 | "default": "" 16 | }, 17 | "customTitle": { 18 | "type": "boolean", 19 | "default": false 20 | }, 21 | "title": { 22 | "type": "string", 23 | "source": "text", 24 | "selector": ".p-name", 25 | "default": "" 26 | }, 27 | "customAuthor": { 28 | "type": "boolean", 29 | "default": false 30 | }, 31 | "author": { 32 | "type": "string", 33 | "source": "text", 34 | "selector": ".p-author", 35 | "default": "" 36 | }, 37 | "empty": { 38 | "type": "boolean", 39 | "default": true 40 | } 41 | }, 42 | "example": { 43 | "attributes": { 44 | "url": "https://example.org/", 45 | "title": "Example Domain", 46 | "author": "Alice" 47 | } 48 | }, 49 | "textdomain": "indieblocks" 50 | } 51 | -------------------------------------------------------------------------------- /blocks/context/block.asset.php: -------------------------------------------------------------------------------- 1 | \IndieBlocks\Plugin::PLUGIN_VERSION, 5 | 'dependencies' => array( 6 | 'wp-blocks', 7 | 'wp-element', 8 | 'wp-block-editor', 9 | 'wp-components', 10 | 'wp-i18n', 11 | ), 12 | ); 13 | -------------------------------------------------------------------------------- /blocks/context/block.js: -------------------------------------------------------------------------------- 1 | ( ( blocks, element, blockEditor, components, i18n ) => { 2 | const { createBlock, getSaveContent, registerBlockType } = blocks; 3 | const { createElement: el, createInterpolateElement: interpolate } = element; 4 | const { BlockControls, useBlockProps } = blockEditor; 5 | const { Placeholder, RadioControl, TextControl } = components; 6 | const { __, sprintf } = i18n; 7 | 8 | const messages = { 9 | /* translators: %s: Link to the bookmarked page. */ 10 | 'u-bookmark-of': __( 'Bookmarked %s.', 'indieblocks' ), 11 | /* translators: %s: Link to the "liked" page. */ 12 | 'u-like-of': __( 'Likes %s.', 'indieblocks' ), 13 | /* translators: %s: Link to the page being replied to. */ 14 | 'u-in-reply-to': __( 'In reply to %s.', 'indieblocks' ), 15 | /* translators: %s: Link to the "page" being reposted. */ 16 | 'u-repost-of': __( 'Reposted %s.', 'indieblocks' ), 17 | }; 18 | 19 | const render = ( blockProps, url, kind ) => { 20 | return el( 21 | 'div', 22 | blockProps, 23 | ! url || 'undefined' === url 24 | ? null // Return nothing. 25 | : el( 26 | 'i', 27 | {}, 28 | interpolate( sprintf( messages[ kind ], '' + encodeURI( url ) + '' ), { 29 | a: el( 'a', { className: kind, href: encodeURI( url ) } ), 30 | } ) 31 | ) 32 | ); 33 | }; 34 | 35 | registerBlockType( 'indieblocks/context', { 36 | edit: ( props ) => { 37 | const url = props.attributes.url; 38 | const kind = props.attributes.kind; 39 | 40 | const placeholderProps = { 41 | icon: 'format-status', 42 | label: __( 'Context', 'indieblocks' ), 43 | isColumnLayout: true, 44 | }; 45 | 46 | if ( ! url || 'undefined' === url ) { 47 | placeholderProps.instructions = __( 48 | 'Add a URL and post type, and have WordPress automatically generate a correctly microformatted introductory paragraph.', 49 | 'indieblocks' 50 | ); 51 | } 52 | 53 | return el( 54 | 'div', 55 | useBlockProps(), 56 | el( BlockControls ), 57 | props.isSelected || ! url || 'undefined' === url 58 | ? el( Placeholder, placeholderProps, [ 59 | el( TextControl, { 60 | label: __( 'URL', 'indieblocks' ), 61 | value: url, 62 | onChange: ( value ) => { 63 | props.setAttributes( { url: value } ); 64 | }, 65 | } ), 66 | el( RadioControl, { 67 | label: __( 'Type', 'indieblocks' ), 68 | selected: kind, 69 | options: [ 70 | { label: __( 'Bookmark', 'indieblocks' ), value: 'u-bookmark-of' }, 71 | { label: __( 'Like', 'indieblocks' ), value: 'u-like-of' }, 72 | { label: __( 'Reply', 'indieblocks' ), value: 'u-in-reply-to' }, 73 | { label: __( 'Repost', 'indieblocks' ), value: 'u-repost-of' }, 74 | ], 75 | onChange: ( value ) => { 76 | props.setAttributes( { kind: value } ); 77 | }, 78 | } ), 79 | ] ) 80 | : render( {}, url, kind ) 81 | ); 82 | }, 83 | save: ( props ) => { 84 | return render( useBlockProps.save(), props.attributes.url, props.attributes.kind ); 85 | }, 86 | deprecated: [ 87 | { 88 | save: ( props ) => { 89 | const url = props.attributes.url; 90 | const kind = props.attributes.kind; 91 | 92 | const messages = { 93 | /* translators: %s: Link to the bookmarked page. */ 94 | 'u-bookmark-of': __( 'Bookmarked %s.', 'indieblocks' ), 95 | /* translators: %s: Link to the "liked" page. */ 96 | 'u-like-of': __( 'Liked %s.', 'indieblocks' ), 97 | /* translators: %s: Link to the page being replied to. */ 98 | 'u-in-reply-to': __( 'In reply to %s.', 'indieblocks' ), 99 | /* translators: %s: Link to the "page" being reposted. */ 100 | 'u-repost-of': __( 'Reposted %s.', 'indieblocks' ), 101 | }; 102 | 103 | return el( 104 | 'div', 105 | useBlockProps.save(), 106 | el( 107 | 'i', 108 | {}, 109 | interpolate( sprintf( messages[ kind ], '' + url + '' ), { 110 | a: el( 'a', { className: kind, href: url } ), 111 | } ) 112 | ) 113 | ); 114 | }, 115 | }, 116 | ], 117 | transforms: { 118 | to: [ 119 | { 120 | type: 'block', 121 | blocks: [ 'core/html' ], 122 | transform: ( attributes ) => { 123 | return createBlock( 'core/html', { 124 | content: getSaveContent( 'indieblocks/context', attributes ), 125 | } ); 126 | }, 127 | }, 128 | ], 129 | }, 130 | } ); 131 | } )( window.wp.blocks, window.wp.element, window.wp.blockEditor, window.wp.components, window.wp.i18n ); 132 | -------------------------------------------------------------------------------- /blocks/context/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": 2, 3 | "title": "Context", 4 | "name": "indieblocks/context", 5 | "category": "text", 6 | "icon": "format-status", 7 | "editorScript": "file:./block.js", 8 | "editorStyle": "file:./editor.css", 9 | "attributes": { 10 | "url": { 11 | "type": "string", 12 | "source": "attribute", 13 | "selector": "a", 14 | "attribute": "href", 15 | "default": "" 16 | }, 17 | "kind": { 18 | "type": "string", 19 | "source": "attribute", 20 | "selector": "a", 21 | "attribute": "class", 22 | "default": "u-bookmark-of" 23 | } 24 | }, 25 | "example": { 26 | "attributes": { 27 | "url": "https://example.org/", 28 | "kind": "u-bookmark-of" 29 | } 30 | }, 31 | "textdomain": "indieblocks" 32 | } 33 | -------------------------------------------------------------------------------- /blocks/context/editor.css: -------------------------------------------------------------------------------- 1 | .wp-block-indieblocks-context .components-checkbox-control__input[type="checkbox"], 2 | .wp-block-indieblocks-context .components-radio-control__input[type="radio"] { 3 | /* Restore "default" values, should themes want to change these. */ 4 | height: 24px; 5 | width: 24px; 6 | } 7 | 8 | @media (min-width: 600px) { 9 | .wp-block-indieblocks-context .components-checkbox-control__input[type="checkbox"], 10 | .wp-block-indieblocks-context .components-radio-control__input[type="radio"] { 11 | height: 20px; 12 | width: 20px; 13 | } 14 | } 15 | 16 | .wp-block-indieblocks-context .components-text-control__input[readonly] { 17 | background-color: rgb(240, 240, 241); 18 | } 19 | -------------------------------------------------------------------------------- /blocks/facepile-content/block.asset.php: -------------------------------------------------------------------------------- 1 | \IndieBlocks\Plugin::PLUGIN_VERSION, 5 | 'dependencies' => array( 6 | 'wp-blocks', 7 | 'wp-element', 8 | 'wp-block-editor', 9 | 'wp-i18n', 10 | 'wp-components', 11 | 'indieblocks-common', 12 | ), 13 | ); 14 | -------------------------------------------------------------------------------- /blocks/facepile-content/block.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --indieblocks-avatar-size: 36px; 3 | } 4 | 5 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-1 { 6 | --indieblocks-avatar-size: 24px; 7 | } 8 | 9 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-2 { 10 | --indieblocks-avatar-size: 36px; 11 | } 12 | 13 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-3 { 14 | --indieblocks-avatar-size: 48px; 15 | } 16 | 17 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-4 { 18 | --indieblocks-avatar-size: 60px; 19 | } 20 | 21 | .wp-block-indieblocks-facepile-content ul { 22 | list-style: none; 23 | display: flex; 24 | flex-wrap: wrap; 25 | margin: 0; 26 | padding: 0; 27 | padding-inline-end: var(--indieblocks-avatar-size); 28 | } 29 | 30 | .wp-block-indieblocks-facepile-content li { 31 | display: inline-block; 32 | margin-inline-end: calc(-0.33 * var(--indieblocks-avatar-size)); 33 | position: relative; 34 | } 35 | 36 | .wp-block-indieblocks-facepile-content li:not(:last-child):hover { 37 | margin-inline-end: calc(-0.05 * var(--indieblocks-avatar-size)); 38 | } 39 | 40 | .wp-block-indieblocks-facepile-content a { 41 | display: block; 42 | position: relative; 43 | } 44 | 45 | .wp-block-indieblocks-facepile-content .avatar { 46 | display: block; 47 | width: var(--indieblocks-avatar-size); 48 | height: var(--indieblocks-avatar-size); 49 | border-radius: 50%; 50 | } 51 | 52 | .wp-block-indieblocks-facepile-content .icon { 53 | display: block; 54 | width: calc(0.5 * var(--indieblocks-avatar-size)); 55 | height: calc(0.5 * var(--indieblocks-avatar-size)); 56 | position: absolute; 57 | right: -3px; 58 | bottom: -3px; 59 | border-radius: 50%; 60 | } 61 | 62 | .wp-block-indieblocks-facepile-content .indieblocks-count .icon { 63 | display: inline; 64 | width: 1.1em; 65 | height: 1.1em; 66 | position: relative; 67 | top: 0.2em; 68 | right: initial; 69 | bottom: initial; 70 | } 71 | -------------------------------------------------------------------------------- /blocks/facepile-content/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": 2, 3 | "title": "Facepile Content", 4 | "description": "Outputs the actual “facepile” avatars.", 5 | "name": "indieblocks/facepile-content", 6 | "category": "theme", 7 | "editorScript": "file:./block.js", 8 | "style": "file:./block.css", 9 | "render": "file:./render.php", 10 | "attributes": { 11 | "avatarSize": { 12 | "type": "integer", 13 | "default": 2 14 | }, 15 | "backgroundColor": { 16 | "type": "string", 17 | "default": "transparent" 18 | }, 19 | "icons": { 20 | "type": "boolean", 21 | "default": true 22 | }, 23 | "color": { 24 | "type": "string", 25 | "default": "#000" 26 | }, 27 | "iconBackgroundColor": { 28 | "type": "string", 29 | "default": "#fff" 30 | }, 31 | "type": { 32 | "type": "array", 33 | "default": [ "bookmark", "like", "repost" ] 34 | }, 35 | "countOnly": { 36 | "type": "boolean", 37 | "default": false 38 | }, 39 | "forceShow": { 40 | "type": "boolean", 41 | "default": false 42 | } 43 | }, 44 | "usesContext": [ "postId", "postType" ], 45 | "textdomain": "indieblocks", 46 | "supports": { 47 | "color": { 48 | "gradients": true, 49 | "link": true, 50 | "__experimentalDefaultControls": { 51 | "background": true, 52 | "text": true, 53 | "link": true 54 | } 55 | }, 56 | "inserter": false 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /blocks/facepile-content/render.php: -------------------------------------------------------------------------------- 1 | context['postId'] ) ) { 7 | $post_id = $block->context['postId']; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 8 | } elseif ( in_the_loop() ) { 9 | $post_id = get_the_ID(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 10 | } 11 | 12 | if ( empty( $post_id ) ) { 13 | return; 14 | } 15 | 16 | $types = array( 'bookmark', 'like', 'repost' ); 17 | 18 | if ( isset( $attributes['type'] ) && is_array( $attributes['type'] ) ) { 19 | $types = $attributes['type']; 20 | } 21 | 22 | if ( empty( $types ) ) { 23 | return; 24 | } 25 | 26 | $facepile_comments = \IndieBlocks\get_facepile_comments( $post_id, $types ); 27 | 28 | if ( empty( $facepile_comments ) && empty( $attributes['forceShow'] ) ) { 29 | return; 30 | } 31 | 32 | add_action( 'wp_footer', '\\IndieBlocks\\print_facepile_icons', 999 ); 33 | 34 | $output = ''; 35 | 36 | if ( ! empty( $attributes['countOnly'] ) ) { 37 | if ( ! empty( $attributes['icons'] ) && ! empty( $attributes['type'] ) ) { 38 | $kind = ( (array) $attributes['type'] )[0]; 39 | $output .= '
' . count( $facepile_comments ) . '
'; 40 | } else { 41 | $output .= '
' . count( $facepile_comments ) . '
'; 42 | } 43 | } else { 44 | // Limit number of avatars shown. Might provide a proper option later. 45 | $facepile_num = apply_filters( 'indieblocks_facepile_num', 25, $post_id ); 46 | $facepile_comments = array_slice( $facepile_comments, 0, $facepile_num ); 47 | 48 | foreach ( $facepile_comments as $facepile_comment ) { 49 | $avatar = get_avatar( $facepile_comment, 40 ); 50 | 51 | if ( empty( $avatar ) ) { 52 | continue; 53 | 54 | // So, normally, WordPress would return a "Mystery Man" or whatever avatar, if there was none. Still, could 55 | // be it doesn't in case there's no author email address. 56 | // $avatar = ''; 57 | } 58 | 59 | $processor = new \WP_HTML_Tag_Processor( $avatar ); 60 | $processor->next_tag( 'img' ); 61 | 62 | if ( ! empty( $attributes['backgroundColor'] ) ) { 63 | $processor->set_attribute( 'style', 'background:' . esc_attr( $attributes['backgroundColor'] ) ); // Even though `WP_HTML_Tag_Processor::set_attribute()` will run, e.g., `esc_attr()` for us. 64 | } 65 | 66 | $alt = $processor->get_attribute( 'alt' ); 67 | $alt = ! empty( $alt ) ? $alt : get_comment_author( $facepile_comment ); 68 | 69 | $processor->set_attribute( 'alt', esc_attr( $alt ) ); 70 | 71 | $avatar = $processor->get_updated_html(); 72 | 73 | $source = get_comment_meta( $facepile_comment->comment_ID, 'indieblocks_webmention_source', true ); 74 | $kind = get_comment_meta( $facepile_comment->comment_ID, 'indieblocks_webmention_kind', true ); 75 | 76 | if ( in_array( $facepile_comment->comment_type, array( 'bookmark', 'like', 'repost' ), true ) ) { 77 | // Mentions initiated by the Webmention plugin use a slightly different data structure. 78 | $source = get_comment_meta( $facepile_comment->comment_ID, 'url', true ); 79 | if ( empty( $source ) ) { 80 | $source = get_comment_meta( $facepile_comment->comment_ID, 'webmention_source_url', true ); 81 | } 82 | 83 | $kind = $facepile_comment->comment_type; 84 | } 85 | 86 | $classes = array( 87 | 'bookmark' => 'p-bookmark', 88 | 'like' => 'p-like', 89 | 'repost' => 'p-repost', 90 | ); 91 | $class = isset( $classes[ $kind ] ) ? esc_attr( $classes[ $kind ] ) : ''; 92 | 93 | $titles = array( 94 | 'bookmark' => '… bookmarked this!', 95 | 'like' => '… liked this!', 96 | 'repost' => '… reposted this!', 97 | ); 98 | $title_attr = isset( $titles[ $kind ] ) ? esc_attr( $titles[ $kind ] ) : ''; 99 | 100 | if ( ! empty( $source ) ) { 101 | $el = '
  • ' . 102 | '' . $avatar . '' . 103 | ( ! empty( $attributes['icons'] ) && ! empty( $kind ) 104 | ? '' 105 | : '' 106 | ) . 107 | "
  • \n"; 108 | } else { 109 | $el = '
  • ' . 110 | '' . $avatar . '' . 111 | ( ! empty( $attributes['icons'] ) && ! empty( $kind ) 112 | ? '' 113 | : '' 114 | ) . 115 | "
  • \n"; 116 | } 117 | 118 | $icon_style = ''; 119 | if ( ! empty( $attributes['color'] ) ) { 120 | $icon_style .= "color:{$attributes['color']};"; 121 | } 122 | if ( ! empty( $attributes['iconBackgroundColor'] ) ) { 123 | $icon_style .= "background-color:{$attributes['iconBackgroundColor']};"; 124 | } 125 | 126 | if ( ! empty( $icon_style ) ) { 127 | $processor = new \WP_HTML_Tag_Processor( $el ); 128 | $processor->next_tag( 'svg' ); 129 | 130 | $processor->set_attribute( 'style', esc_attr( $icon_style ) ); 131 | $el = $processor->get_updated_html(); 132 | } 133 | 134 | $output .= $el; 135 | } 136 | 137 | if ( ! empty( $attributes['avatarSize'] ) ) { 138 | $avatar_size = esc_attr( (int) $attributes['avatarSize'] ); 139 | $opening_ul_tag = "