@livo-tv/sdk 1.10.0 → 1.10.1-pr.6.4bf5bbd
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 +19 -1
- package/dist/condo/index.cjs +648 -0
- package/dist/condo/index.cjs.map +1 -0
- package/dist/condo/index.d.cts +162 -0
- package/dist/condo/index.d.ts +162 -0
- package/dist/condo/index.js +634 -0
- package/dist/condo/index.js.map +1 -0
- package/dist/index.cjs +45 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +45 -9
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +25 -0
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +25 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -1
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.js.map +1 -1
- package/dist/styles.css +142 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -11,7 +11,25 @@ import "@livo-tv/sdk/styles.css";
|
|
|
11
11
|
<LivoStudio guestToken="..." theme={{ primary: "#2563eb" }} />
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`@livo-tv/sdk
|
|
14
|
+
`@livo-tv/sdk`, `@livo-tv/sdk/studio`, and `@livo-tv/sdk/condo` (`LivoCommunity`) are client entries (the published bundles start with `"use client"`). Import them from `"use client"` files in Next.js App Router — do not import those entries from a Server Component page. Tailwind hosts must `@source` `node_modules/@livo-tv/sdk/dist/*.{js,cjs,mjs}` like `@livo-tv/blocks`. RealtimeKit is an optional peer (`@cloudflare/realtimekit`). Root `<LivoStudio>` remains the iframe fallback.
|
|
15
|
+
|
|
16
|
+
Community (condo) — comments on every video, live Q&A only while a stream or webinar is `preview` / `public`. After the event, Q&A stays closed. Comments stay closed unless the organizer sets `commentsAfterEnd`. Pass a public `apiUrl` for viewers (`mode="viewer"`, polls). Pass JWT / `X-Api-Key` headers and `mode="control"` for settings, pin/hide/delete, and Q&A highlight/dismiss/answer. Unregistered posters send `displayName`, optional `picture` URL, and `guestId`.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { LivoCommunity } from "@livo-tv/sdk/condo";
|
|
20
|
+
import "@livo-tv/sdk/styles.css";
|
|
21
|
+
|
|
22
|
+
<LivoCommunity
|
|
23
|
+
apiUrl="https://api.livo.tv"
|
|
24
|
+
targetKind="stream"
|
|
25
|
+
targetId={stream.id}
|
|
26
|
+
flags={stream.community}
|
|
27
|
+
mode="viewer"
|
|
28
|
+
displayName="Ada"
|
|
29
|
+
picture="https://cdn.example/ada.jpg"
|
|
30
|
+
guestId="guest-ada"
|
|
31
|
+
/>;
|
|
32
|
+
```
|
|
15
33
|
|
|
16
34
|
Server (Node / Workers):
|
|
17
35
|
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/condo/community.tsx
|
|
8
|
+
|
|
9
|
+
// src/condo/types.ts
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function asString(value) {
|
|
14
|
+
return typeof value === "string" ? value : null;
|
|
15
|
+
}
|
|
16
|
+
function asNumber(value) {
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
18
|
+
}
|
|
19
|
+
function parseCommunityFlags(value, kind) {
|
|
20
|
+
if (!isRecord(value)) return null;
|
|
21
|
+
if (kind === "vod") {
|
|
22
|
+
if (typeof value.commentsEnabled !== "boolean") return null;
|
|
23
|
+
return {
|
|
24
|
+
commentsEnabled: value.commentsEnabled,
|
|
25
|
+
commentsOpen: typeof value.commentsOpen === "boolean" ? value.commentsOpen : value.commentsEnabled
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (typeof value.commentsEnabled !== "boolean" || typeof value.commentsAfterEnd !== "boolean" || typeof value.qaEnabled !== "boolean") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
commentsEnabled: value.commentsEnabled,
|
|
33
|
+
commentsAfterEnd: value.commentsAfterEnd,
|
|
34
|
+
commentsOpen: typeof value.commentsOpen === "boolean" ? value.commentsOpen : value.commentsEnabled,
|
|
35
|
+
qaEnabled: value.qaEnabled,
|
|
36
|
+
qaOpen: typeof value.qaOpen === "boolean" ? value.qaOpen : false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function parseAnswer(value) {
|
|
40
|
+
if (!isRecord(value)) return null;
|
|
41
|
+
const body = asString(value.body);
|
|
42
|
+
const answeredAt = asNumber(value.answeredAt);
|
|
43
|
+
if (!body || answeredAt == null) return null;
|
|
44
|
+
return {
|
|
45
|
+
body,
|
|
46
|
+
displayName: asString(value.displayName),
|
|
47
|
+
picture: asString(value.picture),
|
|
48
|
+
answeredAt
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function parseCommunityItem(value) {
|
|
52
|
+
if (!isRecord(value)) return null;
|
|
53
|
+
const id = asString(value.id);
|
|
54
|
+
const body = asString(value.body);
|
|
55
|
+
const displayName = asString(value.displayName);
|
|
56
|
+
const guestId = asString(value.guestId);
|
|
57
|
+
const type = value.type === "question" ? "question" : "comment";
|
|
58
|
+
const createdAt = asNumber(value.createdAt);
|
|
59
|
+
if (!id || !body || !displayName || !guestId || createdAt == null) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const replies = Array.isArray(value.replies) ? value.replies.map((row) => parseCommunityItem(row)).filter((row) => row != null) : [];
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
type,
|
|
66
|
+
parentId: asString(value.parentId),
|
|
67
|
+
body,
|
|
68
|
+
displayName,
|
|
69
|
+
picture: asString(value.picture),
|
|
70
|
+
guestId,
|
|
71
|
+
upvoteCount: asNumber(value.upvoteCount) ?? 0,
|
|
72
|
+
pinnedAt: asNumber(value.pinnedAt),
|
|
73
|
+
highlightedAt: asNumber(value.highlightedAt),
|
|
74
|
+
hiddenAt: asNumber(value.hiddenAt),
|
|
75
|
+
dismissedAt: asNumber(value.dismissedAt),
|
|
76
|
+
answer: parseAnswer(value.answer),
|
|
77
|
+
createdAt,
|
|
78
|
+
updatedAt: asNumber(value.updatedAt) ?? createdAt,
|
|
79
|
+
replies
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseCommunityList(value) {
|
|
83
|
+
if (!isRecord(value) || !Array.isArray(value.items)) {
|
|
84
|
+
return { items: [], nextCursor: null };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
items: value.items.map((row) => parseCommunityItem(row)).filter((row) => row != null),
|
|
88
|
+
nextCursor: asString(value.nextCursor)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function hasQa(flags) {
|
|
92
|
+
return "qaEnabled" in flags;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/condo/api.ts
|
|
96
|
+
var CommunityApiError = class extends Error {
|
|
97
|
+
status;
|
|
98
|
+
body;
|
|
99
|
+
constructor(status, message, body) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "CommunityApiError";
|
|
102
|
+
this.status = status;
|
|
103
|
+
this.body = body;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
function collectionPath(kind, id, mode) {
|
|
107
|
+
const plural = kind === "stream" ? "streams" : "vods";
|
|
108
|
+
const prefix = mode === "viewer" ? "/public" : "";
|
|
109
|
+
return `${prefix}/${plural}/${encodeURIComponent(id)}/community`;
|
|
110
|
+
}
|
|
111
|
+
function createCommunityClient(options) {
|
|
112
|
+
const apiUrl = options.apiUrl.replace(/\/$/, "");
|
|
113
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
114
|
+
const mode = options.mode ?? "viewer";
|
|
115
|
+
const base = collectionPath(options.targetKind, options.targetId, mode);
|
|
116
|
+
async function request(path, init = {}, parse) {
|
|
117
|
+
const headers = new Headers(init.headers);
|
|
118
|
+
for (const [key, value] of Object.entries(options.headers ?? {})) {
|
|
119
|
+
headers.set(key, value);
|
|
120
|
+
}
|
|
121
|
+
headers.set("accept", "application/json");
|
|
122
|
+
if (init.body && !headers.has("content-type")) {
|
|
123
|
+
headers.set("content-type", "application/json");
|
|
124
|
+
}
|
|
125
|
+
const res = await fetchFn(`${apiUrl}${path}`, { ...init, headers });
|
|
126
|
+
const body = await res.json().catch(() => ({}));
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const error = typeof body === "object" && body !== null && "error" in body && typeof body.error === "string" ? body.error : `Livo community ${res.status}`;
|
|
129
|
+
throw new CommunityApiError(res.status, error, body);
|
|
130
|
+
}
|
|
131
|
+
return parse(body);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
list(input = {}) {
|
|
135
|
+
const params = new URLSearchParams();
|
|
136
|
+
if (input.type) params.set("type", input.type);
|
|
137
|
+
if (input.q) params.set("q", input.q);
|
|
138
|
+
if (input.cursor) params.set("cursor", input.cursor);
|
|
139
|
+
const query = params.toString();
|
|
140
|
+
return request(
|
|
141
|
+
`${base}${query ? `?${query}` : ""}`,
|
|
142
|
+
{},
|
|
143
|
+
(value) => parseCommunityList(value)
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
create(input) {
|
|
147
|
+
return request(
|
|
148
|
+
base,
|
|
149
|
+
{ method: "POST", body: JSON.stringify(input) },
|
|
150
|
+
(value) => {
|
|
151
|
+
const record = typeof value === "object" && value !== null && "item" in value ? value.item : value;
|
|
152
|
+
const item = parseCommunityItem(record);
|
|
153
|
+
if (!item) throw new CommunityApiError(500, "Invalid item", value);
|
|
154
|
+
return item;
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
patch(itemId, input) {
|
|
159
|
+
return request(
|
|
160
|
+
`${base}/${encodeURIComponent(itemId)}`,
|
|
161
|
+
{ method: "PATCH", body: JSON.stringify(input) },
|
|
162
|
+
(value) => {
|
|
163
|
+
const record = typeof value === "object" && value !== null && "item" in value ? value.item : value;
|
|
164
|
+
const item = parseCommunityItem(record);
|
|
165
|
+
if (!item) throw new CommunityApiError(500, "Invalid item", value);
|
|
166
|
+
return item;
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
upvote(itemId, guestId) {
|
|
171
|
+
return request(
|
|
172
|
+
`${base}/${encodeURIComponent(itemId)}/upvote`,
|
|
173
|
+
{
|
|
174
|
+
method: "POST",
|
|
175
|
+
body: JSON.stringify({ guestId })
|
|
176
|
+
},
|
|
177
|
+
(value) => {
|
|
178
|
+
const record = typeof value === "object" && value !== null && "item" in value ? value.item : value;
|
|
179
|
+
const item = parseCommunityItem(record);
|
|
180
|
+
if (!item) throw new CommunityApiError(500, "Invalid item", value);
|
|
181
|
+
return item;
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
patchSettings(input) {
|
|
186
|
+
const plural = options.targetKind === "stream" ? "streams" : "vods";
|
|
187
|
+
return request(
|
|
188
|
+
`/${plural}/${encodeURIComponent(options.targetId)}`,
|
|
189
|
+
{ method: "PATCH", body: JSON.stringify(input) },
|
|
190
|
+
(value) => {
|
|
191
|
+
const record = typeof value === "object" && value !== null ? value : null;
|
|
192
|
+
const nested = record && typeof record.stream === "object" && record.stream ? record.stream.community : record && typeof record.vod === "object" && record.vod ? record.vod.community : record?.community;
|
|
193
|
+
return parseCommunityFlags(nested, options.targetKind);
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/condo/gates.ts
|
|
201
|
+
function commentsOpen(flags) {
|
|
202
|
+
return flags.commentsOpen;
|
|
203
|
+
}
|
|
204
|
+
function qaOpen(flags) {
|
|
205
|
+
return hasQa(flags) && flags.qaOpen;
|
|
206
|
+
}
|
|
207
|
+
function canPost(flags, type) {
|
|
208
|
+
if (type === "question") return qaOpen(flags);
|
|
209
|
+
return commentsOpen(flags);
|
|
210
|
+
}
|
|
211
|
+
function showQaTab(flags) {
|
|
212
|
+
return hasQa(flags) && flags.qaEnabled;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/condo/identity.ts
|
|
216
|
+
var STORAGE_KEY = "livo-community-guest-id";
|
|
217
|
+
function randomGuestId() {
|
|
218
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
219
|
+
return crypto.randomUUID();
|
|
220
|
+
}
|
|
221
|
+
return `guest-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
222
|
+
}
|
|
223
|
+
function communityGuestId(storage) {
|
|
224
|
+
const store = storage ?? (typeof localStorage === "undefined" ? void 0 : localStorage);
|
|
225
|
+
if (!store) return randomGuestId();
|
|
226
|
+
const existing = store.getItem(STORAGE_KEY)?.trim();
|
|
227
|
+
if (existing) return existing;
|
|
228
|
+
const created = randomGuestId();
|
|
229
|
+
store.setItem(STORAGE_KEY, created);
|
|
230
|
+
return created;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/condo/labels.ts
|
|
234
|
+
var communityLabels = {
|
|
235
|
+
comments: "Comments",
|
|
236
|
+
qa: "Q&A",
|
|
237
|
+
composer: "Write a comment",
|
|
238
|
+
questionComposer: "Ask a question",
|
|
239
|
+
reply: "Reply",
|
|
240
|
+
send: "Send",
|
|
241
|
+
name: "Display name",
|
|
242
|
+
namePlaceholder: "Your name",
|
|
243
|
+
closed: "Comments are closed",
|
|
244
|
+
qaClosed: "Q&A is closed",
|
|
245
|
+
emptyComments: "No comments yet",
|
|
246
|
+
emptyQuestions: "No questions yet",
|
|
247
|
+
pin: "Pin",
|
|
248
|
+
unpin: "Unpin",
|
|
249
|
+
hide: "Hide",
|
|
250
|
+
unhide: "Unhide",
|
|
251
|
+
remove: "Delete",
|
|
252
|
+
highlight: "Highlight",
|
|
253
|
+
dismiss: "Dismiss",
|
|
254
|
+
answer: "Answer",
|
|
255
|
+
upvote: "Upvote",
|
|
256
|
+
settings: "Community",
|
|
257
|
+
commentsEnabled: "Comments",
|
|
258
|
+
commentsAfterEnd: "Keep comments open after the event",
|
|
259
|
+
qaEnabled: "Live Q&A",
|
|
260
|
+
commentsHelp: "Viewers can comment on this video.",
|
|
261
|
+
commentsAfterEndHelp: "When off, comments close as soon as the live event ends.",
|
|
262
|
+
qaHelp: "Questions are only accepted while the event is live."
|
|
263
|
+
};
|
|
264
|
+
function mergeLabels(partial) {
|
|
265
|
+
return { ...communityLabels, ...partial };
|
|
266
|
+
}
|
|
267
|
+
function ItemRow({
|
|
268
|
+
item,
|
|
269
|
+
mode,
|
|
270
|
+
labels,
|
|
271
|
+
onReply,
|
|
272
|
+
onPatch,
|
|
273
|
+
onUpvote
|
|
274
|
+
}) {
|
|
275
|
+
const [answer, setAnswer] = react.useState("");
|
|
276
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
277
|
+
"article",
|
|
278
|
+
{
|
|
279
|
+
className: "livo-condo-item",
|
|
280
|
+
"data-highlighted": item.highlightedAt ? "true" : void 0,
|
|
281
|
+
"data-hidden": item.hiddenAt ? "true" : void 0,
|
|
282
|
+
children: [
|
|
283
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "livo-condo-item__meta", children: [
|
|
284
|
+
item.picture ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: item.picture, alt: "", className: "livo-condo-avatar" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "livo-condo-avatar livo-condo-avatar--fallback", children: item.displayName.slice(0, 1).toUpperCase() }),
|
|
285
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: item.displayName }),
|
|
286
|
+
item.pinnedAt ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "livo-condo-chip", children: "Pinned" }) : null,
|
|
287
|
+
item.highlightedAt ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "livo-condo-chip", children: "Live" }) : null
|
|
288
|
+
] }),
|
|
289
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "livo-condo-item__body", children: item.body }),
|
|
290
|
+
item.answer ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "livo-condo-answer", children: [
|
|
291
|
+
/* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
|
|
292
|
+
item.answer.displayName ?? "Host",
|
|
293
|
+
":"
|
|
294
|
+
] }),
|
|
295
|
+
" ",
|
|
296
|
+
item.answer.body
|
|
297
|
+
] }) : null,
|
|
298
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "livo-condo-item__actions", children: [
|
|
299
|
+
item.type === "question" && onUpvote ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => onUpvote(item.id), children: [
|
|
300
|
+
labels.upvote,
|
|
301
|
+
" ",
|
|
302
|
+
item.upvoteCount
|
|
303
|
+
] }) : null,
|
|
304
|
+
item.type === "comment" && !item.parentId && onReply ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => onReply(item.id), children: labels.reply }) : null,
|
|
305
|
+
mode === "control" && item.type === "comment" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
306
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
307
|
+
"button",
|
|
308
|
+
{
|
|
309
|
+
type: "button",
|
|
310
|
+
onClick: () => onPatch(item.id, { pinned: item.pinnedAt == null }),
|
|
311
|
+
children: item.pinnedAt ? labels.unpin : labels.pin
|
|
312
|
+
}
|
|
313
|
+
),
|
|
314
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
315
|
+
"button",
|
|
316
|
+
{
|
|
317
|
+
type: "button",
|
|
318
|
+
onClick: () => onPatch(item.id, { hidden: item.hiddenAt == null }),
|
|
319
|
+
children: item.hiddenAt ? labels.unhide : labels.hide
|
|
320
|
+
}
|
|
321
|
+
),
|
|
322
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
323
|
+
"button",
|
|
324
|
+
{
|
|
325
|
+
type: "button",
|
|
326
|
+
onClick: () => onPatch(item.id, { deleted: true }),
|
|
327
|
+
children: labels.remove
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
] }) : null,
|
|
331
|
+
mode === "control" && item.type === "question" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
332
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
333
|
+
"button",
|
|
334
|
+
{
|
|
335
|
+
type: "button",
|
|
336
|
+
onClick: () => onPatch(item.id, { highlighted: item.highlightedAt == null }),
|
|
337
|
+
children: labels.highlight
|
|
338
|
+
}
|
|
339
|
+
),
|
|
340
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
341
|
+
"button",
|
|
342
|
+
{
|
|
343
|
+
type: "button",
|
|
344
|
+
onClick: () => onPatch(item.id, { dismissed: item.dismissedAt == null }),
|
|
345
|
+
children: labels.dismiss
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
] }) : null
|
|
349
|
+
] }),
|
|
350
|
+
mode === "control" && item.type === "question" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
351
|
+
"form",
|
|
352
|
+
{
|
|
353
|
+
className: "livo-condo-answer-form",
|
|
354
|
+
onSubmit: (event) => {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
const body = answer.trim();
|
|
357
|
+
if (!body) return;
|
|
358
|
+
onPatch(item.id, { answer: { body } });
|
|
359
|
+
setAnswer("");
|
|
360
|
+
},
|
|
361
|
+
children: [
|
|
362
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
363
|
+
"input",
|
|
364
|
+
{
|
|
365
|
+
value: answer,
|
|
366
|
+
onChange: (event) => setAnswer(event.target.value),
|
|
367
|
+
placeholder: labels.answer,
|
|
368
|
+
"aria-label": labels.answer
|
|
369
|
+
}
|
|
370
|
+
),
|
|
371
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", children: labels.send })
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
) : null,
|
|
375
|
+
item.replies?.map((reply) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
376
|
+
ItemRow,
|
|
377
|
+
{
|
|
378
|
+
item: reply,
|
|
379
|
+
mode,
|
|
380
|
+
labels,
|
|
381
|
+
onPatch
|
|
382
|
+
},
|
|
383
|
+
reply.id
|
|
384
|
+
))
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
function LivoCommunity({
|
|
390
|
+
apiUrl,
|
|
391
|
+
targetKind,
|
|
392
|
+
targetId,
|
|
393
|
+
flags,
|
|
394
|
+
mode = "viewer",
|
|
395
|
+
fetchFn,
|
|
396
|
+
headers,
|
|
397
|
+
displayName: displayNameProp,
|
|
398
|
+
picture,
|
|
399
|
+
guestId: guestIdProp,
|
|
400
|
+
pollMs = 5e3,
|
|
401
|
+
labels: labelsProp,
|
|
402
|
+
className,
|
|
403
|
+
onFlagsChange
|
|
404
|
+
}) {
|
|
405
|
+
const labels = react.useMemo(() => mergeLabels(labelsProp), [labelsProp]);
|
|
406
|
+
const client = react.useMemo(
|
|
407
|
+
() => createCommunityClient({
|
|
408
|
+
apiUrl,
|
|
409
|
+
targetKind,
|
|
410
|
+
targetId,
|
|
411
|
+
mode,
|
|
412
|
+
fetchFn,
|
|
413
|
+
headers
|
|
414
|
+
}),
|
|
415
|
+
[apiUrl, fetchFn, headers, mode, targetId, targetKind]
|
|
416
|
+
);
|
|
417
|
+
const guestId = guestIdProp ?? communityGuestId();
|
|
418
|
+
const [tab, setTab] = react.useState("comment");
|
|
419
|
+
const [items, setItems] = react.useState([]);
|
|
420
|
+
const [body, setBody] = react.useState("");
|
|
421
|
+
const [name, setName] = react.useState(displayNameProp ?? "");
|
|
422
|
+
const [replyTo, setReplyTo] = react.useState(null);
|
|
423
|
+
const [error, setError] = react.useState(null);
|
|
424
|
+
const [settings, setSettings] = react.useState(flags);
|
|
425
|
+
react.useEffect(() => {
|
|
426
|
+
setSettings(flags);
|
|
427
|
+
}, [flags]);
|
|
428
|
+
react.useEffect(() => {
|
|
429
|
+
if (!showQaTab(settings) && tab === "question") setTab("comment");
|
|
430
|
+
}, [settings, tab]);
|
|
431
|
+
const load = react.useCallback(async () => {
|
|
432
|
+
const listed = await client.list({
|
|
433
|
+
type: showQaTab(settings) ? tab : "comment"
|
|
434
|
+
});
|
|
435
|
+
setItems(listed.items);
|
|
436
|
+
}, [client, settings, tab]);
|
|
437
|
+
react.useEffect(() => {
|
|
438
|
+
let cancelled = false;
|
|
439
|
+
const tick = async () => {
|
|
440
|
+
try {
|
|
441
|
+
await load();
|
|
442
|
+
if (!cancelled) setError(null);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
if (!cancelled) {
|
|
445
|
+
setError(err instanceof Error ? err.message : "Failed to load");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
void tick();
|
|
450
|
+
if (pollMs <= 0) return;
|
|
451
|
+
const id = window.setInterval(() => void tick(), pollMs);
|
|
452
|
+
return () => {
|
|
453
|
+
cancelled = true;
|
|
454
|
+
window.clearInterval(id);
|
|
455
|
+
};
|
|
456
|
+
}, [load, pollMs]);
|
|
457
|
+
const open = tab === "question" ? qaOpen(settings) : commentsOpen(settings);
|
|
458
|
+
async function submit(event) {
|
|
459
|
+
event.preventDefault();
|
|
460
|
+
const text = body.trim();
|
|
461
|
+
const displayName = (displayNameProp ?? name).trim();
|
|
462
|
+
if (!text || !displayName) return;
|
|
463
|
+
try {
|
|
464
|
+
await client.create({
|
|
465
|
+
type: tab,
|
|
466
|
+
body: text,
|
|
467
|
+
displayName,
|
|
468
|
+
picture: picture ?? void 0,
|
|
469
|
+
guestId,
|
|
470
|
+
parentId: tab === "comment" ? replyTo ?? void 0 : void 0
|
|
471
|
+
});
|
|
472
|
+
setBody("");
|
|
473
|
+
setReplyTo(null);
|
|
474
|
+
await load();
|
|
475
|
+
setError(null);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
setError(
|
|
478
|
+
err instanceof CommunityApiError ? err.message : "Could not post"
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function handlePatch(itemId, patch) {
|
|
483
|
+
try {
|
|
484
|
+
await client.patch(itemId, patch);
|
|
485
|
+
await load();
|
|
486
|
+
} catch (err) {
|
|
487
|
+
setError(err instanceof Error ? err.message : "Could not update");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function handleUpvote(itemId) {
|
|
491
|
+
try {
|
|
492
|
+
await client.upvote(itemId, guestId);
|
|
493
|
+
await load();
|
|
494
|
+
} catch (err) {
|
|
495
|
+
setError(err instanceof Error ? err.message : "Could not upvote");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function updateSetting(patch) {
|
|
499
|
+
try {
|
|
500
|
+
const parsed = await client.patchSettings(patch);
|
|
501
|
+
const next = parsed ?? { ...settings, ...patch };
|
|
502
|
+
setSettings(next);
|
|
503
|
+
onFlagsChange?.(next);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
setError(err instanceof Error ? err.message : "Could not save");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: ["livo-condo", className].filter(Boolean).join(" "), children: [
|
|
509
|
+
mode === "control" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "livo-condo-settings", children: [
|
|
510
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: labels.settings }),
|
|
511
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
512
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
513
|
+
"input",
|
|
514
|
+
{
|
|
515
|
+
type: "checkbox",
|
|
516
|
+
checked: settings.commentsEnabled,
|
|
517
|
+
onChange: (event) => void updateSetting({ commentsEnabled: event.target.checked })
|
|
518
|
+
}
|
|
519
|
+
),
|
|
520
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
521
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: labels.commentsEnabled }),
|
|
522
|
+
/* @__PURE__ */ jsxRuntime.jsx("small", { children: labels.commentsHelp })
|
|
523
|
+
] })
|
|
524
|
+
] }),
|
|
525
|
+
hasQa(settings) ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
526
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
527
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
528
|
+
"input",
|
|
529
|
+
{
|
|
530
|
+
type: "checkbox",
|
|
531
|
+
checked: settings.commentsAfterEnd,
|
|
532
|
+
onChange: (event) => void updateSetting({
|
|
533
|
+
commentsAfterEnd: event.target.checked
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
),
|
|
537
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
538
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: labels.commentsAfterEnd }),
|
|
539
|
+
/* @__PURE__ */ jsxRuntime.jsx("small", { children: labels.commentsAfterEndHelp })
|
|
540
|
+
] })
|
|
541
|
+
] }),
|
|
542
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
543
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
544
|
+
"input",
|
|
545
|
+
{
|
|
546
|
+
type: "checkbox",
|
|
547
|
+
checked: settings.qaEnabled,
|
|
548
|
+
onChange: (event) => void updateSetting({ qaEnabled: event.target.checked })
|
|
549
|
+
}
|
|
550
|
+
),
|
|
551
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
552
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: labels.qaEnabled }),
|
|
553
|
+
/* @__PURE__ */ jsxRuntime.jsx("small", { children: labels.qaHelp })
|
|
554
|
+
] })
|
|
555
|
+
] })
|
|
556
|
+
] }) : null
|
|
557
|
+
] }) : null,
|
|
558
|
+
showQaTab(settings) ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "livo-condo-tabs", role: "tablist", children: [
|
|
559
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
560
|
+
"button",
|
|
561
|
+
{
|
|
562
|
+
type: "button",
|
|
563
|
+
role: "tab",
|
|
564
|
+
"aria-selected": tab === "comment",
|
|
565
|
+
onClick: () => setTab("comment"),
|
|
566
|
+
children: labels.comments
|
|
567
|
+
}
|
|
568
|
+
),
|
|
569
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
570
|
+
"button",
|
|
571
|
+
{
|
|
572
|
+
type: "button",
|
|
573
|
+
role: "tab",
|
|
574
|
+
"aria-selected": tab === "question",
|
|
575
|
+
onClick: () => setTab("question"),
|
|
576
|
+
children: labels.qa
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "livo-condo-title", children: labels.comments }),
|
|
580
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "livo-condo-list", children: items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "livo-condo-empty", children: tab === "question" ? labels.emptyQuestions : labels.emptyComments }) : items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
581
|
+
ItemRow,
|
|
582
|
+
{
|
|
583
|
+
item,
|
|
584
|
+
mode,
|
|
585
|
+
labels,
|
|
586
|
+
onReply: canPost(settings, "comment") ? setReplyTo : void 0,
|
|
587
|
+
onPatch: handlePatch,
|
|
588
|
+
onUpvote: tab === "question" ? handleUpvote : void 0
|
|
589
|
+
},
|
|
590
|
+
item.id
|
|
591
|
+
)) }),
|
|
592
|
+
open ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
593
|
+
"form",
|
|
594
|
+
{
|
|
595
|
+
className: "livo-condo-composer",
|
|
596
|
+
onSubmit: (event) => void submit(event),
|
|
597
|
+
children: [
|
|
598
|
+
displayNameProp ? null : /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
599
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "livo-sr-only", children: labels.name }),
|
|
600
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
601
|
+
"input",
|
|
602
|
+
{
|
|
603
|
+
value: name,
|
|
604
|
+
onChange: (event) => setName(event.target.value),
|
|
605
|
+
placeholder: labels.namePlaceholder,
|
|
606
|
+
required: true,
|
|
607
|
+
maxLength: 80
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
] }),
|
|
611
|
+
replyTo ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "livo-condo-replying", children: [
|
|
612
|
+
labels.reply,
|
|
613
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setReplyTo(null), children: "\xD7" })
|
|
614
|
+
] }) : null,
|
|
615
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
616
|
+
"textarea",
|
|
617
|
+
{
|
|
618
|
+
value: body,
|
|
619
|
+
onChange: (event) => setBody(event.target.value),
|
|
620
|
+
placeholder: tab === "question" ? labels.questionComposer : labels.composer,
|
|
621
|
+
required: true,
|
|
622
|
+
maxLength: 2e3,
|
|
623
|
+
rows: 3
|
|
624
|
+
}
|
|
625
|
+
),
|
|
626
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", children: labels.send })
|
|
627
|
+
]
|
|
628
|
+
}
|
|
629
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "livo-condo-closed", role: "status", children: tab === "question" ? labels.qaClosed : labels.closed }),
|
|
630
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "livo-condo-error", role: "alert", children: error }) : null
|
|
631
|
+
] });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
exports.CommunityApiError = CommunityApiError;
|
|
635
|
+
exports.LivoCommunity = LivoCommunity;
|
|
636
|
+
exports.canPost = canPost;
|
|
637
|
+
exports.commentsOpen = commentsOpen;
|
|
638
|
+
exports.communityGuestId = communityGuestId;
|
|
639
|
+
exports.communityLabels = communityLabels;
|
|
640
|
+
exports.createCommunityClient = createCommunityClient;
|
|
641
|
+
exports.hasQa = hasQa;
|
|
642
|
+
exports.parseCommunityFlags = parseCommunityFlags;
|
|
643
|
+
exports.parseCommunityItem = parseCommunityItem;
|
|
644
|
+
exports.parseCommunityList = parseCommunityList;
|
|
645
|
+
exports.qaOpen = qaOpen;
|
|
646
|
+
exports.showQaTab = showQaTab;
|
|
647
|
+
//# sourceMappingURL=index.cjs.map
|
|
648
|
+
//# sourceMappingURL=index.cjs.map
|