Files
wordpress-theme-aksrt/inc/helpers/aksrt-identity.php
T

452 lines
18 KiB
PHP

<?php
/**
* Shared identity chrome: VIP / verify / level / badges for author bar & comments.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_user_identity_signals')) {
/**
* @return list<array{type?:string,label:string,icon:string,tone:string,title?:string,icon_url?:string,slug?:string,wear?:bool}>
*/
function aksrt_get_user_identity_signals(int $user_id): array {
if ($user_id <= 0) {
return [];
}
if (class_exists('\AKSRT\User\Home')) {
$signals = \AKSRT\User\Home::get_instance()->get_profile_signals($user_id);
return is_array($signals) ? array_values($signals) : [];
}
return [];
}
}
if (!function_exists('aksrt_render_user_identity_chips')) {
/**
* Render compact identity chips (verify / level / badges).
*
* Badge signals with icon_url render as Zibll-like medal images.
*
* @param int $user_id
* @param string $context author|comment|card
* @param array $args {
* Optional. Additional options.
*
* @type bool $include_vip Whether to prepend VIP badge. Default true.
* @type bool $skip_level Whether to omit the level chip (rendered inline elsewhere). Default false.
* @type bool $skip_badges Whether to omit badge signals from this row. Default false.
* }
*/
function aksrt_render_user_identity_chips(int $user_id, string $context = 'author', array $args = []): string {
if ($user_id <= 0) {
return '';
}
$args = wp_parse_args($args, [
'include_vip' => true,
'skip_level' => false,
]);
$html = '';
if (!empty($args['include_vip']) && function_exists('aksrt_render_vip_badge')) {
$vip = aksrt_render_vip_badge($user_id, 'sm');
if (is_string($vip) && $vip !== '') {
$html .= $vip;
}
}
$signals = aksrt_get_user_identity_signals($user_id);
// Keep the public identity order stable: certification, worn badge, level.
usort($signals, static function (array $left, array $right): int {
$priority = ['verify' => 0, 'badge' => 1, 'level' => 2];
return ($priority[(string) ($left['type'] ?? '')] ?? 3) <=> ($priority[(string) ($right['type'] ?? '')] ?? 3);
});
if ($context === 'comment') {
$signals = array_values(array_filter($signals, static function (array $signal): bool {
return (string) ($signal['type'] ?? '') !== 'badge' || !empty($signal['wear']);
}));
}
// Comments stay compact; author/card can show more.
$limit = $context === 'comment' ? 3 : 6;
$n = 0;
foreach ($signals as $signal) {
if ($n >= $limit) {
break;
}
$label = trim((string) ($signal['label'] ?? ''));
if ($label === '') {
continue;
}
$type = sanitize_html_class((string) ($signal['type'] ?? ''));
// Level is rendered inline next to the name by the caller; skip it here.
if (!empty($args['skip_level']) && $type === 'level') {
continue;
}
if (!empty($args['skip_badges']) && $type === 'badge') {
continue;
}
$tone = sanitize_html_class((string) ($signal['tone'] ?? 'info'));
$icon = sanitize_html_class((string) ($signal['icon'] ?? 'check'));
$icon_url = esc_url((string) ($signal['icon_url'] ?? ''));
$title = (string) ($signal['title'] ?? $label);
$is_wear = !empty($signal['wear']);
// Image medal when dictionary provides icon_url.
if ($icon_url !== '') {
$classes = 'medal-icon img-icon ml3 aksrt-identity-medal aksrt-identity-medal--' . $context;
if ($is_wear) {
$classes .= ' is-wear';
}
if ($type !== '') {
$classes .= ' aksrt-identity-medal--' . $type;
}
$html .= '<img class="' . esc_attr($classes) . '" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
$n++;
continue;
}
// Font Awesome: map short names to fa-* if needed.
if ($icon !== '' && !str_contains($icon, 'fa-')) {
$icon = 'fa-' . $icon;
}
$html .= '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--' . esc_attr($tone) . ' aksrt-identity-chip--' . esc_attr($context) . ($is_wear ? ' is-wear' : '') . '" title="' . esc_attr($title) . '">';
if ($icon !== '') {
$html .= '<i class="fa ' . esc_attr($icon) . '" aria-hidden="true"></i> ';
}
$html .= esc_html($label) . '</span>';
$n++;
}
return $html;
}
}
if (!function_exists('aksrt_get_user_name')) {
/**
* Zibll zib_get_user_name parity: name link + auth + wear medal + level + optional follow.
*
* @param array|int $args User id or args array.
*/
function aksrt_get_user_name($args = []): string {
$defaults = [
'id' => 0,
'class' => 'flex ac flex1',
'name_class' => '',
'vip' => true,
'medal' => true,
'auth' => true,
'level' => true,
'follow' => false,
];
if (is_numeric($args)) {
$defaults['id'] = (int) $args;
$args = $defaults;
} else {
$args = wp_parse_args(is_array($args) ? $args : [], $defaults);
}
$user_id = (int) $args['id'];
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return '';
}
$user = get_userdata($user_id);
if (!$user) {
return '';
}
$home = function_exists('get_author_posts_url') ? get_author_posts_url($user_id) : '';
$display = $user->display_name;
$vip_html = '';
if (!empty($args['vip']) && function_exists('aksrt_render_vip_badge')) {
$vip_html = (string) aksrt_render_vip_badge($user_id, 'sm');
}
$name_class = trim('display-name text-ellipsis ' . (string) $args['name_class']);
$name_link = '<a class="' . esc_attr($name_class) . '" href="' . esc_url($home) . '">' . esc_html($display) . '</a>';
$auth_html = '';
$medal_html = '';
$level_html = '';
$signals = aksrt_get_user_identity_signals($user_id);
foreach ($signals as $signal) {
$type = (string) ($signal['type'] ?? '');
$label = trim((string) ($signal['label'] ?? ''));
if ($label === '') {
continue;
}
$icon_url = esc_url((string) ($signal['icon_url'] ?? ''));
$title = (string) ($signal['title'] ?? $label);
if ($type === 'verify' && !empty($args['auth']) && $auth_html === '') {
$auth_html = $icon_url !== ''
? '<img class="aksrt-identity-certification-icon img-icon ml3" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />'
: '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--success" title="' . esc_attr($title) . '"><i class="fa fa-check-circle" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
} elseif ($type === 'level' && !empty($args['level']) && $level_html === '') {
if ($icon_url !== '') {
$level_html = '<img class="aksrt-user-level-icon img-icon ml3" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
} else {
// Text fallback mirrors zibll LV badge: small pill, never oversized.
$level_html = '<span class="badg badg-sm ml3 aksrt-user-level-badge" title="' . esc_attr($title) . '">' . esc_html($label) . '</span>';
}
} elseif ($type === 'badge' && !empty($args['medal']) && $medal_html === '' && !empty($signal['wear'])) {
if ($icon_url !== '') {
$medal_html = '<img class="medal-icon img-icon ml3 aksrt-identity-medal is-wear" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
} else {
$medal_html = '<span class="badg badg-sm ml3 aksrt-identity-chip is-wear" title="' . esc_attr($title) . '"><i class="fa fa-award" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
}
}
}
// If medal requested but no wear flag, use first badge with icon.
if (!empty($args['medal']) && $medal_html === '') {
foreach ($signals as $signal) {
if ((string) ($signal['type'] ?? '') !== 'badge') {
continue;
}
$label = trim((string) ($signal['label'] ?? ''));
$icon_url = esc_url((string) ($signal['icon_url'] ?? ''));
$title = (string) ($signal['title'] ?? $label);
if ($label === '') {
continue;
}
if ($icon_url !== '') {
$medal_html = '<img class="medal-icon img-icon ml3 aksrt-identity-medal" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
} else {
$medal_html = '<span class="badg badg-sm ml3 aksrt-identity-chip" title="' . esc_attr($title) . '"><i class="fa fa-award" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
}
break;
}
}
$follow_html = '';
if (!empty($args['follow']) && get_current_user_id() !== $user_id) {
if (is_user_logged_in()) {
$follow_html = '<button type="button" class="focus-color ml10 follow flex0 but" data-aksrt-follow="' . (int) $user_id . '"><i class="fa fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</button>';
} else {
$login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
$follow_html = '<a class="focus-color ml10 follow flex0 but" href="' . esc_url($login) . '"><i class="fa fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</a>';
}
}
// Order: name → auth → wear medal → level → follow (Zibll).
$html = '<name class="' . esc_attr((string) $args['class']) . '">' . $name_link . $vip_html . $auth_html . $medal_html . $level_html . $follow_html . '</name>';
/**
* Filter rendered user name chrome.
*
* @param string $html
* @param int $user_id
* @param array $args
*/
return (string) apply_filters('aksrt_user_show_name', $html, $user_id, $args);
}
}
if (!function_exists('aksrt_render_user_medal_entry')) {
/**
* Render the compact Zibll-style badge overview entry.
*
* The configured limit controls preview icons only; the count remains
* the complete owned badge count returned by the privacy-aware DTO.
*
* @param int $user_id
* @param string $context user-center|user-home
* @param bool $can_wear Whether the viewer may initially wear a badge.
*/
function aksrt_render_user_medal_entry(int $user_id, string $context = 'user-home', bool $can_wear = false): string {
if ($user_id <= 0 || !class_exists('AKSRT\\User\\Badges') || !class_exists('AKSRT\\Settings\\Settings')) {
return '';
}
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
if (empty($medal['enabled']) || ($context === 'user-home' && empty($medal['public_display']))) {
return '';
}
$data = \AKSRT\User\Badges::list_for_user($user_id);
if (empty($data['enabled']) || ($context === 'user-home' && empty($data['public_display']))) {
return '';
}
$limit = max(1, min(8, (int) ($medal['limit'] ?? 3)));
$icons = [];
foreach ((array) ($data['categories'] ?? []) as $category) {
foreach ((array) ($category['items'] ?? []) as $item) {
if (empty($item['owned'])) {
continue;
}
$icon_url = esc_url((string) ($item['icon_url'] ?? ''));
if ($icon_url === '') {
continue;
}
$label = sanitize_text_field((string) ($item['label'] ?? __('徽章', 'aksrt')));
$icons[] = '<img class="img-icon medal-icon aksrt-medal-entry__icon" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($label) . '" loading="lazy" decoding="async" />';
if (count($icons) >= $limit) {
break 2;
}
}
}
$count = max(0, (int) ($data['count'] ?? 0));
$label = $count > 0 ? sprintf(_n('%d枚徽章', '%d枚徽章', $count, 'aksrt'), $count) : __('获取徽章', 'aksrt');
$icons_html = implode('', $icons);
if ($icons_html === '') {
$icons_html = '<span class="aksrt-medal-entry__fallback" aria-hidden="true"><i class="fas fa-award"></i></span>';
}
$classes = 'aksrt-medal-entry aksrt-medal-entry--' . sanitize_html_class($context);
return '<button type="button" class="' . esc_attr($classes) . '" data-aksrt-badges-open data-aksrt-badges-user-id="' . (int) $user_id . '" data-aksrt-badges-can-wear="' . ($can_wear ? '1' : '0') . '" aria-label="' . esc_attr($label) . '"><span class="aksrt-medal-entry__icons">' . $icons_html . '</span><span class="aksrt-medal-entry__count">' . esc_html($label) . '</span><i class="fas fa-angle-right aksrt-medal-entry__arrow" aria-hidden="true"></i></button>';
}
}
if (!function_exists('aksrt_user_has_visible_badges')) {
/**
* Whether a user owns at least one badge that exists in the current catalog.
*
* Used by user-center surfaces to decide whether to show the "我的徽章"
* button. Prevents rendering a button that opens an empty modal when the
* badge dictionary has no entries or none of the user's badges match it.
*
* @param int $user_id Target user (0 = current user).
* @return bool
*/
function aksrt_user_has_visible_badges(int $user_id = 0): bool {
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0 || !class_exists('AKSRT\\User\\Badges')) {
return false;
}
$map = \AKSRT\User\Badges::get_dictionary_map();
if (empty($map)) {
return false;
}
$owned = \AKSRT\User\Badges::get_owned($user_id);
foreach ((array) $owned as $slug) {
if (is_string($slug) && $slug !== '' && isset($map[$slug])) {
return true;
}
}
return false;
}
}
if (!function_exists('aksrt_get_worn_medal_icon')) {
/**
* Render the worn medal icon HTML for a user (standalone, zibll parity).
*
* Returns an <img> tag when a badge is worn and has an icon_url in the
* dictionary. Handles the edge case where the worn slug lives in user meta
* but the badge class is unavailable (defensive: rebuilds from settings).
*
* @param int $user_id Target user (0 = current user).
* @param string $class Extra CSS classes ( space-separated ).
* @return string HTML safe for echo; '' when nothing to render.
*/
function aksrt_get_worn_medal_icon(int $user_id = 0, string $class = ''): string {
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return '';
}
$enabled = false;
$slug = '';
$icon = '';
$label = '';
if (class_exists('AKSRT\\User\\Badges') && is_callable(['AKSRT\\User\\Badges', 'enabled'])) {
if (\AKSRT\User\Badges::enabled()) {
$enabled = true;
}
}
if (!$enabled && class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_user_identity_settings')) {
$ident = \AKSRT\Settings\Settings::get_user_identity_settings();
if (!empty($ident['badge']['enabled'])) {
$enabled = true;
}
}
if (!$enabled) {
return '';
}
// Class-backed path (canonical).
if (class_exists('AKSRT\\User\\Badges')
&& is_callable(['AKSRT\\User\\Badges', 'get_wear'])
&& is_callable(['AKSRT\\User\\Badges', 'get_dictionary_map'])) {
$slug = sanitize_key((string) \AKSRT\User\Badges::get_wear($user_id));
$map = \AKSRT\User\Badges::get_dictionary_map();
$entry = is_array($map[$slug] ?? null) ? $map[$slug] : [];
if (is_array($entry) && !empty($entry)) {
$icon = isset($entry['icon_url']) ? esc_url((string) $entry['icon_url']) : '';
$label = isset($entry['label']) ? sanitize_text_field((string) $entry['label']) : '';
}
}
// Fallback when class is missing or returned nothing: rebuild from settings.
if ($slug === '' && $icon === '' && class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_user_identity_settings')) {
$ident = \AKSRT\Settings\Settings::get_user_identity_settings();
$slug = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true));
if ($slug !== '') {
$dictionary = is_array($ident['badge']['dictionary'] ?? null) ? $ident['badge']['dictionary'] : [];
foreach ((array) $dictionary as $item) {
if (is_array($item) && !empty($item['slug']) && (string) $item['slug'] === $slug) {
$icon = isset($item['icon_url']) ? esc_url((string) $item['icon_url']) : '';
$label = isset($item['label']) ? sanitize_text_field((string) $item['label']) : '';
break;
}
}
}
}
if ($slug === '' || ($icon === '' && $label === '')) {
return '';
}
if ($icon !== '') {
$extra = $class !== '' ? ' ' . esc_attr($class) : '';
return '<img class="medal-icon img-icon ml3 aksrt-identity-medal is-wear' . $extra . '" src="' . $icon . '" alt="' . esc_attr($label) . '" title="' . esc_attr($label) . '" loading="lazy" decoding="async" />';
}
// Text fallback (rare): small pill labelled with badge text.
$extra = $class !== '' ? ' ' . esc_attr($class) : '';
return '<span class="badg badg-sm ml3 aksrt-identity-medal is-wear' . $extra . '" title="' . esc_attr($label) . '"><i class="fa fa-award" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
}
}
if (!function_exists('aksrt_is_blog_comment_hot')) {
/**
* "神评" for ordinary post comments when likes cross threshold.
*/
function aksrt_is_blog_comment_hot($comment): bool {
$comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment;
if ($comment_id <= 0) {
return false;
}
if ('1' === (string) get_comment_meta($comment_id, '_aksrt_comment_hot', true)) {
return true;
}
$threshold = 10;
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_comment_display_settings')) {
$display = \AKSRT\Settings\Settings::get_comment_display_settings();
if (isset($display['hot_threshold'])) {
$threshold = max(1, (int) $display['hot_threshold']);
}
}
$all = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
if (isset($all['comment_hot_threshold'])) {
$threshold = max(1, (int) $all['comment_hot_threshold']);
}
$likes = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($comment_id) : (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true);
if ($likes < $threshold) {
return false;
}
update_comment_meta($comment_id, '_aksrt_comment_hot', '1');
return true;
}
}