diff --git a/assets/beep.js b/assets/beep.js
index b048931..60483c8 100755
--- a/assets/beep.js
+++ b/assets/beep.js
@@ -88,26 +88,53 @@
var button = compose.querySelector('[data-beep-submit]');
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() {
autosize(input);
var remaining = limit - countChars(input.value);
counter.textContent = remaining;
counter.classList.toggle('beep-counter-warn', remaining < 20 && 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);
+ // Media / GIF / quote / poll changes also affect whether the button should be enabled.
+ compose.addEventListener('beep:attachments-changed', refresh);
refresh();
button.addEventListener('click', function () {
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;
var quotedPost = compose._beepQuotedPost;
var gifUrl = compose._beepGifUrl;
+ var pending = compose._beepPendingImages;
+ var pendingVoice = compose._beepPendingVoice;
+ var poll = collectPollData(compose);
+
var payload = { content: content };
if (quotedPost && quotedPost.id) payload.quote_id = quotedPost.id;
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', {
method: 'POST',
@@ -128,6 +155,7 @@
clearQuotePreview(compose);
if (compose._beepGifUrl) {
compose._beepGifUrl = null;
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
var gifPrev = compose.querySelector('[data-beep-gif-preview]');
if (gifPrev) { gifPrev.innerHTML = ''; gifPrev.hidden = true; }
}
@@ -308,6 +336,7 @@
if (compose) {
compose._beepQuotedPost = { id: quoted.id, data: quoted };
showQuotePreview(compose, quoted);
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
})
.catch(function() { showToast('Beep not found.', 'error'); });
@@ -344,6 +373,7 @@
var compose = gifRemoveBtn.closest('[data-beep-compose]');
if (compose) {
compose._beepGifUrl = null;
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
var preview = compose.querySelector('[data-beep-gif-preview]');
if (preview) { preview.innerHTML = ''; preview.hidden = true; }
}
@@ -695,6 +725,7 @@
compose._beepPendingVoice = pendingVoice;
voicePreview.innerHTML = '
';
voicePreview.hidden = false;
+ if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
};
reader.readAsDataURL(file);
voiceInput.value = '';
@@ -710,6 +741,7 @@
if (compose) compose._beepPendingVoice = null;
voicePreview.innerHTML = '';
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);
pendingImages.push({ file: file, previewUrl: url, id: id });
renderPreviews();
+ if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
};
reader.readAsDataURL(file);
});
@@ -766,6 +799,7 @@
pendingImages = pendingImages.filter(function (img) { return img.id !== id; });
compose._beepPendingImages = pendingImages;
renderPreviews();
+ if (compose) compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
});
}
@@ -773,6 +807,15 @@
compose._beepPendingImages = pendingImages;
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) {
@@ -886,6 +929,7 @@
.then(function(quoted) {
compose._beepQuotedPost = { id: quotedId, data: quoted };
showQuotePreview(compose, quoted);
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
})
.catch(function(err) { console.warn('Could not load quoted beep:', err); });
}
@@ -919,6 +963,7 @@
var removeBtn = compose.querySelector('[data-beep-quote-remove]');
if (preview) { preview.innerHTML = ''; preview.hidden = true; }
if (removeBtn) removeBtn.hidden = true;
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
/* ---------- GIF Picker (Giphy) ---------- */
@@ -989,6 +1034,7 @@
preview.hidden = false;
}
closeGifPicker();
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
/* ---------- Poll helpers ---------- */
@@ -1005,6 +1051,7 @@
options = pollSection.querySelectorAll('[data-beep-poll-option]');
}
}
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
function addPollOption(compose) {
@@ -1019,6 +1066,7 @@
newRow.innerHTML = '';
optionsContainer.appendChild(newRow);
updatePollButtons(compose);
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
function removePollOption(compose) {
@@ -1029,6 +1077,7 @@
var lastOption = options[options.length - 1];
if (lastOption) lastOption.parentElement.remove();
updatePollButtons(compose);
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
function updatePollButtons(compose) {
@@ -1060,6 +1109,7 @@
options[j].value = '';
}
}
+ compose.dispatchEvent(new CustomEvent('beep:attachments-changed'));
}
function collectPollData(compose) {
diff --git a/beep.php b/beep.php
index 9465338..3fd2dd5 100755
--- a/beep.php
+++ b/beep.php
@@ -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.8.0
+ * Version: 1.8.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.8.0');
+define('BEEP_VERSION', '1.8.1');
define('BEEP_FILE', __FILE__);
define('BEEP_DIR', plugin_dir_path(__FILE__));
define('BEEP_URL', plugin_dir_url(__FILE__));
diff --git a/includes/class-replies.php b/includes/class-replies.php
index 90116cf..122d14e 100755
--- a/includes/class-replies.php
+++ b/includes/class-replies.php
@@ -37,9 +37,6 @@ class Beep_Replies {
public static function submit_beep($request) {
$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) {
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');
$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_type' => 'beep',
'post_content' => $content,
diff --git a/readme.txt b/readme.txt
index 3493d9d..a84049a 100755
--- a/readme.txt
+++ b/readme.txt
@@ -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.8.0
+Stable tag: 1.8.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.
@@ -46,6 +46,10 @@ Features:
== 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 =
* 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.