@meith/web 0.31.0 → 0.32.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/app/(board)/[slug]/feed.xml/route.ts +5 -3
- package/app/(board)/[slug]/new/page.tsx +5 -0
- package/app/(board)/[slug]/page.tsx +7 -1
- package/app/(board)/discover/[view]/page.tsx +6 -1
- package/app/(board)/member/[id]/page.tsx +3 -5
- package/app/(board)/member/[id]/reputation/page.tsx +8 -15
- package/app/(board)/members/page.tsx +3 -8
- package/app/(board)/messages/[id]/page.tsx +3 -7
- package/app/(board)/messages/page.tsx +3 -5
- package/app/(board)/modcp/ip/page.tsx +3 -5
- package/app/(board)/modcp/plugins/[key]/[[...path]]/page.tsx +84 -0
- package/app/(board)/moderation/reports/page.tsx +53 -15
- package/app/(board)/moderation/warn/page.tsx +3 -6
- package/app/(board)/notifications/page.tsx +5 -10
- package/app/(board)/notifications/preferences/page.tsx +8 -0
- package/app/(board)/online/page.tsx +12 -6
- package/app/(board)/staff/page.tsx +4 -5
- package/app/(board)/stats/page.tsx +10 -3
- package/app/(board)/subscriptions/page.tsx +5 -10
- package/app/(board)/thread/[slug]/edit/page.tsx +21 -0
- package/app/(board)/thread/[slug]/feed.xml/route.ts +6 -3
- package/app/(board)/thread/[slug]/page.tsx +49 -11
- package/app/(board)/thread/[slug]/reply/page.tsx +5 -0
- package/app/(board)/unsubscribe/page.tsx +8 -11
- package/app/(board)/usercp/contacts/page.tsx +3 -5
- package/app/(board)/usercp/drafts/page.tsx +70 -0
- package/app/(board)/usercp/options/page.tsx +8 -1
- package/app/(board)/usercp/security/page.tsx +16 -0
- package/app/admin/antispam/page.tsx +4 -12
- package/app/admin/content/attachments/page.tsx +3 -6
- package/app/admin/content/page.tsx +3 -13
- package/app/admin/forums/[id]/permissions/page.tsx +9 -12
- package/app/admin/groups/page.tsx +7 -9
- package/app/admin/page.tsx +12 -6
- package/app/admin/plugins/[key]/[[...path]]/page.tsx +6 -7
- package/app/admin/plugins/page.tsx +3 -6
- package/app/admin/users/[id]/page.tsx +5 -11
- package/app/admin/users/prune/page.tsx +4 -12
- package/app/admin/webhooks/page.tsx +144 -0
- package/app/api/subscribe/thread/[id]/route.ts +53 -0
- package/app/api/unsubscribe/thread/[id]/route.ts +42 -0
- package/app/atom.xml/route.ts +2 -2
- package/app/auth/sso/[provider]/callback/route.ts +10 -0
- package/app/feed.xml/route.ts +2 -2
- package/app/offline/route.ts +98 -0
- package/next.config.mjs +1 -0
- package/package.json +49 -48
- package/public/sw.js +42 -2
- package/src/components/account/draft-forms.tsx +39 -0
- package/src/components/account/feed-token.tsx +95 -0
- package/src/components/account/notification-forms.tsx +37 -2
- package/src/components/account/relation-forms.tsx +3 -2
- package/src/components/account/subscription-forms.tsx +20 -6
- package/src/components/account/usercp-forms.tsx +44 -0
- package/src/components/admin/api-token-forms.tsx +11 -7
- package/src/components/admin/ban-filter-forms.tsx +15 -11
- package/src/components/admin/content-forms.tsx +31 -14
- package/src/components/admin/forum-forms.tsx +241 -129
- package/src/components/admin/forum-tree.tsx +5 -7
- package/src/components/admin/navigation-forms.tsx +43 -26
- package/src/components/admin/navigation-tree.tsx +8 -13
- package/src/components/admin/oklch-picker.tsx +2 -1
- package/src/components/admin/theme-changes.tsx +3 -2
- package/src/components/admin/theme-forms.tsx +3 -2
- package/src/components/admin/webhook-forms.tsx +137 -0
- package/src/components/auth/register-form.tsx +4 -7
- package/src/components/board/live-region.tsx +3 -1
- package/src/components/content/attachment-field.tsx +93 -22
- package/src/components/content/combobox-keys.ts +38 -0
- package/src/components/content/edit-post-form.tsx +75 -1
- package/src/components/content/markdown-editor.tsx +29 -86
- package/src/components/content/mention-combobox.tsx +137 -0
- package/src/components/content/multiquote-button.tsx +15 -12
- package/src/components/content/new-thread-form.tsx +95 -17
- package/src/components/content/poll-edit-form.tsx +61 -39
- package/src/components/content/poll-vote-form.tsx +91 -0
- package/src/components/content/poll.tsx +12 -48
- package/src/components/content/progressive-marker.tsx +13 -0
- package/src/components/content/recipient-segment.ts +39 -0
- package/src/components/content/reply-form.tsx +9 -1
- package/src/components/content/thanks-button.tsx +12 -7
- package/src/components/messages/message-forms.tsx +53 -43
- package/src/components/messages/recipient-field.tsx +88 -0
- package/src/components/moderation/inline-moderation-form.tsx +2 -0
- package/src/components/moderation/modcp-shell.tsx +17 -3
- package/src/components/moderation/queue-form.tsx +3 -6
- package/src/components/moderation/report-forms.tsx +24 -5
- package/src/components/moderation/thread-tools-form.tsx +2 -0
- package/src/components/shell/board-notice.tsx +22 -1
- package/src/components/shell/confirm-dialog.tsx +93 -0
- package/src/components/shell/nav-disclosure-enhancer.tsx +36 -0
- package/src/components/shell/notice-toast.tsx +40 -0
- package/src/components/shell/notification-menu.tsx +5 -3
- package/src/components/shell/page-shell.tsx +3 -0
- package/src/components/shell/scheme-toggle.tsx +72 -0
- package/src/components/shell/theme-switcher.tsx +16 -36
- package/src/server/admin-settings-actions.ts +5 -0
- package/src/server/api/search.ts +8 -2
- package/src/server/api-token-actions.ts +4 -0
- package/src/server/appearance-actions.ts +21 -0
- package/src/server/attachments.ts +35 -0
- package/src/server/auth-actions.ts +2 -0
- package/src/server/auth-form-state.ts +28 -0
- package/src/server/auto-watch.ts +31 -0
- package/src/server/ban-filter-admin-actions.ts +5 -0
- package/src/server/confirm.ts +19 -0
- package/src/server/content-actions.ts +129 -19
- package/src/server/content-admin-actions.ts +31 -0
- package/src/server/discovery.ts +11 -1
- package/src/server/feed-routes.ts +13 -4
- package/src/server/feed-token-actions.ts +61 -0
- package/src/server/feed-token.ts +78 -0
- package/src/server/fixture-post-repo.ts +18 -0
- package/src/server/forum-admin-actions.ts +23 -16
- package/src/server/forum-admin.ts +10 -1
- package/src/server/inline-moderation-actions.ts +6 -0
- package/src/server/message-actions.ts +9 -0
- package/src/server/navigation-admin-actions.ts +6 -0
- package/src/server/plugin-admin.ts +7 -2
- package/src/server/plugin-pages.ts +50 -0
- package/src/server/plugin-panel.ts +34 -1
- package/src/server/plugin-routes.ts +8 -0
- package/src/server/plugin-view.tsx +46 -7
- package/src/server/poll-actions.ts +13 -1
- package/src/server/presence.ts +23 -0
- package/src/server/reply-core.ts +8 -1
- package/src/server/report-actions.ts +13 -2
- package/src/server/reputation-actions.ts +12 -1
- package/src/server/search-page.ts +8 -2
- package/src/server/search.ts +18 -5
- package/src/server/subscription-actions.ts +9 -0
- package/src/server/thread-core.ts +10 -2
- package/src/server/thread-tool-actions.ts +6 -0
- package/src/server/unread-goto.ts +28 -0
- package/src/server/upgrade-notice.ts +1 -1
- package/src/server/usercp-actions.ts +31 -1
- package/src/server/viewer-preferences.ts +5 -0
- package/src/server/webhook-actions.ts +98 -0
- package/src/server/webhooks-admin.ts +122 -0
- package/src/theme/contract.fixture.ts +36 -5
- package/src/view/account-copy.ts +39 -0
- package/src/view/admin-copy.ts +4 -9
- package/src/view/admin-nav.ts +6 -0
- package/src/view/admin-panel-copy.ts +23 -0
- package/src/view/attachments.ts +33 -1
- package/src/view/board-index.ts +31 -2
- package/src/view/board-title.ts +1 -0
- package/src/view/content-copy.ts +22 -12
- package/src/view/copy.ts +17 -0
- package/src/view/discovery-view.ts +2 -1
- package/src/view/drafts.ts +45 -0
- package/src/view/editor-toolbar.ts +14 -0
- package/src/view/forum-display.ts +39 -7
- package/src/view/modcp-nav.ts +32 -0
- package/src/view/moderation-copy.ts +20 -0
- package/src/view/notifications.ts +1 -0
- package/src/view/plugin-panel.ts +13 -0
- package/src/view/poll-vote.ts +46 -0
- package/src/view/post-link.ts +8 -1
- package/src/view/progressive-enhancement.ts +5 -0
- package/src/view/shell.ts +2 -1
- package/src/view/subscriptions.ts +1 -0
- package/src/view/thread-view.ts +41 -2
- package/src/view/usercp-nav.ts +6 -0
- package/src/view/usercp.ts +14 -0
|
@@ -4,13 +4,14 @@ import { getContainer } from '@/server/container'
|
|
|
4
4
|
import { activeWordFilter } from '@/server/content-admin'
|
|
5
5
|
import { feedFor } from '@/server/feed-builder'
|
|
6
6
|
import { feedResponse, noFeed, offlineFeed } from '@/server/feed-routes'
|
|
7
|
-
import {
|
|
7
|
+
import { feedScopeForRequest } from '@/server/feed-token'
|
|
8
|
+
import { FEED_LIMIT, feedRepository, origin } from '@/server/syndication'
|
|
8
9
|
import { leadingId } from '@/view/slug-id'
|
|
9
10
|
|
|
10
11
|
export const dynamic = 'force-dynamic'
|
|
11
12
|
|
|
12
13
|
export async function GET(
|
|
13
|
-
|
|
14
|
+
request: Request,
|
|
14
15
|
{ params }: { params: Promise<{ slug: string }> },
|
|
15
16
|
): Promise<Response> {
|
|
16
17
|
const offline = await offlineFeed()
|
|
@@ -23,7 +24,7 @@ export async function GET(
|
|
|
23
24
|
const id = leadingId(slug)
|
|
24
25
|
if (id === null) return noFeed()
|
|
25
26
|
|
|
26
|
-
const scope = await
|
|
27
|
+
const { scope, tokened } = await feedScopeForRequest(request)
|
|
27
28
|
if (!scope.forumIds.includes(id)) return noFeed()
|
|
28
29
|
|
|
29
30
|
const forum = await getContainer().forums.findById(id)
|
|
@@ -44,5 +45,6 @@ export async function GET(
|
|
|
44
45
|
}),
|
|
45
46
|
'rss',
|
|
46
47
|
'forum',
|
|
48
|
+
{ private: tokened },
|
|
47
49
|
)
|
|
48
50
|
}
|
|
@@ -7,6 +7,7 @@ import { requireSlot, slotCopy } from '@meith/theme-kit'
|
|
|
7
7
|
import { NewThreadForm } from '@/components/content/new-thread-form'
|
|
8
8
|
import { OnboardingBanner } from '@/components/shell/onboarding-banner'
|
|
9
9
|
import { attachmentLimits, canAttach } from '@/server/attachments'
|
|
10
|
+
import { autoWatchChecksByDefault, autoWatchPreference } from '@/server/auto-watch'
|
|
10
11
|
import { getContainer } from '@/server/container'
|
|
11
12
|
import { getActor } from '@/server/context'
|
|
12
13
|
import { getTranslator, tr } from '@/server/i18n'
|
|
@@ -46,6 +47,9 @@ export default async function NewThreadPage({ params }: { params: Promise<{ slug
|
|
|
46
47
|
const prefixes = await threadWrites.listPrefixes(id)
|
|
47
48
|
|
|
48
49
|
const attachTarget = { ...target, allowsAttachments: rules.allowAttachments }
|
|
50
|
+
const subscribeDefault = autoWatchChecksByDefault(
|
|
51
|
+
await autoWatchPreference(actor.userId, 'create'),
|
|
52
|
+
)
|
|
49
53
|
|
|
50
54
|
const view = buildNewThreadView({
|
|
51
55
|
t: await getTranslator(),
|
|
@@ -87,6 +91,7 @@ export default async function NewThreadPage({ params }: { params: Promise<{ slug
|
|
|
87
91
|
prefixes={prefixes.map((p) => ({ id: p.id, label: p.label }))}
|
|
88
92
|
requiresPrefix={rules.requiresPrefix}
|
|
89
93
|
canSubscribe={authorizer.can(actor, 'forum.subscribe', target)}
|
|
94
|
+
subscribeDefault={subscribeDefault}
|
|
90
95
|
canPostPoll={authorizer.can(actor, 'poll.post', target)}
|
|
91
96
|
attachmentLimits={attachable ? attachmentLimits(attachTarget) : null}
|
|
92
97
|
draft={
|
|
@@ -20,7 +20,7 @@ import { getTranslator } from '@/server/i18n'
|
|
|
20
20
|
import { legacyDestination } from '@/server/legacy-redirect'
|
|
21
21
|
import { moderatorTargetFor } from '@/server/modcp'
|
|
22
22
|
import { pageMetadata } from '@/server/page-metadata'
|
|
23
|
-
import { filterView, viewerRef } from '@/server/plugin-view'
|
|
23
|
+
import { filterView, threadRowBadges, viewerRef } from '@/server/plugin-view'
|
|
24
24
|
import { getSettings } from '@/server/settings'
|
|
25
25
|
import { currentTheme } from '@/server/theme'
|
|
26
26
|
import { getViewerPreferences } from '@/server/viewer-preferences'
|
|
@@ -279,6 +279,11 @@ export default async function ForumPage({
|
|
|
279
279
|
),
|
|
280
280
|
)
|
|
281
281
|
|
|
282
|
+
const badgesByThread = await threadRowBadges(
|
|
283
|
+
actor,
|
|
284
|
+
view.threads.map((thread) => ({ threadId: thread.id, authorId: thread.author.userId })),
|
|
285
|
+
)
|
|
286
|
+
|
|
282
287
|
const threadRows = await Promise.all(
|
|
283
288
|
view.threads.map((thread) =>
|
|
284
289
|
filterView(
|
|
@@ -286,6 +291,7 @@ export default async function ForumPage({
|
|
|
286
291
|
{
|
|
287
292
|
thread,
|
|
288
293
|
select: selectionFor('thread', thread.id, `“${thread.title}”`, inlineOffered),
|
|
294
|
+
regions: { pluginBadges: badgesByThread.get(thread.id) },
|
|
289
295
|
},
|
|
290
296
|
pluginContext,
|
|
291
297
|
),
|
|
@@ -59,9 +59,14 @@ export default async function DiscoverPage({
|
|
|
59
59
|
const themed = async (model: DiscoveryViewModel) =>
|
|
60
60
|
filterView('view.discovery-view', model, viewerRef(actor))
|
|
61
61
|
|
|
62
|
+
const views =
|
|
63
|
+
actor.userId === null
|
|
64
|
+
? DISCOVERY_VIEWS.filter((candidate) => candidate !== 'unread')
|
|
65
|
+
: DISCOVERY_VIEWS
|
|
66
|
+
|
|
62
67
|
const common = {
|
|
63
68
|
view,
|
|
64
|
-
views
|
|
69
|
+
views,
|
|
65
70
|
pageSize: DISCOVER_PAGE,
|
|
66
71
|
isFirstPage: after === null,
|
|
67
72
|
now,
|
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
4
|
import { requireSlot, slotCopy } from '@meith/theme-kit'
|
|
5
|
+
import { TextLink } from '@meith/ui'
|
|
5
6
|
|
|
6
7
|
import { RemoveRelationForm, SetRelationForm } from '@/components/account/relation-forms'
|
|
7
8
|
import { AvatarLockForm, SignatureLockForm } from '@/components/moderation/signature-lock-form'
|
|
@@ -114,12 +115,9 @@ export default async function MemberProfilePage({ params }: { params: Promise<{
|
|
|
114
115
|
{translator.t('board.member.reputation')}
|
|
115
116
|
</span>
|
|
116
117
|
<span className="font-medium">{reputationLabel(repSummary, translator)}</span>
|
|
117
|
-
<
|
|
118
|
-
href={`/member/${id}/reputation`}
|
|
119
|
-
className="ms-auto font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
120
|
-
>
|
|
118
|
+
<TextLink href={`/member/${id}/reputation`} className="ms-auto">
|
|
121
119
|
{await tr('page.see-all-ratings')}
|
|
122
|
-
</
|
|
120
|
+
</TextLink>
|
|
123
121
|
</div>
|
|
124
122
|
)}
|
|
125
123
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
|
+
import { TextLink } from '@meith/ui'
|
|
5
|
+
|
|
4
6
|
import { RateMemberForm, WithdrawRatingForm } from '@/components/account/reputation-forms'
|
|
5
7
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
6
8
|
import { getContainer } from '@/server/container'
|
|
@@ -96,12 +98,9 @@ export default async function ReputationPage({
|
|
|
96
98
|
<p className="text-sm text-muted-foreground">
|
|
97
99
|
{reputationLabel(view.summary, translator)}
|
|
98
100
|
</p>
|
|
99
|
-
<
|
|
100
|
-
href={`/member/${id}`}
|
|
101
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
102
|
-
>
|
|
101
|
+
<TextLink href={`/member/${id}`} size="sm">
|
|
103
102
|
{await tr('page.back-their-profile')}
|
|
104
|
-
</
|
|
103
|
+
</TextLink>
|
|
105
104
|
</div>
|
|
106
105
|
|
|
107
106
|
{mayRate && (
|
|
@@ -152,12 +151,9 @@ export default async function ReputationPage({
|
|
|
152
151
|
{row.at.label}
|
|
153
152
|
</time>
|
|
154
153
|
{row.postHref !== null && (
|
|
155
|
-
<
|
|
156
|
-
href={row.postHref}
|
|
157
|
-
className="text-xs font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
158
|
-
>
|
|
154
|
+
<TextLink href={row.postHref} size="xs">
|
|
159
155
|
on a post
|
|
160
|
-
</
|
|
156
|
+
</TextLink>
|
|
161
157
|
)}
|
|
162
158
|
{row.isMine && (
|
|
163
159
|
<span className="ml-auto">
|
|
@@ -182,12 +178,9 @@ export default async function ReputationPage({
|
|
|
182
178
|
)}
|
|
183
179
|
|
|
184
180
|
{view.nextHref !== null && (
|
|
185
|
-
<
|
|
186
|
-
href={view.nextHref}
|
|
187
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
188
|
-
>
|
|
181
|
+
<TextLink href={view.nextHref} size="sm">
|
|
189
182
|
{await tr('page.older-ratings')}
|
|
190
|
-
</
|
|
183
|
+
</TextLink>
|
|
191
184
|
)}
|
|
192
185
|
</div>
|
|
193
186
|
</main>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
|
-
import { Card, CardRows, Empty, EmptyDescription, EmptyTitle } from '@meith/ui'
|
|
4
|
+
import { Card, CardRows, Empty, EmptyDescription, EmptyTitle, TextLink } from '@meith/ui'
|
|
5
5
|
|
|
6
6
|
import { PanelPage } from '@/components/shell/panel-page'
|
|
7
7
|
import { PanelPagination } from '@/components/shell/panel-pagination'
|
|
@@ -112,14 +112,9 @@ export default async function MembersPage({
|
|
|
112
112
|
className="flex flex-col gap-1 px-4 py-3 sm:flex-row sm:items-baseline sm:justify-between sm:gap-4"
|
|
113
113
|
>
|
|
114
114
|
<span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
|
|
115
|
-
<
|
|
116
|
-
href={`/member/${row.id}`}
|
|
117
|
-
className={`font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground ${
|
|
118
|
-
identity?.nameClass ?? ''
|
|
119
|
-
}`}
|
|
120
|
-
>
|
|
115
|
+
<TextLink href={`/member/${row.id}`} className={identity?.nameClass ?? ''}>
|
|
121
116
|
{row.username}
|
|
122
|
-
</
|
|
117
|
+
</TextLink>
|
|
123
118
|
{identity?.groups.map((group) => (
|
|
124
119
|
<span
|
|
125
120
|
key={group.groupId}
|
|
@@ -2,7 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
4
|
import { postBodyHtml } from '@meith/markdown'
|
|
5
|
-
import { Card, CardContent, CardFooter } from '@meith/ui'
|
|
5
|
+
import { Card, CardContent, CardFooter, TextLink } from '@meith/ui'
|
|
6
6
|
|
|
7
7
|
import { PanelPage } from '@/components/shell/panel-page'
|
|
8
8
|
import { getContainer } from '@/server/container'
|
|
@@ -74,13 +74,9 @@ export default async function MessagePage({ params }: { params: Promise<{ id: st
|
|
|
74
74
|
|
|
75
75
|
<CardFooter className="gap-4">
|
|
76
76
|
{view.actions.map((action) => (
|
|
77
|
-
<
|
|
78
|
-
key={action.href}
|
|
79
|
-
href={action.href}
|
|
80
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
81
|
-
>
|
|
77
|
+
<TextLink key={action.href} href={action.href} size="sm">
|
|
82
78
|
{action.label}
|
|
83
|
-
</
|
|
79
|
+
</TextLink>
|
|
84
80
|
))}
|
|
85
81
|
{view.reportHref !== null && (
|
|
86
82
|
<a
|
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
4
|
import { MESSAGES_PAGE_SIZE, parseFolder } from '@meith/messages'
|
|
5
|
+
import { TextLink } from '@meith/ui'
|
|
5
6
|
|
|
6
7
|
import { MessageActionBar } from '@/components/messages/message-forms'
|
|
7
8
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
@@ -72,12 +73,9 @@ export default async function MessagesPage({
|
|
|
72
73
|
<PanelPage
|
|
73
74
|
title={await tr('page.private-messages')}
|
|
74
75
|
actions={
|
|
75
|
-
<
|
|
76
|
-
href={view.composeHref}
|
|
77
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
78
|
-
>
|
|
76
|
+
<TextLink href={view.composeHref} size="sm">
|
|
79
77
|
{await tr('page.write-message')}
|
|
80
|
-
</
|
|
78
|
+
</TextLink>
|
|
81
79
|
}
|
|
82
80
|
>
|
|
83
81
|
{notice !== null && (
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
EmptyTitle,
|
|
15
15
|
Field,
|
|
16
16
|
Input,
|
|
17
|
+
TextLink,
|
|
17
18
|
} from '@meith/ui'
|
|
18
19
|
|
|
19
20
|
import { PanelPage, PanelSection } from '@/components/shell/panel-page'
|
|
@@ -95,12 +96,9 @@ export default async function IpLookupPage({
|
|
|
95
96
|
title={translator.t('board.ip.matches', { username: subject.username })}
|
|
96
97
|
description={
|
|
97
98
|
<>
|
|
98
|
-
<
|
|
99
|
-
href={memberHref(subject.id)}
|
|
100
|
-
className="underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
101
|
-
>
|
|
99
|
+
<TextLink href={memberHref(subject.id)} tone="inherit" weight="normal">
|
|
102
100
|
{subject.username}
|
|
103
|
-
</
|
|
101
|
+
</TextLink>
|
|
104
102
|
. {translator.t('board.ip.ranges')} {translator.t('board.ip.registration')}{' '}
|
|
105
103
|
<code className="font-mono">
|
|
106
104
|
{result.prefixes.registration ?? translator.t('board.ip.none')}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { notFound, redirect } from 'next/navigation'
|
|
3
|
+
|
|
4
|
+
import { PANEL_CARD, PANEL_NOTE } from '@/components/shell/panel-list'
|
|
5
|
+
import { PanelPage } from '@/components/shell/panel-page'
|
|
6
|
+
import { ViewTabs } from '@/components/shell/view-tabs'
|
|
7
|
+
import { boardUrl } from '@/server/board-url'
|
|
8
|
+
import { getActor } from '@/server/context'
|
|
9
|
+
import { getTranslator, tr } from '@/server/i18n'
|
|
10
|
+
import { resolveModCpAccess } from '@/server/modcp'
|
|
11
|
+
import { pluginBoardPageTitle, renderPluginStaffPage } from '@/server/plugin-pages'
|
|
12
|
+
import { pluginStaffPanelSection } from '@/server/plugin-panel'
|
|
13
|
+
import { viewerRef } from '@/server/plugin-view'
|
|
14
|
+
import { pluginStaffPanelTabs } from '@/view/plugin-panel'
|
|
15
|
+
|
|
16
|
+
interface RouteParams {
|
|
17
|
+
readonly key: string
|
|
18
|
+
readonly path?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function generateMetadata({
|
|
22
|
+
params,
|
|
23
|
+
}: {
|
|
24
|
+
params: Promise<RouteParams>
|
|
25
|
+
}): Promise<Metadata> {
|
|
26
|
+
const { key, path } = await params
|
|
27
|
+
const title = pluginBoardPageTitle(key, (path ?? []).join('/'))
|
|
28
|
+
return title === null ? {} : { title }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async function PluginStaffPage({
|
|
32
|
+
params,
|
|
33
|
+
searchParams,
|
|
34
|
+
}: {
|
|
35
|
+
params: Promise<RouteParams>
|
|
36
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
37
|
+
}) {
|
|
38
|
+
const { key, path } = await params
|
|
39
|
+
const segments = path ?? []
|
|
40
|
+
if (segments.length > 1) notFound()
|
|
41
|
+
const pagePath = segments.join('/')
|
|
42
|
+
|
|
43
|
+
const query: Record<string, string> = {}
|
|
44
|
+
for (const [name, value] of Object.entries(await searchParams)) {
|
|
45
|
+
if (typeof value === 'string') query[name] = value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const actor = await getActor()
|
|
49
|
+
const result = await renderPluginStaffPage(key, pagePath, {
|
|
50
|
+
viewer: viewerRef(actor),
|
|
51
|
+
query,
|
|
52
|
+
boardUrl: await boardUrl(),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (result.outcome === 'forbidden') notFound()
|
|
56
|
+
|
|
57
|
+
const t = await getTranslator()
|
|
58
|
+
const section = await pluginStaffPanelSection(key, t)
|
|
59
|
+
|
|
60
|
+
if (result.outcome === 'missing') {
|
|
61
|
+
if (pagePath === '' && section !== null) {
|
|
62
|
+
const access = await resolveModCpAccess()
|
|
63
|
+
if (access?.hasGroupAccess === true) redirect(section.pages[0]!.href)
|
|
64
|
+
}
|
|
65
|
+
notFound()
|
|
66
|
+
}
|
|
67
|
+
const tabs =
|
|
68
|
+
section === null ? [] : pluginStaffPanelTabs({ pages: section.pages, current: pagePath })
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<PanelPage
|
|
72
|
+
back={{ href: '/modcp', label: await tr('page.moderator-control-panel') }}
|
|
73
|
+
title={result.title}
|
|
74
|
+
>
|
|
75
|
+
{tabs.length > 0 && <ViewTabs label={section?.name ?? result.title} tabs={tabs} />}
|
|
76
|
+
|
|
77
|
+
{result.node === null ? (
|
|
78
|
+
<p className={PANEL_NOTE}>{t.t('board.plugin.failed')}</p>
|
|
79
|
+
) : (
|
|
80
|
+
<section className={PANEL_CARD}>{result.node}</section>
|
|
81
|
+
)}
|
|
82
|
+
</PanelPage>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
parseReportCategory,
|
|
6
|
+
REPORT_CATEGORIES,
|
|
7
|
+
REPORTS_PAGE_SIZE,
|
|
8
|
+
ReportService,
|
|
9
|
+
} from '@meith/moderation'
|
|
10
|
+
import { Badge, Card, Empty, EmptyDescription, EmptyTitle, NativeSelect, TextLink } from '@meith/ui'
|
|
6
11
|
|
|
7
12
|
import { AssignReportForm, CloseReportForm } from '@/components/moderation/report-forms'
|
|
8
13
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
@@ -13,7 +18,7 @@ import { getActor } from '@/server/context'
|
|
|
13
18
|
import { getTranslator, tr } from '@/server/i18n'
|
|
14
19
|
import { messageService } from '@/server/messages'
|
|
15
20
|
import { hasReportScope, resolveReportScope } from '@/server/report-scope'
|
|
16
|
-
import { moderationFormsCopy } from '@/view/moderation-copy'
|
|
21
|
+
import { moderationFormsCopy, REPORT_CATEGORY_LABEL_KEYS } from '@/view/moderation-copy'
|
|
17
22
|
import { offsetOf, readPage } from '@/view/pager'
|
|
18
23
|
import { postLink } from '@/view/post-link'
|
|
19
24
|
import { formatTime } from '@/view/time'
|
|
@@ -25,7 +30,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
25
30
|
export default async function ReportsPage({
|
|
26
31
|
searchParams,
|
|
27
32
|
}: {
|
|
28
|
-
searchParams: Promise<{ page?: string; closed?: string }>
|
|
33
|
+
searchParams: Promise<{ page?: string; closed?: string; category?: string }>
|
|
29
34
|
}) {
|
|
30
35
|
const query = await searchParams
|
|
31
36
|
const actor = await getActor()
|
|
@@ -35,11 +40,15 @@ export default async function ReportsPage({
|
|
|
35
40
|
const scope = await resolveReportScope()
|
|
36
41
|
if (!hasReportScope(scope)) notFound()
|
|
37
42
|
|
|
43
|
+
const activeCategory = parseReportCategory(query.category)
|
|
38
44
|
const service = new ReportService({ reports })
|
|
39
45
|
const pageNumber = readPage(query)
|
|
40
46
|
const [page, open] = await Promise.all([
|
|
41
|
-
service.listOpen(scope, {
|
|
42
|
-
|
|
47
|
+
service.listOpen(scope, {
|
|
48
|
+
offset: offsetOf(pageNumber, REPORTS_PAGE_SIZE),
|
|
49
|
+
...(activeCategory === null ? {} : { category: activeCategory }),
|
|
50
|
+
}),
|
|
51
|
+
service.countOpen(scope, activeCategory ?? undefined),
|
|
43
52
|
])
|
|
44
53
|
|
|
45
54
|
const reportedMessages = await reportedPrivateMessages(page.rows)
|
|
@@ -62,6 +71,35 @@ export default async function ReportsPage({
|
|
|
62
71
|
<BoardNotice kind="info" message={notice} dismissHref="/moderation/reports" />
|
|
63
72
|
)}
|
|
64
73
|
|
|
74
|
+
{(open > 0 || activeCategory !== null) && (
|
|
75
|
+
<form method="get" action="/moderation/reports" className="flex flex-wrap items-end gap-2">
|
|
76
|
+
<div className="flex flex-col gap-1 text-xs">
|
|
77
|
+
<label htmlFor="report-category-filter" className="font-medium text-muted-foreground">
|
|
78
|
+
{translator.t('board.reports.filterCategory')}
|
|
79
|
+
</label>
|
|
80
|
+
<NativeSelect
|
|
81
|
+
id="report-category-filter"
|
|
82
|
+
name="category"
|
|
83
|
+
defaultValue={activeCategory ?? ''}
|
|
84
|
+
className="w-48"
|
|
85
|
+
>
|
|
86
|
+
<option value="">{translator.t('board.reports.filterAll')}</option>
|
|
87
|
+
{REPORT_CATEGORIES.map((category) => (
|
|
88
|
+
<option key={category} value={category}>
|
|
89
|
+
{translator.t(REPORT_CATEGORY_LABEL_KEYS[category])}
|
|
90
|
+
</option>
|
|
91
|
+
))}
|
|
92
|
+
</NativeSelect>
|
|
93
|
+
</div>
|
|
94
|
+
<button
|
|
95
|
+
type="submit"
|
|
96
|
+
className="inline-flex h-9 items-center rounded-md border border-border px-3 text-sm font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
97
|
+
>
|
|
98
|
+
{translator.t('board.reports.filterApply')}
|
|
99
|
+
</button>
|
|
100
|
+
</form>
|
|
101
|
+
)}
|
|
102
|
+
|
|
65
103
|
{page.rows.length === 0 && (
|
|
66
104
|
<Card>
|
|
67
105
|
<Empty className="py-8">
|
|
@@ -96,15 +134,13 @@ export default async function ReportsPage({
|
|
|
96
134
|
? translator.t('board.reports.privateMessage')
|
|
97
135
|
: report.kind}
|
|
98
136
|
</span>
|
|
137
|
+
<Badge tone="neutral">
|
|
138
|
+
{translator.t(REPORT_CATEGORY_LABEL_KEYS[report.category])}
|
|
139
|
+
</Badge>
|
|
99
140
|
{href === null ? (
|
|
100
141
|
<span className="font-medium">{report.targetLabel}</span>
|
|
101
142
|
) : (
|
|
102
|
-
<
|
|
103
|
-
href={href}
|
|
104
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
105
|
-
>
|
|
106
|
-
{report.targetLabel}
|
|
107
|
-
</a>
|
|
143
|
+
<TextLink href={href}>{report.targetLabel}</TextLink>
|
|
108
144
|
)}
|
|
109
145
|
<span className="text-xs text-muted-foreground">
|
|
110
146
|
{translator.t('board.reports.reportedBy', {
|
|
@@ -115,9 +151,11 @@ export default async function ReportsPage({
|
|
|
115
151
|
</span>
|
|
116
152
|
</div>
|
|
117
153
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
{report.reason !== '' && (
|
|
155
|
+
<p className="mt-2 whitespace-pre-wrap break-words text-sm leading-relaxed">
|
|
156
|
+
{report.reason}
|
|
157
|
+
</p>
|
|
158
|
+
)}
|
|
121
159
|
|
|
122
160
|
{report.kind === 'private_message' && (
|
|
123
161
|
<div className="mt-2 rounded-md border border-border bg-muted/50 p-3">
|
|
@@ -2,7 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
4
|
import { WarningService } from '@meith/moderation'
|
|
5
|
-
import { Card, Empty, EmptyDescription, EmptyTitle } from '@meith/ui'
|
|
5
|
+
import { Card, Empty, EmptyDescription, EmptyTitle, TextLink } from '@meith/ui'
|
|
6
6
|
|
|
7
7
|
import { IssueWarningForm, RevokeWarningForm } from '@/components/moderation/warning-forms'
|
|
8
8
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
@@ -145,12 +145,9 @@ export default async function WarnPage({
|
|
|
145
145
|
)}
|
|
146
146
|
|
|
147
147
|
{view.nextHref !== null && (
|
|
148
|
-
<
|
|
149
|
-
href={view.nextHref}
|
|
150
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
151
|
-
>
|
|
148
|
+
<TextLink href={view.nextHref} size="sm">
|
|
152
149
|
{await tr('page.older-warnings')}
|
|
153
|
-
</
|
|
150
|
+
</TextLink>
|
|
154
151
|
)}
|
|
155
152
|
</PanelSection>
|
|
156
153
|
</PanelPage>
|
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
4
|
import { NOTIFICATIONS_PAGE_SIZE } from '@meith/notifications'
|
|
5
|
+
import { TextLink } from '@meith/ui'
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
MarkAllNotificationsReadForm,
|
|
@@ -60,12 +61,9 @@ export default async function NotificationsPage({
|
|
|
60
61
|
lede={
|
|
61
62
|
<>
|
|
62
63
|
{translator.t('board.notifications.unread', { count: view.unread })}{' '}
|
|
63
|
-
<
|
|
64
|
-
href={view.preferencesHref}
|
|
65
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
66
|
-
>
|
|
64
|
+
<TextLink href={view.preferencesHref}>
|
|
67
65
|
{await tr('page.choose-which-these-receive-by')}
|
|
68
|
-
</
|
|
66
|
+
</TextLink>
|
|
69
67
|
.
|
|
70
68
|
</>
|
|
71
69
|
}
|
|
@@ -115,12 +113,9 @@ export default async function NotificationsPage({
|
|
|
115
113
|
|
|
116
114
|
<div className="mt-3 flex items-center gap-4">
|
|
117
115
|
{row.href !== null && (
|
|
118
|
-
<
|
|
119
|
-
href={row.href}
|
|
120
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
121
|
-
>
|
|
116
|
+
<TextLink href={row.href} size="sm">
|
|
122
117
|
{translator.t('board.notifications.view')}
|
|
123
|
-
</
|
|
118
|
+
</TextLink>
|
|
124
119
|
)}
|
|
125
120
|
{!row.isRead && (
|
|
126
121
|
<MarkNotificationReadForm
|
|
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation'
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
AnnouncementsOptInForm,
|
|
6
|
+
BoardDigestCadenceForm,
|
|
6
7
|
NotificationPreferencesForm,
|
|
7
8
|
} from '@/components/account/notification-forms'
|
|
8
9
|
import { PushDeviceForm } from '@/components/account/push-device-form'
|
|
@@ -68,6 +69,13 @@ export default async function NotificationPreferencesPage({
|
|
|
68
69
|
copy={notificationFormsCopy(await getTranslator())}
|
|
69
70
|
/>
|
|
70
71
|
|
|
72
|
+
{settings !== null && (
|
|
73
|
+
<BoardDigestCadenceForm
|
|
74
|
+
cadence={settings.boardDigestCadence}
|
|
75
|
+
copy={notificationFormsCopy(await getTranslator())}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
|
|
71
79
|
{settings !== null && (
|
|
72
80
|
<AnnouncementsOptInForm
|
|
73
81
|
optedIn={settings.massMailOptInAt !== null}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardFooter,
|
|
6
|
+
CardRows,
|
|
7
|
+
Empty,
|
|
8
|
+
EmptyDescription,
|
|
9
|
+
EmptyTitle,
|
|
10
|
+
TextLink,
|
|
11
|
+
} from '@meith/ui'
|
|
4
12
|
|
|
5
13
|
import { PanelPage } from '@/components/shell/panel-page'
|
|
6
14
|
import { activeWordFilter } from '@/server/content-admin'
|
|
@@ -72,14 +80,12 @@ export default async function OnlinePage() {
|
|
|
72
80
|
className="flex flex-col gap-1 px-4 py-3 sm:flex-row sm:items-baseline sm:justify-between sm:gap-4"
|
|
73
81
|
>
|
|
74
82
|
<span className="text-sm font-medium">
|
|
75
|
-
<
|
|
83
|
+
<TextLink
|
|
76
84
|
href={`/member/${member.userId}`}
|
|
77
|
-
className={
|
|
78
|
-
identities.get(member.userId)?.nameClass ?? ''
|
|
79
|
-
}`}
|
|
85
|
+
className={identities.get(member.userId)?.nameClass ?? ''}
|
|
80
86
|
>
|
|
81
87
|
{member.username}
|
|
82
|
-
</
|
|
88
|
+
</TextLink>
|
|
83
89
|
{member.invisible && (
|
|
84
90
|
<span className="ml-1 text-xs text-muted-foreground">
|
|
85
91
|
{translator.t('board.online.invisible')}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Empty,
|
|
9
9
|
EmptyDescription,
|
|
10
10
|
EmptyTitle,
|
|
11
|
+
TextLink,
|
|
11
12
|
} from '@meith/ui'
|
|
12
13
|
|
|
13
14
|
import { PanelPage } from '@/components/shell/panel-page'
|
|
@@ -62,14 +63,12 @@ export default async function StaffPage() {
|
|
|
62
63
|
<CardRows>
|
|
63
64
|
{group.members.map((member) => (
|
|
64
65
|
<li key={member.id} className="px-4 py-3 text-sm">
|
|
65
|
-
<
|
|
66
|
+
<TextLink
|
|
66
67
|
href={`/member/${member.id}`}
|
|
67
|
-
className={
|
|
68
|
-
identities.get(member.id)?.nameClass ?? ''
|
|
69
|
-
}`}
|
|
68
|
+
className={identities.get(member.id)?.nameClass ?? ''}
|
|
70
69
|
>
|
|
71
70
|
{member.username}
|
|
72
|
-
</
|
|
71
|
+
</TextLink>
|
|
73
72
|
</li>
|
|
74
73
|
))}
|
|
75
74
|
</CardRows>
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardRows,
|
|
7
|
+
Empty,
|
|
8
|
+
EmptyDescription,
|
|
9
|
+
EmptyTitle,
|
|
10
|
+
textLinkVariants,
|
|
11
|
+
} from '@meith/ui'
|
|
4
12
|
|
|
5
13
|
import { PanelPage, PanelSection } from '@/components/shell/panel-page'
|
|
6
14
|
import { getActor } from '@/server/context'
|
|
@@ -13,8 +21,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
13
21
|
return { title: await tr('page.board-statistics') }
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
const LINK =
|
|
17
|
-
'font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground'
|
|
24
|
+
const LINK = textLinkVariants()
|
|
18
25
|
|
|
19
26
|
const ROW = 'flex items-baseline justify-between gap-4 px-4 py-2.5 text-sm'
|
|
20
27
|
|