diff --git a/assets/beep.css b/assets/beep.css
index c1d94ba..ac30875 100755
--- a/assets/beep.css
+++ b/assets/beep.css
@@ -947,3 +947,113 @@
justify-content: flex-end;
gap: 8px;
}
+
+/* ---------- Voice note button in composer ---------- */
+.beep-voice-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ color: var(--beep-accent);
+ cursor: pointer;
+ transition: background-color 0.15s ease, color 0.15s ease;
+}
+
+.beep-voice-btn:hover {
+ background: rgba(29, 155, 240, 0.10);
+ color: var(--beep-accent);
+}
+
+.beep-voice-btn svg {
+ display: block;
+ flex-shrink: 0;
+}
+
+/* ---------- Voice note preview in composer ---------- */
+.beep-compose-voice-preview {
+ margin-top: 8px;
+ margin-bottom: 4px;
+}
+
+.beep-compose-voice-preview[hidden] {
+ display: none;
+}
+
+.beep-voice-preview-card {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--beep-hover);
+ border: 1px solid var(--beep-border);
+ border-radius: 9999px;
+ padding: 6px 12px 6px 10px;
+ font-size: 13px;
+ color: var(--beep-text);
+}
+
+.beep-voice-preview-card svg {
+ color: var(--beep-accent);
+ flex-shrink: 0;
+}
+
+.beep-voice-remove {
+ background: transparent;
+ border: 0;
+ color: var(--beep-text-secondary);
+ font-size: 18px;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0 0 0 4px;
+ display: flex;
+ align-items: center;
+ transition: color 0.15s ease;
+}
+
+.beep-voice-remove:hover {
+ color: #F4212E;
+}
+
+/* ---------- Voice note player in posts ---------- */
+.beep-voice-note {
+ margin: 6px 0 2px;
+ background: var(--beep-hover);
+ border: 1px solid var(--beep-border);
+ border-radius: 12px;
+ padding: 10px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.beep-voice-note audio {
+ width: 100%;
+ height: 36px;
+ display: block;
+}
+
+/* Style the audio element across browsers */
+.beep-voice-note audio::-webkit-media-controls-panel {
+ background: var(--beep-bg);
+}
+
+.beep-voice-label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ color: var(--beep-text-secondary);
+ font-weight: 500;
+}
+
+.beep-voice-label svg {
+ color: var(--beep-accent);
+ flex-shrink: 0;
+}
+
+.beep-voice-duration {
+ font-size: 12px;
+ color: var(--beep-text-secondary);
+ font-variant-numeric: tabular-nums;
+}
diff --git a/assets/beep.js b/assets/beep.js
index 68e8b2b..0e94811 100755
--- a/assets/beep.js
+++ b/assets/beep.js
@@ -65,13 +65,23 @@
input.value = '';
refresh();
- // Upload pending images to this new beep
+ // Upload pending images and voice note to this new beep
var pending = compose._beepPendingImages;
+ var pendingVoice = compose._beepPendingVoice;
+ var uploadDone = function () {
+ reloadFeed(feed, 'recent');
+ };
+
+ var tasks = [];
if (pending && pending.length && data.id) {
- uploadPendingImages(data.id, pending, compose).then(function () {
- // Refresh the beep in the DOM to show uploaded images
- reloadFeed(feed, 'recent');
- });
+ tasks.push(uploadPendingImages(data.id, pending, compose));
+ }
+ if (pendingVoice && data.id) {
+ tasks.push(uploadPendingVoice(data.id, pendingVoice, compose));
+ }
+
+ if (tasks.length) {
+ Promise.all(tasks).then(uploadDone);
}
})
.catch(function () {
@@ -341,9 +351,57 @@
var input = compose.querySelector('[data-beep-media-input]');
var preview = compose.querySelector('[data-beep-media-preview]');
var counter = compose.querySelector('[data-beep-media-count]');
- if (!input) return;
+ var voiceInput = compose.querySelector('[data-beep-voice-input]');
+ var voicePreview = compose.querySelector('[data-beep-voice-preview]');
+ if (!input && !voiceInput) return;
var pendingImages = [];
+ var pendingVoice = null; // { file, id }
+
+ // Voice note input
+ if (voiceInput) {
+ voiceInput.addEventListener('change', function (e) {
+ var files = e.target.files;
+ if (!files || !files.length) return;
+ var file = files[0];
+ if (file.size > 20 * 1024 * 1024) {
+ window.alert('Voice note must be under 20 MB.');
+ voiceInput.value = '';
+ return;
+ }
+ var accepted = ['audio/mpeg','audio/ogg','audio/wav','audio/webm','audio/mp4','audio/x-m4a'];
+ if (accepted.indexOf(file.type) === -1) {
+ window.alert('Only MP3, OGG, WAV, WebM, and M4A audio are allowed.');
+ voiceInput.value = '';
+ return;
+ }
+ if (pendingVoice) {
+ window.alert('Only one voice note per beep.');
+ voiceInput.value = '';
+ return;
+ }
+ var reader = new FileReader();
+ reader.onload = function (ev) {
+ var id = 'voice-' + Date.now();
+ pendingVoice = { file: file, id: id };
+ voicePreview.innerHTML = '
';
+ voicePreview.hidden = false;
+ };
+ reader.readAsDataURL(file);
+ voiceInput.value = '';
+ });
+ }
+
+ // Voice remove button
+ if (voicePreview) {
+ voicePreview.addEventListener('click', function (e) {
+ var btn = e.target.closest('[data-beep-voice-remove]');
+ if (!btn) return;
+ pendingVoice = null;
+ voicePreview.innerHTML = '';
+ voicePreview.hidden = true;
+ });
+ }
input.addEventListener('change', function (e) {
var files = Array.prototype.slice.call(e.target.files);
@@ -397,6 +455,7 @@
});
compose._beepPendingImages = pendingImages;
+ compose._beepPendingVoice = pendingVoice;
}
function uploadPendingImages(postId, pendingImages, compose) {
@@ -426,6 +485,29 @@
});
}
+ function uploadPendingVoice(postId, pendingVoice, compose) {
+ if (!pendingVoice) return Promise.resolve();
+ var formData = new FormData();
+ formData.append('file', pendingVoice.file);
+ return fetch(config.restUrl + 'voice/' + postId, {
+ method: 'POST',
+ headers: { 'X-WP-Nonce': config.nonce },
+ credentials: 'same-origin',
+ body: formData
+ })
+ .then(handleResponse)
+ .then(function () {
+ pendingVoice = null;
+ if (compose) {
+ var vp = compose.querySelector('[data-beep-voice-preview]');
+ if (vp) { vp.innerHTML = ''; vp.hidden = true; }
+ }
+ })
+ .catch(function (err) {
+ console.warn('Voice upload failed:', err);
+ });
+ }
+
function handleResponse(res) {
if (!res.ok) return res.json().then(function (j) { throw j; }, function () { throw res; });
return res.json();
diff --git a/includes/class-media.php b/includes/class-media.php
index 31e4174..33e2d32 100644
--- a/includes/class-media.php
+++ b/includes/class-media.php
@@ -217,4 +217,98 @@ class Beep_Media {
delete_post_meta($post_id, self::META_KEY_IMAGES);
delete_post_meta($post_id, self::META_KEY_VIDEO);
}
-}
+ // Voice note support
+ private static $audio_types = ['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/webm', 'audio/mp4', 'audio/x-m4a'];
+ private static $max_audio_size = 20 * 1024 * 1024; // 20 MB
+
+ public static function handle_voice_upload($post_id, $file) {
+ if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) {
+ return new WP_Error('upload_error', 'Upload failed.');
+ }
+ if ($file['size'] > self::$max_audio_size) {
+ return new WP_Error('file_too_large', 'Voice note exceeds 20 MB limit.');
+ }
+ $mime = mime_content_type($file['tmp_name']);
+ if (!in_array($mime, self::$audio_types, true)) {
+ return new WP_Error('invalid_type', 'Only MP3, OGG, WAV, WebM, and M4A audio are allowed.');
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ $file_return = wp_handle_upload($file, [
+ 'test_form' => false,
+ 'mimes' => [
+ 'mp3' => 'audio/mpeg',
+ 'ogg' => 'audio/ogg',
+ 'wav' => 'audio/wav',
+ 'webm' => 'audio/webm',
+ 'm4a' => 'audio/mp4',
+ ],
+ ]);
+
+ if (isset($file_return['error'])) {
+ return new WP_Error('upload_error', $file_return['error']);
+ }
+
+ $attachment = [
+ 'post_mime_type' => $file_return['type'],
+ 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])),
+ 'post_content' => '',
+ 'post_status' => 'inherit',
+ 'guid' => $file_return['url'],
+ ];
+
+ $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $post_id);
+ if (is_wp_error($attachment_id)) {
+ return $attachment_id;
+ }
+
+ $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']);
+ wp_update_attachment_metadata($attachment_id, $attach_data);
+
+ update_post_meta($post_id, 'beep_voice_url', wp_get_attachment_url($attachment_id));
+ update_post_meta($post_id, 'beep_voice_id', $attachment_id);
+
+ return [
+ 'id' => $attachment_id,
+ 'url' => wp_get_attachment_url($attachment_id),
+ ];
+ }
+
+ public static function get_voice_url($post_id) {
+ return esc_url_raw(get_post_meta($post_id, 'beep_voice_url', true));
+ }
+
+ public static function remove_voice($post_id) {
+ $id = (int) get_post_meta($post_id, 'beep_voice_id', true);
+ if ($id) {
+ wp_delete_attachment($id, true);
+ }
+ delete_post_meta($post_id, 'beep_voice_url');
+ delete_post_meta($post_id, 'beep_voice_id');
+ }
+
+ public static function render_voice_note($post_id) {
+ $url = self::get_voice_url($post_id);
+ if (!$url) return '';
+ $duration = '';
+ $meta = get_post_meta($post_id, 'beep_voice_id', true);
+ if ($meta) {
+ $metadata = wp_get_attachment_metadata($meta);
+ if ($metadata && isset($metadata['length_formatted'])) {
+ $duration = $metadata['length_formatted'];
+ }
+ }
+ $dur_html = $duration ? '' . esc_html($duration) . '' : '';
+ return '' .
+ '
' .
+ '
' .
+ $dur_html .
+ '
';
+ }
+
+}
\ No newline at end of file
diff --git a/includes/class-shortcode.php b/includes/class-shortcode.php
index 6e1353a..25ab616 100755
--- a/includes/class-shortcode.php
+++ b/includes/class-shortcode.php
@@ -150,6 +150,13 @@ class Beep_Shortcode {
return is_user_logged_in() && !beep_user_is_banned(get_current_user_id());
},
]);
+ register_rest_route('beep/v1', '/voice/(?P\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) {
@@ -188,4 +195,25 @@ class Beep_Shortcode {
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);
+ }
+
+}
\ No newline at end of file
diff --git a/includes/helpers.php b/includes/helpers.php
index 896ee75..c068f7b 100755
--- a/includes/helpers.php
+++ b/includes/helpers.php
@@ -46,12 +46,23 @@ function beep_render_composer() {
0/4
+
-
+
+
+ ID); ?>
+
+