@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
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
|
}
|
package/src/view/thread-view.ts
CHANGED
|
@@ -2,12 +2,14 @@ import type { ForumRow } from '@meith/forums'
|
|
|
2
2
|
import type { Translator } from '@meith/i18n'
|
|
3
3
|
import { type BoardVocabulary, type CompiledWordFilter, postBodyHtml } from '@meith/markdown'
|
|
4
4
|
import { editedNote, type PostListingRow, type PostPage } from '@meith/posts'
|
|
5
|
-
import { suppress } from '@meith/relations'
|
|
5
|
+
import { isOnline, suppress } from '@meith/relations'
|
|
6
|
+
import type { SubscriptionMode } from '@meith/subscriptions'
|
|
6
7
|
import type {
|
|
7
8
|
PaginationModel,
|
|
8
9
|
PostAttachmentModel,
|
|
9
10
|
PostBitModel,
|
|
10
11
|
ThreadViewModel,
|
|
12
|
+
ThreadWatchModel,
|
|
11
13
|
} from '@meith/theme-kit'
|
|
12
14
|
import type { ThreadListingRow } from '@meith/threads'
|
|
13
15
|
|
|
@@ -58,6 +60,11 @@ function withinEditWindow(
|
|
|
58
60
|
return elapsed <= capabilities.editWindowMinutes
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
export interface AuthorPresence {
|
|
64
|
+
readonly lastActiveAt: Date | null
|
|
65
|
+
readonly invisible: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
interface PostContext {
|
|
62
69
|
readonly now: Date
|
|
63
70
|
readonly replyHref: string | null
|
|
@@ -67,6 +74,8 @@ interface PostContext {
|
|
|
67
74
|
readonly signatures: ReadonlyMap<number, string>
|
|
68
75
|
readonly avatars: ReadonlyMap<number, string>
|
|
69
76
|
readonly identities: ReadonlyMap<number, MemberIdentity>
|
|
77
|
+
readonly presence: ReadonlyMap<number, AuthorPresence>
|
|
78
|
+
readonly viewerSeesInvisible: boolean
|
|
70
79
|
readonly ignoredIds: ReadonlySet<number>
|
|
71
80
|
readonly revealedPostIds: ReadonlySet<number>
|
|
72
81
|
readonly revealHref: (postId: number, number: number) => string
|
|
@@ -92,6 +101,12 @@ function post(post: PostListingRow, thread: ThreadListingRow, context: PostConte
|
|
|
92
101
|
const identity =
|
|
93
102
|
post.authorUserId === null ? undefined : context.identities.get(post.authorUserId)
|
|
94
103
|
|
|
104
|
+
const presence = post.authorUserId === null ? undefined : context.presence.get(post.authorUserId)
|
|
105
|
+
const authorIsOnline =
|
|
106
|
+
presence !== undefined &&
|
|
107
|
+
(!presence.invisible || context.viewerSeesInvisible) &&
|
|
108
|
+
isOnline(presence.lastActiveAt, now)
|
|
109
|
+
|
|
95
110
|
const mayEdit =
|
|
96
111
|
post.visibility !== 'deleted' &&
|
|
97
112
|
(isOwn
|
|
@@ -123,7 +138,7 @@ function post(post: PostListingRow, thread: ThreadListingRow, context: PostConte
|
|
|
123
138
|
joinedAt: post.authorJoinedAt === null ? null : formatDate(post.authorJoinedAt, t),
|
|
124
139
|
signatureHtml:
|
|
125
140
|
hidden || post.authorUserId === null ? null : (signatures.get(post.authorUserId) ?? null),
|
|
126
|
-
isOnline:
|
|
141
|
+
isOnline: authorIsOnline,
|
|
127
142
|
fields: hidden || post.authorUserId === null ? [] : (fields.get(post.authorUserId) ?? []),
|
|
128
143
|
},
|
|
129
144
|
bodyHtml: hidden
|
|
@@ -191,6 +206,8 @@ export interface ThreadViewInput {
|
|
|
191
206
|
readonly pagination?: PaginationModel
|
|
192
207
|
readonly nextHref: string | null
|
|
193
208
|
readonly markReadAction?: string | null
|
|
209
|
+
readonly watchOffered?: boolean
|
|
210
|
+
readonly watchMode?: SubscriptionMode | null
|
|
194
211
|
readonly replyHref?: string | null
|
|
195
212
|
readonly capabilities?: PostCapabilities
|
|
196
213
|
readonly now: Date
|
|
@@ -199,6 +216,8 @@ export interface ThreadViewInput {
|
|
|
199
216
|
readonly signatures?: ReadonlyMap<number, string>
|
|
200
217
|
readonly avatars?: ReadonlyMap<number, string>
|
|
201
218
|
readonly identities?: ReadonlyMap<number, MemberIdentity>
|
|
219
|
+
readonly presence?: ReadonlyMap<number, AuthorPresence>
|
|
220
|
+
readonly viewerSeesInvisible?: boolean
|
|
202
221
|
readonly attachments?: ReadonlyMap<number, readonly PostAttachmentModel[]>
|
|
203
222
|
readonly ignoredIds?: ReadonlySet<number>
|
|
204
223
|
readonly revealedPostIds?: ReadonlySet<number>
|
|
@@ -212,6 +231,7 @@ const EMPTY_SIGNATURES: ReadonlyMap<number, string> = new Map()
|
|
|
212
231
|
const EMPTY_ATTACHMENTS: ReadonlyMap<number, readonly PostAttachmentModel[]> = new Map()
|
|
213
232
|
const EMPTY_AVATARS: ReadonlyMap<number, string> = new Map()
|
|
214
233
|
const EMPTY_IDENTITIES: ReadonlyMap<number, MemberIdentity> = new Map()
|
|
234
|
+
const EMPTY_PRESENCE: ReadonlyMap<number, AuthorPresence> = new Map()
|
|
215
235
|
|
|
216
236
|
export function revealHref(currentHref: string, postId: number, number: number): string {
|
|
217
237
|
const [path = '', hash] = currentHref.split('#')
|
|
@@ -227,6 +247,18 @@ export function threadToolsHeading(
|
|
|
227
247
|
return t.t(isForumModerator ? 'thread.moderatorTools' : 'thread.threadTools')
|
|
228
248
|
}
|
|
229
249
|
|
|
250
|
+
export function threadWatch(input: {
|
|
251
|
+
readonly offered: boolean
|
|
252
|
+
readonly mode: SubscriptionMode | null
|
|
253
|
+
readonly threadId: number
|
|
254
|
+
}): ThreadWatchModel | null {
|
|
255
|
+
if (!input.offered) return null
|
|
256
|
+
|
|
257
|
+
return input.mode === null
|
|
258
|
+
? { subscribed: false, action: `/api/subscribe/thread/${input.threadId}` }
|
|
259
|
+
: { subscribed: true, action: `/api/unsubscribe/thread/${input.threadId}` }
|
|
260
|
+
}
|
|
261
|
+
|
|
230
262
|
export function revealedFrom(raw: string | readonly string[] | undefined): ReadonlySet<number> {
|
|
231
263
|
if (raw === undefined) return EMPTY_IDS
|
|
232
264
|
const values = typeof raw === 'string' ? [raw] : raw
|
|
@@ -253,6 +285,11 @@ export function buildThreadView(input: ThreadViewInput): ThreadView {
|
|
|
253
285
|
forum: { label: input.forum.title, href: forumHref(input.forum) },
|
|
254
286
|
replyHref: input.replyHref ?? null,
|
|
255
287
|
markReadAction: input.markReadAction ?? null,
|
|
288
|
+
watch: threadWatch({
|
|
289
|
+
offered: input.watchOffered ?? false,
|
|
290
|
+
mode: input.watchMode ?? null,
|
|
291
|
+
threadId: input.thread.id,
|
|
292
|
+
}),
|
|
256
293
|
},
|
|
257
294
|
posts: input.page.rows.map((entry) =>
|
|
258
295
|
post(entry, input.thread, {
|
|
@@ -265,6 +302,8 @@ export function buildThreadView(input: ThreadViewInput): ThreadView {
|
|
|
265
302
|
attachments: input.attachments ?? EMPTY_ATTACHMENTS,
|
|
266
303
|
avatars: input.avatars ?? EMPTY_AVATARS,
|
|
267
304
|
identities: input.identities ?? EMPTY_IDENTITIES,
|
|
305
|
+
presence: input.presence ?? EMPTY_PRESENCE,
|
|
306
|
+
viewerSeesInvisible: input.viewerSeesInvisible ?? false,
|
|
268
307
|
ignoredIds: input.ignoredIds ?? EMPTY_IDS,
|
|
269
308
|
revealedPostIds: input.revealedPostIds ?? EMPTY_IDS,
|
|
270
309
|
revealHref: (postId, number) => revealHref(input.currentHref ?? '', postId, number),
|
package/src/view/usercp-nav.ts
CHANGED
|
@@ -61,6 +61,12 @@ export const USERCP_SECTIONS: PanelNav = [
|
|
|
61
61
|
icon: 'signature',
|
|
62
62
|
blurbKey: 'userCpNav.usercp-signature.blurb',
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
href: '/usercp/drafts',
|
|
66
|
+
titleKey: 'userCpNav.usercp-drafts.title',
|
|
67
|
+
icon: 'content',
|
|
68
|
+
blurbKey: 'userCpNav.usercp-drafts.blurb',
|
|
69
|
+
},
|
|
64
70
|
{
|
|
65
71
|
href: '/usercp/contacts',
|
|
66
72
|
titleKey: 'userCpNav.usercp-contacts.title',
|
package/src/view/usercp.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from '@meith/accounts'
|
|
7
7
|
import { SOURCE_LOCALE, type Translator } from '@meith/i18n'
|
|
8
8
|
import { maxLengthFor, type ResolvedProfileField } from '@meith/profile-fields'
|
|
9
|
+
import { MODE_LABELS, SUBSCRIPTION_MODES, type SubscriptionMode } from '@meith/subscriptions'
|
|
9
10
|
|
|
10
11
|
import { untranslated } from './time'
|
|
11
12
|
|
|
@@ -129,6 +130,8 @@ export function optionsFormValues(settings: MemberSettings): {
|
|
|
129
130
|
postsPerPage: string
|
|
130
131
|
threadsPerPage: string
|
|
131
132
|
invisible: boolean
|
|
133
|
+
autoWatchOwnThreads: SubscriptionMode
|
|
134
|
+
autoWatchRepliedThreads: SubscriptionMode
|
|
132
135
|
} {
|
|
133
136
|
return {
|
|
134
137
|
timezone: settings.timezone,
|
|
@@ -136,9 +139,20 @@ export function optionsFormValues(settings: MemberSettings): {
|
|
|
136
139
|
postsPerPage: settings.postsPerPage === null ? '' : String(settings.postsPerPage),
|
|
137
140
|
threadsPerPage: settings.threadsPerPage === null ? '' : String(settings.threadsPerPage),
|
|
138
141
|
invisible: settings.invisible,
|
|
142
|
+
autoWatchOwnThreads: settings.autoWatchOwnThreads,
|
|
143
|
+
autoWatchRepliedThreads: settings.autoWatchRepliedThreads,
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
export interface AutoWatchChoice {
|
|
148
|
+
readonly value: SubscriptionMode
|
|
149
|
+
readonly label: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function autoWatchChoices(): readonly AutoWatchChoice[] {
|
|
153
|
+
return SUBSCRIPTION_MODES.map((mode) => ({ value: mode, label: MODE_LABELS[mode] }))
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
export function customFieldInputs(resolved: readonly ResolvedProfileField[]): readonly {
|
|
143
157
|
key: string
|
|
144
158
|
label: string
|