316 lines
12 KiB
PHP
Executable File
316 lines
12 KiB
PHP
Executable File
<?php
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
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']);
|
|
add_action('rest_api_init', [__CLASS__, 'register_thread_routes']);
|
|
add_action('rest_api_init', [__CLASS__, 'register_quote_routes']);
|
|
}
|
|
|
|
public static function register_routes() {
|
|
register_rest_route('beep/v1', '/feed', [
|
|
'methods' => '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();
|
|
?>
|
|
<div class="beep-feed beep-embed beep-theme-<?php echo esc_attr($theme); ?>" data-beep-feed>
|
|
<header class="beep-feed-header">
|
|
<?php if ($atts['header'] !== '') : ?>
|
|
<span class="beep-feed-header-title"><?php echo esc_html($atts['header']); ?></span>
|
|
<?php endif; ?>
|
|
<?php if ($show_wordmark) : ?>
|
|
<img class="beep-feed-wordmark" src="<?php echo esc_url(BEEP_URL . 'assets/beep-wordmark.png?v=' . BEEP_VERSION); ?>" alt="Beep!">
|
|
<?php endif; ?>
|
|
</header>
|
|
|
|
<?php if ($has_posts) : ?>
|
|
<div class="beep-tabs" data-beep-tabs role="tablist">
|
|
<button type="button" class="beep-tab <?php echo $sort === 'latest' ? 'is-active' : ''; ?>"
|
|
data-beep-sort="latest" role="tab"><span>Latest</span></button>
|
|
<button type="button" class="beep-tab <?php echo $sort === 'top' ? 'is-active' : ''; ?>"
|
|
data-beep-sort="top" role="tab"><span>Top</span></button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php echo beep_render_composer(); ?>
|
|
|
|
<div class="beep-list" data-beep-list>
|
|
<?php
|
|
if ($has_posts) {
|
|
foreach ($posts as $p) {
|
|
echo beep_render_post($p);
|
|
}
|
|
} else {
|
|
echo self::empty_state_html();
|
|
}
|
|
?>
|
|
</div><!-- .beep-list -->
|
|
|
|
<?php echo beep_render_thread_modal() . beep_render_gif_modal(); ?>
|
|
</div><!-- .beep-feed -->
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Empty-state markup shared by shortcode and REST.
|
|
*/
|
|
public static function empty_state_html() {
|
|
$empty_bird = beep_bird_svg(['size' => 64]);
|
|
return '<div class="beep-empty"><div class="beep-empty-icon">' . $empty_bird . '</div><p>No beeps yet.</p><span>Be the first to share something!</span></div>';
|
|
}
|
|
|
|
/**
|
|
* Register additional REST routes for media upload.
|
|
*/
|
|
public static function register_media_routes() {
|
|
register_rest_route('beep/v1', '/media/(?P<post_id>\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<post_id>\d+)/(?P<attachment_id>\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<post_id>\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<post_id>\d+)', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_thread'],
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
register_rest_route('beep/v1', '/thread/(?P<post_id>\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<post_id>\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);
|
|
}
|
|
}
|