From 6c9b06ccb207fa32dbcdf26f0b4f65cc8d60fe85 Mon Sep 17 00:00:00 2001 From: Krystie Date: Mon, 18 May 2026 04:01:41 -0700 Subject: [PATCH] Add media support: image uploads, gallery rendering, video oEmbed - New class-media.php: handles image upload via REST, stores in post meta, renders Twitter-style gallery - Added POST /beep/v1/media/{id} and DELETE routes for media management - Updated composer: media upload button, preview area - Updated beep_render_post: displays image gallery + video embed - Supports YouTube, Vimeo, Twitter/X video URLs via oEmbed - Max 4 images per beep, 10MB each (JPEG/PNG/GIF/WebP) --- beep.php | 1 + includes/class-media.php | 220 +++++++++++++++++++++++++++++++++++ includes/class-shortcode.php | 58 +++++++++ includes/helpers.php | 26 +++++ 4 files changed, 305 insertions(+) create mode 100644 includes/class-media.php diff --git a/beep.php b/beep.php index c118224..893d490 100755 --- a/beep.php +++ b/beep.php @@ -26,6 +26,7 @@ require_once BEEP_DIR . 'includes/class-cpt.php'; require_once BEEP_DIR . 'includes/class-likes.php'; require_once BEEP_DIR . 'includes/class-replies.php'; require_once BEEP_DIR . 'includes/class-moderation.php'; +require_once BEEP_DIR . 'includes/class-media.php'; require_once BEEP_DIR . 'includes/class-shortcode.php'; require_once BEEP_DIR . 'includes/class-beep.php'; diff --git a/includes/class-media.php b/includes/class-media.php new file mode 100644 index 0000000..31e4174 --- /dev/null +++ b/includes/class-media.php @@ -0,0 +1,220 @@ + self::$max_file_size) { + return new WP_Error('file_too_large', 'File exceeds 10 MB limit.'); + } + $mime = mime_content_type($file['tmp_name']); + if (!in_array($mime, self::$image_types, true)) { + return new WP_Error('invalid_type', 'Only JPEG, PNG, GIF, and WebP are allowed.'); + } + + $existing = self::get_images($post_id); + if (count($existing) >= self::$max_images) { + return new WP_Error('too_many', 'Maximum ' . self::$max_images . ' images per beep.'); + } + + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + + $file_return = wp_handle_sideload($file, [ + 'test_form' => false, + 'mimes' => [ + 'jpg|jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + ], + ]); + + if (isset($file_return['error'])) { + return new WP_Error('sideload_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); + + add_post_meta($post_id, self::META_KEY_IMAGES, $attachment_id); + + return [ + 'id' => $attachment_id, + 'url' => wp_get_attachment_url($attachment_id), + 'thumb' => self::get_image_url($attachment_id, 'thumbnail'), + 'sizes' => self::get_image_sizes($attachment_id), + ]; + } + + /** + * Remove a single image from a beep post. + */ + public static function remove_image($post_id, $attachment_id) { + $ids = self::get_images($post_id); + $ids = array_values(array_filter($ids, fn($id) => (int) $id !== (int) $attachment_id)); + delete_post_meta($post_id, self::META_KEY_IMAGES); + foreach ($ids as $id) { + add_post_meta($post_id, self::META_KEY_IMAGES, $id); + } + wp_delete_attachment($attachment_id, true); + } + + /** + * Get image attachment IDs for a beep post. + */ + public static function get_images($post_id) { + $ids = get_post_meta($post_id, self::META_KEY_IMAGES, false); + return array_filter(array_map('intval', (array) $ids)); + } + + /** + * Get image srcset-ready sizes for an attachment. + */ + public static function get_image_sizes($attachment_id) { + $sizes = []; + foreach (['thumbnail', 'medium', 'large', 'full'] as $size) { + $img = wp_get_attachment_image_src($attachment_id, $size); + if ($img) { + $sizes[$size] = ['url' => $img[0], 'width' => $img[1], 'height' => $img[2]]; + } + } + return $sizes; + } + + /** + * Get image URL at a given size. + */ + public static function get_image_url($attachment_id, $size = 'medium') { + $img = wp_get_attachment_image_src($attachment_id, $size); + return $img ? $img[0] : ''; + } + + /** + * Set a video URL for a beep post. + */ + public static function set_video_url($post_id, $url) { + $url = esc_url_raw($url); + if (!$url) return new WP_Error('invalid_url', 'Invalid video URL.'); + update_post_meta($post_id, self::META_KEY_VIDEO, $url); + return true; + } + + /** + * Get the video URL for a beep post. + */ + public static function get_video_url($post_id) { + return esc_url_raw(get_post_meta($post_id, self::META_KEY_VIDEO, true)); + } + + /** + * Detect if a URL is a supported video oEmbed. + */ + public static function detect_video_url($url) { + $patterns = [ + 'youtube' => '#https?://(?:www\.)?youtube\.com/watch\?v=[^\s&#]+#', + 'yt_s' => '#https?://youtu\.be/[^\s&#]+#', + 'vimeo' => '#https?://vimeo\.com/\d+#', + 'twitter' => '#https?://(?:twitter\.com|x\.com)/[^\s#/]+/status/\d+#', + ]; + foreach ($patterns as $name => $pattern) { + if (preg_match($pattern, $url)) return $name; + } + return false; + } + + /** + * Render the Twitter-style image gallery. + * + * @param int[] $image_ids + * @return string HTML + */ + public static function render_image_gallery($image_ids) { + if (empty($image_ids)) return ''; + + $count = count($image_ids); + $grid_class = 'beep-gallery'; + if ($count === 1) $grid_class .= ' beep-gallery-1'; + elseif ($count === 2) $grid_class .= ' beep-gallery-2'; + elseif ($count === 3) $grid_class .= ' beep-gallery-3'; + else $grid_class .= ' beep-gallery-4'; + + $html = '
'; + foreach ($image_ids as $i => $id) { + $full = self::get_image_url($id, 'large'); + $thumb = self::get_image_url($id, 'medium'); + if (!$thumb) $thumb = $full; + if (!$full) continue; + $alt = get_post_meta($id, '_wp_attachment_image_alt', true); + $html .= ''; + } + $html .= '
'; + return $html; + } + + /** + * Render video embed via oEmbed. + */ + public static function render_video_embed($url) { + if (!$url) return ''; + $embed = wp_oembed_get($url, ['width' => 560, 'height' => 315]); + if ($embed) { + return '
' . $embed . '
'; + } + return ''; + } + + /** + * Delete all media for a beep post (when beep is deleted). + */ + public static function delete_media($post_id) { + foreach (self::get_images($post_id) as $id) { + wp_delete_attachment($id, true); + } + delete_post_meta($post_id, self::META_KEY_IMAGES); + delete_post_meta($post_id, self::META_KEY_VIDEO); + } +} diff --git a/includes/class-shortcode.php b/includes/class-shortcode.php index 2cc003c..6e1353a 100755 --- a/includes/class-shortcode.php +++ b/includes/class-shortcode.php @@ -8,6 +8,7 @@ class Beep_Shortcode { public static function init() { add_shortcode('beep_feed', [__CLASS__, 'render_feed']); add_action('rest_api_init', [__CLASS__, 'register_routes']); + add_action('rest_api_init', [__CLASS__, 'register_media_routes']); } public static function register_routes() { @@ -130,4 +131,61 @@ class Beep_Shortcode { \d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'upload_media'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + register_rest_route('beep/v1', '/media/(?P\d+)/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [__CLASS__, 'delete_media'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + } + + public static function upload_media($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_upload($post_id, $_FILES['file']); + if (is_wp_error($result)) { + return $result; + } + return rest_ensure_response($result); + } + + public static function delete_media($request) { + $post_id = (int) $request->get_param('post_id'); + $attachment_id = (int) $request->get_param('attachment_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); + } + + Beep_Media::remove_image($post_id, $attachment_id); + return rest_ensure_response(['ok' => true]); + } } diff --git a/includes/helpers.php b/includes/helpers.php index ced1ad7..896ee75 100755 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -40,10 +40,24 @@ function beep_render_composer() { maxlength="" rows="1"> + + + + ID, 'post'); $can_delete = current_user_can('delete_post', $post->ID); $can_interact = is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + $beep_images = Beep_Media::get_images($post->ID); + $beep_video = Beep_Media::get_video_url($post->ID); ob_start(); ?> @@ -91,6 +107,16 @@ function beep_render_post($post) {
+ +
+ +
+ + +
+ +
+