590 lines
32 KiB
PHP
Executable File
590 lines
32 KiB
PHP
Executable File
<?php
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* The Beep bird icon rendered from Sami's real PNG asset.
|
|
* Uses the black-silhouette PNG + CSS filter for brand colour.
|
|
*
|
|
* @param array $args {
|
|
* Optional. Display args.
|
|
* @type int $size Width/height in px (default 24).
|
|
* @type string $fill Colour applied via CSS filter (default #0064cd).
|
|
* @type string $class Extra CSS class(es).
|
|
* }
|
|
*/
|
|
function beep_bird_svg( $args = [] ) {
|
|
$size = isset( $args['size'] ) ? (int) $args['size'] : 24;
|
|
$fill = isset( $args['fill'] ) ? $args['fill'] : '#0064cd';
|
|
$class = isset( $args['class'] ) ? trim( $args['class'] ) : '';
|
|
$extra = $class ? ' class="' . esc_attr( $class ) . '"' : '';
|
|
|
|
// Map brand colour to a CSS filter that turns black → target colour.
|
|
// Hue-rotate value derived from the target hex.
|
|
// #0064cd → hue-rotate ~190deg, #4a90c4 → hue-rotate ~200deg.
|
|
$filter = 'brightness(0) saturate(100%) invert(22%) sepia(100%) saturate(10000%) hue-rotate(190deg)';
|
|
|
|
return '<svg' . $extra
|
|
. ' viewBox="0 0 24 24" width="' . $size . '" height="' . $size . '"'
|
|
. ' xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'
|
|
. '<foreignObject x="0" y="0" width="24" height="24">'
|
|
. '<img xmlns="http://www.w3.org/1999/xhtml"'
|
|
. ' src="' . esc_url( BEEP_URL . 'assets/beep-bird.png' ) . '"'
|
|
. ' alt="" style="width:100%;height:100%;object-fit:contain;filter:' . esc_attr( $filter ) . ';"/>'
|
|
. '</foreignObject>'
|
|
. '</svg>';
|
|
}
|
|
|
|
/**
|
|
* Wordmark (bird + "Beep!") shown at the top of the feed.
|
|
* Uses the real Sami-designed bird PNG with CSS color filter
|
|
* to match brand colours.
|
|
*/
|
|
function beep_logo_svg() {
|
|
// Allow site-owners to swap in a custom logo via settings.
|
|
$custom = Beep_Settings::get( 'logo_url' );
|
|
if ( $custom ) {
|
|
return '<img class="beep-logo" src="' . esc_url( $custom ) . '" alt="Beep">';
|
|
}
|
|
|
|
// Real bird PNG as <img> — CSS filter turns black silhouette → brand blue.
|
|
$filter = 'brightness(0) saturate(100%) invert(30%) sepia(100%) saturate(10000%) hue-rotate(190deg)';
|
|
return '<img class="beep-logo" src="' . esc_url( BEEP_URL . 'assets/beep-icon.png' ) . '"'
|
|
. ' alt="Beep!" style="height:32px;width:auto;filter:' . esc_attr( $filter ) . ';">';
|
|
}
|
|
|
|
/**
|
|
* Is this user banned from posting/liking/replying?
|
|
*/
|
|
function beep_user_is_banned($user_id) {
|
|
if (!$user_id) {
|
|
return false;
|
|
}
|
|
return get_user_meta($user_id, 'beep_banned', true) === '1';
|
|
}
|
|
|
|
/**
|
|
* Render the composer textarea (for logged-in, non-banned users).
|
|
*/
|
|
function beep_render_composer() {
|
|
$user = wp_get_current_user();
|
|
$beep_avatar = get_user_meta($post->post_author, 'beep_avatar', true);
|
|
if ($beep_avatar) {
|
|
$avatar_url = $beep_avatar;
|
|
$filter = '';
|
|
} elseif ($author && !empty($author->user_email)) {
|
|
$avatar_url = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email)) . '?s=96&d=mp';
|
|
$filter = '';
|
|
} else {
|
|
$avatar_url = BEEP_URL . 'assets/beep-bird.png';
|
|
$filter = 'brightness(0) saturate(100%) invert(22%) sepia(100%) saturate(10000%) hue-rotate(190deg)';
|
|
}
|
|
$avatar = '<img class="beep-avatar" src="' . esc_url($avatar_url) . '" alt="" style="width:48px;height:48px;object-fit:contain;' . ($filter ? 'filter:' . esc_attr($filter) : '') . '">';
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="beep-compose" data-beep-compose>
|
|
<?php echo $avatar; ?>
|
|
<div class="beep-compose-body">
|
|
<textarea
|
|
class="beep-input beep-compose-input"
|
|
data-beep-input
|
|
placeholder="What's happening?"
|
|
maxlength="<?php echo (int) BEEP_CHAR_LIMIT; ?>"
|
|
rows="1"></textarea>
|
|
<div class="beep-compose-footer">
|
|
<div class="beep-compose-tools">
|
|
<label class="beep-media-btn" title="Add up to 4 images">
|
|
<input type="file" accept="image/jpeg,image/png,image/gif,image/webp" multiple data-beep-media-input hidden>
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
|
|
</label>
|
|
<span class="beep-media-count" data-beep-media-count hidden>0/4</span>
|
|
<label class="beep-voice-btn" title="Add voice note">
|
|
<input type="file" accept="audio/mpeg,audio/ogg,audio/wav,audio/webm,audio/mp4,audio/x-m4a" data-beep-voice-input hidden>
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08C16.39 17.43 19 14.53 19 11h-2z"/></svg>
|
|
</label>
|
|
<label class="beep-quote-composer-btn" title="Quote a beep">
|
|
<input type="button" data-beep-quote-composer-btn hidden>
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M8 5H6v2H4v12h2v-2h2v2h8V9h-2V7h-2v2H8V5zm10 0v2h2v12h-2v-2h-2v2H8V9h2v2h2V7h2V5h2z"/></svg>
|
|
</label>
|
|
<button type="button" class="beep-gif-btn" data-beep-gif-btn title="Add GIF">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M11.5 9H13v6h-1.5V9zM9 9H6c-.6 0-1 .5-1 1v4c0 .5.4 1 1 1h3c.6 0 1-.5 1-1v-2H8.5v1.5H7v-3c0-.6-.4-1-1-1zm0 4.5H8v-3h1v3zm10-4.5h-3v6h1.5v-2h1v2H19V9c0-.6-.4-1-1-1z"/></svg>
|
|
</button>
|
|
<button type="button" class="beep-poll-btn" data-beep-poll-btn title="Add poll">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7 12h2v5H7v-5zm4-3h2v8h-2V9zm4-3h2v11h-2V6z"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="beep-compose-quote" data-beep-compose-quote hidden>
|
|
<div class="beep-quote-input-row">
|
|
<input type="text" class="beep-quote-lookup" placeholder="Paste beep URL or ID..." data-beep-quote-lookup>
|
|
<button type="button" class="beep-quote-lookup-btn" data-beep-quote-lookup-btn>Add</button>
|
|
</div>
|
|
<div class="beep-quote-preview" data-beep-quote-preview hidden></div>
|
|
<button type="button" class="beep-quote-remove" data-beep-quote-remove hidden>Remove quote</button>
|
|
</div>
|
|
<div class="beep-compose-gif" data-beep-gif-preview hidden>
|
|
<div class="beep-gif-preview-card">
|
|
<span>GIF selected</span>
|
|
<button class="beep-gif-remove" data-beep-gif-remove type="button" title="Remove">×</button>
|
|
</div>
|
|
</div>
|
|
<div class="beep-compose-poll" data-beep-compose-poll hidden>
|
|
<div class="beep-poll-input-row">
|
|
<input type="text" class="beep-poll-question" data-beep-poll-question placeholder="Ask a question...">
|
|
</div>
|
|
<div class="beep-poll-options" data-beep-poll-options>
|
|
<div class="beep-poll-option-row">
|
|
<input type="text" class="beep-poll-option" data-beep-poll-option placeholder="Option 1" maxlength="100">
|
|
</div>
|
|
<div class="beep-poll-option-row">
|
|
<input type="text" class="beep-poll-option" data-beep-poll-option placeholder="Option 2" maxlength="100">
|
|
</div>
|
|
</div>
|
|
<div class="beep-poll-actions">
|
|
<button type="button" class="beep-poll-add-option" data-beep-poll-add-option>+ Add option</button>
|
|
<button type="button" class="beep-poll-remove-option" data-beep-poll-remove-option hidden>- Remove</button>
|
|
<button type="button" class="beep-poll-remove" data-beep-poll-remove>Remove poll</button>
|
|
</div>
|
|
</div>
|
|
<span class="beep-char-counter" data-beep-counter><?php echo (int) BEEP_CHAR_LIMIT; ?></span>
|
|
<button class="beep-button beep-post-button" data-beep-submit disabled>Beep</button>
|
|
</div>
|
|
</div>
|
|
<div class="beep-compose-media-preview" data-beep-media-preview hidden></div>
|
|
<div class="beep-compose-voice-preview" data-beep-voice-preview hidden>
|
|
<div class="beep-voice-preview-card">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08C16.39 17.43 19 14.53 19 11h-2z"/></svg>
|
|
<span>Voice note</span>
|
|
<button class="beep-voice-remove" data-beep-voice-remove type="button" title="Remove">×</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Render a single beep post + its replies tree.
|
|
*/
|
|
function beep_render_post($post) {
|
|
if (!$post) {
|
|
return '';
|
|
}
|
|
|
|
$author = get_userdata($post->post_author);
|
|
$beep_avatar = get_user_meta($post->post_author, 'beep_avatar', true);
|
|
if ($beep_avatar) {
|
|
$avatar_url = $beep_avatar;
|
|
$filter = '';
|
|
} elseif ($author && !empty($author->user_email)) {
|
|
$avatar_url = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email)) . '?s=96&d=mp';
|
|
$filter = '';
|
|
} else {
|
|
$avatar_url = BEEP_URL . 'assets/beep-bird.png';
|
|
$filter = 'brightness(0) saturate(100%) invert(22%) sepia(100%) saturate(10000%) hue-rotate(190deg)';
|
|
}
|
|
$avatar = '<img class="beep-avatar" src="' . esc_url($avatar_url) . '" alt="" style="width:48px;height:48px;object-fit:contain;' . ($filter ? 'filter:' . esc_attr($filter) : '') . '">';
|
|
$name = $author ? esc_html($author->display_name) : 'Unknown';
|
|
$handle = $author ? '@' . esc_html($author->user_login) : '';
|
|
$ts = strtotime($post->post_date_gmt . ' UTC');
|
|
$time = beep_relative_time($ts);
|
|
$time_iso = gmdate('c', $ts);
|
|
$time_full = mysql2date('F j, Y g:i a', $post->post_date);
|
|
|
|
$content = beep_format_content($post->post_content);
|
|
$like_count = Beep_Likes::get_count($post->ID, 'post');
|
|
$reply_count = Beep_Replies::reply_count($post->ID);
|
|
$user_liked = Beep_Likes::user_has_liked($post->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);
|
|
$beep_voice = Beep_Media::get_voice_url($post->ID);
|
|
|
|
ob_start();
|
|
?>
|
|
<article class="beep-post" data-beep-post="<?php echo (int) $post->ID; ?>">
|
|
<?php echo $avatar; ?>
|
|
<div class="beep-post-body">
|
|
<div class="beep-meta">
|
|
<?php echo beep_bird_svg( ['size' => 18, 'fill' => '#0064cd', 'class' => 'beep-post-badge'] ); ?>
|
|
<span class="beep-name"><?php echo $name; ?></span>
|
|
<?php if ($handle) : ?>
|
|
<span class="beep-handle"><?php echo $handle; ?></span>
|
|
<?php endif; ?>
|
|
<span class="beep-dot">·</span>
|
|
<time class="beep-time"
|
|
datetime="<?php echo esc_attr($time_iso); ?>"
|
|
title="<?php echo esc_attr($time_full); ?>"><?php echo esc_html($time); ?></time>
|
|
<span class="beep-spacer"></span>
|
|
</div>
|
|
<div class="beep-text"><?php echo $content; ?></div>
|
|
<?php if (!empty($beep_images)) : ?>
|
|
<div class="beep-media-attachments">
|
|
<?php echo Beep_Media::render_image_gallery($beep_images); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if ($beep_video) : ?>
|
|
<div class="beep-media-attachments">
|
|
<?php echo Beep_Media::render_video_embed($beep_video); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if ($beep_voice) : ?>
|
|
<div class="beep-media-attachments">
|
|
<?php echo Beep_Media::render_voice_note($post->ID); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php
|
|
$quoted_post = Beep_Quotes::get_quoted_post($post->ID);
|
|
?>
|
|
<?php if ($quoted_post) : ?>
|
|
<div class="beep-quoted-container">
|
|
<?php echo Beep_Quotes::render_quoted_post($post->ID); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php
|
|
$beep_gif_url = get_post_meta($post->ID, 'beep_gif_url', true);
|
|
?>
|
|
<?php if ($beep_gif_url) : ?>
|
|
<div class="beep-media-attachments">
|
|
<div class="beep-gif-embed">
|
|
<img src="<?php echo esc_url($beep_gif_url); ?>" alt="GIF" style="max-width:100%;border-radius:12px;">
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php
|
|
$poll = Beep_Polls::get_poll_by_beep_id($post->ID);
|
|
if ($poll) : ?>
|
|
<div class="beep-poll-container" data-beep-poll data-beep-poll-id="<?php echo (int) $poll['id']; ?>">
|
|
<div class="beep-poll-question"><?php echo esc_html($poll['question']); ?></div>
|
|
<div class="beep-poll-options">
|
|
<?php
|
|
$user_id = get_current_user_id();
|
|
$user_vote = $user_id ? Beep_Polls::get_user_vote($poll['id'], $user_id) : null;
|
|
$has_ended = $poll['ends_at'] && strtotime($poll['ends_at']) < time();
|
|
$show_results = $user_vote !== null || $has_ended || !is_user_logged_in();
|
|
$results = Beep_Polls::get_results_with_percentages($poll);
|
|
foreach ($results as $result) :
|
|
$is_voted = $user_vote === $result['index'];
|
|
?>
|
|
<?php if ($show_results) : ?>
|
|
<div class="beep-poll-result<?php echo $is_voted ? ' is-voted' : ''; ?><?php echo ($is_voted || ($result['percentage'] >= 50)) ? ' is-winning' : ''; ?>">
|
|
<div class="beep-poll-result-bar" style="width:<?php echo (int) $result['percentage']; ?>%"></div>
|
|
<span class="beep-poll-result-text">
|
|
<?php echo esc_html($result['option']); ?>
|
|
<span class="beep-poll-result-percent"><?php echo (int) $result['percentage']; ?>%</span>
|
|
</span>
|
|
</div>
|
|
<?php else : ?>
|
|
<button class="beep-poll-vote-btn"
|
|
type="button"
|
|
data-beep-poll-vote="<?php echo (int) $result['index']; ?>"
|
|
data-beep-poll-beep="<?php echo (int) $post->ID; ?>">
|
|
<?php echo esc_html($result['option']); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="beep-poll-meta">
|
|
<span class="beep-poll-votes"><?php echo (int) $poll['total_votes']; ?> votes</span>
|
|
<?php if ($poll['ends_at']) : ?>
|
|
<?php if ($has_ended) : ?>
|
|
<span class="beep-poll-end">Final results</span>
|
|
<?php else : ?>
|
|
<span class="beep-poll-end"><?php echo esc_html(date('M j', strtotime($poll['ends_at']))); ?> · Ends</span>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="beep-actions">
|
|
<button class="beep-action beep-action-reply"
|
|
data-beep-toggle-reply
|
|
type="button"
|
|
title="Reply">
|
|
<?php echo beep_icon('reply'); ?>
|
|
<span class="beep-count" data-beep-reply-count><?php echo $reply_count > 0 ? esc_html($reply_count) : ''; ?></span>
|
|
</button>
|
|
<button class="beep-action beep-action-like<?php echo $user_liked ? ' is-liked' : ''; ?>"
|
|
data-beep-like-post="<?php echo (int) $post->ID; ?>"
|
|
type="button"
|
|
title="Like">
|
|
<span class="beep-icon-outline"><?php echo beep_icon('heart'); ?></span>
|
|
<span class="beep-icon-filled"><?php echo beep_icon('heart_filled'); ?></span>
|
|
<span class="beep-count" data-beep-like-count><?php echo $like_count > 0 ? esc_html($like_count) : ''; ?></span>
|
|
</button>
|
|
<a href="#" class="beep-action beep-action-conversation"
|
|
data-beep-thread="<?php echo (int) $post->ID; ?>"
|
|
title="View conversation">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M21 6h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1zm-4 6V3c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1z"/></svg>
|
|
</a>
|
|
<button class="beep-action beep-action-share"
|
|
data-beep-share="<?php echo esc_url(get_permalink($post->ID)); ?>"
|
|
type="button"
|
|
title="Copy link">
|
|
<?php echo beep_icon('share'); ?>
|
|
</button>
|
|
</div>
|
|
<a href="#" class="beep-action beep-action-quote"
|
|
data-beep-quote="<?php echo (int) $post->ID; ?>"
|
|
title="Quote">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5H6v2H4v12h2v-2h2v2h8V9h-2V7h-2v2H8V5zm10 0v2h2v12h-2v-2h-2v2H8V9h2v2h2V7h2V5h2z"/></svg>
|
|
</a>
|
|
<div class="beep-bottom-bar">
|
|
<?php if ($can_delete) : ?>
|
|
<button class="beep-delete-link"
|
|
data-beep-delete="<?php echo (int) $post->ID; ?>"
|
|
type="button"
|
|
aria-label="Delete this beep"
|
|
title="Delete">×</button>
|
|
<?php endif; ?>
|
|
<span class="beep-wordmark">Beep!</span>
|
|
</div>
|
|
|
|
<?php if ($can_interact) : ?>
|
|
<?php echo beep_render_reply_form($post->ID, 0); ?>
|
|
<?php endif; ?>
|
|
|
|
<div class="beep-replies" data-beep-replies>
|
|
<?php
|
|
$all = Beep_Replies::get_replies($post->ID);
|
|
echo beep_render_replies_tree($all, 0, 0, $post->ID, $can_interact);
|
|
?>
|
|
</div>
|
|
<span class="beep-wordmark">Beep!</span>
|
|
</div>
|
|
</article>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Render a reply form (used both on top-level posts and individual replies).
|
|
* @param int $post_id The beep post ID this reply belongs to.
|
|
* @param int $parent The parent comment ID (0 for top-level reply).
|
|
*/
|
|
function beep_render_reply_form($post_id, $parent) {
|
|
ob_start();
|
|
?>
|
|
<div class="beep-reply-form" data-beep-reply-form data-beep-parent="<?php echo (int) $parent; ?>" hidden>
|
|
<textarea
|
|
class="beep-input beep-reply-input"
|
|
placeholder="Post your reply"
|
|
maxlength="<?php echo (int) BEEP_CHAR_LIMIT; ?>"
|
|
rows="1"></textarea>
|
|
<div class="beep-reply-footer">
|
|
<span class="beep-char-counter"><?php echo (int) BEEP_CHAR_LIMIT; ?></span>
|
|
<button class="beep-button beep-reply-submit"
|
|
data-beep-reply-submit="<?php echo (int) $post_id; ?>"
|
|
data-beep-reply-parent="<?php echo (int) $parent; ?>"
|
|
type="button"
|
|
disabled>Reply</button>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Recursively render replies. Visual indent caps at depth 2.
|
|
*/
|
|
function beep_render_replies_tree($comments, $parent_id, $depth, $post_id, $can_interact) {
|
|
$out = '';
|
|
foreach ($comments as $c) {
|
|
if ((int) $c->comment_parent !== (int) $parent_id) {
|
|
continue;
|
|
}
|
|
$out .= beep_render_reply($c, $comments, $depth, $post_id, $can_interact);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Render a single reply, plus its nested children.
|
|
*/
|
|
function beep_render_reply($comment, $all_comments = [], $depth = 0, $post_id = 0, $can_interact = false) {
|
|
if (!$comment) {
|
|
return '';
|
|
}
|
|
|
|
$author_id = (int) $comment->user_id;
|
|
$avatar_url = BEEP_URL . 'assets/beep-bird.png';
|
|
$filter = 'brightness(0) saturate(100%) invert(22%) sepia(100%) saturate(10000%) hue-rotate(190deg)';
|
|
$avatar = '<img class="beep-avatar beep-avatar-sm" src="' . esc_url($avatar_url) . '" alt="" style="width:36px;height:36px;object-fit:contain;filter:' . esc_attr($filter) . ';">';
|
|
|
|
$author = $author_id ? get_userdata($author_id) : null;
|
|
$name = $author ? esc_html($author->display_name) : esc_html($comment->comment_author);
|
|
$handle = $author ? '@' . esc_html($author->user_login) : '';
|
|
|
|
$ts = strtotime($comment->comment_date_gmt . ' UTC');
|
|
$time = beep_relative_time($ts);
|
|
$time_iso = gmdate('c', $ts);
|
|
|
|
$content = beep_format_content($comment->comment_content);
|
|
$can_delete = current_user_can('moderate_comments');
|
|
$like_count = Beep_Likes::get_count($comment->comment_ID, 'reply');
|
|
$user_liked = Beep_Likes::user_has_liked($comment->comment_ID, 'reply');
|
|
|
|
$depth_class = 'beep-depth-' . min((int) $depth, 2);
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="beep-reply <?php echo esc_attr($depth_class); ?>" data-beep-reply="<?php echo (int) $comment->comment_ID; ?>">
|
|
<?php echo $avatar; ?>
|
|
<div class="beep-reply-body">
|
|
<div class="beep-meta">
|
|
<?php echo beep_bird_svg( ['size' => 16, 'fill' => '#0064cd', 'class' => 'beep-post-badge beep-post-badge-sm'] ); ?>
|
|
<span class="beep-name"><?php echo $name; ?></span>
|
|
<?php if ($handle) : ?>
|
|
<span class="beep-handle"><?php echo $handle; ?></span>
|
|
<?php endif; ?>
|
|
<span class="beep-dot">·</span>
|
|
<time class="beep-time" datetime="<?php echo esc_attr($time_iso); ?>"><?php echo esc_html($time); ?></time>
|
|
<span class="beep-spacer"></span>
|
|
<?php echo beep_bird_svg( ['size' => 16, 'fill' => '#0064cd', 'class' => 'beep-post-badge beep-post-badge-sm'] ); ?>
|
|
</div>
|
|
<div class="beep-text"><?php echo $content; ?></div>
|
|
|
|
<?php if ($can_interact) : ?>
|
|
<div class="beep-actions beep-actions-reply">
|
|
<button class="beep-action beep-action-reply"
|
|
data-beep-toggle-reply
|
|
type="button"
|
|
title="Reply">
|
|
<?php echo beep_icon('reply'); ?>
|
|
</button>
|
|
<button class="beep-action beep-action-like<?php echo $user_liked ? ' is-liked' : ''; ?>"
|
|
data-beep-like-reply="<?php echo (int) $comment->comment_ID; ?>"
|
|
type="button"
|
|
title="Like">
|
|
<span class="beep-icon-outline"><?php echo beep_icon('heart'); ?></span>
|
|
<span class="beep-icon-filled"><?php echo beep_icon('heart_filled'); ?></span>
|
|
<span class="beep-count" data-beep-like-count><?php echo $like_count > 0 ? esc_html($like_count) : ''; ?></span>
|
|
</button>
|
|
</div>
|
|
<?php echo beep_render_reply_form($post_id, $comment->comment_ID); ?>
|
|
<?php endif; ?>
|
|
<?php if ($can_delete) : ?>
|
|
<div class="beep-delete-row">
|
|
<button class="beep-delete-link"
|
|
data-beep-delete-reply="<?php echo (int) $comment->comment_ID; ?>"
|
|
type="button"
|
|
aria-label="Delete this reply"
|
|
title="Delete">×</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php
|
|
// Recursively render children
|
|
$children = beep_render_replies_tree($all_comments, $comment->comment_ID, $depth + 1, $post_id, $can_interact);
|
|
if ($children) {
|
|
echo '<div class="beep-replies beep-replies-nested" data-beep-replies>' . $children . '</div>';
|
|
}
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Format beep text: linkify URLs, @mentions, #hashtags.
|
|
*/
|
|
function beep_format_content($text) {
|
|
$text = wp_kses($text, []);
|
|
// Hashtags
|
|
$text = preg_replace_callback('/(^|\s)#([\p{L}0-9_]+)/u', function ($m) {
|
|
return $m[1] . '<a href="#" class="beep-tag">#' . esc_html($m[2]) . '</a>';
|
|
}, $text);
|
|
// @mentions
|
|
$text = preg_replace_callback('/(^|\s)@([a-zA-Z0-9_]+)/', function ($m) {
|
|
$user = get_user_by('login', $m[2]);
|
|
if ($user) {
|
|
return $m[1] . '<a href="' . esc_url(get_author_posts_url($user->ID)) . '" class="beep-mention">@' . esc_html($m[2]) . '</a>';
|
|
}
|
|
return $m[1] . '<span class="beep-mention beep-mention-stale">@' . esc_html($m[2]) . '</span>';
|
|
}, $text);
|
|
// URLs
|
|
$text = preg_replace_callback('#(https?://[^\s<]+)#i', function ($m) {
|
|
$url = esc_url($m[1]);
|
|
$display = parse_url($url, PHP_URL_HOST);
|
|
if (!$display) {
|
|
$display = $url;
|
|
}
|
|
return '<a href="' . $url . '" class="beep-link" target="_blank" rel="noopener nofollow">' . esc_html($display) . '</a>';
|
|
}, $text);
|
|
// Line breaks
|
|
$text = nl2br($text);
|
|
return $text;
|
|
}
|
|
|
|
function beep_relative_time($timestamp) {
|
|
$now = time();
|
|
$diff = $now - $timestamp;
|
|
if ($diff < 0) {
|
|
$diff = 0;
|
|
}
|
|
if ($diff < 60) {
|
|
return $diff . 's';
|
|
}
|
|
if ($diff < 3600) {
|
|
return floor($diff / 60) . 'm';
|
|
}
|
|
if ($diff < 86400) {
|
|
return floor($diff / 3600) . 'h';
|
|
}
|
|
if ($diff < 604800) {
|
|
return floor($diff / 86400) . 'd';
|
|
}
|
|
return gmdate('M j', $timestamp);
|
|
}
|
|
|
|
function beep_icon($name) {
|
|
$icons = [
|
|
'reply' => '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01z"/></svg>',
|
|
'heart' => '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91zm4.187 7.69c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z"/></svg>',
|
|
'heart_filled' => '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M20.884 13.19c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z"/></svg>',
|
|
'share' => '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M12 2.59l5.7 5.7-1.41 1.42L13 6.41V16h-2V6.41l-3.3 3.3-1.41-1.42L12 2.59zM21 15l-.02 3.51c0 1.38-1.12 2.49-2.5 2.49H5.5C4.11 21 3 19.88 3 18.5V15h2v3.5c0 .28.22.5.5.5h12.98c.28 0 .5-.22.5-.5L19 15h2z"/></svg>',
|
|
'beep_bird' => '<svg viewBox="0 0 24 24" width="18" height="18" fill="#0064cd" aria-hidden="true"><path d="M17.2 1.4c-.8-.1-1.5.2-2 .6C14.3.9 13.2.7 12.1 1c-1.4.4-2.3 1.7-2.1 3.2-2.1-.2-4.2-.9-5.7-2.5-.3-.3-.8-.3-1 0-.6.8-.9 1.7-.9 2.6 0 1.2.5 2.2 1.3 2.9-.3-.1-.6-.1-.8-.3-.4-.2-.9 0-.8.5.3 1.6 1.3 2.8 2.7 3.4-.3 0-.5 0-.8-.1-.4-.1-.8.3-.5.7.8 1.4 2.2 2.3 3.8 2.5-1.3.8-2.8 1.2-4.4 1.2-.5 0-.8.5-.5.9 1.1 1.1 2.9 1.6 5.5 1.6 5.4 0 9.8-4.4 9.8-9.8v-.2c.9-.6 1.6-1.4 2.1-2.4.2-.4-.2-.8-.6-.6-.5.2-1 .3-1.5.4.5-.5.9-1.1 1-1.8.1-.4-.4-.7-.7-.5-.7.4-1.5.6-2.3.7-.8-.7-1.8-1.1-2.9-1.1zm-.8 1.6c.7-.1 1.4.2 1.9.7.2.2.5.3.8.2.3-.1.6-.1.9-.3-.2.3-.5.5-.8.6-.3.1-.4.5-.3.7.2.3.1.5 0 .8v.2c0 4.7-3.8 8.5-8.5 8.5-1.6 0-2.9-.2-3.8-.6 1.7-.3 3.2-1 4.3-2.2.3-.3.1-.8-.3-.8-1.4 0-2.7-.6-3.5-1.7.4 0 .9-.1 1.3-.2.4-.1.4-.7 0-.8-1.5-.3-2.6-1.3-3-2.6.5.1 1 .2 1.5.1.4 0 .5-.6.1-.8C5.2 5.5 4.5 4.5 4.5 3.4c0-.4.1-.9.3-1.3 1.8 1.5 4 2.4 6.4 2.5.3 0 .5-.2.5-.5-.2-1.1.4-2.1 1.4-2.4.7-.2 1.4 0 1.9.4.3.2.7.1.9-.1.3-.4.8-.7 1.3-.7.4.1.8.2 1.2.4-.3.3-.5.6-.5 1 0 .3.3.5.5.4z"/></svg>',
|
|
];
|
|
return isset($icons[$name]) ? $icons[$name] : '';
|
|
}
|
|
|
|
|
|
/**
|
|
* Render the thread/conversation modal (sits outside individual posts).
|
|
*/
|
|
function beep_render_thread_modal() {
|
|
return '<div class="beep-thread-modal" data-beep-thread-modal hidden>' .
|
|
'<div class="beep-thread-header">' .
|
|
'<button class="beep-thread-close" data-beep-thread-close type="button">' .
|
|
'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>' .
|
|
'</button>' .
|
|
'<span class="beep-thread-title">Conversation</span>' .
|
|
'</div>' .
|
|
'<div class="beep-thread-loading">Loading conversation...</div>' .
|
|
'<div class="beep-thread-content" data-beep-thread-content></div>' .
|
|
'</div>';
|
|
}
|
|
|
|
|
|
/**
|
|
* Render the GIF picker modal.
|
|
*/
|
|
function beep_render_gif_modal() {
|
|
return '<div class="beep-gif-modal" data-beep-gif-modal hidden>' .
|
|
'<div class="beep-gif-header">' .
|
|
'<button class="beep-gif-close" data-beep-gif-close type="button">' .
|
|
'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>' .
|
|
'</button>' .
|
|
'<span class="beep-gif-title">Pick a GIF</span>' .
|
|
'</div>' .
|
|
'<div class="beep-gif-search-bar">' .
|
|
'<input type="text" class="beep-gif-search" data-beep-gif-search placeholder="Search GIPHY...">' .
|
|
'<button type="button" class="beep-gif-search-btn" data-beep-gif-search-btn>Search</button>' .
|
|
'</div>' .
|
|
'<div class="beep-gif-grid" data-beep-gif-grid></div>' .
|
|
'</div>';
|
|
}
|