feat: wordmark on every beep + self-hosted Gitea updates (v1.7.0)

- Show the Beep wordmark bottom-right of every beep and reply, beside the
  delete button (delete stays furthest right).
- Add Beep_Updater: WordPress-native plugin updates backed by the Gitea
  releases API, with token auth for the private repo, "View details"
  changelog, archive-folder normalization, and a "Check for updates" link.
- Bump version to 1.7.0; sync readme.txt stable tag + changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krystie
2026-06-07 02:13:50 -07:00
parent d2c8ca6a47
commit 2788dc8d96
6 changed files with 384 additions and 11 deletions
+29
View File
@@ -57,6 +57,35 @@ This is the easiest way to let a small circle into your Beep feed without settin
Swap the bird/wordmark in `assets/`. The accent color is `#1D9BF0`; find/replace in `assets/beep.css` for a different palette. Swap the bird/wordmark in `assets/`. The accent color is `#1D9BF0`; find/replace in `assets/beep.css` for a different palette.
## Updates
Beep updates like a normal WordPress plugin — the "update available" badge appears
in **Plugins → Updates** and one-click update works. New versions are pulled from
the Gitea repo's **Releases**, not WordPress.org.
**One-time setup** (private repo) — add to `wp-config.php`:
```php
define('BEEP_UPDATE_TOKEN', 'your-gitea-access-token');
// Optional overrides (defaults shown):
// define('BEEP_UPDATE_GITEA_BASE', 'https://git.sami');
// define('BEEP_UPDATE_REPO', 'sami7777/beep');
```
The token is only read from `wp-config.php` (or the `beep_update_token` filter) — it
is never stored in the repo. Give it read access to the repo.
**To ship an update:**
1. Bump the version in **both** places in `beep.php` (the `Version:` header and
`BEEP_VERSION`), commit, and push.
2. On Gitea, create a **Release** whose tag is the new version (`v1.7.0` or `1.7.0`).
Optionally attach a `beep.zip` asset; otherwise the source archive is used.
Within ~12 hours (or immediately via the **Check for updates** link on the Plugins
screen) every site running Beep will see the update and can install it in one click.
The release description is shown as the changelog in the "View details" popup.
## What's not in this version ## What's not in this version
Deliberate omissions to keep the plugin understandable: Deliberate omissions to keep the plugin understandable:
+18
View File
@@ -709,6 +709,24 @@
.beep-embed .beep-feed-header { padding: 12px 16px; } .beep-embed .beep-feed-header { padding: 12px 16px; }
} }
/* Post footer: wordmark + delete, pinned bottom-right (delete furthest right) */
.beep-embed .beep-post-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
margin-top: 6px;
}
.beep-embed .beep-post-wordmark {
height: 16px;
width: auto;
opacity: 0.55;
flex: 0 0 auto;
}
.beep-embed .beep-reply-footer .beep-post-wordmark {
height: 13px;
}
/* Delete */ /* Delete */
.beep-embed .beep-delete-row { display: flex; justify-content: flex-end; margin-top: 6px; } .beep-embed .beep-delete-row { display: flex; justify-content: flex-end; margin-top: 6px; }
+9 -2
View File
@@ -2,7 +2,7 @@
/** /**
* Plugin Name: Beep * Plugin Name: Beep
* Description: A Twitter-style microblog for WordPress, with likes, replies, and Gravatars. Use the [beep_feed] shortcode on any page. * Description: A Twitter-style microblog for WordPress, with likes, replies, and Gravatars. Use the [beep_feed] shortcode on any page.
* Version: 1.6.0 * Version: 1.7.0
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 7.4 * Requires PHP: 7.4
* Author: Sami Ahmed * Author: Sami Ahmed
@@ -14,7 +14,7 @@ if (!defined('ABSPATH')) {
exit; exit;
} }
define('BEEP_VERSION', '1.6.0'); define('BEEP_VERSION', '1.7.0');
define('BEEP_FILE', __FILE__); define('BEEP_FILE', __FILE__);
define('BEEP_DIR', plugin_dir_path(__FILE__)); define('BEEP_DIR', plugin_dir_path(__FILE__));
define('BEEP_URL', plugin_dir_url(__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-polls.php';
require_once BEEP_DIR . 'includes/class-shortcode.php'; require_once BEEP_DIR . 'includes/class-shortcode.php';
require_once BEEP_DIR . 'includes/class-settings.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'; require_once BEEP_DIR . 'includes/class-beep.php';
register_activation_hook(__FILE__, ['Beep_Plugin', 'activate']); register_activation_hook(__FILE__, ['Beep_Plugin', 'activate']);
register_deactivation_hook(__FILE__, ['Beep_Plugin', 'deactivate']); register_deactivation_hook(__FILE__, ['Beep_Plugin', 'deactivate']);
add_action('plugins_loaded', ['Beep_Plugin', 'init']); 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();
}
+309
View File
@@ -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;
});
+14 -8
View File
@@ -267,15 +267,18 @@ function beep_render_post($post) {
title="Quote"> title="Quote">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5H6v2H4v12h2v-2h2v2h8V9h-2V7h-2v2H8V5zm10 0v2h2v12h-2v-2h-2v2H8V9h2v2h2V7h2V5h2z"/></svg> <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> </a>
<?php if ($can_delete) : ?> <div class="beep-post-footer">
<div class="beep-delete-row"> <img class="beep-post-wordmark"
src="<?php echo esc_url(Beep_Settings::get('logo_url') ?: BEEP_URL . 'assets/beep-wordmark.png'); ?>"
alt="Beep">
<?php if ($can_delete) : ?>
<button class="beep-delete-link" <button class="beep-delete-link"
data-beep-delete="<?php echo (int) $post->ID; ?>" data-beep-delete="<?php echo (int) $post->ID; ?>"
type="button" type="button"
aria-label="Delete this beep" aria-label="Delete this beep"
title="Delete">&times;</button> title="Delete">&times;</button>
</div> <?php endif; ?>
<?php endif; ?> </div>
<?php if ($can_interact) : ?> <?php if ($can_interact) : ?>
<?php echo beep_render_reply_form($post->ID, 0); ?> <?php echo beep_render_reply_form($post->ID, 0); ?>
@@ -398,15 +401,18 @@ function beep_render_reply($comment, $all_comments = [], $depth = 0, $post_id =
</div> </div>
<?php echo beep_render_reply_form($post_id, $comment->comment_ID); ?> <?php echo beep_render_reply_form($post_id, $comment->comment_ID); ?>
<?php endif; ?> <?php endif; ?>
<?php if ($can_delete) : ?> <div class="beep-post-footer beep-reply-footer">
<div class="beep-delete-row"> <img class="beep-post-wordmark"
src="<?php echo esc_url(Beep_Settings::get('logo_url') ?: BEEP_URL . 'assets/beep-wordmark.png'); ?>"
alt="Beep">
<?php if ($can_delete) : ?>
<button class="beep-delete-link" <button class="beep-delete-link"
data-beep-delete-reply="<?php echo (int) $comment->comment_ID; ?>" data-beep-delete-reply="<?php echo (int) $comment->comment_ID; ?>"
type="button" type="button"
aria-label="Delete this reply" aria-label="Delete this reply"
title="Delete">&times;</button> title="Delete">&times;</button>
</div> <?php endif; ?>
<?php endif; ?> </div>
<?php <?php
// Recursively render children // Recursively render children
+5 -1
View File
@@ -4,7 +4,7 @@ Tags: microblog, twitter, tweets, social, comments
Requires at least: 6.0 Requires at least: 6.0
Tested up to: 6.7 Tested up to: 6.7
Requires PHP: 7.4 Requires PHP: 7.4
Stable tag: 1.4.0 Stable tag: 1.7.0
License: GPLv2 or later License: GPLv2 or later
A Twitter-style microblog for WordPress. Logged-in users post short updates, A Twitter-style microblog for WordPress. Logged-in users post short updates,
@@ -29,6 +29,10 @@ sort tabs are all built in.
== Changelog == == Changelog ==
= 1.7.0 =
* Beep wordmark now shown bottom-right of every beep and reply, beside the delete button.
* Self-hosted plugin updates via the Gitea releases API (update badge + one-click update).
= 1.4.0 = = 1.4.0 =
* Like replies, not just beeps. * Like replies, not just beeps.
* Nested replies (reply to replies, indented up to two levels). * Nested replies (reply to replies, indented up to two levels).