@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
|
@@ -47,6 +47,29 @@ export function apiTokenFormsCopy(
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function webhookFormsCopy(t: Translator = untranslated()): Readonly<Record<string, string>> {
|
|
51
|
+
return copyFor(
|
|
52
|
+
[
|
|
53
|
+
'adminPanel.webhook.copyNow',
|
|
54
|
+
'adminPanel.webhook.url',
|
|
55
|
+
'adminPanel.webhook.urlPlaceholder',
|
|
56
|
+
'adminPanel.webhook.urlHint',
|
|
57
|
+
'adminPanel.webhook.topics',
|
|
58
|
+
'adminPanel.webhook.topicsHint',
|
|
59
|
+
'adminPanel.webhook.format',
|
|
60
|
+
'adminPanel.webhook.formatHint',
|
|
61
|
+
'adminPanel.webhook.format.json',
|
|
62
|
+
'adminPanel.webhook.format.discord',
|
|
63
|
+
'adminPanel.webhook.active',
|
|
64
|
+
'adminPanel.webhook.create',
|
|
65
|
+
'adminPanel.webhook.enable',
|
|
66
|
+
'adminPanel.webhook.disable',
|
|
67
|
+
'adminPanel.webhook.delete',
|
|
68
|
+
],
|
|
69
|
+
t,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
export function systemFormsCopy(t: Translator = untranslated()): Readonly<Record<string, string>> {
|
|
51
74
|
return {
|
|
52
75
|
...copyFor(
|
package/src/view/attachments.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AttachmentRecord,
|
|
3
|
+
type AttachmentStatus,
|
|
4
|
+
attachmentType,
|
|
5
|
+
} from '@meith/attachments/types'
|
|
2
6
|
import type { PostAttachmentModel } from '@meith/theme-kit'
|
|
3
7
|
|
|
4
8
|
export function formatBytes(bytes: number): string {
|
|
@@ -33,6 +37,34 @@ export function attachmentModel(record: AttachmentRecord): PostAttachmentModel |
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
export interface EditableAttachment {
|
|
41
|
+
readonly id: number
|
|
42
|
+
readonly filename: string
|
|
43
|
+
readonly size: string
|
|
44
|
+
readonly status: AttachmentStatus
|
|
45
|
+
readonly isImage: boolean
|
|
46
|
+
readonly href: string | null
|
|
47
|
+
readonly thumbnailHref: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function editableAttachment(record: AttachmentRecord): EditableAttachment {
|
|
51
|
+
const ready = record.status === 'ready' && record.storageKey !== null
|
|
52
|
+
const type = attachmentType(record.contentType)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id: record.id,
|
|
56
|
+
filename: record.filename,
|
|
57
|
+
size: formatBytes(record.sizeBytes),
|
|
58
|
+
status: record.status,
|
|
59
|
+
isImage: ready && type?.inline === true,
|
|
60
|
+
href: ready ? attachmentHref(record.id) : null,
|
|
61
|
+
thumbnailHref:
|
|
62
|
+
ready && type?.inline === true && record.thumbnailKey !== null
|
|
63
|
+
? thumbnailHref(record.id)
|
|
64
|
+
: null,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
export function attachmentsByPost(
|
|
37
69
|
records: readonly AttachmentRecord[],
|
|
38
70
|
): ReadonlyMap<number, readonly PostAttachmentModel[]> {
|
package/src/view/board-index.ts
CHANGED
|
@@ -64,6 +64,26 @@ function toLastPost(
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export interface ForumRowHydration {
|
|
68
|
+
readonly lastPost: LastPostModel | null
|
|
69
|
+
readonly isUnread: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function hydrateForumRow(
|
|
73
|
+
row: ForumListingRow,
|
|
74
|
+
ownThreadsOnly: boolean,
|
|
75
|
+
unreadForumIds: ReadonlySet<number> | undefined,
|
|
76
|
+
now: Date,
|
|
77
|
+
t: Translator | undefined,
|
|
78
|
+
identities: ReadonlyMap<number, MemberIdentity> | undefined,
|
|
79
|
+
wordFilter: CompiledWordFilter | undefined,
|
|
80
|
+
): ForumRowHydration {
|
|
81
|
+
return {
|
|
82
|
+
lastPost: ownThreadsOnly ? null : toLastPost(row, now, t, identities, wordFilter),
|
|
83
|
+
isUnread: !ownThreadsOnly && (unreadForumIds?.has(row.id) ?? false),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
function toForumRow(
|
|
68
88
|
node: ForumNode<ForumListingRow>,
|
|
69
89
|
now: Date,
|
|
@@ -74,6 +94,15 @@ function toForumRow(
|
|
|
74
94
|
wordFilter: CompiledWordFilter | undefined,
|
|
75
95
|
): ForumRowModel {
|
|
76
96
|
const ownThreadsOnly = ownThreadsOnlyForumIds?.has(node.id) ?? false
|
|
97
|
+
const hydration = hydrateForumRow(
|
|
98
|
+
node,
|
|
99
|
+
ownThreadsOnly,
|
|
100
|
+
unreadForumIds,
|
|
101
|
+
now,
|
|
102
|
+
t,
|
|
103
|
+
identities,
|
|
104
|
+
wordFilter,
|
|
105
|
+
)
|
|
77
106
|
|
|
78
107
|
return {
|
|
79
108
|
id: node.id,
|
|
@@ -83,8 +112,8 @@ function toForumRow(
|
|
|
83
112
|
type: node.type,
|
|
84
113
|
threadCount: count(ownThreadsOnly ? 0 : node.threadCount, t),
|
|
85
114
|
postCount: count(ownThreadsOnly ? 0 : node.postCount, t),
|
|
86
|
-
lastPost:
|
|
87
|
-
isUnread:
|
|
115
|
+
lastPost: hydration.lastPost,
|
|
116
|
+
isUnread: hydration.isUnread,
|
|
88
117
|
subforums: node.children.map(
|
|
89
118
|
(child): LinkModel => ({ label: child.title, href: hrefFor(child) }),
|
|
90
119
|
),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BOARD_TITLE = 'Meith'
|
package/src/view/content-copy.ts
CHANGED
|
@@ -28,18 +28,25 @@ export function replyFormCopy(t: Translator = untranslated()): Readonly<Record<s
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function editPostFormCopy(t: Translator = untranslated()): Readonly<Record<string, string>> {
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
return {
|
|
32
|
+
...copyFor(
|
|
33
|
+
[
|
|
34
|
+
'composer.edit.reason',
|
|
35
|
+
'composer.edit.submit',
|
|
36
|
+
'composer.edit.delete',
|
|
37
|
+
'composer.edit.deleteBlurb',
|
|
38
|
+
'composer.edit.restore',
|
|
39
|
+
'composer.edit.restoreBlurb',
|
|
40
|
+
'composer.edit.attachments',
|
|
41
|
+
'composer.edit.remove',
|
|
42
|
+
'composer.edit.attachmentProcessing',
|
|
43
|
+
'composer.edit.attachmentFailed',
|
|
44
|
+
'composer.preview',
|
|
45
|
+
],
|
|
46
|
+
t,
|
|
47
|
+
),
|
|
48
|
+
...patternCopy(['composer.edit.removeAttachment'], t),
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export function newThreadFormCopy(
|
|
@@ -63,7 +70,10 @@ export function newThreadFormCopy(
|
|
|
63
70
|
'composer.newThread.pollChoicesHint',
|
|
64
71
|
'composer.newThread.pollRevote',
|
|
65
72
|
'composer.newThread.pollPublic',
|
|
73
|
+
'composer.newThread.morePollOptions',
|
|
66
74
|
'composer.newThread.submit',
|
|
75
|
+
'pollEdit.closesAt',
|
|
76
|
+
'pollEdit.closesAtHint',
|
|
67
77
|
],
|
|
68
78
|
t,
|
|
69
79
|
),
|
package/src/view/copy.ts
CHANGED
|
@@ -23,6 +23,10 @@ export function patternCopy(
|
|
|
23
23
|
const CHROME_KEYS = [
|
|
24
24
|
'form.working',
|
|
25
25
|
'form.notSaved',
|
|
26
|
+
'confirm.confirm',
|
|
27
|
+
'confirm.cancel',
|
|
28
|
+
'confirm.heading',
|
|
29
|
+
'notice.dismiss',
|
|
26
30
|
'composer.message',
|
|
27
31
|
'composer.tabs',
|
|
28
32
|
'composer.write',
|
|
@@ -36,37 +40,48 @@ const CHROME_KEYS = [
|
|
|
36
40
|
'composer.tool.italic',
|
|
37
41
|
'composer.tool.strikethrough',
|
|
38
42
|
'composer.tool.link',
|
|
43
|
+
'composer.tool.image',
|
|
39
44
|
'composer.tool.quote',
|
|
40
45
|
'composer.tool.code',
|
|
41
46
|
'composer.tool.bulletedList',
|
|
42
47
|
'composer.tool.numberedList',
|
|
48
|
+
'composer.tool.taskList',
|
|
43
49
|
'composer.tool.heading',
|
|
50
|
+
'composer.tool.table',
|
|
44
51
|
'composer.tool.spoiler',
|
|
45
52
|
'composer.tool.attachment',
|
|
46
53
|
'composer.placeholder.bold',
|
|
47
54
|
'composer.placeholder.italic',
|
|
48
55
|
'composer.placeholder.struck',
|
|
49
56
|
'composer.placeholder.spoiler',
|
|
57
|
+
'composer.placeholder.image',
|
|
58
|
+
'composer.placeholder.table',
|
|
50
59
|
'composer.help.summary',
|
|
51
60
|
'composer.help.bold',
|
|
52
61
|
'composer.help.italic',
|
|
53
62
|
'composer.help.struck',
|
|
54
63
|
'composer.help.link',
|
|
64
|
+
'composer.help.image',
|
|
55
65
|
'composer.help.codeInline',
|
|
56
66
|
'composer.help.codeBlock',
|
|
57
67
|
'composer.help.quote',
|
|
58
68
|
'composer.help.list',
|
|
69
|
+
'composer.help.taskList',
|
|
59
70
|
'composer.help.heading',
|
|
71
|
+
'composer.help.table',
|
|
60
72
|
'composer.help.note',
|
|
61
73
|
'composer.help.spoiler',
|
|
62
74
|
'composer.help.mention',
|
|
63
75
|
'composer.help.attachment',
|
|
64
76
|
'composer.attachments.label',
|
|
77
|
+
'composer.attachments.remove',
|
|
65
78
|
'composer.mention.suggestions',
|
|
79
|
+
'composer.recipient.suggestions',
|
|
66
80
|
'composer.attachment.uploading',
|
|
67
81
|
'composer.thanks.thanked',
|
|
68
82
|
'composer.thanks.thanks',
|
|
69
83
|
'composer.multiquote.button',
|
|
84
|
+
'composer.multiquote.tooltip',
|
|
70
85
|
'composer.multiquote.addToReply',
|
|
71
86
|
'composer.multiquote.adding',
|
|
72
87
|
'composer.multiquote.clear',
|
|
@@ -75,8 +90,10 @@ const CHROME_KEYS = [
|
|
|
75
90
|
] as const
|
|
76
91
|
|
|
77
92
|
const CHROME_PATTERN_KEYS = [
|
|
93
|
+
'composer.mention.available',
|
|
78
94
|
'composer.toolShortcut',
|
|
79
95
|
'composer.attachments.hint',
|
|
96
|
+
'composer.attachments.removeChosen',
|
|
80
97
|
'composer.thanks.count',
|
|
81
98
|
'composer.multiquote.selectedToQuote',
|
|
82
99
|
'composer.multiquote.added',
|
|
@@ -9,6 +9,7 @@ import { filterWords } from './word-filter'
|
|
|
9
9
|
export const DISCOVERY_LABELS = {
|
|
10
10
|
new: { labelKey: 'discovery.new.label', blurbKey: 'discovery.new.blurb' },
|
|
11
11
|
today: { labelKey: 'discovery.today.label', blurbKey: 'discovery.today.blurb' },
|
|
12
|
+
unread: { labelKey: 'discovery.unread.label', blurbKey: 'discovery.unread.blurb' },
|
|
12
13
|
mine: { labelKey: 'discovery.mine.label', blurbKey: 'discovery.mine.blurb' },
|
|
13
14
|
participated: {
|
|
14
15
|
labelKey: 'discovery.participated.label',
|
|
@@ -59,7 +60,7 @@ export function buildDiscoveryView(input: {
|
|
|
59
60
|
rows: input.rows.map((row) => ({
|
|
60
61
|
threadId: row.threadId,
|
|
61
62
|
title: filterWords(row.title, input.wordFilter),
|
|
62
|
-
href: `/thread/${row.threadId}-${row.slug}`,
|
|
63
|
+
href: `/thread/${row.threadId}-${row.slug}${input.view === 'unread' ? '?goto=unread' : ''}`,
|
|
63
64
|
forum: { label: row.forumTitle, href: `/${row.forumId}-${row.forumSlug}` },
|
|
64
65
|
authorUsername: row.authorUsername,
|
|
65
66
|
replyCount: count(row.replyCount, input.t),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DraftSummary } from '@meith/drafts'
|
|
2
|
+
import type { Translator } from '@meith/i18n'
|
|
3
|
+
import type { TimeModel } from '@meith/theme-kit'
|
|
4
|
+
|
|
5
|
+
import { copyFor } from './copy'
|
|
6
|
+
import { formatTime, untranslated } from './time'
|
|
7
|
+
|
|
8
|
+
export function draftsPageCopy(t: Translator = untranslated()): Readonly<Record<string, string>> {
|
|
9
|
+
return copyFor(['draftsPage.delete'], t)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DraftRowView {
|
|
13
|
+
readonly key: string
|
|
14
|
+
readonly kind: 'thread' | 'reply'
|
|
15
|
+
readonly kindLabel: string
|
|
16
|
+
readonly targetName: string
|
|
17
|
+
readonly resumeHref: string
|
|
18
|
+
readonly forumId: number
|
|
19
|
+
readonly threadId: number | null
|
|
20
|
+
readonly updatedAt: TimeModel
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildDraftsView(input: {
|
|
24
|
+
readonly drafts: readonly DraftSummary[]
|
|
25
|
+
readonly now: Date
|
|
26
|
+
readonly t?: Translator
|
|
27
|
+
}): readonly DraftRowView[] {
|
|
28
|
+
const t = input.t ?? untranslated()
|
|
29
|
+
|
|
30
|
+
return input.drafts.map((draft) => {
|
|
31
|
+
const isReply = draft.threadId !== null
|
|
32
|
+
return {
|
|
33
|
+
key: isReply ? `thread:${draft.threadId}` : `forum:${draft.forumId}`,
|
|
34
|
+
kind: isReply ? 'reply' : 'thread',
|
|
35
|
+
kindLabel: t.t(isReply ? 'draftsPage.kind.reply' : 'draftsPage.kind.thread'),
|
|
36
|
+
targetName: isReply ? (draft.threadTitle ?? draft.forumTitle) : draft.forumTitle,
|
|
37
|
+
resumeHref: isReply
|
|
38
|
+
? `/thread/${draft.threadId}-${draft.threadSlug}/reply`
|
|
39
|
+
: `/${draft.forumId}-${draft.forumSlug}/new`,
|
|
40
|
+
forumId: draft.forumId,
|
|
41
|
+
threadId: draft.threadId,
|
|
42
|
+
updatedAt: formatTime(draft.updatedAt, input.now, t),
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -32,6 +32,12 @@ const TOOLS: readonly ToolSpec[] = [
|
|
|
32
32
|
placeholderKey: 'composer.placeholder.struck',
|
|
33
33
|
},
|
|
34
34
|
{ tag: 'link', key: 'composer.tool.link', shortcut: 'k', placeholderKey: null },
|
|
35
|
+
{
|
|
36
|
+
tag: 'image',
|
|
37
|
+
key: 'composer.tool.image',
|
|
38
|
+
shortcut: null,
|
|
39
|
+
placeholderKey: 'composer.placeholder.image',
|
|
40
|
+
},
|
|
35
41
|
{ tag: 'quote', key: 'composer.tool.quote', shortcut: null, placeholderKey: null },
|
|
36
42
|
{ tag: 'code', key: 'composer.tool.code', shortcut: null, placeholderKey: null },
|
|
37
43
|
{
|
|
@@ -42,13 +48,21 @@ const TOOLS: readonly ToolSpec[] = [
|
|
|
42
48
|
},
|
|
43
49
|
{ tag: 'bulletedList', key: 'composer.tool.bulletedList', shortcut: null, placeholderKey: null },
|
|
44
50
|
{ tag: 'numberedList', key: 'composer.tool.numberedList', shortcut: null, placeholderKey: null },
|
|
51
|
+
{ tag: 'taskList', key: 'composer.tool.taskList', shortcut: null, placeholderKey: null },
|
|
45
52
|
{ tag: 'heading', key: 'composer.tool.heading', shortcut: null, placeholderKey: null },
|
|
53
|
+
{
|
|
54
|
+
tag: 'table',
|
|
55
|
+
key: 'composer.tool.table',
|
|
56
|
+
shortcut: null,
|
|
57
|
+
placeholderKey: 'composer.placeholder.table',
|
|
58
|
+
},
|
|
46
59
|
]
|
|
47
60
|
|
|
48
61
|
function button(spec: ToolSpec, t: Translator): EditorToolbarButtonModel {
|
|
49
62
|
const label = t.t(spec.key)
|
|
50
63
|
return {
|
|
51
64
|
tag: spec.tag,
|
|
65
|
+
insertion: null,
|
|
52
66
|
label,
|
|
53
67
|
icon: null,
|
|
54
68
|
title:
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from '@meith/theme-kit'
|
|
12
12
|
import type { ReadState, ThreadListingRow, ThreadPage } from '@meith/threads'
|
|
13
13
|
|
|
14
|
-
import { forumHref } from './board-index'
|
|
14
|
+
import { forumHref, hydrateForumRow } from './board-index'
|
|
15
15
|
import { count } from './count'
|
|
16
16
|
import { type MemberIdentity, nameClassOf } from './member-identity'
|
|
17
17
|
import { memberHref } from './member-profile'
|
|
@@ -53,9 +53,22 @@ function lastPost(
|
|
|
53
53
|
function forum(
|
|
54
54
|
row: ForumListingRow,
|
|
55
55
|
ownThreadsOnlyForumIds: ReadonlySet<number> | undefined,
|
|
56
|
+
unreadForumIds: ReadonlySet<number> | undefined,
|
|
57
|
+
now: Date,
|
|
56
58
|
t: Translator | undefined,
|
|
59
|
+
identities: ReadonlyMap<number, MemberIdentity> | undefined,
|
|
60
|
+
wordFilter: CompiledWordFilter | undefined,
|
|
57
61
|
): ForumRowModel {
|
|
58
62
|
const ownThreadsOnly = ownThreadsOnlyForumIds?.has(row.id) ?? false
|
|
63
|
+
const hydration = hydrateForumRow(
|
|
64
|
+
row,
|
|
65
|
+
ownThreadsOnly,
|
|
66
|
+
unreadForumIds,
|
|
67
|
+
now,
|
|
68
|
+
t,
|
|
69
|
+
identities,
|
|
70
|
+
wordFilter,
|
|
71
|
+
)
|
|
59
72
|
return {
|
|
60
73
|
id: row.id,
|
|
61
74
|
title: row.title,
|
|
@@ -64,8 +77,8 @@ function forum(
|
|
|
64
77
|
type: row.type,
|
|
65
78
|
threadCount: count(ownThreadsOnly ? 0 : row.threadCount, t),
|
|
66
79
|
postCount: count(ownThreadsOnly ? 0 : row.postCount, t),
|
|
67
|
-
lastPost:
|
|
68
|
-
isUnread:
|
|
80
|
+
lastPost: hydration.lastPost,
|
|
81
|
+
isUnread: hydration.isUnread,
|
|
69
82
|
subforums: [],
|
|
70
83
|
}
|
|
71
84
|
}
|
|
@@ -87,7 +100,7 @@ export function threadRowModel(
|
|
|
87
100
|
return {
|
|
88
101
|
id: row.id,
|
|
89
102
|
title: filterWords(row.title, wordFilter),
|
|
90
|
-
href: threadHref(row),
|
|
103
|
+
href: isUnread ? `${threadHref(row)}?goto=unread` : threadHref(row),
|
|
91
104
|
prefix: row.prefix,
|
|
92
105
|
author: {
|
|
93
106
|
userId: row.authorUserId,
|
|
@@ -115,7 +128,7 @@ export interface ForumDisplayInput {
|
|
|
115
128
|
readonly nextHref: string | null
|
|
116
129
|
readonly pagination?: PaginationModel
|
|
117
130
|
readonly newThreadHref?: string | null
|
|
118
|
-
readonly readState?: Pick<ReadState, 'forumReadAt' | 'threadLastPostId'> | null
|
|
131
|
+
readonly readState?: Pick<ReadState, 'forumReadAt' | 'threadLastPostId' | 'unreadForumIds'> | null
|
|
119
132
|
readonly markReadAction?: string | null
|
|
120
133
|
readonly now: Date
|
|
121
134
|
readonly t?: Translator
|
|
@@ -131,9 +144,18 @@ export interface ForumDisplayView {
|
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
export function buildForumDisplayView(input: ForumDisplayInput): ForumDisplayView {
|
|
147
|
+
const unreadForumIds = input.readState?.unreadForumIds
|
|
134
148
|
return {
|
|
135
149
|
display: {
|
|
136
|
-
forum: forum(
|
|
150
|
+
forum: forum(
|
|
151
|
+
input.forum,
|
|
152
|
+
input.ownThreadsOnlyForumIds,
|
|
153
|
+
unreadForumIds,
|
|
154
|
+
input.now,
|
|
155
|
+
input.t,
|
|
156
|
+
input.identities,
|
|
157
|
+
input.wordFilter,
|
|
158
|
+
),
|
|
137
159
|
newThreadHref: input.newThreadHref ?? null,
|
|
138
160
|
markReadAction: input.markReadAction ?? null,
|
|
139
161
|
},
|
|
@@ -141,7 +163,17 @@ export function buildForumDisplayView(input: ForumDisplayInput): ForumDisplayVie
|
|
|
141
163
|
input.subforums.length === 0
|
|
142
164
|
? null
|
|
143
165
|
: {
|
|
144
|
-
forums: input.subforums.map((row) =>
|
|
166
|
+
forums: input.subforums.map((row) =>
|
|
167
|
+
forum(
|
|
168
|
+
row,
|
|
169
|
+
input.ownThreadsOnlyForumIds,
|
|
170
|
+
unreadForumIds,
|
|
171
|
+
input.now,
|
|
172
|
+
input.t,
|
|
173
|
+
input.identities,
|
|
174
|
+
input.wordFilter,
|
|
175
|
+
),
|
|
176
|
+
),
|
|
145
177
|
},
|
|
146
178
|
threads: input.page.rows.map((row) =>
|
|
147
179
|
threadRowModel(
|
package/src/view/modcp-nav.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { Translator } from '@meith/i18n'
|
|
2
|
+
|
|
3
|
+
import type { PluginNavSection } from './admin-nav'
|
|
1
4
|
import {
|
|
2
5
|
currentProps,
|
|
3
6
|
deepestHrefIn,
|
|
@@ -6,6 +9,7 @@ import {
|
|
|
6
9
|
type PanelSection,
|
|
7
10
|
sectionHrefIn,
|
|
8
11
|
} from './panel-nav'
|
|
12
|
+
import { untranslated } from './time'
|
|
9
13
|
|
|
10
14
|
export { currentProps, isUnder }
|
|
11
15
|
|
|
@@ -67,6 +71,34 @@ export function modCpNav(access: ModCpNavAccess): PanelNav {
|
|
|
67
71
|
return [MODCP_OVERVIEW, ...modCpSections(access)]
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
export const MODCP_PLUGINS_HREF = '/modcp/plugins'
|
|
75
|
+
|
|
76
|
+
const MODCP_PLUGIN_KEY_PATTERN = /^\/modcp\/plugins\/([a-z][a-z0-9-]*)(?:\/|$|\?)/
|
|
77
|
+
|
|
78
|
+
export function pluginKeyAtModCp(pathname: string): string | null {
|
|
79
|
+
return MODCP_PLUGIN_KEY_PATTERN.exec(pathname)?.[1] ?? null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function modCpNavWithPlugin(
|
|
83
|
+
access: ModCpNavAccess,
|
|
84
|
+
plugin: PluginNavSection | null,
|
|
85
|
+
t: Translator = untranslated(),
|
|
86
|
+
): PanelNav {
|
|
87
|
+
const base = modCpNav(access)
|
|
88
|
+
if (plugin === null || plugin.pages.length === 0) return base
|
|
89
|
+
|
|
90
|
+
const href = `${MODCP_PLUGINS_HREF}/${plugin.key}`
|
|
91
|
+
const section: PanelSection = {
|
|
92
|
+
href,
|
|
93
|
+
titleText: plugin.name,
|
|
94
|
+
icon: 'plugins',
|
|
95
|
+
blurbText: t.t('modCpNav.plugin.blurb', { name: plugin.name }),
|
|
96
|
+
children: plugin.pages.filter((page) => page.href !== href),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [...base, section]
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
export function activeSectionHref(access: ModCpNavAccess, pathname: string): string | null {
|
|
71
103
|
return sectionHrefIn(modCpNav(access), pathname)
|
|
72
104
|
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import type { Translator } from '@meith/i18n'
|
|
2
|
+
import type { ReportCategory } from '@meith/moderation'
|
|
2
3
|
|
|
3
4
|
import { copyFor, patternCopy, splitAround } from './copy'
|
|
4
5
|
import { untranslated } from './time'
|
|
5
6
|
|
|
7
|
+
export const REPORT_CATEGORY_ORDER = [
|
|
8
|
+
'spam',
|
|
9
|
+
'off_topic',
|
|
10
|
+
'abuse',
|
|
11
|
+
'other',
|
|
12
|
+
] as const satisfies readonly ReportCategory[]
|
|
13
|
+
|
|
14
|
+
export const REPORT_CATEGORY_LABEL_KEYS: Readonly<Record<ReportCategory, string>> = {
|
|
15
|
+
spam: 'moderationForm.report.category.spam',
|
|
16
|
+
off_topic: 'moderationForm.report.category.off_topic',
|
|
17
|
+
abuse: 'moderationForm.report.category.abuse',
|
|
18
|
+
other: 'moderationForm.report.category.other',
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
export function moderationFormsCopy(
|
|
7
22
|
t: Translator = untranslated(),
|
|
8
23
|
): Readonly<Record<string, string>> {
|
|
@@ -15,6 +30,11 @@ export function moderationFormsCopy(
|
|
|
15
30
|
'moderationForm.report.label',
|
|
16
31
|
'moderationForm.report.hint',
|
|
17
32
|
'moderationForm.report.submit',
|
|
33
|
+
'moderationForm.report.categoryLegend',
|
|
34
|
+
'moderationForm.report.category.spam',
|
|
35
|
+
'moderationForm.report.category.off_topic',
|
|
36
|
+
'moderationForm.report.category.abuse',
|
|
37
|
+
'moderationForm.report.category.other',
|
|
18
38
|
'moderationForm.report.putBack',
|
|
19
39
|
'moderationForm.report.take',
|
|
20
40
|
'moderationForm.report.note',
|
|
@@ -78,6 +78,7 @@ export function notificationNotice(
|
|
|
78
78
|
t: Translator = untranslated(),
|
|
79
79
|
): string | null {
|
|
80
80
|
if (query.saved === 'announcements') return t.t('notice.announcementsSaved')
|
|
81
|
+
if (query.saved === 'digest-cadence') return t.t('notice.boardDigestCadenceSaved')
|
|
81
82
|
if (query.saved !== undefined) return t.t('notice.preferencesSaved')
|
|
82
83
|
if (query.read === 'all') return t.t('notice.allRead')
|
|
83
84
|
if (query.read === 'one') return t.t('notice.markedRead')
|
package/src/view/plugin-panel.ts
CHANGED
|
@@ -37,6 +37,19 @@ export function pluginPanelTabs(input: {
|
|
|
37
37
|
]
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function pluginStaffPanelTabs(input: {
|
|
41
|
+
readonly pages: readonly PluginPage[]
|
|
42
|
+
readonly current: string | null
|
|
43
|
+
}): readonly PluginPanelTab[] {
|
|
44
|
+
if (input.pages.length < 2) return []
|
|
45
|
+
|
|
46
|
+
return input.pages.map((page) => ({
|
|
47
|
+
href: page.href,
|
|
48
|
+
label: page.title,
|
|
49
|
+
isCurrent: page.path === input.current,
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
export function pluginNavChildren(pages: readonly PluginPage[]): readonly PanelSubsection[] {
|
|
41
54
|
return pages.map((page) => ({ href: page.href, titleText: page.title }))
|
|
42
55
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Poll, pollOptionShares } from '@meith/polls'
|
|
2
|
+
|
|
3
|
+
export interface PollOptionVoteView {
|
|
4
|
+
readonly id: number
|
|
5
|
+
readonly label: string
|
|
6
|
+
readonly votes: number
|
|
7
|
+
readonly share: number
|
|
8
|
+
readonly checked: boolean
|
|
9
|
+
readonly voterNames: readonly string[]
|
|
10
|
+
readonly moreVoters: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PollVoteView {
|
|
14
|
+
readonly options: readonly PollOptionVoteView[]
|
|
15
|
+
readonly total: number
|
|
16
|
+
readonly multiple: boolean
|
|
17
|
+
readonly closed: boolean
|
|
18
|
+
readonly hasVoted: boolean
|
|
19
|
+
readonly mayCast: boolean
|
|
20
|
+
readonly publicVotes: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function pollVoteView(poll: Poll, mayVote: boolean, now: Date): PollVoteView {
|
|
24
|
+
const total = poll.options.reduce((sum, option) => sum + option.votes, 0)
|
|
25
|
+
const shares = pollOptionShares(poll.options.map((option) => option.votes))
|
|
26
|
+
const closed = poll.closesAt !== null && poll.closesAt <= now
|
|
27
|
+
const hasVoted = poll.votedOptionIds.length > 0
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
total,
|
|
31
|
+
multiple: poll.maxOptions !== 1,
|
|
32
|
+
closed,
|
|
33
|
+
hasVoted,
|
|
34
|
+
mayCast: mayVote && !closed && (!hasVoted || poll.allowRevote),
|
|
35
|
+
publicVotes: poll.publicVotes,
|
|
36
|
+
options: poll.options.map((option, index) => ({
|
|
37
|
+
id: option.id,
|
|
38
|
+
label: option.label,
|
|
39
|
+
votes: option.votes,
|
|
40
|
+
share: shares[index] ?? 0,
|
|
41
|
+
checked: poll.votedOptionIds.includes(option.id),
|
|
42
|
+
voterNames: poll.publicVotes ? option.voters.map((voter) => voter.username) : [],
|
|
43
|
+
moreVoters: poll.publicVotes ? Math.max(0, option.votes - option.voters.length) : 0,
|
|
44
|
+
})),
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/view/post-link.ts
CHANGED
|
@@ -16,7 +16,14 @@ export function locatedHref(
|
|
|
16
16
|
): string {
|
|
17
17
|
const params = new URLSearchParams()
|
|
18
18
|
for (const [key, value] of Object.entries(query)) {
|
|
19
|
-
if (
|
|
19
|
+
if (
|
|
20
|
+
value === undefined ||
|
|
21
|
+
key === 'post' ||
|
|
22
|
+
key === 'after' ||
|
|
23
|
+
key === 'page' ||
|
|
24
|
+
key === 'goto'
|
|
25
|
+
)
|
|
26
|
+
continue
|
|
20
27
|
for (const one of typeof value === 'string' ? [value] : value) params.append(key, one)
|
|
21
28
|
}
|
|
22
29
|
if (location.afterId !== null) {
|
package/src/view/shell.ts
CHANGED
|
@@ -9,12 +9,13 @@ import type {
|
|
|
9
9
|
ViewerModel,
|
|
10
10
|
} from '@meith/theme-kit'
|
|
11
11
|
|
|
12
|
+
import { BOARD_TITLE } from './board-title'
|
|
12
13
|
import { count } from './count'
|
|
13
14
|
import { memberHref } from './member-profile'
|
|
14
15
|
import { buildNavigation, defaultNavigationItems } from './navigation'
|
|
15
16
|
import { timezoneLabel, untranslated } from './time'
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
+
export { BOARD_TITLE }
|
|
18
19
|
|
|
19
20
|
export const TIMEZONE_LABEL = 'UTC'
|
|
20
21
|
|
|
@@ -80,6 +80,7 @@ export function unsubscribeNotice(
|
|
|
80
80
|
): string | null {
|
|
81
81
|
if (done === 'announcements') return t.t('subscription.announcementsOff')
|
|
82
82
|
if (done === 'email') return t.t('subscription.emailsOff')
|
|
83
|
+
if (done === 'boardDigest') return t.t('subscription.boardDigestOff')
|
|
83
84
|
if (done === 'one') return t.t('subscription.doneOne')
|
|
84
85
|
return null
|
|
85
86
|
}
|