@hhkaos/webmentions-widget 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 +21 -0
- package/package.json +1 -1
- package/src/react.js +54 -4
- package/src/render.js +27 -0
package/README.md
CHANGED
|
@@ -134,6 +134,27 @@ Two things this buys beyond politeness: the section survives a webmention.io
|
|
|
134
134
|
outage, and the committed JSON is a durable copy of your mentions if the
|
|
135
135
|
service ever disappears.
|
|
136
136
|
|
|
137
|
+
### Saying how fresh it is
|
|
138
|
+
|
|
139
|
+
A snapshot is by definition a little behind. Pass an `updated` label and the
|
|
140
|
+
widget dates what it is showing:
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
<Webmentions
|
|
144
|
+
targets={targets}
|
|
145
|
+
snapshot={snapshot}
|
|
146
|
+
locale="es"
|
|
147
|
+
labels={{updated: {en: 'Updated', es: 'Actualizado'}}}
|
|
148
|
+
/>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
It renders only when the mentions came from a snapshot and there is at least one
|
|
152
|
+
to qualify — on a live fetch the data is current and dating it would mislead.
|
|
153
|
+
`renderUpdated={(iso, formatted) => …}` takes over the wording entirely.
|
|
154
|
+
|
|
155
|
+
The widget owns this because it is the only layer that knows which source the
|
|
156
|
+
mentions came from; the host owns the copy.
|
|
157
|
+
|
|
137
158
|
## API
|
|
138
159
|
|
|
139
160
|
### `fetchWebmentions(options)`
|
package/package.json
CHANGED
package/src/react.js
CHANGED
|
@@ -107,14 +107,43 @@ export function useWebmentions(targets, options = {}) {
|
|
|
107
107
|
// A live result wins once it lands; until then the snapshot renders. A failed
|
|
108
108
|
// revalidation never blanks a section the snapshot could still fill.
|
|
109
109
|
if (fetched.groups) {
|
|
110
|
-
return fetched;
|
|
110
|
+
return {...fetched, source: 'network', generatedAt: null};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (seededGroups) {
|
|
114
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
status: 'success',
|
|
116
|
+
groups: seededGroups,
|
|
117
|
+
error: fetched.error,
|
|
118
|
+
source: 'snapshot',
|
|
119
|
+
// Only meaningful for a snapshot: how stale the data on screen may be.
|
|
120
|
+
generatedAt: initialMentions ? null : snapshot?.generatedAt ?? null,
|
|
121
|
+
};
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
status: fetched.status,
|
|
126
|
+
groups: EMPTY_GROUPS,
|
|
127
|
+
error: fetched.error,
|
|
128
|
+
source: null,
|
|
129
|
+
generatedAt: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A label is a plain string, or a `{en, es}` map for sites that ship both
|
|
135
|
+
* languages and toggle them with CSS.
|
|
136
|
+
*/
|
|
137
|
+
function renderLabel(label, keyPrefix) {
|
|
138
|
+
if (label == null || typeof label === 'string') {
|
|
139
|
+
return label ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Object.entries(label).map(([lang, text]) => h(
|
|
143
|
+
'span',
|
|
144
|
+
{key: `${keyPrefix}-${lang}`, className: `i18n-${lang}`, lang},
|
|
145
|
+
text,
|
|
146
|
+
));
|
|
118
147
|
}
|
|
119
148
|
|
|
120
149
|
function Face({mention, classNames}) {
|
|
@@ -213,6 +242,7 @@ const REACT_CLASS_NAMES = {
|
|
|
213
242
|
threadAuthor: 'p-author h-card u-url',
|
|
214
243
|
threadContent: 'e-content webmention-content',
|
|
215
244
|
threadSource: 'u-url webmention-source',
|
|
245
|
+
updated: 'webmentions-updated',
|
|
216
246
|
};
|
|
217
247
|
|
|
218
248
|
/**
|
|
@@ -231,10 +261,11 @@ export function Webmentions({
|
|
|
231
261
|
innerClassName = null,
|
|
232
262
|
renderEmpty = null,
|
|
233
263
|
renderError = null,
|
|
264
|
+
renderUpdated = null,
|
|
234
265
|
...options
|
|
235
266
|
}) {
|
|
236
267
|
const classNames = {...REACT_CLASS_NAMES, ...classNameOverrides};
|
|
237
|
-
const {status, groups, error} = useWebmentions(targets, options);
|
|
268
|
+
const {status, groups, error, source, generatedAt} = useWebmentions(targets, options);
|
|
238
269
|
|
|
239
270
|
if (status === 'error') {
|
|
240
271
|
return renderError ? renderError(error) : null;
|
|
@@ -275,6 +306,25 @@ export function Webmentions({
|
|
|
275
306
|
})),
|
|
276
307
|
)
|
|
277
308
|
: null,
|
|
309
|
+
// Only when rendering from a snapshot: on a live fetch the mentions are
|
|
310
|
+
// current and dating them would be misleading.
|
|
311
|
+
source === 'snapshot' && generatedAt && (labels.updated || renderUpdated)
|
|
312
|
+
? h(
|
|
313
|
+
'p',
|
|
314
|
+
{key: 'updated', className: classNames.updated},
|
|
315
|
+
renderUpdated
|
|
316
|
+
? renderUpdated(generatedAt, formatMentionDate(generatedAt, locale))
|
|
317
|
+
: [
|
|
318
|
+
renderLabel(labels.updated, 'updated'),
|
|
319
|
+
' ',
|
|
320
|
+
h(
|
|
321
|
+
'time',
|
|
322
|
+
{key: 'updated-time', dateTime: generatedAt},
|
|
323
|
+
formatMentionDate(generatedAt, locale),
|
|
324
|
+
),
|
|
325
|
+
],
|
|
326
|
+
)
|
|
327
|
+
: null,
|
|
278
328
|
];
|
|
279
329
|
|
|
280
330
|
return h(
|
package/src/render.js
CHANGED
|
@@ -33,6 +33,7 @@ export const DEFAULT_CLASS_NAMES = {
|
|
|
33
33
|
threadContent: 'webmentions__content p-content',
|
|
34
34
|
threadSource: 'webmentions__source u-url',
|
|
35
35
|
threadPhoto: '',
|
|
36
|
+
updated: 'webmentions__updated',
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
const FACEPILE_META = {
|
|
@@ -253,6 +254,10 @@ export function renderGroups(groups, {
|
|
|
253
254
|
classNames: classNameOverrides = {},
|
|
254
255
|
facepileMode = 'flat',
|
|
255
256
|
maxLength,
|
|
257
|
+
// Timestamp of the snapshot these groups came from, if any. Rendering it
|
|
258
|
+
// tells a reader how stale the list may be; on a live fetch, leave it unset.
|
|
259
|
+
updatedAt = null,
|
|
260
|
+
updated,
|
|
256
261
|
document: doc = globalThis.document,
|
|
257
262
|
} = {}) {
|
|
258
263
|
const classNames = {...DEFAULT_CLASS_NAMES, ...classNameOverrides};
|
|
@@ -287,6 +292,28 @@ export function renderGroups(groups, {
|
|
|
287
292
|
})));
|
|
288
293
|
}
|
|
289
294
|
|
|
295
|
+
const updatedElement = resolveElement(updated, containerElement, doc)
|
|
296
|
+
|| containerElement?.querySelector(`.${classNames.updated.split(' ')[0]}`);
|
|
297
|
+
|
|
298
|
+
if (updatedElement) {
|
|
299
|
+
const show = Boolean(updatedAt) && labels.updated != null
|
|
300
|
+
&& (groups.interactions.length > 0 || groups.threads.length > 0);
|
|
301
|
+
|
|
302
|
+
updatedElement.replaceChildren();
|
|
303
|
+
|
|
304
|
+
if (show) {
|
|
305
|
+
appendLabel(updatedElement, labels.updated, doc);
|
|
306
|
+
updatedElement.appendChild(doc.createTextNode(' '));
|
|
307
|
+
|
|
308
|
+
const time = doc.createElement('time');
|
|
309
|
+
time.dateTime = updatedAt;
|
|
310
|
+
time.textContent = formatMentionDate(updatedAt, locale);
|
|
311
|
+
updatedElement.appendChild(time);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
updatedElement.hidden = !show;
|
|
315
|
+
}
|
|
316
|
+
|
|
290
317
|
if (containerElement) {
|
|
291
318
|
const empty = !groups.interactions.length && !groups.threads.length;
|
|
292
319
|
containerElement.hidden = empty;
|