@meith/web 0.31.0 → 0.33.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/layout.tsx +23 -8
- 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/admin-forms.tsx +57 -9
- 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/components/shell/view-tabs.tsx +1 -1
- package/src/server/admin-actions.ts +118 -49
- package/src/server/admin-settings-actions.ts +5 -0
- package/src/server/admin.ts +70 -2
- 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/cookies.ts +18 -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/push-actions.ts +9 -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/session-cookies.ts +18 -0
- 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/styles/globals.css +1 -1
- 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 +29 -1
- 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
|
@@ -147,10 +147,23 @@ export const SLOT_FIXTURES: { readonly [K in SlotName]?: SlotFixture<K> } = {
|
|
|
147
147
|
boardTitle: 'The Bike Shed',
|
|
148
148
|
homeHref: '/',
|
|
149
149
|
viewer: VIEWER,
|
|
150
|
-
navigation: [
|
|
150
|
+
navigation: [
|
|
151
|
+
{ label: 'Unanswered', href: '/discover/unanswered' },
|
|
152
|
+
{
|
|
153
|
+
label: 'Community',
|
|
154
|
+
href: '/community',
|
|
155
|
+
submenu: [{ label: 'Rules', href: '/community/rules' }],
|
|
156
|
+
},
|
|
157
|
+
],
|
|
151
158
|
children: region('user-panel'),
|
|
152
159
|
},
|
|
153
|
-
requires: [
|
|
160
|
+
requires: [
|
|
161
|
+
'The Bike Shed',
|
|
162
|
+
region('user-panel'),
|
|
163
|
+
'/discover/unanswered',
|
|
164
|
+
'/community/rules',
|
|
165
|
+
'Rules',
|
|
166
|
+
],
|
|
154
167
|
},
|
|
155
168
|
|
|
156
169
|
UserPanel: {
|
|
@@ -331,8 +344,18 @@ export const SLOT_FIXTURES: { readonly [K in SlotName]?: SlotFixture<K> } = {
|
|
|
331
344
|
},
|
|
332
345
|
|
|
333
346
|
ThreadRow: {
|
|
334
|
-
model: {
|
|
335
|
-
|
|
347
|
+
model: {
|
|
348
|
+
thread: { ...THREAD, visibility: 'deleted' },
|
|
349
|
+
select: null,
|
|
350
|
+
regions: { pluginBadges: pluginRegion('threadrow-badges') },
|
|
351
|
+
},
|
|
352
|
+
requires: [
|
|
353
|
+
'Bikeshedding the bike shed',
|
|
354
|
+
'/thread/91-bikeshedding',
|
|
355
|
+
'27',
|
|
356
|
+
'data-visibility',
|
|
357
|
+
pluginRegion('threadrow-badges'),
|
|
358
|
+
],
|
|
336
359
|
},
|
|
337
360
|
|
|
338
361
|
SubforumList: {
|
|
@@ -351,13 +374,19 @@ export const SLOT_FIXTURES: { readonly [K in SlotName]?: SlotFixture<K> } = {
|
|
|
351
374
|
forum: { label: 'General discussion', href: '/f/3-general' },
|
|
352
375
|
replyHref: '/thread/91-bikeshedding/reply',
|
|
353
376
|
markReadAction: '/thread/91-bikeshedding/mark-read',
|
|
377
|
+
watch: { subscribed: true, action: '/thread/91-bikeshedding/unwatch' },
|
|
354
378
|
regions: {
|
|
355
379
|
posts: region('posts'),
|
|
356
380
|
pagination: region('pagination'),
|
|
357
381
|
quickReply: region('quick-reply'),
|
|
358
382
|
},
|
|
359
383
|
},
|
|
360
|
-
requires: [
|
|
384
|
+
requires: [
|
|
385
|
+
'Bikeshedding the bike shed',
|
|
386
|
+
'/thread/91-bikeshedding/unwatch',
|
|
387
|
+
region('posts'),
|
|
388
|
+
region('pagination'),
|
|
389
|
+
],
|
|
361
390
|
},
|
|
362
391
|
|
|
363
392
|
PostBit: {
|
|
@@ -417,6 +446,7 @@ export const SLOT_FIXTURES: { readonly [K in SlotName]?: SlotFixture<K> } = {
|
|
|
417
446
|
buttons: [
|
|
418
447
|
{
|
|
419
448
|
tag: 'bold',
|
|
449
|
+
insertion: null,
|
|
420
450
|
label: 'Bold',
|
|
421
451
|
title: 'Bold (Ctrl+B)',
|
|
422
452
|
keyShortcut: 'Control+b',
|
|
@@ -425,6 +455,7 @@ export const SLOT_FIXTURES: { readonly [K in SlotName]?: SlotFixture<K> } = {
|
|
|
425
455
|
},
|
|
426
456
|
{
|
|
427
457
|
tag: 'link',
|
|
458
|
+
insertion: null,
|
|
428
459
|
label: 'Link',
|
|
429
460
|
title: 'Link',
|
|
430
461
|
keyShortcut: null,
|
package/src/view/account-copy.ts
CHANGED
|
@@ -48,6 +48,10 @@ export function optionsFormCopy(
|
|
|
48
48
|
'accountForm.options.pageSizeHint',
|
|
49
49
|
'accountForm.options.invisible',
|
|
50
50
|
'accountForm.options.invisibleHint',
|
|
51
|
+
'accountForm.options.autoWatchOwnThreads',
|
|
52
|
+
'accountForm.options.autoWatchOwnThreadsHint',
|
|
53
|
+
'accountForm.options.autoWatchRepliedThreads',
|
|
54
|
+
'accountForm.options.autoWatchRepliedThreadsHint',
|
|
51
55
|
'accountForm.options.submit',
|
|
52
56
|
],
|
|
53
57
|
t,
|
|
@@ -286,6 +290,36 @@ export function activityDetailLabel(
|
|
|
286
290
|
return t.t('accountForm.activity.detail', { at, device, address })
|
|
287
291
|
}
|
|
288
292
|
|
|
293
|
+
export function feedTokenCopy(t: Translator = untranslated()): Readonly<Record<string, string>> {
|
|
294
|
+
return copyFor(
|
|
295
|
+
[
|
|
296
|
+
'accountForm.feed.title',
|
|
297
|
+
'accountForm.feed.blurb',
|
|
298
|
+
'accountForm.feed.none',
|
|
299
|
+
'accountForm.feed.active',
|
|
300
|
+
'accountForm.feed.generate',
|
|
301
|
+
'accountForm.feed.regenerate',
|
|
302
|
+
'accountForm.feed.revoke',
|
|
303
|
+
'accountForm.feed.copyNow',
|
|
304
|
+
'accountForm.feed.revealHint',
|
|
305
|
+
'accountForm.feed.rssLabel',
|
|
306
|
+
'accountForm.feed.atomLabel',
|
|
307
|
+
'accountForm.feed.warning',
|
|
308
|
+
'form.working',
|
|
309
|
+
],
|
|
310
|
+
t,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function feedTokenDetailLabel(
|
|
315
|
+
created: string,
|
|
316
|
+
lastUsed: string | null,
|
|
317
|
+
t: Translator = untranslated(),
|
|
318
|
+
): string {
|
|
319
|
+
const used = lastUsed === null ? t.t('accountForm.feed.neverUsed') : lastUsed
|
|
320
|
+
return t.t('accountForm.feed.detail', { created, used })
|
|
321
|
+
}
|
|
322
|
+
|
|
289
323
|
export function notificationFormsCopy(
|
|
290
324
|
t: Translator = untranslated(),
|
|
291
325
|
): Readonly<Record<string, string>> {
|
|
@@ -304,6 +338,11 @@ export function notificationFormsCopy(
|
|
|
304
338
|
'accountForm.announcements.description',
|
|
305
339
|
'accountForm.announcements.blurb',
|
|
306
340
|
'accountForm.announcements.submit',
|
|
341
|
+
'accountForm.boardDigest.legend',
|
|
342
|
+
'accountForm.boardDigest.description',
|
|
343
|
+
'accountForm.boardDigest.weekly',
|
|
344
|
+
'accountForm.boardDigest.monthly',
|
|
345
|
+
'accountForm.boardDigest.submit',
|
|
307
346
|
'accountForm.push.heading',
|
|
308
347
|
'accountForm.push.subscribe',
|
|
309
348
|
'accountForm.push.unsubscribe',
|
package/src/view/admin-copy.ts
CHANGED
|
@@ -61,8 +61,10 @@ export function forumAdminCopy(t: Translator = untranslated()): Readonly<Record<
|
|
|
61
61
|
'adminForum.permGroup.moderation',
|
|
62
62
|
'adminForum.permGroup.attachments',
|
|
63
63
|
'adminForum.permGroup.approvals',
|
|
64
|
+
'adminForum.permGroup.numeric',
|
|
64
65
|
'adminForum.permGroup.other',
|
|
65
|
-
'adminForum.
|
|
66
|
+
'adminForum.permission',
|
|
67
|
+
'adminForum.saveMatrix',
|
|
66
68
|
'adminForum.unsavedChanges',
|
|
67
69
|
'adminForum.copied',
|
|
68
70
|
'adminForum.copyNote',
|
|
@@ -102,14 +104,7 @@ export function forumAdminCopy(t: Translator = untranslated()): Readonly<Record<
|
|
|
102
104
|
t,
|
|
103
105
|
),
|
|
104
106
|
...patternCopy(
|
|
105
|
-
[
|
|
106
|
-
'adminForum.perm.setHere',
|
|
107
|
-
'adminForum.perm.inheritedDefault',
|
|
108
|
-
'adminForum.perm.inheritedFrom',
|
|
109
|
-
'adminForum.saveGroup',
|
|
110
|
-
'adminForum.copyTo',
|
|
111
|
-
'adminForum.overridesSet',
|
|
112
|
-
],
|
|
107
|
+
['adminForum.perm.inheritedDefault', 'adminForum.perm.inheritedFrom', 'adminForum.copyTo'],
|
|
113
108
|
t,
|
|
114
109
|
),
|
|
115
110
|
}
|
package/src/view/admin-nav.ts
CHANGED
|
@@ -104,6 +104,12 @@ export const ADMIN_SECTIONS: PanelNav = [
|
|
|
104
104
|
icon: 'tokens',
|
|
105
105
|
blurbKey: 'adminNav.admin-api-tokens.blurb',
|
|
106
106
|
},
|
|
107
|
+
{
|
|
108
|
+
href: '/admin/webhooks',
|
|
109
|
+
titleKey: 'adminNav.admin-webhooks.title',
|
|
110
|
+
icon: 'subscriptions',
|
|
111
|
+
blurbKey: 'adminNav.admin-webhooks.blurb',
|
|
112
|
+
},
|
|
107
113
|
{
|
|
108
114
|
href: '/admin/system',
|
|
109
115
|
titleKey: 'adminNav.admin-system.title',
|
|
@@ -15,7 +15,12 @@ export function adminFormsCopy(t: Translator = untranslated()): Readonly<Record<
|
|
|
15
15
|
'adminPanel.reason.default',
|
|
16
16
|
'adminPanel.password',
|
|
17
17
|
'adminPanel.twoFactorCode',
|
|
18
|
-
'adminPanel.
|
|
18
|
+
'adminPanel.twoFactor.title',
|
|
19
|
+
'adminPanel.twoFactor.lede',
|
|
20
|
+
'adminPanel.twoFactor.recoveryHint',
|
|
21
|
+
'adminPanel.twoFactor.recoveryHintExhausted',
|
|
22
|
+
'adminPanel.twoFactor.submit',
|
|
23
|
+
'adminPanel.twoFactor.cancel',
|
|
19
24
|
'adminPanel.enter',
|
|
20
25
|
'adminPanel.leave',
|
|
21
26
|
],
|
|
@@ -47,6 +52,29 @@ export function apiTokenFormsCopy(
|
|
|
47
52
|
)
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
export function webhookFormsCopy(t: Translator = untranslated()): Readonly<Record<string, string>> {
|
|
56
|
+
return copyFor(
|
|
57
|
+
[
|
|
58
|
+
'adminPanel.webhook.copyNow',
|
|
59
|
+
'adminPanel.webhook.url',
|
|
60
|
+
'adminPanel.webhook.urlPlaceholder',
|
|
61
|
+
'adminPanel.webhook.urlHint',
|
|
62
|
+
'adminPanel.webhook.topics',
|
|
63
|
+
'adminPanel.webhook.topicsHint',
|
|
64
|
+
'adminPanel.webhook.format',
|
|
65
|
+
'adminPanel.webhook.formatHint',
|
|
66
|
+
'adminPanel.webhook.format.json',
|
|
67
|
+
'adminPanel.webhook.format.discord',
|
|
68
|
+
'adminPanel.webhook.active',
|
|
69
|
+
'adminPanel.webhook.create',
|
|
70
|
+
'adminPanel.webhook.enable',
|
|
71
|
+
'adminPanel.webhook.disable',
|
|
72
|
+
'adminPanel.webhook.delete',
|
|
73
|
+
],
|
|
74
|
+
t,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
export function systemFormsCopy(t: Translator = untranslated()): Readonly<Record<string, string>> {
|
|
51
79
|
return {
|
|
52
80
|
...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(
|