@blogmcp/community 0.2.0 → 0.3.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/README.md +64 -12
- package/dist/blogmcp-community.global.js +7 -7
- package/dist/chunks/{mount-WadUnaTa.js → mount--2_V2q3M.js} +4724 -1097
- package/dist/core/attrs.d.ts +17 -2
- package/dist/core/discussion.d.ts +92 -18
- package/dist/core/follow.d.ts +26 -0
- package/dist/core/forum-category.d.ts +85 -0
- package/dist/core/forum-search.d.ts +35 -0
- package/dist/core/forum-state.d.ts +30 -0
- package/dist/core/forum-words.d.ts +8 -0
- package/dist/core/forum.d.ts +21 -0
- package/dist/core/moderation.d.ts +80 -0
- package/dist/core/onboarding.d.ts +7 -0
- package/dist/core/profile.d.ts +6 -1
- package/dist/core/realtime.d.ts +13 -2
- package/dist/core/topic.d.ts +47 -0
- package/dist/element.d.ts +26 -1
- package/dist/global.d.ts +7 -6
- package/dist/index.d.ts +13 -4
- package/dist/index.js +54 -27
- package/dist/mount.d.ts +50 -1
- package/dist/react.d.ts +17 -4
- package/dist/react.js +32 -8
- package/dist/ui/FollowButtonView.vue.d.ts +17 -0
- package/dist/ui/FollowToggle.vue.d.ts +6 -0
- package/dist/ui/ForumCategoryView.vue.d.ts +13 -0
- package/dist/ui/ForumModerationView.vue.d.ts +12 -0
- package/dist/ui/ForumNotices.vue.d.ts +7 -0
- package/dist/ui/ForumSearchView.vue.d.ts +11 -0
- package/dist/ui/ForumView.vue.d.ts +12 -0
- package/dist/ui/MembersOnly.vue.d.ts +15 -0
- package/dist/ui/MessageComposer.vue.d.ts +6 -2
- package/dist/ui/MessageMenu.vue.d.ts +2 -0
- package/dist/ui/ModeratorDialog.vue.d.ts +11 -0
- package/dist/ui/TopicRow.vue.d.ts +10 -0
- package/dist/ui/TopicView.vue.d.ts +32 -0
- package/dist/ui/context.d.ts +7 -1
- package/dist/ui/discussion-context.d.ts +11 -3
- package/dist/vue.d.ts +164 -5
- package/dist/vue.js +134 -8
- package/package.json +3 -3
- package/src/core/attrs.ts +22 -2
- package/src/core/discussion.ts +309 -43
- package/src/core/follow.ts +139 -0
- package/src/core/forum-category.ts +323 -0
- package/src/core/forum-search.ts +153 -0
- package/src/core/forum-state.ts +59 -0
- package/src/core/forum-words.ts +38 -0
- package/src/core/forum.ts +83 -0
- package/src/core/moderation.ts +414 -0
- package/src/core/onboarding.ts +16 -4
- package/src/core/outcomes.ts +10 -0
- package/src/core/profile.ts +12 -1
- package/src/core/realtime.ts +15 -2
- package/src/core/topic.ts +234 -0
- package/src/element.ts +63 -1
- package/src/global.ts +27 -5
- package/src/index.ts +49 -2
- package/src/mount.ts +144 -2
- package/src/react.ts +45 -2
- package/src/ui/FollowButtonView.vue +85 -0
- package/src/ui/FollowToggle.vue +39 -0
- package/src/ui/ForumCategoryView.vue +243 -0
- package/src/ui/ForumModerationView.vue +309 -0
- package/src/ui/ForumNotices.vue +23 -0
- package/src/ui/ForumSearchView.vue +171 -0
- package/src/ui/ForumView.vue +128 -0
- package/src/ui/MembersOnly.vue +59 -0
- package/src/ui/MessageComposer.vue +51 -9
- package/src/ui/MessageItem.vue +12 -1
- package/src/ui/MessageMenu.vue +3 -2
- package/src/ui/ModeratorDialog.vue +102 -0
- package/src/ui/MyCommunityView.vue +3 -0
- package/src/ui/NoticeStatusView.vue +15 -2
- package/src/ui/NotificationCentreView.vue +37 -3
- package/src/ui/ProfileView.vue +15 -4
- package/src/ui/TopicRow.vue +70 -0
- package/src/ui/TopicView.vue +395 -0
- package/src/ui/context.ts +8 -3
- package/src/ui/discussion-context.ts +11 -3
- package/src/ui/styles.css +14 -0
- package/src/vue.ts +145 -3
package/src/core/realtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The discussion's live updates (m2-contracts.md §3.5, §3.10): `<blogmcp-discussion>`
|
|
2
|
+
* The discussion's live updates (m2-contracts.md §3.5, §3.10): `<blogmcp-discussion>` (and M3 `<blogmcp-topic>`) takes a
|
|
3
|
+
* `realtime` PROPERTY —
|
|
3
4
|
* the session factory of `@blogmcp/sdk/community/realtime`, which the HOST imports and passes:
|
|
4
5
|
*
|
|
5
6
|
* import { createRealtimeSession } from '@blogmcp/sdk/community/realtime'
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
*
|
|
12
13
|
* The types here are the factory's shape as far as the element uses it, so `createRealtimeSession` fits as it is.
|
|
13
14
|
*/
|
|
14
|
-
import type { CommunityClient, DiscussionStore } from '@blogmcp/sdk/community'
|
|
15
|
+
import type { CommunityClient, DiscussionStore, TopicStore } from '@blogmcp/sdk/community'
|
|
15
16
|
|
|
16
17
|
/** `connecting`, `live`, `polling` (live updates are off or the server cut the socket for good), `stopped`. */
|
|
17
18
|
export type DiscussionRealtimeMode = 'connecting' | 'live' | 'polling' | 'stopped' | (string & {})
|
|
@@ -37,3 +38,15 @@ export interface DiscussionRealtimeSession {
|
|
|
37
38
|
|
|
38
39
|
/** `createRealtimeSession` of `@blogmcp/sdk/community/realtime`, or any function of its shape. */
|
|
39
40
|
export type DiscussionRealtime = (options: DiscussionRealtimeOptions) => DiscussionRealtimeSession
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* M3 (m3-contracts.md §3.5, §5.3): what `<blogmcp-topic>` calls the same factory with — the topic's store
|
|
44
|
+
* (`createTopicStore`) in place of a discussion's; `createRealtimeSession` takes both, re-reads a topic with
|
|
45
|
+
* `forum/topics/get` and falls back to `forum/topics/head`.
|
|
46
|
+
*/
|
|
47
|
+
export interface TopicRealtimeOptions extends Omit<DiscussionRealtimeOptions, 'store'> {
|
|
48
|
+
store: TopicStore
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `createRealtimeSession`, as `<blogmcp-topic>`'s `realtime` property takes it. */
|
|
52
|
+
export type TopicRealtime = (options: TopicRealtimeOptions) => DiscussionRealtimeSession
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<blogmcp-topic conversation-id>`'s brain (m3-contracts.md §3.1–§3.5, §4.5, §5.3): the topic's thread — the
|
|
3
|
+
* discussion's brain over the SDK's topic store (createTopicThreadController: the composer, replies, edits with the
|
|
4
|
+
* title of the opening post, reactions, reports, blocks, the onboarding, the brakes, live updates or the head poll) —
|
|
5
|
+
* and what the topic has of its own:
|
|
6
|
+
*
|
|
7
|
+
* - its header (title, state, pin, category) from `forum/topics/get`, a title read through topicTitleOf (deleted, or
|
|
8
|
+
* withheld from this viewer);
|
|
9
|
+
* - the follow button (the topic's `following`; `me/follows/set` by its conversation id);
|
|
10
|
+
* - for a moderator of its category (`viewer.moderator`): close or reopen it, pin or unpin it, move it into another
|
|
11
|
+
* category they moderate, and on a message — remove it (a rule and the facts; the statement comes back) or pass it to
|
|
12
|
+
* the team (`moderation/escalate`, a category of the illegal line). kb-api re-checks the appointment on every call:
|
|
13
|
+
* a refusal is `notFound`, and the topic is read again.
|
|
14
|
+
*/
|
|
15
|
+
import { reactive } from 'vue'
|
|
16
|
+
import {
|
|
17
|
+
ESCALATION_CATEGORIES,
|
|
18
|
+
communityRulesOf,
|
|
19
|
+
isCategoryModerator,
|
|
20
|
+
type CaseCategory,
|
|
21
|
+
type CommunityCallOptions,
|
|
22
|
+
type CommunityClient,
|
|
23
|
+
type CommunityFailure,
|
|
24
|
+
type CommunityRule,
|
|
25
|
+
type DiscussionMessage,
|
|
26
|
+
type ForumCategory,
|
|
27
|
+
type Topic,
|
|
28
|
+
} from '@blogmcp/sdk/community'
|
|
29
|
+
import { createTopicThreadController, type TopicThreadController, type TopicThreadControllerOptions } from './discussion'
|
|
30
|
+
import { createFollow, type FollowController } from './follow'
|
|
31
|
+
import type { TextRef } from './i18n'
|
|
32
|
+
import { failureText } from './outcomes'
|
|
33
|
+
import { codePointLength, normalizeBody } from './text'
|
|
34
|
+
|
|
35
|
+
/** The facts of a moderator's removal, as an appeal's text (kb-api's `facts` limit is its own; the composer's is safe). */
|
|
36
|
+
const FACTS_MAX = 4000
|
|
37
|
+
|
|
38
|
+
export type ModeratorDialogKind = 'remove' | 'escalate' | 'move'
|
|
39
|
+
|
|
40
|
+
/** A moderator's open dialog: a message's removal or escalation, or the topic's move. */
|
|
41
|
+
export interface ModeratorDialog {
|
|
42
|
+
kind: ModeratorDialogKind
|
|
43
|
+
/** The message (remove, escalate). */
|
|
44
|
+
seq: number | null
|
|
45
|
+
ruleId: string
|
|
46
|
+
facts: string
|
|
47
|
+
category: CaseCategory | ''
|
|
48
|
+
categoryId: string
|
|
49
|
+
sending: boolean
|
|
50
|
+
failure: CommunityFailure | null
|
|
51
|
+
/** A removal's statement of reasons, as kb-api rendered it. */
|
|
52
|
+
statement: string | null
|
|
53
|
+
done: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TopicExtrasUi {
|
|
57
|
+
/** The rules of the tenant's current rules text (a moderator's removal cites one), once read. */
|
|
58
|
+
rules: CommunityRule[] | null
|
|
59
|
+
/** The categories a moderator may move the topic into (theirs, but this one), once read. */
|
|
60
|
+
moveTargets: ForumCategory[] | null
|
|
61
|
+
dialog: ModeratorDialog | null
|
|
62
|
+
busy: Record<string, boolean>
|
|
63
|
+
announcement: TextRef | null
|
|
64
|
+
announceId: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TopicController {
|
|
68
|
+
readonly thread: TopicThreadController
|
|
69
|
+
readonly follow: FollowController | null
|
|
70
|
+
readonly extras: TopicExtrasUi
|
|
71
|
+
start(): Promise<void>
|
|
72
|
+
/** The viewer moderates the topic's category (kb-api's `viewer.moderator`, me/get's appointments beside it). */
|
|
73
|
+
isModerator(): boolean
|
|
74
|
+
/** A moderator's message actions apply (another member's live message). */
|
|
75
|
+
canModerate(m: DiscussionMessage): boolean
|
|
76
|
+
setState(state: 'open' | 'locked'): Promise<void>
|
|
77
|
+
setPinned(pinned: boolean): Promise<void>
|
|
78
|
+
openDialog(kind: ModeratorDialogKind, seq?: number | null): void
|
|
79
|
+
closeDialog(): void
|
|
80
|
+
submitDialog(): Promise<void>
|
|
81
|
+
dispose(): void
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createTopicController(o: TopicThreadControllerOptions): TopicController {
|
|
85
|
+
const { client, lang } = o
|
|
86
|
+
const conversationId = o.conversationId.toLowerCase()
|
|
87
|
+
const thread = createTopicThreadController(o)
|
|
88
|
+
const abort = new AbortController()
|
|
89
|
+
const call: CommunityCallOptions = { signal: abort.signal }
|
|
90
|
+
let disposed = false
|
|
91
|
+
const follow: FollowController | null = createFollow({ client, target: { conversationId }, lang })
|
|
92
|
+
|
|
93
|
+
const extras = reactive<TopicExtrasUi>({ rules: null, moveTargets: null, dialog: null, busy: {}, announcement: null, announceId: 0 }) as TopicExtrasUi
|
|
94
|
+
|
|
95
|
+
const announce = (text: TextRef): void => {
|
|
96
|
+
extras.announcement = text
|
|
97
|
+
extras.announceId += 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const topic = () => thread.state.thread.topic
|
|
101
|
+
|
|
102
|
+
// The follow button follows the header: a re-read (another tab, a resync) may change it.
|
|
103
|
+
let lastFollowing: boolean | null = null
|
|
104
|
+
const unsubscribe = thread.store.subscribe((s) => {
|
|
105
|
+
const following = s.topic?.following
|
|
106
|
+
if (typeof following !== 'boolean' || following === lastFollowing) return
|
|
107
|
+
if (lastFollowing === null && follow && follow.state.phase === 'idle') void follow.load(following)
|
|
108
|
+
else follow?.sync(following)
|
|
109
|
+
lastFollowing = following
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
function isModerator(): boolean {
|
|
113
|
+
const t = topic()
|
|
114
|
+
if (!t?.category) return false
|
|
115
|
+
return thread.state.thread.viewer.moderator === true || (thread.state.me !== null && isCategoryModerator(thread.state.me, t.category.id))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const canModerate = (m: DiscussionMessage): boolean =>
|
|
119
|
+
isModerator() && !thread.isOwn(m) && !m.pending && m.seq > 0 && m.body !== null && m.state !== 'deleted' && m.author.kind === 'member'
|
|
120
|
+
|
|
121
|
+
/** A moderator's change answered the topic: the store takes the header; a refusal reads the topic again. */
|
|
122
|
+
async function topicChange(key: string, run: () => Promise<{ ok: true; value: { topic: Topic } } | CommunityFailure>): Promise<void> {
|
|
123
|
+
if (extras.busy[key]) return
|
|
124
|
+
extras.busy[key] = true
|
|
125
|
+
const r = await run()
|
|
126
|
+
delete extras.busy[key]
|
|
127
|
+
if (disposed) return
|
|
128
|
+
if (r.ok) {
|
|
129
|
+
thread.store.applyTopic(r.value.topic)
|
|
130
|
+
announce({ key: 'moderation.done' })
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
announce(failureText(r, lang))
|
|
134
|
+
// The appointment or the topic changed under the page (kb-api's notFound): what is true now.
|
|
135
|
+
if (r.code === 'notFound') void thread.refresh()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loadRules(): Promise<void> {
|
|
139
|
+
if (extras.rules !== null) return
|
|
140
|
+
const r = await client.legal.current({ kind: 'rules', locale: lang }, call)
|
|
141
|
+
if (!disposed && r.ok) extras.rules = communityRulesOf(r.value.bodyJson)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function loadMoveTargets(): Promise<void> {
|
|
145
|
+
if (extras.moveTargets !== null) return
|
|
146
|
+
const r = await client.forum.categories.list(call)
|
|
147
|
+
if (disposed || !r.ok) return
|
|
148
|
+
const here = topic()?.category?.id
|
|
149
|
+
const me = thread.state.me
|
|
150
|
+
extras.moveTargets = r.value.categories.filter((c) => c.kind === 'topics' && c.id !== here && me !== null && isCategoryModerator(me, c.id))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
thread,
|
|
155
|
+
follow,
|
|
156
|
+
extras,
|
|
157
|
+
async start() {
|
|
158
|
+
await thread.start()
|
|
159
|
+
},
|
|
160
|
+
isModerator,
|
|
161
|
+
canModerate,
|
|
162
|
+
setState: (state) => topicChange(`state:${state}`, () => client.moderation.setTopicState({ conversationId, state }, call)),
|
|
163
|
+
setPinned: (pinned) => topicChange(`pin:${pinned}`, () => client.moderation.pinTopic({ conversationId, pinned }, call)),
|
|
164
|
+
openDialog(kind, seq = null) {
|
|
165
|
+
extras.dialog = { kind, seq, ruleId: '', facts: '', category: '', categoryId: '', sending: false, failure: null, statement: null, done: false }
|
|
166
|
+
if (kind === 'remove') void loadRules()
|
|
167
|
+
if (kind === 'move') void loadMoveTargets()
|
|
168
|
+
},
|
|
169
|
+
closeDialog() {
|
|
170
|
+
extras.dialog = null
|
|
171
|
+
},
|
|
172
|
+
async submitDialog() {
|
|
173
|
+
const d = extras.dialog
|
|
174
|
+
if (!d || d.sending || d.done) return
|
|
175
|
+
if (d.kind === 'move') {
|
|
176
|
+
if (d.categoryId === '') return
|
|
177
|
+
d.sending = true
|
|
178
|
+
const r = await client.moderation.moveTopic({ conversationId, categoryId: d.categoryId }, call)
|
|
179
|
+
d.sending = false
|
|
180
|
+
if (disposed) return
|
|
181
|
+
if (r.ok) {
|
|
182
|
+
thread.store.applyTopic(r.value.topic)
|
|
183
|
+
extras.dialog = null
|
|
184
|
+
extras.moveTargets = null
|
|
185
|
+
announce({ key: 'moderation.done' })
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
d.failure = r
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
const m = d.seq === null ? null : thread.state.thread.messages.flatMap((x) => [x, ...x.replies]).find((x) => x.seq === d.seq)
|
|
192
|
+
if (!m) return
|
|
193
|
+
if (d.kind === 'escalate') {
|
|
194
|
+
if (d.category === '' || !ESCALATION_CATEGORIES.includes(d.category)) return
|
|
195
|
+
d.sending = true
|
|
196
|
+
const r = await client.moderation.escalate({ conversationId, seq: m.seq, category: d.category }, call)
|
|
197
|
+
d.sending = false
|
|
198
|
+
if (disposed) return
|
|
199
|
+
if (r.ok) {
|
|
200
|
+
d.done = true
|
|
201
|
+
announce({ key: 'moderation.escalated' })
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
d.failure = r
|
|
205
|
+
if (r.code === 'notFound') void thread.refresh()
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
// remove: a rule of the current rules text and the facts, as the statement will state them.
|
|
209
|
+
const facts = normalizeBody(d.facts)
|
|
210
|
+
if (d.ruleId === '' || facts === '' || codePointLength(facts) > FACTS_MAX) return
|
|
211
|
+
d.sending = true
|
|
212
|
+
const r = await client.moderation.remove({ conversationId, seq: m.seq, ruleId: d.ruleId, facts, locale: lang }, call)
|
|
213
|
+
d.sending = false
|
|
214
|
+
if (disposed) return
|
|
215
|
+
if (r.ok) {
|
|
216
|
+
d.done = true
|
|
217
|
+
d.statement = r.value.statement
|
|
218
|
+
announce({ key: 'moderation.removed' })
|
|
219
|
+
void thread.refresh()
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
d.failure = r
|
|
223
|
+
if (r.code === 'notFound') void thread.refresh()
|
|
224
|
+
},
|
|
225
|
+
dispose() {
|
|
226
|
+
if (disposed) return
|
|
227
|
+
disposed = true
|
|
228
|
+
abort.abort()
|
|
229
|
+
unsubscribe()
|
|
230
|
+
follow?.dispose()
|
|
231
|
+
thread.dispose()
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
}
|
package/src/element.ts
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
* <blogmcp-member-settings> api-base, site-base, lang, sw-path, routes, sign-in-href, nonce (M2)
|
|
12
12
|
* <blogmcp-profile> handle, api-base, site-base, lang, routes, sign-in-href, nonce (M2)
|
|
13
13
|
* <blogmcp-welcome> api-base, site-base, lang, routes, sign-in-href, nonce (M2)
|
|
14
|
+
* <blogmcp-forum> api-base, site-base, lang, routes, sign-in-href, nonce (M3)
|
|
15
|
+
* <blogmcp-forum-category> slug, api-base, site-base, lang, routes, sign-in-href, nonce (M3)
|
|
16
|
+
* <blogmcp-topic> conversation-id, api-base, site-base, lang, routes, sign-in-href, poll-ms,
|
|
17
|
+
* nonce (+ the `realtime` property, as the discussion's) (M3)
|
|
18
|
+
* <blogmcp-forum-search> api-base, site-base, lang, routes, sign-in-href, nonce (M3)
|
|
19
|
+
* <blogmcp-follow-button> target-kind, target-id, following, api-base, site-base, lang, nonce (M3)
|
|
20
|
+
* <blogmcp-forum-moderation> category-id, api-base, site-base, lang, routes, nonce (M3)
|
|
14
21
|
*
|
|
15
22
|
* Each carries ONE inline stylesheet in its shadow root, with the CSP nonce honoured: the one given when the
|
|
16
23
|
* elements are defined (the script build passes `document.currentScript.nonce`), else the element's own
|
|
@@ -19,11 +26,17 @@
|
|
|
19
26
|
import { defineCustomElement, type Component } from 'vue'
|
|
20
27
|
import BadgeView from './ui/BadgeView.vue'
|
|
21
28
|
import DiscussionView from './ui/DiscussionView.vue'
|
|
29
|
+
import FollowButtonView from './ui/FollowButtonView.vue'
|
|
30
|
+
import ForumCategoryView from './ui/ForumCategoryView.vue'
|
|
31
|
+
import ForumModerationView from './ui/ForumModerationView.vue'
|
|
32
|
+
import ForumSearchView from './ui/ForumSearchView.vue'
|
|
33
|
+
import ForumView from './ui/ForumView.vue'
|
|
22
34
|
import MemberSettingsView from './ui/MemberSettingsView.vue'
|
|
23
35
|
import MyCommunityView from './ui/MyCommunityView.vue'
|
|
24
36
|
import NoticeFormView from './ui/NoticeFormView.vue'
|
|
25
37
|
import NotificationCentreView from './ui/NotificationCentreView.vue'
|
|
26
38
|
import ProfileView from './ui/ProfileView.vue'
|
|
39
|
+
import TopicView from './ui/TopicView.vue'
|
|
27
40
|
import WelcomeView from './ui/WelcomeView.vue'
|
|
28
41
|
import css from './ui/styles.css?inline'
|
|
29
42
|
|
|
@@ -53,6 +66,13 @@ export const BADGE_ELEMENT_TAG = 'blogmcp-badge'
|
|
|
53
66
|
export const MEMBER_SETTINGS_ELEMENT_TAG = 'blogmcp-member-settings'
|
|
54
67
|
export const PROFILE_ELEMENT_TAG = 'blogmcp-profile'
|
|
55
68
|
export const WELCOME_ELEMENT_TAG = 'blogmcp-welcome'
|
|
69
|
+
/** M3 (m3-contracts.md §5.3): the forum's elements — they live here, not in the SDK (A33). */
|
|
70
|
+
export const FORUM_ELEMENT_TAG = 'blogmcp-forum'
|
|
71
|
+
export const FORUM_CATEGORY_ELEMENT_TAG = 'blogmcp-forum-category'
|
|
72
|
+
export const TOPIC_ELEMENT_TAG = 'blogmcp-topic'
|
|
73
|
+
export const FORUM_SEARCH_ELEMENT_TAG = 'blogmcp-forum-search'
|
|
74
|
+
export const FOLLOW_BUTTON_ELEMENT_TAG = 'blogmcp-follow-button'
|
|
75
|
+
export const FORUM_MODERATION_ELEMENT_TAG = 'blogmcp-forum-moderation'
|
|
56
76
|
|
|
57
77
|
export interface DefineCommunityElementOptions {
|
|
58
78
|
/** Another tag name for the element. */
|
|
@@ -73,6 +93,12 @@ export interface DefineCommunityElementsOptions {
|
|
|
73
93
|
memberSettings?: string
|
|
74
94
|
profile?: string
|
|
75
95
|
welcome?: string
|
|
96
|
+
forum?: string
|
|
97
|
+
forumCategory?: string
|
|
98
|
+
topic?: string
|
|
99
|
+
forumSearch?: string
|
|
100
|
+
followButton?: string
|
|
101
|
+
forumModeration?: string
|
|
76
102
|
}
|
|
77
103
|
}
|
|
78
104
|
|
|
@@ -152,7 +178,37 @@ export function defineWelcomeElement(opts: DefineCommunityElementOptions = {}):
|
|
|
152
178
|
define(opts.tag ?? WELCOME_ELEMENT_TAG, WelcomeView, opts.nonce)
|
|
153
179
|
}
|
|
154
180
|
|
|
155
|
-
/**
|
|
181
|
+
/** M3: register `<blogmcp-forum>` (once). Browser only. */
|
|
182
|
+
export function defineForumElement(opts: DefineCommunityElementOptions = {}): void {
|
|
183
|
+
define(opts.tag ?? FORUM_ELEMENT_TAG, ForumView, opts.nonce)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** M3: register `<blogmcp-forum-category>` (once). Browser only. */
|
|
187
|
+
export function defineForumCategoryElement(opts: DefineCommunityElementOptions = {}): void {
|
|
188
|
+
define(opts.tag ?? FORUM_CATEGORY_ELEMENT_TAG, ForumCategoryView, opts.nonce)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** M3: register `<blogmcp-topic>` (once). Browser only. */
|
|
192
|
+
export function defineTopicElement(opts: DefineCommunityElementOptions = {}): void {
|
|
193
|
+
define(opts.tag ?? TOPIC_ELEMENT_TAG, TopicView, opts.nonce)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** M3: register `<blogmcp-forum-search>` (once). Browser only. */
|
|
197
|
+
export function defineForumSearchElement(opts: DefineCommunityElementOptions = {}): void {
|
|
198
|
+
define(opts.tag ?? FORUM_SEARCH_ELEMENT_TAG, ForumSearchView, opts.nonce)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** M3: register `<blogmcp-follow-button>` (once). Browser only. */
|
|
202
|
+
export function defineFollowButtonElement(opts: DefineCommunityElementOptions = {}): void {
|
|
203
|
+
define(opts.tag ?? FOLLOW_BUTTON_ELEMENT_TAG, FollowButtonView, opts.nonce)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** M3: register `<blogmcp-forum-moderation>` (once). Browser only. */
|
|
207
|
+
export function defineForumModerationElement(opts: DefineCommunityElementOptions = {}): void {
|
|
208
|
+
define(opts.tag ?? FORUM_MODERATION_ELEMENT_TAG, ForumModerationView, opts.nonce)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Register all fourteen (once). Browser only. */
|
|
156
212
|
export function defineCommunityElements(opts: DefineCommunityElementsOptions = {}): void {
|
|
157
213
|
defineDiscussionElement({ tag: opts.tags?.discussion, nonce: opts.nonce })
|
|
158
214
|
defineNoticeFormElement({ tag: opts.tags?.noticeForm, nonce: opts.nonce })
|
|
@@ -162,4 +218,10 @@ export function defineCommunityElements(opts: DefineCommunityElementsOptions = {
|
|
|
162
218
|
defineMemberSettingsElement({ tag: opts.tags?.memberSettings, nonce: opts.nonce })
|
|
163
219
|
defineProfileElement({ tag: opts.tags?.profile, nonce: opts.nonce })
|
|
164
220
|
defineWelcomeElement({ tag: opts.tags?.welcome, nonce: opts.nonce })
|
|
221
|
+
defineForumElement({ tag: opts.tags?.forum, nonce: opts.nonce })
|
|
222
|
+
defineForumCategoryElement({ tag: opts.tags?.forumCategory, nonce: opts.nonce })
|
|
223
|
+
defineTopicElement({ tag: opts.tags?.topic, nonce: opts.nonce })
|
|
224
|
+
defineForumSearchElement({ tag: opts.tags?.forumSearch, nonce: opts.nonce })
|
|
225
|
+
defineFollowButtonElement({ tag: opts.tags?.followButton, nonce: opts.nonce })
|
|
226
|
+
defineForumModerationElement({ tag: opts.tags?.forumModeration, nonce: opts.nonce })
|
|
165
227
|
}
|
package/src/global.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `<script>` build: registers every element — `<blogmcp-discussion>`, `<blogmcp-notice-form>`,
|
|
3
|
-
* `<blogmcp-my-community
|
|
4
|
-
* `<blogmcp-profile>`, `<blogmcp-welcome>`
|
|
5
|
-
*
|
|
6
|
-
* and
|
|
3
|
+
* `<blogmcp-my-community>`, (M2) `<blogmcp-notification-centre>`, `<blogmcp-badge>`, `<blogmcp-member-settings>`,
|
|
4
|
+
* `<blogmcp-profile>`, `<blogmcp-welcome>` and (M3) `<blogmcp-forum>`, `<blogmcp-forum-category>`, `<blogmcp-topic>`,
|
|
5
|
+
* `<blogmcp-forum-search>`, `<blogmcp-follow-button>`, `<blogmcp-forum-moderation>` — with the script tag's own CSP
|
|
6
|
+
* nonce, and exposes `window.BlogmcpCommunity`. Live updates are not in it: a site that wants them bundles
|
|
7
|
+
* `@blogmcp/sdk/community/realtime` and sets the discussion's (or the topic's) `realtime` property.
|
|
7
8
|
*/
|
|
8
9
|
import { defineCommunityElements } from './element'
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
mountBadge,
|
|
12
|
+
mountDiscussion,
|
|
13
|
+
mountFollowButton,
|
|
14
|
+
mountForum,
|
|
15
|
+
mountForumCategory,
|
|
16
|
+
mountForumModeration,
|
|
17
|
+
mountForumSearch,
|
|
18
|
+
mountMemberSettings,
|
|
19
|
+
mountMyCommunity,
|
|
20
|
+
mountNoticeForm,
|
|
21
|
+
mountNotificationCentre,
|
|
22
|
+
mountProfile,
|
|
23
|
+
mountTopic,
|
|
24
|
+
mountWelcome,
|
|
25
|
+
} from './mount'
|
|
10
26
|
import { VERSION } from './version'
|
|
11
27
|
|
|
12
28
|
const script = typeof document !== 'undefined' ? (document.currentScript as HTMLScriptElement | null) : null
|
|
@@ -22,5 +38,11 @@ export {
|
|
|
22
38
|
mountMemberSettings,
|
|
23
39
|
mountProfile,
|
|
24
40
|
mountWelcome,
|
|
41
|
+
mountForum,
|
|
42
|
+
mountForumCategory,
|
|
43
|
+
mountTopic,
|
|
44
|
+
mountForumSearch,
|
|
45
|
+
mountFollowButton,
|
|
46
|
+
mountForumModeration,
|
|
25
47
|
VERSION,
|
|
26
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* - M2: `<blogmcp-notification-centre>` the notifications, `<blogmcp-badge>` the header's unread count,
|
|
11
11
|
* `<blogmcp-member-settings>` the switches, push and the digest's unsubscribe, `<blogmcp-profile>` a member's
|
|
12
12
|
* public profile, `<blogmcp-welcome>` the welcome page; the discussion's `realtime` property (live updates);
|
|
13
|
+
* - M3, the forum: `<blogmcp-forum>` its index, `<blogmcp-forum-category slug>` a category, `<blogmcp-topic
|
|
14
|
+
* conversation-id>` a topic (the discussion's view, with its `realtime` property), `<blogmcp-forum-search>`,
|
|
15
|
+
* `<blogmcp-follow-button target-kind target-id>`, `<blogmcp-forum-moderation>` a category moderator's queue;
|
|
13
16
|
* - `defineCommunityElements()` and one `define…Element()` each — the custom elements;
|
|
14
17
|
* - `mountDiscussion(…)`, `mountNoticeForm(…)`, `mountMyCommunity(…)` — mount one from code;
|
|
15
18
|
* - `@blogmcp/community/vue`, `@blogmcp/community/react` — `<KbDiscussion>`, `<KbNoticeForm>`, `<KbMyCommunity>`;
|
|
@@ -25,6 +28,12 @@ export {
|
|
|
25
28
|
defineMemberSettingsElement,
|
|
26
29
|
defineProfileElement,
|
|
27
30
|
defineWelcomeElement,
|
|
31
|
+
defineForumElement,
|
|
32
|
+
defineForumCategoryElement,
|
|
33
|
+
defineTopicElement,
|
|
34
|
+
defineForumSearchElement,
|
|
35
|
+
defineFollowButtonElement,
|
|
36
|
+
defineForumModerationElement,
|
|
28
37
|
DISCUSSION_ELEMENT_TAG,
|
|
29
38
|
NOTICE_FORM_ELEMENT_TAG,
|
|
30
39
|
MY_COMMUNITY_ELEMENT_TAG,
|
|
@@ -33,6 +42,12 @@ export {
|
|
|
33
42
|
MEMBER_SETTINGS_ELEMENT_TAG,
|
|
34
43
|
PROFILE_ELEMENT_TAG,
|
|
35
44
|
WELCOME_ELEMENT_TAG,
|
|
45
|
+
FORUM_ELEMENT_TAG,
|
|
46
|
+
FORUM_CATEGORY_ELEMENT_TAG,
|
|
47
|
+
TOPIC_ELEMENT_TAG,
|
|
48
|
+
FORUM_SEARCH_ELEMENT_TAG,
|
|
49
|
+
FOLLOW_BUTTON_ELEMENT_TAG,
|
|
50
|
+
FORUM_MODERATION_ELEMENT_TAG,
|
|
36
51
|
type DefineCommunityElementOptions,
|
|
37
52
|
type DefineCommunityElementsOptions,
|
|
38
53
|
} from './element'
|
|
@@ -45,6 +60,12 @@ export {
|
|
|
45
60
|
mountMemberSettings,
|
|
46
61
|
mountProfile,
|
|
47
62
|
mountWelcome,
|
|
63
|
+
mountForum,
|
|
64
|
+
mountForumCategory,
|
|
65
|
+
mountTopic,
|
|
66
|
+
mountForumSearch,
|
|
67
|
+
mountFollowButton,
|
|
68
|
+
mountForumModeration,
|
|
48
69
|
type MountDiscussionOptions,
|
|
49
70
|
type MountNoticeFormOptions,
|
|
50
71
|
type MountMyCommunityOptions,
|
|
@@ -53,13 +74,39 @@ export {
|
|
|
53
74
|
type MountMemberSettingsOptions,
|
|
54
75
|
type MountProfileOptions,
|
|
55
76
|
type MountWelcomeOptions,
|
|
77
|
+
type MountForumOptions,
|
|
78
|
+
type MountForumCategoryOptions,
|
|
79
|
+
type MountTopicOptions,
|
|
80
|
+
type MountForumSearchOptions,
|
|
81
|
+
type MountFollowButtonOptions,
|
|
82
|
+
type MountForumModerationOptions,
|
|
56
83
|
} from './mount'
|
|
57
84
|
export { createNotificationCentre, type NotificationCentreController, type CentreUi } from './core/centre'
|
|
58
85
|
export { createBadge, badgeText, BADGE_INTERVAL_MS, type BadgeController, type BadgeOptions, type BadgeUi } from './core/badge'
|
|
59
86
|
export { createMemberSettings, SETTING_KEYS, type MemberSettingsController, type MemberSettingsOptions, type SettingsUi, type SettingKey } from './core/settings'
|
|
60
87
|
export { createProfile, createWelcome, type ProfileController, type WelcomeController } from './core/profile'
|
|
61
|
-
export type { DiscussionRealtime, DiscussionRealtimeOptions, DiscussionRealtimeSession, DiscussionRealtimeMode } from './core/realtime'
|
|
62
|
-
export {
|
|
88
|
+
export type { DiscussionRealtime, DiscussionRealtimeOptions, DiscussionRealtimeSession, DiscussionRealtimeMode, TopicRealtime, TopicRealtimeOptions } from './core/realtime'
|
|
89
|
+
export {
|
|
90
|
+
createDiscussionController,
|
|
91
|
+
createTopicThreadController,
|
|
92
|
+
type DiscussionController,
|
|
93
|
+
type DiscussionControllerOptions,
|
|
94
|
+
type DiscussionUi,
|
|
95
|
+
type ComposerMode,
|
|
96
|
+
type Draft,
|
|
97
|
+
type ThreadController,
|
|
98
|
+
type ThreadState,
|
|
99
|
+
type ThreadUi,
|
|
100
|
+
type TopicThreadController,
|
|
101
|
+
type TopicThreadControllerOptions,
|
|
102
|
+
} from './core/discussion'
|
|
103
|
+
// M3: the forum's brains (framework-free but for Vue's reactivity, as the others), for a host that renders its own view.
|
|
104
|
+
export { createForum, type ForumController, type ForumUi } from './core/forum'
|
|
105
|
+
export { createCategory, type CategoryController, type CategoryUi, type TopicDraft } from './core/forum-category'
|
|
106
|
+
export { createTopicController, type TopicController, type ModeratorDialog } from './core/topic'
|
|
107
|
+
export { createSearch, queryFromHash, searchLink, type SearchController, type SearchUi } from './core/forum-search'
|
|
108
|
+
export { createFollow, type FollowController, type FollowUi } from './core/follow'
|
|
109
|
+
export { createModeration, type ModerationController, type ModerationUi } from './core/moderation'
|
|
63
110
|
export { createNoticeForm, type NoticeFormController, type NoticeFormOptions, type NoticeFormUi } from './core/notice'
|
|
64
111
|
export { createPanel, type PanelController, type PanelOptions, type PanelUi } from './core/panel'
|
|
65
112
|
export { createOnboarding, type OnboardingModel, type OnboardingOptions, type OnboardingStep } from './core/onboarding'
|