fix(v1.7.1): wordmark image on every beep + reply, restore updater
- Render the Beep wordmark bottom-right of every beep AND reply, with the delete button furthest right (was: wordmark right of delete, missing on replies, and duplicated after the replies block). - Draw the wordmark as crisp inline SVG via beep_post_wordmark() (filterable); the bundled beep-wordmark.png assets are corrupted. Readable on dark theme. - Restore class-updater.php (self-hosted Gitea updates) deleted in the master->main consolidation; re-wire it in beep.php. - Remove stray includes/class-shortcode.php.bak. - Bump to 1.7.1; sync readme.txt stable tag + changelog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+14
-10
@@ -1054,8 +1054,7 @@
|
||||
/* Bottom bar: wordmark left, delete right */
|
||||
/* Bottom bar: wordmark left, delete right */
|
||||
/* Bottom bar: delete left, wordmark right (blue, large) */
|
||||
/* Bottom bar: delete left, wordmark right (blue) */
|
||||
/* Bottom bar: delete left, wordmark text right */
|
||||
/* Bottom bar: wordmark on the left, delete furthest right */
|
||||
.beep-embed .beep-bottom-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1063,15 +1062,20 @@
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.beep-embed .beep-wordmark {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #0064cd;
|
||||
opacity: 0.5;
|
||||
letter-spacing: -0.5px;
|
||||
/* Brand wordmark (inline SVG, inherits colour via currentColor) */
|
||||
.beep-embed .beep-post-wordmark {
|
||||
color: var(--beep-accent);
|
||||
opacity: 0.55;
|
||||
flex: 0 0 auto;
|
||||
height: 16px;
|
||||
width: auto;
|
||||
transition: opacity 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
.beep-embed .beep-post:hover .beep-wordmark {
|
||||
opacity: 1;
|
||||
.beep-embed .beep-bottom-bar-reply .beep-post-wordmark {
|
||||
height: 13px;
|
||||
}
|
||||
.beep-embed .beep-post:hover .beep-post-wordmark,
|
||||
.beep-embed .beep-reply:hover .beep-post-wordmark {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Plugin Name: Beep
|
||||
* Description: A Twitter-style microblog for WordPress, with likes, replies, and Gravatars. Use the [beep_feed] shortcode on any page.
|
||||
* Version: 1.7.0
|
||||
* Version: 1.7.1
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: Sami Ahmed
|
||||
@@ -14,7 +14,7 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define('BEEP_VERSION', '1.7.0');
|
||||
define('BEEP_VERSION', '1.7.1');
|
||||
define('BEEP_FILE', __FILE__);
|
||||
define('BEEP_DIR', plugin_dir_path(__FILE__));
|
||||
define('BEEP_URL', plugin_dir_url(__FILE__));
|
||||
@@ -31,9 +31,16 @@ require_once BEEP_DIR . 'includes/class-quotes.php';
|
||||
require_once BEEP_DIR . 'includes/class-polls.php';
|
||||
require_once BEEP_DIR . 'includes/class-shortcode.php';
|
||||
require_once BEEP_DIR . 'includes/class-settings.php';
|
||||
require_once BEEP_DIR . 'includes/class-updater.php';
|
||||
require_once BEEP_DIR . 'includes/class-beep.php';
|
||||
|
||||
register_activation_hook(__FILE__, ['Beep_Plugin', 'activate']);
|
||||
register_deactivation_hook(__FILE__, ['Beep_Plugin', 'deactivate']);
|
||||
|
||||
add_action('plugins_loaded', ['Beep_Plugin', 'init']);
|
||||
|
||||
// Self-hosted updates via the Gitea releases API.
|
||||
// Register in admin and during wp-cron (where WordPress runs its update checks).
|
||||
if (is_admin() || (defined('DOING_CRON') && DOING_CRON)) {
|
||||
Beep_Updater::init();
|
||||
}
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Beep_Shortcode {
|
||||
|
||||
public static function init() {
|
||||
add_shortcode('beep_feed', [__CLASS__, 'render_feed']);
|
||||
add_action('rest_api_init', [__CLASS__, 'register_routes']);
|
||||
add_action('rest_api_init', [__CLASS__, 'register_media_routes']);
|
||||
add_action('rest_api_init', [__CLASS__, 'register_thread_routes']);
|
||||
add_action('rest_api_init', [__CLASS__, 'register_quote_routes']);
|
||||
}
|
||||
|
||||
public static function register_routes() {
|
||||
register_rest_route('beep/v1', '/feed', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'fetch_feed'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'sort' => ['default' => 'latest'],
|
||||
'limit' => ['default' => 50, 'validate_callback' => function ($v) { return is_numeric($v); }],
|
||||
'offset' => ['default' => 0, 'validate_callback' => function ($v) { return is_numeric($v); }],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST endpoint: re-render the feed list with the given sort.
|
||||
*/
|
||||
public static function fetch_feed($request) {
|
||||
$sort = $request->get_param('sort') === 'top' ? 'top' : 'latest';
|
||||
$limit = max(1, (int) $request->get_param('limit'));
|
||||
$offset = max(0, (int) $request->get_param('offset'));
|
||||
$posts = ($sort === 'top') ? self::get_top_beeps($limit + 1, $offset) : self::get_latest_beeps($limit + 1, $offset);
|
||||
|
||||
$has_more = count($posts) > $limit;
|
||||
if ($has_more) {
|
||||
$posts = array_slice($posts, 0, $limit);
|
||||
}
|
||||
|
||||
$html = '';
|
||||
foreach ($posts as $p) {
|
||||
$html .= beep_render_post($p);
|
||||
}
|
||||
if ($html === '' && $offset === 0) {
|
||||
$empty_bird = beep_bird_svg( ['size' => 64, 'fill' => '#4a90c4'] );
|
||||
$html = '<div class="beep-empty"><div class="beep-empty-icon">' . $empty_bird . '</div><p>No beeps yet.</p><span>Be the first to share something!</span></div>';
|
||||
}
|
||||
return rest_ensure_response(['html' => $html, 'sort' => $sort, 'has_more' => $has_more, 'offset' => $offset + count($posts)]);
|
||||
}
|
||||
|
||||
public static function get_latest_beeps($limit, $offset = 0) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->posts}
|
||||
WHERE post_type = %s AND post_status = 'publish'
|
||||
ORDER BY post_date DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
Beep_CPT::POST_TYPE, $limit, $offset
|
||||
));
|
||||
}
|
||||
|
||||
public static function get_top_beeps($limit, $offset = 0) {
|
||||
global $wpdb;
|
||||
$likes = Beep_Likes::table_name();
|
||||
return $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT p.*, COALESCE(l.cnt, 0) AS like_count
|
||||
FROM {$wpdb->posts} p
|
||||
LEFT JOIN (
|
||||
SELECT post_id, COUNT(*) AS cnt
|
||||
FROM $likes
|
||||
WHERE object_type = 'post'
|
||||
GROUP BY post_id
|
||||
) l ON l.post_id = p.ID
|
||||
WHERE p.post_type = %s AND p.post_status = 'publish'
|
||||
ORDER BY COALESCE(l.cnt, 0) DESC, p.post_date DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
Beep_CPT::POST_TYPE, $limit, $offset
|
||||
));
|
||||
}
|
||||
|
||||
public static function render_feed($atts) {
|
||||
$atts = shortcode_atts([
|
||||
'limit' => 50,
|
||||
'header' => 'Beep!',
|
||||
'show_wordmark' => true,
|
||||
'sort' => 'latest',
|
||||
], $atts, 'beep_feed');
|
||||
|
||||
$limit = max(1, (int) $atts['limit']);
|
||||
$sort = ($atts['sort'] === 'top') ? 'top' : 'latest';
|
||||
$posts = ($sort === 'top') ? self::get_top_beeps($limit) : self::get_latest_beeps($limit);
|
||||
$has_posts = !empty($posts);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="beep-feed" data-beep-feed>
|
||||
<div class="beep-embed">
|
||||
<div class="beep-feed-header">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;"><strong style="color:#FFFFFF;">Messages from </strong><img src="https://sami-ahmed.net/wp-content/plugins/beep/assets/beep-wordmark.png" alt="Beep!" style="height:22px;width:auto;vertical-align:middle;"></div>
|
||||
</div>
|
||||
|
||||
<?php if ($has_posts) : ?>
|
||||
<div class="beep-tabs" data-beep-tabs role="tablist">
|
||||
<button type="button" class="beep-tab <?php echo $sort === 'latest' ? 'is-active' : ''; ?>"
|
||||
data-beep-sort="latest" role="tab">Latest</button>
|
||||
<button type="button" class="beep-tab <?php echo $sort === 'top' ? 'is-active' : ''; ?>"
|
||||
data-beep-sort="top" role="tab">Top</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (is_user_logged_in() && !beep_user_is_banned(get_current_user_id())) : ?>
|
||||
<?php echo beep_render_composer(); ?>
|
||||
<?php elseif (is_user_logged_in()) : ?>
|
||||
<div class="beep-notice beep-notice-banned">
|
||||
Your account has been suspended from beeping.
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="beep-notice beep-notice-login">
|
||||
<p><strong>Join in.</strong> Sign in to like and reply.</p>
|
||||
<a class="beep-button" href="<?php echo esc_url(wp_login_url(get_permalink())); ?>">Log in</a>
|
||||
<?php if (get_option('users_can_register')) : ?>
|
||||
<a class="beep-button beep-button-secondary" href="<?php echo esc_url(wp_registration_url()); ?>">Sign up</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="beep-list" data-beep-list>
|
||||
<?php
|
||||
if ($has_posts) {
|
||||
foreach ($posts as $p) {
|
||||
echo beep_render_post($p);
|
||||
}
|
||||
} else {
|
||||
$empty_bird = beep_bird_svg( ['size' => 64, 'fill' => '#4a90c4'] );
|
||||
echo '<div class="beep-empty"><div class="beep-empty-icon">' . $empty_bird . '</div><p>No beeps yet.</p><span>Be the first to share something!</span></div>';
|
||||
}
|
||||
?>
|
||||
</div><!-- .beep-list -->
|
||||
</div><!-- .beep-embed -->
|
||||
</div><!-- .beep-feed -->
|
||||
<?php
|
||||
return ob_get_clean() . beep_render_thread_modal() . beep_render_gif_modal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register additional REST routes for media upload.
|
||||
*/
|
||||
public static function register_media_routes() {
|
||||
register_rest_route('beep/v1', '/media/(?P<post_id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [__CLASS__, 'upload_media'],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in() && !beep_user_is_banned(get_current_user_id());
|
||||
},
|
||||
]);
|
||||
register_rest_route('beep/v1', '/media/(?P<post_id>\d+)/(?P<attachment_id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [__CLASS__, 'delete_media'],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in() && !beep_user_is_banned(get_current_user_id());
|
||||
},
|
||||
]);
|
||||
register_rest_route('beep/v1', '/voice/(?P<post_id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [__CLASS__, 'upload_voice'],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in() && !beep_user_is_banned(get_current_user_id());
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public static function upload_media($request) {
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
$post = get_post($post_id);
|
||||
|
||||
if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) {
|
||||
return new WP_Error('not_found', 'Beep not found.', 404);
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return new WP_Error('forbidden', 'Cannot edit this beep.', 403);
|
||||
}
|
||||
if (empty($_FILES['file'])) {
|
||||
return new WP_Error('no_file', 'No file uploaded.');
|
||||
}
|
||||
|
||||
$result = Beep_Media::handle_upload($post_id, $_FILES['file']);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response($result);
|
||||
}
|
||||
|
||||
public static function delete_media($request) {
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
$attachment_id = (int) $request->get_param('attachment_id');
|
||||
$post = get_post($post_id);
|
||||
|
||||
if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) {
|
||||
return new WP_Error('not_found', 'Beep not found.', 404);
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return new WP_Error('forbidden', 'Cannot edit this beep.', 403);
|
||||
}
|
||||
|
||||
Beep_Media::remove_image($post_id, $attachment_id);
|
||||
return rest_ensure_response(['ok' => true]);
|
||||
}
|
||||
public static function upload_voice($request) {
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
$post = get_post($post_id);
|
||||
|
||||
if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) {
|
||||
return new WP_Error('not_found', 'Beep not found.', 404);
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return new WP_Error('forbidden', 'Cannot edit this beep.', 403);
|
||||
}
|
||||
if (empty($_FILES['file'])) {
|
||||
return new WP_Error('no_file', 'No file uploaded.');
|
||||
}
|
||||
|
||||
$result = Beep_Media::handle_voice_upload($post_id, $_FILES['file']);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response($result);
|
||||
}
|
||||
|
||||
|
||||
public static function register_thread_routes() {
|
||||
register_rest_route('beep/v1', '/thread/(?P<post_id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_thread'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
}
|
||||
|
||||
public static function get_thread($request) {
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) {
|
||||
return new WP_Error('not_found', 'Beep not found.', 404);
|
||||
}
|
||||
$depth = (int) $request->get_param('depth') ?: 10;
|
||||
$thread = Beep_Replies::get_thread($post_id, 0, $depth);
|
||||
return rest_ensure_response($thread);
|
||||
}
|
||||
|
||||
public static function register_quote_routes() {
|
||||
register_rest_route('beep/v1', '/quote/(?P<post_id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_quoted_post'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
register_rest_route('beep/v1', '/lookup', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'lookup_beep'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
}
|
||||
|
||||
public static function get_quoted_post($request) {
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
$quoted = Beep_Quotes::get_quoted_post($post_id);
|
||||
if (!$quoted) {
|
||||
return new WP_Error('not_found', 'No quoted post found.', 404);
|
||||
}
|
||||
return rest_ensure_response($quoted);
|
||||
}
|
||||
|
||||
public static function lookup_beep($request) {
|
||||
$query = sanitize_text_field($request->get_param('q'));
|
||||
if (!$query) {
|
||||
return new WP_Error('missing_query', 'Query parameter q is required.', 400);
|
||||
}
|
||||
if (is_numeric($query)) {
|
||||
$post = get_post((int) $query);
|
||||
if ($post && $post->post_type === Beep_CPT::POST_TYPE) {
|
||||
return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID));
|
||||
}
|
||||
}
|
||||
if (preg_match('/\[beep:(\d+)\]/', $query, $m)) {
|
||||
$post = get_post((int) $m[1]);
|
||||
if ($post && $post->post_type === Beep_CPT::POST_TYPE) {
|
||||
return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID));
|
||||
}
|
||||
}
|
||||
return new WP_Error('not_found', 'Beep not found.', 404);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-hosted plugin updater backed by the Gitea releases API.
|
||||
*
|
||||
* Makes Beep update like a normal WordPress plugin: the "update available"
|
||||
* badge appears in Plugins → Updates and one-click update works, pulling the
|
||||
* latest published Release from the Gitea repo.
|
||||
*
|
||||
* Configuration (define in wp-config.php):
|
||||
* define('BEEP_UPDATE_TOKEN', 'xxxxxxxx'); // required for a private repo
|
||||
* define('BEEP_UPDATE_GITEA_BASE', 'https://git.sami'); // optional override
|
||||
* define('BEEP_UPDATE_REPO', 'sami7777/beep'); // optional override
|
||||
*
|
||||
* To publish an update: bump the Version header in beep.php, push, then create
|
||||
* a Release on Gitea whose tag is the new version (e.g. "v1.7.0" or "1.7.0").
|
||||
* Optionally attach a "beep.zip" asset; otherwise the source archive is used.
|
||||
*/
|
||||
class Beep_Updater {
|
||||
|
||||
const TRANSIENT = 'beep_update_release';
|
||||
const CACHE_HOURS = 12;
|
||||
|
||||
/** @var string e.g. "beep/beep.php" */
|
||||
protected $plugin_file;
|
||||
/** @var string e.g. "beep" */
|
||||
protected $plugin_slug;
|
||||
/** @var string installed version */
|
||||
protected $version;
|
||||
/** @var string Gitea base URL, no trailing slash */
|
||||
protected $api_base;
|
||||
/** @var string "owner/repo" */
|
||||
protected $repo;
|
||||
|
||||
public static function init() {
|
||||
$self = new self();
|
||||
add_filter('pre_set_site_transient_update_plugins', [$self, 'check_for_update']);
|
||||
add_filter('plugins_api', [$self, 'plugin_info'], 20, 3);
|
||||
add_filter('http_request_args', [$self, 'authorize_request'], 10, 2);
|
||||
add_filter('upgrader_source_selection', [$self, 'fix_source_dir'], 10, 4);
|
||||
add_action('upgrader_process_complete', [$self, 'clear_cache'], 10, 2);
|
||||
add_filter('plugin_action_links_' . $self->plugin_file, [$self, 'action_links']);
|
||||
}
|
||||
|
||||
public function __construct() {
|
||||
$this->plugin_file = plugin_basename(BEEP_FILE);
|
||||
$this->plugin_slug = dirname($this->plugin_file);
|
||||
if ($this->plugin_slug === '.' || $this->plugin_slug === '') {
|
||||
$this->plugin_slug = 'beep';
|
||||
}
|
||||
$this->version = BEEP_VERSION;
|
||||
$base = defined('BEEP_UPDATE_GITEA_BASE') ? BEEP_UPDATE_GITEA_BASE : 'https://git.sami';
|
||||
$this->api_base = rtrim($base, '/');
|
||||
$this->repo = defined('BEEP_UPDATE_REPO') ? BEEP_UPDATE_REPO : 'sami7777/beep';
|
||||
}
|
||||
|
||||
/** Resolve the access token (constant first, then filter). */
|
||||
protected function token() {
|
||||
if (defined('BEEP_UPDATE_TOKEN') && BEEP_UPDATE_TOKEN) {
|
||||
return BEEP_UPDATE_TOKEN;
|
||||
}
|
||||
return (string) apply_filters('beep_update_token', '');
|
||||
}
|
||||
|
||||
protected function api_url($path) {
|
||||
return $this->api_base . '/api/v1/repos/' . $this->repo . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch + cache the latest release. Returns an info array or null.
|
||||
*/
|
||||
protected function get_remote($force = false) {
|
||||
if (!$force) {
|
||||
$cached = get_site_transient(self::TRANSIENT);
|
||||
if ($cached !== false) {
|
||||
return is_array($cached) ? $cached : null; // '' means "checked, nothing"
|
||||
}
|
||||
}
|
||||
|
||||
$headers = ['Accept' => 'application/json'];
|
||||
$token = $this->token();
|
||||
if ($token) {
|
||||
$headers['Authorization'] = 'token ' . $token;
|
||||
}
|
||||
|
||||
$res = wp_remote_get($this->api_url('/releases/latest'), [
|
||||
'timeout' => 15,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
|
||||
if (is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200) {
|
||||
// Brief negative cache so a flaky/unauthorized server doesn't stall every admin page.
|
||||
set_site_transient(self::TRANSIENT, '', HOUR_IN_SECONDS);
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode(wp_remote_retrieve_body($res), true);
|
||||
if (!is_array($data) || empty($data['tag_name'])) {
|
||||
set_site_transient(self::TRANSIENT, '', HOUR_IN_SECONDS);
|
||||
return null;
|
||||
}
|
||||
|
||||
$info = $this->parse_release($data);
|
||||
set_site_transient(self::TRANSIENT, $info, self::CACHE_HOURS * HOUR_IN_SECONDS);
|
||||
return $info;
|
||||
}
|
||||
|
||||
/** Normalize a Gitea release payload into the fields we need. */
|
||||
protected function parse_release($data) {
|
||||
$version = ltrim((string) $data['tag_name'], 'vV');
|
||||
|
||||
// Prefer an attached .zip asset; fall back to the source archive.
|
||||
$package = '';
|
||||
if (!empty($data['assets']) && is_array($data['assets'])) {
|
||||
foreach ($data['assets'] as $asset) {
|
||||
if (!empty($asset['browser_download_url'])
|
||||
&& strtolower(substr($asset['name'], -4)) === '.zip') {
|
||||
$package = $asset['browser_download_url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$package) {
|
||||
$package = !empty($data['zipball_url'])
|
||||
? $data['zipball_url']
|
||||
: $this->api_base . '/' . $this->repo . '/archive/' . $data['tag_name'] . '.zip';
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => $version,
|
||||
'package' => $package,
|
||||
'changelog' => isset($data['body']) ? (string) $data['body'] : '',
|
||||
'published_at' => isset($data['published_at']) ? $data['published_at'] : '',
|
||||
'name' => isset($data['name']) ? $data['name'] : '',
|
||||
'html_url' => isset($data['html_url']) ? $data['html_url'] : ($this->api_base . '/' . $this->repo),
|
||||
];
|
||||
}
|
||||
|
||||
/** Inject our plugin into the WordPress update transient. */
|
||||
public function check_for_update($transient) {
|
||||
if (!is_object($transient)) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$info = $this->get_remote();
|
||||
if (!$info) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$has_update = version_compare($info['version'], $this->version, '>');
|
||||
|
||||
$obj = (object) [
|
||||
'slug' => $this->plugin_slug,
|
||||
'plugin' => $this->plugin_file,
|
||||
'new_version' => $has_update ? $info['version'] : $this->version,
|
||||
'url' => $info['html_url'],
|
||||
'package' => $has_update ? $info['package'] : '',
|
||||
'icons' => [],
|
||||
'banners' => [],
|
||||
'tested' => '',
|
||||
'requires_php' => '',
|
||||
];
|
||||
|
||||
if ($has_update) {
|
||||
$transient->response[$this->plugin_file] = $obj;
|
||||
} else {
|
||||
// Listing it as no_update keeps "View details" / auto-update UI working.
|
||||
if (!isset($transient->no_update)) {
|
||||
$transient->no_update = [];
|
||||
}
|
||||
$transient->no_update[$this->plugin_file] = $obj;
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/** Provide the "View details" popup content. */
|
||||
public function plugin_info($result, $action, $args) {
|
||||
if ($action !== 'plugin_information') {
|
||||
return $result;
|
||||
}
|
||||
if (empty($args->slug) || $args->slug !== $this->plugin_slug) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$info = $this->get_remote();
|
||||
if (!$info) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$meta = get_file_data(BEEP_FILE, [
|
||||
'Name' => 'Plugin Name',
|
||||
'Author' => 'Author',
|
||||
'RequiresWP' => 'Requires at least',
|
||||
'RequiresPHP' => 'Requires PHP',
|
||||
]);
|
||||
|
||||
return (object) [
|
||||
'name' => $meta['Name'] ?: 'Beep',
|
||||
'slug' => $this->plugin_slug,
|
||||
'version' => $info['version'],
|
||||
'author' => esc_html($meta['Author']),
|
||||
'homepage' => $info['html_url'],
|
||||
'download_link' => $info['package'],
|
||||
'requires' => $meta['RequiresWP'],
|
||||
'requires_php' => $meta['RequiresPHP'],
|
||||
'last_updated' => $info['published_at'],
|
||||
'sections' => [
|
||||
'changelog' => $this->format_changelog($info['changelog']),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** Very small Markdown → HTML pass for the changelog body. */
|
||||
protected function format_changelog($md) {
|
||||
if ($md === '') {
|
||||
return 'No changelog provided for this release.';
|
||||
}
|
||||
$html = esc_html($md);
|
||||
$html = preg_replace('/^###\s+(.*)$/m', '<h4>$1</h4>', $html);
|
||||
$html = preg_replace('/^##\s+(.*)$/m', '<h3>$1</h3>', $html);
|
||||
$html = preg_replace('/^[\*\-]\s+(.*)$/m', '<li>$1</li>', $html);
|
||||
$html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html);
|
||||
return wpautop($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the auth token to any request aimed at our Gitea host so the
|
||||
* API check AND the package download both authenticate on a private repo.
|
||||
*/
|
||||
public function authorize_request($args, $url) {
|
||||
$token = $this->token();
|
||||
if (!$token) {
|
||||
return $args;
|
||||
}
|
||||
$host = wp_parse_url($this->api_base, PHP_URL_HOST);
|
||||
$url_host = wp_parse_url($url, PHP_URL_HOST);
|
||||
if ($host && $url_host && strcasecmp($host, $url_host) === 0) {
|
||||
if (empty($args['headers']) || !is_array($args['headers'])) {
|
||||
$args['headers'] = [];
|
||||
}
|
||||
$args['headers']['Authorization'] = 'token ' . $token;
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gitea archives unzip to a folder like "beep" or "sami7777-beep-<hash>".
|
||||
* Rename it to the plugin slug so WP updates the existing folder in place.
|
||||
*/
|
||||
public function fix_source_dir($source, $remote_source, $upgrader, $hook_extra = null) {
|
||||
if (!is_array($hook_extra)
|
||||
|| empty($hook_extra['plugin'])
|
||||
|| $hook_extra['plugin'] !== $this->plugin_file) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
global $wp_filesystem;
|
||||
if (!$wp_filesystem) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
$desired = trailingslashit($remote_source) . $this->plugin_slug . '/';
|
||||
if (untrailingslashit($source) === untrailingslashit($desired)) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
if ($wp_filesystem->move($source, $desired, true)) {
|
||||
return $desired;
|
||||
}
|
||||
return $source;
|
||||
}
|
||||
|
||||
/** Drop the cache after an update so the new version is reflected at once. */
|
||||
public function clear_cache($upgrader = null, $options = []) {
|
||||
delete_site_transient(self::TRANSIENT);
|
||||
}
|
||||
|
||||
/** "Check for updates" link on the Plugins screen. */
|
||||
public function action_links($links) {
|
||||
$url = wp_nonce_url(
|
||||
self_admin_url('plugins.php?beep_force_update_check=1'),
|
||||
'beep_force_update_check'
|
||||
);
|
||||
$links[] = '<a href="' . esc_url($url) . '">Check for updates</a>';
|
||||
return $links;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the manual "Check for updates" click: clear cache + force a re-check.
|
||||
*/
|
||||
add_action('admin_init', function () {
|
||||
if (empty($_GET['beep_force_update_check'])) {
|
||||
return;
|
||||
}
|
||||
if (!current_user_can('update_plugins')
|
||||
|| !check_admin_referer('beep_force_update_check')) {
|
||||
return;
|
||||
}
|
||||
delete_site_transient(Beep_Updater::TRANSIENT);
|
||||
delete_site_transient('update_plugins');
|
||||
wp_update_plugins();
|
||||
wp_safe_redirect(self_admin_url('plugins.php'));
|
||||
exit;
|
||||
});
|
||||
+25
-6
@@ -54,6 +54,25 @@ function beep_logo_svg() {
|
||||
. ' alt="Beep!" style="height:32px;width:auto;filter:' . esc_attr( $filter ) . ';">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Small "Beep!" brand wordmark shown at the bottom-right of every beep and reply.
|
||||
*
|
||||
* Rendered as crisp inline SVG so it stays sharp at any size and inherits its
|
||||
* colour from CSS (currentColor). The bundled beep-wordmark.png assets are
|
||||
* corrupted, so we draw the wordmark as vector text instead. To use a real
|
||||
* image once you have a clean asset, hook the `beep_post_wordmark` filter:
|
||||
* add_filter('beep_post_wordmark', fn() => '<img class="beep-post-wordmark" src="...">');
|
||||
*/
|
||||
function beep_post_wordmark() {
|
||||
$svg = '<svg class="beep-post-wordmark" viewBox="0 0 62 20" width="50" height="16"'
|
||||
. ' role="img" aria-label="Beep">'
|
||||
. '<text x="0" y="15"'
|
||||
. ' font-family="-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif"'
|
||||
. ' font-size="16" font-weight="800" letter-spacing="-0.5" fill="currentColor">Beep!</text>'
|
||||
. '</svg>';
|
||||
return apply_filters('beep_post_wordmark', $svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this user banned from posting/liking/replying?
|
||||
*/
|
||||
@@ -332,6 +351,7 @@ function beep_render_post($post) {
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5H6v2H4v12h2v-2h2v2h8V9h-2V7h-2v2H8V5zm10 0v2h2v12h-2v-2h-2v2H8V9h2v2h2V7h2V5h2z"/></svg>
|
||||
</a>
|
||||
<div class="beep-bottom-bar">
|
||||
<?php echo beep_post_wordmark(); ?>
|
||||
<?php if ($can_delete) : ?>
|
||||
<button class="beep-delete-link"
|
||||
data-beep-delete="<?php echo (int) $post->ID; ?>"
|
||||
@@ -339,7 +359,6 @@ function beep_render_post($post) {
|
||||
aria-label="Delete this beep"
|
||||
title="Delete">×</button>
|
||||
<?php endif; ?>
|
||||
<span class="beep-wordmark">Beep!</span>
|
||||
</div>
|
||||
|
||||
<?php if ($can_interact) : ?>
|
||||
@@ -352,7 +371,6 @@ function beep_render_post($post) {
|
||||
echo beep_render_replies_tree($all, 0, 0, $post->ID, $can_interact);
|
||||
?>
|
||||
</div>
|
||||
<span class="beep-wordmark">Beep!</span>
|
||||
</div>
|
||||
</article>
|
||||
<?php
|
||||
@@ -465,15 +483,16 @@ function beep_render_reply($comment, $all_comments = [], $depth = 0, $post_id =
|
||||
</div>
|
||||
<?php echo beep_render_reply_form($post_id, $comment->comment_ID); ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($can_delete) : ?>
|
||||
<div class="beep-delete-row">
|
||||
<div class="beep-bottom-bar beep-bottom-bar-reply">
|
||||
<?php echo beep_post_wordmark(); ?>
|
||||
<?php if ($can_delete) : ?>
|
||||
<button class="beep-delete-link"
|
||||
data-beep-delete-reply="<?php echo (int) $comment->comment_ID; ?>"
|
||||
type="button"
|
||||
aria-label="Delete this reply"
|
||||
title="Delete">×</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Recursively render children
|
||||
|
||||
+10
-1
@@ -4,7 +4,7 @@ Tags: microblog, twitter, tweets, social, comments, polls, elementor
|
||||
Requires at least: 6.0
|
||||
Tested up to: 6.9
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.6.0
|
||||
Stable tag: 1.7.1
|
||||
License: GPLv2 or later
|
||||
|
||||
A Twitter-style microblog for WordPress. Posts, likes, replies, polls, quotes, GIFs, and Gravatars. Use [beep_feed] shortcode or Elementor widget.
|
||||
@@ -40,6 +40,15 @@ Features:
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.7.1 =
|
||||
* Beep wordmark now shown bottom-right of every beep AND reply, with the delete button furthest right.
|
||||
* Wordmark rendered as crisp inline SVG (readable on the dark theme); removed a duplicate wordmark.
|
||||
* Restored self-hosted plugin updates via the Gitea releases API (update badge + one-click update).
|
||||
|
||||
= 1.7.0 =
|
||||
* Shortcode-only rendering ([beep_feed] where you place it); removed auto-inject.
|
||||
* Public compose form (posting still requires login).
|
||||
|
||||
= 1.6.0 =
|
||||
* Elementor drag-and-drop widget for feed placement.
|
||||
* Admin settings page (Settings > Beep) for Giphy API key and branding.
|
||||
|
||||
Reference in New Issue
Block a user