@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/dist/core/attrs.d.ts
CHANGED
|
@@ -49,6 +49,17 @@ export interface CommunityRoutes {
|
|
|
49
49
|
* message's anchor in the fragment). No default: without it those rows stay unlinked.
|
|
50
50
|
*/
|
|
51
51
|
articlePath?: string;
|
|
52
|
+
/** M3 (m3-contracts.md §5.1, D-M3-12): the forum — its categories, their latest topics, the search box. */
|
|
53
|
+
forumPath?: string;
|
|
54
|
+
/** M3: a category's page; `{slug}` is the category's slug (`/komunita/c/{slug}`) — a template, as profilePath is. */
|
|
55
|
+
categoryPath?: string;
|
|
56
|
+
/**
|
|
57
|
+
* M3: a topic's page; `{id}` is its conversation id (`/komunita/t/{id}`) — a template; a message's anchor goes in the
|
|
58
|
+
* fragment (`#bcm-msg-<seq>`, topicMessageLink).
|
|
59
|
+
*/
|
|
60
|
+
topicPath?: string;
|
|
61
|
+
/** M3: the forum's search (the forum's search box goes there with the query in the fragment, `#q=…`). */
|
|
62
|
+
searchPath?: string;
|
|
52
63
|
}
|
|
53
64
|
/** A path on the host's own site (the SDK's links.ts rule). */
|
|
54
65
|
export declare function isSitePath(path: unknown): path is string;
|
|
@@ -61,8 +72,8 @@ export declare function parseRoutes(value: unknown): CommunityRoutes;
|
|
|
61
72
|
*/
|
|
62
73
|
export declare function siteBaseOf(value: unknown): string | null;
|
|
63
74
|
/**
|
|
64
|
-
* The pages with the managed blog's defaults filled in under the site's base (statementPath
|
|
65
|
-
* templates; articlePath, a template too, has no default).
|
|
75
|
+
* The pages with the managed blog's defaults filled in under the site's base (statementPath, profilePath, M3's
|
|
76
|
+
* categoryPath and topicPath stay templates; articlePath, a template too, has no default).
|
|
66
77
|
* A page the host or the tenant names is its own full path.
|
|
67
78
|
*/
|
|
68
79
|
export declare function routesWithDefaults(routes: CommunityRoutes, tenant?: {
|
|
@@ -81,6 +92,10 @@ export declare function routesWithDefaults(routes: CommunityRoutes, tenant?: {
|
|
|
81
92
|
welcomePath: string;
|
|
82
93
|
discussionsPath: string;
|
|
83
94
|
articlePath: string | null;
|
|
95
|
+
forumPath: string;
|
|
96
|
+
categoryPath: string;
|
|
97
|
+
topicPath: string;
|
|
98
|
+
searchPath: string;
|
|
84
99
|
};
|
|
85
100
|
/** A notice target of one of kb-api's four shapes, or null (the form then has nothing to report). */
|
|
86
101
|
export declare function parseTarget(value: unknown): NoticeTarget | null;
|
|
@@ -4,14 +4,41 @@
|
|
|
4
4
|
* with its optimistic rows, the Russmedia confirmation and the cooldowns, reactions, blocks, reports, and the
|
|
5
5
|
* inline onboarding. The element renders `controller.state`; tests drive it directly over a fake transport.
|
|
6
6
|
*
|
|
7
|
+
* M3 (m3-contracts.md §5.3): the same brain under `<blogmcp-topic>` — a forum topic IS a discussion (its opening post is
|
|
8
|
+
* seq 1, its replies two levels deep), so createTopicThreadController runs every rule above over the SDK's topic store,
|
|
9
|
+
* `forum/topics/get` and `forum/topics/head`, posts by the topic's conversation id, and adds what a topic has of its
|
|
10
|
+
* own: a viewer the forum is not open to (`membersOnly`, with its `needs`) and a topic the viewer may not see
|
|
11
|
+
* (`notFound`) as states of the element, a write's `notFound` read again (settleTopicFailure: the forum's gate answers
|
|
12
|
+
* every refusal of a topic's id that way, A11), a closed topic, the forum's own brake beside the module's, the title of
|
|
13
|
+
* the opening post in its edit, and the art. 9 consent every forum write needs (U14). The article's own reactions are
|
|
14
|
+
* an article's only.
|
|
15
|
+
*
|
|
7
16
|
* Every request goes through the SDK client (the host's relative base); nothing here builds a path.
|
|
8
17
|
*/
|
|
9
18
|
import { type ShallowRef } from 'vue';
|
|
10
|
-
import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type DiscussionMessage, type DiscussionState, type DiscussionStore, type MyState, type ReactionTarget, type ViewerNeeds } from '@blogmcp/sdk/community';
|
|
19
|
+
import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type DiscussionMessage, type DiscussionState, type DiscussionStore, type DiscussionViewer, type MyState, type ReactionTarget, type TopicStore, type TopicStoreState, type ViewerNeeds } from '@blogmcp/sdk/community';
|
|
11
20
|
import type { Lang, TextRef } from './i18n';
|
|
12
21
|
import { type OnboardingModel } from './onboarding';
|
|
13
|
-
import type { DiscussionRealtime } from './realtime';
|
|
14
|
-
|
|
22
|
+
import type { DiscussionRealtime, TopicRealtime } from './realtime';
|
|
23
|
+
/**
|
|
24
|
+
* `membersOnly` (M3, a topic): the forum is not open to this viewer — `state.failure.needs` says what a member session
|
|
25
|
+
* still lacks (none: a visitor, sign in); `notFound` (M3, a topic): the topic is not there for this viewer.
|
|
26
|
+
*/
|
|
27
|
+
export type DiscussionPhase = 'loading' | 'ready' | 'failed' | 'hidden' | 'membersOnly' | 'notFound';
|
|
28
|
+
/** What a discussion and a topic have in common, as the controller and the views read them (the SDK's two states). */
|
|
29
|
+
export interface ThreadState {
|
|
30
|
+
readonly conversationId: string | null;
|
|
31
|
+
readonly count: number;
|
|
32
|
+
readonly lastSeq: number;
|
|
33
|
+
readonly lastChangeAt: string | null;
|
|
34
|
+
readonly messages: readonly DiscussionMessage[];
|
|
35
|
+
readonly nextCursor: number | null;
|
|
36
|
+
readonly reactionSet: readonly string[];
|
|
37
|
+
readonly preset: string | null;
|
|
38
|
+
readonly viewer: DiscussionViewer;
|
|
39
|
+
readonly loaded: boolean;
|
|
40
|
+
readonly stale: boolean;
|
|
41
|
+
}
|
|
15
42
|
/** One composer's text and what happened to it. */
|
|
16
43
|
export interface Draft {
|
|
17
44
|
body: string;
|
|
@@ -35,6 +62,11 @@ export interface InlineDraft extends Draft {
|
|
|
35
62
|
seq: number;
|
|
36
63
|
/** A reply to a reply names whom it answers. */
|
|
37
64
|
replyingTo: string | null;
|
|
65
|
+
/**
|
|
66
|
+
* M3: the topic's title, on its author's edit of the opening post (seq 1) — sent with the body when it changed (one
|
|
67
|
+
* edit of the opening post, §2.5); null everywhere else.
|
|
68
|
+
*/
|
|
69
|
+
title: string | null;
|
|
38
70
|
}
|
|
39
71
|
export interface ReportDraft {
|
|
40
72
|
conversationId: string;
|
|
@@ -59,19 +91,28 @@ export interface ReportDraft {
|
|
|
59
91
|
* A module that is `read_only` changes nothing here (m2-contracts.md §3.1: the discussions under articles keep
|
|
60
92
|
* taking writes; only live updates, push and the Pro surfaces stop) — the discussion polls instead of a socket.
|
|
61
93
|
*/
|
|
62
|
-
export type ComposerMode = 'write' | 'signIn' | 'onboarding' | 'ageRefused' | 'sanction' | 'notMoved' | 'restricted' | 'braked' | 'pending'
|
|
63
|
-
|
|
94
|
+
export type ComposerMode = 'write' | 'signIn' | 'onboarding' | 'ageRefused' | 'sanction' | 'notMoved' | 'restricted' | 'braked' | 'pending'
|
|
95
|
+
/** M3, a topic: closed (`locked`, `archived`, or a state this SDK does not know) — the composer gives way to a sentence. */
|
|
96
|
+
| 'locked'
|
|
97
|
+
/** M3, a topic: the module is `read_only` — the forum is one of its features and takes no writes meanwhile. */
|
|
98
|
+
| 'readOnly'
|
|
99
|
+
/** M3, a topic: the viewer reads only (the team at the keyboard: it writes through the KB admin) — nothing is shown. */
|
|
100
|
+
| 'none';
|
|
101
|
+
export interface ThreadUi<S extends ThreadState> {
|
|
64
102
|
phase: DiscussionPhase;
|
|
65
|
-
/** Why the first load failed (`failed`). */
|
|
103
|
+
/** Why the first load failed (`failed`), or why the topic is not shown (`membersOnly`, `notFound`). */
|
|
66
104
|
failure: CommunityFailure | null;
|
|
67
|
-
thread:
|
|
105
|
+
thread: S;
|
|
68
106
|
loadingMore: boolean;
|
|
69
107
|
refreshing: boolean;
|
|
70
108
|
/** A head poll saw messages the page does not show yet. */
|
|
71
109
|
newMessages: boolean;
|
|
72
110
|
me: MyState | null;
|
|
73
|
-
/**
|
|
74
|
-
|
|
111
|
+
/**
|
|
112
|
+
* What a write or the member's state taught: no writes yet, none for this member, or a pause (BRAKED); M3, a topic:
|
|
113
|
+
* the module is `read_only` (the forum is a Pro surface).
|
|
114
|
+
*/
|
|
115
|
+
block: 'notMoved' | 'restricted' | 'braked' | 'readOnly' | null;
|
|
75
116
|
composer: Draft;
|
|
76
117
|
inline: InlineDraft | null;
|
|
77
118
|
report: ReportDraft | null;
|
|
@@ -98,9 +139,11 @@ export interface DiscussionUi {
|
|
|
98
139
|
*/
|
|
99
140
|
realtimeOff: boolean;
|
|
100
141
|
}
|
|
101
|
-
|
|
142
|
+
/** `<blogmcp-discussion>`'s state: an article's discussion. */
|
|
143
|
+
export type DiscussionUi = ThreadUi<DiscussionState>;
|
|
144
|
+
/** What both controllers take besides what their thread is (an article, a topic). */
|
|
145
|
+
interface ThreadControllerOptions {
|
|
102
146
|
client: CommunityClient;
|
|
103
|
-
articleId: string;
|
|
104
147
|
lang: Lang;
|
|
105
148
|
/** Head poll interval (default 20 s; 0 = no polling). */
|
|
106
149
|
pollMs?: number;
|
|
@@ -111,15 +154,24 @@ export interface DiscussionControllerOptions {
|
|
|
111
154
|
refusedAge?: number | null;
|
|
112
155
|
/** The onboarding's age band was refused: the element keeps it for its next discussions. */
|
|
113
156
|
onAgeRefused?(minAge: number): void;
|
|
157
|
+
}
|
|
158
|
+
export interface DiscussionControllerOptions extends ThreadControllerOptions {
|
|
159
|
+
articleId: string;
|
|
114
160
|
/**
|
|
115
161
|
* M2 (§3.5, §3.10): the host's live-updates session factory (`createRealtimeSession` of
|
|
116
162
|
* `@blogmcp/sdk/community/realtime`). Absent: the head poll, as before.
|
|
117
163
|
*/
|
|
118
164
|
realtime?: DiscussionRealtime | null;
|
|
119
165
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
166
|
+
/** M3: `<blogmcp-topic>`'s thread — a forum topic by its conversation id. */
|
|
167
|
+
export interface TopicThreadControllerOptions extends ThreadControllerOptions {
|
|
168
|
+
conversationId: string;
|
|
169
|
+
/** The host's live-updates session factory over the topic's store (`createRealtimeSession`). Absent: the head poll. */
|
|
170
|
+
realtime?: TopicRealtime | null;
|
|
171
|
+
}
|
|
172
|
+
/** What a thread's controller does — the article's discussion's and a topic's alike; the views read it. */
|
|
173
|
+
export interface ThreadController<S extends ThreadState = ThreadState> {
|
|
174
|
+
readonly state: ThreadUi<S>;
|
|
123
175
|
readonly onboarding: ShallowRef<OnboardingModel | null>;
|
|
124
176
|
/** The first load, then the poll. */
|
|
125
177
|
start(): Promise<void>;
|
|
@@ -157,11 +209,10 @@ export interface DiscussionController {
|
|
|
157
209
|
submitReport(): Promise<void>;
|
|
158
210
|
/**
|
|
159
211
|
* The onboarding inline; `offerArt9` asks the art. 9 consent also of a member who refused it (`public_write`, M2 T9) —
|
|
160
|
-
* the give-consent link on their own message held until the consent.
|
|
212
|
+
* the give-consent link on their own message held until the consent. M3, a topic: the art. 9 step is asked before a
|
|
213
|
+
* write whatever the tenant asks elsewhere (`forceArt9`, implied), but not to read the forum (`toRead`).
|
|
161
214
|
*/
|
|
162
|
-
beginOnboarding(options?:
|
|
163
|
-
offerArt9?: boolean;
|
|
164
|
-
}): void;
|
|
215
|
+
beginOnboarding(options?: OnboardingStart): void;
|
|
165
216
|
cancelOnboarding(): void;
|
|
166
217
|
/** Seconds a draft still waits (0 when it may go). */
|
|
167
218
|
waitLeft(draft: Draft): number;
|
|
@@ -170,6 +221,29 @@ export interface DiscussionController {
|
|
|
170
221
|
* when nothing is known to limit it (no member yet, an older kb-api, a module whose mentions notify nobody).
|
|
171
222
|
*/
|
|
172
223
|
mentionCap(): number | null;
|
|
224
|
+
/** M3: the edit of this message carries the topic's title (its author's opening post of a topic). */
|
|
225
|
+
editsTitle(m: Pick<DiscussionMessage, 'seq' | 'author'>): boolean;
|
|
173
226
|
dispose(): void;
|
|
174
227
|
}
|
|
228
|
+
/** `<blogmcp-discussion>`'s controller: an article's discussion over the SDK's discussion store. */
|
|
229
|
+
export interface DiscussionController extends ThreadController<DiscussionState> {
|
|
230
|
+
readonly store: DiscussionStore;
|
|
231
|
+
}
|
|
232
|
+
/** M3: `<blogmcp-topic>`'s thread over the SDK's topic store (its header is `state.thread.topic`). */
|
|
233
|
+
export interface TopicThreadController extends ThreadController<TopicStoreState> {
|
|
234
|
+
readonly store: TopicStore;
|
|
235
|
+
}
|
|
236
|
+
/** How the inline onboarding starts (beginOnboarding). */
|
|
237
|
+
export interface OnboardingStart {
|
|
238
|
+
/** The give-consent flow (M2 T9): the art. 9 step for a member who refused it, no refusal offered. */
|
|
239
|
+
offerArt9?: boolean;
|
|
240
|
+
/** M3: the art. 9 step whatever the tenant asks elsewhere — a forum write's refusal named it (U14). */
|
|
241
|
+
forceArt9?: boolean;
|
|
242
|
+
/** M3, a topic: the onboarding the forum asks only to be read (the members-only state) — no art. 9 step forced. */
|
|
243
|
+
toRead?: boolean;
|
|
244
|
+
}
|
|
245
|
+
/** `<blogmcp-discussion>`'s controller over one article's discussion. */
|
|
175
246
|
export declare function createDiscussionController(o: DiscussionControllerOptions): DiscussionController;
|
|
247
|
+
/** M3: `<blogmcp-topic>`'s thread over one forum topic (m3-contracts.md §3.1–§3.3, §5.3). */
|
|
248
|
+
export declare function createTopicThreadController(o: TopicThreadControllerOptions): TopicThreadController;
|
|
249
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CommunityClient, type CommunityFailure, type FollowTarget } from '@blogmcp/sdk/community';
|
|
2
|
+
import type { Lang, TextRef } from './i18n';
|
|
3
|
+
export type FollowPhase = 'idle' | 'loading' | 'ready' | 'signIn' | 'unavailable' | 'failed';
|
|
4
|
+
export interface FollowUi {
|
|
5
|
+
phase: FollowPhase;
|
|
6
|
+
following: boolean;
|
|
7
|
+
busy: boolean;
|
|
8
|
+
/** The last refusal of a click (the state stays as it was), or null. */
|
|
9
|
+
failure: CommunityFailure | null;
|
|
10
|
+
announcement: TextRef | null;
|
|
11
|
+
announceId: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FollowController {
|
|
14
|
+
readonly state: FollowUi;
|
|
15
|
+
/** The state from what the page knows (`known`), else the member's follows. */
|
|
16
|
+
load(known?: boolean | null): Promise<void>;
|
|
17
|
+
/** A newer `following` the page learnt (a topic re-read): taken unless a click is on its way. */
|
|
18
|
+
sync(following: boolean): void;
|
|
19
|
+
toggle(): Promise<void>;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createFollow(o: {
|
|
23
|
+
client: CommunityClient;
|
|
24
|
+
target: FollowTarget;
|
|
25
|
+
lang: Lang;
|
|
26
|
+
}): FollowController;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<blogmcp-forum-category slug>`'s brain (m3-contracts.md §3.2, §3.3, §5.3): one category — its row of
|
|
3
|
+
* `forum/categories/list` (its description, who opens topics there, its mode, whether the member follows it) and its
|
|
4
|
+
* topics (`forum/topics/list` by the slug, the activity or the newest order, the forum's keyset cursor) — and the
|
|
5
|
+
* new-topic composer where the category takes topics from members (`forum/topics/create`: the title and the opening
|
|
6
|
+
* post as one text; the Russmedia confirmation, the cooldowns, the onboarding with the art. 9 consent a forum write
|
|
7
|
+
* needs, a pre-moderated topic waiting for review). In „Články" (kind `article_discussions`) the rows are the article
|
|
8
|
+
* discussions, and nothing is created there.
|
|
9
|
+
*/
|
|
10
|
+
import { type ShallowRef } from 'vue';
|
|
11
|
+
import { type CommunityClient, type CommunityFailure, type ForumCategory, type ForumSort, type KnownTopicItem, type MyState } from '@blogmcp/sdk/community';
|
|
12
|
+
import type { Draft } from './discussion';
|
|
13
|
+
import type { Lang, TextRef } from './i18n';
|
|
14
|
+
import { type OnboardingModel } from './onboarding';
|
|
15
|
+
export type CategoryPhase = 'loading' | 'ready' | 'failed' | 'hidden' | 'membersOnly' | 'notFound';
|
|
16
|
+
/** The new topic's draft: the composer's, with its title. */
|
|
17
|
+
export interface TopicDraft extends Draft {
|
|
18
|
+
title: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CategoryUi {
|
|
21
|
+
phase: CategoryPhase;
|
|
22
|
+
failure: CommunityFailure | null;
|
|
23
|
+
me: MyState | null;
|
|
24
|
+
/** The category's row of the list (null when the list did not name it: the page header stays). */
|
|
25
|
+
category: ForumCategory | null;
|
|
26
|
+
/** The page's own header of the category (its id, slug, name, kind). */
|
|
27
|
+
header: {
|
|
28
|
+
id: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
name: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
} | null;
|
|
33
|
+
items: KnownTopicItem[];
|
|
34
|
+
nextCursor: string | null;
|
|
35
|
+
sort: ForumSort;
|
|
36
|
+
loadingMore: boolean;
|
|
37
|
+
/** The list is read again (another order). */
|
|
38
|
+
sorting: boolean;
|
|
39
|
+
composer: TopicDraft;
|
|
40
|
+
/** The topic the member just opened (its link), and whether it waits for review. */
|
|
41
|
+
created: {
|
|
42
|
+
conversationId: string;
|
|
43
|
+
held: boolean;
|
|
44
|
+
} | null;
|
|
45
|
+
/** kb-api said the category takes no topic from the member (TOPIC_CREATION_CLOSED). */
|
|
46
|
+
creationClosed: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* A write was refused BRAKED — by the forum's own brake (`surface: 'forum'`) or the module's (none named): the composer
|
|
49
|
+
* gives way to the pause until the category is read again.
|
|
50
|
+
*/
|
|
51
|
+
brakedBy: 'forum' | 'module' | null;
|
|
52
|
+
clock: number;
|
|
53
|
+
announcement: TextRef | null;
|
|
54
|
+
announceId: number;
|
|
55
|
+
}
|
|
56
|
+
export interface CategoryController {
|
|
57
|
+
readonly state: CategoryUi;
|
|
58
|
+
readonly onboarding: ShallowRef<OnboardingModel | null>;
|
|
59
|
+
load(): Promise<void>;
|
|
60
|
+
loadMore(): Promise<void>;
|
|
61
|
+
setSort(sort: ForumSort): Promise<void>;
|
|
62
|
+
/** The member may open a topic here now (the composer shows). */
|
|
63
|
+
canCreate(): boolean;
|
|
64
|
+
/** Why the composer does not show where it would: the team's category, a pause, read_only — or null. */
|
|
65
|
+
notCreating(): 'team' | 'paused' | 'readOnly' | null;
|
|
66
|
+
create(): Promise<void>;
|
|
67
|
+
setConfirmed(code: string, checked: boolean): void;
|
|
68
|
+
sendConfirmed(): Promise<void>;
|
|
69
|
+
beginOnboarding(options?: {
|
|
70
|
+
forceArt9?: boolean;
|
|
71
|
+
}): void;
|
|
72
|
+
cancelOnboarding(): void;
|
|
73
|
+
waitLeft(): number;
|
|
74
|
+
dispose(): void;
|
|
75
|
+
}
|
|
76
|
+
export declare function createCategory(o: {
|
|
77
|
+
client: CommunityClient;
|
|
78
|
+
slug: string;
|
|
79
|
+
lang: Lang;
|
|
80
|
+
now?: () => number;
|
|
81
|
+
onCreated?(topic: {
|
|
82
|
+
conversationId: string;
|
|
83
|
+
held: boolean;
|
|
84
|
+
}): void;
|
|
85
|
+
}): CategoryController;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type CommunityClient, type CommunityFailure, type ForumCategory, type ForumSearchResult, type KnownForumSpace } from '@blogmcp/sdk/community';
|
|
2
|
+
export type SearchPhase = 'idle' | 'searching' | 'ready' | 'failed' | 'hidden' | 'membersOnly';
|
|
3
|
+
export interface SearchUi {
|
|
4
|
+
phase: SearchPhase;
|
|
5
|
+
failure: CommunityFailure | null;
|
|
6
|
+
query: string;
|
|
7
|
+
/** '' every space. */
|
|
8
|
+
space: KnownForumSpace | '';
|
|
9
|
+
/** '' every category. */
|
|
10
|
+
categoryId: string;
|
|
11
|
+
/** The categories to narrow by (read once). */
|
|
12
|
+
categories: ForumCategory[];
|
|
13
|
+
/** The query is too short or too long to ask (said, nothing sent). */
|
|
14
|
+
invalid: 'short' | 'long' | null;
|
|
15
|
+
results: ForumSearchResult[];
|
|
16
|
+
page: number;
|
|
17
|
+
hasMore: boolean;
|
|
18
|
+
loadingMore: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface SearchController {
|
|
21
|
+
readonly state: SearchUi;
|
|
22
|
+
/** The categories for the filter (and the forum's own refusals: a visitor learns the forum is for members). */
|
|
23
|
+
load(): Promise<void>;
|
|
24
|
+
search(): Promise<void>;
|
|
25
|
+
more(): Promise<void>;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
/** The query a page's fragment carries (`#q=<encoded>`), or null. */
|
|
29
|
+
export declare function queryFromHash(hash: string | null | undefined): string | null;
|
|
30
|
+
/** The search page's link with the query in its fragment (the forum's search box), or null. */
|
|
31
|
+
export declare function searchLink(searchPath: string, raw: string): string | null;
|
|
32
|
+
export declare function createSearch(o: {
|
|
33
|
+
client: CommunityClient;
|
|
34
|
+
query?: string | null;
|
|
35
|
+
}): SearchController;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every forum element reads out of `me/get` beside its own operation (m3-contracts.md §3.5, §4.8, §4.9, §5.3):
|
|
3
|
+
* the team's away mode (one sentence, `state.away`), the pause of the forum's writes — the module's brake (both
|
|
4
|
+
* surfaces: `state.braked`) or the forum's own (`state.forumBraked`; the article discussions go on) —, the module's
|
|
5
|
+
* `read_only` (the forum is one of its features), and what a `membersOnly` refusal asks of the viewer.
|
|
6
|
+
*/
|
|
7
|
+
import { type CommunityFailure, type CommunityMessageKey, type MyState } from '@blogmcp/sdk/community';
|
|
8
|
+
export interface ForumNotices {
|
|
9
|
+
/** The team is away: new topics wait for review (`state.away`). */
|
|
10
|
+
away: boolean;
|
|
11
|
+
/** The forum's writes are paused — the module's brake, or the forum's own — or not. */
|
|
12
|
+
paused: 'module' | 'forum' | null;
|
|
13
|
+
/** The module is `read_only`: the forum takes no writes meanwhile (`error.readOnly`). */
|
|
14
|
+
readOnly: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** The notices of `me/get`'s module block, at `now`. */
|
|
17
|
+
export declare function forumNoticesOf(me: MyState | null | undefined, now?: number): ForumNotices;
|
|
18
|
+
/** The sentence of a pause of the forum's writes. */
|
|
19
|
+
export declare const pausedKey: (paused: ForumNotices["paused"]) => CommunityMessageKey | null;
|
|
20
|
+
/**
|
|
21
|
+
* What a `membersOnly` refusal asks of the viewer (§3.1 step 3): to join — a member session that has not finished its
|
|
22
|
+
* onboarding (`needs` `onboarding` or `reconsent_rules`) — or to sign in (a visitor, or a session the tenant does not
|
|
23
|
+
* admit as a member: `sign_in`, or no needs at all).
|
|
24
|
+
*/
|
|
25
|
+
export declare function membersOnlyAsk(failure: Pick<CommunityFailure, 'needs'> | null | undefined): 'join' | 'signIn';
|
|
26
|
+
/**
|
|
27
|
+
* Whether the viewer is a member who may take part (an onboarded member's session): me/get names a member and no
|
|
28
|
+
* need to sign in. The team at the keyboard reads the forum but writes through the KB admin.
|
|
29
|
+
*/
|
|
30
|
+
export declare const isForumMember: (me: MyState | null | undefined) => boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forum's words for the discussion's components (m3-contracts.md §5.3): a topic's thread reuses the discussion's
|
|
3
|
+
* view — the same messages, composer, dialogs and refusals — and reads it with these keys in place of the
|
|
4
|
+
* discussion's, so a forum reads „příspěvek" (a post) where an article's discussion reads „komentář" (a comment). Every
|
|
5
|
+
* key is the SDK catalogue's in both languages; a plural maps by its name (test/forum-elements.spec.ts checks both).
|
|
6
|
+
*/
|
|
7
|
+
import type { CommunityMessageKey } from '@blogmcp/sdk/community';
|
|
8
|
+
export declare const FORUM_WORDS: Readonly<Partial<Record<CommunityMessageKey, CommunityMessageKey>>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CommunityClient, type CommunityFailure, type ForumCategory, type KnownTopicItem, type MyState } from '@blogmcp/sdk/community';
|
|
2
|
+
export type ForumPhase = 'loading' | 'ready' | 'failed' | 'hidden' | 'membersOnly';
|
|
3
|
+
/** The categories whose latest topics the index shows, and how many of each (one request a category). */
|
|
4
|
+
export declare const FORUM_LATEST_CATEGORIES = 10;
|
|
5
|
+
export declare const FORUM_LATEST_PER_CATEGORY = 3;
|
|
6
|
+
export interface ForumUi {
|
|
7
|
+
phase: ForumPhase;
|
|
8
|
+
failure: CommunityFailure | null;
|
|
9
|
+
me: MyState | null;
|
|
10
|
+
categories: ForumCategory[];
|
|
11
|
+
/** The latest rows of a category, by its id (a category whose list failed has none). */
|
|
12
|
+
latest: Record<string, KnownTopicItem[]>;
|
|
13
|
+
}
|
|
14
|
+
export interface ForumController {
|
|
15
|
+
readonly state: ForumUi;
|
|
16
|
+
load(): Promise<void>;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createForum(o: {
|
|
20
|
+
client: CommunityClient;
|
|
21
|
+
}): ForumController;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type ForumCategory, type ModerationCase, type ModerationItem, type ModerationRestriction, type MyState } from '@blogmcp/sdk/community';
|
|
2
|
+
import type { Lang, TextRef } from './i18n';
|
|
3
|
+
export type ModerationPhase = 'loading' | 'ready' | 'none' | 'failed' | 'hidden' | 'membersOnly';
|
|
4
|
+
/** A case opened from the queue: its package and the decision being made. */
|
|
5
|
+
export interface OpenCase {
|
|
6
|
+
item: ModerationItem;
|
|
7
|
+
kase: ModerationCase | null;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
restriction: ModerationRestriction | '';
|
|
10
|
+
facts: string;
|
|
11
|
+
/** The rule the restriction rests on ('' the case's own). */
|
|
12
|
+
ground: string;
|
|
13
|
+
/** kb-api asked for the rule (`invalid` on `contractualGround`): the rule must be chosen. */
|
|
14
|
+
needsRule: boolean;
|
|
15
|
+
/** The statement of the previewed decision, for the same choice. */
|
|
16
|
+
preview: string | null;
|
|
17
|
+
previewing: boolean;
|
|
18
|
+
deciding: boolean;
|
|
19
|
+
failure: CommunityFailure | null;
|
|
20
|
+
decided: boolean;
|
|
21
|
+
escalation: {
|
|
22
|
+
category: CaseCategory | '';
|
|
23
|
+
sending: boolean;
|
|
24
|
+
failure: CommunityFailure | null;
|
|
25
|
+
done: boolean;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
/** A pre-moderated message being reviewed. */
|
|
29
|
+
export interface PremodReview {
|
|
30
|
+
item: ModerationItem;
|
|
31
|
+
outcome: 'approve' | 'reject' | '';
|
|
32
|
+
ruleId: string;
|
|
33
|
+
facts: string;
|
|
34
|
+
sending: boolean;
|
|
35
|
+
failure: CommunityFailure | null;
|
|
36
|
+
statement: string | null;
|
|
37
|
+
done: 'approved' | 'rejected' | null;
|
|
38
|
+
/** M3 FX (R3): the author edited the text while it was being reviewed — `item` holds the new one; decide again. */
|
|
39
|
+
changed: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface ModerationUi {
|
|
42
|
+
phase: ModerationPhase;
|
|
43
|
+
failure: CommunityFailure | null;
|
|
44
|
+
me: MyState | null;
|
|
45
|
+
/** '' every category of the moderator's. */
|
|
46
|
+
categoryId: string;
|
|
47
|
+
/** The moderator's categories (their names), in the forum's order. */
|
|
48
|
+
categories: ForumCategory[];
|
|
49
|
+
items: ModerationItem[];
|
|
50
|
+
nextCursor: string | null;
|
|
51
|
+
loadingMore: boolean;
|
|
52
|
+
rules: CommunityRule[] | null;
|
|
53
|
+
open: OpenCase | null;
|
|
54
|
+
review: PremodReview | null;
|
|
55
|
+
announcement: TextRef | null;
|
|
56
|
+
announceId: number;
|
|
57
|
+
}
|
|
58
|
+
export interface ModerationController {
|
|
59
|
+
readonly state: ModerationUi;
|
|
60
|
+
load(): Promise<void>;
|
|
61
|
+
loadMore(): Promise<void>;
|
|
62
|
+
setCategory(categoryId: string): Promise<void>;
|
|
63
|
+
openCase(item: ModerationItem): Promise<void>;
|
|
64
|
+
closeCase(): void;
|
|
65
|
+
/** Whether the case's decision may be previewed now (a restriction, facts, a rule where one is needed). */
|
|
66
|
+
canPreview(): boolean;
|
|
67
|
+
preview(): Promise<void>;
|
|
68
|
+
decide(): Promise<void>;
|
|
69
|
+
startEscalation(): void;
|
|
70
|
+
escalate(): Promise<void>;
|
|
71
|
+
openReview(item: ModerationItem, outcome: 'approve' | 'reject'): void;
|
|
72
|
+
closeReview(): void;
|
|
73
|
+
review(): Promise<void>;
|
|
74
|
+
dispose(): void;
|
|
75
|
+
}
|
|
76
|
+
export declare function createModeration(o: {
|
|
77
|
+
client: CommunityClient;
|
|
78
|
+
lang: Lang;
|
|
79
|
+
categoryId?: string | null;
|
|
80
|
+
}): ModerationController;
|
|
@@ -83,6 +83,13 @@ export interface OnboardingOptions {
|
|
|
83
83
|
* member who refused it, or has not given it, where the tenant asks for it — and no refusal is offered.
|
|
84
84
|
*/
|
|
85
85
|
offerArt9?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* M3 (m3-contracts.md §3.1 step 5, U14): a forum write needs the art. 9 consent whatever the tenant's
|
|
88
|
+
* `require_art9_consent` says, so the step is asked whenever the consent is not in force — me/get names no
|
|
89
|
+
* `art9_consent` need (and may report the consent as null) where the tenant does not ask for it elsewhere. A refusal
|
|
90
|
+
* is offered as for the discussion (`art9Refusal = public_write`), and a member who refused there is not asked again.
|
|
91
|
+
*/
|
|
92
|
+
forceArt9?: boolean;
|
|
86
93
|
signal?: AbortSignal;
|
|
87
94
|
}
|
|
88
95
|
/** kb-api's issue code for a refusal the tenant does not take (`art9Refusal = read_only`, §3.8). */
|
package/dist/core/profile.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommunityClient, type CommunityFailure, type CommunityMessageKey, type CommunityRule, type KnownAuthor } from '@blogmcp/sdk/community';
|
|
1
|
+
import { type CommunityClient, type CommunityFailure, type CommunityMessageKey, type CommunityRule, type ForumCategoryRef, type KnownAuthor } from '@blogmcp/sdk/community';
|
|
2
2
|
export type LoadPhase = 'idle' | 'loading' | 'ready' | 'failed' | 'signIn' | 'notFound';
|
|
3
3
|
export interface ProfileUi {
|
|
4
4
|
phase: LoadPhase;
|
|
@@ -8,6 +8,11 @@ export interface ProfileUi {
|
|
|
8
8
|
bio: string | null;
|
|
9
9
|
/** The trust level's catalogue key (`trust.<n>`), or null for a label this SDK does not know. */
|
|
10
10
|
trustKey: CommunityMessageKey | null;
|
|
11
|
+
/**
|
|
12
|
+
* M3 (m3-contracts.md §3.5, §4.5; D-M3-7): the categories the member moderates now — „Moderátor" names this
|
|
13
|
+
* appointment on the profile, never a trust level; kb-api sends it to a forum member only (else none).
|
|
14
|
+
*/
|
|
15
|
+
moderates: ForumCategoryRef[];
|
|
11
16
|
}
|
|
12
17
|
export interface ProfileController {
|
|
13
18
|
readonly state: ProfileUi;
|
package/dist/core/realtime.d.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
|
/** `connecting`, `live`, `polling` (live updates are off or the server cut the socket for good), `stopped`. */
|
|
16
17
|
export type DiscussionRealtimeMode = 'connecting' | 'live' | 'polling' | 'stopped' | (string & {});
|
|
17
18
|
/** What the element calls the factory with. */
|
|
@@ -33,3 +34,13 @@ export interface DiscussionRealtimeSession {
|
|
|
33
34
|
}
|
|
34
35
|
/** `createRealtimeSession` of `@blogmcp/sdk/community/realtime`, or any function of its shape. */
|
|
35
36
|
export type DiscussionRealtime = (options: DiscussionRealtimeOptions) => DiscussionRealtimeSession;
|
|
37
|
+
/**
|
|
38
|
+
* M3 (m3-contracts.md §3.5, §5.3): what `<blogmcp-topic>` calls the same factory with — the topic's store
|
|
39
|
+
* (`createTopicStore`) in place of a discussion's; `createRealtimeSession` takes both, re-reads a topic with
|
|
40
|
+
* `forum/topics/get` and falls back to `forum/topics/head`.
|
|
41
|
+
*/
|
|
42
|
+
export interface TopicRealtimeOptions extends Omit<DiscussionRealtimeOptions, 'store'> {
|
|
43
|
+
store: TopicStore;
|
|
44
|
+
}
|
|
45
|
+
/** `createRealtimeSession`, as `<blogmcp-topic>`'s `realtime` property takes it. */
|
|
46
|
+
export type TopicRealtime = (options: TopicRealtimeOptions) => DiscussionRealtimeSession;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CaseCategory, type CommunityFailure, type CommunityRule, type DiscussionMessage, type ForumCategory } from '@blogmcp/sdk/community';
|
|
2
|
+
import { type TopicThreadController, type TopicThreadControllerOptions } from './discussion';
|
|
3
|
+
import { type FollowController } from './follow';
|
|
4
|
+
import type { TextRef } from './i18n';
|
|
5
|
+
export type ModeratorDialogKind = 'remove' | 'escalate' | 'move';
|
|
6
|
+
/** A moderator's open dialog: a message's removal or escalation, or the topic's move. */
|
|
7
|
+
export interface ModeratorDialog {
|
|
8
|
+
kind: ModeratorDialogKind;
|
|
9
|
+
/** The message (remove, escalate). */
|
|
10
|
+
seq: number | null;
|
|
11
|
+
ruleId: string;
|
|
12
|
+
facts: string;
|
|
13
|
+
category: CaseCategory | '';
|
|
14
|
+
categoryId: string;
|
|
15
|
+
sending: boolean;
|
|
16
|
+
failure: CommunityFailure | null;
|
|
17
|
+
/** A removal's statement of reasons, as kb-api rendered it. */
|
|
18
|
+
statement: string | null;
|
|
19
|
+
done: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface TopicExtrasUi {
|
|
22
|
+
/** The rules of the tenant's current rules text (a moderator's removal cites one), once read. */
|
|
23
|
+
rules: CommunityRule[] | null;
|
|
24
|
+
/** The categories a moderator may move the topic into (theirs, but this one), once read. */
|
|
25
|
+
moveTargets: ForumCategory[] | null;
|
|
26
|
+
dialog: ModeratorDialog | null;
|
|
27
|
+
busy: Record<string, boolean>;
|
|
28
|
+
announcement: TextRef | null;
|
|
29
|
+
announceId: number;
|
|
30
|
+
}
|
|
31
|
+
export interface TopicController {
|
|
32
|
+
readonly thread: TopicThreadController;
|
|
33
|
+
readonly follow: FollowController | null;
|
|
34
|
+
readonly extras: TopicExtrasUi;
|
|
35
|
+
start(): Promise<void>;
|
|
36
|
+
/** The viewer moderates the topic's category (kb-api's `viewer.moderator`, me/get's appointments beside it). */
|
|
37
|
+
isModerator(): boolean;
|
|
38
|
+
/** A moderator's message actions apply (another member's live message). */
|
|
39
|
+
canModerate(m: DiscussionMessage): boolean;
|
|
40
|
+
setState(state: 'open' | 'locked'): Promise<void>;
|
|
41
|
+
setPinned(pinned: boolean): Promise<void>;
|
|
42
|
+
openDialog(kind: ModeratorDialogKind, seq?: number | null): void;
|
|
43
|
+
closeDialog(): void;
|
|
44
|
+
submitDialog(): Promise<void>;
|
|
45
|
+
dispose(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function createTopicController(o: TopicThreadControllerOptions): TopicController;
|