@blogmcp/community 0.1.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 +162 -13
- package/dist/blogmcp-community.global.js +8 -8
- package/dist/chunks/mount--2_V2q3M.js +10063 -0
- package/dist/core/attrs.d.ts +42 -1
- package/dist/core/badge.d.ts +30 -0
- package/dist/core/centre.d.ts +28 -0
- package/dist/core/digest-token.d.ts +2 -0
- package/dist/core/discussion.d.ts +120 -15
- package/dist/core/follow.d.ts +26 -0
- package/dist/core/format.d.ts +7 -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 +49 -2
- package/dist/core/outcomes.d.ts +2 -1
- package/dist/core/panel.d.ts +17 -1
- package/dist/core/profile.d.ts +45 -0
- package/dist/core/push.d.ts +63 -0
- package/dist/core/realtime.d.ts +46 -0
- package/dist/core/settings.d.ts +53 -0
- package/dist/core/text.d.ts +13 -0
- package/dist/core/topic.d.ts +47 -0
- package/dist/element.d.ts +47 -1
- package/dist/global.d.ts +8 -4
- package/dist/index.d.ts +20 -3
- package/dist/index.js +62 -12
- package/dist/mount.d.ts +89 -0
- package/dist/react.d.ts +27 -3
- package/dist/react.js +48 -4
- package/dist/ui/BadgeView.vue.d.ts +13 -0
- package/dist/ui/DiscussionView.vue.d.ts +11 -0
- 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/LinkedText.vue.d.ts +1 -0
- package/dist/ui/MemberSettingsView.vue.d.ts +13 -0
- package/dist/ui/MembersOnly.vue.d.ts +15 -0
- package/dist/ui/MessageComposer.vue.d.ts +8 -2
- package/dist/ui/MessageMenu.vue.d.ts +2 -0
- package/dist/ui/ModeratorDialog.vue.d.ts +11 -0
- package/dist/ui/NotificationCentreView.vue.d.ts +12 -0
- package/dist/ui/OnboardingSteps.vue.d.ts +1 -1
- package/dist/ui/ProfileView.vue.d.ts +13 -0
- package/dist/ui/TopicRow.vue.d.ts +10 -0
- package/dist/ui/TopicView.vue.d.ts +32 -0
- package/dist/ui/WelcomeView.vue.d.ts +12 -0
- package/dist/ui/context.d.ts +7 -1
- package/dist/ui/discussion-context.d.ts +11 -3
- package/dist/vue.d.ts +273 -3
- package/dist/vue.js +235 -5
- package/package.json +3 -3
- package/src/core/attrs.ts +61 -2
- package/src/core/badge.ts +120 -0
- package/src/core/centre.ts +136 -0
- package/src/core/digest-token.ts +26 -0
- package/src/core/discussion.ts +497 -46
- package/src/core/follow.ts +139 -0
- package/src/core/format.ts +22 -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 +177 -18
- package/src/core/outcomes.ts +23 -1
- package/src/core/panel.ts +39 -4
- package/src/core/profile.ts +155 -0
- package/src/core/push.ts +174 -0
- package/src/core/realtime.ts +52 -0
- package/src/core/settings.ts +363 -0
- package/src/core/text.ts +32 -0
- package/src/core/topic.ts +234 -0
- package/src/element.ts +124 -6
- package/src/global.ts +40 -4
- package/src/index.ts +84 -2
- package/src/mount.ts +250 -1
- package/src/react.ts +90 -3
- package/src/ui/BadgeView.vue +51 -0
- package/src/ui/DiscussionView.vue +43 -3
- 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/LinkedText.vue +11 -5
- package/src/ui/MemberSettingsView.vue +175 -0
- package/src/ui/MembersOnly.vue +59 -0
- package/src/ui/MessageComposer.vue +64 -11
- package/src/ui/MessageItem.vue +36 -7
- package/src/ui/MessageMenu.vue +3 -2
- package/src/ui/ModeratorDialog.vue +102 -0
- package/src/ui/MyCommunityView.vue +12 -0
- package/src/ui/NoticeStatusView.vue +15 -2
- package/src/ui/NotificationCentreView.vue +193 -0
- package/src/ui/OnboardingSteps.vue +17 -6
- package/src/ui/ProfileView.vue +102 -0
- package/src/ui/TopicRow.vue +70 -0
- package/src/ui/TopicView.vue +395 -0
- package/src/ui/WelcomeView.vue +72 -0
- package/src/ui/context.ts +8 -3
- package/src/ui/discussion-context.ts +11 -3
- package/src/ui/styles.css +30 -0
- package/src/vue.ts +271 -3
- package/dist/chunks/mount-DzjwnHyx.js +0 -4597
|
@@ -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
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
/// <reference path="./shims.d.ts" />
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
3
|
+
* The custom elements (Vue inside a shadow root each), m1-contracts.md §10.4 and m2-contracts.md §3.10:
|
|
4
4
|
*
|
|
5
|
-
* <blogmcp-discussion>
|
|
6
|
-
*
|
|
7
|
-
* <blogmcp-
|
|
5
|
+
* <blogmcp-discussion> article-id, api-base, site-base, lang, sign-in-href, routes, poll-ms, nonce
|
|
6
|
+
* (+ the `realtime` property: a session factory the host passes, M2)
|
|
7
|
+
* <blogmcp-notice-form> api-base, site-base, lang, turnstile-site-key, target | article-id, routes, nonce
|
|
8
|
+
* <blogmcp-my-community> api-base, site-base, lang, decision-id, routes, sign-in-href, nonce
|
|
9
|
+
* <blogmcp-notification-centre> api-base, site-base, lang, routes, sign-in-href, nonce (M2)
|
|
10
|
+
* <blogmcp-badge> api-base, site-base, lang, routes, poll-ms, nonce (M2)
|
|
11
|
+
* <blogmcp-member-settings> api-base, site-base, lang, sw-path, routes, sign-in-href, nonce (M2)
|
|
12
|
+
* <blogmcp-profile> handle, api-base, site-base, lang, routes, sign-in-href, nonce (M2)
|
|
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)
|
|
8
21
|
*
|
|
9
22
|
* Each carries ONE inline stylesheet in its shadow root, with the CSP nonce honoured: the one given when the
|
|
10
23
|
* elements are defined (the script build passes `document.currentScript.nonce`), else the element's own
|
|
11
24
|
* `nonce` — read from the property, because a page with a CSP hides the attribute once it is connected.
|
|
12
25
|
*/
|
|
13
26
|
import { defineCustomElement, type Component } from 'vue'
|
|
27
|
+
import BadgeView from './ui/BadgeView.vue'
|
|
14
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'
|
|
34
|
+
import MemberSettingsView from './ui/MemberSettingsView.vue'
|
|
15
35
|
import MyCommunityView from './ui/MyCommunityView.vue'
|
|
16
36
|
import NoticeFormView from './ui/NoticeFormView.vue'
|
|
37
|
+
import NotificationCentreView from './ui/NotificationCentreView.vue'
|
|
38
|
+
import ProfileView from './ui/ProfileView.vue'
|
|
39
|
+
import TopicView from './ui/TopicView.vue'
|
|
40
|
+
import WelcomeView from './ui/WelcomeView.vue'
|
|
17
41
|
import css from './ui/styles.css?inline'
|
|
18
42
|
|
|
19
43
|
/**
|
|
@@ -36,6 +60,19 @@ function ensureVueFeatureFlags(): void {
|
|
|
36
60
|
export const DISCUSSION_ELEMENT_TAG = 'blogmcp-discussion'
|
|
37
61
|
export const NOTICE_FORM_ELEMENT_TAG = 'blogmcp-notice-form'
|
|
38
62
|
export const MY_COMMUNITY_ELEMENT_TAG = 'blogmcp-my-community'
|
|
63
|
+
/** M2 (m2-contracts.md §3.10). */
|
|
64
|
+
export const NOTIFICATION_CENTRE_ELEMENT_TAG = 'blogmcp-notification-centre'
|
|
65
|
+
export const BADGE_ELEMENT_TAG = 'blogmcp-badge'
|
|
66
|
+
export const MEMBER_SETTINGS_ELEMENT_TAG = 'blogmcp-member-settings'
|
|
67
|
+
export const PROFILE_ELEMENT_TAG = 'blogmcp-profile'
|
|
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'
|
|
39
76
|
|
|
40
77
|
export interface DefineCommunityElementOptions {
|
|
41
78
|
/** Another tag name for the element. */
|
|
@@ -47,7 +84,22 @@ export interface DefineCommunityElementOptions {
|
|
|
47
84
|
export interface DefineCommunityElementsOptions {
|
|
48
85
|
/** CSP nonce for the elements' `<style>`. */
|
|
49
86
|
nonce?: string
|
|
50
|
-
tags?: {
|
|
87
|
+
tags?: {
|
|
88
|
+
discussion?: string
|
|
89
|
+
noticeForm?: string
|
|
90
|
+
myCommunity?: string
|
|
91
|
+
notificationCentre?: string
|
|
92
|
+
badge?: string
|
|
93
|
+
memberSettings?: string
|
|
94
|
+
profile?: string
|
|
95
|
+
welcome?: string
|
|
96
|
+
forum?: string
|
|
97
|
+
forumCategory?: string
|
|
98
|
+
topic?: string
|
|
99
|
+
forumSearch?: string
|
|
100
|
+
followButton?: string
|
|
101
|
+
forumModeration?: string
|
|
102
|
+
}
|
|
51
103
|
}
|
|
52
104
|
|
|
53
105
|
/** The element's own nonce: the property (a CSP hides the attribute once connected), else the attribute. */
|
|
@@ -101,9 +153,75 @@ export function defineMyCommunityElement(opts: DefineCommunityElementOptions = {
|
|
|
101
153
|
define(opts.tag ?? MY_COMMUNITY_ELEMENT_TAG, MyCommunityView, opts.nonce)
|
|
102
154
|
}
|
|
103
155
|
|
|
104
|
-
/** Register
|
|
156
|
+
/** Register `<blogmcp-notification-centre>` (once). Browser only. */
|
|
157
|
+
export function defineNotificationCentreElement(opts: DefineCommunityElementOptions = {}): void {
|
|
158
|
+
define(opts.tag ?? NOTIFICATION_CENTRE_ELEMENT_TAG, NotificationCentreView, opts.nonce)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Register `<blogmcp-badge>` (once). Browser only. */
|
|
162
|
+
export function defineBadgeElement(opts: DefineCommunityElementOptions = {}): void {
|
|
163
|
+
define(opts.tag ?? BADGE_ELEMENT_TAG, BadgeView, opts.nonce)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Register `<blogmcp-member-settings>` (once). Browser only. */
|
|
167
|
+
export function defineMemberSettingsElement(opts: DefineCommunityElementOptions = {}): void {
|
|
168
|
+
define(opts.tag ?? MEMBER_SETTINGS_ELEMENT_TAG, MemberSettingsView, opts.nonce)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Register `<blogmcp-profile>` (once). Browser only. */
|
|
172
|
+
export function defineProfileElement(opts: DefineCommunityElementOptions = {}): void {
|
|
173
|
+
define(opts.tag ?? PROFILE_ELEMENT_TAG, ProfileView, opts.nonce)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Register `<blogmcp-welcome>` (once). Browser only. */
|
|
177
|
+
export function defineWelcomeElement(opts: DefineCommunityElementOptions = {}): void {
|
|
178
|
+
define(opts.tag ?? WELCOME_ELEMENT_TAG, WelcomeView, opts.nonce)
|
|
179
|
+
}
|
|
180
|
+
|
|
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. */
|
|
105
212
|
export function defineCommunityElements(opts: DefineCommunityElementsOptions = {}): void {
|
|
106
213
|
defineDiscussionElement({ tag: opts.tags?.discussion, nonce: opts.nonce })
|
|
107
214
|
defineNoticeFormElement({ tag: opts.tags?.noticeForm, nonce: opts.nonce })
|
|
108
215
|
defineMyCommunityElement({ tag: opts.tags?.myCommunity, nonce: opts.nonce })
|
|
216
|
+
defineNotificationCentreElement({ tag: opts.tags?.notificationCentre, nonce: opts.nonce })
|
|
217
|
+
defineBadgeElement({ tag: opts.tags?.badge, nonce: opts.nonce })
|
|
218
|
+
defineMemberSettingsElement({ tag: opts.tags?.memberSettings, nonce: opts.nonce })
|
|
219
|
+
defineProfileElement({ tag: opts.tags?.profile, nonce: opts.nonce })
|
|
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 })
|
|
109
227
|
}
|
package/src/global.ts
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `<script>` build: registers `<blogmcp-discussion>`, `<blogmcp-notice-form
|
|
3
|
-
*
|
|
2
|
+
* The `<script>` build: registers every element — `<blogmcp-discussion>`, `<blogmcp-notice-form>`,
|
|
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.
|
|
4
8
|
*/
|
|
5
9
|
import { defineCommunityElements } from './element'
|
|
6
|
-
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'
|
|
7
26
|
import { VERSION } from './version'
|
|
8
27
|
|
|
9
28
|
const script = typeof document !== 'undefined' ? (document.currentScript as HTMLScriptElement | null) : null
|
|
10
29
|
defineCommunityElements({ nonce: script?.nonce || undefined })
|
|
11
30
|
|
|
12
|
-
export {
|
|
31
|
+
export {
|
|
32
|
+
defineCommunityElements,
|
|
33
|
+
mountDiscussion,
|
|
34
|
+
mountMyCommunity,
|
|
35
|
+
mountNoticeForm,
|
|
36
|
+
mountNotificationCentre,
|
|
37
|
+
mountBadge,
|
|
38
|
+
mountMemberSettings,
|
|
39
|
+
mountProfile,
|
|
40
|
+
mountWelcome,
|
|
41
|
+
mountForum,
|
|
42
|
+
mountForumCategory,
|
|
43
|
+
mountTopic,
|
|
44
|
+
mountForumSearch,
|
|
45
|
+
mountFollowButton,
|
|
46
|
+
mountForumModeration,
|
|
47
|
+
VERSION,
|
|
48
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* - `<blogmcp-discussion>` the discussion under an article;
|
|
8
8
|
* - `<blogmcp-notice-form>` the DSA notice form, for anyone;
|
|
9
9
|
* - `<blogmcp-my-community>` the reader's „Moje komunita", a statement page, a notifier's status page;
|
|
10
|
+
* - M2: `<blogmcp-notification-centre>` the notifications, `<blogmcp-badge>` the header's unread count,
|
|
11
|
+
* `<blogmcp-member-settings>` the switches, push and the digest's unsubscribe, `<blogmcp-profile>` a member's
|
|
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;
|
|
10
16
|
* - `defineCommunityElements()` and one `define…Element()` each — the custom elements;
|
|
11
17
|
* - `mountDiscussion(…)`, `mountNoticeForm(…)`, `mountMyCommunity(…)` — mount one from code;
|
|
12
18
|
* - `@blogmcp/community/vue`, `@blogmcp/community/react` — `<KbDiscussion>`, `<KbNoticeForm>`, `<KbMyCommunity>`;
|
|
@@ -17,14 +23,90 @@ export {
|
|
|
17
23
|
defineDiscussionElement,
|
|
18
24
|
defineNoticeFormElement,
|
|
19
25
|
defineMyCommunityElement,
|
|
26
|
+
defineNotificationCentreElement,
|
|
27
|
+
defineBadgeElement,
|
|
28
|
+
defineMemberSettingsElement,
|
|
29
|
+
defineProfileElement,
|
|
30
|
+
defineWelcomeElement,
|
|
31
|
+
defineForumElement,
|
|
32
|
+
defineForumCategoryElement,
|
|
33
|
+
defineTopicElement,
|
|
34
|
+
defineForumSearchElement,
|
|
35
|
+
defineFollowButtonElement,
|
|
36
|
+
defineForumModerationElement,
|
|
20
37
|
DISCUSSION_ELEMENT_TAG,
|
|
21
38
|
NOTICE_FORM_ELEMENT_TAG,
|
|
22
39
|
MY_COMMUNITY_ELEMENT_TAG,
|
|
40
|
+
NOTIFICATION_CENTRE_ELEMENT_TAG,
|
|
41
|
+
BADGE_ELEMENT_TAG,
|
|
42
|
+
MEMBER_SETTINGS_ELEMENT_TAG,
|
|
43
|
+
PROFILE_ELEMENT_TAG,
|
|
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,
|
|
23
51
|
type DefineCommunityElementOptions,
|
|
24
52
|
type DefineCommunityElementsOptions,
|
|
25
53
|
} from './element'
|
|
26
|
-
export {
|
|
27
|
-
|
|
54
|
+
export {
|
|
55
|
+
mountDiscussion,
|
|
56
|
+
mountNoticeForm,
|
|
57
|
+
mountMyCommunity,
|
|
58
|
+
mountNotificationCentre,
|
|
59
|
+
mountBadge,
|
|
60
|
+
mountMemberSettings,
|
|
61
|
+
mountProfile,
|
|
62
|
+
mountWelcome,
|
|
63
|
+
mountForum,
|
|
64
|
+
mountForumCategory,
|
|
65
|
+
mountTopic,
|
|
66
|
+
mountForumSearch,
|
|
67
|
+
mountFollowButton,
|
|
68
|
+
mountForumModeration,
|
|
69
|
+
type MountDiscussionOptions,
|
|
70
|
+
type MountNoticeFormOptions,
|
|
71
|
+
type MountMyCommunityOptions,
|
|
72
|
+
type MountNotificationCentreOptions,
|
|
73
|
+
type MountBadgeOptions,
|
|
74
|
+
type MountMemberSettingsOptions,
|
|
75
|
+
type MountProfileOptions,
|
|
76
|
+
type MountWelcomeOptions,
|
|
77
|
+
type MountForumOptions,
|
|
78
|
+
type MountForumCategoryOptions,
|
|
79
|
+
type MountTopicOptions,
|
|
80
|
+
type MountForumSearchOptions,
|
|
81
|
+
type MountFollowButtonOptions,
|
|
82
|
+
type MountForumModerationOptions,
|
|
83
|
+
} from './mount'
|
|
84
|
+
export { createNotificationCentre, type NotificationCentreController, type CentreUi } from './core/centre'
|
|
85
|
+
export { createBadge, badgeText, BADGE_INTERVAL_MS, type BadgeController, type BadgeOptions, type BadgeUi } from './core/badge'
|
|
86
|
+
export { createMemberSettings, SETTING_KEYS, type MemberSettingsController, type MemberSettingsOptions, type SettingsUi, type SettingKey } from './core/settings'
|
|
87
|
+
export { createProfile, createWelcome, type ProfileController, type WelcomeController } from './core/profile'
|
|
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'
|
|
28
110
|
export { createNoticeForm, type NoticeFormController, type NoticeFormOptions, type NoticeFormUi } from './core/notice'
|
|
29
111
|
export { createPanel, type PanelController, type PanelOptions, type PanelUi } from './core/panel'
|
|
30
112
|
export { createOnboarding, type OnboardingModel, type OnboardingOptions, type OnboardingStep } from './core/onboarding'
|