Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/icon-180.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions class-wp-press-this-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,13 @@ public function html() {
<head>
<meta http-equiv="Content-Type" content="<?php echo esc_attr( get_bloginfo( 'html_type' ) ); ?>; charset=<?php echo esc_attr( get_option( 'blog_charset' ) ); ?>" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="<?php echo esc_attr__( 'Press This', 'press-this' ); ?>">
<meta name="theme-color" content="#2271b1">
<link rel="apple-touch-icon" href="<?php echo esc_url( plugins_url( 'assets/icon-180.png', __FILE__ ) ); ?>">
<link rel="manifest" href="<?php echo esc_url( rest_url( 'press-this/v1/manifest' ) ); ?>">
<title><?php esc_html_e( 'Press This!', 'press-this' ); ?></title>

<script>
Expand Down
1 change: 1 addition & 0 deletions includes/class-press-this-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ public function get_script_dependencies() {
'wp-format-library',
'wp-html-entities',
'wp-i18n',
'wp-keyboard-shortcuts',
'wp-primitives',
);
}
Expand Down
57 changes: 57 additions & 0 deletions press-this-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ function wp_ajax_press_this_plugin_add_category() {
* @since 2.0.1
*/
function press_this_register_rest_routes() {
// Web App Manifest for Add to Home Screen / PWA support.
register_rest_route(
'press-this/v1',
'/manifest',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'press_this_rest_manifest',
'permission_callback' => '__return_true',
)
);

// URL scraping endpoint for Direct Access Mode.
register_rest_route(
'press-this/v1',
Expand Down Expand Up @@ -926,6 +937,52 @@ function press_this_is_proxy_enabled() {
return apply_filters( 'press_this_enable_url_proxy', false );
}

/**
* REST callback for Web App Manifest.
*
* Returns a JSON manifest for Add to Home Screen / PWA support.
* Must be publicly accessible so the browser can fetch it without authentication.
*
* @since 2.0.1
*
* @return WP_REST_Response Manifest JSON.
*/
function press_this_rest_manifest() {
$manifest = array(
'name' => __( 'Press This', 'press-this' ),
'short_name' => __( 'Press This', 'press-this' ),
'start_url' => admin_url( 'press-this.php' ),
'display' => 'standalone',
'theme_color' => '#2271b1',
'background_color' => '#ffffff',
'icons' => array(
array(
'src' => plugins_url( 'assets/icon-192.png', __FILE__ ),
'sizes' => '192x192',
'type' => 'image/png',
),
array(
'src' => plugins_url( 'assets/icon-512.png', __FILE__ ),
'sizes' => '512x512',
'type' => 'image/png',
),
),
'share_target' => array(
'action' => admin_url( 'press-this.php' ),
'method' => 'GET',
'params' => array(
'url' => 'u',
'title' => 't',
),
),
);

$response = new WP_REST_Response( $manifest, 200 );
$response->header( 'Content-Type', 'application/manifest+json; charset=utf-8' );

return $response;
}

/**
* Permission callback for REST scrape endpoint.
*
Expand Down
43 changes: 42 additions & 1 deletion src/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
/**
* WordPress dependencies
*/
import { useState, useCallback, useEffect, useRef } from '@wordpress/element';
import {
useState,
useCallback,
useEffect,
useRef,
useMemo,
} from '@wordpress/element';
import {
Button,
TextControl,
Expand Down Expand Up @@ -66,7 +72,7 @@
* @param {Function} props.onRedo Redo callback.
* @param {boolean} props.hasUndo Whether undo is available.
* @param {boolean} props.hasRedo Whether redo is available.
* @return {JSX.Element} Header component.

Check failure on line 75 in src/components/Header.js

View workflow job for this annotation

GitHub Actions / Code Linting

The type 'JSX' is undefined
*/
export default function Header( {
siteName,
Expand All @@ -93,6 +99,15 @@
const [ showUpgradeNotice, setShowUpgradeNotice ] =
useState( isLegacyBookmarklet );

// Detect standalone display mode (added to home screen).
const isStandalone = useMemo(
() =>
( typeof window.matchMedia === 'function' &&
window.matchMedia( '(display-mode: standalone)' ).matches ) ||
window.navigator.standalone === true,
Comment on lines +104 to +107
[]
);

// Track if initial auto-scan has been performed.
const hasAutoScanned = useRef( false );

Expand Down Expand Up @@ -404,6 +419,32 @@
'press-this'
) }
</MenuItem>
{ isStandalone && (
<>
<MenuItem
href={ siteUrl }
onClick={ onClose }
>
{ __(
'View Site',
'press-this'
) }
</MenuItem>
<MenuItem
onClick={ () => {
// Reload without query params for a blank post.
window.location.href =
window.location.pathname;
onClose();
} }
>
{ __(
'New Post',
'press-this'
) }
</MenuItem>
</>
) }
Comment on lines +422 to +447
</MenuGroup>
) }
</DropdownMenu>
Expand Down
20 changes: 20 additions & 0 deletions src/components/PressThisEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
*
* This must be a separate component to access BlockEditorProvider context.
*
* @return {JSX.Element|null} Block Inspector panel or null if no block selected.

Check failure on line 60 in src/components/PressThisEditor.js

View workflow job for this annotation

GitHub Actions / Code Linting

The type 'JSX' is undefined
*/
function SidebarBlockInspector() {
const hasSelectedBlock = useSelect( ( select ) => {
Expand Down Expand Up @@ -87,7 +87,7 @@
* position rather than always appended at the end.
*
* @param {Object} props Props passed through to ScrapedMediaPanel.
* @return {JSX.Element|null} ScrapedMediaPanel with store-connected insertion.

Check failure on line 90 in src/components/PressThisEditor.js

View workflow job for this annotation

GitHub Actions / Code Linting

The type 'JSX' is undefined
*/
function ConnectedScrapedMediaPanel( props ) {
const { insertBlock } = useDispatch( blockEditorStore );
Expand Down Expand Up @@ -179,9 +179,29 @@
* @param {string} url URL to redirect to.
* @param {boolean} inParentWindow Whether to redirect in parent window.
*/

/**
* Check if running in standalone display mode (added to home screen).
*
* @return {boolean} True if standalone mode.
*/
function isStandaloneMode() {
return (
( typeof window.matchMedia === 'function' &&
window.matchMedia( '(display-mode: standalone)' ).matches ) ||
window.navigator.standalone === true
Comment on lines +189 to +192
);
}

function performSafeRedirect( url, inParentWindow = false ) {
const safeUrl = safeRedirect( url );

// In standalone mode there's no opener window, so always redirect self.
if ( isStandaloneMode() ) {
window.location.href = safeUrl;
return;
}
Comment on lines 196 to +203

if ( inParentWindow && window.opener ) {
try {
// Attempt to check if opener is same origin.
Expand Down Expand Up @@ -247,7 +267,7 @@
* @param {Function} props.onUndoReady Callback when undo/redo handlers are ready (receives { handleUndo, handleRedo, hasUndo, hasRedo }).
* @param {string} props.categoryNonce
* @param {string} props.ajaxUrl
* @return {JSX.Element} Press This Editor component.

Check failure on line 270 in src/components/PressThisEditor.js

View workflow job for this annotation

GitHub Actions / Code Linting

The type 'JSX' is undefined
*/
export default function PressThisEditor( {
post,
Expand Down
11 changes: 11 additions & 0 deletions tests/components/header-publish-controls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,15 @@ describe( 'Header Publish Controls', () => {
// Check for disabled state.
expect( headerContent ).toContain( 'disabled' );
} );

test( 'Standalone mode detection guards matchMedia availability', () => {
expect( headerContent ).toContain( "typeof window.matchMedia === 'function'" );
} );

test( 'Standalone mode shows View Site and New Post menu items', () => {
expect( headerContent ).toContain( 'View Site' );
expect( headerContent ).toContain( 'New Post' );
// Items are conditionally rendered only in standalone mode.
expect( headerContent ).toContain( '{ isStandalone && (' );
} );
} );
32 changes: 32 additions & 0 deletions tests/components/undo-redo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,35 @@ describe( 'Undo/Redo: PressThisEditor stack and shortcuts', () => {
);
} );
} );

describe( 'Standalone mode: PressThisEditor', () => {
let editorContent;

beforeAll( () => {
const editorPath = path.resolve(
__dirname,
'../../src/components/PressThisEditor.js'
);
editorContent = fs.readFileSync( editorPath, 'utf8' );
} );

test( 'isStandaloneMode guards matchMedia availability', () => {
expect( editorContent ).toContain( "typeof window.matchMedia === 'function'" );
} );

test( 'performSafeRedirect checks standalone mode before opener redirect', () => {
// isStandaloneMode is called inside performSafeRedirect.
expect( editorContent ).toMatch(
/performSafeRedirect[\s\S]*?isStandaloneMode\(\)/
);
} );

test( 'Standalone mode redirects self instead of using opener', () => {
// In standalone mode, redirect goes to window.location.href directly.
const standaloneBlock = editorContent.match(
/if\s*\(\s*isStandaloneMode\(\)\s*\)\s*\{([\s\S]*?)\}/
);
expect( standaloneBlock ).not.toBeNull();
expect( standaloneBlock[ 1 ] ).toContain( 'window.location.href' );
} );
} );
Loading