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 = '
Voice note ready
'; 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.