g-comments 0.0.0-stage → 0.1.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +354 -2
- package/dist/chunk-6E3M3PQN.js +3056 -0
- package/dist/chunk-6E3M3PQN.js.map +1 -0
- package/dist/chunk-X6RAR374.cjs +3077 -0
- package/dist/chunk-X6RAR374.cjs.map +1 -0
- package/dist/element.d.ts +770 -0
- package/dist/element.js +1226 -0
- package/dist/element.js.map +1 -0
- package/dist/index.cjs +68 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +37 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/messages-BlBkT6Kr.d.cts +649 -0
- package/dist/messages-BlBkT6Kr.d.ts +649 -0
- package/dist/react.cjs +2557 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +320 -0
- package/dist/react.d.ts +320 -0
- package/dist/react.js +2549 -0
- package/dist/react.js.map +1 -0
- package/package.json +89 -3
- package/styles/styles.css +1145 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `g-comments` are listed here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the package uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - Unreleased
|
|
10
|
+
|
|
11
|
+
First release, against the `discourse-comments` forum plugin.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `g-comments`: the framework-agnostic client. Threads (open, sort, paging,
|
|
16
|
+
per-branch replies, context), posting, editing, deleting, likes, pins,
|
|
17
|
+
flags, mentions, previews and uploads; popup sign-in with a redirect
|
|
18
|
+
fallback (PKCE); realtime through the forum's MessageBus with a polling
|
|
19
|
+
fallback; batched comment counts; `en` and `pt-BR` messages; a page-wide
|
|
20
|
+
client registry; typed `CommentsError`s with setup hints.
|
|
21
|
+
- `g-comments/react`: `<DiscourseComments>` provider, the drop-in `<Comments>`
|
|
22
|
+
built from `Comments.*` primitives, `<CommentCount>`, `useThread`,
|
|
23
|
+
`useCommenter` and `useCommentCount`; inline, on-submit and manual sign-in
|
|
24
|
+
modes; a diagnostics panel on development hosts.
|
|
25
|
+
- `g-comments/styles.css`: the default theme with `--gc-*` tokens, light, dark,
|
|
26
|
+
`auto` and `inherit` colour schemes.
|
|
27
|
+
- `g-comments/element`: `<discourse-comments>` and `<discourse-comment-count>`
|
|
28
|
+
custom elements for pages without React. Self-contained (React and the
|
|
29
|
+
stylesheet are bundled), rendered in a shadow root, attributes reflected as
|
|
30
|
+
properties, every `gc-*` class exposed through `::part()`, widget events
|
|
31
|
+
dispatched as `gc:*` custom events.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Grubba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,355 @@
|
|
|
1
|
-
#
|
|
1
|
+
# g-comments
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Comments for any web page, powered by a Discourse forum running the
|
|
4
|
+
`discourse-comments` plugin. The package root is the framework-agnostic client;
|
|
5
|
+
the React components (`g-comments/react`), the custom elements
|
|
6
|
+
(`g-comments/element`) and the default stylesheet (`g-comments/styles.css`)
|
|
7
|
+
build on it.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add g-comments
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
On the forum: install the plugin, then add your site's host under
|
|
16
|
+
**Admin → Customize → Embedding** (or to `discourse_comments_allowed_origins`).
|
|
17
|
+
|
|
18
|
+
## React
|
|
19
|
+
|
|
20
|
+
React 18 or 19 is a peer dependency of `g-comments/react` only.
|
|
21
|
+
|
|
22
|
+
### Option A: provider and drop-in components
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { CommentCount, Comments, DiscourseComments } from "g-comments/react";
|
|
26
|
+
import "g-comments/styles.css";
|
|
27
|
+
|
|
28
|
+
// Once, near the root of the app.
|
|
29
|
+
<DiscourseComments
|
|
30
|
+
forum="https://forum.example.com"
|
|
31
|
+
theme="auto" // "light" | "dark" | "auto" (prefers-color-scheme) | "inherit"
|
|
32
|
+
locale="en" // "pt-BR" is bundled too; `messages` overrides single strings
|
|
33
|
+
appearance={{ variables: { accent: "var(--brand)", radius: "0" } }}
|
|
34
|
+
onEvent={(event) => analytics.track(event.type, event)}
|
|
35
|
+
>
|
|
36
|
+
<App />
|
|
37
|
+
</DiscourseComments>
|
|
38
|
+
|
|
39
|
+
// Per page. Without props the page URL (canonical link, else the address) and
|
|
40
|
+
// document.title identify the thread.
|
|
41
|
+
<Comments />
|
|
42
|
+
<Comments identifier="blog-1433307" url={canonicalUrl} title={post.title} sort="newest" />
|
|
43
|
+
|
|
44
|
+
<CommentCount url={postUrl} /> // "12 comments", fetched once, batched with the others
|
|
45
|
+
<CommentCount identifier="blog-1433307" render={(n) => n} />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`<Comments>` props: `identifier`, `url`, `title`, `sort`, `maxDepth` (visual
|
|
49
|
+
nesting, default 2; deeper replies collapse into "Continue this thread"),
|
|
50
|
+
`signIn` (`"inline"` panel with the forum's providers, `"on-submit"` signs in
|
|
51
|
+
when an anonymous reader posts and keeps the draft, `"manual"` leaves it to your
|
|
52
|
+
UI), `onSignInRequired` (manual mode: called when an anonymous reader tries to
|
|
53
|
+
like or reply; without it those actions are unavailable, and manual mode never
|
|
54
|
+
opens the forum's popup itself), `readOnly`, `highlight` (comment id; defaults
|
|
55
|
+
to the `#gc-<id>` fragment), `guidelinesUrl`, `onPosted(result)`, `appearance`,
|
|
56
|
+
`unstyled`, `theme`, `locale`, `className`.
|
|
57
|
+
|
|
58
|
+
Open reply, edit, report and delete panels close as soon as the reader loses
|
|
59
|
+
the right to use them (read-only, forum degraded, signed out), and a post
|
|
60
|
+
waiting for an on-submit sign-in is dropped if the reader cancels it or the
|
|
61
|
+
widget goes away first.
|
|
62
|
+
|
|
63
|
+
The provider also accepts the client options (`fetch`, `headers` such as
|
|
64
|
+
`Authorization` for a forum behind basic auth, `storage`, `clientId`) or a
|
|
65
|
+
ready-made `client`, plus `messages` and `debug`. On localhost (or with
|
|
66
|
+
`debug`) a failing widget shows a diagnostics panel naming the probable setup
|
|
67
|
+
problem; production readers only see "Comments are unavailable right now."
|
|
68
|
+
|
|
69
|
+
`onEvent` receives `comment.posted`, `comment.pending` (sent to the review
|
|
70
|
+
queue), `comment.liked`, `auth.signedIn`, `auth.signedOut` (`expired` when the
|
|
71
|
+
forum ended the session) and `error` (with the failing `action`).
|
|
72
|
+
|
|
73
|
+
### Option B: no provider
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<Comments forum="https://forum.example.com" identifier="blog-1433307" />
|
|
77
|
+
<CommentCount forum="https://forum.example.com" identifier="blog-1433307" />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Clients come from a page-wide registry keyed by the normalized forum URL, so
|
|
81
|
+
every component (and a provider) naming the same forum shares one client: one
|
|
82
|
+
sign-in state, one config fetch, one realtime connection. The first options
|
|
83
|
+
used for a forum win. `clientRegistry` is exported from `g-comments` for
|
|
84
|
+
non-React code.
|
|
85
|
+
|
|
86
|
+
### Option C: hooks and primitives
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { Comments, useCommenter, useCommentCount, useThread } from "g-comments/react";
|
|
90
|
+
|
|
91
|
+
const { thread, snapshot, error, retry } = useThread({ identifier, sort: "newest" });
|
|
92
|
+
// snapshot: comments (tree), status, hasMore, pendingNew, branches, awaitingApproval, …
|
|
93
|
+
// thread: post, edit, remove, like, flag, pin, loadMore, loadReplies, showPending, context, …
|
|
94
|
+
|
|
95
|
+
const me = useCommenter();
|
|
96
|
+
// me.user, me.status, me.providers, me.degraded, me.signIn({ provider }), me.cancelSignIn(), me.signOut()
|
|
97
|
+
|
|
98
|
+
const { count } = useCommentCount({ url: postUrl });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The drop-in `<Comments>` is assembled from primitives you can rearrange or
|
|
102
|
+
replace:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Comments.Root identifier="blog-1433307" signIn="manual">
|
|
106
|
+
<Comments.Header>
|
|
107
|
+
<Comments.Count as="h3" />
|
|
108
|
+
<Comments.Sort />
|
|
109
|
+
</Comments.Header>
|
|
110
|
+
<Comments.SignIn>{(me) => <MyButton onClick={() => me.signIn()}>Join</MyButton>}</Comments.SignIn>
|
|
111
|
+
<Comments.Composer placeholder="Say something…" />
|
|
112
|
+
<Comments.List>
|
|
113
|
+
{(comment, depth) => (
|
|
114
|
+
<Comments.Item comment={comment} depth={depth}>
|
|
115
|
+
<Comments.Avatar />
|
|
116
|
+
<Comments.Body />
|
|
117
|
+
<Comments.Actions />
|
|
118
|
+
</Comments.Item>
|
|
119
|
+
)}
|
|
120
|
+
</Comments.List>
|
|
121
|
+
<Comments.LoadMore />
|
|
122
|
+
</Comments.Root>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Also available: `Comments.Meta` (author, badges, permalink time),
|
|
126
|
+
`Comments.NewComments` (the live "N new comments" pill),
|
|
127
|
+
`Comments.AwaitingApproval`, `Comments.Notice` (closed, read-only) and
|
|
128
|
+
`Comments.DevPanel`. The `List` render function is reused for replies at every
|
|
129
|
+
depth, and `Item` keeps the inline reply, edit, report and delete panels.
|
|
130
|
+
|
|
131
|
+
## Styling
|
|
132
|
+
|
|
133
|
+
`g-comments/styles.css` styles everything under the widget root
|
|
134
|
+
(`.gc-root.gc-root--styled`); nothing outside it is touched. Class names follow
|
|
135
|
+
BEM (`gc-comment`, `gc-comment__body`, `gc-composer--replying`,
|
|
136
|
+
`gc-button--primary`, …) and stay in place with `unstyled`, which only drops
|
|
137
|
+
`gc-root--styled`. Selectors carry one class of specificity, so your own class
|
|
138
|
+
rules win when loaded after the stylesheet. Cooked comment HTML is wrapped in
|
|
139
|
+
`.gc-prose`.
|
|
140
|
+
|
|
141
|
+
`appearance.elements` adds class names: `root`, `header`, `count`, `sort`,
|
|
142
|
+
`signIn`, `composer`, `textarea`, `toolbar`, `list`, `comment`, `avatar`,
|
|
143
|
+
`meta`, `body`, `actions`, `menu`, `button`, `primaryButton`, `loadMore`,
|
|
144
|
+
`pill`, `notice`, `dialog`.
|
|
145
|
+
|
|
146
|
+
Tokens can be set through `appearance.variables` (camelCase keys), on the
|
|
147
|
+
widget root, or on any ancestor such as `:root`; values you set apply in every
|
|
148
|
+
colour scheme.
|
|
149
|
+
|
|
150
|
+
| Token | `variables` key | Light default | Dark default |
|
|
151
|
+
| --- | --- | --- | --- |
|
|
152
|
+
| `--gc-font` | `font` | system UI stack | |
|
|
153
|
+
| `--gc-fg` | `fg` | `#1f2328` | `#e6edf3` |
|
|
154
|
+
| `--gc-fg-muted` | `fgMuted` | `#59636e` | `#9198a1` |
|
|
155
|
+
| `--gc-bg` | `bg` | `#ffffff` | `#0d1117` |
|
|
156
|
+
| `--gc-bg-subtle` | `bgSubtle` | `#f6f8fa` | `#151b23` |
|
|
157
|
+
| `--gc-border` | `border` | `#d1d9e0` | `#3d444d` |
|
|
158
|
+
| `--gc-accent` | `accent` | `#0969da` | `#4493f8` |
|
|
159
|
+
| `--gc-accent-fg` | `accentFg` | `#ffffff` | `#0d1117` |
|
|
160
|
+
| `--gc-danger` | `danger` | `#cf222e` | `#f85149` |
|
|
161
|
+
| `--gc-radius` | `radius` | `8px` | |
|
|
162
|
+
| `--gc-shadow` | `shadow` | soft drop shadow | darker drop shadow |
|
|
163
|
+
| `--gc-space` | `space` | `0.5rem` (spacing unit) | |
|
|
164
|
+
| `--gc-avatar-size` | `avatarSize` | `2.25rem` (`2rem` in narrow containers) | |
|
|
165
|
+
|
|
166
|
+
`theme` sets `data-gc-theme` on the root: `light`, `dark`, or `auto`, which
|
|
167
|
+
switches with `prefers-color-scheme`. `inherit` sets nothing and derives the
|
|
168
|
+
neutral colours from the page's text colour, for sites that theme the widget
|
|
169
|
+
themselves. The layout works down to 320px wide.
|
|
170
|
+
|
|
171
|
+
## Custom elements (no framework)
|
|
172
|
+
|
|
173
|
+
`g-comments/element` registers `<discourse-comments>` and
|
|
174
|
+
`<discourse-comment-count>` when imported. The module is self-contained: React,
|
|
175
|
+
ReactDOM, the client and the default stylesheet are bundled (about 110 kB
|
|
176
|
+
gzipped), so one script tag works on any page and nothing else needs
|
|
177
|
+
installing. On a site that already renders with React, use `g-comments/react`
|
|
178
|
+
instead; the element keeps its own React and cannot share the page's.
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/g-comments/dist/element.js"></script>
|
|
182
|
+
<!-- or, with a bundler: import "g-comments/element"; -->
|
|
183
|
+
|
|
184
|
+
<discourse-comments forum="https://forum.example.com" identifier="blog-1433307"></discourse-comments>
|
|
185
|
+
<discourse-comment-count forum="https://forum.example.com" identifier="blog-1433307"></discourse-comment-count>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Attributes mirror the React props, in kebab-case:
|
|
189
|
+
|
|
190
|
+
| `<discourse-comments>` | Prop | Values |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `forum` | `forum` | forum URL (required, or set `element.client`) |
|
|
193
|
+
| `identifier`, `url`, `title` | same | thread identity; `url` and `title` default to the page |
|
|
194
|
+
| `sort` | `sort` | `best`, `hot`, `newest`, `oldest` |
|
|
195
|
+
| `sign-in` | `signIn` | `inline` (default), `on-submit`, `manual` |
|
|
196
|
+
| `theme` | `theme` | `auto` (default), `light`, `dark`, `inherit` |
|
|
197
|
+
| `locale` | `locale` | `en`, `pt-BR` |
|
|
198
|
+
| `max-depth` | `maxDepth` | number, default 2 |
|
|
199
|
+
| `read-only`, `unstyled`, `debug` | same | boolean: present means true (`"false"` is false) |
|
|
200
|
+
| `highlight` | `highlight` | comment id |
|
|
201
|
+
| `guidelines-url` | `guidelinesUrl` | URL |
|
|
202
|
+
|
|
203
|
+
`<discourse-comment-count forum identifier host url topic-id format>` takes
|
|
204
|
+
the `<CommentCount>` props the same way; `format="number"` renders just the
|
|
205
|
+
number instead of "12 comments". Counts on one page share one request.
|
|
206
|
+
|
|
207
|
+
Every attribute is also a property with the prop's name
|
|
208
|
+
(`element.theme = "dark"` sets `theme="dark"`; `element.readOnly = false`
|
|
209
|
+
removes `read-only`), so frameworks that bind properties work unchanged. Values
|
|
210
|
+
that are not valid for an attribute are ignored and the default applies. A
|
|
211
|
+
`client` property (not an attribute) takes a client from `createClient`, for
|
|
212
|
+
options such as `headers`; otherwise the element uses the page-wide shared
|
|
213
|
+
client for its `forum`, available as `element.client`.
|
|
214
|
+
|
|
215
|
+
The widget's events are dispatched from the element as bubbling, composed
|
|
216
|
+
`CustomEvent`s named `gc:` plus the event type, with the event as `detail`:
|
|
217
|
+
`gc:comment.posted`, `gc:comment.pending`, `gc:comment.liked`,
|
|
218
|
+
`gc:auth.signedIn`, `gc:auth.signedOut` and `gc:error`. With
|
|
219
|
+
`sign-in="manual"`, `gc:sign-in-required` fires when an anonymous reader asks
|
|
220
|
+
to like or reply; start your own sign-in there (for instance
|
|
221
|
+
`element.client.auth.signIn()`).
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
document.querySelector("discourse-comments").addEventListener("gc:comment.posted", (event) => {
|
|
225
|
+
analytics.track("comment", { id: event.detail.comment.id });
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The widget renders in an open shadow root with `g-comments/styles.css`
|
|
230
|
+
applied, so the page's CSS does not reach it and its CSS does not leak out.
|
|
231
|
+
Style it through the `--gc-*` tokens, which cross the shadow boundary from the
|
|
232
|
+
element or any ancestor, and through `::part()`: every `gc-*` class inside the
|
|
233
|
+
widget is exposed as a part of the same name, modifiers included.
|
|
234
|
+
|
|
235
|
+
```css
|
|
236
|
+
discourse-comments {
|
|
237
|
+
--gc-accent: var(--brand);
|
|
238
|
+
}
|
|
239
|
+
discourse-comments::part(gc-button--primary) {
|
|
240
|
+
border-radius: 999px;
|
|
241
|
+
}
|
|
242
|
+
discourse-comments::part(gc-comment--pinned) {
|
|
243
|
+
outline: 1px solid var(--brand);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`unstyled` drops the default stylesheet's effect (the classes and parts stay),
|
|
248
|
+
and `theme="inherit"` derives the neutral colours from the page's text colour.
|
|
249
|
+
The count element has no styles of its own and inherits the page's font.
|
|
250
|
+
|
|
251
|
+
`defineElements({ comments: "my-comments", count: "my-count" })` registers the
|
|
252
|
+
same elements under other names; names that are already defined are left
|
|
253
|
+
alone, so loading the module twice is harmless. Importing the module on a
|
|
254
|
+
server is a no-op, and the elements render nothing until they are connected
|
|
255
|
+
in a browser. The classes (`DiscourseCommentsElement`,
|
|
256
|
+
`DiscourseCommentCountElement`) and the `gc:*` event map are exported and
|
|
257
|
+
typed, including `HTMLElementTagNameMap` entries for `querySelector`.
|
|
258
|
+
|
|
259
|
+
## Example apps
|
|
260
|
+
|
|
261
|
+
Neither is published. `examples/vite-react` renders the React components
|
|
262
|
+
against a forum, from the SDK source:
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
VITE_FORUM=http://localhost:3000 pnpm --filter example dev # http://localhost:5174
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`examples/plain-html` is a page with no framework that loads the built custom
|
|
269
|
+
element (`pnpm build` first); `?forum=` points it at another forum:
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
pnpm --filter example-plain-html dev # http://localhost:5175
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Allow `localhost:5174` and `localhost:5175` as embedding hosts on that forum
|
|
276
|
+
first (`bin/rake "discourse_comments:seed_dev[localhost:5175]"` from a
|
|
277
|
+
Discourse checkout with the plugin does it for one host).
|
|
278
|
+
|
|
279
|
+
## Publishing
|
|
280
|
+
|
|
281
|
+
`pnpm build`, `pnpm test`, `pnpm typecheck` and `pnpm --filter g-comments
|
|
282
|
+
lint:package` (publint) should all pass; `pnpm --filter g-comments pack`
|
|
283
|
+
shows what ships: `dist/`, `styles/`, this file, the changelog and the
|
|
284
|
+
license.
|
|
285
|
+
|
|
286
|
+
## Create a client
|
|
287
|
+
|
|
288
|
+
Without React, the same client powers everything below.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { createClient } from "g-comments";
|
|
292
|
+
|
|
293
|
+
const comments = createClient({ forum: "https://forum.example.com" });
|
|
294
|
+
// or share one per forum across the page:
|
|
295
|
+
// const comments = clientRegistry.acquire({ forum: "https://forum.example.com" });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Options: `fetch`, `headers` (e.g. `Authorization` for a forum behind basic
|
|
299
|
+
auth), `storage` (defaults to `localStorage`), `clientId`, `locale`, `debug`.
|
|
300
|
+
|
|
301
|
+
## Open the page's thread
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
// url defaults to <link rel="canonical"> or location.href, title to document.title
|
|
305
|
+
const thread = await comments.threads.open({ identifier: "blog-1433307" });
|
|
306
|
+
|
|
307
|
+
thread.subscribe(() => render(thread.snapshot()));
|
|
308
|
+
// snapshot(): { thread, comments (tree), sort, status, hasMore, pendingNew, error, … }
|
|
309
|
+
|
|
310
|
+
await thread.setSort("newest");
|
|
311
|
+
await thread.loadMore();
|
|
312
|
+
await thread.loadReplies(comment.id);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Sign in and comment
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
await comments.auth.signIn(); // popup; falls back to a redirect when blocked
|
|
319
|
+
comments.auth.onChange(({ status, user }) => updateHeader(status, user));
|
|
320
|
+
|
|
321
|
+
thread.composer.start(); // when the composer opens
|
|
322
|
+
thread.composer.typing(); // on input; lets first posts skip the review queue
|
|
323
|
+
const result = await thread.post({ body: "Nice post!", replyTo: comment.id });
|
|
324
|
+
if (result.type === "pending") showAwaitingApproval(result.pending);
|
|
325
|
+
|
|
326
|
+
await thread.like(comment.id);
|
|
327
|
+
await comments.auth.signOut();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Redirect sign-ins finish on page load (`#gc_code=…`), as long as the page
|
|
331
|
+
creates its client on load.
|
|
332
|
+
|
|
333
|
+
## Realtime
|
|
334
|
+
|
|
335
|
+
Threads follow the forum's MessageBus anonymously; new top-level comments from
|
|
336
|
+
others wait in `snapshot().pendingNew` until `thread.showPending()`, while
|
|
337
|
+
replies, edits, likes and deletions apply in place. When live updates are not
|
|
338
|
+
available the thread refreshes every 30 s (backing off to 2 min, paused in
|
|
339
|
+
background tabs); `snapshot().connection` says which mode is active.
|
|
340
|
+
|
|
341
|
+
## Counts
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const counts = await comments.counts([{ url: "https://example.com/blog/1" }, { identifier: "blog-2" }]);
|
|
345
|
+
// { "url:https://example.com/blog/1": 3, "id:example.com/blog-2": 0 }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Lookups made in the same task share one request.
|
|
349
|
+
|
|
350
|
+
## Errors
|
|
351
|
+
|
|
352
|
+
Every failure is a `CommentsError` with a `type` (`rate_limit` carries
|
|
353
|
+
`waitSeconds`) and `status`. `unreachable` means no response at all; its `hint`
|
|
354
|
+
lists the usual setup causes (origin not allowed, basic auth blocking CORS
|
|
355
|
+
preflights, plugin disabled).
|