'GET', 'callback' => [__CLASS__, 'fetch_feed'], 'permission_callback' => '__return_true', 'args' => [ 'sort' => ['default' => 'latest'], 'limit' => ['default' => 50, 'validate_callback' => function ($v) { return is_numeric($v); }], 'offset' => ['default' => 0, 'validate_callback' => function ($v) { return is_numeric($v); }], ], ]); } /** * REST endpoint: re-render the feed list with the given sort. */ public static function fetch_feed($request) { $sort = $request->get_param('sort') === 'top' ? 'top' : 'latest'; $limit = max(1, (int) $request->get_param('limit')); $offset = max(0, (int) $request->get_param('offset')); $posts = ($sort === 'top') ? self::get_top_beeps($limit + 1, $offset) : self::get_latest_beeps($limit + 1, $offset); $has_more = count($posts) > $limit; if ($has_more) { $posts = array_slice($posts, 0, $limit); } $html = ''; foreach ($posts as $p) { $html .= beep_render_post($p); } if ($html === '' && $offset === 0) { $html = self::empty_state_html(); } return rest_ensure_response(['html' => $html, 'sort' => $sort, 'has_more' => $has_more, 'offset' => $offset + count($posts)]); } public static function get_latest_beeps($limit, $offset = 0) { global $wpdb; return $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish' ORDER BY post_date DESC LIMIT %d OFFSET %d", Beep_CPT::POST_TYPE, $limit, $offset )); } public static function get_top_beeps($limit, $offset = 0) { global $wpdb; $likes = Beep_Likes::table_name(); return $wpdb->get_results($wpdb->prepare( "SELECT p.*, COALESCE(l.cnt, 0) AS like_count FROM {$wpdb->posts} p LEFT JOIN ( SELECT post_id, COUNT(*) AS cnt FROM $likes WHERE object_type = 'post' GROUP BY post_id ) l ON l.post_id = p.ID WHERE p.post_type = %s AND p.post_status = 'publish' ORDER BY COALESCE(l.cnt, 0) DESC, p.post_date DESC LIMIT %d OFFSET %d", Beep_CPT::POST_TYPE, $limit, $offset )); } public static function render_feed($atts) { $atts = shortcode_atts([ 'limit' => 50, 'header' => 'Messages from', 'show_wordmark' => 'yes', 'sort' => 'latest', 'theme' => 'auto', // auto | dark | light ], $atts, 'beep_feed'); $limit = max(1, (int) $atts['limit']); $sort = ($atts['sort'] === 'top') ? 'top' : 'latest'; $theme = in_array($atts['theme'], ['dark', 'light'], true) ? $atts['theme'] : 'auto'; $show_wordmark = !in_array(strtolower((string) $atts['show_wordmark']), ['no', 'false', '0'], true); $posts = ($sort === 'top') ? self::get_top_beeps($limit) : self::get_latest_beeps($limit); $has_posts = !empty($posts); ob_start(); ?>
Beep! Beep!
64]); return '
' . $empty_bird . '

No beeps yet.

Be the first to share something!
'; } /** * Register additional REST routes for media upload. */ public static function register_media_routes() { register_rest_route('beep/v1', '/media/(?P\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()); }, ]); 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) { $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]); } 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); } public static function register_thread_routes() { register_rest_route('beep/v1', '/thread/(?P\d+)', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_thread'], 'permission_callback' => '__return_true', ]); register_rest_route('beep/v1', '/thread/(?P\d+)/html', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_thread_html'], 'permission_callback' => '__return_true', ]); } /** * REST endpoint: server-rendered conversation view (root post + replies tree). * Reuses the exact same markup/styles as the feed. */ public static function get_thread_html($request) { $post_id = (int) $request->get_param('post_id'); $post = get_post($post_id); if (!$post || $post->post_type !== Beep_CPT::POST_TYPE || $post->post_status !== 'publish') { return new WP_Error('not_found', 'Beep not found.', 404); } $html = beep_render_post($post, ['in_thread' => true, 'show_replies' => true]); return rest_ensure_response([ 'html' => $html, 'id' => $post_id, 'reply_count' => Beep_Replies::reply_count($post_id), ]); } public static function get_thread($request) { $post_id = (int) $request->get_param('post_id'); if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) { return new WP_Error('not_found', 'Beep not found.', 404); } $depth = (int) $request->get_param('depth') ?: 10; $thread = Beep_Replies::get_thread($post_id, 0, $depth); return rest_ensure_response($thread); } public static function register_quote_routes() { register_rest_route('beep/v1', '/quote/(?P\d+)', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_quoted_post'], 'permission_callback' => '__return_true', ]); register_rest_route('beep/v1', '/lookup', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'lookup_beep'], 'permission_callback' => '__return_true', ]); } public static function get_quoted_post($request) { $post_id = (int) $request->get_param('post_id'); $quoted = Beep_Quotes::get_quoted_post($post_id); if (!$quoted) { return new WP_Error('not_found', 'No quoted post found.', 404); } return rest_ensure_response($quoted); } public static function lookup_beep($request) { $query = sanitize_text_field($request->get_param('q')); if (!$query) { return new WP_Error('missing_query', 'Query parameter q is required.', 400); } if (is_numeric($query)) { $post = get_post((int) $query); if ($post && $post->post_type === Beep_CPT::POST_TYPE) { return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); } } if (preg_match('/\[beep:(\d+)\]/', $query, $m)) { $post = get_post((int) $m[1]); if ($post && $post->post_type === Beep_CPT::POST_TYPE) { return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); } } return new WP_Error('not_found', 'Beep not found.', 404); } }