Skip to content
Merged
2 changes: 2 additions & 0 deletions plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ function is_frontend() {
require_once( plugin_dir_path( __FILE__ ) . 'src/plugins/global-settings/block-styles/index.php' );
require_once( plugin_dir_path( __FILE__ ) . 'src/css-optimize.php' );
require_once( plugin_dir_path( __FILE__ ) . 'src/compatibility/index.php' );

if ( ! is_admin() ) {
require_once( plugin_dir_path( __FILE__ ) . 'src/lightbox/index.php' );
require_once( plugin_dir_path( __FILE__ ) . 'src/block/accordion/index.php' );
Expand All @@ -314,6 +315,7 @@ function is_frontend() {
*/
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/getting-started.php' );
if ( is_admin() ) {
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/useful-plugins.php' ); // For cross-marketing
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/index.php' );
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/news.php' );
require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/freemius.php' );
Expand Down
43 changes: 39 additions & 4 deletions src/components/image-control2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Button from '../button'
* External dependencies
*/
import classnames from 'classnames'
import { i18n } from 'stackable'
import { i18n, cimo } from 'stackable'
import {
useAttributeName, useBlockAttributesContext, useBlockSetAttributesContext,
} from '~stackable/hooks'
Expand All @@ -20,8 +20,11 @@ import {
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { Fragment, memo } from '@wordpress/element'
import {
Fragment, memo, useEffect, useState,
} from '@wordpress/element'
import { MediaUpload } from '@wordpress/block-editor'
import { currentUserHasCapability } from '~stackable/util'

const ImageControl = memo( props => {
const attrNameId = useAttributeName( `${ props.attribute }Id`, props.responsive, props.hover )
Expand Down Expand Up @@ -81,7 +84,37 @@ const ImageControl = memo( props => {
} )
}

return (
const [ CimoDownloadNotice, setCimoDownloadNotice ] = useState( null )

useEffect( () => {
// Skip displaying the Cimo notice if the plugin is already activated or the user has chosen to hide the notice
if ( ! cimo || cimo.hideNotice || cimo.status === 'activated' ) {
return
}

const userCanInstall = currentUserHasCapability( 'install_plugins' )
const userCanActivate = currentUserHasCapability( 'activate_plugins' )
// Show the Cimo notice only if the user has permissions to install or activate plugins
if ( ( cimo.status === 'not_installed' && userCanInstall ) || ( cimo.status === 'installed' && userCanActivate ) ) {
const loadNotice = async () => {
try {
// Import the Cimo notice component with explicit chunk naming
const { default: CimoNoticeComponent } = await import(
/* webpackChunkName: "cimo-download-notice" */
/* webpackMode: "lazy" */
'../../lazy-components/cimo'
)
setCimoDownloadNotice( () => CimoNoticeComponent )
} catch ( err ) {
// eslint-disable-next-line no-console
console.error( 'Failed to load Cimo download notice component:', err )
}
}
loadNotice()
}
}, [] )

return ( <>
<AdvancedControl
{ ...controlProps }
valueCheckAttribute={ props.attribute + 'Url' }
Expand Down Expand Up @@ -112,7 +145,7 @@ const ImageControl = memo( props => {
/>
) }
{ type === 'image' && (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<img
className="ugb-image-preview"
draggable="false"
Expand Down Expand Up @@ -172,6 +205,8 @@ const ImageControl = memo( props => {
hasPanelModifiedIndicator={ props.hasPanelModifiedIndicator }
/>
</AdvancedControl>
{ CimoDownloadNotice && <CimoDownloadNotice onDismiss={ () => setCimoDownloadNotice( null ) } /> }
</>
)
} )

Expand Down
1 change: 1 addition & 0 deletions src/init.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ public function register_block_editor_assets() {
'version' => array_shift( $version_parts ),
'wpVersion' => ! empty( $wp_version ) ? preg_replace( '/-.*/', '', $wp_version ) : $wp_version, // Ensure semver, strip out after dash
'adminUrl' => admin_url(),
'ajaxUrl' => admin_url('admin-ajax.php'),

// Fonts.
'locale' => get_locale(),
Expand Down
198 changes: 198 additions & 0 deletions src/lazy-components/cimo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import {
cimo, i18n, ajaxUrl,
} from 'stackable'
import { createRoot } from '~stackable/util'

import { __ } from '@wordpress/i18n'
import { Dashicon } from '@wordpress/components'
import domReady from '@wordpress/dom-ready'
import {
useState, useRef, useEffect,
} from '@wordpress/element'
import { models } from '@wordpress/api'

const CimoDownloadNotice = props => {
const [ data, setData ] = useState( { status: cimo?.status, action: cimo?.action } )
const pollCountRef = useRef( 0 )

const onDismiss = () => {
const settings = new models.Settings( { stackable_hide_cimo_notice: true } ) // eslint-disable-line camelcase
settings.save()

if ( cimo ) {
cimo.hideNotice = true
}

// Update the global stackable.cimo hideNotice variable
if ( typeof window !== 'undefined' && window.stackable?.cimo ) {
window.stackable.cimo.hideNotice = true
}

props?.onDismiss?.()
}

// Polls the Cimo plugin status to detect installation or activation state changes
const pollStatus = ( action, link, pollOnce = false ) => {
fetch( ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams( {
action: 'stackable_check_cimo_status',
// eslint-disable-next-line camelcase
user_action: action,
nonce: cimo.nonce,
} ),
credentials: 'same-origin',
} ).then( res => res.json() ).then( res => {
if ( ! res.success ) {
setData( { status: 'error', action: '' } )

const errorMessage = res?.data?.message ? res.data.message : 'Server error'

throw new Error( 'Stackable: ' + errorMessage )
}

if ( pollCountRef.current === 0 && link ) {
window.open( link, '_blank' )
}

pollCountRef.current += 1

const _data = res.data

if ( data.status !== _data.status ) {
setData( _data )

// Update the global stackable.cimo status/action variables
// so new image block selections reflect the latest Cimo installation state
if ( typeof window !== 'undefined' && window.stackable?.cimo ) {
window.stackable.cimo.status = _data.status
window.stackable.cimo.action = _data.action
}
}

// Stop polling if it has reached 3 attempts, or plugin status indicates installation/activation is complete
if ( pollOnce || pollCountRef.current >= 3 ||
( action === 'install' && ( _data.status === 'installed' || _data.status === 'activated' ) ) ||
( action === 'activate' && _data.status === 'activated' )
) {
return
}

setTimeout( () => {
pollStatus( action )
}, 3000 * pollCountRef.current )
} ).catch( e => {
// eslint-disable-next-line no-console
console.error( e.message )
} )
}

useEffect( () => {
const _media = wp.media
const old = _media.view.MediaFrame.Select

// When the media library closes, check and update the Cimo plugin status
// to ensure the UI reflects the latest installation or activation state.
_media.view.MediaFrame.Select = old.extend( {
initialize() {
old.prototype.initialize.apply( this, arguments )

this.on( 'close', () => {
pollCountRef.current = 0
if ( data.status === 'activated' ) {
return
}

if ( data.status === 'not_installed' ) {
pollStatus( 'install', null, true )
return
}

pollStatus( 'activate', null, true )
} )
},
} )
}, [] )

const onActionClick = e => {
e.preventDefault()
pollCountRef.current = 0

if ( data.status === 'not_installed' ) {
setData( { status: 'installing', action: '' } )
pollStatus( 'install', e.currentTarget.href )
return
}

setData( { status: 'activating', action: '' } )
pollStatus( 'activate', e.currentTarget.href )
}

return ( <>
<button aria-label="dismiss" onClick={ onDismiss }><Dashicon icon="no" /></button>
{ data.status === 'activated'
? <p>
{ __( 'Cimo Image Optimizer has been activated. Please refresh this page to begin optimizing your images automatically.', i18n ) }
</p>
: <p> { __( 'Instantly optimize images as you upload them with Cimo Image Optimizer.', i18n ) }
&nbsp;
{ data.status === 'installing'
? <span> { __( 'Installing', i18n ) }</span>
: ( data.status === 'activating'
? <span>{ __( 'Activating', i18n ) } </span>
: <a href={ data.action } target="_blank" rel="noreferrer" onClick={ onActionClick }>
{ data.status === 'installed' ? __( 'Activate now', i18n ) : __( 'Install now', i18n ) }
</a>
)
}
</p>
}
</> )
}

const CimoDownloadNoticeWrapper = props => {
return <div className="stk-cimo-notice"> <CimoDownloadNotice { ...props } /> </div>
}

export default CimoDownloadNoticeWrapper

domReady( () => {
if ( ! cimo || cimo.status === 'activated' || cimo.hideNotice ||
typeof wp === 'undefined' || ! wp?.media?.view?.Attachment?.Details
) {
return
}

const CurrentDetailsView = wp.media.view.Attachment.Details

// Display the Cimo download notice in the media library
const CustomDetailsView = CurrentDetailsView.extend( {
render() {
const result = CurrentDetailsView.prototype.render.apply( this, arguments )

if ( cimo?.hideNotice ) {
return result
}

const details = this.el.querySelector( '.attachment-info .details' )
if ( details && ! this.el.querySelector( '.stk-cimo-notice' ) ) {
const noticeDiv = document.createElement( 'div' )
noticeDiv.className = 'stk-cimo-notice'

const onDismiss = () => {
if ( noticeDiv && noticeDiv.parentNode ) {
noticeDiv.parentNode.removeChild( noticeDiv )
}
}

createRoot( noticeDiv ).render( <CimoDownloadNotice onDismiss={ onDismiss } /> )
details.insertAdjacentElement( 'afterend', noticeDiv )
}

return result
},
} )

wp.media.view.Attachment.Details = CustomDetailsView
} )
47 changes: 47 additions & 0 deletions src/lazy-components/cimo/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.stk-cimo-notice {
clear: both;
padding: 16px;
border: 2px solid #16a249;
background: #fff;
margin: 12px 0;
border-radius: 4px;
box-shadow: 0 1px 4px #33533f70;
position: relative;

button {
background: none;
border: none;
height: 14px;
width: 14px;
position: absolute;
right: 4px;
top: 4px;
cursor: pointer;
}

.dashicon {
font-size: 14px;
height: 14px;
width: 14px;
&:hover {
color: var(--stk-skin-error, #f15449);
}
}

p {
margin: 0;
font-size: 12px;

a {
color: inherit;
font-weight: 700;

&:hover {
color: #16a249;
}
}
span {
font-weight: 700;
}
}
}
9 changes: 9 additions & 0 deletions src/welcome/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { GettingStarted } from './getting-started'
import { BLOCK_STATE } from '~stackable/util/blocks'
import { BlockToggler, OptimizationSettings } from '~stackable/deprecated/v2/welcome/admin'
import blockData from '~stackable/deprecated/v2/welcome/blocks'
import { UsefulPlugins } from './useful-plugins'

const [ FREE_BLOCKS, BLOCK_DEPENDENCIES ] = importBlocks( require.context( '../block', true, /block\.json$/ ) )

Expand Down Expand Up @@ -1658,4 +1659,12 @@ domReady( () => {
<Settings />
)
}

if ( document.getElementById( 's-useful-plugins' ) ) {
createRoot(
document.getElementById( 's-useful-plugins' )
).render(
<UsefulPlugins />
)
}
} )
Loading
Loading