@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,31 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import type { MarkdownPipeline } from '@meith/markdown'
|
|
4
|
+
|
|
5
|
+
import { resolveInlineAttachments } from './attachment-embed'
|
|
6
|
+
import { highlightCodeBlocks } from './code-highlighting'
|
|
7
|
+
import { unfurlEmbeds } from './embeds'
|
|
8
|
+
import { filterView } from './plugin-view'
|
|
9
|
+
|
|
10
|
+
export const boardRendering: MarkdownPipeline = {
|
|
11
|
+
text: (text, context) =>
|
|
12
|
+
filterView('markdown.parse.text', text, { ...context.viewer, source: context.source }),
|
|
13
|
+
|
|
14
|
+
html: async (html, context) => {
|
|
15
|
+
const withAttachments = await resolveInlineAttachments(html, {
|
|
16
|
+
viewerUserId: context.viewer.userId,
|
|
17
|
+
postId: context.postId ?? null,
|
|
18
|
+
})
|
|
19
|
+
const withEmbeds = await unfurlEmbeds(await highlightCodeBlocks(withAttachments))
|
|
20
|
+
return filterView('markdown.render.html', withEmbeds, {
|
|
21
|
+
...context.viewer,
|
|
22
|
+
source: context.source,
|
|
23
|
+
})
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
vocabulary: async (source) => ({
|
|
27
|
+
revision: source.revision,
|
|
28
|
+
directives: await filterView('markdown.directives', source.directives, {}),
|
|
29
|
+
smilies: await filterView('smilies.list', source.smilies, {}),
|
|
30
|
+
}),
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { revalidatePath } from 'next/cache'
|
|
4
|
+
|
|
5
|
+
import { isAppError, logger } from '@meith/core'
|
|
6
|
+
|
|
7
|
+
import { recordAdminAction, requireAdmin } from './admin'
|
|
8
|
+
import type { FormState } from './auth-form-state'
|
|
9
|
+
import { tr } from './i18n'
|
|
10
|
+
import { refreshMarketplaceNow } from './marketplace-admin'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The Browse tab's "Refresh" button. Runs the exact same fetch, validate,
|
|
14
|
+
* cache, notify pass as the daily task (`refreshMarketplaceNow`, shared with
|
|
15
|
+
* `packages/runtime`'s task worker) — this is the "on demand" half of that
|
|
16
|
+
* one behaviour, not a second implementation of it.
|
|
17
|
+
*/
|
|
18
|
+
export async function refreshMarketplaceAction(
|
|
19
|
+
_prev: FormState,
|
|
20
|
+
_form: FormData,
|
|
21
|
+
): Promise<FormState> {
|
|
22
|
+
try {
|
|
23
|
+
await requireAdmin()
|
|
24
|
+
|
|
25
|
+
const result = await refreshMarketplaceNow()
|
|
26
|
+
|
|
27
|
+
revalidatePath('/admin/plugins/browse')
|
|
28
|
+
revalidatePath('/admin/themes/browse')
|
|
29
|
+
|
|
30
|
+
await recordAdminAction({
|
|
31
|
+
action: 'marketplace.refresh',
|
|
32
|
+
detail: { ok: result.ok, listingCount: result.listingCount },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (result.ok) return { notice: 'refreshed' }
|
|
36
|
+
return {
|
|
37
|
+
error: result.errorMessage ?? (await tr('notice.app.something-went-wrong-please-try')),
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (isAppError(err)) return { error: err.message }
|
|
41
|
+
logger({ module: 'marketplace-admin' }).error(
|
|
42
|
+
{ err },
|
|
43
|
+
'failed to refresh the marketplace catalog',
|
|
44
|
+
)
|
|
45
|
+
return { error: await tr('notice.app.something-went-wrong-please-try') }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { env, logger, readPluginEnv } from '@meith/core'
|
|
4
|
+
import { getDb, PostgresMarketplaceCacheRepository } from '@meith/db'
|
|
5
|
+
import {
|
|
6
|
+
type CachedMarketplace,
|
|
7
|
+
computeListingStatus,
|
|
8
|
+
EMPTY_CACHE,
|
|
9
|
+
type InstalledEntry,
|
|
10
|
+
type ListingKind,
|
|
11
|
+
type ListingStatus,
|
|
12
|
+
type MarketplaceListing,
|
|
13
|
+
MEITH_VERSION,
|
|
14
|
+
PLUGIN_API_MAJOR,
|
|
15
|
+
refreshCatalog,
|
|
16
|
+
THEME_API_MAJOR,
|
|
17
|
+
} from '@meith/marketplace'
|
|
18
|
+
|
|
19
|
+
import { notificationService } from './notifications'
|
|
20
|
+
import { configuredPlugins } from './plugin-host'
|
|
21
|
+
import { getSettings, getSettingsUncached } from './settings'
|
|
22
|
+
import { themeListing } from './theme-admin'
|
|
23
|
+
|
|
24
|
+
const BUILD_INFO = {
|
|
25
|
+
meithVersion: MEITH_VERSION,
|
|
26
|
+
pluginApiMajor: PLUGIN_API_MAJOR,
|
|
27
|
+
themeApiMajor: THEME_API_MAJOR,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether this process is the stock image rather than a graduated custom
|
|
32
|
+
* board — both run identical @meith/web code, so an env var set only by
|
|
33
|
+
* docker/Dockerfile is the only signal available: `BOARD_PLUGINS_MANIFEST`,
|
|
34
|
+
* baked in for `community board:eject` (see apps/cli/src/board-eject.ts). A
|
|
35
|
+
* graduated board's own scaffolded Dockerfile never sets it. Read fresh
|
|
36
|
+
* rather than cached at module load, so it is not fixed by whichever test
|
|
37
|
+
* happens to import this module first.
|
|
38
|
+
*/
|
|
39
|
+
function onStockImage(): boolean {
|
|
40
|
+
return readPluginEnv('BOARD_PLUGINS_MANIFEST') !== undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Caps against untrusted feed content — a self-hosted mirror can serve anything. */
|
|
44
|
+
const MAX_NAME_LENGTH = 120
|
|
45
|
+
const MAX_DESCRIPTION_LENGTH = 500
|
|
46
|
+
const MAX_LICENCE_LENGTH = 100
|
|
47
|
+
const MAX_REASON_LENGTH = 300
|
|
48
|
+
|
|
49
|
+
function truncate(value: string, max: number): string {
|
|
50
|
+
return value.length > max ? `${value.slice(0, max - 1)}…` : value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** https only, and a real URL — never rendered as a link otherwise. */
|
|
54
|
+
function safeHttpsUrl(value: string): string | null {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(value)
|
|
57
|
+
return url.protocol === 'https:' ? url.toString() : null
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function marketplaceCacheRepository(): PostgresMarketplaceCacheRepository | null {
|
|
64
|
+
return env.DATA_SOURCE === 'postgres' ? new PostgresMarketplaceCacheRepository(getDb()) : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readCache(): Promise<CachedMarketplace> {
|
|
68
|
+
const repository = marketplaceCacheRepository()
|
|
69
|
+
if (repository === null) return EMPTY_CACHE
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return await repository.read()
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger().warn({ err: String(error) }, 'could not read the cached marketplace feed')
|
|
75
|
+
return EMPTY_CACHE
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MarketplaceListingRow {
|
|
80
|
+
readonly key: string
|
|
81
|
+
readonly kind: ListingKind
|
|
82
|
+
readonly name: string
|
|
83
|
+
readonly description: string
|
|
84
|
+
readonly version: string
|
|
85
|
+
readonly package: string
|
|
86
|
+
readonly licence: string
|
|
87
|
+
readonly repositoryUrl: string | null
|
|
88
|
+
/** Same-origin URLs — the browser never fetches the feed's own host directly. */
|
|
89
|
+
readonly screenshotHrefs: readonly string[]
|
|
90
|
+
readonly status: ListingStatus
|
|
91
|
+
readonly incompatibleReason: string | null
|
|
92
|
+
readonly installedVersion: string | null
|
|
93
|
+
/** Set only for 'not-installed': the exact steps this board would need. Never an affordance that acts. */
|
|
94
|
+
readonly installSteps: readonly string[] | null
|
|
95
|
+
/**
|
|
96
|
+
* True only for 'not-installed' on the stock image, where installSteps'
|
|
97
|
+
* own `community plugin:add`/`community.config.ts` line cannot actually
|
|
98
|
+
* run — the image is fixed at build time. Signposts
|
|
99
|
+
* docs/marketplace.md's "Moving to a custom board" walkthrough; never a
|
|
100
|
+
* claim this board can graduate itself.
|
|
101
|
+
*/
|
|
102
|
+
readonly onStockImage: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface MarketplaceCatalogView {
|
|
106
|
+
readonly listings: readonly MarketplaceListingRow[]
|
|
107
|
+
readonly fetchedAt: Date | null
|
|
108
|
+
/** True once a fetch has ever succeeded — an empty `listings` before that is "not fetched yet", not "empty catalog". */
|
|
109
|
+
readonly hasEverFetched: boolean
|
|
110
|
+
readonly unreachable: boolean
|
|
111
|
+
readonly errorMessage: string | null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function installSteps(kind: ListingKind, packageName: string): readonly string[] {
|
|
115
|
+
return kind === 'plugin'
|
|
116
|
+
? [
|
|
117
|
+
`pnpm add ${packageName} --filter @meith/web`,
|
|
118
|
+
`community plugin:add ${packageName}`,
|
|
119
|
+
'Rebuild and redeploy for it to take effect.',
|
|
120
|
+
]
|
|
121
|
+
: [
|
|
122
|
+
`pnpm add ${packageName} --filter @meith/web`,
|
|
123
|
+
'Register it in community.config.ts (and set it as defaultTheme if it should be the board default).',
|
|
124
|
+
'Rebuild and redeploy for it to take effect.',
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function pluginInstalledEntries(): Promise<ReadonlyMap<string, InstalledEntry>> {
|
|
129
|
+
const map = new Map<string, InstalledEntry>()
|
|
130
|
+
for (const plugin of configuredPlugins()) {
|
|
131
|
+
if (!plugin.hasDefinition || plugin.version === null) continue
|
|
132
|
+
map.set(plugin.key, { enabled: plugin.enabled, version: plugin.version })
|
|
133
|
+
}
|
|
134
|
+
return map
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function themeInstalledEntries(): Promise<ReadonlyMap<string, InstalledEntry>> {
|
|
138
|
+
const map = new Map<string, InstalledEntry>()
|
|
139
|
+
for (const theme of await themeListing()) {
|
|
140
|
+
map.set(theme.key, { enabled: theme.enabled, version: null })
|
|
141
|
+
}
|
|
142
|
+
return map
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function screenshotHref(listingKey: string, index: number): string {
|
|
146
|
+
return `/admin/api/marketplace/screenshot?key=${encodeURIComponent(listingKey)}&index=${index}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toRow(
|
|
150
|
+
listing: MarketplaceListing,
|
|
151
|
+
installed: InstalledEntry | null,
|
|
152
|
+
): MarketplaceListingRow {
|
|
153
|
+
const { status, incompatibleReason } = computeListingStatus({ ...listing, installed }, BUILD_INFO)
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
key: listing.key,
|
|
157
|
+
kind: listing.kind,
|
|
158
|
+
name: truncate(listing.name, MAX_NAME_LENGTH),
|
|
159
|
+
description: truncate(listing.description, MAX_DESCRIPTION_LENGTH),
|
|
160
|
+
version: listing.version,
|
|
161
|
+
package: listing.package,
|
|
162
|
+
licence: truncate(listing.licence, MAX_LICENCE_LENGTH),
|
|
163
|
+
repositoryUrl: safeHttpsUrl(listing.repository),
|
|
164
|
+
screenshotHrefs: listing.screenshots.map((_, index) => screenshotHref(listing.key, index)),
|
|
165
|
+
status,
|
|
166
|
+
incompatibleReason:
|
|
167
|
+
incompatibleReason === null ? null : truncate(incompatibleReason, MAX_REASON_LENGTH),
|
|
168
|
+
installedVersion: installed?.version ?? null,
|
|
169
|
+
installSteps: status === 'not-installed' ? installSteps(listing.kind, listing.package) : null,
|
|
170
|
+
onStockImage: status === 'not-installed' && onStockImage(),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The Browse tab's whole read model for one kind — reads the cached feed
|
|
176
|
+
* only (never the network) and computes status against what this build
|
|
177
|
+
* actually contains. Renders honestly when the cache is empty: `listings`
|
|
178
|
+
* is `[]` either way, and `hasEverFetched`/`unreachable` say why.
|
|
179
|
+
*/
|
|
180
|
+
export async function marketplaceCatalog(kind: ListingKind): Promise<MarketplaceCatalogView> {
|
|
181
|
+
const cache = await readCache()
|
|
182
|
+
const installed =
|
|
183
|
+
kind === 'plugin' ? await pluginInstalledEntries() : await themeInstalledEntries()
|
|
184
|
+
|
|
185
|
+
const listings = (cache.feed?.listings ?? [])
|
|
186
|
+
.filter((listing) => listing.kind === kind)
|
|
187
|
+
.map((listing) => toRow(listing, installed.get(listing.key) ?? null))
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
listings,
|
|
191
|
+
fetchedAt: cache.fetchedAt,
|
|
192
|
+
hasEverFetched: cache.fetchedAt !== null,
|
|
193
|
+
unreachable: cache.error !== null,
|
|
194
|
+
errorMessage: cache.error,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One listing's screenshot, resolved against the feed's own host — never a client-supplied URL. */
|
|
199
|
+
export async function marketplaceScreenshotUrl(key: string, index: number): Promise<string | null> {
|
|
200
|
+
const cache = await readCache()
|
|
201
|
+
if (cache.feed === null || cache.sourceUrl === null) return null
|
|
202
|
+
|
|
203
|
+
const listing = cache.feed.listings.find((entry) => entry.key === key)
|
|
204
|
+
const path = listing?.screenshots[index]
|
|
205
|
+
if (path === undefined) return null
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
return new URL(path, cache.sourceUrl).toString()
|
|
209
|
+
} catch {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Runs the same fetch, validate, cache, notify pass the daily task does — the
|
|
216
|
+
* admin panel's "Refresh" button and the task call the exact same function,
|
|
217
|
+
* per docs/marketplace.md.
|
|
218
|
+
*/
|
|
219
|
+
export async function refreshMarketplaceNow(): Promise<{
|
|
220
|
+
readonly ok: boolean
|
|
221
|
+
readonly listingCount: number
|
|
222
|
+
readonly errorMessage: string | null
|
|
223
|
+
}> {
|
|
224
|
+
const repository = marketplaceCacheRepository()
|
|
225
|
+
if (repository === null) {
|
|
226
|
+
return { ok: false, listingCount: 0, errorMessage: 'This board is running in fixture mode.' }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const url = (await getSettingsUncached()).get('marketplace.feed_url')
|
|
230
|
+
const pluginEntries = await pluginInstalledEntries()
|
|
231
|
+
const notifications = notificationService()
|
|
232
|
+
|
|
233
|
+
const result = await refreshCatalog({
|
|
234
|
+
url,
|
|
235
|
+
repository,
|
|
236
|
+
build: BUILD_INFO,
|
|
237
|
+
resolveInstalled: (listing) =>
|
|
238
|
+
listing.kind === 'plugin' ? (pluginEntries.get(listing.key) ?? null) : null,
|
|
239
|
+
notifyUpdate: async (listing) => {
|
|
240
|
+
if (notifications === null) return
|
|
241
|
+
await notifications.raiseForAdministrators({
|
|
242
|
+
kind: 'marketplace.update_available',
|
|
243
|
+
data: {
|
|
244
|
+
key: listing.key,
|
|
245
|
+
name: listing.name,
|
|
246
|
+
package: listing.package,
|
|
247
|
+
version: listing.version,
|
|
248
|
+
},
|
|
249
|
+
href: '/admin/plugins/browse',
|
|
250
|
+
dedupeKey: `marketplace.update_available:${listing.key}:${listing.version}`,
|
|
251
|
+
})
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
return { ok: result.ok, listingCount: result.listingCount, errorMessage: result.error }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function currentFeedUrl(): Promise<string> {
|
|
259
|
+
return (await getSettings()).get('marketplace.feed_url')
|
|
260
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import type { MemberSuggestion } from '@meith/accounts'
|
|
4
|
+
|
|
5
|
+
import { getContainer } from './container'
|
|
6
|
+
import { getActor } from './context'
|
|
7
|
+
import { searchMentionCandidates } from './mention-search'
|
|
8
|
+
import { relationService } from './relations'
|
|
9
|
+
|
|
10
|
+
export async function searchMentionCandidatesAction(
|
|
11
|
+
query: string,
|
|
12
|
+
): Promise<readonly MemberSuggestion[]> {
|
|
13
|
+
const actor = await getActor()
|
|
14
|
+
const { authorizer, memberProfiles } = getContainer()
|
|
15
|
+
|
|
16
|
+
return searchMentionCandidates(actor, typeof query === 'string' ? query : '', {
|
|
17
|
+
authorizer,
|
|
18
|
+
memberProfiles,
|
|
19
|
+
relations: relationService(),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MemberProfileRepository, MemberSuggestion } from '@meith/accounts'
|
|
2
|
+
import type { Actor, Authorizer } from '@meith/authorization'
|
|
3
|
+
import type { RelationService } from '@meith/relations'
|
|
4
|
+
|
|
5
|
+
const MIN_QUERY_LENGTH = 1
|
|
6
|
+
const MAX_QUERY_LENGTH = 32
|
|
7
|
+
const SUGGESTION_LIMIT = 8
|
|
8
|
+
|
|
9
|
+
export interface MentionSearchDeps {
|
|
10
|
+
readonly authorizer: Authorizer
|
|
11
|
+
readonly memberProfiles: MemberProfileRepository
|
|
12
|
+
readonly relations: RelationService | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function searchMentionCandidates(
|
|
16
|
+
actor: Actor,
|
|
17
|
+
rawQuery: string,
|
|
18
|
+
deps: MentionSearchDeps,
|
|
19
|
+
): Promise<readonly MemberSuggestion[]> {
|
|
20
|
+
const query = rawQuery.trim()
|
|
21
|
+
if (query.length < MIN_QUERY_LENGTH || query.length > MAX_QUERY_LENGTH) return []
|
|
22
|
+
if (!deps.authorizer.can(actor, 'profile.view')) return []
|
|
23
|
+
|
|
24
|
+
const [matches, ignored] = await Promise.all([
|
|
25
|
+
deps.memberProfiles.searchByUsernamePrefix(query, SUGGESTION_LIMIT * 2),
|
|
26
|
+
actor.userId === null || deps.relations === null
|
|
27
|
+
? Promise.resolve<readonly number[]>([])
|
|
28
|
+
: deps.relations.ignoredIds(actor.userId),
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
if (ignored.length === 0) return matches.slice(0, SUGGESTION_LIMIT)
|
|
32
|
+
|
|
33
|
+
const ignoredIds = new Set(ignored)
|
|
34
|
+
return matches.filter((candidate) => !ignoredIds.has(candidate.id)).slice(0, SUGGESTION_LIMIT)
|
|
35
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { redirect } from 'next/navigation'
|
|
4
|
+
|
|
5
|
+
import { ValidationError } from '@meith/core'
|
|
6
|
+
import { msg } from '@meith/i18n'
|
|
7
|
+
import { parseFolder } from '@meith/messages'
|
|
8
|
+
|
|
9
|
+
import { folderHref } from '../view/messages'
|
|
10
|
+
import { dailyLimitMessage, limitMessage, spendDailyLimit, spendLimit } from './antispam'
|
|
11
|
+
import type { FormState } from './auth-form-state'
|
|
12
|
+
import { getActor } from './context'
|
|
13
|
+
import { formStateReporter } from './form-state-reporter'
|
|
14
|
+
import { text } from './form-values'
|
|
15
|
+
import { requireMessaging } from './messages'
|
|
16
|
+
|
|
17
|
+
const toFormState = formStateReporter('message-actions', 'unexpected error in a message action')
|
|
18
|
+
|
|
19
|
+
export async function sendMessageAction(_prev: FormState, form: FormData): Promise<FormState> {
|
|
20
|
+
const values = {
|
|
21
|
+
to: text(form, 'to'),
|
|
22
|
+
bcc: text(form, 'bcc'),
|
|
23
|
+
subject: text(form, 'subject'),
|
|
24
|
+
message: text(form, 'message'),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const actor = await getActor()
|
|
29
|
+
const { service, userId, username } = await requireMessaging(actor)
|
|
30
|
+
|
|
31
|
+
const limited = await spendLimit({ scope: 'message', actor })
|
|
32
|
+
if (limited !== null && !limited.allowed) {
|
|
33
|
+
return { error: limitMessage(limited), values }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const daily = await spendDailyLimit({ scope: 'message_day', actor })
|
|
37
|
+
if (daily !== null && !daily.allowed) {
|
|
38
|
+
return { error: dailyLimitMessage('message_day', daily), values }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const replyTo = Number(text(form, 'replyTo'))
|
|
42
|
+
await service.send({
|
|
43
|
+
authorUserId: userId,
|
|
44
|
+
authorUsername: username,
|
|
45
|
+
to: values.to,
|
|
46
|
+
bcc: values.bcc,
|
|
47
|
+
subject: values.subject,
|
|
48
|
+
message: values.message,
|
|
49
|
+
receiptRequested: form.get('receipt') === 'on',
|
|
50
|
+
replyToId: Number.isInteger(replyTo) && replyTo > 0 ? replyTo : null,
|
|
51
|
+
})
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return toFormState(err, values)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
redirect('/messages?folder=sent&sent=1')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function messageBulkAction(_prev: FormState, form: FormData): Promise<FormState> {
|
|
60
|
+
const folder = parseFolder(text(form, 'folder')) ?? 'inbox'
|
|
61
|
+
const command = text(form, 'command')
|
|
62
|
+
|
|
63
|
+
let query: string
|
|
64
|
+
try {
|
|
65
|
+
const { service, userId } = await requireMessaging(await getActor())
|
|
66
|
+
|
|
67
|
+
if (command === 'empty') {
|
|
68
|
+
query = `emptied=${await service.emptyTrash(userId)}`
|
|
69
|
+
} else {
|
|
70
|
+
const copyIds = selectedIds(form)
|
|
71
|
+
if (copyIds.length === 0)
|
|
72
|
+
throw new ValidationError(msg('error.app.select-at-least-one-message'))
|
|
73
|
+
|
|
74
|
+
switch (command) {
|
|
75
|
+
case 'read':
|
|
76
|
+
query = `marked=${await service.markRead(userId, copyIds)}`
|
|
77
|
+
break
|
|
78
|
+
case 'unread':
|
|
79
|
+
query = `marked=${await service.markUnread(userId, copyIds)}`
|
|
80
|
+
break
|
|
81
|
+
case 'trash':
|
|
82
|
+
query = `moved=${await service.move(userId, copyIds, 'trash')}`
|
|
83
|
+
break
|
|
84
|
+
case 'restore':
|
|
85
|
+
query = `moved=${await service.move(userId, copyIds, 'inbox')}`
|
|
86
|
+
break
|
|
87
|
+
case 'delete':
|
|
88
|
+
query = `deleted=${await service.remove(userId, copyIds)}`
|
|
89
|
+
break
|
|
90
|
+
default:
|
|
91
|
+
throw new ValidationError(msg('error.app.something-message'))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return toFormState(err)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const base = folderHref(folder)
|
|
99
|
+
redirect(`${base}${base.includes('?') ? '&' : '?'}${query}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function selectedIds(form: FormData): readonly number[] {
|
|
103
|
+
const ids: number[] = []
|
|
104
|
+
for (const value of form.getAll('copyId')) {
|
|
105
|
+
if (typeof value !== 'string') continue
|
|
106
|
+
const id = Number(value)
|
|
107
|
+
if (Number.isInteger(id) && id > 0) ids.push(id)
|
|
108
|
+
}
|
|
109
|
+
return ids
|
|
110
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { cache } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { Actor } from '@meith/authorization'
|
|
6
|
+
import { ForbiddenError } from '@meith/core'
|
|
7
|
+
import { currentRequestId } from '@meith/core/logger'
|
|
8
|
+
import { msg } from '@meith/i18n'
|
|
9
|
+
import { EMPTY_VOCABULARY } from '@meith/markdown'
|
|
10
|
+
import {
|
|
11
|
+
type MessageAudit,
|
|
12
|
+
type MessageNotifierPort,
|
|
13
|
+
type MessagePolicy,
|
|
14
|
+
MessageService,
|
|
15
|
+
} from '@meith/messages'
|
|
16
|
+
|
|
17
|
+
import { getContainer } from './container'
|
|
18
|
+
import { activeVocabulary } from './content-admin'
|
|
19
|
+
import { boardRendering } from './markdown-pipeline'
|
|
20
|
+
import { notificationService } from './notifications'
|
|
21
|
+
import { emitEvent, filterView } from './plugin-view'
|
|
22
|
+
import { relationService } from './relations'
|
|
23
|
+
|
|
24
|
+
const PLUGIN_AUDIT: MessageAudit = {
|
|
25
|
+
before: (input) => filterView('pm.send.before', input, { requestId: currentRequestId() ?? null }),
|
|
26
|
+
|
|
27
|
+
sent: (input) => emitEvent('pm.sent', input, { requestId: currentRequestId() ?? null }),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function messageService(): MessageService | null {
|
|
31
|
+
const { messages } = getContainer()
|
|
32
|
+
if (messages === null) return null
|
|
33
|
+
|
|
34
|
+
return new MessageService({
|
|
35
|
+
messages,
|
|
36
|
+
policy: messagePolicy(),
|
|
37
|
+
notifier: messageNotifier(),
|
|
38
|
+
vocabulary: async () => (await activeVocabulary()) ?? EMPTY_VOCABULARY,
|
|
39
|
+
rendering: boardRendering,
|
|
40
|
+
audit: PLUGIN_AUDIT,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Messaging {
|
|
45
|
+
readonly service: MessageService
|
|
46
|
+
readonly userId: number
|
|
47
|
+
readonly username: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function requireMessaging(actor: Actor): Promise<Messaging> {
|
|
51
|
+
const { authorizer, accountStore } = getContainer()
|
|
52
|
+
|
|
53
|
+
if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
|
|
54
|
+
if (!authorizer.can(actor, 'pm.use')) {
|
|
55
|
+
throw new ForbiddenError(msg('error.app.use-private-messages'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const service = messageService()
|
|
59
|
+
if (service === null) {
|
|
60
|
+
throw new ForbiddenError(msg('error.app.board-running-in-memory-sample-data-21'))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const account = await accountStore.accounts.findById(actor.userId)
|
|
64
|
+
if (account === null) throw new ForbiddenError(msg('error.app.must-logged'))
|
|
65
|
+
|
|
66
|
+
return { service, userId: actor.userId, username: account.username }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function messagePolicy(): MessagePolicy {
|
|
70
|
+
const { accountStore, actorSource, authorizer } = getContainer()
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
async lookup(username) {
|
|
74
|
+
const { foldIdentifier } = await import('@meith/accounts')
|
|
75
|
+
const account = await accountStore.accounts.findByUsernameLower(foldIdentifier(username))
|
|
76
|
+
if (account === null) return null
|
|
77
|
+
return { id: account.id, username: account.username }
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async limitsFor(userId) {
|
|
81
|
+
const actor = await actorSource.buildForUser(userId)
|
|
82
|
+
if (actor === null) return { quota: 0, canReceive: false }
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
canReceive: authorizer.can(actor, 'pm.use'),
|
|
86
|
+
quota: authorizer.globalLimit(actor, 'privateMessageQuota'),
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async blocks(ownerUserId, senderUserId) {
|
|
91
|
+
const service = relationService()
|
|
92
|
+
if (service === null) return false
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return await service.ignores(ownerUserId, senderUserId)
|
|
96
|
+
} catch {
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function messageNotifier(): MessageNotifierPort | null {
|
|
104
|
+
const service = notificationService()
|
|
105
|
+
if (service === null) return null
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
async messageReceived(input) {
|
|
109
|
+
await service.raise({
|
|
110
|
+
userId: input.userId,
|
|
111
|
+
kind: 'pm.received',
|
|
112
|
+
data: { fromUsername: input.fromUsername, subject: input.subject },
|
|
113
|
+
href: `/messages/${input.messageId}`,
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async receiptRead(input) {
|
|
118
|
+
await service.raise({
|
|
119
|
+
userId: input.userId,
|
|
120
|
+
kind: 'pm.receipt',
|
|
121
|
+
data: { byUsername: input.byUsername, subject: input.subject },
|
|
122
|
+
dedupeKey: `pm.receipt:${input.messageId}`,
|
|
123
|
+
href: `/messages/${input.messageId}`,
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const unreadMessageCount = cache(async function unreadMessageCount(
|
|
130
|
+
userId: number | null,
|
|
131
|
+
): Promise<number> {
|
|
132
|
+
if (userId === null) return 0
|
|
133
|
+
const service = messageService()
|
|
134
|
+
if (service === null) return 0
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
return (await service.counts(userId)).unread
|
|
138
|
+
} catch {
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
})
|