@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,188 @@
|
|
|
1
|
+
export const NAVIGATION_NUDGES = ['up', 'down', 'in', 'out'] as const
|
|
2
|
+
|
|
3
|
+
export type NavigationNudge = (typeof NAVIGATION_NUDGES)[number]
|
|
4
|
+
|
|
5
|
+
export const NAVIGATION_MAX_DEPTH = 1
|
|
6
|
+
|
|
7
|
+
export interface NavigationOutlineRow {
|
|
8
|
+
readonly id: number
|
|
9
|
+
readonly parentId: number | null
|
|
10
|
+
readonly depth: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface NavigationDropTarget {
|
|
14
|
+
readonly parentId: number | null
|
|
15
|
+
readonly afterId: number | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NavigationDropProjection extends NavigationDropTarget {
|
|
19
|
+
readonly index: number
|
|
20
|
+
readonly depth: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isNavigationNudge(value: string): value is NavigationNudge {
|
|
24
|
+
return (NAVIGATION_NUDGES as readonly string[]).includes(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function childrenOf<T extends NavigationOutlineRow>(
|
|
28
|
+
outline: readonly T[],
|
|
29
|
+
parentId: number | null,
|
|
30
|
+
): T[] {
|
|
31
|
+
return outline.filter((row) => row.parentId === parentId)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function hasChildren(outline: readonly NavigationOutlineRow[], id: number): boolean {
|
|
35
|
+
return outline.some((row) => row.parentId === id)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function subtreeOf<T extends NavigationOutlineRow>(outline: readonly T[], id: number): T[] {
|
|
39
|
+
const start = outline.findIndex((row) => row.id === id)
|
|
40
|
+
if (start === -1) return []
|
|
41
|
+
|
|
42
|
+
const root = outline[start] as T
|
|
43
|
+
let end = start + 1
|
|
44
|
+
while (end < outline.length && (outline[end] as T).depth > root.depth) end += 1
|
|
45
|
+
|
|
46
|
+
return outline.slice(start, end)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function withoutSubtree<T extends NavigationOutlineRow>(
|
|
50
|
+
outline: readonly T[],
|
|
51
|
+
id: number,
|
|
52
|
+
): T[] {
|
|
53
|
+
const moving = new Set(subtreeOf(outline, id).map((row) => row.id))
|
|
54
|
+
return outline.filter((row) => !moving.has(row.id))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function depthAllowedFor(outline: readonly NavigationOutlineRow[], id: number): number {
|
|
58
|
+
return hasChildren(outline, id) ? 0 : NAVIGATION_MAX_DEPTH
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function nudgeTarget(
|
|
62
|
+
outline: readonly NavigationOutlineRow[],
|
|
63
|
+
id: number,
|
|
64
|
+
nudge: NavigationNudge,
|
|
65
|
+
): NavigationDropTarget | null {
|
|
66
|
+
const row = outline.find((entry) => entry.id === id)
|
|
67
|
+
if (row === undefined) return null
|
|
68
|
+
|
|
69
|
+
const siblings = childrenOf(outline, row.parentId)
|
|
70
|
+
const at = siblings.findIndex((entry) => entry.id === id)
|
|
71
|
+
|
|
72
|
+
if (nudge === 'up') {
|
|
73
|
+
if (at <= 0) return null
|
|
74
|
+
return { parentId: row.parentId, afterId: siblings[at - 2]?.id ?? null }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (nudge === 'down') {
|
|
78
|
+
const next = siblings[at + 1]
|
|
79
|
+
if (next === undefined) return null
|
|
80
|
+
return { parentId: row.parentId, afterId: next.id }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (nudge === 'in') {
|
|
84
|
+
const previous = siblings[at - 1]
|
|
85
|
+
if (previous === undefined || previous.depth >= NAVIGATION_MAX_DEPTH) return null
|
|
86
|
+
if (hasChildren(outline, id)) return null
|
|
87
|
+
return { parentId: previous.id, afterId: childrenOf(outline, previous.id).at(-1)?.id ?? null }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (row.parentId === null) return null
|
|
91
|
+
return { parentId: null, afterId: row.parentId }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function availableNudges(
|
|
95
|
+
outline: readonly NavigationOutlineRow[],
|
|
96
|
+
id: number,
|
|
97
|
+
): Record<NavigationNudge, boolean> {
|
|
98
|
+
return {
|
|
99
|
+
up: nudgeTarget(outline, id, 'up') !== null,
|
|
100
|
+
down: nudgeTarget(outline, id, 'down') !== null,
|
|
101
|
+
in: nudgeTarget(outline, id, 'in') !== null,
|
|
102
|
+
out: nudgeTarget(outline, id, 'out') !== null,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function projectDrop(
|
|
107
|
+
rows: readonly NavigationOutlineRow[],
|
|
108
|
+
index: number,
|
|
109
|
+
depth: number,
|
|
110
|
+
maxDepth: number = NAVIGATION_MAX_DEPTH,
|
|
111
|
+
): NavigationDropProjection {
|
|
112
|
+
const at = Math.min(Math.max(index, 0), rows.length)
|
|
113
|
+
const previous = rows[at - 1]
|
|
114
|
+
const next = rows[at]
|
|
115
|
+
|
|
116
|
+
if (previous === undefined) return { index: at, depth: 0, parentId: null, afterId: null }
|
|
117
|
+
|
|
118
|
+
const deepest = Math.min(NAVIGATION_MAX_DEPTH, maxDepth)
|
|
119
|
+
const shallowest = Math.min(next?.depth ?? 0, deepest)
|
|
120
|
+
const settled = Math.min(Math.max(depth, shallowest), deepest)
|
|
121
|
+
|
|
122
|
+
if (settled > 0) {
|
|
123
|
+
return previous.depth === 0
|
|
124
|
+
? { index: at, depth: settled, parentId: previous.id, afterId: null }
|
|
125
|
+
: { index: at, depth: settled, parentId: previous.parentId, afterId: previous.id }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (let cursor = at - 1; cursor >= 0; cursor -= 1) {
|
|
129
|
+
const candidate = rows[cursor] as NavigationOutlineRow
|
|
130
|
+
if (candidate.depth === 0) return { index: at, depth: 0, parentId: null, afterId: candidate.id }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { index: at, depth: 0, parentId: null, afterId: null }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function projectionOf(
|
|
137
|
+
outline: readonly NavigationOutlineRow[],
|
|
138
|
+
id: number,
|
|
139
|
+
target: NavigationDropTarget,
|
|
140
|
+
): NavigationDropProjection {
|
|
141
|
+
const rows = withoutSubtree(outline, id)
|
|
142
|
+
const parent = rows.find((row) => row.id === target.parentId)
|
|
143
|
+
const depth = target.parentId === null ? 0 : (parent?.depth ?? 0) + 1
|
|
144
|
+
|
|
145
|
+
if (target.afterId === null) {
|
|
146
|
+
const index = parent === undefined ? 0 : rows.indexOf(parent) + 1
|
|
147
|
+
return { ...target, index, depth }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const after = rows.find((row) => row.id === target.afterId)
|
|
151
|
+
if (after === undefined) return { ...target, index: rows.length, depth }
|
|
152
|
+
|
|
153
|
+
return { ...target, index: rows.indexOf(after) + subtreeOf(rows, after.id).length, depth }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function applyDrop<T extends NavigationOutlineRow>(
|
|
157
|
+
outline: readonly T[],
|
|
158
|
+
id: number,
|
|
159
|
+
projection: NavigationDropProjection,
|
|
160
|
+
): T[] {
|
|
161
|
+
const block = subtreeOf(outline, id)
|
|
162
|
+
if (block.length === 0) return [...outline]
|
|
163
|
+
|
|
164
|
+
const rest = withoutSubtree(outline, id)
|
|
165
|
+
const shift = projection.depth - (block[0] as T).depth
|
|
166
|
+
|
|
167
|
+
const moved = block.map((row, offset) => ({
|
|
168
|
+
...row,
|
|
169
|
+
depth: row.depth + shift,
|
|
170
|
+
...(offset === 0 ? { parentId: projection.parentId } : {}),
|
|
171
|
+
}))
|
|
172
|
+
|
|
173
|
+
return [...rest.slice(0, projection.index), ...moved, ...rest.slice(projection.index)]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function isWhereItIs(
|
|
177
|
+
outline: readonly NavigationOutlineRow[],
|
|
178
|
+
id: number,
|
|
179
|
+
target: NavigationDropTarget,
|
|
180
|
+
): boolean {
|
|
181
|
+
const row = outline.find((entry) => entry.id === id)
|
|
182
|
+
if (row === undefined || row.parentId !== target.parentId) return false
|
|
183
|
+
|
|
184
|
+
const siblings = childrenOf(outline, row.parentId)
|
|
185
|
+
const at = siblings.findIndex((entry) => entry.id === id)
|
|
186
|
+
|
|
187
|
+
return (siblings[at - 1]?.id ?? null) === target.afterId
|
|
188
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { NavigationAudience, NavigationItemRow } from '@meith/db'
|
|
2
|
+
import type { Translator } from '@meith/i18n'
|
|
3
|
+
import type { LinkModel, ViewerModel } from '@meith/theme-kit'
|
|
4
|
+
|
|
5
|
+
import { untranslated } from './time'
|
|
6
|
+
|
|
7
|
+
export type { NavigationAudience }
|
|
8
|
+
|
|
9
|
+
export const NAVIGATION_AUDIENCE_MESSAGE_KEYS: Readonly<Record<NavigationAudience, string>> = {
|
|
10
|
+
all: 'adminNavigation.audience.all',
|
|
11
|
+
guests: 'adminNavigation.audience.guests',
|
|
12
|
+
members: 'adminNavigation.audience.members',
|
|
13
|
+
staff: 'adminNavigation.audience.staff',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const NAVIGATION_AUDIENCE_VALUES = Object.keys(
|
|
17
|
+
NAVIGATION_AUDIENCE_MESSAGE_KEYS,
|
|
18
|
+
) as readonly NavigationAudience[]
|
|
19
|
+
|
|
20
|
+
export interface BuiltInNavigationItem {
|
|
21
|
+
readonly key: string
|
|
22
|
+
readonly messageKey: string
|
|
23
|
+
readonly href: string
|
|
24
|
+
readonly audience: NavigationAudience
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const BUILT_IN_NAVIGATION: readonly BuiltInNavigationItem[] = [
|
|
28
|
+
{ key: 'home', messageKey: 'nav.home', href: '/', audience: 'all' },
|
|
29
|
+
{ key: 'new-posts', messageKey: 'nav.newPosts', href: '/discover/new', audience: 'all' },
|
|
30
|
+
{
|
|
31
|
+
key: 'unanswered',
|
|
32
|
+
messageKey: 'nav.unanswered',
|
|
33
|
+
href: '/discover/unanswered',
|
|
34
|
+
audience: 'all',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'my-posts',
|
|
38
|
+
messageKey: 'nav.myPosts',
|
|
39
|
+
href: '/discover/participated',
|
|
40
|
+
audience: 'members',
|
|
41
|
+
},
|
|
42
|
+
{ key: 'search', messageKey: 'nav.search', href: '/search', audience: 'all' },
|
|
43
|
+
{ key: 'online', messageKey: 'nav.online', href: '/online', audience: 'all' },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const SEARCH_KEY = 'search'
|
|
47
|
+
|
|
48
|
+
export type NavigationOutlineItem = NavigationItemRow & { readonly depth: number }
|
|
49
|
+
|
|
50
|
+
export function builtInNavigation(key: string | null): BuiltInNavigationItem | null {
|
|
51
|
+
if (key === null) return null
|
|
52
|
+
return BUILT_IN_NAVIGATION.find((item) => item.key === key) ?? null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function defaultNavigationItems(): readonly NavigationItemRow[] {
|
|
56
|
+
return BUILT_IN_NAVIGATION.map((item, index) => ({
|
|
57
|
+
id: index + 1,
|
|
58
|
+
key: item.key,
|
|
59
|
+
parentId: null,
|
|
60
|
+
label: '',
|
|
61
|
+
href: item.href,
|
|
62
|
+
displayOrder: index * 10,
|
|
63
|
+
audience: item.audience,
|
|
64
|
+
newTab: false,
|
|
65
|
+
enabled: true,
|
|
66
|
+
visibleToGroups: [],
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What a plugin called an item, for the rows the board holds on its behalf.
|
|
72
|
+
* Keyed by the same `plugin.<plugin>.<item>` the row carries.
|
|
73
|
+
*/
|
|
74
|
+
export interface NavigationName {
|
|
75
|
+
readonly label: string
|
|
76
|
+
readonly labelKey: string | null
|
|
77
|
+
readonly labelArgs?: Parameters<Translator['t']>[1] | null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface NavigationNames {
|
|
81
|
+
readonly [key: string]: NavigationName
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function navigationLabel(
|
|
85
|
+
item: Pick<NavigationItemRow, 'key' | 'label'>,
|
|
86
|
+
t: Translator = untranslated(),
|
|
87
|
+
names: NavigationNames = {},
|
|
88
|
+
): string {
|
|
89
|
+
if (item.label.trim() !== '') return item.label
|
|
90
|
+
|
|
91
|
+
const builtIn = builtInNavigation(item.key)
|
|
92
|
+
if (builtIn !== null) return t.t(builtIn.messageKey)
|
|
93
|
+
|
|
94
|
+
const named = item.key === null ? undefined : names[item.key]
|
|
95
|
+
if (named === undefined) return ''
|
|
96
|
+
if (named.labelKey === null || !t.has(named.labelKey)) return named.label
|
|
97
|
+
return named.labelArgs === null || named.labelArgs === undefined
|
|
98
|
+
? t.t(named.labelKey)
|
|
99
|
+
: t.t(named.labelKey, named.labelArgs)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inOrder(rows: readonly NavigationItemRow[]): readonly NavigationItemRow[] {
|
|
103
|
+
return [...rows].sort((a, b) => a.displayOrder - b.displayOrder || a.id - b.id)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function outlineOf(rows: readonly NavigationItemRow[]): NavigationOutlineItem[] {
|
|
107
|
+
const sorted = inOrder(rows)
|
|
108
|
+
const outline: NavigationOutlineItem[] = []
|
|
109
|
+
|
|
110
|
+
for (const top of sorted.filter((row) => row.parentId === null)) {
|
|
111
|
+
outline.push({ ...top, depth: 0 })
|
|
112
|
+
for (const child of sorted.filter((row) => row.parentId === top.id)) {
|
|
113
|
+
outline.push({ ...child, depth: 1 })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return outline
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isStaff(viewer: ViewerModel): boolean {
|
|
121
|
+
return viewer.canAccessAdminCp || viewer.canAccessModCp
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function audienceAdmits(audience: NavigationAudience, viewer: ViewerModel): boolean {
|
|
125
|
+
if (audience === 'guests') return viewer.isGuest
|
|
126
|
+
if (audience === 'members') return !viewer.isGuest
|
|
127
|
+
if (audience === 'staff') return isStaff(viewer)
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface NavigationOptions {
|
|
132
|
+
readonly searchEnabled?: boolean
|
|
133
|
+
readonly t?: Translator
|
|
134
|
+
readonly admits?: (item: NavigationItemRow) => boolean
|
|
135
|
+
readonly names?: NavigationNames
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildNavigation(
|
|
139
|
+
items: readonly NavigationItemRow[],
|
|
140
|
+
viewer: ViewerModel,
|
|
141
|
+
options: NavigationOptions = {},
|
|
142
|
+
): readonly LinkModel[] {
|
|
143
|
+
const t = options.t ?? untranslated()
|
|
144
|
+
const admits = options.admits ?? (() => true)
|
|
145
|
+
const names = options.names ?? {}
|
|
146
|
+
|
|
147
|
+
const shown = (item: NavigationItemRow): boolean =>
|
|
148
|
+
item.enabled &&
|
|
149
|
+
!(item.key === SEARCH_KEY && options.searchEnabled === false) &&
|
|
150
|
+
audienceAdmits(item.audience, viewer) &&
|
|
151
|
+
admits(item) &&
|
|
152
|
+
navigationLabel(item, t, names) !== ''
|
|
153
|
+
|
|
154
|
+
const linkOf = (item: NavigationItemRow, submenu: readonly LinkModel[]): LinkModel => ({
|
|
155
|
+
label: navigationLabel(item, t, names),
|
|
156
|
+
href: item.href,
|
|
157
|
+
...(item.newTab ? { newTab: true } : {}),
|
|
158
|
+
...(submenu.length === 0 ? {} : { submenu }),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const sorted = inOrder(items)
|
|
162
|
+
|
|
163
|
+
return sorted
|
|
164
|
+
.filter((item) => item.parentId === null && shown(item))
|
|
165
|
+
.map((item) =>
|
|
166
|
+
linkOf(
|
|
167
|
+
item,
|
|
168
|
+
sorted
|
|
169
|
+
.filter((child) => child.parentId === item.id && shown(child))
|
|
170
|
+
.map((child) => linkOf(child, [])),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
2
|
+
import type { MessageListRow } from '@meith/messages'
|
|
3
|
+
import type { QueueItem, ReportRow } from '@meith/moderation'
|
|
4
|
+
import type { NotificationView } from '@meith/notifications'
|
|
5
|
+
import type { TimeModel } from '@meith/theme-kit'
|
|
6
|
+
|
|
7
|
+
import { copyFor } from './copy'
|
|
8
|
+
import { formatTime, untranslated } from './time'
|
|
9
|
+
|
|
10
|
+
export const NOTIFICATION_MENU_PREVIEW = 10
|
|
11
|
+
|
|
12
|
+
export type NotificationMenuTabKind = 'notifications' | 'messages' | 'mod'
|
|
13
|
+
|
|
14
|
+
export interface NotificationMenuRow {
|
|
15
|
+
readonly key: string
|
|
16
|
+
/** The id a mark-seen form submits, or `null` for a row that is acted on elsewhere. */
|
|
17
|
+
readonly seenId: number | null
|
|
18
|
+
readonly subject: string
|
|
19
|
+
readonly meta: string | null
|
|
20
|
+
readonly href: string | null
|
|
21
|
+
readonly at: TimeModel
|
|
22
|
+
readonly isUnread: boolean
|
|
23
|
+
/** A short category label shown before the subject, or `null`. */
|
|
24
|
+
readonly tag: string | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ModerationSummary {
|
|
28
|
+
readonly queue: readonly QueueItem[]
|
|
29
|
+
readonly reports: readonly ReportRow[]
|
|
30
|
+
readonly pending: number
|
|
31
|
+
readonly openReports: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface NotificationMenuTab {
|
|
35
|
+
readonly kind: NotificationMenuTabKind
|
|
36
|
+
readonly label: string
|
|
37
|
+
readonly unread: number
|
|
38
|
+
readonly rows: readonly NotificationMenuRow[]
|
|
39
|
+
readonly allHref: string
|
|
40
|
+
readonly emptyLabel: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface NotificationMenuModel {
|
|
44
|
+
readonly total: number
|
|
45
|
+
readonly tabs: readonly NotificationMenuTab[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function notificationRow(row: NotificationView, now: Date, t: Translator): NotificationMenuRow {
|
|
49
|
+
return {
|
|
50
|
+
key: `notification-${row.id}`,
|
|
51
|
+
seenId: row.id,
|
|
52
|
+
subject: row.subject,
|
|
53
|
+
meta: row.body === '' ? null : firstLine(row.body),
|
|
54
|
+
href: row.href,
|
|
55
|
+
at: formatTime(row.updatedAt, now, t),
|
|
56
|
+
isUnread: !row.isRead,
|
|
57
|
+
tag: null,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function messageRow(row: MessageListRow, now: Date, t: Translator): NotificationMenuRow {
|
|
62
|
+
const from = row.counterparties[0] ?? null
|
|
63
|
+
const more = row.moreCounterparties
|
|
64
|
+
return {
|
|
65
|
+
key: `message-${row.copyId}`,
|
|
66
|
+
seenId: row.copyId,
|
|
67
|
+
subject: row.subject === '' ? t.t('board.notificationMenu.noSubject') : row.subject,
|
|
68
|
+
meta:
|
|
69
|
+
from === null
|
|
70
|
+
? null
|
|
71
|
+
: more > 0
|
|
72
|
+
? t.t('board.notificationMenu.fromMany', { name: from, count: more })
|
|
73
|
+
: t.t('board.notificationMenu.from', { name: from }),
|
|
74
|
+
href: `/messages/${row.messageId}`,
|
|
75
|
+
at: formatTime(row.sentAt, now, t),
|
|
76
|
+
isUnread: row.readAt === null && row.role !== 'author',
|
|
77
|
+
tag: null,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function queueRow(item: QueueItem, now: Date, t: Translator): NotificationMenuRow {
|
|
82
|
+
return {
|
|
83
|
+
key: `queue-${item.kind}-${item.id}`,
|
|
84
|
+
seenId: null,
|
|
85
|
+
subject: item.threadTitle,
|
|
86
|
+
meta: item.forumTitle,
|
|
87
|
+
href: '/moderation',
|
|
88
|
+
at: formatTime(item.createdAt, now, t),
|
|
89
|
+
isUnread: false,
|
|
90
|
+
tag: t.t('board.notificationMenu.mod.pending'),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function reportRow(report: ReportRow, now: Date, t: Translator): NotificationMenuRow {
|
|
95
|
+
return {
|
|
96
|
+
key: `report-${report.id}`,
|
|
97
|
+
seenId: null,
|
|
98
|
+
subject: report.targetLabel,
|
|
99
|
+
meta: firstLine(report.reason),
|
|
100
|
+
href: '/moderation/reports',
|
|
101
|
+
at: formatTime(report.createdAt, now, t),
|
|
102
|
+
isUnread: false,
|
|
103
|
+
tag: t.t('board.notificationMenu.mod.report'),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function firstLine(body: string): string {
|
|
108
|
+
const line = body.split('\n', 1)[0] ?? ''
|
|
109
|
+
return line.length > 140 ? `${line.slice(0, 139)}…` : line
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildNotificationMenuView(input: {
|
|
113
|
+
readonly notifications: readonly NotificationView[]
|
|
114
|
+
readonly notificationsUnread: number
|
|
115
|
+
readonly messages: readonly MessageListRow[]
|
|
116
|
+
readonly messagesUnread: number
|
|
117
|
+
readonly moderation: ModerationSummary | null
|
|
118
|
+
readonly now: Date
|
|
119
|
+
readonly t?: Translator
|
|
120
|
+
}): NotificationMenuModel {
|
|
121
|
+
const t = input.t ?? untranslated()
|
|
122
|
+
|
|
123
|
+
const notificationsTab: NotificationMenuTab = {
|
|
124
|
+
kind: 'notifications',
|
|
125
|
+
label: t.t('board.notificationMenu.tab.notifications'),
|
|
126
|
+
unread: input.notificationsUnread,
|
|
127
|
+
rows: input.notifications
|
|
128
|
+
.filter((row) => !row.isRead)
|
|
129
|
+
.slice(0, NOTIFICATION_MENU_PREVIEW)
|
|
130
|
+
.map((row) => notificationRow(row, input.now, t)),
|
|
131
|
+
allHref: '/notifications',
|
|
132
|
+
emptyLabel: t.t('board.notificationMenu.empty.notifications'),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const messagesTab: NotificationMenuTab = {
|
|
136
|
+
kind: 'messages',
|
|
137
|
+
label: t.t('board.notificationMenu.tab.messages'),
|
|
138
|
+
unread: input.messagesUnread,
|
|
139
|
+
rows: input.messages
|
|
140
|
+
.filter((row) => row.readAt === null && row.role !== 'author')
|
|
141
|
+
.slice(0, NOTIFICATION_MENU_PREVIEW)
|
|
142
|
+
.map((row) => messageRow(row, input.now, t)),
|
|
143
|
+
allHref: '/messages',
|
|
144
|
+
emptyLabel: t.t('board.notificationMenu.empty.messages'),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tabs: NotificationMenuTab[] = [notificationsTab, messagesTab]
|
|
148
|
+
|
|
149
|
+
const moderationUnread =
|
|
150
|
+
input.moderation === null ? 0 : input.moderation.pending + input.moderation.openReports
|
|
151
|
+
|
|
152
|
+
if (input.moderation !== null) {
|
|
153
|
+
const rows = [
|
|
154
|
+
...input.moderation.reports.map((report) => ({
|
|
155
|
+
at: report.createdAt,
|
|
156
|
+
row: reportRow(report, input.now, t),
|
|
157
|
+
})),
|
|
158
|
+
...input.moderation.queue.map((item) => ({
|
|
159
|
+
at: item.createdAt,
|
|
160
|
+
row: queueRow(item, input.now, t),
|
|
161
|
+
})),
|
|
162
|
+
]
|
|
163
|
+
.sort((a, b) => b.at.getTime() - a.at.getTime())
|
|
164
|
+
.slice(0, NOTIFICATION_MENU_PREVIEW)
|
|
165
|
+
.map((entry) => entry.row)
|
|
166
|
+
|
|
167
|
+
tabs.push({
|
|
168
|
+
kind: 'mod',
|
|
169
|
+
label: t.t('board.notificationMenu.tab.mod'),
|
|
170
|
+
unread: moderationUnread,
|
|
171
|
+
rows,
|
|
172
|
+
allHref: '/moderation',
|
|
173
|
+
emptyLabel: t.t('board.notificationMenu.empty.mod'),
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
total: input.notificationsUnread + input.messagesUnread + moderationUnread,
|
|
179
|
+
tabs,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function notificationMenuCopy(
|
|
184
|
+
t: Translator = untranslated(),
|
|
185
|
+
): Readonly<Record<string, string>> {
|
|
186
|
+
return copyFor(
|
|
187
|
+
[
|
|
188
|
+
'board.notificationMenu.trigger',
|
|
189
|
+
'board.notificationMenu.heading',
|
|
190
|
+
'board.notificationMenu.markSeen',
|
|
191
|
+
'board.notificationMenu.markAllSeen',
|
|
192
|
+
'board.notificationMenu.viewAll',
|
|
193
|
+
'board.notificationMenu.close',
|
|
194
|
+
'board.notificationMenu.unreadNotifications',
|
|
195
|
+
'board.notificationMenu.unreadMessages',
|
|
196
|
+
'board.notificationMenu.unreadMod',
|
|
197
|
+
'board.notificationMenu.totalUnread',
|
|
198
|
+
'form.working',
|
|
199
|
+
],
|
|
200
|
+
t,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
2
|
+
import type { NotificationPreferenceView, NotificationView } from '@meith/notifications'
|
|
3
|
+
import type { TimeModel } from '@meith/theme-kit'
|
|
4
|
+
|
|
5
|
+
import { formatTime, untranslated } from './time'
|
|
6
|
+
|
|
7
|
+
export interface NotificationRowView {
|
|
8
|
+
readonly id: number
|
|
9
|
+
readonly subject: string
|
|
10
|
+
readonly body: string
|
|
11
|
+
readonly href: string | null
|
|
12
|
+
readonly at: TimeModel
|
|
13
|
+
readonly isRead: boolean
|
|
14
|
+
readonly repeated: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NotificationCentreView {
|
|
18
|
+
readonly rows: readonly NotificationRowView[]
|
|
19
|
+
readonly unread: number
|
|
20
|
+
readonly nextHref: string | null
|
|
21
|
+
readonly preferencesHref: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildNotificationCentreView(input: {
|
|
25
|
+
readonly rows: readonly NotificationView[]
|
|
26
|
+
readonly unread: number
|
|
27
|
+
readonly nextCursor?: string | undefined
|
|
28
|
+
readonly now: Date
|
|
29
|
+
readonly t?: Translator
|
|
30
|
+
}): NotificationCentreView {
|
|
31
|
+
return {
|
|
32
|
+
rows: input.rows.map((row) => ({
|
|
33
|
+
id: row.id,
|
|
34
|
+
subject: row.subject,
|
|
35
|
+
body: row.body,
|
|
36
|
+
href: row.href,
|
|
37
|
+
at: formatTime(row.updatedAt, input.now, input.t),
|
|
38
|
+
isRead: row.isRead,
|
|
39
|
+
repeated: row.occurrences > 1 ? `Happened ${row.occurrences} times` : null,
|
|
40
|
+
})),
|
|
41
|
+
unread: input.unread,
|
|
42
|
+
nextHref:
|
|
43
|
+
input.nextCursor === undefined
|
|
44
|
+
? null
|
|
45
|
+
: `/notifications?after=${encodeURIComponent(input.nextCursor)}`,
|
|
46
|
+
preferencesHref: '/notifications/preferences',
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PreferencesView {
|
|
51
|
+
readonly rows: readonly NotificationPreferenceView[]
|
|
52
|
+
readonly backHref: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildPreferencesView(
|
|
56
|
+
rows: readonly NotificationPreferenceView[],
|
|
57
|
+
translator: Translator = untranslated(),
|
|
58
|
+
): PreferencesView {
|
|
59
|
+
return {
|
|
60
|
+
rows: rows.map((row) => ({
|
|
61
|
+
...row,
|
|
62
|
+
title: translated(translator, row.titleKey, row.title),
|
|
63
|
+
description: translated(translator, row.descriptionKey, row.description),
|
|
64
|
+
})),
|
|
65
|
+
backHref: '/notifications',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function translated(translator: Translator, key: string | undefined, fallback: string): string {
|
|
70
|
+
return key !== undefined && translator.has(key) ? translator.t(key) : fallback
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function notificationNotice(
|
|
74
|
+
query: {
|
|
75
|
+
readonly read?: string | undefined
|
|
76
|
+
readonly saved?: string | undefined
|
|
77
|
+
},
|
|
78
|
+
t: Translator = untranslated(),
|
|
79
|
+
): string | null {
|
|
80
|
+
if (query.saved !== undefined) return t.t('notice.preferencesSaved')
|
|
81
|
+
if (query.read === 'all') return t.t('notice.allRead')
|
|
82
|
+
if (query.read === 'one') return t.t('notice.markedRead')
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type OnboardingPanel = 'welcome' | 'profile' | 'first-post'
|
|
2
|
+
|
|
3
|
+
const ONBOARDING_COOKIE_PREFIX = 'meith_onboarding_'
|
|
4
|
+
|
|
5
|
+
export const ONBOARDING_PANELS: readonly OnboardingPanel[] = ['welcome', 'profile', 'first-post']
|
|
6
|
+
|
|
7
|
+
export const ONBOARDING_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
|
|
8
|
+
|
|
9
|
+
export function onboardingCookieName(panel: OnboardingPanel): string {
|
|
10
|
+
return `${ONBOARDING_COOKIE_PREFIX}${panel.replace('-', '_')}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isOnboardingPanel(value: unknown): value is OnboardingPanel {
|
|
14
|
+
return (ONBOARDING_PANELS as readonly unknown[]).includes(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isNewMember(postCount: number): boolean {
|
|
18
|
+
return postCount === 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hasCompleteProfile(profile: {
|
|
22
|
+
avatarUrl: string | null
|
|
23
|
+
bio: string | null
|
|
24
|
+
}): boolean {
|
|
25
|
+
return profile.avatarUrl !== null && profile.bio !== null && profile.bio.trim() !== ''
|
|
26
|
+
}
|