进行了一些小优化
This commit is contained in:
1 parent
4a4009133c
commit
11f1c23fa7
12 files changed
+463
-10
No files matched your search
@@ -25,6 +25,7 @@
|
|||||||
!/tests/widget-system/auth-source-contract.test.cjs
|
!/tests/widget-system/auth-source-contract.test.cjs
|
||||||
!/tests/widget-system/user-badges-contract.test.cjs
|
!/tests/widget-system/user-badges-contract.test.cjs
|
||||||
!/tests/widget-system/user-features-contract.test.cjs
|
!/tests/widget-system/user-features-contract.test.cjs
|
||||||
|
!/tests/widget-system/notification-preferences-contract.test.cjs
|
||||||
!/tests/widget-system/asset-ledger-contract.test.cjs
|
!/tests/widget-system/asset-ledger-contract.test.cjs
|
||||||
!/tests/widget-system/financial-flow-contract.test.cjs
|
!/tests/widget-system/financial-flow-contract.test.cjs
|
||||||
!/tests/widget-system/pay-orders-filter-contract.test.cjs
|
!/tests/widget-system/pay-orders-filter-contract.test.cjs
|
||||||
|
|||||||
@@ -2,6 +2,134 @@
|
|||||||
* User Center — account settings + invite panel
|
* User Center — account settings + invite panel
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* 消息通知偏好 */
|
||||||
|
.uc-notification-preferences__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid var(--color-line-soft, rgba(0,0,0,0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preferences__header h2 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preferences__header p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-ink-4);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preferences__header-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
flex: 0 0 44px;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preferences__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preference {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
min-height: 82px;
|
||||||
|
padding: 18px 0;
|
||||||
|
border-bottom: 1px solid var(--color-line-soft, rgba(0,0,0,0.06));
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preference__copy {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preference__copy strong {
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preference__copy small {
|
||||||
|
color: var(--color-ink-4);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
flex: 0 0 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-switch input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-switch span {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-line, #cbd5e1);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-switch span::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(15,23,42,0.22);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-switch input:checked + span {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-switch input:checked + span::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-switch input:focus-visible + span {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preferences__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uc-notification-preferences__footer .btn-primary {
|
||||||
|
min-width: 112px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 设置区增强 */
|
/* 设置区增强 */
|
||||||
.uc-content .settings-section {
|
.uc-content .settings-section {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|||||||
@@ -577,6 +577,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 消息通知偏好 ==================== */
|
||||||
|
function initNotificationPreferences(root) {
|
||||||
|
root = root || document;
|
||||||
|
var form = root.matches && root.matches('[data-aksrt-notification-preferences]')
|
||||||
|
? root
|
||||||
|
: root.querySelector('[data-aksrt-notification-preferences]');
|
||||||
|
if (!form || form.dataset.aksrtInitialized === 'true') return;
|
||||||
|
form.dataset.aksrtInitialized = 'true';
|
||||||
|
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var button = form.querySelector('[type="submit"]');
|
||||||
|
if (!button || button.disabled) return;
|
||||||
|
var toastId = 'aksrt-notification-preferences-toast';
|
||||||
|
var restUrl = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || cfg.restUrl || '';
|
||||||
|
var nonce = (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || cfg.restNonce || '';
|
||||||
|
var originalText = button.textContent;
|
||||||
|
var preferences = {};
|
||||||
|
['posts', 'like', 'system'].forEach(function(key) {
|
||||||
|
var input = form.querySelector('[name="' + key + '"]');
|
||||||
|
preferences[key] = Boolean(input && input.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = i18n.saving || '保存中…';
|
||||||
|
notyf(i18n.saving || '正在保存设置…', 'load', '', toastId);
|
||||||
|
fetch(restUrl.replace(/\/$/, '') + '/user/notification-preferences', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': nonce
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ preferences: preferences })
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json().then(function(payload) {
|
||||||
|
if (!response.ok || !payload || !payload.success) {
|
||||||
|
throw new Error(payload && payload.message ? payload.message : (i18n.saveFailed || '保存失败'));
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
});
|
||||||
|
}).then(function(payload) {
|
||||||
|
notyf(payload.message || i18n.saved || '设置已保存', 'success', 5, toastId);
|
||||||
|
}).catch(function(error) {
|
||||||
|
notyf(error.message || i18n.networkError || '网络错误,请重试', 'danger', 5, toastId);
|
||||||
|
}).finally(function() {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = originalText;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 账号设置:弹窗 / 密码 / 邮箱 / 绑定 ==================== */
|
/* ==================== 账号设置:弹窗 / 密码 / 邮箱 / 绑定 ==================== */
|
||||||
function initSettings(root) {
|
function initSettings(root) {
|
||||||
if (!ajaxUrl || !settingsNonce) return;
|
if (!ajaxUrl || !settingsNonce) return;
|
||||||
@@ -873,6 +925,9 @@
|
|||||||
initSettings(root);
|
initSettings(root);
|
||||||
initBindPhone(root);
|
initBindPhone(root);
|
||||||
});
|
});
|
||||||
|
window.AksrtFrontend.register('aksrt-notification-preferences', function(root) {
|
||||||
|
initNotificationPreferences(root);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -884,6 +939,7 @@
|
|||||||
initUnfavorite();
|
initUnfavorite();
|
||||||
initPostDeletion();
|
initPostDeletion();
|
||||||
initNotifications();
|
initNotifications();
|
||||||
|
initNotificationPreferences();
|
||||||
initSettings();
|
initSettings();
|
||||||
initBindPhone();
|
initBindPhone();
|
||||||
initSettingsCallbackToast();
|
initSettingsCallbackToast();
|
||||||
@@ -895,6 +951,7 @@
|
|||||||
initUnfavorite();
|
initUnfavorite();
|
||||||
initPostDeletion();
|
initPostDeletion();
|
||||||
initNotifications();
|
initNotifications();
|
||||||
|
initNotificationPreferences();
|
||||||
initSettings();
|
initSettings();
|
||||||
initBindPhone();
|
initBindPhone();
|
||||||
initSettingsCallbackToast();
|
initSettingsCallbackToast();
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ if (class_exists('\AKSRT\User\Restriction')) {
|
|||||||
if (class_exists('\AKSRT\User\Center')) {
|
if (class_exists('\AKSRT\User\Center')) {
|
||||||
\AKSRT\User\Center::get_instance()->init();
|
\AKSRT\User\Center::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
if (class_exists('\AKSRT\User\NotificationPreferences')) {
|
||||||
|
\AKSRT\User\NotificationPreferences::init();
|
||||||
|
}
|
||||||
if (class_exists('\AKSRT\User\BadgeAutoAward')) {
|
if (class_exists('\AKSRT\User\BadgeAutoAward')) {
|
||||||
\AKSRT\User\BadgeAutoAward::get_instance()->init();
|
\AKSRT\User\BadgeAutoAward::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ if (!function_exists('aksrt_capture_user_center_view')) {
|
|||||||
'orders' => 'orders',
|
'orders' => 'orders',
|
||||||
'auth' => 'auth',
|
'auth' => 'auth',
|
||||||
'revenue' => 'revenue',
|
'revenue' => 'revenue',
|
||||||
|
'msg' => 'msg',
|
||||||
'settings' => 'settings',
|
'settings' => 'settings',
|
||||||
'header' => 'header',
|
'header' => 'header',
|
||||||
'sidebar' => 'sidebar',
|
'sidebar' => 'sidebar',
|
||||||
@@ -201,7 +202,7 @@ if (!function_exists('aksrt_capture_user_center_panel')) {
|
|||||||
* Capture a built-in or registered user-center panel body.
|
* Capture a built-in or registered user-center panel body.
|
||||||
*/
|
*/
|
||||||
function aksrt_capture_user_center_panel(string $tab_key, array $tab, array $context): string {
|
function aksrt_capture_user_center_panel(string $tab_key, array $tab, array $context): string {
|
||||||
$built_in_tabs = ['profile', 'level', 'creator', 'posts', 'follow', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'revenue', 'settings'];
|
$built_in_tabs = ['profile', 'level', 'creator', 'posts', 'follow', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'revenue', 'msg', 'settings'];
|
||||||
if (in_array($tab_key, $built_in_tabs, true)) {
|
if (in_array($tab_key, $built_in_tabs, true)) {
|
||||||
return aksrt_capture_user_center_view($tab_key, $context);
|
return aksrt_capture_user_center_view($tab_key, $context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* User center notification preferences panel.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
$context = is_array($args['context'] ?? null) ? $args['context'] : [];
|
||||||
|
$user_id = (int) ($context['user_id'] ?? get_current_user_id());
|
||||||
|
$preferences = class_exists('\AKSRT\User\NotificationPreferences')
|
||||||
|
? \AKSRT\User\NotificationPreferences::get($user_id)
|
||||||
|
: ['posts' => true, 'like' => true, 'system' => true];
|
||||||
|
$items = [
|
||||||
|
'posts' => [
|
||||||
|
'title' => __('文章评论', 'aksrt'),
|
||||||
|
'description' => __('接收文章、评论、收藏等相关消息', 'aksrt'),
|
||||||
|
],
|
||||||
|
'like' => [
|
||||||
|
'title' => __('点赞喜欢', 'aksrt'),
|
||||||
|
'description' => __('接收点赞、关注等相关消息', 'aksrt'),
|
||||||
|
],
|
||||||
|
'system' => [
|
||||||
|
'title' => __('系统消息', 'aksrt'),
|
||||||
|
'description' => __('接收订单、活动、等系统消息', 'aksrt'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<form class="uc-notification-preferences" data-aksrt-notification-preferences>
|
||||||
|
<div class="uc-notification-preferences__header">
|
||||||
|
<div>
|
||||||
|
<h2><?php esc_html_e('消息通知', 'aksrt'); ?></h2>
|
||||||
|
<p><?php esc_html_e('选择你希望接收的站内消息类型。', 'aksrt'); ?></p>
|
||||||
|
</div>
|
||||||
|
<span class="uc-notification-preferences__header-icon" aria-hidden="true"><i class="fa fas fa-bell"></i></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uc-notification-preferences__list">
|
||||||
|
<?php foreach ($items as $key => $item) : ?>
|
||||||
|
<label class="uc-notification-preference" for="aksrt-notification-<?php echo esc_attr($key); ?>">
|
||||||
|
<span class="uc-notification-preference__copy">
|
||||||
|
<strong><?php echo esc_html($item['title']); ?></strong>
|
||||||
|
<small><?php echo esc_html($item['description']); ?></small>
|
||||||
|
</span>
|
||||||
|
<span class="uc-notification-switch">
|
||||||
|
<input id="aksrt-notification-<?php echo esc_attr($key); ?>" type="checkbox" name="<?php echo esc_attr($key); ?>" value="1" role="switch" <?php checked(!empty($preferences[$key])); ?>>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uc-notification-preferences__footer">
|
||||||
|
<button type="submit" class="btn-primary"><?php esc_html_e('保存设置', 'aksrt'); ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -43,8 +43,8 @@ $settings = array_values(array_filter([
|
|||||||
'label' => __('消息通知', 'aksrt'),
|
'label' => __('消息通知', 'aksrt'),
|
||||||
'icon' => 'bell',
|
'icon' => 'bell',
|
||||||
'tone' => 'notice',
|
'tone' => 'notice',
|
||||||
'url' => (string) ($messages_url ?? ''),
|
'url' => isset($dashboard_tabs['msg']) ? aksrt_get_user_center_tab_url('msg') : '',
|
||||||
'count' => (int) ($notification_unread_count ?? 0),
|
'tab' => 'msg',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => __('个人资料', 'aksrt'),
|
'label' => __('个人资料', 'aksrt'),
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ function aksrt_get_user_notifications(int $user_id = 0): array {
|
|||||||
'status' => 'hold',
|
'status' => 'hold',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($pending_comments > 0 && (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::notification_enabled('pending_comments'))) {
|
if (
|
||||||
|
$pending_comments > 0
|
||||||
|
&& (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::notification_enabled('pending_comments'))
|
||||||
|
&& (!class_exists('\AKSRT\User\NotificationPreferences') || \AKSRT\User\NotificationPreferences::allows($user_id, 'pending_comments'))
|
||||||
|
) {
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'type' => 'pending_comments',
|
'type' => 'pending_comments',
|
||||||
'title' => __('评论待审核', 'aksrt'),
|
'title' => __('评论待审核', 'aksrt'),
|
||||||
@@ -249,6 +253,9 @@ if (!function_exists('aksrt_add_user_notification')) :
|
|||||||
function aksrt_add_user_notification(int $user_id, array $payload): void {
|
function aksrt_add_user_notification(int $user_id, array $payload): void {
|
||||||
if ($user_id <= 0) return;
|
if ($user_id <= 0) return;
|
||||||
$type = sanitize_key((string) ($payload['type'] ?? 'notice'));
|
$type = sanitize_key((string) ($payload['type'] ?? 'notice'));
|
||||||
|
if (class_exists('\AKSRT\User\NotificationPreferences') && !\AKSRT\User\NotificationPreferences::allows($user_id, $type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::notification_enabled($type)) {
|
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::notification_enabled($type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ add_action('template_redirect', function () {
|
|||||||
$tab = $tab === 'balance' ? 'credits' : sanitize_key($tab);
|
$tab = $tab === 'balance' ? 'credits' : sanitize_key($tab);
|
||||||
$allowed = class_exists('\\AKSRT\\User\\Center')
|
$allowed = class_exists('\\AKSRT\\User\\Center')
|
||||||
? array_keys(\AKSRT\User\Center::get_instance()->get_dashboard_tabs())
|
? array_keys(\AKSRT\User\Center::get_instance()->get_dashboard_tabs())
|
||||||
: ['profile', 'level', 'creator', 'posts', 'follow', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'revenue', 'settings'];
|
: ['profile', 'level', 'creator', 'posts', 'follow', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'revenue', 'msg', 'settings'];
|
||||||
if (!in_array($tab, $allowed, true)) {
|
if (!in_array($tab, $allowed, true)) {
|
||||||
global $wp_query;
|
global $wp_query;
|
||||||
if ($wp_query instanceof WP_Query) { $wp_query->set_404(); }
|
if ($wp_query instanceof WP_Query) { $wp_query->set_404(); }
|
||||||
|
|||||||
+5
-5
@@ -188,11 +188,11 @@ class Center {
|
|||||||
'priority' => 55,
|
'priority' => 55,
|
||||||
'type' => 'panel',
|
'type' => 'panel',
|
||||||
],
|
],
|
||||||
'notif' => [
|
'msg' => [
|
||||||
'label' => __('消息通知', 'aksrt'),
|
'label' => __('消息通知', 'aksrt'),
|
||||||
'icon' => 'bell',
|
'icon' => 'bell',
|
||||||
'priority' => 60,
|
'priority' => 60,
|
||||||
'type' => 'link',
|
'type' => 'panel',
|
||||||
],
|
],
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'label' => __('官方认证', 'aksrt'),
|
'label' => __('官方认证', 'aksrt'),
|
||||||
@@ -307,6 +307,9 @@ class Center {
|
|||||||
if (!in_array('settings', $order, true)) {
|
if (!in_array('settings', $order, true)) {
|
||||||
$order[] = 'settings';
|
$order[] = 'settings';
|
||||||
}
|
}
|
||||||
|
if (!in_array('msg', $order, true)) {
|
||||||
|
$order[] = 'msg';
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($order as $key) {
|
foreach ($order as $key) {
|
||||||
if (!isset($definitions[$key])) {
|
if (!isset($definitions[$key])) {
|
||||||
@@ -339,9 +342,6 @@ class Center {
|
|||||||
$this->panels[$key] = $panel;
|
$this->panels[$key] = $panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notif is a link entry (messages page), not a dashboard panel body.
|
|
||||||
$this->panels['notif'] = $definitions['notif'];
|
|
||||||
|
|
||||||
// Keep stable order by priority.
|
// Keep stable order by priority.
|
||||||
uasort($this->panels, static function (array $a, array $b): int {
|
uasort($this->panels, static function (array $a, array $b): int {
|
||||||
return ((int) ($a['priority'] ?? 100)) <=> ((int) ($b['priority'] ?? 100));
|
return ((int) ($a['priority'] ?? 100)) <=> ((int) ($b['priority'] ?? 100));
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* User notification category preferences and REST persistence.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\User;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
final class NotificationPreferences {
|
||||||
|
private const META_KEY = 'aksrt_notification_preferences';
|
||||||
|
|
||||||
|
private const CATEGORIES = ['posts', 'like', 'system'];
|
||||||
|
|
||||||
|
public static function init(): void {
|
||||||
|
add_action('rest_api_init', [self::class, 'register_rest_route']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register_rest_route(): void {
|
||||||
|
register_rest_route('aksrt/v1', '/user/notification-preferences', [
|
||||||
|
'methods' => \WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => [self::class, 'save'],
|
||||||
|
'permission_callback' => static function (): bool {
|
||||||
|
return get_current_user_id() > 0 && current_user_can('read');
|
||||||
|
},
|
||||||
|
'args' => [
|
||||||
|
'preferences' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'required' => true,
|
||||||
|
'properties' => [
|
||||||
|
'posts' => ['type' => 'boolean', 'required' => true],
|
||||||
|
'like' => ['type' => 'boolean', 'required' => true],
|
||||||
|
'system' => ['type' => 'boolean', 'required' => true],
|
||||||
|
],
|
||||||
|
'additionalProperties' => false,
|
||||||
|
'validate_callback' => [self::class, 'validate_preferences'],
|
||||||
|
'sanitize_callback' => [self::class, 'sanitize_preferences'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{posts:bool,like:bool,system:bool} */
|
||||||
|
public static function get(int $user_id): array {
|
||||||
|
$defaults = array_fill_keys(self::CATEGORIES, true);
|
||||||
|
$stored = get_user_meta($user_id, self::META_KEY, true);
|
||||||
|
if (!is_array($stored)) {
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::CATEGORIES as $category) {
|
||||||
|
if (array_key_exists($category, $stored)) {
|
||||||
|
$defaults[$category] = (bool) $stored[$category];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function allows(int $user_id, string $notification_type): bool {
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$preferences = self::get($user_id);
|
||||||
|
return $preferences[self::category_for_type($notification_type)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function category_for_type(string $notification_type): string {
|
||||||
|
$type = sanitize_key($notification_type);
|
||||||
|
if (
|
||||||
|
in_array($type, ['posts', 'post', 'comment', 'favorite', 'pending_comments'], true)
|
||||||
|
|| str_starts_with($type, 'post_')
|
||||||
|
|| str_starts_with($type, 'comment_')
|
||||||
|
|| str_starts_with($type, 'favorite_')
|
||||||
|
) {
|
||||||
|
return 'posts';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
in_array($type, ['like', 'follow', 'followed', 'hot'], true)
|
||||||
|
|| str_starts_with($type, 'like_')
|
||||||
|
|| str_starts_with($type, 'follow_')
|
||||||
|
|| str_starts_with($type, 'followed_')
|
||||||
|
) {
|
||||||
|
return 'like';
|
||||||
|
}
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validate_preferences($value): bool {
|
||||||
|
if (
|
||||||
|
!is_array($value)
|
||||||
|
|| count($value) !== count(self::CATEGORIES)
|
||||||
|
|| array_diff(array_keys($value), self::CATEGORIES)
|
||||||
|
|| array_diff(self::CATEGORIES, array_keys($value))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (self::CATEGORIES as $category) {
|
||||||
|
if (!is_bool($value[$category])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{posts:bool,like:bool,system:bool} */
|
||||||
|
public static function sanitize_preferences($value): array {
|
||||||
|
$value = is_array($value) ? $value : [];
|
||||||
|
return [
|
||||||
|
'posts' => (bool) ($value['posts'] ?? false),
|
||||||
|
'like' => (bool) ($value['like'] ?? false),
|
||||||
|
'system' => (bool) ($value['system'] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function save(\WP_REST_Request $request) {
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
return new \WP_Error('aksrt_notification_preferences_auth', __('请先登录', 'aksrt'), ['status' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$preferences = self::sanitize_preferences($request->get_param('preferences'));
|
||||||
|
$result = update_user_meta($user_id, self::META_KEY, $preferences);
|
||||||
|
if ($result === false && self::get($user_id) !== $preferences) {
|
||||||
|
return new \WP_Error('aksrt_notification_preferences_save', __('消息通知设置保存失败', 'aksrt'), ['status' => 500]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest_ensure_response([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('消息通知设置已保存', 'aksrt'),
|
||||||
|
'preferences' => $preferences,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||||
|
|
||||||
|
test('message notification settings are a canonical user/msg inline panel', () => {
|
||||||
|
const center = read('src/User/Center.php');
|
||||||
|
const helpers = read('inc/helpers/aksrt-user-center.php');
|
||||||
|
const sidebar = read('inc/helpers/user-center-renderers/sidebar-nav.php');
|
||||||
|
const routing = read('inc/modules/user-parts/routing.php');
|
||||||
|
|
||||||
|
assert.match(center, /'msg'\s*=>\s*\[[\s\S]*?'type'\s*=>\s*'panel'/);
|
||||||
|
assert.doesNotMatch(center, /'notif'\s*=>/);
|
||||||
|
assert.match(helpers, /'msg'\s*=>\s*'msg'/);
|
||||||
|
assert.match(sidebar, /aksrt_get_user_center_tab_url\('msg'\)/);
|
||||||
|
assert.match(sidebar, /'tab'\s*=>\s*'msg'/);
|
||||||
|
assert.doesNotMatch(sidebar, /'url'\s*=>\s*\(string\) \(\$messages_url/);
|
||||||
|
assert.match(routing, /'revenue', 'msg', 'settings'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notification preference endpoint stores only the three boolean categories', () => {
|
||||||
|
const controller = read('src/User/NotificationPreferences.php');
|
||||||
|
|
||||||
|
assert.match(controller, /META_KEY = 'aksrt_notification_preferences'/);
|
||||||
|
assert.match(controller, /CATEGORIES = \['posts', 'like', 'system'\]/);
|
||||||
|
assert.match(controller, /register_rest_route\('aksrt\/v1', '\/user\/notification-preferences'/);
|
||||||
|
assert.match(controller, /permission_callback/);
|
||||||
|
assert.match(controller, /current_user_can\('read'\)/);
|
||||||
|
assert.match(controller, /'additionalProperties'\s*=>\s*false/);
|
||||||
|
assert.match(controller, /array_diff\(array_keys\(\$value\), self::CATEGORIES\)/);
|
||||||
|
assert.doesNotMatch(controller, /message_shield|migration|legacy/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notification preference UI uses the global toast lifecycle', () => {
|
||||||
|
const template = read('inc/helpers/user-center-renderers/profile-msg.php');
|
||||||
|
const script = read('assets/js/routes/user-center.js');
|
||||||
|
|
||||||
|
for (const text of [
|
||||||
|
'文章评论',
|
||||||
|
'接收文章、评论、收藏等相关消息',
|
||||||
|
'点赞喜欢',
|
||||||
|
'接收点赞、关注等相关消息',
|
||||||
|
'系统消息',
|
||||||
|
'接收订单、活动、等系统消息',
|
||||||
|
]) {
|
||||||
|
assert.ok(template.includes(text));
|
||||||
|
}
|
||||||
|
assert.match(template, /role="switch"/);
|
||||||
|
assert.match(script, /aksrt-notification-preferences-toast/);
|
||||||
|
assert.match(script, /notyf\([^;]+?'load'[^;]+?toastId/);
|
||||||
|
assert.match(script, /notyf\([^;]+?'success'[^;]+?toastId/);
|
||||||
|
assert.match(script, /notyf\([^;]+?'danger'[^;]+?toastId/);
|
||||||
|
assert.doesNotMatch(script, /\b(?:window\.)?alert\s*\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notification writes and stateful alerts honor the category preference', () => {
|
||||||
|
const notifications = read('inc/modules/user-notifications.php');
|
||||||
|
|
||||||
|
assert.match(notifications, /NotificationPreferences::allows\(\$user_id, 'pending_comments'\)/);
|
||||||
|
assert.match(notifications, /NotificationPreferences::allows\(\$user_id, \$type\)/);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user