@meith/web 0.30.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/app/(board)/[slug]/feed.xml/route.ts +5 -3
  2. package/app/(board)/[slug]/new/page.tsx +5 -0
  3. package/app/(board)/[slug]/page.tsx +7 -1
  4. package/app/(board)/discover/[view]/page.tsx +6 -1
  5. package/app/(board)/member/[id]/page.tsx +3 -5
  6. package/app/(board)/member/[id]/reputation/page.tsx +8 -15
  7. package/app/(board)/members/page.tsx +3 -8
  8. package/app/(board)/messages/[id]/page.tsx +3 -7
  9. package/app/(board)/messages/page.tsx +3 -5
  10. package/app/(board)/modcp/ip/page.tsx +3 -5
  11. package/app/(board)/modcp/plugins/[key]/[[...path]]/page.tsx +84 -0
  12. package/app/(board)/moderation/reports/page.tsx +53 -15
  13. package/app/(board)/moderation/warn/page.tsx +3 -6
  14. package/app/(board)/notifications/page.tsx +5 -10
  15. package/app/(board)/notifications/preferences/page.tsx +8 -0
  16. package/app/(board)/online/page.tsx +12 -6
  17. package/app/(board)/staff/page.tsx +4 -5
  18. package/app/(board)/stats/page.tsx +10 -3
  19. package/app/(board)/subscriptions/page.tsx +5 -10
  20. package/app/(board)/thread/[slug]/edit/page.tsx +21 -0
  21. package/app/(board)/thread/[slug]/feed.xml/route.ts +6 -3
  22. package/app/(board)/thread/[slug]/page.tsx +49 -11
  23. package/app/(board)/thread/[slug]/reply/page.tsx +5 -0
  24. package/app/(board)/unsubscribe/page.tsx +8 -11
  25. package/app/(board)/usercp/contacts/page.tsx +3 -5
  26. package/app/(board)/usercp/drafts/page.tsx +70 -0
  27. package/app/(board)/usercp/options/page.tsx +8 -1
  28. package/app/(board)/usercp/security/page.tsx +16 -0
  29. package/app/admin/antispam/page.tsx +4 -12
  30. package/app/admin/content/attachments/page.tsx +3 -6
  31. package/app/admin/content/page.tsx +3 -13
  32. package/app/admin/forums/[id]/permissions/page.tsx +9 -12
  33. package/app/admin/groups/page.tsx +7 -9
  34. package/app/admin/page.tsx +12 -6
  35. package/app/admin/plugins/[key]/[[...path]]/page.tsx +6 -7
  36. package/app/admin/plugins/page.tsx +3 -6
  37. package/app/admin/users/[id]/page.tsx +5 -11
  38. package/app/admin/users/prune/page.tsx +4 -12
  39. package/app/admin/webhooks/page.tsx +144 -0
  40. package/app/api/subscribe/thread/[id]/route.ts +53 -0
  41. package/app/api/unsubscribe/thread/[id]/route.ts +42 -0
  42. package/app/atom.xml/route.ts +2 -2
  43. package/app/auth/sso/[provider]/callback/route.ts +10 -0
  44. package/app/feed.xml/route.ts +2 -2
  45. package/app/offline/route.ts +98 -0
  46. package/next.config.mjs +1 -0
  47. package/package.json +49 -48
  48. package/public/sw.js +42 -2
  49. package/src/components/account/draft-forms.tsx +39 -0
  50. package/src/components/account/feed-token.tsx +95 -0
  51. package/src/components/account/notification-forms.tsx +37 -2
  52. package/src/components/account/relation-forms.tsx +3 -2
  53. package/src/components/account/subscription-forms.tsx +20 -6
  54. package/src/components/account/usercp-forms.tsx +44 -0
  55. package/src/components/admin/api-token-forms.tsx +11 -7
  56. package/src/components/admin/ban-filter-forms.tsx +15 -11
  57. package/src/components/admin/content-forms.tsx +31 -14
  58. package/src/components/admin/forum-forms.tsx +241 -129
  59. package/src/components/admin/forum-tree.tsx +5 -7
  60. package/src/components/admin/navigation-forms.tsx +43 -26
  61. package/src/components/admin/navigation-tree.tsx +8 -13
  62. package/src/components/admin/oklch-picker.tsx +2 -1
  63. package/src/components/admin/theme-changes.tsx +3 -2
  64. package/src/components/admin/theme-forms.tsx +3 -2
  65. package/src/components/admin/webhook-forms.tsx +137 -0
  66. package/src/components/auth/register-form.tsx +4 -7
  67. package/src/components/board/live-region.tsx +3 -1
  68. package/src/components/content/attachment-field.tsx +93 -22
  69. package/src/components/content/combobox-keys.ts +38 -0
  70. package/src/components/content/edit-post-form.tsx +75 -1
  71. package/src/components/content/markdown-editor.tsx +29 -86
  72. package/src/components/content/mention-combobox.tsx +137 -0
  73. package/src/components/content/multiquote-button.tsx +15 -12
  74. package/src/components/content/new-thread-form.tsx +95 -17
  75. package/src/components/content/poll-edit-form.tsx +61 -39
  76. package/src/components/content/poll-vote-form.tsx +91 -0
  77. package/src/components/content/poll.tsx +12 -48
  78. package/src/components/content/progressive-marker.tsx +13 -0
  79. package/src/components/content/recipient-segment.ts +39 -0
  80. package/src/components/content/reply-form.tsx +9 -1
  81. package/src/components/content/thanks-button.tsx +12 -7
  82. package/src/components/messages/message-forms.tsx +53 -43
  83. package/src/components/messages/recipient-field.tsx +88 -0
  84. package/src/components/moderation/inline-moderation-form.tsx +2 -0
  85. package/src/components/moderation/modcp-shell.tsx +17 -3
  86. package/src/components/moderation/queue-form.tsx +3 -6
  87. package/src/components/moderation/report-forms.tsx +24 -5
  88. package/src/components/moderation/thread-tools-form.tsx +2 -0
  89. package/src/components/shell/board-notice.tsx +22 -1
  90. package/src/components/shell/confirm-dialog.tsx +93 -0
  91. package/src/components/shell/nav-disclosure-enhancer.tsx +36 -0
  92. package/src/components/shell/notice-toast.tsx +40 -0
  93. package/src/components/shell/notification-menu.tsx +5 -3
  94. package/src/components/shell/page-shell.tsx +3 -0
  95. package/src/components/shell/scheme-toggle.tsx +72 -0
  96. package/src/components/shell/theme-switcher.tsx +16 -36
  97. package/src/server/admin-settings-actions.ts +5 -0
  98. package/src/server/api/search.ts +8 -2
  99. package/src/server/api-token-actions.ts +4 -0
  100. package/src/server/appearance-actions.ts +21 -0
  101. package/src/server/attachments.ts +35 -0
  102. package/src/server/auth-actions.ts +2 -0
  103. package/src/server/auth-form-state.ts +28 -0
  104. package/src/server/auto-watch.ts +31 -0
  105. package/src/server/ban-filter-admin-actions.ts +5 -0
  106. package/src/server/confirm.ts +19 -0
  107. package/src/server/content-actions.ts +129 -19
  108. package/src/server/content-admin-actions.ts +31 -0
  109. package/src/server/discovery.ts +11 -1
  110. package/src/server/feed-routes.ts +13 -4
  111. package/src/server/feed-token-actions.ts +61 -0
  112. package/src/server/feed-token.ts +78 -0
  113. package/src/server/fixture-post-repo.ts +18 -0
  114. package/src/server/forum-admin-actions.ts +23 -16
  115. package/src/server/forum-admin.ts +10 -1
  116. package/src/server/inline-moderation-actions.ts +6 -0
  117. package/src/server/message-actions.ts +9 -0
  118. package/src/server/navigation-admin-actions.ts +6 -0
  119. package/src/server/plugin-admin.ts +7 -2
  120. package/src/server/plugin-pages.ts +50 -0
  121. package/src/server/plugin-panel.ts +34 -1
  122. package/src/server/plugin-routes.ts +8 -0
  123. package/src/server/plugin-view.tsx +46 -7
  124. package/src/server/poll-actions.ts +13 -1
  125. package/src/server/presence.ts +23 -0
  126. package/src/server/reply-core.ts +8 -1
  127. package/src/server/report-actions.ts +13 -2
  128. package/src/server/reputation-actions.ts +12 -1
  129. package/src/server/search-page.ts +8 -2
  130. package/src/server/search.ts +18 -5
  131. package/src/server/subscription-actions.ts +9 -0
  132. package/src/server/thread-core.ts +10 -2
  133. package/src/server/thread-tool-actions.ts +6 -0
  134. package/src/server/unread-goto.ts +28 -0
  135. package/src/server/upgrade-notice.ts +1 -1
  136. package/src/server/usercp-actions.ts +31 -1
  137. package/src/server/viewer-preferences.ts +5 -0
  138. package/src/server/webhook-actions.ts +98 -0
  139. package/src/server/webhooks-admin.ts +122 -0
  140. package/src/theme/contract.fixture.ts +36 -5
  141. package/src/view/account-copy.ts +39 -0
  142. package/src/view/admin-copy.ts +4 -9
  143. package/src/view/admin-nav.ts +6 -0
  144. package/src/view/admin-panel-copy.ts +23 -0
  145. package/src/view/attachments.ts +33 -1
  146. package/src/view/board-index.ts +31 -2
  147. package/src/view/board-title.ts +1 -0
  148. package/src/view/content-copy.ts +22 -12
  149. package/src/view/copy.ts +17 -0
  150. package/src/view/discovery-view.ts +2 -1
  151. package/src/view/drafts.ts +45 -0
  152. package/src/view/editor-toolbar.ts +14 -0
  153. package/src/view/forum-display.ts +39 -7
  154. package/src/view/modcp-nav.ts +32 -0
  155. package/src/view/moderation-copy.ts +20 -0
  156. package/src/view/notifications.ts +1 -0
  157. package/src/view/plugin-panel.ts +13 -0
  158. package/src/view/poll-vote.ts +46 -0
  159. package/src/view/post-link.ts +8 -1
  160. package/src/view/progressive-enhancement.ts +5 -0
  161. package/src/view/shell.ts +2 -1
  162. package/src/view/subscriptions.ts +1 -0
  163. package/src/view/thread-view.ts +41 -2
  164. package/src/view/usercp-nav.ts +6 -0
  165. package/src/view/usercp.ts +14 -0
@@ -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: false,
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),
@@ -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',
@@ -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