@meith/web 0.30.1 → 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
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
4
|
import { SubscriptionService } from '@meith/subscriptions'
|
|
5
|
+
import { TextLink } from '@meith/ui'
|
|
5
6
|
|
|
6
7
|
import { SubscriptionRowForm } from '@/components/account/subscription-forms'
|
|
7
8
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
@@ -52,12 +53,9 @@ export default async function SubscriptionsPage({
|
|
|
52
53
|
lede={
|
|
53
54
|
<>
|
|
54
55
|
What you follow, and how often you hear about it.{' '}
|
|
55
|
-
<
|
|
56
|
-
href="/notifications/preferences"
|
|
57
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
58
|
-
>
|
|
56
|
+
<TextLink href="/notifications/preferences">
|
|
59
57
|
{await tr('page.whether-any-it-also-arrives')}
|
|
60
|
-
</
|
|
58
|
+
</TextLink>{' '}
|
|
61
59
|
is a separate setting.
|
|
62
60
|
</>
|
|
63
61
|
}
|
|
@@ -120,12 +118,9 @@ function Section({
|
|
|
120
118
|
{rows.map((row) => (
|
|
121
119
|
<li key={row.key} className="rounded-lg border border-border bg-card p-4">
|
|
122
120
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
123
|
-
<
|
|
124
|
-
href={row.href}
|
|
125
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
126
|
-
>
|
|
121
|
+
<TextLink href={row.href} size="sm">
|
|
127
122
|
{row.title}
|
|
128
|
-
</
|
|
123
|
+
</TextLink>
|
|
129
124
|
<span className="text-xs text-muted-foreground">
|
|
130
125
|
{since[0]}
|
|
131
126
|
<time dateTime={row.since.iso}>{row.since.label}</time>
|
|
@@ -4,11 +4,18 @@ import { notFound } from 'next/navigation'
|
|
|
4
4
|
import { requireSlot, slotCopy } from '@meith/theme-kit'
|
|
5
5
|
|
|
6
6
|
import { DeletePostForm, EditPostForm, RestorePostForm } from '@/components/content/edit-post-form'
|
|
7
|
+
import {
|
|
8
|
+
attachmentLimits as attachmentLimitsFor,
|
|
9
|
+
attachmentsForPosts,
|
|
10
|
+
canAttach,
|
|
11
|
+
resolveEditAttachmentScope,
|
|
12
|
+
} from '@/server/attachments'
|
|
7
13
|
import { getActor } from '@/server/context'
|
|
8
14
|
import { getTranslator, tr } from '@/server/i18n'
|
|
9
15
|
import { filterView, viewerRef } from '@/server/plugin-view'
|
|
10
16
|
import { resolvePostScope } from '@/server/post-scope'
|
|
11
17
|
import { currentTheme } from '@/server/theme'
|
|
18
|
+
import { editableAttachment } from '@/view/attachments'
|
|
12
19
|
import { editPostFormCopy } from '@/view/content-copy'
|
|
13
20
|
import { buildEditorToolbarModel } from '@/view/editor-toolbar'
|
|
14
21
|
import { buildEditView } from '@/view/post-form'
|
|
@@ -73,6 +80,18 @@ export default async function EditPostPage({
|
|
|
73
80
|
<EditorToolbar {...toolbarModel} copy={slotCopy(theme, 'EditorToolbar', translator)} />
|
|
74
81
|
)
|
|
75
82
|
|
|
83
|
+
const editingActor = await getActor()
|
|
84
|
+
const attachmentScope = editable
|
|
85
|
+
? await resolveEditAttachmentScope(editingActor, scope.target.forum.id)
|
|
86
|
+
: null
|
|
87
|
+
const existingAttachments = editable
|
|
88
|
+
? (await attachmentsForPosts([post])).map(editableAttachment)
|
|
89
|
+
: []
|
|
90
|
+
const editAttachmentLimits =
|
|
91
|
+
attachmentScope !== null && canAttach(editingActor, attachmentScope)
|
|
92
|
+
? attachmentLimitsFor(attachmentScope)
|
|
93
|
+
: null
|
|
94
|
+
|
|
76
95
|
const formModel = await filterView(
|
|
77
96
|
'view.post-form',
|
|
78
97
|
{
|
|
@@ -89,6 +108,8 @@ export default async function EditPostPage({
|
|
|
89
108
|
postId={post}
|
|
90
109
|
message={scope.target.post.message}
|
|
91
110
|
reason={null}
|
|
111
|
+
attachments={existingAttachments}
|
|
112
|
+
attachmentLimits={editAttachmentLimits}
|
|
92
113
|
toolbar={editorToolbar}
|
|
93
114
|
/>
|
|
94
115
|
)}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { activeWordFilter } from '@/server/content-admin'
|
|
2
2
|
import { feedFor } from '@/server/feed-builder'
|
|
3
3
|
import { feedResponse, noFeed, offlineFeed } from '@/server/feed-routes'
|
|
4
|
-
import {
|
|
4
|
+
import { feedScopeForRequest } from '@/server/feed-token'
|
|
5
|
+
import { FEED_LIMIT, feedRepository, origin } from '@/server/syndication'
|
|
5
6
|
import { leadingId } from '@/view/slug-id'
|
|
6
7
|
|
|
7
8
|
export const dynamic = 'force-dynamic'
|
|
8
9
|
|
|
9
10
|
export async function GET(
|
|
10
|
-
|
|
11
|
+
request: Request,
|
|
11
12
|
{ params }: { params: Promise<{ slug: string }> },
|
|
12
13
|
): Promise<Response> {
|
|
13
14
|
const offline = await offlineFeed()
|
|
@@ -20,7 +21,8 @@ export async function GET(
|
|
|
20
21
|
const id = leadingId(slug)
|
|
21
22
|
if (id === null) return noFeed()
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const { scope, tokened } = await feedScopeForRequest(request)
|
|
25
|
+
const posts = await repo.recentPosts(id, FEED_LIMIT, scope)
|
|
24
26
|
if (posts.length === 0) return noFeed()
|
|
25
27
|
|
|
26
28
|
const first = posts[0]!
|
|
@@ -38,5 +40,6 @@ export async function GET(
|
|
|
38
40
|
}),
|
|
39
41
|
'rss',
|
|
40
42
|
'thread',
|
|
43
|
+
{ private: tokened },
|
|
41
44
|
)
|
|
42
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { notFound, redirect } from 'next/navigation'
|
|
3
|
+
import { after as scheduleAfterResponse } from 'next/server'
|
|
3
4
|
|
|
4
5
|
import { currentRequestId } from '@meith/core/logger'
|
|
5
6
|
import { acceptsThreads, canHoldThreads } from '@meith/forums'
|
|
@@ -19,6 +20,7 @@ import { ThreadToolsForm } from '@/components/moderation/thread-tools-form'
|
|
|
19
20
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
20
21
|
import { BOARD_MEASURE } from '@/components/shell/measure'
|
|
21
22
|
import { attachmentLimits, attachmentsForPosts, canAttach } from '@/server/attachments'
|
|
23
|
+
import { autoWatchChecksByDefault } from '@/server/auto-watch'
|
|
22
24
|
import { avatarsFor } from '@/server/avatars'
|
|
23
25
|
import { getContainer } from '@/server/container'
|
|
24
26
|
import { activeVocabulary, activeWordFilter } from '@/server/content-admin'
|
|
@@ -30,12 +32,14 @@ import { cspNonce } from '@/server/nonce'
|
|
|
30
32
|
import { pageMetadata } from '@/server/page-metadata'
|
|
31
33
|
import { filterView, pluginRegion, viewerRef } from '@/server/plugin-view'
|
|
32
34
|
import { resolvePollScope } from '@/server/poll-scope'
|
|
35
|
+
import { presenceFor } from '@/server/presence'
|
|
33
36
|
import { postbitProfileFields } from '@/server/profile-fields'
|
|
34
37
|
import { viewerIgnoredIds } from '@/server/relations'
|
|
35
38
|
import { reputationSettings, thanksForPosts } from '@/server/reputation'
|
|
36
39
|
import { getSettings } from '@/server/settings'
|
|
37
40
|
import { signaturesFor } from '@/server/signatures'
|
|
38
41
|
import { currentTheme } from '@/server/theme'
|
|
42
|
+
import { resolveUnreadGoto } from '@/server/unread-goto'
|
|
39
43
|
import { getViewerPreferences } from '@/server/viewer-preferences'
|
|
40
44
|
import { followFormCopy } from '@/view/account-copy'
|
|
41
45
|
import { attachmentsByPost } from '@/view/attachments'
|
|
@@ -171,6 +175,7 @@ export default async function ThreadPage({
|
|
|
171
175
|
replied?: string
|
|
172
176
|
posted?: string
|
|
173
177
|
post?: string
|
|
178
|
+
goto?: string
|
|
174
179
|
removed?: string
|
|
175
180
|
unchanged?: string
|
|
176
181
|
tool?: string
|
|
@@ -194,6 +199,7 @@ export default async function ThreadPage({
|
|
|
194
199
|
posts,
|
|
195
200
|
threads,
|
|
196
201
|
authorizer,
|
|
202
|
+
readState,
|
|
197
203
|
threadViews,
|
|
198
204
|
threadWrites,
|
|
199
205
|
postWrites,
|
|
@@ -236,6 +242,19 @@ export default async function ThreadPage({
|
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
if (query.goto === 'unread' && actor.userId !== null && readState !== null) {
|
|
246
|
+
const location = await resolveUnreadGoto(
|
|
247
|
+
posts,
|
|
248
|
+
readState,
|
|
249
|
+
actor.userId,
|
|
250
|
+
{ id: thread.id, forumId: forum.id, lastPostId: thread.lastPost?.postId ?? null },
|
|
251
|
+
{ scope, pageSize: preferences.postsPerPage },
|
|
252
|
+
)
|
|
253
|
+
if (location !== null) {
|
|
254
|
+
redirect(locatedHref(`/thread/${thread.id}-${thread.slug}`, query, location))
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
239
258
|
if (threadViews && after === undefined) {
|
|
240
259
|
await threadViews.record(thread.id).catch(() => undefined)
|
|
241
260
|
}
|
|
@@ -248,6 +267,18 @@ export default async function ThreadPage({
|
|
|
248
267
|
scope,
|
|
249
268
|
})
|
|
250
269
|
|
|
270
|
+
if (readState !== null && actor.userId !== null) {
|
|
271
|
+
const readerId = actor.userId
|
|
272
|
+
const lastRenderedPostId = postPage.rows.at(-1)?.id
|
|
273
|
+
if (lastRenderedPostId !== undefined) {
|
|
274
|
+
scheduleAfterResponse(() =>
|
|
275
|
+
readState
|
|
276
|
+
.markThreadRead(readerId, thread.id, lastRenderedPostId, new Date())
|
|
277
|
+
.catch(() => undefined),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
251
282
|
const pager = buildOffsetPager({
|
|
252
283
|
path: `/thread/${thread.id}-${thread.slug}`,
|
|
253
284
|
params: query,
|
|
@@ -350,12 +381,25 @@ export default async function ThreadPage({
|
|
|
350
381
|
|
|
351
382
|
const identities = await identitiesFor(authorIds)
|
|
352
383
|
|
|
384
|
+
const presence = await presenceFor(authorIds)
|
|
385
|
+
|
|
353
386
|
const attachments = attachmentsByPost(
|
|
354
387
|
await attachmentsForPosts(postPage.rows.map((row) => row.id)),
|
|
355
388
|
)
|
|
356
389
|
|
|
357
390
|
const wordFilter = await activeWordFilter()
|
|
358
391
|
|
|
392
|
+
const { subscriptions } = getContainer()
|
|
393
|
+
const followMode =
|
|
394
|
+
subscriptions === null || actor.userId === null
|
|
395
|
+
? null
|
|
396
|
+
: await subscriptions.modeFor(actor.userId, 'thread', thread.id)
|
|
397
|
+
const followOffered = subscriptions !== null && actor.userId !== null
|
|
398
|
+
const followModes = buildSubscriptionsView({
|
|
399
|
+
rows: [],
|
|
400
|
+
now: new Date(),
|
|
401
|
+
}).modes
|
|
402
|
+
|
|
359
403
|
const view = buildThreadView({
|
|
360
404
|
thread,
|
|
361
405
|
wordFilter,
|
|
@@ -371,6 +415,8 @@ export default async function ThreadPage({
|
|
|
371
415
|
actor.userId === null || postPage.rows.at(-1) === undefined
|
|
372
416
|
? null
|
|
373
417
|
: `/api/read/thread/${thread.id}?post=${postPage.rows.at(-1)!.id}`,
|
|
418
|
+
watchOffered: followOffered,
|
|
419
|
+
watchMode: followMode,
|
|
374
420
|
now: new Date(),
|
|
375
421
|
t: await getTranslator(),
|
|
376
422
|
authorFields,
|
|
@@ -378,6 +424,8 @@ export default async function ThreadPage({
|
|
|
378
424
|
attachments,
|
|
379
425
|
avatars,
|
|
380
426
|
identities,
|
|
427
|
+
presence,
|
|
428
|
+
viewerSeesInvisible: authorizer.can(actor, 'modcp.access'),
|
|
381
429
|
ignoredIds,
|
|
382
430
|
revealedPostIds: revealedFrom(query.reveal),
|
|
383
431
|
currentHref:
|
|
@@ -385,17 +433,6 @@ export default async function ThreadPage({
|
|
|
385
433
|
(after === undefined ? `?page=${page}` : `?after=${after}&page=${page}`),
|
|
386
434
|
})
|
|
387
435
|
|
|
388
|
-
const { subscriptions } = getContainer()
|
|
389
|
-
const followMode =
|
|
390
|
-
subscriptions === null || actor.userId === null
|
|
391
|
-
? null
|
|
392
|
-
: await subscriptions.modeFor(actor.userId, 'thread', thread.id)
|
|
393
|
-
const followOffered = subscriptions !== null && actor.userId !== null
|
|
394
|
-
const followModes = buildSubscriptionsView({
|
|
395
|
-
rows: [],
|
|
396
|
-
now: new Date(),
|
|
397
|
-
}).modes
|
|
398
|
-
|
|
399
436
|
const theme = await currentTheme()
|
|
400
437
|
const ThreadView = requireSlot(theme, 'ThreadView')
|
|
401
438
|
const Navigation = requireSlot(theme, 'Navigation')
|
|
@@ -548,6 +585,7 @@ export default async function ThreadPage({
|
|
|
548
585
|
seenLastPostId={thread.lastPost?.postId ?? null}
|
|
549
586
|
prefill=""
|
|
550
587
|
canSubscribe={authorizer.can(actor, 'forum.subscribe', replyTarget)}
|
|
588
|
+
subscribeDefault={autoWatchChecksByDefault(preferences.autoWatchRepliedThreads)}
|
|
551
589
|
attachmentLimits={quickReplyAttachable ? attachmentLimits(replyTarget) : null}
|
|
552
590
|
draft={
|
|
553
591
|
actor.userId === null || drafts === null
|
|
@@ -8,6 +8,7 @@ import { MultiQuoteSelection } from '@/components/content/multiquote-selection'
|
|
|
8
8
|
import { ReplyForm } from '@/components/content/reply-form'
|
|
9
9
|
import { OnboardingBanner } from '@/components/shell/onboarding-banner'
|
|
10
10
|
import { attachmentLimits, canAttach } from '@/server/attachments'
|
|
11
|
+
import { autoWatchChecksByDefault, autoWatchPreference } from '@/server/auto-watch'
|
|
11
12
|
import { getContainer } from '@/server/container'
|
|
12
13
|
import { activeWordFilter } from '@/server/content-admin'
|
|
13
14
|
import { getActor } from '@/server/context'
|
|
@@ -86,6 +87,9 @@ export default async function ReplyPage({
|
|
|
86
87
|
const EditorToolbar = requireSlot(theme, 'EditorToolbar')
|
|
87
88
|
const translator = await getTranslator()
|
|
88
89
|
const attachable = canAttach(actor, scope)
|
|
90
|
+
const subscribeDefault = autoWatchChecksByDefault(
|
|
91
|
+
await autoWatchPreference(actor.userId, 'reply'),
|
|
92
|
+
)
|
|
89
93
|
const toolbarModel = locked
|
|
90
94
|
? null
|
|
91
95
|
: await filterView(
|
|
@@ -113,6 +117,7 @@ export default async function ReplyPage({
|
|
|
113
117
|
seenLastPostId={target.lastPostId}
|
|
114
118
|
prefill={prefill}
|
|
115
119
|
canSubscribe={authorizer.can(actor, 'forum.subscribe', scope)}
|
|
120
|
+
subscribeDefault={subscribeDefault}
|
|
116
121
|
attachmentLimits={attachable ? attachmentLimits(scope) : null}
|
|
117
122
|
draft={
|
|
118
123
|
actor.userId === null || drafts === null
|
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
|
|
3
3
|
import { env } from '@meith/core'
|
|
4
4
|
import { readUnsubscribeToken } from '@meith/subscriptions'
|
|
5
|
+
import { TextLink } from '@meith/ui'
|
|
5
6
|
|
|
6
7
|
import { UnsubscribeConfirmForm } from '@/components/account/subscription-forms'
|
|
7
8
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
@@ -36,24 +37,18 @@ export default async function UnsubscribePage({
|
|
|
36
37
|
<BoardNotice kind="info" message={done} dismissHref="/" />
|
|
37
38
|
<p className="text-sm text-muted-foreground">
|
|
38
39
|
{translator.t('unsubscribePage.doneBefore')}{' '}
|
|
39
|
-
<
|
|
40
|
-
href="/subscriptions"
|
|
41
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
42
|
-
>
|
|
40
|
+
<TextLink href="/subscriptions">
|
|
43
41
|
{translator.t('unsubscribePage.subscriptions')}
|
|
44
|
-
</
|
|
42
|
+
</TextLink>
|
|
45
43
|
{translator.t('unsubscribePage.doneAfter')}
|
|
46
44
|
</p>
|
|
47
45
|
</>
|
|
48
46
|
) : claim === null ? (
|
|
49
47
|
<p className="text-sm text-muted-foreground">
|
|
50
48
|
{translator.t('unsubscribePage.invalidBefore')}{' '}
|
|
51
|
-
<
|
|
52
|
-
href="/subscriptions"
|
|
53
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
54
|
-
>
|
|
49
|
+
<TextLink href="/subscriptions">
|
|
55
50
|
{translator.t('unsubscribePage.subscriptions')}
|
|
56
|
-
</
|
|
51
|
+
</TextLink>{' '}
|
|
57
52
|
{translator.t('unsubscribePage.invalidAfter')}
|
|
58
53
|
</p>
|
|
59
54
|
) : (
|
|
@@ -64,7 +59,9 @@ export default async function UnsubscribePage({
|
|
|
64
59
|
? await tr('page.unsubscribe.massMailScope')
|
|
65
60
|
: claim.scope === 'email'
|
|
66
61
|
? await tr('page.unsubscribe.emailScope')
|
|
67
|
-
:
|
|
62
|
+
: claim.scope === 'board-digest'
|
|
63
|
+
? await tr('page.unsubscribe.boardDigestScope')
|
|
64
|
+
: await tr('page.unsubscribe.targetScope')
|
|
68
65
|
}
|
|
69
66
|
copy={followFormCopy(translator)}
|
|
70
67
|
/>
|
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
4
|
import { MAX_RELATIONS } from '@meith/relations'
|
|
5
|
+
import { TextLink } from '@meith/ui'
|
|
5
6
|
|
|
6
7
|
import { RemoveRelationForm } from '@/components/account/relation-forms'
|
|
7
8
|
import { BoardNotice } from '@/components/shell/board-notice'
|
|
@@ -123,12 +124,9 @@ async function ContactLine({ row }: { row: ContactRowView }) {
|
|
|
123
124
|
|
|
124
125
|
<span className="ml-auto flex items-center gap-4">
|
|
125
126
|
{row.messageHref !== null && (
|
|
126
|
-
<
|
|
127
|
-
href={row.messageHref}
|
|
128
|
-
className="text-sm font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
129
|
-
>
|
|
127
|
+
<TextLink href={row.messageHref} size="sm">
|
|
130
128
|
{translator.t('board.contacts.message')}
|
|
131
|
-
</
|
|
129
|
+
</TextLink>
|
|
132
130
|
)}
|
|
133
131
|
<RemoveRelationForm userId={row.userId} username={row.username} returnTo={RETURN_TO} />
|
|
134
132
|
</span>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { notFound } from 'next/navigation'
|
|
3
|
+
|
|
4
|
+
import { buttonVariants, TextLink } from '@meith/ui'
|
|
5
|
+
|
|
6
|
+
import { DeleteDraftForm } from '@/components/account/draft-forms'
|
|
7
|
+
import { PanelPage } from '@/components/shell/panel-page'
|
|
8
|
+
import { getContainer } from '@/server/container'
|
|
9
|
+
import { getActor } from '@/server/context'
|
|
10
|
+
import { getTranslator, tr } from '@/server/i18n'
|
|
11
|
+
import { buildDraftsView, draftsPageCopy } from '@/view/drafts'
|
|
12
|
+
|
|
13
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
14
|
+
return { title: await tr('page.drafts') }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function DraftsPage() {
|
|
18
|
+
const actor = await getActor()
|
|
19
|
+
const { drafts } = getContainer()
|
|
20
|
+
if (actor.userId === null || drafts === null) notFound()
|
|
21
|
+
|
|
22
|
+
const translator = await getTranslator()
|
|
23
|
+
const rows = buildDraftsView({
|
|
24
|
+
drafts: await drafts.listByUser(actor.userId),
|
|
25
|
+
now: new Date(),
|
|
26
|
+
t: translator,
|
|
27
|
+
})
|
|
28
|
+
const copy = draftsPageCopy(translator)
|
|
29
|
+
const resumeLabel = await tr('draftsPage.resume')
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<PanelPage title={await tr('page.drafts')} lede={await tr('draftsPage.lede')}>
|
|
33
|
+
{rows.length === 0 ? (
|
|
34
|
+
<p className="rounded-lg border border-border bg-card p-4 text-sm text-muted-foreground">
|
|
35
|
+
{await tr('draftsPage.none')}
|
|
36
|
+
</p>
|
|
37
|
+
) : (
|
|
38
|
+
<ul className="flex flex-col gap-3">
|
|
39
|
+
{rows.map((row) => (
|
|
40
|
+
<li key={row.key} className="rounded-lg border border-border bg-card p-4">
|
|
41
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
42
|
+
<span className="flex flex-col gap-0.5">
|
|
43
|
+
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
44
|
+
{row.kindLabel}
|
|
45
|
+
</span>
|
|
46
|
+
<TextLink href={row.resumeHref} size="sm">
|
|
47
|
+
{row.targetName}
|
|
48
|
+
</TextLink>
|
|
49
|
+
</span>
|
|
50
|
+
<time dateTime={row.updatedAt.iso} className="text-xs text-muted-foreground">
|
|
51
|
+
{row.updatedAt.label}
|
|
52
|
+
</time>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
56
|
+
<a
|
|
57
|
+
href={row.resumeHref}
|
|
58
|
+
className={buttonVariants({ variant: 'primary', size: 'sm' })}
|
|
59
|
+
>
|
|
60
|
+
{resumeLabel}
|
|
61
|
+
</a>
|
|
62
|
+
<DeleteDraftForm forumId={row.forumId} threadId={row.threadId} copy={copy} />
|
|
63
|
+
</div>
|
|
64
|
+
</li>
|
|
65
|
+
))}
|
|
66
|
+
</ul>
|
|
67
|
+
)}
|
|
68
|
+
</PanelPage>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -9,7 +9,13 @@ import { getActor } from '@/server/context'
|
|
|
9
9
|
import { catalogs, getTranslator, tr } from '@/server/i18n'
|
|
10
10
|
import { getSettings } from '@/server/settings'
|
|
11
11
|
import { optionsFormCopy } from '@/view/account-copy'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
autoWatchChoices,
|
|
14
|
+
localeChoices,
|
|
15
|
+
optionsFormValues,
|
|
16
|
+
timezoneChoices,
|
|
17
|
+
userCpNotice,
|
|
18
|
+
} from '@/view/usercp'
|
|
13
19
|
|
|
14
20
|
export async function generateMetadata(): Promise<Metadata> {
|
|
15
21
|
return { title: await tr('page.options') }
|
|
@@ -45,6 +51,7 @@ export default async function OptionsPage({
|
|
|
45
51
|
{...values}
|
|
46
52
|
timezones={timezoneChoices(await getTranslator())}
|
|
47
53
|
locales={localeChoices(catalogs.locales, await getTranslator())}
|
|
54
|
+
autoWatchChoices={autoWatchChoices()}
|
|
48
55
|
copy={optionsFormCopy(
|
|
49
56
|
board.get('display.posts_per_page'),
|
|
50
57
|
board.get('display.threads_per_page'),
|
|
@@ -4,6 +4,7 @@ import { notFound } from 'next/navigation'
|
|
|
4
4
|
import { PASSKEY_LIMIT, providerLabel } from '@meith/accounts'
|
|
5
5
|
|
|
6
6
|
import { ActiveSessions, type SessionView } from '@/components/account/active-sessions'
|
|
7
|
+
import { FeedTokenPanel } from '@/components/account/feed-token'
|
|
7
8
|
import { PasskeyEnrol } from '@/components/account/passkey-enrol'
|
|
8
9
|
import { type ActivityView, SecurityActivity } from '@/components/account/security-activity'
|
|
9
10
|
import {
|
|
@@ -22,6 +23,7 @@ import { memberSecurityActivity } from '@/server/auth-events'
|
|
|
22
23
|
import { getContainer } from '@/server/container'
|
|
23
24
|
import { getActor } from '@/server/context'
|
|
24
25
|
import { memberManagedSignIns, passkeysEnabled, signInProviders } from '@/server/federation'
|
|
26
|
+
import { feedTokenSummary } from '@/server/feed-token'
|
|
25
27
|
import { getTranslator, tr } from '@/server/i18n'
|
|
26
28
|
import { currentSessionId } from '@/server/session-actions'
|
|
27
29
|
import { pendingEnrolment, secondFactorPosture, twoFactorState } from '@/server/two-factor'
|
|
@@ -29,6 +31,8 @@ import {
|
|
|
29
31
|
activeSessionsCopy,
|
|
30
32
|
activityDetailLabel,
|
|
31
33
|
emailFormCopy,
|
|
34
|
+
feedTokenCopy,
|
|
35
|
+
feedTokenDetailLabel,
|
|
32
36
|
linkedUsageLabel,
|
|
33
37
|
passkeyUsageLabel,
|
|
34
38
|
passwordFormCopy,
|
|
@@ -159,6 +163,12 @@ export default async function SecurityPage({
|
|
|
159
163
|
|
|
160
164
|
const anywhereToSignIn = providers.length > 0 || linked.length > 0
|
|
161
165
|
|
|
166
|
+
const feedToken = await feedTokenSummary(actor.userId)
|
|
167
|
+
const feedTokenDetail =
|
|
168
|
+
feedToken === null
|
|
169
|
+
? null
|
|
170
|
+
: feedTokenDetailLabel(on(feedToken.createdAt) ?? '', on(feedToken.lastUsedAt), translator)
|
|
171
|
+
|
|
162
172
|
return (
|
|
163
173
|
<PanelPage
|
|
164
174
|
title={await tr('page.account-security')}
|
|
@@ -211,6 +221,12 @@ export default async function SecurityPage({
|
|
|
211
221
|
|
|
212
222
|
<ActiveSessions sessions={sessionView} copy={activeSessionsCopy(translator)} />
|
|
213
223
|
|
|
224
|
+
<FeedTokenPanel
|
|
225
|
+
active={feedToken !== null}
|
|
226
|
+
detail={feedTokenDetail}
|
|
227
|
+
copy={feedTokenCopy(translator)}
|
|
228
|
+
/>
|
|
229
|
+
|
|
214
230
|
<SecurityActivity events={activity} copy={securityActivityCopy(translator)} />
|
|
215
231
|
</PanelPage>
|
|
216
232
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
|
|
3
|
-
import { cn } from '@meith/ui'
|
|
3
|
+
import { cn, TextLink } from '@meith/ui'
|
|
4
4
|
|
|
5
5
|
import { CaptchaQuestionRowForm, NewCaptchaQuestionForm } from '@/components/admin/content-forms'
|
|
6
6
|
import { PANEL_CARD } from '@/components/shell/panel-list'
|
|
@@ -34,12 +34,9 @@ export default async function AdminAntispamPage() {
|
|
|
34
34
|
lede={
|
|
35
35
|
<>
|
|
36
36
|
The thresholds are in{' '}
|
|
37
|
-
<
|
|
38
|
-
href="/admin/settings?group=antispam"
|
|
39
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
40
|
-
>
|
|
37
|
+
<TextLink href="/admin/settings?group=antispam">
|
|
41
38
|
{await tr('page.settings-anti-spam')}
|
|
42
|
-
</
|
|
39
|
+
</TextLink>
|
|
43
40
|
{translator.t('adminAntispam.ledeEnd')}
|
|
44
41
|
</>
|
|
45
42
|
}
|
|
@@ -115,12 +112,7 @@ export default async function AdminAntispamPage() {
|
|
|
115
112
|
</dt>
|
|
116
113
|
<dd>
|
|
117
114
|
{translator.t('adminAntispam.firstPostsBefore')}{' '}
|
|
118
|
-
<
|
|
119
|
-
href="/modcp"
|
|
120
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
121
|
-
>
|
|
122
|
-
{translator.t('adminAntispam.moderationQueue')}
|
|
123
|
-
</a>
|
|
115
|
+
<TextLink href="/modcp">{translator.t('adminAntispam.moderationQueue')}</TextLink>
|
|
124
116
|
{translator.t('adminAntispam.firstPostsAfter')}
|
|
125
117
|
</dd>
|
|
126
118
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
|
|
3
|
-
import { cn } from '@meith/ui'
|
|
3
|
+
import { cn, TextLink } from '@meith/ui'
|
|
4
4
|
|
|
5
5
|
import { DeleteAttachmentForm } from '@/components/admin/content-forms'
|
|
6
6
|
import { PANEL_CARD, PANEL_LIST, PANEL_NOTE, PANEL_ROW } from '@/components/shell/panel-list'
|
|
@@ -160,12 +160,9 @@ export default async function AdminAttachmentsPage({
|
|
|
160
160
|
{row.threadSlug !== null && (
|
|
161
161
|
<>
|
|
162
162
|
{' · '}
|
|
163
|
-
<
|
|
164
|
-
href={postLink(`/thread/${row.threadSlug}`, row.postId)}
|
|
165
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
166
|
-
>
|
|
163
|
+
<TextLink href={postLink(`/thread/${row.threadSlug}`, row.postId)}>
|
|
167
164
|
{translator.t('adminAttachments.post')}
|
|
168
|
-
</
|
|
165
|
+
</TextLink>
|
|
169
166
|
</>
|
|
170
167
|
)}
|
|
171
168
|
</span>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
|
|
3
|
-
import { cn } from '@meith/ui'
|
|
3
|
+
import { cn, TextLink } from '@meith/ui'
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
DeletePrefixForm,
|
|
@@ -107,23 +107,13 @@ export default async function AdminContentPage() {
|
|
|
107
107
|
<section className={cn(PANEL_CARD, 'gap-2 text-sm')}>
|
|
108
108
|
<h2 className="font-heading text-lg font-semibold">{t.t('contentPage.announcements')}</h2>
|
|
109
109
|
<p className="text-muted-foreground">{t.t('contentPage.announcementsHelp')}</p>
|
|
110
|
-
<
|
|
111
|
-
href="/admin/content/announcements"
|
|
112
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
113
|
-
>
|
|
114
|
-
{await tr('page.announcements-b')}
|
|
115
|
-
</a>
|
|
110
|
+
<TextLink href="/admin/content/announcements">{await tr('page.announcements-b')}</TextLink>
|
|
116
111
|
</section>
|
|
117
112
|
|
|
118
113
|
<section className={cn(PANEL_CARD, 'gap-2 text-sm')}>
|
|
119
114
|
<h2 className="font-heading text-lg font-semibold">{t.t('contentPage.attachments')}</h2>
|
|
120
115
|
<p className="text-muted-foreground">{t.t('contentPage.attachmentsHelp')}</p>
|
|
121
|
-
<
|
|
122
|
-
href="/admin/content/attachments"
|
|
123
|
-
className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
|
|
124
|
-
>
|
|
125
|
-
{await tr('page.attachments-b')}
|
|
126
|
-
</a>
|
|
116
|
+
<TextLink href="/admin/content/attachments">{await tr('page.attachments-b')}</TextLink>
|
|
127
117
|
</section>
|
|
128
118
|
|
|
129
119
|
<section className={PANEL_CARD}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { notFound } from 'next/navigation'
|
|
3
3
|
|
|
4
|
-
import { CopyPermissionsForm,
|
|
4
|
+
import { CopyPermissionsForm, ForumPermissionMatrixForm } from '@/components/admin/forum-forms'
|
|
5
5
|
import { PANEL_CARD } from '@/components/shell/panel-list'
|
|
6
6
|
import { PanelPage } from '@/components/shell/panel-page'
|
|
7
7
|
import { adminPageContext } from '@/server/admin'
|
|
@@ -42,18 +42,15 @@ export default async function ForumPermissionsPage({
|
|
|
42
42
|
</>
|
|
43
43
|
}
|
|
44
44
|
gap="loose"
|
|
45
|
+
width="wide"
|
|
45
46
|
>
|
|
46
|
-
<
|
|
47
|
-
{view.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
copy={forumAdminCopy(t)}
|
|
54
|
-
/>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
47
|
+
<ForumPermissionMatrixForm
|
|
48
|
+
forumId={view.forum.id}
|
|
49
|
+
columns={view.columns}
|
|
50
|
+
fields={view.fields}
|
|
51
|
+
forumTitles={forumTitles}
|
|
52
|
+
copy={forumAdminCopy(t)}
|
|
53
|
+
/>
|
|
57
54
|
|
|
58
55
|
<section className={PANEL_CARD}>
|
|
59
56
|
<h2 className="font-heading text-lg font-semibold">{await tr('page.copy-subforums')}</h2>
|