esc_url_raw( rest_url( PINORA_REST_NS . '/notes' ) ),
'board' => esc_url_raw( pinora_board_url() ),
'home' => esc_url_raw( home_url( '/' ) ),
'css' => esc_url_raw( pinora_asset_url( 'css/pinora.css' ) ),
'screenshotLib' => esc_url_raw( pinora_asset_url( 'vendor/html2canvas.min.js' ) ),
'locale' => pinora_locale_tag(),
'view' => $view,
/**
* Filters how often open browsers reload the notes, in milliseconds.
*
* @param int $interval Default 15000.
*/
'interval' => max( 5000, (int) apply_filters( 'pinora_sync_interval', 15000 ) ),
);
if ( 'board' === $view ) {
$config['i18n'] = pinora_js_strings()['i18n'];
$config['labels'] = pinora_labels();
$config['credit'] = get_option( 'pinora_credit' ) ? esc_url_raw( pinora_avangardism_url( 'board-credit' ) ) : '';
$config['logo'] = esc_url_raw( PINORA_URL . 'assets/images/avangardism.svg' );
} else {
$config['script'] = esc_url_raw( pinora_asset_url( 'js/pinora.js' ) );
$config['strings'] = esc_url_raw( rest_url( PINORA_REST_NS . '/strings' ) );
}
return $config;
}
/**
* Link to the AVANGARDISM website, tagged so the studio knows where visits
* come from. Nothing is sent anywhere unless someone clicks.
*
* @param string $campaign Where the link sits.
* @param string $path Page on the website.
* @return string
*/
function pinora_avangardism_url( $campaign, $path = '/' ) {
return add_query_arg(
array(
'utm_source' => 'pinora',
'utm_medium' => 'plugin',
'utm_campaign' => $campaign,
),
'https://avangardism.com' . $path
);
}
/**
* Loads a tiny loader on public pages.
*
* Regular visitors only get these few hundred bytes: the toolbar script is
* downloaded only once `?pinora=1` has been opened in this browser. Deciding in
* PHP (with a cookie) would break as soon as a page cache sits in front of the
* site.
*/
function pinora_enqueue() {
if ( is_admin() ) {
return;
}
$loader = <<<'JS'
(function (c) {
var p = new URLSearchParams(window.location.search);
var on = p.has('pinora') || p.has('pinora_note') || /[#&]pinora_key=/.test(window.location.hash);
if (!on) {
try { on = window.localStorage.getItem('pinora:' + c.rest + ':mode') === '1'; } catch (e) {}
}
if (!on) return;
var s = document.createElement('script');
s.src = c.script;
s.async = true;
document.head.appendChild(s);
})(window.PINORA);
JS;
wp_register_script( 'pinora-loader', false, array(), PINORA_VERSION, array( 'in_footer' => true ) );
wp_enqueue_script( 'pinora-loader' );
wp_add_inline_script( 'pinora-loader', 'window.PINORA = ' . wp_json_encode( pinora_config( 'site' ) ) . ";\n" . $loader );
}
add_action( 'wp_enqueue_scripts', 'pinora_enqueue' );
/**
* Serves the board on `/feedback-board/`, outside the theme templates.
*
* Caught on `parse_request`, before WordPress looks for content at this path:
* no rewrite rule to register, no permalinks to flush. A real page with the
* same path keeps priority.
*
* @param WP $wp Parsed request.
*/
function pinora_board( $wp ) {
$path = pinora_board_path();
if ( '' === $path || trim( (string) $wp->request, '/' ) !== $path || get_page_by_path( $path ) ) {
return;
}
nocache_headers();
header( 'Content-Type: text/html; charset=utf-8' );
header( 'X-Robots-Tag: noindex, nofollow' );
wp_register_style( 'pinora', PINORA_URL . 'assets/css/pinora.css', array(), pinora_asset_version( 'css/pinora.css' ) );
wp_register_script( 'pinora', PINORA_URL . 'assets/js/pinora.js', array(), pinora_asset_version( 'js/pinora.js' ), array( 'in_footer' => true ) );
wp_register_script( 'pinora-board', PINORA_URL . 'assets/js/pinora-board.js', array( 'pinora' ), pinora_asset_version( 'js/pinora-board.js' ), array( 'in_footer' => true ) );
wp_add_inline_script( 'pinora', 'window.PINORA = ' . wp_json_encode( pinora_config( 'board' ) ) . ';', 'before' );
?>
>
add_node(
array(
'id' => 'pinora',
'title' => '' . esc_html__( 'Feedback', 'pinora' ) . '',
'href' => pinora_invite_url( $current ),
'meta' => array( 'title' => __( 'Pin feedback on this page', 'pinora' ) ),
)
);
$bar->add_node(
array(
'parent' => 'pinora',
'id' => 'pinora-review',
'title' => esc_html__( 'Review this page', 'pinora' ),
'href' => pinora_invite_url( $current ),
)
);
$bar->add_node(
array(
'parent' => 'pinora',
'id' => 'pinora-board',
'title' => esc_html__( 'Open the board', 'pinora' ),
'href' => pinora_board_invite_url(),
)
);
$bar->add_node(
array(
'parent' => 'pinora',
'id' => 'pinora-settings',
'title' => esc_html__( 'Settings', 'pinora' ),
'href' => admin_url( 'tools.php?page=pinora' ),
)
);
}
add_action( 'admin_bar_menu', 'pinora_admin_bar', 80 );
/**
* Labels used by the JavaScript, keyed by their English source.
*
* Kept here as plain `__()` calls so that they are picked up by the usual
* translation tools and by translate.wordpress.org.
*
* @return array{i18n: array, labels: array}
*/
function pinora_js_strings() {
$i18n = array(
/* translators: 1: number of notes to do, 2: in progress, 3: done. */
'%1$d to do · %2$d in progress · %3$d done.' => __( '%1$d to do · %2$d in progress · %3$d done.', 'pinora' ),
/* translators: number of open notes on the page. */
'%d open' => __( '%d open', 'pinora' ),
'(no section)' => __( '(no section)', 'pinora' ),
'+ Note' => __( '+ Note', 'pinora' ),
'Add' => __( 'Add', 'pinora' ),
'Add a note' => __( 'Add a note', 'pinora' ),
'Add or edit an answer' => __( 'Add or edit an answer', 'pinora' ),
'All pages' => __( 'All pages', 'pinora' ),
'All severities' => __( 'All severities', 'pinora' ),
'Anonymous' => __( 'Anonymous', 'pinora' ),
'Answer' => _x( 'Answer', 'Verb, button label.', 'pinora' ),
'Answer the note' => __( 'Answer the note', 'pinora' ),
'Answer:' => __( 'Answer:', 'pinora' ),
'Board' => _x( 'Board', 'Noun, link to the feedback board.', 'pinora' ),
'Both fields are required.' => __( 'Both fields are required.', 'pinora' ),
'Cancel' => __( 'Cancel', 'pinora' ),
'Click the element you want to comment on, or reach it with Tab and press Enter · Esc to cancel' => __( 'Click the element you want to comment on, or reach it with Tab and press Enter · Esc to cancel', 'pinora' ),
'Click “Sign in” and enter the key that came with your review link.' => __( 'Click “Sign in” and enter the key that came with your review link.', 'pinora' ),
'Close' => __( 'Close', 'pinora' ),
'Close the list' => __( 'Close the list', 'pinora' ),
'Continue' => __( 'Continue', 'pinora' ),
'Copied.' => __( 'Copied.', 'pinora' ),
'Copy' => __( 'Copy', 'pinora' ),
/* translators: error message. */
'Could not add: %s' => __( 'Could not add: %s', 'pinora' ),
/* translators: error message. */
'Could not delete: %s' => __( 'Could not delete: %s', 'pinora' ),
/* translators: error message. */
'Could not move: %s' => __( 'Could not move: %s', 'pinora' ),
/* translators: error message. */
'Could not save: %s' => __( 'Could not save: %s', 'pinora' ),
/* translators: error message. */
'Could not update: %s' => __( 'Could not update: %s', 'pinora' ),
'Delete' => __( 'Delete', 'pinora' ),
'Delete the note' => __( 'Delete the note', 'pinora' ),
'Delete this note for the whole team?' => __( 'Delete this note for the whole team?', 'pinora' ),
'Do not attach' => __( 'Do not attach', 'pinora' ),
'Done' => __( 'Done', 'pinora' ),
'Download' => __( 'Download', 'pinora' ),
'Edit the answer' => __( 'Edit the answer', 'pinora' ),
'Enlarge the screenshot' => __( 'Enlarge the screenshot', 'pinora' ),
'Everyone' => __( 'Everyone', 'pinora' ),
'Export' => __( 'Export', 'pinora' ),
/* translators: number of notes. */
'Export %d note(s)' => __( 'Export %d note(s)', 'pinora' ),
'Pinora' => __( 'Pinora', 'pinora' ),
'Pinora, a free plugin by' => __( 'Pinora, a free plugin by', 'pinora' ),
'Feedback board' => __( 'Feedback board', 'pinora' ),
/* translators: number of notes. */
'Feedback · %d note(s)' => __( 'Feedback · %d note(s)', 'pinora' ),
'Filter by page' => __( 'Filter by page', 'pinora' ),
'Filter by person' => __( 'Filter by person', 'pinora' ),
'Filter by severity' => __( 'Filter by severity', 'pinora' ),
'Follows the current filters. Grouped by page then section: paste it as is into a ticket or an AI assistant.' => __( 'Follows the current filters. Grouped by page then section: paste it as is into a ticket or an AI assistant.', 'pinora' ),
'Footer' => __( 'Footer', 'pinora' ),
'Header' => __( 'Header', 'pinora' ),
'In progress' => __( 'In progress', 'pinora' ),
'In sync with the team' => __( 'In sync with the team', 'pinora' ),
/* translators: error message. */
'Key refused: %s' => __( 'Key refused: %s', 'pinora' ),
'Leave review mode' => __( 'Leave review mode', 'pinora' ),
'List' => _x( 'List', 'Noun, button that opens the list of notes.', 'pinora' ),
'Mark as fixed' => __( 'Mark as fixed', 'pinora' ),
/* translators: %s: column name, e.g. "In progress". */
'Moved to %s' => __( 'Moved to %s', 'pinora' ),
'Missing review key.' => __( 'Missing review key.', 'pinora' ),
/* translators: column name, e.g. "In progress". */
'Move to %s' => __( 'Move to %s', 'pinora' ),
'New note' => __( 'New note', 'pinora' ),
'New “To do” note' => __( 'New “To do” note', 'pinora' ),
'No open note on this page. Click “+ Note” to add one.' => __( 'No open note on this page. Click “+ Note” to add one.', 'pinora' ),
'No screenshot on this page: the note will be located by its position.' => __( 'No screenshot on this page: the note will be located by its position.', 'pinora' ),
/* translators: pin number. */
'Note %d' => __( 'Note %d', 'pinora' ),
'Note saved, the whole team can see it.' => __( 'Note saved, the whole team can see it.', 'pinora' ),
'Notes are shared: what one person pins shows up for everyone else, without reloading.' => __( 'Notes are shared: what one person pins shows up for everyone else, without reloading.', 'pinora' ),
'Notes on this page' => __( 'Notes on this page', 'pinora' ),
'Nothing to do. Add a note (+) or pin one right on the site.' => __( 'Nothing to do. Add a note (+) or pin one right on the site.', 'pinora' ),
'Open the page at this spot' => __( 'Open the page at this spot', 'pinora' ),
'Page (path)' => __( 'Page (path)', 'pinora' ),
'Pin number on the page' => __( 'Pin number on the page', 'pinora' ),
'Reopen' => __( 'Reopen', 'pinora' ),
'Review key' => __( 'Review key', 'pinora' ),
'Review the site →' => __( 'Review the site →', 'pinora' ),
'Save' => __( 'Save', 'pinora' ),
'Screenshot attached to the note' => __( 'Screenshot attached to the note', 'pinora' ),
/* translators: pin number. */
'Screenshot of note #%d' => __( 'Screenshot of note #%d', 'pinora' ),
'Screenshot removed.' => __( 'Screenshot removed.', 'pinora' ),
'Screenshot taken with the note' => __( 'Screenshot taken with the note', 'pinora' ),
'Screenshot…' => __( 'Screenshot…', 'pinora' ),
'Section (optional)' => __( 'Section (optional)', 'pinora' ),
/* translators: section name, e.g. "Footer". */
'Section: %s' => __( 'Section: %s', 'pinora' ),
'Sending…' => __( 'Sending…', 'pinora' ),
'Severity' => __( 'Severity', 'pinora' ),
/* translators: number of closed notes. */
'Show %d closed note(s)' => __( 'Show %d closed note(s)', 'pinora' ),
'Sign in' => __( 'Sign in', 'pinora' ),
/* translators: reviewer first name. */
'Signed “%s”. ⌘/Ctrl + Enter to save.' => __( 'Signed “%s”. ⌘/Ctrl + Enter to save.', 'pinora' ),
/* translators: error message. */
'Sync failed: %s' => __( 'Sync failed: %s', 'pinora' ),
'Taking a screenshot…' => __( 'Taking a screenshot…', 'pinora' ),
'The browser refused to copy: the text is selected, press ⌘/Ctrl + C.' => __( 'The browser refused to copy: the text is selected, press ⌘/Ctrl + C.', 'pinora' ),
'This note has been deleted in the meantime.' => __( 'This note has been deleted in the meantime.', 'pinora' ),
'To do' => __( 'To do', 'pinora' ),
'View on the page →' => __( 'View on the page →', 'pinora' ),
'Who is reviewing?' => __( 'Who is reviewing?', 'pinora' ),
/* translators: reviewer first name. */
'You are %s' => __( 'You are %s', 'pinora' ),
'Your first name' => __( 'Your first name', 'pinora' ),
'Your note' => __( 'Your note', 'pinora' ),
'Your first name signs your notes, so the team knows who spotted what. The key came with your review link.' => __( 'Your first name signs your notes, so the team knows who spotted what. The key came with your review link.', 'pinora' ),
'answer' => _x( 'answer', 'Noun, label in the Markdown export.', 'pinora' ),
'e.g. Camille' => __( 'e.g. Camille', 'pinora' ),
'e.g. Footer' => __( 'e.g. Footer', 'pinora' ),
'e.g. align the spacing between the result cards' => __( 'e.g. align the spacing between the result cards', 'pinora' ),
'e.g. fixed, live in 3 minutes · or: won’t fix, because…' => __( 'e.g. fixed, live in 3 minutes · or: won’t fix, because…', 'pinora' ),
'e.g. the button is too small on mobile, the text should say…' => __( 'e.g. the button is too small on mobile, the text should say…', 'pinora' ),
/* translators: export date. */
'exported %s' => __( 'exported %s', 'pinora' ),
/* translators: pin number. */
'pin #%d' => __( 'pin #%d', 'pinora' ),
'screen' => _x( 'screen', 'Noun, label in the Markdown export.', 'pinora' ),
'screenshot' => _x( 'screenshot', 'Noun, label in the Markdown export.', 'pinora' ),
'selector' => _x( 'selector', 'Noun, CSS selector label in the Markdown export.', 'pinora' ),
);
// Untranslated strings are left out: the browser falls back to English.
$i18n = array_filter(
$i18n,
static function ( $translation, $source ) {
return $translation !== $source;
},
ARRAY_FILTER_USE_BOTH
);
return array(
'i18n' => $i18n,
'labels' => pinora_labels(),
);
}