v1.8.1: allow media-only beeps (image/GIF/voice/quote/poll without text)

- PHP: submit_beep accepts empty content when any attachment flag is set
- JS: composer button enables when text is empty but media is attached
- JS: button refreshes on every media state change via custom event
- Version bump + readme changelog
This commit is contained in:
Krystie
2026-07-04 00:41:33 -07:00
parent 5bb39316b1
commit 64344bfef6
4 changed files with 74 additions and 8 deletions
+52 -2
View File
@@ -88,26 +88,53 @@
var button = compose.querySelector('[data-beep-submit]'); var button = compose.querySelector('[data-beep-submit]');
if (!input || !button) return; if (!input || !button) return;
function hasAttachments() {
var pendingImages = compose._beepPendingImages;
var pendingVoice = compose._beepPendingVoice;
var hasGif = !!compose._beepGifUrl;
var hasQuote = !!(compose._beepQuotedPost && compose._beepQuotedPost.id);
var hasPoll = !!collectPollData(compose);
return (
(pendingImages && pendingImages.length) ||
pendingVoice ||
hasGif ||
hasQuote ||
hasPoll
);
}
function refresh() { function refresh() {
autosize(input); autosize(input);
var remaining = limit - countChars(input.value); var remaining = limit - countChars(input.value);
counter.textContent = remaining; counter.textContent = remaining;
counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0); counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0);
counter.classList.toggle('beep-counter-bad', remaining < 0); counter.classList.toggle('beep-counter-bad', remaining < 0);
button.disabled = input.value.trim() === '' || remaining < 0; // Button is enabled when there's text OR any attachment (images, voice, GIF, quote, poll).
button.disabled = !(input.value.trim() !== '' || hasAttachments()) || remaining < 0;
} }
input.addEventListener('input', refresh); input.addEventListener('input', refresh);
// Media / GIF / quote / poll changes also affect whether the button should be enabled.
compose.addEventListener('beep:attachments-changed', refresh);
refresh(); refresh();
button.addEventListener('click', function () { button.addEventListener('click', function () {
var content = input.value.trim(); var content = input.value.trim();
if (!content) return; // Allow image-only / media-only posts. Gate is "must have text OR an attachment".
if (!content && !hasAttachments()) return;
button.disabled = true; button.disabled = true;
var quotedPost = compose._beepQuotedPost; var quotedPost = compose._beepQuotedPost;
var gifUrl = compose._beepGifUrl; var gifUrl = compose._beepGifUrl;
var pending = compose._beepPendingImages;
var pendingVoice = compose._beepPendingVoice;
var poll = collectPollData(compose);
var payload = { content: content }; var payload = { content: content };
if (quotedPost && quotedPost.id) payload.quote_id = quotedPost.id; if (quotedPost && quotedPost.id) payload.quote_id = quotedPost.id;
if (gifUrl) payload.gif_url = gifUrl; if (gifUrl) payload.gif_url = gifUrl;
// Tell the server what attachments are coming so it can accept empty content.
if (pending && pending.length) payload.has_images = 1;
if (pendingVoice) payload.has_voice = 1;
if (poll) payload.has_poll = 1;
fetch(config.restUrl + 'post', { fetch(config.restUrl + 'post', {
method: 'POST', method: 'POST',
@@ -128,6 +155,7 @@
clearQuotePreview(compose); clearQuotePreview(compose);
if (compose._beepGifUrl) { if (compose._beepGifUrl) {
compose._beepGifUrl = null; compose._beepGifUrl = null;
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
var gifPrev = compose.querySelector('[data-beep-gif-preview]'); var gifPrev = compose.querySelector('[data-beep-gif-preview]');
if (gifPrev) { gifPrev.innerHTML = ''; gifPrev.hidden = true; } if (gifPrev) { gifPrev.innerHTML = ''; gifPrev.hidden = true; }
} }
@@ -308,6 +336,7 @@
if (compose) { if (compose) {
compose._beepQuotedPost = { id: quoted.id, data: quoted }; compose._beepQuotedPost = { id: quoted.id, data: quoted };
showQuotePreview(compose, quoted); showQuotePreview(compose, quoted);
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
} }
}) })
.catch(function() { showToast('Beep not found.', 'error'); }); .catch(function() { showToast('Beep not found.', 'error'); });
@@ -344,6 +373,7 @@
var compose = gifRemoveBtn.closest('[data-beep-compose]'); var compose = gifRemoveBtn.closest('[data-beep-compose]');
if (compose) { if (compose) {
compose._beepGifUrl = null; compose._beepGifUrl = null;
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
var preview = compose.querySelector('[data-beep-gif-preview]'); var preview = compose.querySelector('[data-beep-gif-preview]');
if (preview) { preview.innerHTML = ''; preview.hidden = true; } if (preview) { preview.innerHTML = ''; preview.hidden = true; }
} }
@@ -695,6 +725,7 @@
compose._beepPendingVoice = pendingVoice; compose._beepPendingVoice = pendingVoice;
voicePreview.innerHTML = '<div class="beep-voice-preview-card"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08C16.39 17.43 19 14.53 19 11h-2z"/></svg><span>Voice note ready</span><button class="beep-voice-remove" data-beep-voice-remove type="button" title="Remove">&times;</button></div>'; voicePreview.innerHTML = '<div class="beep-voice-preview-card"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08C16.39 17.43 19 14.53 19 11h-2z"/></svg><span>Voice note ready</span><button class="beep-voice-remove" data-beep-voice-remove type="button" title="Remove">&times;</button></div>';
voicePreview.hidden = false; voicePreview.hidden = false;
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
voiceInput.value = ''; voiceInput.value = '';
@@ -710,6 +741,7 @@
if (compose) compose._beepPendingVoice = null; if (compose) compose._beepPendingVoice = null;
voicePreview.innerHTML = ''; voicePreview.innerHTML = '';
voicePreview.hidden = true; voicePreview.hidden = true;
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}); });
} }
@@ -731,6 +763,7 @@
var id = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6); var id = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
pendingImages.push({ file: file, previewUrl: url, id: id }); pendingImages.push({ file: file, previewUrl: url, id: id });
renderPreviews(); renderPreviews();
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
@@ -766,6 +799,7 @@
pendingImages = pendingImages.filter(function (img) { return img.id !== id; }); pendingImages = pendingImages.filter(function (img) { return img.id !== id; });
compose._beepPendingImages = pendingImages; compose._beepPendingImages = pendingImages;
renderPreviews(); renderPreviews();
if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}); });
} }
@@ -773,6 +807,15 @@
compose._beepPendingImages = pendingImages; compose._beepPendingImages = pendingImages;
compose._beepPendingVoice = pendingVoice; compose._beepPendingVoice = pendingVoice;
} }
// Poll input changes also affect "has attachments" (poll becomes valid mid-typing).
if (compose) {
compose.addEventListener('input', function (e) {
if (e.target.matches('[data-beep-poll-question], [data-beep-poll-option]')) {
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
});
}
} }
function uploadPendingImages(postId, pendingImages, compose) { function uploadPendingImages(postId, pendingImages, compose) {
@@ -886,6 +929,7 @@
.then(function(quoted) { .then(function(quoted) {
compose._beepQuotedPost = { id: quotedId, data: quoted }; compose._beepQuotedPost = { id: quotedId, data: quoted };
showQuotePreview(compose, quoted); showQuotePreview(compose, quoted);
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}) })
.catch(function(err) { console.warn('Could not load quoted beep:', err); }); .catch(function(err) { console.warn('Could not load quoted beep:', err); });
} }
@@ -919,6 +963,7 @@
var removeBtn = compose.querySelector('[data-beep-quote-remove]'); var removeBtn = compose.querySelector('[data-beep-quote-remove]');
if (preview) { preview.innerHTML = ''; preview.hidden = true; } if (preview) { preview.innerHTML = ''; preview.hidden = true; }
if (removeBtn) removeBtn.hidden = true; if (removeBtn) removeBtn.hidden = true;
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
} }
/* ---------- GIF Picker (Giphy) ---------- */ /* ---------- GIF Picker (Giphy) ---------- */
@@ -989,6 +1034,7 @@
preview.hidden = false; preview.hidden = false;
} }
closeGifPicker(); closeGifPicker();
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
} }
/* ---------- Poll helpers ---------- */ /* ---------- Poll helpers ---------- */
@@ -1005,6 +1051,7 @@
options = pollSection.querySelectorAll('[data-beep-poll-option]'); options = pollSection.querySelectorAll('[data-beep-poll-option]');
} }
} }
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
} }
function addPollOption(compose) { function addPollOption(compose) {
@@ -1019,6 +1066,7 @@
newRow.innerHTML = '<input type="text" class="beep-poll-option-input" data-beep-poll-option placeholder="Option ' + (optionCount + 1) + '" maxlength="100">'; newRow.innerHTML = '<input type="text" class="beep-poll-option-input" data-beep-poll-option placeholder="Option ' + (optionCount + 1) + '" maxlength="100">';
optionsContainer.appendChild(newRow); optionsContainer.appendChild(newRow);
updatePollButtons(compose); updatePollButtons(compose);
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
} }
function removePollOption(compose) { function removePollOption(compose) {
@@ -1029,6 +1077,7 @@
var lastOption = options[options.length - 1]; var lastOption = options[options.length - 1];
if (lastOption) lastOption.parentElement.remove(); if (lastOption) lastOption.parentElement.remove();
updatePollButtons(compose); updatePollButtons(compose);
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
} }
function updatePollButtons(compose) { function updatePollButtons(compose) {
@@ -1060,6 +1109,7 @@
options[j].value = ''; options[j].value = '';
} }
} }
compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
} }
function collectPollData(compose) { function collectPollData(compose) {
+2 -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.8.0 * Version: 1.8.1
* 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.8.0'); define('BEEP_VERSION', '1.8.1');
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__));
+15 -3
View File
@@ -37,9 +37,6 @@ class Beep_Replies {
public static function submit_beep($request) { public static function submit_beep($request) {
$content = sanitize_textarea_field($request->get_param('content')); $content = sanitize_textarea_field($request->get_param('content'));
if (empty($content)) {
return new WP_Error('empty_content', 'Beep cannot be empty.', ['status' => 400]);
}
if (mb_strlen($content) > BEEP_CHAR_LIMIT) { if (mb_strlen($content) > BEEP_CHAR_LIMIT) {
return new WP_Error('content_too_long', 'Beep exceeds ' . BEEP_CHAR_LIMIT . ' character limit.', ['status' => 400]); return new WP_Error('content_too_long', 'Beep exceeds ' . BEEP_CHAR_LIMIT . ' character limit.', ['status' => 400]);
} }
@@ -48,6 +45,21 @@ class Beep_Replies {
$quote_id = $request->get_param('quote_id'); $quote_id = $request->get_param('quote_id');
$gif_url = $request->get_param('gif_url'); $gif_url = $request->get_param('gif_url');
// Allow empty content when media / quote / gif / poll / voice / video is attached.
// The composer is expected to gate this client-side too, but the server is the source of truth.
$has_payload = (
$content !== ''
|| ($quote_id && is_numeric($quote_id))
|| ($gif_url && filter_var($gif_url, FILTER_VALIDATE_URL))
|| !empty($request->get_param('has_images'))
|| !empty($request->get_param('has_voice'))
|| !empty($request->get_param('has_video'))
|| !empty($request->get_param('has_poll'))
);
if (!$has_payload) {
return new WP_Error('empty_content', 'Beep cannot be empty.', ['status' => 400]);
}
$post_id = wp_insert_post([ $post_id = wp_insert_post([
'post_type' => 'beep', 'post_type' => 'beep',
'post_content' => $content, 'post_content' => $content,
+5 -1
View File
@@ -4,7 +4,7 @@ Tags: microblog, twitter, tweets, social, comments, polls, elementor
Requires at least: 6.0 Requires at least: 6.0
Tested up to: 6.9 Tested up to: 6.9
Requires PHP: 7.4 Requires PHP: 7.4
Stable tag: 1.8.0 Stable tag: 1.8.1
License: GPLv2 or later 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. A Twitter-style microblog for WordPress. Posts, likes, replies, polls, quotes, GIFs, and Gravatars. Use [beep_feed] shortcode or Elementor widget.
@@ -46,6 +46,10 @@ Features:
== Changelog == == Changelog ==
= 1.8.1 =
* Image-only / media-only posts: you can now post images, GIFs, voice notes, quotes, or polls without typing any words. The Beep button enables automatically when any attachment is present (server-side too — empty content + no attachments still returns empty_content).
* The composer button refreshes whenever media is added or removed (images, voice, GIF, quote, poll).
= 1.8.0 = = 1.8.0 =
* Twitter-style feed: the timeline now shows only top-level beeps; clicking a beep (or its reply count) opens the full conversation in a modal with working reply forms, likes, and nested threads. * Twitter-style feed: the timeline now shows only top-level beeps; clicking a beep (or its reply count) opens the full conversation in a modal with working reply forms, likes, and nested threads.
* Light theme + auto mode: theme="auto|dark|light" shortcode attribute; auto follows the visitor's system preference. * Light theme + auto mode: theme="auto|dark|light" shortcode attribute; auto follows the visitor's system preference.