@meith/web 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +165 -0
- package/app/(auth)/layout.tsx +6 -0
- package/app/(auth)/login/page.tsx +106 -0
- package/app/(auth)/login/verify/page.tsx +55 -0
- package/app/(auth)/register/page.tsx +83 -0
- package/app/(auth)/reset/confirm/page.tsx +45 -0
- package/app/(auth)/reset/page.tsx +22 -0
- package/app/(auth)/verify/page.tsx +44 -0
- package/app/(auth)/verify/resend/page.tsx +51 -0
- package/app/(board)/[slug]/feed.xml/route.ts +48 -0
- package/app/(board)/[slug]/new/page.tsx +112 -0
- package/app/(board)/[slug]/page.tsx +403 -0
- package/app/(board)/discover/[view]/page.tsx +115 -0
- package/app/(board)/discover/page.tsx +5 -0
- package/app/(board)/layout.tsx +22 -0
- package/app/(board)/member/[id]/page.tsx +187 -0
- package/app/(board)/member/[id]/reputation/page.tsx +195 -0
- package/app/(board)/member/by-name/[name]/page.tsx +32 -0
- package/app/(board)/messages/[id]/page.tsx +103 -0
- package/app/(board)/messages/compose/page.tsx +60 -0
- package/app/(board)/messages/layout.tsx +5 -0
- package/app/(board)/messages/page.tsx +179 -0
- package/app/(board)/modcp/forums/page.tsx +59 -0
- package/app/(board)/modcp/ip/page.tsx +158 -0
- package/app/(board)/modcp/layout.tsx +5 -0
- package/app/(board)/modcp/log/page.tsx +98 -0
- package/app/(board)/modcp/page.tsx +112 -0
- package/app/(board)/moderation/layout.tsx +5 -0
- package/app/(board)/moderation/page.tsx +111 -0
- package/app/(board)/moderation/reports/page.tsx +193 -0
- package/app/(board)/moderation/warn/page.tsx +158 -0
- package/app/(board)/notifications/layout.tsx +5 -0
- package/app/(board)/notifications/page.tsx +144 -0
- package/app/(board)/notifications/preferences/page.tsx +65 -0
- package/app/(board)/online/page.tsx +119 -0
- package/app/(board)/page.tsx +195 -0
- package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
- package/app/(board)/privacy/page.tsx +12 -0
- package/app/(board)/report/page.tsx +71 -0
- package/app/(board)/rules/page.tsx +12 -0
- package/app/(board)/search/[token]/page.tsx +130 -0
- package/app/(board)/search/page.tsx +289 -0
- package/app/(board)/stats/page.tsx +202 -0
- package/app/(board)/subscriptions/layout.tsx +5 -0
- package/app/(board)/subscriptions/page.tsx +142 -0
- package/app/(board)/terms/page.tsx +12 -0
- package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
- package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
- package/app/(board)/thread/[slug]/page.tsx +707 -0
- package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
- package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
- package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
- package/app/(board)/unsubscribe/page.tsx +73 -0
- package/app/(board)/usercp/avatar/page.tsx +56 -0
- package/app/(board)/usercp/contacts/page.tsx +141 -0
- package/app/(board)/usercp/email/confirm/page.tsx +52 -0
- package/app/(board)/usercp/layout.tsx +23 -0
- package/app/(board)/usercp/options/page.tsx +56 -0
- package/app/(board)/usercp/page.tsx +71 -0
- package/app/(board)/usercp/profile/page.tsx +81 -0
- package/app/(board)/usercp/security/page.tsx +238 -0
- package/app/(board)/usercp/security/verify/page.tsx +92 -0
- package/app/(board)/usercp/signature/page.tsx +67 -0
- package/app/admin/antispam/page.tsx +155 -0
- package/app/admin/api/marketplace/screenshot/route.ts +68 -0
- package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
- package/app/admin/api-tokens/page.tsx +129 -0
- package/app/admin/content/announcements/page.tsx +110 -0
- package/app/admin/content/attachments/page.tsx +199 -0
- package/app/admin/content/navigation/page.tsx +123 -0
- package/app/admin/content/page.tsx +170 -0
- package/app/admin/forums/[id]/page.tsx +96 -0
- package/app/admin/forums/[id]/permissions/page.tsx +108 -0
- package/app/admin/forums/page.tsx +45 -0
- package/app/admin/groups/[id]/page.tsx +133 -0
- package/app/admin/groups/memberships/page.tsx +56 -0
- package/app/admin/groups/page.tsx +104 -0
- package/app/admin/groups/promotions/page.tsx +143 -0
- package/app/admin/layout.tsx +86 -0
- package/app/admin/log/page.tsx +116 -0
- package/app/admin/page.tsx +206 -0
- package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
- package/app/admin/plugins/browse/page.tsx +70 -0
- package/app/admin/plugins/page.tsx +165 -0
- package/app/admin/security/page.tsx +124 -0
- package/app/admin/settings/page.tsx +190 -0
- package/app/admin/system/page.tsx +303 -0
- package/app/admin/themes/[key]/page.tsx +110 -0
- package/app/admin/themes/browse/page.tsx +70 -0
- package/app/admin/themes/page.tsx +163 -0
- package/app/admin/users/[id]/merge/page.tsx +136 -0
- package/app/admin/users/[id]/page.tsx +208 -0
- package/app/admin/users/mail/page.tsx +68 -0
- package/app/admin/users/page.tsx +260 -0
- package/app/admin/users/prune/page.tsx +186 -0
- package/app/api/health/route.ts +11 -0
- package/app/api/metrics/route.ts +66 -0
- package/app/api/plugins/[key]/[...path]/route.ts +27 -0
- package/app/api/read/all/route.ts +15 -0
- package/app/api/read/forum/[id]/route.ts +29 -0
- package/app/api/read/thread/[id]/route.ts +40 -0
- package/app/api/ready/route.ts +63 -0
- package/app/api/system/tick/route.ts +77 -0
- package/app/api/v1/[...path]/route.ts +208 -0
- package/app/api/v1/openapi.json/route.ts +12 -0
- package/app/apple-icon.ts +11 -0
- package/app/atom.xml/route.ts +7 -0
- package/app/attachment/[id]/route.ts +10 -0
- package/app/attachment/[id]/thumb/route.ts +10 -0
- package/app/auth/passkey/options/route.ts +145 -0
- package/app/auth/passkey/verify/route.ts +241 -0
- package/app/auth/resume/route.ts +55 -0
- package/app/auth/sso/[provider]/callback/route.ts +173 -0
- package/app/auth/sso/[provider]/go/route.ts +57 -0
- package/app/auth/sso/[provider]/route.ts +105 -0
- package/app/avatar/[id]/route.ts +31 -0
- package/app/brand/icon-192.png/route.ts +7 -0
- package/app/brand/icon-512.png/route.ts +7 -0
- package/app/brand/icon-maskable.png/route.ts +7 -0
- package/app/demo/stripe/[[...path]]/route.ts +27 -0
- package/app/error.tsx +15 -0
- package/app/feed.xml/route.ts +7 -0
- package/app/forumdisplay.php/page.tsx +11 -0
- package/app/group/[id]/badge/[scheme]/route.ts +26 -0
- package/app/icon.ts +13 -0
- package/app/install/page.tsx +221 -0
- package/app/jump/page.tsx +28 -0
- package/app/layout.tsx +124 -0
- package/app/logo/[scheme]/route.ts +24 -0
- package/app/manifest.ts +43 -0
- package/app/member.php/page.tsx +11 -0
- package/app/memberlist.php/page.tsx +11 -0
- package/app/not-found.tsx +14 -0
- package/app/opengraph-image.ts +11 -0
- package/app/redirect/page.tsx +44 -0
- package/app/robots.txt/route.ts +35 -0
- package/app/showthread.php/page.tsx +11 -0
- package/app/sitemap/[page]/route.ts +64 -0
- package/app/sitemap.xml/route.ts +31 -0
- package/app/twitter-image.ts +11 -0
- package/app/viewforum.php/page.tsx +11 -0
- package/app/viewtopic.php/page.tsx +11 -0
- package/bin/forum-web.mjs +241 -0
- package/components.json +21 -0
- package/instrumentation.ts +18 -0
- package/next.config.mjs +182 -0
- package/package.json +99 -0
- package/postcss.config.mjs +7 -0
- package/proxy.ts +160 -0
- package/src/components/account/active-sessions.tsx +87 -0
- package/src/components/account/avatar-form.tsx +119 -0
- package/src/components/account/credential-proof-form.tsx +76 -0
- package/src/components/account/logout-form.tsx +14 -0
- package/src/components/account/notification-forms.tsx +157 -0
- package/src/components/account/passkey-enrol.tsx +81 -0
- package/src/components/account/passkey-proof-button.tsx +47 -0
- package/src/components/account/push-device-form.tsx +193 -0
- package/src/components/account/relation-forms.tsx +69 -0
- package/src/components/account/reputation-forms.tsx +124 -0
- package/src/components/account/security-activity.tsx +46 -0
- package/src/components/account/sign-in-methods.tsx +166 -0
- package/src/components/account/subscription-forms.tsx +151 -0
- package/src/components/account/two-factor-forms.tsx +226 -0
- package/src/components/account/usercp-forms.tsx +387 -0
- package/src/components/account/usercp-nav.tsx +14 -0
- package/src/components/account/usercp-shell.tsx +34 -0
- package/src/components/admin/admin-forms.tsx +92 -0
- package/src/components/admin/admin-nav.tsx +52 -0
- package/src/components/admin/admin-undo.tsx +32 -0
- package/src/components/admin/api-token-forms.tsx +89 -0
- package/src/components/admin/badge-forms.tsx +97 -0
- package/src/components/admin/branding-forms.tsx +102 -0
- package/src/components/admin/content-forms.tsx +712 -0
- package/src/components/admin/form-bits.tsx +11 -0
- package/src/components/admin/forum-forms.tsx +524 -0
- package/src/components/admin/forum-tree.tsx +454 -0
- package/src/components/admin/group-forms.tsx +646 -0
- package/src/components/admin/mail-test-card.tsx +90 -0
- package/src/components/admin/marketplace-forms.tsx +30 -0
- package/src/components/admin/marketplace-listing.tsx +123 -0
- package/src/components/admin/navigation-forms.tsx +229 -0
- package/src/components/admin/navigation-tree.tsx +442 -0
- package/src/components/admin/oklch-picker.tsx +180 -0
- package/src/components/admin/plugin-forms.tsx +225 -0
- package/src/components/admin/settings-form.tsx +148 -0
- package/src/components/admin/system-forms.tsx +169 -0
- package/src/components/admin/theme-changes.tsx +354 -0
- package/src/components/admin/theme-forms.tsx +636 -0
- package/src/components/admin/theme-palette.tsx +119 -0
- package/src/components/admin/theme-preview.tsx +461 -0
- package/src/components/admin/user-bulk-toolbar.tsx +99 -0
- package/src/components/admin/user-forms.tsx +488 -0
- package/src/components/auth/auth-page.tsx +45 -0
- package/src/components/auth/form-controls.tsx +117 -0
- package/src/components/auth/login-form.tsx +47 -0
- package/src/components/auth/passkey-client.ts +171 -0
- package/src/components/auth/passkey-second-factor.tsx +69 -0
- package/src/components/auth/passkey-sign-in.tsx +61 -0
- package/src/components/auth/register-form.tsx +136 -0
- package/src/components/auth/resend-verification-form.tsx +34 -0
- package/src/components/auth/reset-confirm-form.tsx +43 -0
- package/src/components/auth/reset-request-form.tsx +35 -0
- package/src/components/auth/second-factor-form.tsx +51 -0
- package/src/components/auth/sso-buttons.tsx +47 -0
- package/src/components/board/live-region.tsx +74 -0
- package/src/components/board/search-off-notice.tsx +18 -0
- package/src/components/board/section-page.tsx +114 -0
- package/src/components/content/attachment-field.tsx +51 -0
- package/src/components/content/composer-field.ts +31 -0
- package/src/components/content/composer-ids.ts +6 -0
- package/src/components/content/composer-intents.tsx +27 -0
- package/src/components/content/composer-recovery.tsx +215 -0
- package/src/components/content/edit-post-form.tsx +121 -0
- package/src/components/content/markdown-editor.tsx +590 -0
- package/src/components/content/markdown-syntax.ts +53 -0
- package/src/components/content/mention-token.ts +23 -0
- package/src/components/content/multiquote-button.tsx +32 -0
- package/src/components/content/multiquote-selection.tsx +105 -0
- package/src/components/content/multiquote.ts +125 -0
- package/src/components/content/new-thread-form.tsx +169 -0
- package/src/components/content/poll-edit-form.tsx +96 -0
- package/src/components/content/poll.tsx +89 -0
- package/src/components/content/quote-in-place.tsx +51 -0
- package/src/components/content/reply-form.tsx +113 -0
- package/src/components/content/thanks-button.tsx +65 -0
- package/src/components/content/thread-rating.tsx +143 -0
- package/src/components/install/install-form.tsx +497 -0
- package/src/components/legal/legal-document.tsx +22 -0
- package/src/components/messages/message-forms.tsx +152 -0
- package/src/components/moderation/inline-moderation-form.tsx +158 -0
- package/src/components/moderation/modcp-nav.tsx +15 -0
- package/src/components/moderation/modcp-shell.tsx +41 -0
- package/src/components/moderation/queue-form.tsx +96 -0
- package/src/components/moderation/report-forms.tsx +120 -0
- package/src/components/moderation/signature-lock-form.tsx +83 -0
- package/src/components/moderation/thread-surgery-form.tsx +96 -0
- package/src/components/moderation/thread-tools-form.tsx +137 -0
- package/src/components/moderation/warning-forms.tsx +125 -0
- package/src/components/profile/custom-field.tsx +79 -0
- package/src/components/shell/appearance-icons.tsx +35 -0
- package/src/components/shell/board-notice.tsx +21 -0
- package/src/components/shell/copy-record.ts +20 -0
- package/src/components/shell/copy.tsx +17 -0
- package/src/components/shell/crash-notice.tsx +86 -0
- package/src/components/shell/demo-banner.tsx +49 -0
- package/src/components/shell/group-name-style.tsx +6 -0
- package/src/components/shell/measure.ts +1 -0
- package/src/components/shell/notification-menu.tsx +361 -0
- package/src/components/shell/offline-notice.tsx +29 -0
- package/src/components/shell/onboarding-banner.tsx +39 -0
- package/src/components/shell/page-shell.tsx +167 -0
- package/src/components/shell/panel-list.tsx +95 -0
- package/src/components/shell/panel-nav.tsx +40 -0
- package/src/components/shell/panel-overview.tsx +81 -0
- package/src/components/shell/panel-page.tsx +96 -0
- package/src/components/shell/panel-pagination.tsx +28 -0
- package/src/components/shell/panel-shell.tsx +31 -0
- package/src/components/shell/theme-runtime-style.tsx +6 -0
- package/src/components/shell/theme-switcher.tsx +97 -0
- package/src/components/shell/timezone-probe.tsx +45 -0
- package/src/components/shell/view-tabs.tsx +60 -0
- package/src/config.ts +13 -0
- package/src/server/absent-services.ts +29 -0
- package/src/server/admin-actions.ts +171 -0
- package/src/server/admin-settings-actions.ts +83 -0
- package/src/server/admin-undo-actions.ts +70 -0
- package/src/server/admin-undo.ts +75 -0
- package/src/server/admin.ts +112 -0
- package/src/server/announcements.ts +82 -0
- package/src/server/antispam.ts +310 -0
- package/src/server/api/content.ts +486 -0
- package/src/server/api/http.ts +119 -0
- package/src/server/api/members.ts +109 -0
- package/src/server/api/messages.ts +113 -0
- package/src/server/api/registry.ts +30 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/subscriptions.ts +106 -0
- package/src/server/api-auth.ts +66 -0
- package/src/server/api-token-actions.ts +85 -0
- package/src/server/api-tokens-admin.ts +95 -0
- package/src/server/appearance-actions.ts +39 -0
- package/src/server/attachment-download.ts +43 -0
- package/src/server/attachment-embed.ts +63 -0
- package/src/server/attachment-upload-actions.ts +92 -0
- package/src/server/attachments.ts +257 -0
- package/src/server/auth-actions.ts +479 -0
- package/src/server/auth-config.ts +61 -0
- package/src/server/auth-events.ts +92 -0
- package/src/server/auth-form-state.ts +14 -0
- package/src/server/auth-mail.ts +104 -0
- package/src/server/avatars.ts +83 -0
- package/src/server/board-latest-actions.ts +7 -0
- package/src/server/board-latest.tsx +103 -0
- package/src/server/board-offline.ts +23 -0
- package/src/server/board-url.ts +14 -0
- package/src/server/brand-assets.ts +151 -0
- package/src/server/brand-mark.tsx +110 -0
- package/src/server/branding-actions.ts +53 -0
- package/src/server/branding.ts +101 -0
- package/src/server/cache-targets.ts +13 -0
- package/src/server/code-highlighting.ts +33 -0
- package/src/server/container.ts +402 -0
- package/src/server/content-actions.ts +459 -0
- package/src/server/content-admin-actions.ts +492 -0
- package/src/server/content-admin.ts +95 -0
- package/src/server/content-security-policy.ts +35 -0
- package/src/server/context.ts +32 -0
- package/src/server/cookies.ts +139 -0
- package/src/server/credential-proof-actions.ts +42 -0
- package/src/server/credential-proof.ts +35 -0
- package/src/server/current-location.ts +19 -0
- package/src/server/demo-stripe.ts +104 -0
- package/src/server/demo-uploads.ts +28 -0
- package/src/server/demo.ts +80 -0
- package/src/server/discovery.ts +104 -0
- package/src/server/embeds.ts +113 -0
- package/src/server/error-notice.tsx +41 -0
- package/src/server/federation-actions.ts +113 -0
- package/src/server/federation.ts +138 -0
- package/src/server/feed-builder.ts +72 -0
- package/src/server/feed-routes.ts +122 -0
- package/src/server/fixture-actor-source.ts +62 -0
- package/src/server/fixture-forum-repo.ts +59 -0
- package/src/server/fixture-member-profile-repo.ts +30 -0
- package/src/server/fixture-post-repo.ts +108 -0
- package/src/server/fixture-thread-repo.ts +98 -0
- package/src/server/form-state-reporter.ts +34 -0
- package/src/server/form-values.ts +23 -0
- package/src/server/forum-admin-actions.ts +353 -0
- package/src/server/forum-admin.ts +98 -0
- package/src/server/group-admin-actions.ts +341 -0
- package/src/server/group-admin.ts +98 -0
- package/src/server/group-badge-actions.ts +61 -0
- package/src/server/group-badge.ts +62 -0
- package/src/server/group-identity.ts +104 -0
- package/src/server/i18n-catalogs.ts +42 -0
- package/src/server/i18n.ts +69 -0
- package/src/server/image-upload.ts +109 -0
- package/src/server/inline-moderation-actions.ts +129 -0
- package/src/server/install-actions.ts +124 -0
- package/src/server/install.ts +281 -0
- package/src/server/legacy-redirect.ts +83 -0
- package/src/server/legal.ts +43 -0
- package/src/server/location-header.ts +37 -0
- package/src/server/mail-brand.ts +43 -0
- package/src/server/mail-health.ts +41 -0
- package/src/server/mail-send.ts +44 -0
- package/src/server/mail-test-actions.ts +67 -0
- package/src/server/mail-test.ts +56 -0
- package/src/server/markdown-pipeline.ts +31 -0
- package/src/server/marketplace-actions.ts +47 -0
- package/src/server/marketplace-admin.ts +260 -0
- package/src/server/mention-actions.ts +21 -0
- package/src/server/mention-search.ts +35 -0
- package/src/server/message-actions.ts +110 -0
- package/src/server/messages.ts +141 -0
- package/src/server/modcp.ts +125 -0
- package/src/server/moderation-actions.ts +165 -0
- package/src/server/navigation-admin-actions.ts +217 -0
- package/src/server/navigation.ts +112 -0
- package/src/server/nonce.ts +13 -0
- package/src/server/notification-actions.ts +84 -0
- package/src/server/notification-audience.ts +10 -0
- package/src/server/notification-menu-actions.ts +82 -0
- package/src/server/notification-menu.ts +96 -0
- package/src/server/notifications.ts +93 -0
- package/src/server/onboarding-actions.ts +27 -0
- package/src/server/onboarding.ts +89 -0
- package/src/server/page-metadata.ts +14 -0
- package/src/server/passkey-challenge.ts +42 -0
- package/src/server/plugin-admin-actions.ts +232 -0
- package/src/server/plugin-admin.ts +276 -0
- package/src/server/plugin-host.ts +143 -0
- package/src/server/plugin-pages.ts +192 -0
- package/src/server/plugin-panel.ts +49 -0
- package/src/server/plugin-routes.ts +298 -0
- package/src/server/plugin-view.tsx +58 -0
- package/src/server/poll-actions.ts +104 -0
- package/src/server/poll-scope.ts +52 -0
- package/src/server/post-notifications.ts +71 -0
- package/src/server/post-scope.ts +86 -0
- package/src/server/presence.ts +177 -0
- package/src/server/profile-fields.ts +112 -0
- package/src/server/push-actions.ts +113 -0
- package/src/server/push.ts +40 -0
- package/src/server/redirect-back.ts +15 -0
- package/src/server/registration.ts +7 -0
- package/src/server/relation-actions.ts +89 -0
- package/src/server/relations.ts +45 -0
- package/src/server/reply-core.ts +173 -0
- package/src/server/report-actions.ts +165 -0
- package/src/server/report-scope.ts +21 -0
- package/src/server/reputation-actions.ts +146 -0
- package/src/server/reputation-target.ts +49 -0
- package/src/server/reputation.ts +70 -0
- package/src/server/request-fingerprint.ts +65 -0
- package/src/server/safe-path.ts +16 -0
- package/src/server/same-origin.ts +45 -0
- package/src/server/search-page.ts +313 -0
- package/src/server/search.ts +45 -0
- package/src/server/secret-auth.ts +20 -0
- package/src/server/see-other.ts +11 -0
- package/src/server/seed-board.ts +417 -0
- package/src/server/session-actions.ts +78 -0
- package/src/server/session-cookies.ts +124 -0
- package/src/server/session-key.ts +17 -0
- package/src/server/settings.ts +47 -0
- package/src/server/signatures.ts +62 -0
- package/src/server/sso-handshake.ts +79 -0
- package/src/server/stats.ts +63 -0
- package/src/server/subscription-actions.ts +161 -0
- package/src/server/surgery-actions.ts +180 -0
- package/src/server/syndication.ts +77 -0
- package/src/server/system-admin-actions.ts +134 -0
- package/src/server/system-admin.ts +90 -0
- package/src/server/test-container.ts +114 -0
- package/src/server/theme-admin-actions.ts +242 -0
- package/src/server/theme-admin.ts +203 -0
- package/src/server/theme-runtime.ts +89 -0
- package/src/server/theme-style.ts +279 -0
- package/src/server/theme.ts +52 -0
- package/src/server/thread-core.ts +207 -0
- package/src/server/thread-rating-actions.ts +55 -0
- package/src/server/thread-tool-actions.ts +154 -0
- package/src/server/two-factor-actions.ts +178 -0
- package/src/server/two-factor.ts +86 -0
- package/src/server/upgrade-notice.ts +41 -0
- package/src/server/user-admin-actions.ts +572 -0
- package/src/server/user-admin.ts +165 -0
- package/src/server/usercp-actions.ts +263 -0
- package/src/server/usercp-mail.ts +120 -0
- package/src/server/viewer-preferences.ts +64 -0
- package/src/server/warning-actions.ts +122 -0
- package/src/styles/globals.css +969 -0
- package/src/theme/contract.fixture.ts +793 -0
- package/src/view/account-copy.ts +351 -0
- package/src/view/admin-content-copy.ts +66 -0
- package/src/view/admin-copy.ts +106 -0
- package/src/view/admin-forum-tree-copy.ts +40 -0
- package/src/view/admin-group-copy.ts +80 -0
- package/src/view/admin-log.ts +58 -0
- package/src/view/admin-nav.ts +164 -0
- package/src/view/admin-navigation-copy.ts +58 -0
- package/src/view/admin-panel-copy.ts +194 -0
- package/src/view/admin-settings.ts +173 -0
- package/src/view/admin-theme-copy.ts +229 -0
- package/src/view/admin-user-copy.ts +96 -0
- package/src/view/attachments.ts +51 -0
- package/src/view/auth-copy.ts +147 -0
- package/src/view/board-index.ts +167 -0
- package/src/view/board-latest.ts +101 -0
- package/src/view/breadcrumb.ts +50 -0
- package/src/view/contacts.ts +85 -0
- package/src/view/content-copy.ts +118 -0
- package/src/view/contrast.ts +286 -0
- package/src/view/copy.ts +101 -0
- package/src/view/count.ts +8 -0
- package/src/view/discovery-view.ts +74 -0
- package/src/view/editor-toolbar.ts +84 -0
- package/src/view/error-notice.ts +52 -0
- package/src/view/feed.ts +130 -0
- package/src/view/forum-cursor.ts +54 -0
- package/src/view/forum-display.ts +152 -0
- package/src/view/forum-jump.ts +60 -0
- package/src/view/forum-notice.ts +24 -0
- package/src/view/inline-moderation.ts +87 -0
- package/src/view/install-copy.ts +95 -0
- package/src/view/legal.ts +58 -0
- package/src/view/locale.ts +40 -0
- package/src/view/marketplace-panel.ts +51 -0
- package/src/view/member-identity.ts +23 -0
- package/src/view/member-profile.ts +65 -0
- package/src/view/messages.ts +222 -0
- package/src/view/metadata.ts +75 -0
- package/src/view/modcp-nav.ts +76 -0
- package/src/view/moderation-copy.ts +84 -0
- package/src/view/moderation-queue.ts +67 -0
- package/src/view/navigation-arrange.ts +188 -0
- package/src/view/navigation.ts +173 -0
- package/src/view/notification-menu.ts +202 -0
- package/src/view/notifications.ts +84 -0
- package/src/view/onboarding.ts +26 -0
- package/src/view/pager.ts +191 -0
- package/src/view/panel-nav.ts +203 -0
- package/src/view/plugin-panel.ts +42 -0
- package/src/view/post-form.ts +61 -0
- package/src/view/post-link.ts +29 -0
- package/src/view/presence.ts +99 -0
- package/src/view/promotion-rules.ts +52 -0
- package/src/view/redirect-notice.ts +21 -0
- package/src/view/reputation.ts +107 -0
- package/src/view/search-controls.ts +103 -0
- package/src/view/search-results.ts +291 -0
- package/src/view/security-activity.ts +73 -0
- package/src/view/setting-groups.ts +43 -0
- package/src/view/shell.ts +164 -0
- package/src/view/slug-id.ts +12 -0
- package/src/view/sso-hand-off.ts +44 -0
- package/src/view/sso-notices.ts +48 -0
- package/src/view/subscriptions.ts +76 -0
- package/src/view/theme-draft.ts +167 -0
- package/src/view/theme-preference.ts +30 -0
- package/src/view/theme-tokens.ts +333 -0
- package/src/view/thread-view.ts +280 -0
- package/src/view/time.ts +31 -0
- package/src/view/timezone.ts +35 -0
- package/src/view/two-factor.ts +6 -0
- package/src/view/usercp-nav.ts +103 -0
- package/src/view/usercp.ts +196 -0
- package/src/view/warnings.ts +128 -0
- package/src/view/word-filter.ts +5 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { redirect } from 'next/navigation'
|
|
4
|
+
|
|
5
|
+
import { ForbiddenError } from '@meith/core'
|
|
6
|
+
import { msg } from '@meith/i18n'
|
|
7
|
+
import type { NotificationService } from '@meith/notifications'
|
|
8
|
+
|
|
9
|
+
import type { FormState } from './auth-form-state'
|
|
10
|
+
import { getActor } from './context'
|
|
11
|
+
import { formStateReporter } from './form-state-reporter'
|
|
12
|
+
import { positiveInt } from './form-values'
|
|
13
|
+
import { tr } from './i18n'
|
|
14
|
+
import { audiencesForActor } from './notification-audience'
|
|
15
|
+
import { notificationService } from './notifications'
|
|
16
|
+
import { pushAvailability } from './push'
|
|
17
|
+
|
|
18
|
+
const toFormState = formStateReporter('notification-actions', 'unexpected error in notifications')
|
|
19
|
+
|
|
20
|
+
async function requireOwnCentre(): Promise<{
|
|
21
|
+
service: NotificationService
|
|
22
|
+
userId: number
|
|
23
|
+
}> {
|
|
24
|
+
const actor = await getActor()
|
|
25
|
+
if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
|
|
26
|
+
|
|
27
|
+
const service = notificationService()
|
|
28
|
+
if (service === null) {
|
|
29
|
+
throw new ForbiddenError(msg('error.app.board-running-in-memory-sample-data-16'))
|
|
30
|
+
}
|
|
31
|
+
return { service, userId: actor.userId }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function markNotificationReadAction(
|
|
35
|
+
_prev: FormState,
|
|
36
|
+
form: FormData,
|
|
37
|
+
): Promise<FormState> {
|
|
38
|
+
const notificationId = positiveInt(form, 'notificationId')
|
|
39
|
+
if (notificationId === null) return { error: await tr('notice.app.notification-exist') }
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const { service, userId } = await requireOwnCentre()
|
|
43
|
+
await service.markRead(userId, notificationId)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return toFormState(err)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
redirect('/notifications?read=one')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function markAllNotificationsReadAction(
|
|
52
|
+
_prev: FormState,
|
|
53
|
+
_form: FormData,
|
|
54
|
+
): Promise<FormState> {
|
|
55
|
+
try {
|
|
56
|
+
const { service, userId } = await requireOwnCentre()
|
|
57
|
+
await service.markAllRead(userId)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return toFormState(err)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
redirect('/notifications?read=all')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function saveNotificationPreferencesAction(
|
|
66
|
+
_prev: FormState,
|
|
67
|
+
form: FormData,
|
|
68
|
+
): Promise<FormState> {
|
|
69
|
+
try {
|
|
70
|
+
const { service, userId } = await requireOwnCentre()
|
|
71
|
+
const email = form.getAll('email').filter((value): value is string => typeof value === 'string')
|
|
72
|
+
const push = form.getAll('push').filter((value): value is string => typeof value === 'string')
|
|
73
|
+
const offersPush = (await pushAvailability()).enabled
|
|
74
|
+
|
|
75
|
+
for (const audience of await audiencesForActor()) {
|
|
76
|
+
await service.savePreferences(userId, audience, email, 'email')
|
|
77
|
+
if (offersPush) await service.savePreferences(userId, audience, push, 'push')
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return toFormState(err)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
redirect('/notifications/preferences?saved=1')
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { getActor } from './context'
|
|
4
|
+
|
|
5
|
+
export type PreferenceAudience = 'member' | 'staff'
|
|
6
|
+
|
|
7
|
+
export async function audiencesForActor(): Promise<readonly PreferenceAudience[]> {
|
|
8
|
+
const actor = await getActor()
|
|
9
|
+
return actor.global.canAccessAdminCp === true ? ['member', 'staff'] : ['member']
|
|
10
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { revalidatePath } from 'next/cache'
|
|
4
|
+
|
|
5
|
+
import { ForbiddenError } from '@meith/core'
|
|
6
|
+
import { msg } from '@meith/i18n'
|
|
7
|
+
|
|
8
|
+
import type { FormState } from './auth-form-state'
|
|
9
|
+
import { EMPTY_STATE } from './auth-form-state'
|
|
10
|
+
import { getActor } from './context'
|
|
11
|
+
import { formStateReporter } from './form-state-reporter'
|
|
12
|
+
import { positiveInt } from './form-values'
|
|
13
|
+
import { requireMessaging } from './messages'
|
|
14
|
+
import { notificationService } from './notifications'
|
|
15
|
+
|
|
16
|
+
const toFormState = formStateReporter(
|
|
17
|
+
'notification-menu-actions',
|
|
18
|
+
'unexpected error in the notifications menu',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
function refreshShell(): void {
|
|
22
|
+
revalidatePath('/', 'layout')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function markNotificationSeenAction(
|
|
26
|
+
_prev: FormState,
|
|
27
|
+
form: FormData,
|
|
28
|
+
): Promise<FormState> {
|
|
29
|
+
const notificationId = positiveInt(form, 'notificationId')
|
|
30
|
+
if (notificationId === null) return EMPTY_STATE
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const actor = await getActor()
|
|
34
|
+
if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
|
|
35
|
+
|
|
36
|
+
const service = notificationService()
|
|
37
|
+
if (service !== null) await service.markRead(actor.userId, notificationId)
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return toFormState(err)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
refreshShell()
|
|
43
|
+
return EMPTY_STATE
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function markAllNotificationsSeenAction(
|
|
47
|
+
_prev: FormState,
|
|
48
|
+
_form: FormData,
|
|
49
|
+
): Promise<FormState> {
|
|
50
|
+
try {
|
|
51
|
+
const actor = await getActor()
|
|
52
|
+
if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
|
|
53
|
+
|
|
54
|
+
const service = notificationService()
|
|
55
|
+
if (service !== null) await service.markAllRead(actor.userId)
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return toFormState(err)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
refreshShell()
|
|
61
|
+
return EMPTY_STATE
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function markMessagesSeenAction(_prev: FormState, form: FormData): Promise<FormState> {
|
|
65
|
+
const copyIds = form
|
|
66
|
+
.getAll('copyId')
|
|
67
|
+
.filter((value): value is string => typeof value === 'string')
|
|
68
|
+
.map((value) => Number(value))
|
|
69
|
+
.filter((value) => Number.isInteger(value) && value > 0)
|
|
70
|
+
|
|
71
|
+
if (copyIds.length === 0) return EMPTY_STATE
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const { service, userId } = await requireMessaging(await getActor())
|
|
75
|
+
await service.markRead(userId, copyIds)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return toFormState(err)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
refreshShell()
|
|
81
|
+
return EMPTY_STATE
|
|
82
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import type { Actor } from '@meith/authorization'
|
|
4
|
+
import type { MessageListRow } from '@meith/messages'
|
|
5
|
+
import { ModerationQueue, type QueueItem, type ReportRow, ReportService } from '@meith/moderation'
|
|
6
|
+
import type { NotificationView } from '@meith/notifications'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
buildNotificationMenuView,
|
|
10
|
+
type ModerationSummary,
|
|
11
|
+
type NotificationMenuModel,
|
|
12
|
+
} from '../view/notification-menu'
|
|
13
|
+
import { getContainer } from './container'
|
|
14
|
+
import { getTranslator } from './i18n'
|
|
15
|
+
import { messageService, unreadMessageCount } from './messages'
|
|
16
|
+
import { modCpCounts, resolveModCpAccess } from './modcp'
|
|
17
|
+
import { notificationService, unreadNotificationCount } from './notifications'
|
|
18
|
+
import { hasReportScope, resolveReportScope } from './report-scope'
|
|
19
|
+
|
|
20
|
+
async function moderationSummary(): Promise<ModerationSummary | null> {
|
|
21
|
+
const access = await resolveModCpAccess()
|
|
22
|
+
if (access === null) return null
|
|
23
|
+
|
|
24
|
+
const { moderationQueue, reports } = getContainer()
|
|
25
|
+
|
|
26
|
+
const counts = await modCpCounts()
|
|
27
|
+
|
|
28
|
+
const queue: readonly QueueItem[] =
|
|
29
|
+
moderationQueue === null
|
|
30
|
+
? []
|
|
31
|
+
: await new ModerationQueue({ queue: moderationQueue })
|
|
32
|
+
.list(access.forumIds, { offset: 0 })
|
|
33
|
+
.then((page) => page.items)
|
|
34
|
+
.catch(() => [])
|
|
35
|
+
|
|
36
|
+
const scope = await resolveReportScope()
|
|
37
|
+
const reportRows: readonly ReportRow[] =
|
|
38
|
+
reports === null || !hasReportScope(scope)
|
|
39
|
+
? []
|
|
40
|
+
: await new ReportService({ reports })
|
|
41
|
+
.listOpen(scope, { offset: 0 })
|
|
42
|
+
.then((page) => page.rows)
|
|
43
|
+
.catch(() => [])
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
queue,
|
|
47
|
+
reports: reportRows,
|
|
48
|
+
pending: counts.pending,
|
|
49
|
+
openReports: counts.openReports,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function buildNotificationMenuModel(
|
|
54
|
+
actor: Actor,
|
|
55
|
+
): Promise<NotificationMenuModel | null> {
|
|
56
|
+
if (actor.userId === null) return null
|
|
57
|
+
|
|
58
|
+
const { authorizer } = getContainer()
|
|
59
|
+
const translator = await getTranslator()
|
|
60
|
+
const now = new Date()
|
|
61
|
+
|
|
62
|
+
const notifications = notificationService()
|
|
63
|
+
const notificationRows: readonly NotificationView[] =
|
|
64
|
+
notifications === null
|
|
65
|
+
? []
|
|
66
|
+
: await notifications
|
|
67
|
+
.list(actor.userId, {}, translator)
|
|
68
|
+
.then((page) => page.rows)
|
|
69
|
+
.catch(() => [])
|
|
70
|
+
|
|
71
|
+
const canPm = authorizer.can(actor, 'pm.use')
|
|
72
|
+
const messages = messageService()
|
|
73
|
+
const messageRows: readonly MessageListRow[] =
|
|
74
|
+
!canPm || messages === null
|
|
75
|
+
? []
|
|
76
|
+
: await messages
|
|
77
|
+
.list({ userId: actor.userId, folder: 'inbox', offset: 0 })
|
|
78
|
+
.then((page) => page.rows)
|
|
79
|
+
.catch(() => [])
|
|
80
|
+
|
|
81
|
+
const [notificationsUnread, messagesUnread, moderation] = await Promise.all([
|
|
82
|
+
unreadNotificationCount(actor.userId),
|
|
83
|
+
canPm ? unreadMessageCount(actor.userId) : Promise.resolve(0),
|
|
84
|
+
moderationSummary(),
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
return buildNotificationMenuView({
|
|
88
|
+
notifications: notificationRows,
|
|
89
|
+
notificationsUnread,
|
|
90
|
+
messages: messageRows,
|
|
91
|
+
messagesUnread,
|
|
92
|
+
moderation,
|
|
93
|
+
now,
|
|
94
|
+
t: translator,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import forumConfig from '@board/config'
|
|
4
|
+
import { cache } from 'react'
|
|
5
|
+
|
|
6
|
+
import { currentRequestId } from '@meith/core/logger'
|
|
7
|
+
import type { ReportNotifierPort, WarningNotifierPort } from '@meith/moderation'
|
|
8
|
+
import { type NotificationAudit, NotificationService } from '@meith/notifications'
|
|
9
|
+
import {
|
|
10
|
+
type PluginDefinition,
|
|
11
|
+
type PluginNotificationKindSpec,
|
|
12
|
+
pluginNotificationKindSpecs,
|
|
13
|
+
} from '@meith/plugin-kit'
|
|
14
|
+
|
|
15
|
+
import { getContainer } from './container'
|
|
16
|
+
import { emitEvent, filterView } from './plugin-view'
|
|
17
|
+
|
|
18
|
+
const PLUGIN_KINDS: readonly PluginNotificationKindSpec[] = (forumConfig.plugins ?? []).flatMap(
|
|
19
|
+
(entry) => {
|
|
20
|
+
if (entry.enabled === false) return []
|
|
21
|
+
const definition = entry.plugin as PluginDefinition | undefined
|
|
22
|
+
if (definition === undefined) return []
|
|
23
|
+
return pluginNotificationKindSpecs(definition.key, definition.notifications ?? [])
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const PLUGIN_AUDIT: NotificationAudit = {
|
|
28
|
+
before: (input) =>
|
|
29
|
+
filterView('notification.create.before', input, { requestId: currentRequestId() ?? null }),
|
|
30
|
+
|
|
31
|
+
created: (input) =>
|
|
32
|
+
emitEvent('notification.created', input, { requestId: currentRequestId() ?? null }),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function notificationService(): NotificationService | null {
|
|
36
|
+
const { notifications } = getContainer()
|
|
37
|
+
return notifications === null
|
|
38
|
+
? null
|
|
39
|
+
: new NotificationService({ notifications, extraKinds: PLUGIN_KINDS, audit: PLUGIN_AUDIT })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function warningNotifier(): WarningNotifierPort | null {
|
|
43
|
+
const service = notificationService()
|
|
44
|
+
if (service === null) return null
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
async warned(input) {
|
|
48
|
+
await service.raise({
|
|
49
|
+
userId: input.userId,
|
|
50
|
+
kind: 'warning.received',
|
|
51
|
+
data: {
|
|
52
|
+
title: input.title,
|
|
53
|
+
points: input.points,
|
|
54
|
+
totalPoints: input.totalPoints,
|
|
55
|
+
reason: input.reason,
|
|
56
|
+
restriction: input.restriction,
|
|
57
|
+
},
|
|
58
|
+
href: null,
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function reportNotifier(): ReportNotifierPort | null {
|
|
65
|
+
const service = notificationService()
|
|
66
|
+
if (service === null) return null
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
async reportClosed(input) {
|
|
70
|
+
await service.raise({
|
|
71
|
+
userId: input.reporterUserId,
|
|
72
|
+
kind: 'report.actioned',
|
|
73
|
+
data: { outcome: input.outcome, targetLabel: input.targetLabel },
|
|
74
|
+
dedupeKey: `report.actioned:${input.reportId}`,
|
|
75
|
+
href: null,
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const unreadNotificationCount = cache(async function unreadNotificationCount(
|
|
82
|
+
userId: number | null,
|
|
83
|
+
): Promise<number> {
|
|
84
|
+
if (userId === null) return 0
|
|
85
|
+
const service = notificationService()
|
|
86
|
+
if (service === null) return 0
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return await service.unreadCount(userId)
|
|
90
|
+
} catch {
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { cookies } from 'next/headers'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isOnboardingPanel,
|
|
7
|
+
ONBOARDING_COOKIE_MAX_AGE,
|
|
8
|
+
onboardingCookieName,
|
|
9
|
+
} from '@/view/onboarding'
|
|
10
|
+
|
|
11
|
+
import { redirectToCurrentPath } from './redirect-back'
|
|
12
|
+
|
|
13
|
+
const COOKIE_OPTIONS = {
|
|
14
|
+
path: '/',
|
|
15
|
+
maxAge: ONBOARDING_COOKIE_MAX_AGE,
|
|
16
|
+
sameSite: 'lax',
|
|
17
|
+
httpOnly: false,
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
export async function dismissOnboardingAction(form: FormData): Promise<void> {
|
|
21
|
+
const panel = form.get('panel')
|
|
22
|
+
if (typeof panel === 'string' && isOnboardingPanel(panel)) {
|
|
23
|
+
;(await cookies()).set(onboardingCookieName(panel), '1', COOKIE_OPTIONS)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await redirectToCurrentPath()
|
|
27
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { cookies } from 'next/headers'
|
|
4
|
+
|
|
5
|
+
import type { Actor } from '@meith/authorization'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
hasCompleteProfile,
|
|
9
|
+
isNewMember,
|
|
10
|
+
type OnboardingPanel,
|
|
11
|
+
onboardingCookieName,
|
|
12
|
+
} from '@/view/onboarding'
|
|
13
|
+
|
|
14
|
+
import { avatarsFor } from './avatars'
|
|
15
|
+
import { getContainer } from './container'
|
|
16
|
+
import { getTranslator } from './i18n'
|
|
17
|
+
import { legalDocument } from './legal'
|
|
18
|
+
|
|
19
|
+
export interface OnboardingNoticeModel {
|
|
20
|
+
readonly panel: OnboardingPanel
|
|
21
|
+
readonly message: string
|
|
22
|
+
readonly ctaHref: string | null
|
|
23
|
+
readonly ctaLabel: string | null
|
|
24
|
+
readonly dismissLabel: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function dismissed(panel: OnboardingPanel): Promise<boolean> {
|
|
28
|
+
return (await cookies()).get(onboardingCookieName(panel))?.value === '1'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* At most one of the welcome banner or the profile-completion nudge, never
|
|
33
|
+
* both — stacking unrelated notices in the same board-wide slot is the
|
|
34
|
+
* "heavy multi-step tour" the feature is meant to avoid.
|
|
35
|
+
*/
|
|
36
|
+
export async function onboardingBanner(actor: Actor): Promise<OnboardingNoticeModel | null> {
|
|
37
|
+
if (actor.userId === null) return null
|
|
38
|
+
|
|
39
|
+
const profile = await getContainer()
|
|
40
|
+
.memberProfiles.findPublicById(actor.userId)
|
|
41
|
+
.catch(() => null)
|
|
42
|
+
if (profile === null || !isNewMember(profile.postCount)) return null
|
|
43
|
+
|
|
44
|
+
const t = await getTranslator()
|
|
45
|
+
const dismissLabel = t.t('onboarding.dismiss')
|
|
46
|
+
|
|
47
|
+
if (!(await dismissed('welcome'))) {
|
|
48
|
+
return {
|
|
49
|
+
panel: 'welcome',
|
|
50
|
+
message: t.t('onboarding.welcome.message'),
|
|
51
|
+
ctaHref: null,
|
|
52
|
+
ctaLabel: null,
|
|
53
|
+
dismissLabel,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const avatarUrl = (await avatarsFor([actor.userId])).get(actor.userId) ?? null
|
|
58
|
+
if (!hasCompleteProfile({ avatarUrl, bio: profile.bio }) && !(await dismissed('profile'))) {
|
|
59
|
+
return {
|
|
60
|
+
panel: 'profile',
|
|
61
|
+
message: t.t('onboarding.profile.message'),
|
|
62
|
+
ctaHref: '/usercp/profile',
|
|
63
|
+
ctaLabel: t.t('onboarding.profile.cta'),
|
|
64
|
+
dismissLabel,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function firstPostGuidance(actor: Actor): Promise<OnboardingNoticeModel | null> {
|
|
72
|
+
if (actor.userId === null || (await dismissed('first-post'))) return null
|
|
73
|
+
|
|
74
|
+
const profile = await getContainer()
|
|
75
|
+
.memberProfiles.findPublicById(actor.userId)
|
|
76
|
+
.catch(() => null)
|
|
77
|
+
if (profile === null || !isNewMember(profile.postCount)) return null
|
|
78
|
+
|
|
79
|
+
const rules = await legalDocument('rules')
|
|
80
|
+
const t = await getTranslator()
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
panel: 'first-post',
|
|
84
|
+
message: t.t('onboarding.firstPost.message'),
|
|
85
|
+
ctaHref: rules?.href ?? null,
|
|
86
|
+
ctaLabel: rules === null ? null : t.t('onboarding.firstPost.cta'),
|
|
87
|
+
dismissLabel: t.t('onboarding.dismiss'),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { filterView } from './plugin-view'
|
|
4
|
+
|
|
5
|
+
export interface PageMetadata {
|
|
6
|
+
readonly title: string
|
|
7
|
+
readonly description: string | null
|
|
8
|
+
readonly canonical: string
|
|
9
|
+
readonly imageUrl: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function pageMetadata(route: string, page: PageMetadata): Promise<PageMetadata> {
|
|
13
|
+
return filterView('metadata.page', page, { route })
|
|
14
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
export type PasskeyPurpose = 'register' | 'authenticate' | 'second-factor' | 'credential-proof'
|
|
4
|
+
|
|
5
|
+
export interface PasskeyChallenge {
|
|
6
|
+
readonly challenge: string
|
|
7
|
+
readonly userId?: number
|
|
8
|
+
readonly sessionId?: number
|
|
9
|
+
readonly provedAt?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function packChallenge(
|
|
13
|
+
purpose: PasskeyPurpose,
|
|
14
|
+
challenge: string,
|
|
15
|
+
binding: Omit<PasskeyChallenge, 'challenge'> = {},
|
|
16
|
+
): string {
|
|
17
|
+
return `${purpose}:${encodeURIComponent(JSON.stringify({ challenge, ...binding }))}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function unpackChallenge(
|
|
21
|
+
raw: string | undefined,
|
|
22
|
+
purpose: PasskeyPurpose,
|
|
23
|
+
): PasskeyChallenge | null {
|
|
24
|
+
if (raw === undefined) return null
|
|
25
|
+
|
|
26
|
+
const separator = raw.indexOf(':')
|
|
27
|
+
if (separator < 0 || raw.slice(0, separator) !== purpose) return null
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(decodeURIComponent(raw.slice(separator + 1))) as Record<
|
|
31
|
+
string,
|
|
32
|
+
unknown
|
|
33
|
+
>
|
|
34
|
+
if (typeof value.challenge !== 'string' || value.challenge === '') return null
|
|
35
|
+
if (value.userId !== undefined && !Number.isInteger(value.userId)) return null
|
|
36
|
+
if (value.sessionId !== undefined && !Number.isInteger(value.sessionId)) return null
|
|
37
|
+
if (value.provedAt !== undefined && !Number.isInteger(value.provedAt)) return null
|
|
38
|
+
return value as unknown as PasskeyChallenge
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|