@rathnasgala/theme 0.0.20 → 2.0.2
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/package.json +1 -1
- package/payload/.gala/managed-files.json +28 -23
- package/payload/eleventy.config.js +15 -1
- package/payload/lib/accent.js +105 -0
- package/payload/lib/render-markdown.js +1 -1
- package/payload/lib/seo.js +17 -3
- package/payload/lib/site-config.js +75 -5
- package/payload/package-lock.json +487 -2
- package/payload/package.json +7 -6
- package/payload/scripts/build-reader.js +24 -0
- package/payload/scripts/lint.js +21 -0
- package/payload/src/_data/site.js +16 -0
- package/payload/src/_includes/components/ui.njk +38 -17
- package/payload/src/_includes/layouts/base.njk +33 -26
- package/payload/src/_includes/layouts/post.njk +11 -7
- package/payload/src/accent.11ty.js +10 -0
- package/payload/src/assets/engagement-comments.js +125 -234
- package/payload/src/assets/engagement-transport.js +10 -4
- package/payload/src/assets/interactions.js +161 -39
- package/payload/src/assets/reader.js +2 -0
- package/payload/src/assets/theme.css +1 -676
- package/payload/src/client/reader.js +5 -0
- package/payload/src/posts.11ty.js +6 -0
- package/payload/src/styles/theme.css +1058 -0
- package/payload/static/favicon.ico +0 -0
- package/payload/src/assets/vendor/hooks.js +0 -2
- package/payload/src/assets/vendor/htm.js +0 -2
- package/payload/src/assets/vendor/preact.js +0 -2
- package/payload/src/assets/vendor/signals-core.js +0 -2
- package/payload/src/assets/vendor/signals.js +0 -2
|
@@ -1,264 +1,155 @@
|
|
|
1
|
-
|
|
2
|
-
* The conversation under an article.
|
|
3
|
-
*
|
|
4
|
-
* Built as an island rather than by hand-writing DOM, because the state here is real: a thread
|
|
5
|
-
* five deep, a page of roots with more behind a cursor, a reply form belonging to one comment, an
|
|
6
|
-
* edit in progress, and a reader who may sign in halfway through. The imperative version rebuilt
|
|
7
|
-
* the whole region from a fresh fetch after every write, which is how a posted comment could take
|
|
8
|
-
* a browser cache's word for it and not appear at all — and a comment that is not rendered has no
|
|
9
|
-
* Reply button, so the thread died with it.
|
|
10
|
-
*
|
|
11
|
-
* The article itself is never touched. Eleventy renders the post and everything search engines
|
|
12
|
-
* read; this mounts only into the comments container, which was always filled from the API and so
|
|
13
|
-
* has nothing to lose by being rendered here.
|
|
14
|
-
*/
|
|
15
|
-
import { h, render } from './vendor/preact.js';
|
|
16
|
-
import { useCallback, useEffect, useMemo, useState } from './vendor/hooks.js';
|
|
17
|
-
import htm from './vendor/htm.js';
|
|
18
|
-
// Importing the signals integration is what lets a component re-render when it reads `.value`.
|
|
19
|
-
import './vendor/signals.js';
|
|
20
|
-
import {
|
|
21
|
-
engagementErrorMessage, requestSignIn, sendEngagementWrite, sessionUser,
|
|
22
|
-
} from './engagement-transport.js';
|
|
1
|
+
import { engagementErrorMessage, requestSignIn, sendEngagementWrite, sessionUser } from './engagement-transport.js';
|
|
23
2
|
|
|
24
|
-
const html = htm.bind(h);
|
|
25
|
-
|
|
26
|
-
/** The server's own limit. A reply deeper than this is refused, so it is not offered. */
|
|
27
3
|
const MAXIMUM_DEPTH = 5;
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
4
|
+
const element = (name, className, text) => {
|
|
5
|
+
const node = document.createElement(name);
|
|
6
|
+
if (className) node.className = className;
|
|
7
|
+
if (text !== undefined) node.textContent = text;
|
|
8
|
+
return node;
|
|
32
9
|
};
|
|
33
10
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const byParent = new Map();
|
|
11
|
+
function threads(items) {
|
|
12
|
+
const children = new Map();
|
|
37
13
|
for (const item of items) {
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
40
|
-
|
|
14
|
+
const parent = item.parentCommentId ?? '';
|
|
15
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
16
|
+
children.get(parent).push(item);
|
|
41
17
|
}
|
|
42
|
-
const build = (
|
|
43
|
-
...
|
|
44
|
-
depth,
|
|
45
|
-
replies: build(comment.commentId, depth + 1),
|
|
18
|
+
const build = (parent, depth) => (children.get(parent) ?? []).map((item) => ({
|
|
19
|
+
...item, depth, replies: build(item.commentId, depth + 1),
|
|
46
20
|
}));
|
|
47
21
|
return build('', 0);
|
|
48
22
|
}
|
|
49
23
|
|
|
50
|
-
function
|
|
51
|
-
const [
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const mine = !comment.deleted && reader && comment.author?.userId === reader.id;
|
|
55
|
-
const canReply = !comment.deleted && comment.depth < MAXIMUM_DEPTH;
|
|
56
|
-
|
|
57
|
-
const submitReply = async (event) => {
|
|
58
|
-
event.preventDefault();
|
|
59
|
-
if (!draft.trim()) return;
|
|
60
|
-
const posted = await onReply(comment.commentId, draft.trim());
|
|
61
|
-
if (posted) { setDraft(''); setReplying(false); }
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const submitEdit = async (event) => {
|
|
65
|
-
event.preventDefault();
|
|
66
|
-
if (!draft.trim()) return;
|
|
67
|
-
const saved = await onEdit(comment.commentId, draft.trim());
|
|
68
|
-
if (saved) setEditing(false);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
return html`
|
|
72
|
-
<li class="gala-comment" data-comment-id=${comment.commentId} data-depth=${comment.depth}>
|
|
73
|
-
<p class="gala-comment__meta">
|
|
74
|
-
<strong>${comment.author?.displayName ?? '[deleted]'}</strong>
|
|
75
|
-
${comment.createdAt && html`<time datetime=${comment.createdAt}>${isoTime(comment.createdAt)}</time>`}
|
|
76
|
-
${comment.editedAt && html`<span class="gala-comment__edited">edited</span>`}
|
|
77
|
-
${comment.pending && html`<span class="gala-comment__pending">sending…</span>`}
|
|
78
|
-
</p>
|
|
79
|
-
|
|
80
|
-
${editing
|
|
81
|
-
? html`<form class="gala-comment__form" onSubmit=${submitEdit}>
|
|
82
|
-
<label class="gala-visually-hidden" for=${`edit-${comment.commentId}`}>Edit your comment</label>
|
|
83
|
-
<textarea id=${`edit-${comment.commentId}`} rows="3" value=${draft}
|
|
84
|
-
onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
85
|
-
<div class="gala-comment__controls">
|
|
86
|
-
<button type="submit" disabled=${busy}>Save</button>
|
|
87
|
-
<button type="button" onClick=${() => setEditing(false)}>Cancel</button>
|
|
88
|
-
</div>
|
|
89
|
-
</form>`
|
|
90
|
-
: html`<p class="gala-comment__body">${comment.deleted ? '[deleted]' : comment.body}</p>`}
|
|
91
|
-
|
|
92
|
-
${!comment.deleted && html`
|
|
93
|
-
<div class="gala-comment-actions">
|
|
94
|
-
${canReply && html`<button type="button" data-reply-comment=${comment.commentId}
|
|
95
|
-
onClick=${() => { setReplying((open) => !open); setDraft(''); }}>Reply</button>`}
|
|
96
|
-
${mine && html`<button type="button" data-edit-comment=${comment.commentId}
|
|
97
|
-
onClick=${() => { setEditing(true); setDraft(comment.body ?? ''); }}>Edit</button>`}
|
|
98
|
-
${mine && html`<button type="button" data-delete-comment=${comment.commentId}
|
|
99
|
-
onClick=${() => onDelete(comment.commentId)} disabled=${busy}>Delete</button>`}
|
|
100
|
-
</div>`}
|
|
101
|
-
|
|
102
|
-
${replying && html`
|
|
103
|
-
<form class="gala-comment__form" onSubmit=${submitReply}>
|
|
104
|
-
<label class="gala-visually-hidden" for=${`reply-${comment.commentId}`}>
|
|
105
|
-
Reply to ${comment.author?.displayName ?? 'this comment'}
|
|
106
|
-
</label>
|
|
107
|
-
<textarea id=${`reply-${comment.commentId}`} rows="3" value=${draft} autofocus
|
|
108
|
-
placeholder="Write a reply" onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
109
|
-
<div class="gala-comment__controls">
|
|
110
|
-
<button type="submit" disabled=${busy || !draft.trim()}>Post reply</button>
|
|
111
|
-
<button type="button" onClick=${() => setReplying(false)}>Cancel</button>
|
|
112
|
-
</div>
|
|
113
|
-
</form>`}
|
|
114
|
-
|
|
115
|
-
${comment.replies.length > 0 && html`
|
|
116
|
-
<ol class="gala-comment-replies">
|
|
117
|
-
${comment.replies.map((reply) => html`
|
|
118
|
-
<${Comment} key=${reply.commentId} comment=${reply} reader=${reader} busy=${busy}
|
|
119
|
-
onReply=${onReply} onEdit=${onEdit} onDelete=${onDelete} />`)}
|
|
120
|
-
</ol>`}
|
|
121
|
-
</li>`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function Comments({ endpoint }) {
|
|
125
|
-
// Reading `.value` during render subscribes this component to the signal, so a sign-in
|
|
126
|
-
// anywhere on the page re-renders the conversation without anything having to tell it.
|
|
127
|
-
const reader = sessionUser.value;
|
|
128
|
-
const [items, setItems] = useState([]);
|
|
129
|
-
const [cursor, setCursor] = useState(null);
|
|
130
|
-
const [total, setTotal] = useState(0);
|
|
131
|
-
const [status, setStatus] = useState('');
|
|
132
|
-
const [busy, setBusy] = useState(false);
|
|
133
|
-
const [draft, setDraft] = useState('');
|
|
134
|
-
const [loading, setLoading] = useState(true);
|
|
24
|
+
function commentsController(root, endpoint) {
|
|
25
|
+
const state = { items: [], cursor: null, total: 0, busy: false, status: '', phase: 'loading' };
|
|
26
|
+
const articleId = /\/v1\/articles\/([^/]+)\/engagement/.exec(endpoint)?.[1] ?? '';
|
|
27
|
+
const statusNode = root.closest('.gala-conversation')?.querySelector('[data-engagement-status]');
|
|
135
28
|
|
|
136
|
-
|
|
137
|
-
cold visit and wrong straight after a write: the browser would answer from its own cache with
|
|
138
|
-
the state from before the reader wrote anything. */
|
|
139
|
-
const load = useCallback(async (nextCursor = '', { append = false, fresh = false } = {}) => {
|
|
29
|
+
async function load(nextCursor = '', { append = false, fresh = false } = {}) {
|
|
140
30
|
const url = new URL(endpoint);
|
|
141
31
|
if (nextCursor) url.searchParams.set('commentsCursor', nextCursor);
|
|
142
32
|
const response = await fetch(url, {
|
|
143
|
-
headers: { Accept: 'application/json' },
|
|
144
|
-
credentials: 'omit',
|
|
145
|
-
cache: fresh ? 'no-store' : 'default',
|
|
33
|
+
headers: { Accept: 'application/json' }, credentials: 'omit', cache: fresh ? 'no-store' : 'default',
|
|
146
34
|
});
|
|
147
35
|
if (!response.ok) throw new Error(`Comments returned HTTP ${response.status}`);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return page;
|
|
155
|
-
}, [endpoint]);
|
|
156
|
-
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
let live = true;
|
|
159
|
-
load().catch(() => { if (live) setStatus('Comments are temporarily unavailable.'); })
|
|
160
|
-
.finally(() => { if (live) setLoading(false); });
|
|
161
|
-
return () => { live = false; };
|
|
162
|
-
}, [load]);
|
|
163
|
-
|
|
164
|
-
// Signing in changes what the reader may do, not what the article says, so only re-read when
|
|
165
|
-
// the reader actually changes.
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
if (!reader) return undefined;
|
|
168
|
-
load('', { fresh: true }).catch(() => {});
|
|
169
|
-
return undefined;
|
|
170
|
-
}, [reader?.id]);
|
|
36
|
+
const page = (await response.json())?.data?.comments;
|
|
37
|
+
if (!page || !Array.isArray(page.items)) throw new TypeError('Comment page is invalid');
|
|
38
|
+
state.items = append ? [...state.items, ...page.items] : page.items;
|
|
39
|
+
state.cursor = page.nextCursor ?? null;
|
|
40
|
+
if (Number.isSafeInteger(page.totalCount)) state.total = page.totalCount;
|
|
41
|
+
}
|
|
171
42
|
|
|
172
|
-
|
|
173
|
-
if (!
|
|
174
|
-
|
|
175
|
-
setStatus('');
|
|
176
|
-
if (optimistic) setItems((current) => [optimistic, ...current]);
|
|
43
|
+
async function write(operation, payload) {
|
|
44
|
+
if (!sessionUser.value) { requestSignIn({ kind: 'comment' }); return false; }
|
|
45
|
+
state.busy = true; state.status = ''; render();
|
|
177
46
|
try {
|
|
178
47
|
await sendEngagementWrite(operation, payload);
|
|
179
|
-
/* Re-read past the cache so what is on screen is what the server actually holds — the
|
|
180
|
-
optimistic row above is a promise to the reader, not a source of truth. */
|
|
181
48
|
await load('', { fresh: true });
|
|
182
49
|
return true;
|
|
183
50
|
} catch (error) {
|
|
184
|
-
|
|
185
|
-
setItems((current) => current.filter((item) => item.commentId !== optimistic.commentId));
|
|
186
|
-
}
|
|
187
|
-
setStatus(engagementErrorMessage(error.message));
|
|
51
|
+
state.status = engagementErrorMessage(error.message);
|
|
188
52
|
return false;
|
|
189
|
-
} finally {
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
}, [reader?.id, load]);
|
|
193
|
-
|
|
194
|
-
const articleId = useMemo(() => {
|
|
195
|
-
const match = /\/v1\/articles\/([^/]+)\/engagement/.exec(endpoint);
|
|
196
|
-
return match ? match[1] : '';
|
|
197
|
-
}, [endpoint]);
|
|
53
|
+
} finally { state.busy = false; render(); }
|
|
54
|
+
}
|
|
198
55
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
author: { userId: reader?.id, displayName: reader?.displayName },
|
|
210
|
-
pending: true,
|
|
56
|
+
function form(label, initial, submit) {
|
|
57
|
+
const formNode = element('form', 'gala-comment__form');
|
|
58
|
+
const field = element('textarea');
|
|
59
|
+
field.rows = 3; field.value = initial; field.placeholder = label; field.setAttribute('aria-label', label);
|
|
60
|
+
const send = element('button', '', 'Post'); send.type = 'submit'; send.disabled = state.busy;
|
|
61
|
+
formNode.append(field, send);
|
|
62
|
+
formNode.addEventListener('submit', async (event) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
const body = field.value.trim();
|
|
65
|
+
if (body && await submit(body)) field.value = '';
|
|
211
66
|
});
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const reply = (parentCommentId, body) =>
|
|
216
|
-
write('comment.create', { articleId, parentCommentId, body });
|
|
217
|
-
const edit = (commentId, body) => write('comment.edit', { articleId, commentId, body });
|
|
218
|
-
const remove = (commentId) => write('comment.delete', { articleId, commentId });
|
|
219
|
-
|
|
220
|
-
const threads = useMemo(() => toThreads(items), [items]);
|
|
221
|
-
|
|
222
|
-
return html`
|
|
223
|
-
<section class="gala-comments-island" aria-label="Comments">
|
|
224
|
-
<h2 class="gala-comments__heading">${total === 1 ? '1 comment' : `${total} comments`}</h2>
|
|
225
|
-
|
|
226
|
-
${reader
|
|
227
|
-
? html`<form class="gala-comment__form" onSubmit=${post}>
|
|
228
|
-
<label class="gala-visually-hidden" for="gala-new-comment">Add a comment</label>
|
|
229
|
-
<textarea id="gala-new-comment" rows="3" value=${draft} placeholder="Add a comment"
|
|
230
|
-
onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
231
|
-
<button type="submit" disabled=${busy || !draft.trim()}>Post comment</button>
|
|
232
|
-
</form>`
|
|
233
|
-
: html`<p class="gala-comments__prompt">
|
|
234
|
-
<button type="button" onClick=${() => requestSignIn({ kind: 'comment' })}>
|
|
235
|
-
Sign in to join the conversation
|
|
236
|
-
</button>
|
|
237
|
-
</p>`}
|
|
67
|
+
return formNode;
|
|
68
|
+
}
|
|
238
69
|
|
|
239
|
-
|
|
70
|
+
function commentNode(comment) {
|
|
71
|
+
const item = element('li', 'gala-comment');
|
|
72
|
+
item.dataset.commentId = comment.commentId; item.dataset.depth = comment.depth;
|
|
73
|
+
const meta = element('p', 'gala-comment__meta');
|
|
74
|
+
meta.append(element('strong', '', comment.author?.displayName ?? '[deleted]'));
|
|
75
|
+
if (comment.createdAt) {
|
|
76
|
+
const time = element('time', '', new Date(comment.createdAt).toLocaleDateString());
|
|
77
|
+
time.dateTime = comment.createdAt; meta.append(time);
|
|
78
|
+
}
|
|
79
|
+
item.append(meta, element('p', 'gala-comment__body', comment.deleted ? '[deleted]' : comment.body));
|
|
80
|
+
if (!comment.deleted) {
|
|
81
|
+
const actions = element('div', 'gala-comment-actions');
|
|
82
|
+
if (comment.depth < MAXIMUM_DEPTH) {
|
|
83
|
+
const reply = element('button', '', 'Reply'); reply.type = 'button'; reply.dataset.replyComment = comment.commentId;
|
|
84
|
+
reply.addEventListener('click', () => {
|
|
85
|
+
const existing = item.querySelector(':scope > .gala-comment__form');
|
|
86
|
+
if (existing) { existing.remove(); return; }
|
|
87
|
+
actions.after(form(`Reply to ${comment.author?.displayName ?? 'this comment'}`, '',
|
|
88
|
+
(body) => write('comment.create', { articleId, parentCommentId: comment.commentId, body })));
|
|
89
|
+
}); actions.append(reply);
|
|
90
|
+
}
|
|
91
|
+
const mine = sessionUser.value && comment.author?.userId === sessionUser.value.id;
|
|
92
|
+
if (mine) {
|
|
93
|
+
const edit = element('button', '', 'Edit'); edit.type = 'button'; edit.dataset.editComment = comment.commentId;
|
|
94
|
+
edit.addEventListener('click', () => actions.after(form('Edit your comment', comment.body ?? '',
|
|
95
|
+
(body) => write('comment.edit', { articleId, commentId: comment.commentId, body }))));
|
|
96
|
+
const remove = element('button', '', 'Delete'); remove.type = 'button'; remove.dataset.deleteComment = comment.commentId;
|
|
97
|
+
remove.addEventListener('click', () => write('comment.delete', { articleId, commentId: comment.commentId }));
|
|
98
|
+
actions.append(edit, remove);
|
|
99
|
+
} else {
|
|
100
|
+
const report = element('button', '', 'Report'); report.type = 'button'; report.dataset.reportComment = comment.commentId;
|
|
101
|
+
report.addEventListener('click', () => write('comment.report', { articleId, commentId: comment.commentId, reason: 'OTHER' }));
|
|
102
|
+
actions.append(report);
|
|
103
|
+
}
|
|
104
|
+
item.append(actions);
|
|
105
|
+
}
|
|
106
|
+
if (comment.replies.length) {
|
|
107
|
+
const replies = element('ol', 'gala-comment-replies');
|
|
108
|
+
replies.append(...comment.replies.map(commentNode)); item.append(replies);
|
|
109
|
+
}
|
|
110
|
+
return item;
|
|
111
|
+
}
|
|
240
112
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
113
|
+
function render() {
|
|
114
|
+
if (statusNode) {
|
|
115
|
+
statusNode.textContent = state.phase === 'loading'
|
|
116
|
+
? 'Loading comments…'
|
|
117
|
+
: state.phase === 'unavailable' ? 'Comments are temporarily unavailable.' : state.status;
|
|
118
|
+
}
|
|
119
|
+
if (state.phase !== 'ready') {
|
|
120
|
+
root.replaceChildren();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const section = element('section', 'gala-comments-island'); section.setAttribute('aria-label', 'Comments');
|
|
124
|
+
section.append(element('p', 'gala-comments__heading', state.total === 1 ? '1 comment' : `${state.total} comments`));
|
|
125
|
+
if (sessionUser.value) section.append(form('Add a comment', '', (body) => write('comment.create', { articleId, body })));
|
|
126
|
+
else {
|
|
127
|
+
const prompt = element('p', 'gala-comments__prompt');
|
|
128
|
+
const signIn = element('button', '', 'Sign in to join the conversation'); signIn.type = 'button';
|
|
129
|
+
signIn.addEventListener('click', () => requestSignIn({ kind: 'comment' })); prompt.append(signIn); section.append(prompt);
|
|
130
|
+
}
|
|
131
|
+
if (!state.items.length) section.append(element('p', 'gala-comments__empty', 'No comments yet.'));
|
|
132
|
+
else { const list = element('ol', 'gala-comments'); list.append(...threads(state.items).map(commentNode)); section.append(list); }
|
|
133
|
+
if (state.cursor) {
|
|
134
|
+
const more = element('button', 'gala-comments__more', 'Show more comments'); more.type = 'button'; more.disabled = state.busy;
|
|
135
|
+
more.addEventListener('click', async () => {
|
|
136
|
+
state.busy = true; state.status = 'Loading more comments…'; render();
|
|
137
|
+
try { await load(state.cursor, { append: true }); state.status = ''; }
|
|
138
|
+
catch { state.status = 'More comments couldn’t be loaded. Try again.'; }
|
|
139
|
+
state.busy = false; render();
|
|
140
|
+
}); section.append(more);
|
|
141
|
+
}
|
|
142
|
+
root.replaceChildren(section);
|
|
143
|
+
}
|
|
250
144
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
Show more comments
|
|
257
|
-
</button>`}
|
|
258
|
-
</section>`;
|
|
145
|
+
window.addEventListener('gala-session-change', render);
|
|
146
|
+
render();
|
|
147
|
+
load().then(() => { state.phase = 'ready'; })
|
|
148
|
+
.catch(() => { state.phase = 'unavailable'; })
|
|
149
|
+
.finally(render);
|
|
259
150
|
}
|
|
260
151
|
|
|
261
|
-
|
|
262
|
-
const endpoint =
|
|
263
|
-
if (endpoint)
|
|
264
|
-
}
|
|
152
|
+
document.querySelectorAll('[data-gala-comments]').forEach((root) => {
|
|
153
|
+
const endpoint = root.closest('[data-engagement-url]')?.dataset.engagementUrl;
|
|
154
|
+
if (endpoint) commentsController(root, endpoint);
|
|
155
|
+
});
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
* `sessionUser` is a signal rather than a variable because more than one island depends on it and
|
|
10
10
|
* they must not each keep their own copy going stale in its own way.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export const sessionUser = signal(null);
|
|
12
|
+
export const sessionUser = { value: null };
|
|
15
13
|
|
|
16
14
|
const sessionFrame = document.querySelector('[data-gala-session-frame]');
|
|
17
15
|
const frameOrigin = sessionFrame ? new URL(sessionFrame.src).origin : null;
|
|
@@ -39,7 +37,14 @@ export function sendEngagementWrite(operation, payload) {
|
|
|
39
37
|
}
|
|
40
38
|
const requestId = crypto.randomUUID();
|
|
41
39
|
return new Promise((resolve, reject) => {
|
|
42
|
-
|
|
40
|
+
const timeout = setTimeout(() => {
|
|
41
|
+
pending.delete(requestId);
|
|
42
|
+
reject(new Error('REQUEST_TIMEOUT'));
|
|
43
|
+
}, 10_000);
|
|
44
|
+
pending.set(requestId, {
|
|
45
|
+
resolve: (value) => { clearTimeout(timeout); resolve(value); },
|
|
46
|
+
reject: (error) => { clearTimeout(timeout); reject(error); }
|
|
47
|
+
});
|
|
43
48
|
sessionFrame.contentWindow.postMessage(
|
|
44
49
|
{ type: 'gala-engagement-write', requestId, operation, payload }, frameOrigin);
|
|
45
50
|
});
|
|
@@ -69,5 +74,6 @@ if (sessionFrame) {
|
|
|
69
74
|
if (event.data?.type !== 'gala-session') return;
|
|
70
75
|
const user = event.data.user;
|
|
71
76
|
sessionUser.value = user && typeof user.id === 'string' ? user : null;
|
|
77
|
+
window.dispatchEvent(new CustomEvent('gala-session-change'));
|
|
72
78
|
});
|
|
73
79
|
}
|