atproto-embed 2.0.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/LICENSE +201 -0
- package/README.md +199 -0
- package/dist/embed.css +1371 -0
- package/dist/embed.js +197 -0
- package/dist/members.css +248 -0
- package/dist/members.js +596 -0
- package/dist/post.css +1371 -0
- package/dist/post.js +1980 -0
- package/dist/profile.css +318 -0
- package/dist/profile.js +542 -0
- package/package.json +15 -0
package/dist/post.js
ADDED
|
@@ -0,0 +1,1980 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var API_BASE = "https://public.api.bsky.app/xrpc/";
|
|
5
|
+
var HANDLE_CACHE = new Map();
|
|
6
|
+
var PROFILE_CACHE = new Map();
|
|
7
|
+
var INFLIGHT = new Map();
|
|
8
|
+
var CONTAINER_ABORTS = new WeakMap();
|
|
9
|
+
var SIGNAL_IDS = new WeakMap();
|
|
10
|
+
var NEXT_SIGNAL_ID = 1;
|
|
11
|
+
var TEXT_ENCODER = new TextEncoder();
|
|
12
|
+
var TEXT_DECODER = new TextDecoder();
|
|
13
|
+
var DATE_FMT = new Intl.DateTimeFormat("en-US", {
|
|
14
|
+
year: "numeric",
|
|
15
|
+
month: "short",
|
|
16
|
+
day: "numeric",
|
|
17
|
+
});
|
|
18
|
+
var TIME_FMT = new Intl.DateTimeFormat("en-US", {
|
|
19
|
+
hour: "numeric",
|
|
20
|
+
minute: "2-digit",
|
|
21
|
+
hour12: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function getSignalId(signal) {
|
|
25
|
+
if (!signal) return 0;
|
|
26
|
+
var id = SIGNAL_IDS.get(signal);
|
|
27
|
+
if (!id) {
|
|
28
|
+
id = NEXT_SIGNAL_ID++;
|
|
29
|
+
SIGNAL_IDS.set(signal, id);
|
|
30
|
+
}
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fetchJsonOrThrow(url, errMsg, signal) {
|
|
35
|
+
return fetch(url, signal ? { signal: signal } : undefined).then(function (res) {
|
|
36
|
+
if (!res.ok) throw new Error(errMsg);
|
|
37
|
+
return res.json();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fetchJsonDedup(key, url, errMsg, signal) {
|
|
42
|
+
var sigId = getSignalId(signal);
|
|
43
|
+
var inflightKey = key + "|s" + sigId;
|
|
44
|
+
if (INFLIGHT.has(inflightKey)) return INFLIGHT.get(inflightKey);
|
|
45
|
+
var p = fetchJsonOrThrow(url, errMsg, signal)
|
|
46
|
+
.finally(function () {
|
|
47
|
+
INFLIGHT.delete(inflightKey);
|
|
48
|
+
});
|
|
49
|
+
INFLIGHT.set(inflightKey, p);
|
|
50
|
+
return p;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ───── URI resolution ───── */
|
|
54
|
+
|
|
55
|
+
function parseInput(raw) {
|
|
56
|
+
raw = raw.trim();
|
|
57
|
+
if (raw.startsWith("at://")) return { atUri: raw, sourceDomain: null };
|
|
58
|
+
const m = raw.match(
|
|
59
|
+
/https?:\/\/([^/]+)\/profile\/([^/]+)\/post\/([a-zA-Z0-9]+)/
|
|
60
|
+
);
|
|
61
|
+
if (m)
|
|
62
|
+
return {
|
|
63
|
+
handle: m[2],
|
|
64
|
+
rkey: m[3],
|
|
65
|
+
sourceDomain: m[1],
|
|
66
|
+
sourceUrl: raw,
|
|
67
|
+
};
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function resolveHandle(handle) {
|
|
72
|
+
if (handle.startsWith("did:")) return handle;
|
|
73
|
+
if (HANDLE_CACHE.has(handle)) return HANDLE_CACHE.get(handle);
|
|
74
|
+
const url =
|
|
75
|
+
API_BASE +
|
|
76
|
+
"com.atproto.identity.resolveHandle?handle=" +
|
|
77
|
+
encodeURIComponent(handle);
|
|
78
|
+
var p = fetchJsonDedup(
|
|
79
|
+
"resolveHandle:" + handle,
|
|
80
|
+
url,
|
|
81
|
+
"Handle resolution failed",
|
|
82
|
+
null
|
|
83
|
+
)
|
|
84
|
+
.then(function (data) {
|
|
85
|
+
return data.did;
|
|
86
|
+
})
|
|
87
|
+
.catch(function (err) {
|
|
88
|
+
HANDLE_CACHE.delete(handle);
|
|
89
|
+
throw err;
|
|
90
|
+
});
|
|
91
|
+
HANDLE_CACHE.set(handle, p);
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function resolveUri(raw) {
|
|
96
|
+
const parsed = parseInput(raw);
|
|
97
|
+
if (!parsed) throw new Error("Invalid URI: " + raw);
|
|
98
|
+
if (parsed.atUri)
|
|
99
|
+
return { atUri: parsed.atUri, sourceDomain: null, sourceUrl: null };
|
|
100
|
+
const did = await resolveHandle(parsed.handle);
|
|
101
|
+
return {
|
|
102
|
+
atUri: "at://" + did + "/app.bsky.feed.post/" + parsed.rkey,
|
|
103
|
+
sourceDomain: parsed.sourceDomain,
|
|
104
|
+
sourceUrl: parsed.sourceUrl,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ───── API ───── */
|
|
109
|
+
|
|
110
|
+
async function fetchPost(atUri, signal) {
|
|
111
|
+
const url =
|
|
112
|
+
API_BASE +
|
|
113
|
+
"app.bsky.feed.getPostThread?uri=" +
|
|
114
|
+
encodeURIComponent(atUri) +
|
|
115
|
+
"&depth=0&parentHeight=10";
|
|
116
|
+
const data = await fetchJsonDedup(url, url, "Failed to fetch post", signal);
|
|
117
|
+
return data.thread;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function fetchThread(atUri, signal) {
|
|
121
|
+
const url =
|
|
122
|
+
API_BASE +
|
|
123
|
+
"app.bsky.feed.getPostThread?uri=" +
|
|
124
|
+
encodeURIComponent(atUri) +
|
|
125
|
+
"&depth=6&parentHeight=10";
|
|
126
|
+
const data = await fetchJsonDedup(url, url, "Failed to fetch thread", signal);
|
|
127
|
+
return data.thread;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function fetchQuotes(atUri, cursor, signal) {
|
|
131
|
+
let url =
|
|
132
|
+
API_BASE +
|
|
133
|
+
"app.bsky.feed.getQuotes?uri=" +
|
|
134
|
+
encodeURIComponent(atUri) +
|
|
135
|
+
"&limit=25";
|
|
136
|
+
if (cursor) url += "&cursor=" + encodeURIComponent(cursor);
|
|
137
|
+
return fetchJsonDedup(url, url, "Failed to fetch quotes", signal);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fetchLikes(atUri, signal) {
|
|
141
|
+
const url =
|
|
142
|
+
API_BASE +
|
|
143
|
+
"app.bsky.feed.getLikes?uri=" +
|
|
144
|
+
encodeURIComponent(atUri) +
|
|
145
|
+
"&limit=60";
|
|
146
|
+
return fetchJsonDedup(url, url, "Failed to fetch likes", signal);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function fetchProfile(did) {
|
|
150
|
+
try {
|
|
151
|
+
if (PROFILE_CACHE.has(did)) return PROFILE_CACHE.get(did);
|
|
152
|
+
const url =
|
|
153
|
+
API_BASE +
|
|
154
|
+
"app.bsky.actor.getProfile?actor=" +
|
|
155
|
+
encodeURIComponent(did);
|
|
156
|
+
var p = fetchJsonDedup(url, url, "Failed to fetch profile", null)
|
|
157
|
+
.then(function (data) {
|
|
158
|
+
return data;
|
|
159
|
+
})
|
|
160
|
+
.catch(function (_) {
|
|
161
|
+
PROFILE_CACHE.delete(did);
|
|
162
|
+
return null;
|
|
163
|
+
});
|
|
164
|
+
PROFILE_CACHE.set(did, p);
|
|
165
|
+
var resData = await p;
|
|
166
|
+
if (!resData) PROFILE_CACHE.delete(did);
|
|
167
|
+
return resData;
|
|
168
|
+
} catch (_) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractDid(atUri) {
|
|
174
|
+
const m = atUri.match(/^at:\/\/(did:[^/]+)/);
|
|
175
|
+
return m ? m[1] : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ───── Helpers ───── */
|
|
179
|
+
|
|
180
|
+
function el(tag, className, attrs) {
|
|
181
|
+
const e = document.createElement(tag);
|
|
182
|
+
if (className) e.className = className;
|
|
183
|
+
if (attrs) {
|
|
184
|
+
for (const k in attrs) {
|
|
185
|
+
if (k === "textContent") e.textContent = attrs[k];
|
|
186
|
+
else if (k === "innerHTML") e.innerHTML = attrs[k];
|
|
187
|
+
else e.setAttribute(k, attrs[k]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return e;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function appendInChunks(items, chunkSize, renderFn, container, beforeNode) {
|
|
194
|
+
if (!items || items.length === 0) return;
|
|
195
|
+
var i = 0;
|
|
196
|
+
var raf =
|
|
197
|
+
typeof requestAnimationFrame === "function"
|
|
198
|
+
? requestAnimationFrame
|
|
199
|
+
: function (cb) {
|
|
200
|
+
setTimeout(cb, 16);
|
|
201
|
+
};
|
|
202
|
+
function step() {
|
|
203
|
+
var frag = document.createDocumentFragment();
|
|
204
|
+
var end = Math.min(i + chunkSize, items.length);
|
|
205
|
+
for (; i < end; i++) {
|
|
206
|
+
var node = renderFn(items[i], i);
|
|
207
|
+
if (node) frag.appendChild(node);
|
|
208
|
+
}
|
|
209
|
+
if (beforeNode) container.insertBefore(frag, beforeNode);
|
|
210
|
+
else container.appendChild(frag);
|
|
211
|
+
if (i < items.length) {
|
|
212
|
+
raf(step);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
step();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function formatTimestamp(iso) {
|
|
219
|
+
var d = new Date(iso);
|
|
220
|
+
return DATE_FMT.format(d) + " at " + TIME_FMT.format(d);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatRelativeTime(iso) {
|
|
224
|
+
var now = Date.now();
|
|
225
|
+
var then = new Date(iso).getTime();
|
|
226
|
+
if (!isFinite(then)) return "";
|
|
227
|
+
var diff = Math.max(0, now - then);
|
|
228
|
+
var sec = Math.floor(diff / 1000);
|
|
229
|
+
if (sec < 60) return sec + "s";
|
|
230
|
+
var min = Math.floor(sec / 60);
|
|
231
|
+
if (min < 60) return min + "m";
|
|
232
|
+
var hr = Math.floor(min / 60);
|
|
233
|
+
if (hr < 24) return hr + "h";
|
|
234
|
+
var day = Math.floor(hr / 24);
|
|
235
|
+
if (day < 7) return day + "d";
|
|
236
|
+
var week = Math.floor(day / 7);
|
|
237
|
+
if (day < 30) return week + "w";
|
|
238
|
+
var month = Math.floor(day / 30);
|
|
239
|
+
if (day < 365) return month + "mo";
|
|
240
|
+
var year = Math.floor(day / 365);
|
|
241
|
+
return year + "y";
|
|
242
|
+
}
|
|
243
|
+
function formatCount(n) {
|
|
244
|
+
if (n >= 1000000)
|
|
245
|
+
return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
246
|
+
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
247
|
+
return String(n);
|
|
248
|
+
}
|
|
249
|
+
function formatCountLabel(n, singular, plural) {
|
|
250
|
+
if (n === 1) return "1 " + singular;
|
|
251
|
+
return String(n) + " " + (plural || singular + "s");
|
|
252
|
+
}
|
|
253
|
+
function formatReplyLabel(n) {
|
|
254
|
+
if (n === 1) return "1 reply";
|
|
255
|
+
return String(n) + " replies";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getRkeyFromUri(uri) {
|
|
259
|
+
var idx = uri.lastIndexOf("/");
|
|
260
|
+
return idx === -1 ? uri : uri.slice(idx + 1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function bskyPostUrl(post) {
|
|
264
|
+
var rkey = getRkeyFromUri(post.uri);
|
|
265
|
+
return "https://bsky.app/profile/" + post.author.handle + "/post/" + rkey;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function sortPosts(posts, criteria) {
|
|
269
|
+
var arr = posts.slice();
|
|
270
|
+
arr.sort(function (a, b) {
|
|
271
|
+
var postA = a.post || a;
|
|
272
|
+
var postB = b.post || b;
|
|
273
|
+
var recA = postA.record || postA.value || {};
|
|
274
|
+
var recB = postB.record || postB.value || {};
|
|
275
|
+
|
|
276
|
+
var valA, valB;
|
|
277
|
+
switch (criteria) {
|
|
278
|
+
case "oldest":
|
|
279
|
+
valA = new Date(recA.createdAt || postA.indexedAt).getTime();
|
|
280
|
+
valB = new Date(recB.createdAt || postB.indexedAt).getTime();
|
|
281
|
+
return valA - valB;
|
|
282
|
+
case "likes":
|
|
283
|
+
return (postB.likeCount || 0) - (postA.likeCount || 0);
|
|
284
|
+
case "reposts":
|
|
285
|
+
return (postB.repostCount || 0) - (postA.repostCount || 0);
|
|
286
|
+
case "quotes":
|
|
287
|
+
return (postB.quoteCount || 0) - (postA.quoteCount || 0);
|
|
288
|
+
case "bookmarks":
|
|
289
|
+
return (postB.bookmarkCount || 0) - (postA.bookmarkCount || 0);
|
|
290
|
+
case "replies":
|
|
291
|
+
return (postB.replyCount || 0) - (postA.replyCount || 0);
|
|
292
|
+
case "newest":
|
|
293
|
+
default:
|
|
294
|
+
valA = new Date(recA.createdAt || postA.indexedAt).getTime();
|
|
295
|
+
valB = new Date(recB.createdAt || postB.indexedAt).getTime();
|
|
296
|
+
return valB - valA;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return arr;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getViaInfo(sourceInfo, post) {
|
|
303
|
+
var bskyUrl = bskyPostUrl(post);
|
|
304
|
+
if (!sourceInfo || !sourceInfo.sourceDomain) {
|
|
305
|
+
return { label: "AT Proto", url: bskyUrl };
|
|
306
|
+
}
|
|
307
|
+
var domain = sourceInfo.sourceDomain;
|
|
308
|
+
if (domain === "bsky.app" || domain === "staging.bsky.app") {
|
|
309
|
+
return { label: "Bluesky", url: sourceInfo.sourceUrl || bskyUrl };
|
|
310
|
+
}
|
|
311
|
+
return { label: domain, url: sourceInfo.sourceUrl || bskyUrl };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function getPostLink(sourceInfo, post) {
|
|
315
|
+
var rkey = getRkeyFromUri(post.uri);
|
|
316
|
+
var domain =
|
|
317
|
+
sourceInfo && sourceInfo.sourceDomain ? sourceInfo.sourceDomain : "bsky.app";
|
|
318
|
+
return "https://" + domain + "/profile/" + post.author.handle + "/post/" + rkey;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
var ICONS = {
|
|
322
|
+
"avatar-fallback": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2248%22%20height%3D%2248%22%20fill%3D%22none%22%20viewBox%3D%220%200%2048%2048%22%3E%3Crect%20width%3D%2248%22%20height%3D%2248%22%20fill%3D%22%23e0e0e0%22%20rx%3D%2224%22%2F%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M32%2020a8%208%200%201%201-16%200%208%208%200%200%201%2016%200%22%20opacity%3D%22.2%22%2F%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M36.865%2034.5c-1.904-3.291-4.838-5.651-8.261-6.77a9%209%200%201%200-9.208%200c-3.424%201.117-6.357%203.477-8.261%206.77a.997.997%200%200%200%20.352%201.389%201%201%200%200%200%201.38-.389C15.22%2031.43%2019.383%2029%2024%2029s8.779%202.43%2011.134%206.5a1.001%201.001%200%201%200%201.73-1M17%2020a7%207%200%201%201%207%207%207.007%207.007%200%200%201-7-7%22%2F%3E%3C%2Fsvg%3E",
|
|
323
|
+
"bookmark-fill": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M23%204H9a2%202%200%200%200-2%202v22a1%201%200%200%200%201.53.848l7.47-4.67%207.471%204.67A.999.999%200%200%200%2025%2028V6a2%202%200%200%200-2-2%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%2216%22%20x2%3D%2216%22%20y1%3D%224%22%20y2%3D%2229%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23a855f7%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%238300ff%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
|
|
324
|
+
"caret-down": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22m213.66%20101.66-80%2080a8%208%200%200%201-11.32%200l-80-80a8%208%200%200%201%2011.32-11.32L128%20164.69l74.34-74.35a8%208%200%200%201%2011.32%2011.32%22%2F%3E%3C%2Fsvg%3E",
|
|
325
|
+
"caret-up": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M213.66%20165.66a8%208%200%200%201-11.32%200L128%2091.31l-74.34%2074.35a8%208%200%200%201-11.32-11.32l80-80a8%208%200%200%201%2011.32%200l80%2080a8%208%200%200%201%200%2011.32%22%2F%3E%3C%2Fsvg%3E",
|
|
326
|
+
"check-circle": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M16%203a13%2013%200%201%200%2013%2013A13.013%2013.013%200%200%200%2016%203m5.708%2010.708-7%207a1%201%200%200%201-1.415%200l-3-3a1%201%200%200%201%201.415-1.415L14%2018.586l6.293-6.293a1%201%200%200%201%201.415%201.415%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%223%22%20x2%3D%2229%22%20y1%3D%223%22%20y2%3D%2229%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23006aff%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23004099%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
|
|
327
|
+
"label": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M30.414%2017%2018%204.586A1.98%201.98%200%200%200%2016.586%204H5a1%201%200%200%200-1%201v11.586A1.98%201.98%200%200%200%204.586%2018L17%2030.414a2%202%200%200%200%202.829%200l10.585-10.585a2%202%200%200%200%200-2.829M10.5%2012a1.5%201.5%200%201%201%200-3%201.5%201.5%200%200%201%200%203%22%2F%3E%3C%2Fsvg%3E",
|
|
328
|
+
"like-fill": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M30%2012.75c0%208.75-12.974%2015.833-13.526%2016.125a1%201%200%200%201-.948%200C14.974%2028.582%202%2021.5%202%2012.75A7.76%207.76%200%200%201%209.75%205c2.581%200%204.841%201.11%206.25%202.986C17.409%206.11%2019.669%205%2022.25%205A7.76%207.76%200%200%201%2030%2012.75%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%2216%22%20x2%3D%2216%22%20y1%3D%225%22%20y2%3D%2228.994%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23ec4899%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23ff3a3d%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
|
|
329
|
+
"original-seal": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M28.233%2012.853c-.472-.493-.96-1-1.143-1.447-.17-.409-.18-1.086-.19-1.742-.019-1.22-.039-2.603-1-3.564s-2.344-.981-3.564-1c-.656-.01-1.333-.02-1.742-.19-.445-.184-.954-.671-1.447-1.142C18.286%202.938%2017.306%202%2016%202s-2.284.939-3.148%201.768c-.492.47-1%20.958-1.446%201.142-.406.17-1.086.18-1.742.19-1.22.019-2.603.039-3.564%201s-.975%202.344-1%203.564c-.01.656-.02%201.334-.19%201.742-.184.445-.671.954-1.142%201.446C2.938%2013.715%202%2014.696%202%2016s.939%202.284%201.768%203.148c.47.492.958%201%201.142%201.446.17.409.18%201.086.19%201.742.019%201.22.039%202.603%201%203.564s2.344.981%203.564%201c.656.01%201.334.02%201.742.19.445.184.954.671%201.446%201.143C13.715%2029.06%2014.696%2030%2016%2030s2.284-.939%203.148-1.767c.492-.472%201-.96%201.446-1.143.409-.17%201.086-.18%201.742-.19%201.22-.019%202.603-.039%203.564-1s.981-2.344%201-3.564c.01-.656.02-1.333.19-1.742.184-.445.671-.954%201.143-1.447C29.06%2018.286%2030%2017.306%2030%2016s-.939-2.284-1.767-3.148m-6.526.854-7%207a1%201%200%200%201-1.415%200l-3-3a1%201%200%200%201%201.415-1.415L14%2018.587l6.293-6.293a1%201%200%200%201%201.415%201.415%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%222%22%20x2%3D%2230%22%20y1%3D%222%22%20y2%3D%2230%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23ff6200%22%2F%3E%3Cstop%20offset%3D%22.615%22%20stop-color%3D%22%23f80%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23ff5900%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
|
|
330
|
+
"private": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M26%2010h-4V7a6%206%200%201%200-12%200v3H6a2%202%200%200%200-2%202v14a2%202%200%200%200%202%202h20a2%202%200%200%200%202-2V12a2%202%200%200%200-2-2M16%2020.5a1.5%201.5%200%201%201%200-3%201.5%201.5%200%200%201%200%203M20%2010h-8V7a4%204%200%201%201%208%200z%22%2F%3E%3C%2Fsvg%3E",
|
|
331
|
+
"quote-fill": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M14.5%209v11a6.006%206.006%200%200%201-6%206%201%201%200%200%201%200-2%204%204%200%200%200%204-4v-1H5a2%202%200%200%201-2-2V9a2%202%200%200%201%202-2h7.5a2%202%200%200%201%202%202M27%207h-7.5a2%202%200%200%200-2%202v8a2%202%200%200%200%202%202H27v1a4%204%200%200%201-4%204%201%201%200%200%200%200%202%206.006%206.006%200%200%200%206-6V9a2%202%200%200%200-2-2%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%2216%22%20x2%3D%2216%22%20y1%3D%227%22%20y2%3D%2226%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%233b82f6%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%230061ff%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
|
|
332
|
+
"reply-fill": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M29%206v16a2%202%200%200%201-2%202H10.375L6.3%2027.52l-.011.009a1.99%201.99%200%200%201-2.138.281A1.98%201.98%200%200%201%203%2026V6a2%202%200%200%201%202-2h22a2%202%200%200%201%202%202%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%224%22%20x2%3D%2228%22%20y1%3D%224%22%20y2%3D%2228%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23ffa309%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23da4100%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
|
|
333
|
+
"reply": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22%234f575e%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M229.66%2C157.66l-48%2C48A8%2C8%2C0%2C0%2C1%2C168%2C200V160H128A104.11%2C104.11%2C0%2C0%2C1%2C24%2C56a8%2C8%2C0%2C0%2C1%2C16%2C0%2C88.1%2C88.1%2C0%2C0%2C0%2C88%2C88h40V104a8%2C8%2C0%2C0%2C1%2C13.66-5.66l48%2C48A8%2C8%2C0%2C0%2C1%2C229.66%2C157.66Z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E",
|
|
334
|
+
"repost-fill": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22%2322c55e%22%20d%3D%22M2.5%2016A9.51%209.51%200%200%201%2012%206.5h12.375l-.44-.439a1.503%201.503%200%200%201%202.125-2.125l3%203a1.5%201.5%200%200%201%200%202.125l-3%203a1.503%201.503%200%200%201-2.125-2.125l.44-.436H12A6.51%206.51%200%200%200%205.5%2016a1.5%201.5%200%201%201-3%200M28%2014.5a1.5%201.5%200%200%200-1.5%201.5%206.507%206.507%200%200%201-6.5%206.5H7.625l.44-.439a1.502%201.502%200%201%200-2.125-2.125l-3%203a1.5%201.5%200%200%200%200%202.125l3%203a1.503%201.503%200%200%200%202.125-2.125l-.44-.436H20a9.51%209.51%200%200%200%209.5-9.5%201.5%201.5%200%200%200-1.5-1.5%22%2F%3E%3Cpath%20fill%3D%22%2322c55e%22%20d%3D%22M28%207.75v8a8%208%200%200%201-8%208H4v-8a8%208%200%200%201%208-8z%22%20opacity%3D%22.2%22%2F%3E%3C%2Fsvg%3E",
|
|
335
|
+
"spinner": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M17%204v4a1%201%200%200%201-2%200V4a1%201%200%200%201%202%200m4.656%207.344a1%201%200%200%200%20.708-.294l2.828-2.828a1%201%200%200%200-1.415-1.414L20.95%209.636a1%201%200%200%200%20.706%201.708M28%2015h-4a1%201%200%200%200%200%202h4a1%201%200%200%200%200-2m-5.636%205.95a1%201%200%200%200-1.414%201.414l2.828%202.828a1%201%200%201%200%201.415-1.415zM16%2023a1%201%200%200%200-1%201v4a1%201%200%200%200%202%200v-4a1%201%200%200%200-1-1m-6.364-2.05-2.828%202.828a1%201%200%201%200%201.415%201.415l2.827-2.83a1%201%200%200%200-1.414-1.413M9%2016a1%201%200%200%200-1-1H4a1%201%200%200%200%200%202h4a1%201%200%200%200%201-1m-.777-9.192a1%201%200%201%200-1.416%201.415l2.83%202.827a1%201%200%200%200%201.413-1.414z%22%2F%3E%3C%2Fsvg%3E",
|
|
336
|
+
"trusted-seal": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M28.233%2012.853c-.472-.493-.96-1-1.143-1.447-.17-.409-.18-1.086-.19-1.742-.019-1.22-.039-2.603-1-3.564s-2.344-.981-3.564-1c-.656-.01-1.333-.02-1.742-.19-.445-.184-.954-.671-1.447-1.142C18.286%202.938%2017.306%202%2016%202s-2.284.939-3.148%201.768c-.492.47-1%20.958-1.446%201.142-.406.17-1.086.18-1.742.19-1.22.019-2.603.039-3.564%201s-.975%202.344-1%203.564c-.01.656-.02%201.334-.19%201.742-.184.445-.671.954-1.142%201.446C2.938%2013.715%202%2014.696%202%2016s.939%202.284%201.768%203.148c.47.492.958%201%201.142%201.446.17.409.18%201.086.19%201.742.019%201.22.039%202.603%201%203.564s2.344.981%203.564%201c.656.01%201.334.02%201.742.19.445.184.954.671%201.446%201.143C13.715%2029.06%2014.696%2030%2016%2030s2.284-.939%203.148-1.767c.492-.472%201-.96%201.446-1.143.409-.17%201.086-.18%201.742-.19%201.22-.019%202.603-.039%203.564-1s.981-2.344%201-3.564c.01-.656.02-1.333.19-1.742.184-.445.671-.954%201.143-1.447C29.06%2018.286%2030%2017.306%2030%2016s-.939-2.284-1.767-3.148m-6.526.854-7%207a1%201%200%200%201-1.415%200l-3-3a1%201%200%200%201%201.415-1.415L14%2018.587l6.293-6.293a1%201%200%200%201%201.415%201.415%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%222%22%20x2%3D%2230%22%20y1%3D%222%22%20y2%3D%2230%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%2329de6b%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%230f9f44%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
|
|
337
|
+
"warn": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M28.233%2012.853c-.472-.493-.96-1-1.143-1.447-.17-.409-.18-1.086-.19-1.742-.019-1.22-.039-2.603-1-3.564s-2.344-.981-3.564-1c-.656-.01-1.333-.02-1.742-.19-.445-.184-.954-.671-1.447-1.142C18.286%202.938%2017.306%202%2016%202s-2.284.939-3.148%201.768c-.492.47-1%20.958-1.446%201.142-.406.17-1.086.18-1.742.19-1.22.019-2.603.039-3.564%201s-.975%202.344-1%203.564c-.01.656-.02%201.334-.19%201.742-.184.445-.671.954-1.142%201.446C2.938%2013.715%202%2014.696%202%2016s.939%202.284%201.768%203.148c.47.492.958%201%201.142%201.446.17.409.18%201.086.19%201.742.019%201.22.039%202.603%201%203.564s2.344.981%203.564%201c.656.01%201.334.02%201.742.19.445.184.954.671%201.446%201.143C13.715%2029.06%2014.696%2030%2016%2030s2.284-.939%203.148-1.767c.492-.472%201-.96%201.446-1.143.409-.17%201.086-.18%201.742-.19%201.22-.019%202.603-.039%203.564-1s.981-2.344%201-3.564c.01-.656.02-1.333.19-1.742.184-.445.671-.954%201.143-1.447C29.06%2018.286%2030%2017.306%2030%2016s-.939-2.284-1.767-3.148M15%2010a1%201%200%200%201%202%200v7a1%201%200%200%201-2%200zm1%2013a1.5%201.5%200%201%201%200-3%201.5%201.5%200%200%201%200%203%22%2F%3E%3C%2Fsvg%3E"
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
function getIconUrl(name) {
|
|
341
|
+
return ICONS[name] || "";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
var METRIC_ICONS = {
|
|
345
|
+
post: {
|
|
346
|
+
like: "like-fill",
|
|
347
|
+
repost: "repost-fill",
|
|
348
|
+
reply: "reply-fill",
|
|
349
|
+
quote: "quote-fill",
|
|
350
|
+
bookmark: "bookmark-fill",
|
|
351
|
+
},
|
|
352
|
+
discussion: {
|
|
353
|
+
like: "like-fill",
|
|
354
|
+
repost: "repost-fill",
|
|
355
|
+
reply: "reply-fill",
|
|
356
|
+
quote: "quote-fill",
|
|
357
|
+
bookmark: "bookmark-fill",
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
function metricIcon(name, styleMode) {
|
|
362
|
+
var set = METRIC_ICONS[styleMode] || METRIC_ICONS.discussion;
|
|
363
|
+
return set[name] || name;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
var BADGE_ICONS = {
|
|
367
|
+
crown: "original-seal",
|
|
368
|
+
seal: "trusted-seal",
|
|
369
|
+
check: "check-circle",
|
|
370
|
+
lock: "private",
|
|
371
|
+
tag: "label",
|
|
372
|
+
warning: "warn",
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
var PRELOAD_ICON_NAMES = [
|
|
376
|
+
"like-fill",
|
|
377
|
+
"repost-fill",
|
|
378
|
+
"reply-fill",
|
|
379
|
+
"quote-fill",
|
|
380
|
+
"bookmark-fill",
|
|
381
|
+
"original-seal",
|
|
382
|
+
"trusted-seal",
|
|
383
|
+
"check-circle",
|
|
384
|
+
"private",
|
|
385
|
+
"label",
|
|
386
|
+
"warn",
|
|
387
|
+
"reply",
|
|
388
|
+
"spinner",
|
|
389
|
+
"avatar-fallback",
|
|
390
|
+
"caret-down",
|
|
391
|
+
"caret-up",
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
function maybePreloadIcons() {
|
|
395
|
+
if (typeof document === "undefined") return;
|
|
396
|
+
var current = document.currentScript;
|
|
397
|
+
if (!current || current.getAttribute("data-preload-icons") !== "true") return;
|
|
398
|
+
if (typeof fetch !== "function") return;
|
|
399
|
+
var sample = getIconUrl("like-fill");
|
|
400
|
+
if (sample && sample.indexOf("data:") === 0) return;
|
|
401
|
+
PRELOAD_ICON_NAMES.forEach(function (name) {
|
|
402
|
+
var url = getIconUrl(name);
|
|
403
|
+
if (!url || url.indexOf("data:") === 0) return;
|
|
404
|
+
fetch(url, { cache: "force-cache" }).catch(function () { });
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
maybePreloadIcons();
|
|
409
|
+
|
|
410
|
+
/* ───── Badge detection ───── */
|
|
411
|
+
|
|
412
|
+
function detectBadges(author) {
|
|
413
|
+
var badges = [];
|
|
414
|
+
var v = author.verification || {};
|
|
415
|
+
var labels = author.labels || [];
|
|
416
|
+
var associated = author.associated || {};
|
|
417
|
+
if (v.trustedVerifierStatus === "valid") {
|
|
418
|
+
badges.push(author.handle === "bsky.app" ? "crown" : "seal");
|
|
419
|
+
} else if (v.verifiedStatus === "valid") {
|
|
420
|
+
badges.push("check");
|
|
421
|
+
}
|
|
422
|
+
var hasNoUnauth = labels.some(function (l) {
|
|
423
|
+
return l.val === "!no-unauthenticated";
|
|
424
|
+
});
|
|
425
|
+
if (hasNoUnauth) badges.push("lock");
|
|
426
|
+
var warningVals = ["porn", "spam", "nudity", "sexual", "gore"];
|
|
427
|
+
var hasWarning = labels.some(function (l) {
|
|
428
|
+
if (l.val === "!no-unauthenticated") return false;
|
|
429
|
+
return warningVals.indexOf(l.val) !== -1 || l.val.charAt(0) === "!";
|
|
430
|
+
});
|
|
431
|
+
if (hasWarning) badges.push("warning");
|
|
432
|
+
if (associated.labeler === true) badges.push("tag");
|
|
433
|
+
return badges;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderBadges(author) {
|
|
437
|
+
var badges = detectBadges(author);
|
|
438
|
+
var frag = document.createDocumentFragment();
|
|
439
|
+
badges.forEach(function (b) {
|
|
440
|
+
if (BADGE_ICONS[b]) {
|
|
441
|
+
var span = el("span", "atproto-badge-wrap");
|
|
442
|
+
span.appendChild(
|
|
443
|
+
el("img", "atproto-badge atproto-badge--" + b, {
|
|
444
|
+
src: getIconUrl(BADGE_ICONS[b]),
|
|
445
|
+
alt: b,
|
|
446
|
+
width: "16",
|
|
447
|
+
height: "16",
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
frag.appendChild(span);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return frag;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function countReplyDescendants(thread) {
|
|
457
|
+
if (!thread || !thread.replies || !thread.replies.length) return 0;
|
|
458
|
+
var total = thread.replies.length;
|
|
459
|
+
thread.replies.forEach(function (child) {
|
|
460
|
+
total += countReplyDescendants(child);
|
|
461
|
+
});
|
|
462
|
+
return total;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* ───── Configuration ───── */
|
|
466
|
+
|
|
467
|
+
function parseConfig(container) {
|
|
468
|
+
var mode = (container.getAttribute("data-mode") || "post").toLowerCase();
|
|
469
|
+
if (mode !== "post" && mode !== "discussion") {
|
|
470
|
+
mode = "post";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
var config = {
|
|
474
|
+
mode: mode,
|
|
475
|
+
// Post card options
|
|
476
|
+
showLikes: true,
|
|
477
|
+
showReposts: true,
|
|
478
|
+
showReplies: true,
|
|
479
|
+
showQuotes: true,
|
|
480
|
+
showBookmarks: true,
|
|
481
|
+
showMetrics: true,
|
|
482
|
+
showTimestamp: true,
|
|
483
|
+
showActions: true,
|
|
484
|
+
showReplyContext: true,
|
|
485
|
+
showEmbeds: true,
|
|
486
|
+
showBadges: true,
|
|
487
|
+
showLabels: true,
|
|
488
|
+
showImages: true,
|
|
489
|
+
showVideo: true,
|
|
490
|
+
showExternal: true,
|
|
491
|
+
showQuotePosts: true,
|
|
492
|
+
externalLayout: "vertical",
|
|
493
|
+
// Discussion-specific options
|
|
494
|
+
showMainPost: true,
|
|
495
|
+
showLikedBy: true,
|
|
496
|
+
showRepliesTab: true,
|
|
497
|
+
showQuotesTab: true,
|
|
498
|
+
showTabs: true,
|
|
499
|
+
showSort: true,
|
|
500
|
+
showJoinButton: true,
|
|
501
|
+
repliesSort: "oldest",
|
|
502
|
+
// Sizing
|
|
503
|
+
width: null,
|
|
504
|
+
maxWidth: null,
|
|
505
|
+
// Discussion-only: labels for replies/quotes
|
|
506
|
+
showReplyQuoteLabels: true,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
function parseAttr(name, defaultVal) {
|
|
510
|
+
var val = container.getAttribute("data-" + name);
|
|
511
|
+
if (val === null) return defaultVal;
|
|
512
|
+
return val === "true" || val === "1" || val === "";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function parseStringAttr(name, defaultVal) {
|
|
516
|
+
var val = container.getAttribute("data-" + name);
|
|
517
|
+
return val !== null ? val : defaultVal;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Post card
|
|
521
|
+
config.showLikes = parseAttr("show-likes", config.showLikes);
|
|
522
|
+
config.showReposts = parseAttr("show-reposts", config.showReposts);
|
|
523
|
+
config.showReplies = parseAttr("show-replies", config.showReplies);
|
|
524
|
+
config.showQuotes = parseAttr("show-quotes", config.showQuotes);
|
|
525
|
+
config.showBookmarks = parseAttr("show-bookmarks", config.showBookmarks);
|
|
526
|
+
config.showMetrics = parseAttr("show-metrics", config.showMetrics);
|
|
527
|
+
config.showTimestamp = parseAttr("show-timestamp", config.showTimestamp);
|
|
528
|
+
config.showActions = parseAttr("show-actions", config.showActions);
|
|
529
|
+
config.showReplyContext = parseAttr(
|
|
530
|
+
"show-reply-context",
|
|
531
|
+
config.showReplyContext
|
|
532
|
+
);
|
|
533
|
+
config.showEmbeds = parseAttr("show-embeds", config.showEmbeds);
|
|
534
|
+
config.showBadges = parseAttr("show-badges", config.showBadges);
|
|
535
|
+
config.showLabels = parseAttr("show-labels", config.showLabels);
|
|
536
|
+
config.showImages = parseAttr("show-images", config.showImages);
|
|
537
|
+
config.showVideo = parseAttr("show-video", config.showVideo);
|
|
538
|
+
config.showExternal = parseAttr("show-external", config.showExternal);
|
|
539
|
+
config.showQuotePosts = parseAttr("show-quote-posts", config.showQuotePosts);
|
|
540
|
+
config.externalLayout = parseStringAttr(
|
|
541
|
+
"external-layout",
|
|
542
|
+
config.externalLayout
|
|
543
|
+
);
|
|
544
|
+
if (config.externalLayout !== "horizontal") {
|
|
545
|
+
config.externalLayout = "vertical";
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Discussion
|
|
549
|
+
config.showMainPost = parseAttr("show-main-post", config.showMainPost);
|
|
550
|
+
config.showLikedBy = parseAttr("show-liked-by", config.showLikedBy);
|
|
551
|
+
config.showRepliesTab = parseAttr("show-replies-tab", config.showRepliesTab);
|
|
552
|
+
config.showQuotesTab = parseAttr("show-quotes-tab", config.showQuotesTab);
|
|
553
|
+
config.showTabs = parseAttr("show-tabs", config.showTabs);
|
|
554
|
+
config.showSort = parseAttr("show-sort", config.showSort);
|
|
555
|
+
config.showJoinButton = parseAttr("show-join-button", config.showJoinButton);
|
|
556
|
+
config.repliesSort = parseStringAttr("replies-sort", config.repliesSort);
|
|
557
|
+
|
|
558
|
+
config.width = parseStringAttr("width", config.width);
|
|
559
|
+
config.maxWidth = parseStringAttr("max-width", config.maxWidth);
|
|
560
|
+
|
|
561
|
+
if (container.hasAttribute("data-show-reply-quote-labels")) {
|
|
562
|
+
config.showReplyQuoteLabels = parseAttr(
|
|
563
|
+
"show-reply-quote-labels",
|
|
564
|
+
config.showReplyQuoteLabels
|
|
565
|
+
);
|
|
566
|
+
} else {
|
|
567
|
+
// Back-compat: previous name
|
|
568
|
+
config.showReplyQuoteLabels = parseAttr(
|
|
569
|
+
"show-reply-labels",
|
|
570
|
+
config.showReplyQuoteLabels
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return config;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function applySizeConfig(container, config) {
|
|
578
|
+
if (!config) return;
|
|
579
|
+
if (config.width) {
|
|
580
|
+
container.style.setProperty("--atproto-width", config.width);
|
|
581
|
+
}
|
|
582
|
+
if (config.maxWidth) {
|
|
583
|
+
container.style.setProperty("--atproto-max-width", config.maxWidth);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function clientBase(sourceInfo) {
|
|
588
|
+
if (sourceInfo && sourceInfo.sourceDomain)
|
|
589
|
+
return "https://" + sourceInfo.sourceDomain;
|
|
590
|
+
return "https://bsky.app";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function renderFacets(record, sourceInfo) {
|
|
594
|
+
var text = record.text || "";
|
|
595
|
+
var facets = record.facets;
|
|
596
|
+
if (!facets || !facets.length) return document.createTextNode(text);
|
|
597
|
+
|
|
598
|
+
var base = clientBase(sourceInfo);
|
|
599
|
+
var bytes = TEXT_ENCODER.encode(text);
|
|
600
|
+
|
|
601
|
+
var sorted = facets.slice().sort(function (a, b) {
|
|
602
|
+
return a.index.byteStart - b.index.byteStart;
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
var frag = document.createDocumentFragment();
|
|
606
|
+
var cursor = 0;
|
|
607
|
+
|
|
608
|
+
sorted.forEach(function (facet) {
|
|
609
|
+
var start = facet.index.byteStart;
|
|
610
|
+
var end = facet.index.byteEnd;
|
|
611
|
+
|
|
612
|
+
if (start > cursor) {
|
|
613
|
+
frag.appendChild(
|
|
614
|
+
document.createTextNode(TEXT_DECODER.decode(bytes.slice(cursor, start)))
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
var slice = TEXT_DECODER.decode(bytes.slice(start, end));
|
|
619
|
+
var feat = facet.features[0];
|
|
620
|
+
|
|
621
|
+
if (feat.$type === "app.bsky.richtext.facet#mention") {
|
|
622
|
+
frag.appendChild(
|
|
623
|
+
el("a", "atproto-post__mention", {
|
|
624
|
+
textContent: slice,
|
|
625
|
+
href: base + "/profile/" + feat.did,
|
|
626
|
+
target: "_blank",
|
|
627
|
+
rel: "noopener noreferrer",
|
|
628
|
+
})
|
|
629
|
+
);
|
|
630
|
+
} else if (feat.$type === "app.bsky.richtext.facet#tag") {
|
|
631
|
+
var tag = feat.tag || slice.replace(/^#/, "");
|
|
632
|
+
frag.appendChild(
|
|
633
|
+
el("a", "atproto-post__hashtag", {
|
|
634
|
+
textContent: slice,
|
|
635
|
+
href: base + "/hashtag/" + encodeURIComponent(tag),
|
|
636
|
+
target: "_blank",
|
|
637
|
+
rel: "noopener noreferrer",
|
|
638
|
+
})
|
|
639
|
+
);
|
|
640
|
+
} else if (feat.$type === "app.bsky.richtext.facet#link") {
|
|
641
|
+
frag.appendChild(
|
|
642
|
+
el("a", "atproto-post__link", {
|
|
643
|
+
textContent: slice,
|
|
644
|
+
href: feat.uri,
|
|
645
|
+
target: "_blank",
|
|
646
|
+
rel: "noopener noreferrer",
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
} else {
|
|
650
|
+
frag.appendChild(document.createTextNode(slice));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
cursor = end;
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
if (cursor < bytes.length) {
|
|
657
|
+
frag.appendChild(
|
|
658
|
+
document.createTextNode(TEXT_DECODER.decode(bytes.slice(cursor)))
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return frag;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderText(record, sourceInfo, opts) {
|
|
666
|
+
var nodesFrag = renderFacets(record, sourceInfo);
|
|
667
|
+
var finalFrag = document.createDocumentFragment();
|
|
668
|
+
var currentPara = el("div", "atproto-post__paragraph");
|
|
669
|
+
var splitOnSingleNewline = !!(opts && opts.splitOnSingleNewline);
|
|
670
|
+
|
|
671
|
+
function pushPara() {
|
|
672
|
+
if (currentPara.childNodes.length > 0) {
|
|
673
|
+
finalFrag.appendChild(currentPara);
|
|
674
|
+
currentPara = el("div", "atproto-post__paragraph");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
var nodes = [];
|
|
679
|
+
if (nodesFrag.nodeType === 11) {
|
|
680
|
+
nodes = Array.prototype.slice.call(nodesFrag.childNodes);
|
|
681
|
+
} else {
|
|
682
|
+
nodes = [nodesFrag];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
nodes.forEach(function (node) {
|
|
686
|
+
if (node.nodeType === 3) {
|
|
687
|
+
var text = node.textContent;
|
|
688
|
+
var splitter = "\n\n";
|
|
689
|
+
if (
|
|
690
|
+
splitOnSingleNewline &&
|
|
691
|
+
text.indexOf("\n\n") === -1 &&
|
|
692
|
+
text.indexOf("\n") !== -1
|
|
693
|
+
) {
|
|
694
|
+
splitter = "\n";
|
|
695
|
+
}
|
|
696
|
+
var parts = text.split(splitter);
|
|
697
|
+
parts.forEach(function (part, i) {
|
|
698
|
+
if (i > 0) pushPara();
|
|
699
|
+
if (part) currentPara.appendChild(document.createTextNode(part));
|
|
700
|
+
});
|
|
701
|
+
} else {
|
|
702
|
+
currentPara.appendChild(node);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
pushPara();
|
|
707
|
+
return finalFrag;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* ───── Embed renderers ───── */
|
|
711
|
+
|
|
712
|
+
function renderImages(embed, config) {
|
|
713
|
+
if (config && config.showImages === false) return null;
|
|
714
|
+
var images = embed.images;
|
|
715
|
+
var count = images.length;
|
|
716
|
+
var container = el(
|
|
717
|
+
"div",
|
|
718
|
+
"atproto-embed__images atproto-embed__images--" + count
|
|
719
|
+
);
|
|
720
|
+
images.forEach(function (img) {
|
|
721
|
+
var link = el("a", "atproto-embed__image-link", {
|
|
722
|
+
href: img.fullsize,
|
|
723
|
+
target: "_blank",
|
|
724
|
+
rel: "noopener noreferrer",
|
|
725
|
+
});
|
|
726
|
+
link.appendChild(
|
|
727
|
+
el("img", null, { src: img.thumb, alt: img.alt || "", loading: "lazy" })
|
|
728
|
+
);
|
|
729
|
+
container.appendChild(link);
|
|
730
|
+
});
|
|
731
|
+
return container;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function renderVideo(embed, config) {
|
|
735
|
+
if (config && config.showVideo === false) return null;
|
|
736
|
+
var container = el("div", "atproto-embed__video");
|
|
737
|
+
if (embed.playlist) {
|
|
738
|
+
var video = el("video", null, {
|
|
739
|
+
controls: "true",
|
|
740
|
+
preload: "metadata",
|
|
741
|
+
playsinline: "true",
|
|
742
|
+
});
|
|
743
|
+
if (embed.thumbnail) {
|
|
744
|
+
video.setAttribute("poster", embed.thumbnail);
|
|
745
|
+
}
|
|
746
|
+
if (embed.aspectRatio) {
|
|
747
|
+
video.style.aspectRatio = embed.aspectRatio.width + "/" + embed.aspectRatio.height;
|
|
748
|
+
}
|
|
749
|
+
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
750
|
+
video.src = embed.playlist;
|
|
751
|
+
} else {
|
|
752
|
+
var script = document.createElement("script");
|
|
753
|
+
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js";
|
|
754
|
+
script.onload = function () {
|
|
755
|
+
if (window.Hls && window.Hls.isSupported()) {
|
|
756
|
+
var hls = new window.Hls();
|
|
757
|
+
hls.loadSource(embed.playlist);
|
|
758
|
+
hls.attachMedia(video);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
document.head.appendChild(script);
|
|
762
|
+
}
|
|
763
|
+
container.appendChild(video);
|
|
764
|
+
}
|
|
765
|
+
return container;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function isGifUrl(uri) {
|
|
769
|
+
if (!uri) return false;
|
|
770
|
+
if (/\.gif(\?|$)/i.test(uri)) return true;
|
|
771
|
+
if (uri.indexOf("tenor.com") !== -1 || uri.indexOf("giphy.com") !== -1)
|
|
772
|
+
return true;
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function renderExternal(embed, config) {
|
|
777
|
+
if (config && config.showExternal === false) return null;
|
|
778
|
+
var ext = embed.external;
|
|
779
|
+
var layout =
|
|
780
|
+
config && config.externalLayout === "horizontal" ? "horizontal" : "vertical";
|
|
781
|
+
|
|
782
|
+
if (isGifUrl(ext.uri)) {
|
|
783
|
+
var gifWrap = el("a", "atproto-embed__gif", {
|
|
784
|
+
href: ext.uri,
|
|
785
|
+
target: "_blank",
|
|
786
|
+
rel: "noopener noreferrer",
|
|
787
|
+
});
|
|
788
|
+
gifWrap.appendChild(
|
|
789
|
+
el("img", "atproto-embed__gif-img", {
|
|
790
|
+
src: ext.uri,
|
|
791
|
+
alt: ext.description || ext.title || "",
|
|
792
|
+
loading: "lazy",
|
|
793
|
+
})
|
|
794
|
+
);
|
|
795
|
+
return gifWrap;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
var link = el(
|
|
799
|
+
"a",
|
|
800
|
+
"atproto-embed__external atproto-embed__external--" + layout,
|
|
801
|
+
{
|
|
802
|
+
href: ext.uri,
|
|
803
|
+
target: "_blank",
|
|
804
|
+
rel: "noopener noreferrer",
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
if (ext.thumb) {
|
|
808
|
+
link.appendChild(
|
|
809
|
+
el("img", "atproto-embed__external-thumb", {
|
|
810
|
+
src: ext.thumb,
|
|
811
|
+
alt: "",
|
|
812
|
+
loading: "lazy",
|
|
813
|
+
})
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
var content = el("div", "atproto-embed__external-content");
|
|
817
|
+
if (ext.title)
|
|
818
|
+
content.appendChild(
|
|
819
|
+
el("div", "atproto-embed__external-title", { textContent: ext.title })
|
|
820
|
+
);
|
|
821
|
+
if (ext.description)
|
|
822
|
+
content.appendChild(
|
|
823
|
+
el("div", "atproto-embed__external-desc", {
|
|
824
|
+
textContent: ext.description,
|
|
825
|
+
})
|
|
826
|
+
);
|
|
827
|
+
var domain = ext.uri;
|
|
828
|
+
try {
|
|
829
|
+
domain = new URL(ext.uri).hostname;
|
|
830
|
+
} catch (_) { }
|
|
831
|
+
content.appendChild(
|
|
832
|
+
el("div", "atproto-embed__external-domain", { textContent: domain })
|
|
833
|
+
);
|
|
834
|
+
link.appendChild(content);
|
|
835
|
+
return link;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function renderQuoteEmbed(embed, config, styleMode) {
|
|
839
|
+
if (config && config.showQuotePosts === false) return null;
|
|
840
|
+
var rec = embed.record;
|
|
841
|
+
if (!rec || rec.$type === "app.bsky.embed.record#viewNotFound") return null;
|
|
842
|
+
if (rec.$type === "app.bsky.embed.record#viewBlocked") {
|
|
843
|
+
var blocked = el("div", "atproto-embed__quote");
|
|
844
|
+
blocked.appendChild(
|
|
845
|
+
el("div", null, {
|
|
846
|
+
textContent: "This post is from a blocked account.",
|
|
847
|
+
style: "padding:12px;color:#687684;font-size:14px;",
|
|
848
|
+
})
|
|
849
|
+
);
|
|
850
|
+
return blocked;
|
|
851
|
+
}
|
|
852
|
+
var wrapper = el("div", "atproto-embed__quote");
|
|
853
|
+
wrapper.appendChild(
|
|
854
|
+
renderPostCard(
|
|
855
|
+
{
|
|
856
|
+
post: {
|
|
857
|
+
author: rec.author,
|
|
858
|
+
uri: rec.uri,
|
|
859
|
+
record: rec.value,
|
|
860
|
+
embed: rec.embeds && rec.embeds[0] ? rec.embeds[0] : null,
|
|
861
|
+
indexedAt: rec.indexedAt,
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
true,
|
|
865
|
+
null,
|
|
866
|
+
false,
|
|
867
|
+
config,
|
|
868
|
+
styleMode,
|
|
869
|
+
null,
|
|
870
|
+
null
|
|
871
|
+
)
|
|
872
|
+
);
|
|
873
|
+
return wrapper;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function renderRecordWithMedia(embed, skipRecord, config, styleMode) {
|
|
877
|
+
var frag = document.createDocumentFragment();
|
|
878
|
+
var media = embed.media;
|
|
879
|
+
if (media) {
|
|
880
|
+
var mt = media.$type;
|
|
881
|
+
if (mt === "app.bsky.embed.images#view")
|
|
882
|
+
frag.appendChild(renderImages(media, config));
|
|
883
|
+
else if (mt === "app.bsky.embed.video#view")
|
|
884
|
+
frag.appendChild(renderVideo(media, config));
|
|
885
|
+
else if (mt === "app.bsky.embed.external#view")
|
|
886
|
+
frag.appendChild(renderExternal(media, config));
|
|
887
|
+
}
|
|
888
|
+
if (!skipRecord && embed.record) {
|
|
889
|
+
var q = renderQuoteEmbed(embed.record, config, styleMode);
|
|
890
|
+
if (q) frag.appendChild(q);
|
|
891
|
+
}
|
|
892
|
+
return frag;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function renderEmbed(post, skipQuoteOfRoot, config, styleMode) {
|
|
896
|
+
var embed = post.embed;
|
|
897
|
+
if (!embed) return null;
|
|
898
|
+
var t = embed.$type;
|
|
899
|
+
if (t === "app.bsky.embed.images#view") return renderImages(embed, config);
|
|
900
|
+
if (t === "app.bsky.embed.video#view") return renderVideo(embed, config);
|
|
901
|
+
if (t === "app.bsky.embed.external#view") return renderExternal(embed, config);
|
|
902
|
+
if (t === "app.bsky.embed.record#view") {
|
|
903
|
+
if (skipQuoteOfRoot) return null;
|
|
904
|
+
return renderQuoteEmbed(embed, config, styleMode);
|
|
905
|
+
}
|
|
906
|
+
if (t === "app.bsky.embed.recordWithMedia#view")
|
|
907
|
+
return renderRecordWithMedia(embed, skipQuoteOfRoot, config, styleMode);
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* ───── Post card ───── */
|
|
912
|
+
|
|
913
|
+
function renderPostCard(
|
|
914
|
+
thread,
|
|
915
|
+
isQuote,
|
|
916
|
+
sourceInfo,
|
|
917
|
+
skipQuoteEmbed,
|
|
918
|
+
config,
|
|
919
|
+
styleMode,
|
|
920
|
+
contextType,
|
|
921
|
+
iconMode
|
|
922
|
+
) {
|
|
923
|
+
if (!config) config = {};
|
|
924
|
+
if (!thread || !thread.post) return null;
|
|
925
|
+
|
|
926
|
+
var post = thread.post;
|
|
927
|
+
if (!post.author) {
|
|
928
|
+
post.author = {
|
|
929
|
+
handle: "unknown",
|
|
930
|
+
displayName: "Unknown User",
|
|
931
|
+
avatar: getIconUrl("avatar-fallback"),
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
var record = post.record || post.value;
|
|
936
|
+
var card = el("div", "atproto-post");
|
|
937
|
+
var frag = document.createDocumentFragment();
|
|
938
|
+
|
|
939
|
+
var profileUrl =
|
|
940
|
+
post.author.handle === "unknown"
|
|
941
|
+
? "#"
|
|
942
|
+
: "https://bsky.app/profile/" + post.author.handle;
|
|
943
|
+
var header = el("div", "atproto-post__header");
|
|
944
|
+
var headerMain = el("div", "atproto-post__header-main");
|
|
945
|
+
headerMain.appendChild(
|
|
946
|
+
el("img", "atproto-post__avatar", {
|
|
947
|
+
src: post.author.avatar || getIconUrl("avatar-fallback"),
|
|
948
|
+
alt: post.author.displayName || post.author.handle,
|
|
949
|
+
loading: "lazy",
|
|
950
|
+
})
|
|
951
|
+
);
|
|
952
|
+
var isDiscussionReplyOrQuote =
|
|
953
|
+
styleMode === "discussion" &&
|
|
954
|
+
(contextType === "reply" || contextType === "quote");
|
|
955
|
+
if (isDiscussionReplyOrQuote) {
|
|
956
|
+
card.classList.add("atproto-post--compact");
|
|
957
|
+
var authorInline = el("div", "atproto-post__author-inline");
|
|
958
|
+
var nameWrapInline = el("span", "atproto-post__display-name-wrap");
|
|
959
|
+
nameWrapInline.appendChild(
|
|
960
|
+
el("span", "atproto-post__display-name", {
|
|
961
|
+
textContent: post.author.displayName || post.author.handle,
|
|
962
|
+
})
|
|
963
|
+
);
|
|
964
|
+
if (config.showBadges !== false) {
|
|
965
|
+
var badgesInline = el("span", "atproto-post__badges-inline");
|
|
966
|
+
badgesInline.appendChild(renderBadges(post.author));
|
|
967
|
+
nameWrapInline.appendChild(badgesInline);
|
|
968
|
+
}
|
|
969
|
+
authorInline.appendChild(nameWrapInline);
|
|
970
|
+
authorInline.appendChild(
|
|
971
|
+
el("span", "atproto-post__handle", {
|
|
972
|
+
textContent: "@" + post.author.handle,
|
|
973
|
+
})
|
|
974
|
+
);
|
|
975
|
+
headerMain.appendChild(authorInline);
|
|
976
|
+
} else {
|
|
977
|
+
var author = el("div", "atproto-post__author");
|
|
978
|
+
var nameWrap = el("span", "atproto-post__display-name-wrap");
|
|
979
|
+
nameWrap.appendChild(
|
|
980
|
+
el("span", "atproto-post__display-name", {
|
|
981
|
+
textContent: post.author.displayName || post.author.handle,
|
|
982
|
+
})
|
|
983
|
+
);
|
|
984
|
+
if (config.showBadges !== false) {
|
|
985
|
+
nameWrap.appendChild(renderBadges(post.author));
|
|
986
|
+
}
|
|
987
|
+
author.appendChild(nameWrap);
|
|
988
|
+
author.appendChild(
|
|
989
|
+
el("span", "atproto-post__handle", {
|
|
990
|
+
textContent: "@" + post.author.handle,
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
headerMain.appendChild(author);
|
|
994
|
+
}
|
|
995
|
+
var headerLink = el("a", "atproto-post__header-main-link", {
|
|
996
|
+
href: profileUrl,
|
|
997
|
+
target: "_blank",
|
|
998
|
+
rel: "noopener noreferrer",
|
|
999
|
+
});
|
|
1000
|
+
headerLink.appendChild(headerMain);
|
|
1001
|
+
header.appendChild(headerLink);
|
|
1002
|
+
|
|
1003
|
+
if (styleMode === "discussion" && contextType === "root") {
|
|
1004
|
+
card.classList.add("atproto-post--main");
|
|
1005
|
+
}
|
|
1006
|
+
if (isDiscussionReplyOrQuote && config.showTimestamp !== false) {
|
|
1007
|
+
var postLink = getPostLink(sourceInfo, post);
|
|
1008
|
+
var relTime = formatRelativeTime(record.createdAt || post.indexedAt);
|
|
1009
|
+
if (relTime) {
|
|
1010
|
+
header.appendChild(
|
|
1011
|
+
el("a", "atproto-post__header-right", {
|
|
1012
|
+
href: postLink,
|
|
1013
|
+
target: "_blank",
|
|
1014
|
+
rel: "noopener noreferrer",
|
|
1015
|
+
textContent: relTime,
|
|
1016
|
+
})
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
frag.appendChild(header);
|
|
1021
|
+
|
|
1022
|
+
if (
|
|
1023
|
+
!isQuote &&
|
|
1024
|
+
record &&
|
|
1025
|
+
record.reply &&
|
|
1026
|
+
thread.parent &&
|
|
1027
|
+
config.showReplyContext !== false
|
|
1028
|
+
) {
|
|
1029
|
+
var parent = thread.parent;
|
|
1030
|
+
if (parent.post) {
|
|
1031
|
+
var ctx = el("div", "atproto-post__reply-context");
|
|
1032
|
+
ctx.appendChild(
|
|
1033
|
+
el("img", null, { src: getIconUrl("reply"), width: "14", height: "14" })
|
|
1034
|
+
);
|
|
1035
|
+
ctx.appendChild(
|
|
1036
|
+
document.createTextNode(
|
|
1037
|
+
"Replying to " +
|
|
1038
|
+
(parent.post.author.displayName || "@" + parent.post.author.handle)
|
|
1039
|
+
)
|
|
1040
|
+
);
|
|
1041
|
+
frag.appendChild(ctx);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
var hasText = false;
|
|
1046
|
+
if (record && record.text) {
|
|
1047
|
+
var textDiv = el("div", "atproto-post__text");
|
|
1048
|
+
textDiv.appendChild(
|
|
1049
|
+
renderText(record, sourceInfo, {
|
|
1050
|
+
splitOnSingleNewline: contextType === "reply",
|
|
1051
|
+
})
|
|
1052
|
+
);
|
|
1053
|
+
frag.appendChild(textDiv);
|
|
1054
|
+
hasText = true;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
var hasEmbed = false;
|
|
1058
|
+
if (config.showEmbeds !== false && (!isQuote || post.embed)) {
|
|
1059
|
+
var embedEl = renderEmbed(post, isQuote || !!skipQuoteEmbed, config, styleMode);
|
|
1060
|
+
if (embedEl) {
|
|
1061
|
+
var embedWrap = el("div", "atproto-post__embed");
|
|
1062
|
+
embedWrap.appendChild(embedEl);
|
|
1063
|
+
frag.appendChild(embedWrap);
|
|
1064
|
+
hasEmbed = true;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (isDiscussionReplyOrQuote && hasText && !hasEmbed) {
|
|
1069
|
+
card.classList.add("atproto-post--no-embed");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
var footerAdded = false;
|
|
1073
|
+
if (!isQuote) {
|
|
1074
|
+
var hasAnyMetrics =
|
|
1075
|
+
config.showMetrics !== false &&
|
|
1076
|
+
(config.showLikes !== false ||
|
|
1077
|
+
config.showReposts !== false ||
|
|
1078
|
+
config.showReplies !== false ||
|
|
1079
|
+
config.showQuotes !== false ||
|
|
1080
|
+
config.showBookmarks !== false);
|
|
1081
|
+
|
|
1082
|
+
var footer = null;
|
|
1083
|
+
var metricsAdded = false;
|
|
1084
|
+
var postLink = getPostLink(sourceInfo, post);
|
|
1085
|
+
|
|
1086
|
+
if (hasAnyMetrics) {
|
|
1087
|
+
var metricsRow = el("div", "atproto-post__metrics-row");
|
|
1088
|
+
var metricsLeft = el("div", "atproto-post__metrics-left");
|
|
1089
|
+
var metricsRight = el("div", "atproto-post__metrics-right");
|
|
1090
|
+
var iconStyle = iconMode || styleMode;
|
|
1091
|
+
var iconSize = iconStyle === "post" ? 20 : 16;
|
|
1092
|
+
var isSimpleMetrics = isDiscussionReplyOrQuote;
|
|
1093
|
+
|
|
1094
|
+
if (isSimpleMetrics) {
|
|
1095
|
+
metricsRow.classList.add("atproto-post__metrics-row--simple");
|
|
1096
|
+
|
|
1097
|
+
if (config.showLikes !== false && (post.likeCount || 0) > 0) {
|
|
1098
|
+
metricsLeft.appendChild(
|
|
1099
|
+
el("span", "atproto-post__stat-text", {
|
|
1100
|
+
textContent: formatCountLabel(post.likeCount || 0, "like"),
|
|
1101
|
+
})
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (config.showReposts !== false && (post.repostCount || 0) > 0) {
|
|
1106
|
+
metricsLeft.appendChild(
|
|
1107
|
+
el("span", "atproto-post__stat-text", {
|
|
1108
|
+
textContent: formatCountLabel(post.repostCount || 0, "repost"),
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (config.showQuotes !== false && (post.quoteCount || 0) > 0) {
|
|
1114
|
+
metricsRight.appendChild(
|
|
1115
|
+
el("a", "atproto-post__stat-link", {
|
|
1116
|
+
href: postLink + "/quotes",
|
|
1117
|
+
target: "_blank",
|
|
1118
|
+
rel: "noopener noreferrer",
|
|
1119
|
+
textContent: formatCountLabel(
|
|
1120
|
+
post.quoteCount || 0,
|
|
1121
|
+
"talking",
|
|
1122
|
+
"talking"
|
|
1123
|
+
),
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
} else {
|
|
1128
|
+
if (config.showLikes !== false) {
|
|
1129
|
+
var likes = el("span", "atproto-post__stat--likes");
|
|
1130
|
+
likes.appendChild(
|
|
1131
|
+
el("img", null, {
|
|
1132
|
+
src: getIconUrl(metricIcon("like", iconStyle)),
|
|
1133
|
+
width: String(iconSize),
|
|
1134
|
+
height: String(iconSize),
|
|
1135
|
+
})
|
|
1136
|
+
);
|
|
1137
|
+
likes.appendChild(
|
|
1138
|
+
document.createTextNode(" " + formatCount(post.likeCount || 0))
|
|
1139
|
+
);
|
|
1140
|
+
metricsLeft.appendChild(likes);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (config.showReposts !== false) {
|
|
1144
|
+
var reposts = el("span", "atproto-post__stat--reposts");
|
|
1145
|
+
reposts.appendChild(
|
|
1146
|
+
el("img", null, {
|
|
1147
|
+
src: getIconUrl(metricIcon("repost", iconStyle)),
|
|
1148
|
+
width: String(iconSize),
|
|
1149
|
+
height: String(iconSize),
|
|
1150
|
+
})
|
|
1151
|
+
);
|
|
1152
|
+
reposts.appendChild(
|
|
1153
|
+
document.createTextNode(" " + formatCount(post.repostCount || 0))
|
|
1154
|
+
);
|
|
1155
|
+
metricsLeft.appendChild(reposts);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (config.showReplies !== false) {
|
|
1159
|
+
var replies = el("span", "atproto-post__stat--replies");
|
|
1160
|
+
replies.appendChild(
|
|
1161
|
+
el("img", null, {
|
|
1162
|
+
src: getIconUrl(metricIcon("reply", iconStyle)),
|
|
1163
|
+
width: String(iconSize),
|
|
1164
|
+
height: String(iconSize),
|
|
1165
|
+
})
|
|
1166
|
+
);
|
|
1167
|
+
replies.appendChild(
|
|
1168
|
+
document.createTextNode(" " + formatCount(post.replyCount || 0))
|
|
1169
|
+
);
|
|
1170
|
+
metricsLeft.appendChild(replies);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (config.showQuotes !== false) {
|
|
1174
|
+
var quotes = el("a", "atproto-post__stat--quotes", {
|
|
1175
|
+
href: postLink + "/quotes",
|
|
1176
|
+
target: "_blank",
|
|
1177
|
+
rel: "noopener noreferrer",
|
|
1178
|
+
});
|
|
1179
|
+
quotes.appendChild(
|
|
1180
|
+
el("img", null, {
|
|
1181
|
+
src: getIconUrl(metricIcon("quote", iconStyle)),
|
|
1182
|
+
width: String(iconSize),
|
|
1183
|
+
height: String(iconSize),
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
quotes.appendChild(
|
|
1187
|
+
document.createTextNode(" " + formatCount(post.quoteCount || 0))
|
|
1188
|
+
);
|
|
1189
|
+
metricsRight.appendChild(quotes);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (config.showBookmarks !== false) {
|
|
1193
|
+
var bookmarks = el("span", "atproto-post__stat--bookmarks");
|
|
1194
|
+
bookmarks.appendChild(
|
|
1195
|
+
el("img", null, {
|
|
1196
|
+
src: getIconUrl(metricIcon("bookmark", iconStyle)),
|
|
1197
|
+
width: String(iconSize),
|
|
1198
|
+
height: String(iconSize),
|
|
1199
|
+
})
|
|
1200
|
+
);
|
|
1201
|
+
bookmarks.appendChild(
|
|
1202
|
+
document.createTextNode(" " + formatCount(post.bookmarkCount || 0))
|
|
1203
|
+
);
|
|
1204
|
+
metricsRight.appendChild(bookmarks);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
var hasLeft = metricsLeft.childNodes.length > 0;
|
|
1209
|
+
var hasRight = metricsRight.childNodes.length > 0;
|
|
1210
|
+
if (hasLeft) metricsRow.appendChild(metricsLeft);
|
|
1211
|
+
if (hasRight) metricsRow.appendChild(metricsRight);
|
|
1212
|
+
if (hasLeft || hasRight) {
|
|
1213
|
+
footer = footer || el("div", "atproto-post__footer");
|
|
1214
|
+
footer.appendChild(metricsRow);
|
|
1215
|
+
metricsAdded = true;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (config.showTimestamp !== false && !isDiscussionReplyOrQuote) {
|
|
1220
|
+
var infoRow = el("div", "atproto-post__info-row");
|
|
1221
|
+
|
|
1222
|
+
var timestamp = el("a", "atproto-post__timestamp", {
|
|
1223
|
+
href: postLink,
|
|
1224
|
+
target: "_blank",
|
|
1225
|
+
rel: "noopener noreferrer",
|
|
1226
|
+
textContent:
|
|
1227
|
+
formatTimestamp(record.createdAt || post.indexedAt),
|
|
1228
|
+
});
|
|
1229
|
+
infoRow.appendChild(timestamp);
|
|
1230
|
+
|
|
1231
|
+
var viaInfo = getViaInfo(sourceInfo, post);
|
|
1232
|
+
var via = el("span", "atproto-post__via");
|
|
1233
|
+
via.appendChild(document.createTextNode("via "));
|
|
1234
|
+
via.appendChild(
|
|
1235
|
+
el("a", null, {
|
|
1236
|
+
href: viaInfo.url,
|
|
1237
|
+
target: "_blank",
|
|
1238
|
+
rel: "noopener noreferrer",
|
|
1239
|
+
textContent: viaInfo.label,
|
|
1240
|
+
})
|
|
1241
|
+
);
|
|
1242
|
+
infoRow.appendChild(via);
|
|
1243
|
+
|
|
1244
|
+
footer = footer || el("div", "atproto-post__footer");
|
|
1245
|
+
footer.appendChild(infoRow);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (footer) {
|
|
1249
|
+
frag.appendChild(footer);
|
|
1250
|
+
footerAdded = true;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (styleMode === "post" && config.showActions !== false) {
|
|
1254
|
+
var actions = el("div", "atproto-post__actions");
|
|
1255
|
+
actions.appendChild(
|
|
1256
|
+
el("a", "atproto-post__action atproto-post__action--primary", {
|
|
1257
|
+
href: postLink,
|
|
1258
|
+
target: "_blank",
|
|
1259
|
+
rel: "noopener noreferrer",
|
|
1260
|
+
textContent: "Write your reply",
|
|
1261
|
+
})
|
|
1262
|
+
);
|
|
1263
|
+
actions.appendChild(
|
|
1264
|
+
el("a", "atproto-post__action atproto-post__action--secondary", {
|
|
1265
|
+
href: postLink + "/quotes",
|
|
1266
|
+
target: "_blank",
|
|
1267
|
+
rel: "noopener noreferrer",
|
|
1268
|
+
textContent: "View quotes",
|
|
1269
|
+
})
|
|
1270
|
+
);
|
|
1271
|
+
frag.appendChild(actions);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
var isReplyOrQuote = contextType === "reply" || contextType === "quote";
|
|
1276
|
+
if (
|
|
1277
|
+
isReplyOrQuote &&
|
|
1278
|
+
post.labels &&
|
|
1279
|
+
post.labels.length &&
|
|
1280
|
+
config.showReplyQuoteLabels === false
|
|
1281
|
+
) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (post.labels && post.labels.length && config.showLabels !== false) {
|
|
1286
|
+
frag.appendChild(
|
|
1287
|
+
el("div", "atproto-post__label", {
|
|
1288
|
+
textContent:
|
|
1289
|
+
"Content warning: " +
|
|
1290
|
+
post.labels
|
|
1291
|
+
.map(function (l) {
|
|
1292
|
+
return l.val;
|
|
1293
|
+
})
|
|
1294
|
+
.join(", "),
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (!isQuote && !footerAdded) {
|
|
1300
|
+
card.classList.add("atproto-post--no-footer");
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (frag.childNodes.length > 0) {
|
|
1304
|
+
card.appendChild(frag);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
return card;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/* ───── Liked by ───── */
|
|
1311
|
+
|
|
1312
|
+
function renderLikedBy(likesData, totalLikes, config) {
|
|
1313
|
+
if (config && config.showLikedBy === false) return null;
|
|
1314
|
+
if (!totalLikes || totalLikes === 0) return null;
|
|
1315
|
+
|
|
1316
|
+
var section = el("div", "atproto-discussion__liked-by");
|
|
1317
|
+
section.appendChild(
|
|
1318
|
+
el("div", "atproto-liked-by__label", { textContent: "Liked by" })
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
var avatarsContainer = el("div", "atproto-liked-by__avatars");
|
|
1322
|
+
var likers = likesData.likes || [];
|
|
1323
|
+
var displayCount;
|
|
1324
|
+
var rows = 1;
|
|
1325
|
+
if (totalLikes > 12) rows = 3;
|
|
1326
|
+
else if (totalLikes > 6) rows = 2;
|
|
1327
|
+
var maxWidth = 600;
|
|
1328
|
+
if (config && config.maxWidth && typeof config.maxWidth === "string") {
|
|
1329
|
+
var px = parseInt(config.maxWidth, 10);
|
|
1330
|
+
if (isFinite(px)) maxWidth = px;
|
|
1331
|
+
}
|
|
1332
|
+
var avatarSize = 32;
|
|
1333
|
+
var gap = 6;
|
|
1334
|
+
var perRow = Math.max(3, Math.floor(maxWidth / (avatarSize + gap)));
|
|
1335
|
+
displayCount = Math.min(likers.length, rows * perRow);
|
|
1336
|
+
if (displayCount >= totalLikes) {
|
|
1337
|
+
displayCount = likers.length;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
for (var i = 0; i < displayCount; i++) {
|
|
1341
|
+
var liker = likers[i];
|
|
1342
|
+
if (!liker || !liker.actor) continue;
|
|
1343
|
+
var avatarLink = el("a", null, {
|
|
1344
|
+
href: "https://bsky.app/profile/" + liker.actor.handle,
|
|
1345
|
+
target: "_blank",
|
|
1346
|
+
rel: "noopener noreferrer",
|
|
1347
|
+
title: liker.actor.displayName || liker.actor.handle,
|
|
1348
|
+
});
|
|
1349
|
+
avatarLink.appendChild(
|
|
1350
|
+
el("img", null, {
|
|
1351
|
+
src: liker.actor.avatar || getIconUrl("avatar-fallback"),
|
|
1352
|
+
alt: liker.actor.displayName || liker.actor.handle,
|
|
1353
|
+
loading: "lazy",
|
|
1354
|
+
})
|
|
1355
|
+
);
|
|
1356
|
+
avatarsContainer.appendChild(avatarLink);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
var remaining = totalLikes - displayCount;
|
|
1360
|
+
if (remaining > 0) {
|
|
1361
|
+
var overflow = el("span", "atproto-liked-by__overflow", {
|
|
1362
|
+
textContent: "+" + formatCount(remaining),
|
|
1363
|
+
});
|
|
1364
|
+
if (String(remaining).length >= 3) {
|
|
1365
|
+
overflow.classList.add("atproto-liked-by__overflow--pill");
|
|
1366
|
+
}
|
|
1367
|
+
avatarsContainer.appendChild(overflow);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
section.appendChild(avatarsContainer);
|
|
1371
|
+
|
|
1372
|
+
return section;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* ───── Threaded replies ───── */
|
|
1376
|
+
|
|
1377
|
+
function renderReplyThread(replyThread, depth, sourceInfo, config) {
|
|
1378
|
+
if (!replyThread || !replyThread.post) return document.createDocumentFragment();
|
|
1379
|
+
|
|
1380
|
+
var wrapper = el("div", "atproto-reply-thread");
|
|
1381
|
+
wrapper.setAttribute("data-depth", depth);
|
|
1382
|
+
|
|
1383
|
+
var card = renderPostCard(
|
|
1384
|
+
{ post: replyThread.post },
|
|
1385
|
+
false,
|
|
1386
|
+
sourceInfo,
|
|
1387
|
+
false,
|
|
1388
|
+
config,
|
|
1389
|
+
"discussion",
|
|
1390
|
+
"reply",
|
|
1391
|
+
null
|
|
1392
|
+
);
|
|
1393
|
+
if (!card) return document.createDocumentFragment();
|
|
1394
|
+
|
|
1395
|
+
wrapper.appendChild(card);
|
|
1396
|
+
|
|
1397
|
+
if (replyThread.replies && replyThread.replies.length) {
|
|
1398
|
+
var children = el("div", "atproto-reply-children");
|
|
1399
|
+
replyThread.replies.forEach(function (child) {
|
|
1400
|
+
var childEl = renderReplyThread(child, depth + 1, sourceInfo, config);
|
|
1401
|
+
if (childEl) children.appendChild(childEl);
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
if (depth === 0) {
|
|
1405
|
+
var childCount = countReplyDescendants(replyThread);
|
|
1406
|
+
if (childCount > 0) {
|
|
1407
|
+
var toggleWrap = el("div", "atproto-replies-toggle");
|
|
1408
|
+
var toggleBtn = el("button", "atproto-replies-toggle__btn", {
|
|
1409
|
+
type: "button",
|
|
1410
|
+
});
|
|
1411
|
+
var toggleLabel = formatReplyLabel(childCount);
|
|
1412
|
+
var toggleText = el("span", "atproto-replies-toggle__label", {
|
|
1413
|
+
textContent: toggleLabel,
|
|
1414
|
+
});
|
|
1415
|
+
var toggleIcon = el("img", "atproto-replies-toggle__icon", {
|
|
1416
|
+
src: getIconUrl("caret-down"),
|
|
1417
|
+
alt: "",
|
|
1418
|
+
width: "12",
|
|
1419
|
+
height: "12",
|
|
1420
|
+
});
|
|
1421
|
+
toggleBtn.appendChild(toggleText);
|
|
1422
|
+
toggleBtn.appendChild(toggleIcon);
|
|
1423
|
+
toggleWrap.appendChild(toggleBtn);
|
|
1424
|
+
|
|
1425
|
+
var isOpen = false;
|
|
1426
|
+
children.style.display = "none";
|
|
1427
|
+
toggleBtn.addEventListener("click", function () {
|
|
1428
|
+
isOpen = !isOpen;
|
|
1429
|
+
children.style.display = isOpen ? "" : "none";
|
|
1430
|
+
toggleText.textContent = isOpen ? "Hide replies" : toggleLabel;
|
|
1431
|
+
toggleIcon.setAttribute(
|
|
1432
|
+
"src",
|
|
1433
|
+
getIconUrl(isOpen ? "caret-up" : "caret-down")
|
|
1434
|
+
);
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
card.appendChild(toggleWrap);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
wrapper.appendChild(children);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
return wrapper;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/* ───── Tabs ───── */
|
|
1448
|
+
|
|
1449
|
+
function renderTabs(repliesData, quotesState, atUri, sourceInfo, config, mainPost, hostContainer) {
|
|
1450
|
+
var currentSort = config.repliesSort || "oldest";
|
|
1451
|
+
var tabs = el("div", "atproto-discussion__tabs");
|
|
1452
|
+
|
|
1453
|
+
var rc = mainPost ? mainPost.replyCount || repliesData.length : repliesData.length;
|
|
1454
|
+
var qc = mainPost ? mainPost.quoteCount || quotesState.posts.length : quotesState.posts.length;
|
|
1455
|
+
|
|
1456
|
+
var tabBar = el("div", "atproto-discussion__tab-bar");
|
|
1457
|
+
var repliesBtn = el("button", null, {
|
|
1458
|
+
textContent: "Replies (" + formatCount(rc) + ")",
|
|
1459
|
+
"aria-selected": "true",
|
|
1460
|
+
});
|
|
1461
|
+
var quotesBtn = el("button", null, {
|
|
1462
|
+
textContent: "Quotes (" + formatCount(qc) + ")",
|
|
1463
|
+
"aria-selected": "false",
|
|
1464
|
+
});
|
|
1465
|
+
if (
|
|
1466
|
+
config.showTabs !== false &&
|
|
1467
|
+
(config.showRepliesTab !== false || config.showQuotesTab !== false)
|
|
1468
|
+
) {
|
|
1469
|
+
tabBar.appendChild(repliesBtn);
|
|
1470
|
+
tabBar.appendChild(quotesBtn);
|
|
1471
|
+
tabs.appendChild(tabBar);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
var sortBar = el("div", "atproto-discussion__sort-bar");
|
|
1475
|
+
var sortSelect = el("select", "atproto-discussion__sort-select");
|
|
1476
|
+
[
|
|
1477
|
+
{ val: "newest", text: "Newest" },
|
|
1478
|
+
{ val: "oldest", text: "Oldest" },
|
|
1479
|
+
{ val: "likes", text: "Most Liked" },
|
|
1480
|
+
{ val: "reposts", text: "Most Reposted" },
|
|
1481
|
+
{ val: "quotes", text: "Most Quoted" },
|
|
1482
|
+
{ val: "bookmarks", text: "Most Bookmarked" },
|
|
1483
|
+
{ val: "replies", text: "Most Replies" },
|
|
1484
|
+
].forEach(function (opt) {
|
|
1485
|
+
sortSelect.appendChild(
|
|
1486
|
+
el("option", null, { value: opt.val, textContent: opt.text })
|
|
1487
|
+
);
|
|
1488
|
+
});
|
|
1489
|
+
sortSelect.value = currentSort;
|
|
1490
|
+
if (config.showSort !== false) {
|
|
1491
|
+
sortBar.appendChild(sortSelect);
|
|
1492
|
+
tabs.appendChild(sortBar);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
var tabContent = el("div", "atproto-discussion__tab-content");
|
|
1496
|
+
tabs.appendChild(tabContent);
|
|
1497
|
+
|
|
1498
|
+
function refresh() {
|
|
1499
|
+
var isReplies = repliesBtn.getAttribute("aria-selected") === "true";
|
|
1500
|
+
var frag = document.createDocumentFragment();
|
|
1501
|
+
if (config.showTabs === false || isReplies) {
|
|
1502
|
+
var sortedReplies = sortPosts(repliesData, currentSort);
|
|
1503
|
+
frag.appendChild(renderRepliesPanel(sortedReplies, sourceInfo, config));
|
|
1504
|
+
} else {
|
|
1505
|
+
var sortedQuotes = sortPosts(quotesState.posts, currentSort);
|
|
1506
|
+
frag.appendChild(
|
|
1507
|
+
renderQuotesPanel(
|
|
1508
|
+
{ posts: sortedQuotes, cursor: quotesState.cursor },
|
|
1509
|
+
atUri,
|
|
1510
|
+
sourceInfo,
|
|
1511
|
+
config
|
|
1512
|
+
)
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
tabContent.replaceChildren(frag);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
repliesBtn.addEventListener("click", function () {
|
|
1519
|
+
repliesBtn.setAttribute("aria-selected", "true");
|
|
1520
|
+
quotesBtn.setAttribute("aria-selected", "false");
|
|
1521
|
+
refresh();
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
quotesBtn.addEventListener("click", function () {
|
|
1525
|
+
quotesBtn.setAttribute("aria-selected", "true");
|
|
1526
|
+
repliesBtn.setAttribute("aria-selected", "false");
|
|
1527
|
+
refresh();
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
sortSelect.addEventListener("change", function () {
|
|
1531
|
+
currentSort = sortSelect.value;
|
|
1532
|
+
if (hostContainer) {
|
|
1533
|
+
hostContainer.setAttribute("data-replies-sort", currentSort);
|
|
1534
|
+
}
|
|
1535
|
+
refresh();
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
refresh();
|
|
1539
|
+
return tabs;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/* ───── Replies panel ───── */
|
|
1543
|
+
|
|
1544
|
+
function renderRepliesPanel(repliesData, sourceInfo, config) {
|
|
1545
|
+
if (config.showRepliesTab === false) return el("div");
|
|
1546
|
+
var panel = el("div", "atproto-replies");
|
|
1547
|
+
var replies = repliesData || [];
|
|
1548
|
+
|
|
1549
|
+
if (!replies.length) {
|
|
1550
|
+
panel.appendChild(
|
|
1551
|
+
el("div", "atproto-discussion__empty", {
|
|
1552
|
+
textContent: "No replies yet",
|
|
1553
|
+
})
|
|
1554
|
+
);
|
|
1555
|
+
return panel;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
var pageSize = 20;
|
|
1559
|
+
var displayedCount = 0;
|
|
1560
|
+
|
|
1561
|
+
function renderMore() {
|
|
1562
|
+
var next = replies.slice(displayedCount, displayedCount + pageSize);
|
|
1563
|
+
appendInChunks(
|
|
1564
|
+
next,
|
|
1565
|
+
10,
|
|
1566
|
+
function (reply) {
|
|
1567
|
+
return renderReplyThread(reply, 0, sourceInfo, config);
|
|
1568
|
+
},
|
|
1569
|
+
panel,
|
|
1570
|
+
loadMoreWrap
|
|
1571
|
+
);
|
|
1572
|
+
displayedCount += next.length;
|
|
1573
|
+
|
|
1574
|
+
if (displayedCount >= replies.length) {
|
|
1575
|
+
loadMoreWrap.style.display = "none";
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
var loadMoreWrap = el("div", "atproto-load-more");
|
|
1580
|
+
var loadMoreBtn = el("button", null, {
|
|
1581
|
+
textContent: "Load more replies",
|
|
1582
|
+
});
|
|
1583
|
+
loadMoreBtn.addEventListener("click", renderMore);
|
|
1584
|
+
loadMoreWrap.appendChild(loadMoreBtn);
|
|
1585
|
+
panel.appendChild(loadMoreWrap);
|
|
1586
|
+
|
|
1587
|
+
renderMore();
|
|
1588
|
+
|
|
1589
|
+
return panel;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/* ───── Quotes panel ───── */
|
|
1593
|
+
|
|
1594
|
+
function renderQuoteCard(quotePost, sourceInfo, config) {
|
|
1595
|
+
return renderPostCard(
|
|
1596
|
+
{
|
|
1597
|
+
post: {
|
|
1598
|
+
author: quotePost.author,
|
|
1599
|
+
uri: quotePost.uri,
|
|
1600
|
+
record: quotePost.record || quotePost.value,
|
|
1601
|
+
embed: quotePost.embed || (quotePost.embeds && quotePost.embeds[0]) || null,
|
|
1602
|
+
indexedAt: quotePost.indexedAt,
|
|
1603
|
+
replyCount: quotePost.replyCount || 0,
|
|
1604
|
+
repostCount: quotePost.repostCount || 0,
|
|
1605
|
+
likeCount: quotePost.likeCount || 0,
|
|
1606
|
+
labels: quotePost.labels,
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
false,
|
|
1610
|
+
sourceInfo,
|
|
1611
|
+
true,
|
|
1612
|
+
config,
|
|
1613
|
+
"discussion",
|
|
1614
|
+
"quote",
|
|
1615
|
+
null
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function renderQuotesPanel(quotesState, atUri, sourceInfo, config) {
|
|
1620
|
+
if (config.showQuotesTab === false) return el("div");
|
|
1621
|
+
var panel = el("div", "atproto-quotes");
|
|
1622
|
+
|
|
1623
|
+
var posts = quotesState.posts || [];
|
|
1624
|
+
if (!posts.length && !quotesState.cursor) {
|
|
1625
|
+
panel.appendChild(
|
|
1626
|
+
el("div", "atproto-discussion__empty", {
|
|
1627
|
+
textContent: "No quotes yet",
|
|
1628
|
+
})
|
|
1629
|
+
);
|
|
1630
|
+
return panel;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
appendInChunks(
|
|
1634
|
+
posts,
|
|
1635
|
+
12,
|
|
1636
|
+
function (qp) {
|
|
1637
|
+
if (!qp) return null;
|
|
1638
|
+
return renderQuoteCard(qp, sourceInfo, config);
|
|
1639
|
+
},
|
|
1640
|
+
panel
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
if (quotesState.cursor) {
|
|
1644
|
+
var loadMoreWrap = el("div", "atproto-load-more");
|
|
1645
|
+
var loadMoreBtn = el("button", null, {
|
|
1646
|
+
textContent: "Load more quotes",
|
|
1647
|
+
});
|
|
1648
|
+
loadMoreBtn.addEventListener("click", async function () {
|
|
1649
|
+
loadMoreBtn.innerHTML =
|
|
1650
|
+
"<img src='" +
|
|
1651
|
+
getIconUrl("spinner") +
|
|
1652
|
+
"' class='atproto-spinner' style='width:16px;height:16px;margin-right:6px;vertical-align:middle;'> Loading…";
|
|
1653
|
+
loadMoreBtn.disabled = true;
|
|
1654
|
+
try {
|
|
1655
|
+
var data = await fetchQuotes(atUri, quotesState.cursor);
|
|
1656
|
+
var newPosts = data.posts || [];
|
|
1657
|
+
quotesState.cursor = data.cursor || null;
|
|
1658
|
+
|
|
1659
|
+
appendInChunks(
|
|
1660
|
+
newPosts,
|
|
1661
|
+
12,
|
|
1662
|
+
function (qp) {
|
|
1663
|
+
if (!qp) return null;
|
|
1664
|
+
return renderQuoteCard(qp, sourceInfo, config);
|
|
1665
|
+
},
|
|
1666
|
+
panel,
|
|
1667
|
+
loadMoreWrap
|
|
1668
|
+
);
|
|
1669
|
+
|
|
1670
|
+
if (!quotesState.cursor) {
|
|
1671
|
+
loadMoreWrap.remove();
|
|
1672
|
+
} else {
|
|
1673
|
+
loadMoreBtn.textContent = "Load more quotes";
|
|
1674
|
+
loadMoreBtn.disabled = false;
|
|
1675
|
+
}
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
loadMoreBtn.textContent = "Failed — retry";
|
|
1678
|
+
loadMoreBtn.disabled = false;
|
|
1679
|
+
console.error("[atproto-embed]", err);
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
loadMoreWrap.appendChild(loadMoreBtn);
|
|
1683
|
+
panel.appendChild(loadMoreWrap);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
return panel;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/* ───── Discussion render ───── */
|
|
1690
|
+
|
|
1691
|
+
function buildDiscussion(atUri, sourceInfo, config, data, hostContainer) {
|
|
1692
|
+
var thread = data.thread;
|
|
1693
|
+
var quotesData = data.quotesData;
|
|
1694
|
+
var likesData = data.likesData;
|
|
1695
|
+
|
|
1696
|
+
var discussion = el("div", "atproto-discussion");
|
|
1697
|
+
|
|
1698
|
+
if (config.showMainPost !== false) {
|
|
1699
|
+
var rootCard = renderPostCard(
|
|
1700
|
+
thread,
|
|
1701
|
+
false,
|
|
1702
|
+
sourceInfo,
|
|
1703
|
+
false,
|
|
1704
|
+
config,
|
|
1705
|
+
"discussion",
|
|
1706
|
+
"root",
|
|
1707
|
+
"post"
|
|
1708
|
+
);
|
|
1709
|
+
if (!rootCard) throw new Error("Root post unavailable");
|
|
1710
|
+
discussion.appendChild(rootCard);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
var headerBar = el("div", "atproto-discussion__comments-bar");
|
|
1714
|
+
headerBar.appendChild(
|
|
1715
|
+
el("div", "atproto-discussion__comments-title", {
|
|
1716
|
+
textContent: "Comments",
|
|
1717
|
+
})
|
|
1718
|
+
);
|
|
1719
|
+
if (config.showJoinButton !== false) {
|
|
1720
|
+
var joinLink = getPostLink(sourceInfo, thread.post);
|
|
1721
|
+
headerBar.appendChild(
|
|
1722
|
+
el("a", "atproto-post__action atproto-post__action--primary", {
|
|
1723
|
+
href: joinLink,
|
|
1724
|
+
target: "_blank",
|
|
1725
|
+
rel: "noopener noreferrer",
|
|
1726
|
+
textContent: "Write your reply",
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
discussion.appendChild(headerBar);
|
|
1731
|
+
|
|
1732
|
+
var totalLikes = thread.post.likeCount || 0;
|
|
1733
|
+
var likedByEl = renderLikedBy(likesData, totalLikes, config);
|
|
1734
|
+
if (likedByEl) discussion.appendChild(likedByEl);
|
|
1735
|
+
|
|
1736
|
+
var repliesData = thread.replies || [];
|
|
1737
|
+
var quotesState = {
|
|
1738
|
+
posts: quotesData.posts || [],
|
|
1739
|
+
cursor: quotesData.cursor || null,
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
discussion.appendChild(
|
|
1743
|
+
renderTabs(repliesData, quotesState, atUri, sourceInfo, config, thread.post, hostContainer)
|
|
1744
|
+
);
|
|
1745
|
+
|
|
1746
|
+
return discussion;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
async function fetchDiscussionData(atUri, signal) {
|
|
1750
|
+
var did = extractDid(atUri);
|
|
1751
|
+
var results = await Promise.all([
|
|
1752
|
+
fetchThread(atUri, signal),
|
|
1753
|
+
fetchQuotes(atUri, null, signal),
|
|
1754
|
+
fetchLikes(atUri, signal),
|
|
1755
|
+
fetchProfile(did),
|
|
1756
|
+
]);
|
|
1757
|
+
|
|
1758
|
+
var thread = results[0];
|
|
1759
|
+
var quotesData = results[1] || { posts: [] };
|
|
1760
|
+
var likesData = results[2] || { likes: [] };
|
|
1761
|
+
var profile = results[3];
|
|
1762
|
+
|
|
1763
|
+
if (profile && thread && thread.post && thread.post.author) {
|
|
1764
|
+
if (profile.verification) thread.post.author.verification = profile.verification;
|
|
1765
|
+
if (profile.associated) thread.post.author.associated = profile.associated;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
return {
|
|
1769
|
+
thread: thread,
|
|
1770
|
+
quotesData: quotesData,
|
|
1771
|
+
likesData: likesData,
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
async function renderPostEmbed(container, resolved, config, signal) {
|
|
1776
|
+
container.innerHTML = "";
|
|
1777
|
+
var loadingDiv = el("div", "atproto-embed--loading");
|
|
1778
|
+
loadingDiv.appendChild(
|
|
1779
|
+
el("img", "atproto-spinner", { src: getIconUrl("spinner") })
|
|
1780
|
+
);
|
|
1781
|
+
loadingDiv.appendChild(document.createTextNode("Loading post…"));
|
|
1782
|
+
container.appendChild(loadingDiv);
|
|
1783
|
+
|
|
1784
|
+
try {
|
|
1785
|
+
var did = extractDid(resolved.atUri);
|
|
1786
|
+
var results = await Promise.all([
|
|
1787
|
+
fetchPost(resolved.atUri, signal),
|
|
1788
|
+
fetchProfile(did),
|
|
1789
|
+
]);
|
|
1790
|
+
var thread = results[0];
|
|
1791
|
+
var profile = results[1];
|
|
1792
|
+
|
|
1793
|
+
if (thread && thread.post && thread.post.author && profile) {
|
|
1794
|
+
if (profile.verification) thread.post.author.verification = profile.verification;
|
|
1795
|
+
if (profile.associated) thread.post.author.associated = profile.associated;
|
|
1796
|
+
}
|
|
1797
|
+
var card = renderPostCard(
|
|
1798
|
+
thread,
|
|
1799
|
+
false,
|
|
1800
|
+
{
|
|
1801
|
+
sourceDomain: resolved.sourceDomain,
|
|
1802
|
+
sourceUrl: resolved.sourceUrl,
|
|
1803
|
+
},
|
|
1804
|
+
false,
|
|
1805
|
+
config,
|
|
1806
|
+
"post",
|
|
1807
|
+
"root",
|
|
1808
|
+
null
|
|
1809
|
+
);
|
|
1810
|
+
if (!card) throw new Error("Post not found or unavailable");
|
|
1811
|
+
container.innerHTML = "";
|
|
1812
|
+
container.appendChild(card);
|
|
1813
|
+
} catch (err) {
|
|
1814
|
+
if (err && err.name === "AbortError") return;
|
|
1815
|
+
container.innerHTML = "";
|
|
1816
|
+
container.appendChild(
|
|
1817
|
+
el("div", "atproto-embed--error", {
|
|
1818
|
+
textContent: "Failed to load post",
|
|
1819
|
+
})
|
|
1820
|
+
);
|
|
1821
|
+
console.error("[atproto-embed]", err);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
async function renderDiscussionEmbed(container, resolved, config, hostContainer, signal) {
|
|
1826
|
+
container.innerHTML = "";
|
|
1827
|
+
var loadingDiv = el("div", "atproto-embed--loading");
|
|
1828
|
+
loadingDiv.appendChild(
|
|
1829
|
+
el("img", "atproto-spinner", { src: getIconUrl("spinner") })
|
|
1830
|
+
);
|
|
1831
|
+
loadingDiv.appendChild(document.createTextNode("Loading discussion…"));
|
|
1832
|
+
container.appendChild(loadingDiv);
|
|
1833
|
+
|
|
1834
|
+
try {
|
|
1835
|
+
var data = await fetchDiscussionData(resolved.atUri, signal);
|
|
1836
|
+
container.innerHTML = "";
|
|
1837
|
+
container.appendChild(
|
|
1838
|
+
buildDiscussion(
|
|
1839
|
+
resolved.atUri,
|
|
1840
|
+
{
|
|
1841
|
+
sourceDomain: resolved.sourceDomain,
|
|
1842
|
+
sourceUrl: resolved.sourceUrl,
|
|
1843
|
+
},
|
|
1844
|
+
config,
|
|
1845
|
+
data,
|
|
1846
|
+
hostContainer
|
|
1847
|
+
)
|
|
1848
|
+
);
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
if (err && err.name === "AbortError") return;
|
|
1851
|
+
container.innerHTML = "";
|
|
1852
|
+
container.appendChild(
|
|
1853
|
+
el("div", "atproto-embed--error", {
|
|
1854
|
+
textContent: "Failed to load discussion",
|
|
1855
|
+
})
|
|
1856
|
+
);
|
|
1857
|
+
console.error("[atproto-embed]", err);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
|
|
1863
|
+
/* ───── CSS injection ───── */
|
|
1864
|
+
|
|
1865
|
+
function injectStyles(root) {
|
|
1866
|
+
var style = document.createElement("style");
|
|
1867
|
+
style.textContent = "@import url('https://fonts.googleapis.com/css2?family=Google+Sans+Flex:opsz,wght@6..144,1..1000&display=swap');:root,:host{--neutral-0:#ffffff;--neutral-1:#f8f9fa;--neutral-2:#f1f3f5;--neutral-3:#e9ecef;--neutral-4:#dee2e6;--neutral-5:#ced4da;--neutral-6:#adb5bd;--neutral-7:#6a7178;--neutral-8:#4f575e;--neutral-9:#272b30;--neutral-10:#101213;--neutral-11:#000000;--primary-light:#f8f9ff;--primary-base:#0a66f4;--primary-hover:#20439b;--primary-dark:#1c2855;--font-displayLarge:45px;--font-displaymedium:40px;--font-displaySmall:36px;--font-heading1:32px;--font-heading2:28px;--font-heading3:25px;--font-heading4:22px;--font-heading5:20px;--font-heading6:18px;--font-subtitle:16px;--font-body:14px;--font-caption:12px;--font-label:11px;--font-tagline:10px;--font-sans:\"Google Sans Flex\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;--transition:all 0.32s ease-in-out;--atproto-font-family:var(--font-sans);--atproto-display-name-color:var(--neutral-10);--atproto-display-name-size:var(--font-body);--atproto-display-name-weight:700;--atproto-handle-color:var(--neutral-7);--atproto-handle-size:var(--font-body);--atproto-text-color:var(--neutral-11);--atproto-text-size:var(--font-subtitle);--atproto-text-line-height:1.5;--atproto-timestamp-color:var(--neutral-7);--atproto-timestamp-size:var(--font-caption);--atproto-mention-color:var(--primary-base);--atproto-hashtag-color:var(--primary-base);--atproto-link-color:var(--primary-base);--atproto-link-decoration:none;--atproto-bg:var(--neutral-0);--atproto-border-color:var(--neutral-1);--atproto-border-radius:12px;--atproto-border-width:1px;--atproto-max-width:600px;--atproto-image-radius:8px;--atproto-image-gap:4px;--atproto-video-radius:8px;--atproto-card-bg:var(--neutral-1);--atproto-card-border-color:var(--neutral-2);--atproto-card-title-color:var(--neutral-10);--atproto-card-title-size:var(--font-body);--atproto-card-desc-color:var(--neutral-7);--atproto-card-desc-size:var(--font-caption);--atproto-card-domain-color:var(--neutral-9);--atproto-card-domain-size:var(--font-label);--atproto-external-thumb-ratio:1.91 / 1;--atproto-quote-bg:var(--neutral-0);--atproto-quote-border-color:var(--neutral-1);--atproto-stat-color:var(--neutral-10);--atproto-stat-size:var(--font-body);--atproto-stat-icon-color:var(--neutral-7);--atproto-via-color:var(--primary-base);--atproto-via-size:var(--font-caption);--atproto-label-bg:#fff3cd;--atproto-label-color:#856404;--atproto-label-border-color:#ffc107;--atproto-action-size:var(--font-body);--atproto-avatar-size:42px;--atproto-avatar-radius:50%;--atproto-thread-line-color:var(--neutral-2);--atproto-thread-line-width:2px;--atproto-reply-indent:24px;--atproto-load-more-color:var(--primary-base);--atproto-load-more-bg:transparent;--atproto-load-more-border-color:var(--neutral-2);--atproto-liked-by-label-color:var(--neutral-7);--atproto-liked-by-avatar-size:36px;--atproto-liked-by-avatar-gap:6px;--atproto-liked-by-overflow-color:var(--neutral-7);--atproto-liked-by-overflow-bg:var(--neutral-2)}:host(.atproto-embed-host){display:block;width:var(--atproto-width,100%);max-width:var(--atproto-max-width);min-width:0;box-sizing:border-box;margin:32px auto}:host(.atproto-embed-host--post){margin:32px auto}:is(.atproto-embed--post,.atproto-embed--discussion){width:var(--atproto-width,100%);max-width:var(--atproto-max-width);font-family:var(--atproto-font-family);box-sizing:border-box}:is(.atproto-embed--post,.atproto-embed--discussion) *,:is(.atproto-embed--post,.atproto-embed--discussion) *::before,:is(.atproto-embed--post,.atproto-embed--discussion) *::after{box-sizing:border-box;margin:0;padding:0;scrollbar-width:thin;scrollbar-color:var(--neutral-7) transparent}a,label,select,input,textarea,button{font-family:inherit}:is(.atproto-embed--post,.atproto-embed--discussion) .atproto-post{background:var(--atproto-bg);border:1px solid var(--neutral-3);border-radius:20px;box-shadow:0 0 0 3px var(--neutral-1);padding:16px;color:var(--atproto-text-color)}.atproto-embed--post .atproto-post__header,.atproto-embed--discussion .atproto-post__header{display:flex;align-items:center;gap:8px;margin-bottom:8px;text-decoration:none;color:inherit}.atproto-embed--post .atproto-post__header-main-link,.atproto-embed--discussion .atproto-post__header-main-link{display:inline-flex;align-items:center;gap:8px;text-decoration:none;color:inherit;min-width:0}.atproto-embed--post .atproto-post__header-main,.atproto-embed--discussion .atproto-post__header-main{display:flex;align-items:center;gap:8px;min-width:0}.atproto-embed--post .atproto-post__author,.atproto-embed--discussion .atproto-post__author{display:flex;flex-direction:column;min-width:0;gap:2px}.atproto-embed--post .atproto-post__display-name,.atproto-embed--discussion .atproto-post__display-name{color:var(--atproto-display-name-color);font-size:var(--atproto-display-name-size);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-0.01em;line-height:1.2}.atproto-embed--post .atproto-post__display-name-wrap,.atproto-embed--discussion .atproto-post__display-name-wrap{display:flex;align-items:center;gap:4px;min-width:0}.atproto-embed--post .atproto-badge-wrap,.atproto-embed--discussion .atproto-badge-wrap{display:inline-flex;align-items:center;flex-shrink:0}.atproto-embed--post .atproto-badge,.atproto-embed--discussion .atproto-badge{display:block}.atproto-embed--post .atproto-badge-wrap img,.atproto-embed--discussion .atproto-badge-wrap img{width:16px;height:16px}.atproto-embed--post .atproto-post__handle,.atproto-embed--discussion .atproto-post__handle{color:var(--atproto-handle-color);font-size:var(--atproto-handle-size);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}.atproto-embed--post .atproto-post__reply-context,.atproto-embed--discussion .atproto-post__reply-context{background:var(--neutral-1);color:var(--neutral-8);border-radius:8px;padding:4px 8px;font-size:var(--font-caption);margin-bottom:8px;display:flex;align-items:center;gap:6px;width:fit-content}.atproto-embed--post .atproto-post__reply-context img,.atproto-embed--discussion .atproto-post__reply-context img{width:14px;height:14px;flex-shrink:0}.atproto-embed--post .atproto-post__text,.atproto-embed--discussion .atproto-post__text{color:var(--atproto-text-color);font-size:var(--atproto-text-size);line-height:var(--atproto-text-line-height);margin-bottom:12px;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word}.atproto-embed--post .atproto-post__paragraph:not(:last-child),.atproto-embed--discussion .atproto-post__paragraph:not(:last-child){margin-bottom:8px}.atproto-embed--post .atproto-post__mention,.atproto-embed--discussion .atproto-post__mention{color:var(--atproto-mention-color);font-weight:600;text-decoration:none}.atproto-embed--post .atproto-post__hashtag,.atproto-embed--discussion .atproto-post__hashtag{color:var(--atproto-hashtag-color);font-weight:600;text-decoration:none}.atproto-embed--post .atproto-post__link,.atproto-embed--discussion .atproto-post__link{color:var(--atproto-link-color);font-weight:600;text-decoration:var(--atproto-link-decoration)}@media (hover:hover){.atproto-embed--post .atproto-post__mention:hover,.atproto-embed--discussion .atproto-post__mention:hover,.atproto-embed--post .atproto-post__hashtag:hover,.atproto-embed--discussion .atproto-post__hashtag:hover,.atproto-embed--post .atproto-post__link:hover,.atproto-embed--discussion .atproto-post__link:hover{text-decoration:underline}.atproto-embed--post .atproto-post__timestamp:hover,.atproto-embed--discussion .atproto-post__timestamp:hover{text-decoration:underline}.atproto-embed--post .atproto-post__via a:hover,.atproto-embed--discussion .atproto-post__via a:hover{text-decoration:underline}}.atproto-embed--post .atproto-post__embed,.atproto-embed--discussion .atproto-post__embed{margin-top:12px}.atproto-embed--post .atproto-post__embed>*:not(:first-child),.atproto-embed--discussion .atproto-post__embed>*:not(:first-child){margin-top:12px}.atproto-embed--post .atproto-embed__images,.atproto-embed--discussion .atproto-embed__images{display:grid;gap:var(--atproto-image-gap);border-radius:var(--atproto-image-radius);overflow:hidden;max-height:530px}.atproto-embed--post .atproto-embed__images--1,.atproto-embed--discussion .atproto-embed__images--1{grid-template-columns:1fr}.atproto-embed--post .atproto-embed__images--2,.atproto-embed--discussion .atproto-embed__images--2{grid-template-columns:1fr 1fr;height:320px}.atproto-embed--post .atproto-embed__images--3,.atproto-embed--discussion .atproto-embed__images--3{grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;height:320px}.atproto-embed--post .atproto-embed__images--3 .atproto-embed__image-link:first-child,.atproto-embed--discussion .atproto-embed__images--3 .atproto-embed__image-link:first-child{grid-row:1 / -1}.atproto-embed--post .atproto-embed__images--4,.atproto-embed--discussion .atproto-embed__images--4{grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;height:400px}.atproto-embed--post .atproto-embed__image-link,.atproto-embed--discussion .atproto-embed__image-link{display:block;width:100%;height:100%;overflow:hidden}.atproto-embed--post .atproto-embed__images img,.atproto-embed--discussion .atproto-embed__images img{width:100%;height:100%;display:block;object-fit:cover;transition:opacity 0.2s,transform 0.3s ease-out}.atproto-embed--post .atproto-embed__image-link:hover img,.atproto-embed--discussion .atproto-embed__image-link:hover img{opacity:0.9;transform:scale(1.02)}.atproto-embed--post .atproto-embed__images--1 img,.atproto-embed--discussion .atproto-embed__images--1 img{max-height:530px;object-fit:contain;object-position:center;background:var(--neutral-1);border:2px solid var(--neutral-2);border-radius:16px}.atproto-embed--post .atproto-embed__images--2 img,.atproto-embed--post .atproto-embed__images--3 img,.atproto-embed--post .atproto-embed__images--4 img,.atproto-embed--discussion .atproto-embed__images--2 img,.atproto-embed--discussion .atproto-embed__images--3 img,.atproto-embed--discussion .atproto-embed__images--4 img{height:100%}.atproto-embed--post .atproto-embed__video,.atproto-embed--discussion .atproto-embed__video{border-radius:var(--atproto-video-radius);overflow:hidden;background:#000}.atproto-embed--post .atproto-embed__video video,.atproto-embed--post .atproto-embed__video iframe,.atproto-embed--discussion .atproto-embed__video video,.atproto-embed--discussion .atproto-embed__video iframe{width:100%;max-height:530px;display:block;border:none}.atproto-embed--post .atproto-embed__gif,.atproto-embed--discussion .atproto-embed__gif{display:block;border-radius:var(--atproto-image-radius);overflow:hidden;text-decoration:none}.atproto-embed--post .atproto-embed__gif-img,.atproto-embed--discussion .atproto-embed__gif-img{width:100%;max-height:400px;display:block;object-fit:contain;object-position:center;background:var(--neutral-0)}.atproto-embed--post .atproto-embed__external,.atproto-embed--discussion .atproto-embed__external{border-radius:16px;overflow:hidden;text-decoration:none;display:block;color:inherit;transition:box-shadow 0.18s,border-color 0.18s}.atproto-embed--post .atproto-embed__external:hover,.atproto-embed--discussion .atproto-embed__external:hover{box-shadow:0 3px 12px rgba(0,0,0,0.1)}.atproto-embed--post .atproto-embed__external-thumb,.atproto-embed--discussion .atproto-embed__external-thumb{width:100%;height:auto;aspect-ratio:var(--atproto-external-thumb-ratio);object-fit:cover;object-position:center;display:block;background:var(--neutral-0)}.atproto-embed--post .atproto-embed__external-content,.atproto-embed--discussion .atproto-embed__external-content{padding:16px;background:var(--atproto-card-bg)}.atproto-embed--post .atproto-embed__external--horizontal,.atproto-embed--discussion .atproto-embed__external--horizontal{display:flex;align-items:stretch}.atproto-embed--post .atproto-embed__external--horizontal .atproto-embed__external-thumb,.atproto-embed--discussion .atproto-embed__external--horizontal .atproto-embed__external-thumb{width:38%;max-width:220px;flex:0 0 auto}@media (max-width:520px){.atproto-embed--post .atproto-embed__external--horizontal,.atproto-embed--discussion .atproto-embed__external--horizontal{display:block}.atproto-embed--post .atproto-embed__external--horizontal .atproto-embed__external-thumb,.atproto-embed--discussion .atproto-embed__external--horizontal .atproto-embed__external-thumb{width:100%;max-width:none}}.atproto-embed--post .atproto-embed__external-title,.atproto-embed--discussion .atproto-embed__external-title{color:var(--atproto-card-title-color);font-size:var(--atproto-card-title-size);line-height:1.4;font-weight:600;margin-bottom:2px;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.atproto-embed--post .atproto-embed__external-desc,.atproto-embed--discussion .atproto-embed__external-desc{color:var(--atproto-card-desc-color);font-size:var(--atproto-card-desc-size);display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;margin-bottom:8px;line-height:1.5}.atproto-embed--post .atproto-embed__external-domain,.atproto-embed--discussion .atproto-embed__external-domain{color:var(--atproto-card-domain-color);font-size:var(--atproto-card-domain-size);font-weight:600}.atproto-embed--post .atproto-embed__quote,.atproto-embed--discussion .atproto-embed__quote{border:2px solid var(--atproto-quote-border-color);border-radius:16px;padding:16px;background:var(--atproto-quote-bg)}.atproto-embed--post .atproto-embed__quote .atproto-post,.atproto-embed--discussion .atproto-embed__quote .atproto-post{border:none;padding:0;background:transparent;box-shadow:none}.atproto-embed--post .atproto-post__footer,.atproto-embed--discussion .atproto-post__footer{margin-top:16px}.atproto-embed--post .atproto-post__metrics-row,.atproto-embed--post .atproto-post__info-row,.atproto-embed--discussion .atproto-post__metrics-row,.atproto-embed--discussion .atproto-post__info-row{display:flex;align-items:center;justify-content:space-between;gap:12px}.atproto-embed--post .atproto-post__info-row{padding-top:12px;margin-top:12px;border-top:1px dashed var(--neutral-3)}.atproto-embed--post .atproto-post__metrics-left,.atproto-embed--post .atproto-post__metrics-right,.atproto-embed--discussion .atproto-post__metrics-left,.atproto-embed--discussion .atproto-post__metrics-right{display:flex;align-items:center;gap:16px}.atproto-embed--post .atproto-post__stat--likes,.atproto-embed--post .atproto-post__stat--reposts,.atproto-embed--post .atproto-post__stat--replies,.atproto-embed--post .atproto-post__stat--quotes,.atproto-embed--post .atproto-post__stat--bookmarks,.atproto-embed--discussion .atproto-post__stat--likes,.atproto-embed--discussion .atproto-post__stat--reposts,.atproto-embed--discussion .atproto-post__stat--replies,.atproto-embed--discussion .atproto-post__stat--quotes,.atproto-embed--discussion .atproto-post__stat--bookmarks{display:flex;align-items:center;gap:4px;color:var(--atproto-stat-color);font-size:var(--atproto-stat-size);font-weight:500}.atproto-embed--post .atproto-post__stat--quotes,.atproto-embed--discussion .atproto-post__stat--quotes{text-decoration:none}.atproto-embed--post .atproto-post__metrics-row img,.atproto-embed--discussion .atproto-post__metrics-row img{width:18px;height:18px}.atproto-embed--post .atproto-post__timestamp,.atproto-embed--discussion .atproto-post__timestamp{color:var(--atproto-timestamp-color);font-size:var(--atproto-timestamp-size);text-decoration:none;font-weight:500}.atproto-embed--post .atproto-post__via,.atproto-embed--discussion .atproto-post__via{font-size:var(--atproto-via-size);color:var(--atproto-handle-color)}.atproto-embed--post .atproto-post__via a,.atproto-embed--discussion .atproto-post__via a{color:var(--atproto-via-color);text-decoration:none;font-weight:600}.atproto-embed--post .atproto-post__label,.atproto-embed--discussion .atproto-post__label{background:var(--atproto-label-bg);color:var(--atproto-label-color);border:1px solid var(--atproto-label-border-color);border-radius:100px;padding:4px 12px;font-size:var(--font-label);margin-top:12px;font-weight:500;width:fit-content}.atproto-embed--post .atproto-embed--loading,.atproto-embed--discussion .atproto-embed--loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px 32px;color:var(--atproto-handle-color);font-size:14px;font-weight:500;text-align:center}.atproto-embed--post .atproto-spinner,.atproto-embed--discussion .atproto-spinner{width:24px;height:24px;animation:atproto-spin 1s linear infinite;opacity:0.6}@keyframes atproto-spin{100%{transform:rotate(360deg)}}.atproto-embed--post .atproto-embed--error,.atproto-embed--discussion .atproto-embed--error{display:flex;align-items:center;justify-content:center;padding:24px;color:#cf222e;font-size:14px;border:1px solid #fdd;border-radius:var(--atproto-border-radius);background:#fff5f5;text-align:center;white-space:nowrap}.atproto-embed--post .atproto-post__actions,.atproto-embed--discussion .atproto-post__actions{display:flex;gap:10px;margin-top:16px}.atproto-embed--post .atproto-post__action,.atproto-embed--discussion .atproto-post__action{display:inline-flex;align-items:center;justify-content:center;padding:10px 20px;border-radius:100px;font-size:var(--atproto-action-size);font-weight:600;font-family:var(--atproto-font-family);text-decoration:none;transition:background 0.15s,box-shadow 0.15s,opacity 0.15s;cursor:pointer;border:none;line-height:1;letter-spacing:-0.01em;white-space:nowrap}.atproto-embed--post .atproto-post__action--primary,.atproto-embed--discussion .atproto-post__action--primary{background:var(--primary-base);color:var(--primary-light)}.atproto-embed--post .atproto-post__action--primary:hover,.atproto-embed--discussion .atproto-post__action--primary:hover{background:var(--primary-hover)}.atproto-embed--post .atproto-post__action--secondary,.atproto-embed--discussion .atproto-post__action--secondary{background:var(--neutral-2);color:var(--neutral-11)}.atproto-embed--post .atproto-post__action--secondary:hover,.atproto-embed--discussion .atproto-post__action--secondary:hover{background:var(--neutral-4)}.atproto-embed--post .atproto-post__avatar{width:42px;height:42px;border-radius:50%;object-fit:cover;flex-shrink:0;border:none}.atproto-embed--post .atproto-post__header-main-link:hover .atproto-post__display-name{text-decoration:none}.atproto-embed--post .atproto-post__actions{justify-content:space-between}.atproto-embed--post .atproto-post__action{width:100%}.atproto-embed--discussion .atproto-post__avatar{width:var(--atproto-avatar-size);height:var(--atproto-avatar-size);border-radius:var(--atproto-avatar-radius);object-fit:cover;flex-shrink:0;border:none}@media (hover:hover){.atproto-embed--discussion .atproto-post__header-main-link:hover .atproto-post__display-name{text-decoration:underline}.atproto-embed--discussion .atproto-post__header-right:hover{text-decoration:underline}}.atproto-embed--discussion .atproto-post--compact .atproto-post__header{gap:10px}.atproto-embed--discussion .atproto-post--compact .atproto-post__author-simple{display:none}.atproto-embed--discussion .atproto-post--compact .atproto-post__avatar{width:24px;height:24px}.atproto-embed--discussion .atproto-post--compact .atproto-post__author-inline{display:inline-flex;align-items:center;gap:2px;min-width:0;max-width:100%}.atproto-embed--discussion .atproto-post--compact .atproto-post__display-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.atproto-embed--discussion .atproto-post--compact .atproto-post__handle{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:80%;line-height:1.2}.atproto-embed--discussion .atproto-post--compact .atproto-post__header-main{min-width:0;max-width:100%}.atproto-embed--discussion .atproto-post--compact .atproto-post__header-main-link{max-width:calc(100% - 48px)}.atproto-embed--discussion .atproto-post--compact .atproto-post__text{font-size:var(--font-body)}.atproto-embed--discussion .atproto-post--compact .atproto-post__text:last-child{margin-bottom:0}.atproto-embed--discussion .atproto-post__badges-inline{display:inline-flex;align-items:center;gap:4px}.atproto-embed--discussion .atproto-post__header-right{margin-left:auto;color:var(--atproto-timestamp-color);font-size:var(--font-caption);text-decoration:none;font-weight:500;white-space:nowrap}.atproto-embed--discussion .atproto-discussion{background:var(--atproto-bg);border:1px solid var(--neutral-3);border-radius:20px;overflow:hidden;box-shadow:0 0 0 3px var(--neutral-1)}.atproto-embed--discussion .atproto-discussion>.atproto-post.atproto-post--main{border:none;border-bottom:2px solid var(--atproto-border-color);border-radius:0;box-shadow:none;padding:16px}.atproto-embed--discussion .atproto-post--main .atproto-post__header{gap:8px;margin-bottom:8px}.atproto-embed--discussion .atproto-post--main .atproto-post__avatar{border:none}.atproto-embed--discussion .atproto-post--main .atproto-post__metrics-row{padding-bottom:12px;margin-bottom:12px;border-bottom:1px dashed var(--neutral-3)}.atproto-embed--discussion .atproto-post--main .atproto-post__footer{margin-top:16px;padding-top:0;border-top:0}.atproto-embed--discussion .atproto-post--main .atproto-post__metrics-row img{width:18px;height:18px;opacity:1}.atproto-embed--discussion .atproto-post__metrics-row--simple{padding-bottom:0;margin-bottom:0;border-bottom:0;font-size:var(--font-caption);color:var(--atproto-stat-color)}.atproto-embed--discussion .atproto-post__stat-text{color:var(--atproto-stat-color);font-weight:600;font-size:var(--font-caption)}.atproto-embed--discussion .atproto-post__stat-link{color:var(--atproto-via-color);font-weight:600;text-decoration:none;font-size:var(--font-caption)}.atproto-embed--discussion .atproto-replies-toggle{margin-top:10px}.atproto-embed--discussion .atproto-replies-toggle__btn{display:inline-flex;align-items:center;gap:6px;padding:0;border:none;background:transparent;color:var(--atproto-via-color);font-size:var(--font-caption);font-weight:600;cursor:pointer}@media (hover:hover){.atproto-embed--discussion .atproto-post__stat-link:hover,.atproto-embed--discussion .atproto-replies-toggle__btn:hover{text-decoration:underline}}.atproto-embed--discussion .atproto-replies-toggle__icon{width:12px;height:12px;flex-shrink:0}.atproto-embed--discussion .atproto-post--no-embed .atproto-post__text{margin-bottom:12px}.atproto-embed--discussion .atproto-post--no-embed.atproto-post--no-footer .atproto-post__text,.atproto-embed--post .atproto-post--no-embed.atproto-post--no-footer .atproto-post__text{margin-bottom:0}.atproto-embed--discussion .atproto-post--no-embed .atproto-post__footer{margin-top:8px}.atproto-embed--discussion .atproto-discussion__liked-by{padding:0 16px 24px;background:var(--neutral-0)}.atproto-embed--discussion .atproto-liked-by__label{color:var(--neutral-8);font-size:var(--font-tagline);font-weight:600;margin-bottom:10px}.atproto-embed--discussion .atproto-liked-by__avatars{display:grid;align-items:center;gap:var(--atproto-liked-by-avatar-gap);grid-template-columns:repeat(auto-fill,minmax(var(--atproto-liked-by-avatar-size),1fr))}.atproto-embed--discussion .atproto-liked-by__avatars a{display:block;justify-self:center;border-radius:50%;transition:transform 0.15s}.atproto-embed--discussion .atproto-liked-by__avatars a:hover{transform:scale(1.12);z-index:1;position:relative}.atproto-embed--discussion .atproto-liked-by__avatars img{width:var(--atproto-liked-by-avatar-size);height:var(--atproto-liked-by-avatar-size);border-radius:50%;object-fit:cover;display:block;box-shadow:0 0 0 1px rgba(0,0,0,0.08)}.atproto-embed--discussion .atproto-liked-by__overflow{display:inline-flex;align-items:center;justify-content:center;min-width:var(--atproto-liked-by-avatar-size);height:var(--atproto-liked-by-avatar-size);border-radius:50%;background:var(--neutral-1);color:var(--neutral-11);font-size:var(--font-tagline);font-weight:700;padding:0 6px;border:2px solid var(--neutral-0);box-shadow:0 0 0 1px var(--neutral-3);width:fit-content;justify-self:start}.atproto-embed--discussion .atproto-liked-by__overflow--pill{border-radius:10px;min-width:auto;padding:0 10px}.atproto-embed--discussion .atproto-discussion__join{display:flex;justify-content:center;padding:20px;border-bottom:1px solid var(--neutral-2);background:var(--atproto-bg)}.atproto-embed--discussion .atproto-discussion__join .atproto-post__action{width:100%}.atproto-embed--discussion .atproto-discussion__comments-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:20px 16px 12px;background:var(--atproto-bg)}.atproto-embed--discussion .atproto-discussion__comments-title{font-size:var(--font-heading4);font-weight:600;color:var(--neutral-10)}.atproto-embed--discussion .atproto-discussion__comments-bar .atproto-post__action{width:auto;padding:8px 16px}@media (max-width:768px){.atproto-embed--post .atproto-post__action,.atproto-embed--discussion .atproto-post__action,.atproto-embed--discussion .atproto-discussion__comments-bar .atproto-post__action{font-size:var(--font-caption)}}.atproto-embed--discussion .atproto-discussion__tabs{display:flex;flex-direction:column}.atproto-embed--discussion .atproto-discussion__tab-bar{display:flex;align-items:center;justify-content:flex-start;gap:0;background:var(--neutral-1);border-bottom:2px solid var(--neutral-2);border-top:2px solid var(--neutral-2);padding:8px 16px}.atproto-embed--discussion .atproto-discussion__tab-bar button{padding:6px 12px;background:transparent;cursor:pointer;font-size:var(--font-caption);font-weight:600;color:var(--neutral-7);border:2px solid transparent;transition:color 0.15s,border-color 0.15s;border-radius:100px;width:fit-content;white-space:nowrap}.atproto-embed--discussion .atproto-discussion__tab-bar button[aria-selected=\"true\"]{color:var(--neutral-11);background:var(--neutral-0);border-color:var(--neutral-2)}.atproto-embed--discussion .atproto-discussion__tab-bar button:hover:not([aria-selected=\"true\"]){color:var(--neutral-10)}.atproto-embed--discussion .atproto-discussion__sort-bar{display:flex;align-items:center;justify-content:flex-end;padding:8px 12px;background:var(--atproto-bg);border-bottom:2px solid var(--neutral-1);font-size:var(--font-caption);color:var(--neutral-7)}.atproto-embed--discussion .atproto-discussion__sort-select{padding:8px 4px;border-radius:8px;border:none;background:transparent;font-size:var(--font-caption);font-weight:600;color:var(--neutral-9);cursor:pointer;transition:var(--transition);outline:1px solid transparent;width:100%}.atproto-embed--discussion .atproto-discussion__sort-select:hover,.atproto-embed--discussion .atproto-discussion__sort-select:focus{outline:1px solid var(--neutral-4)}.atproto-embed--discussion .atproto-discussion__tab-content{min-height:60px}.atproto-embed--discussion .atproto-replies,.atproto-embed--discussion .atproto-quotes{padding:0;content-visibility:auto;contain-intrinsic-size:1px 600px}.atproto-embed--discussion .atproto-quotes>.atproto-post{border:none;border-radius:0;border-bottom:2px solid var(--atproto-border-color);box-shadow:none;padding:16px}.atproto-embed--discussion .atproto-quotes>.atproto-post:last-child{border-bottom:none}.atproto-embed--discussion .atproto-replies{background:var(--atproto-bg)}.atproto-embed--discussion .atproto-reply-thread{position:relative;display:flex;flex-direction:column}.atproto-embed--discussion .atproto-reply-thread .atproto-post{border:none !important;border-radius:0 !important;padding:16px !important;box-shadow:none !important;background:transparent !important}.atproto-embed--discussion .atproto-reply-children .atproto-post{padding:12px !important}.atproto-embed--discussion .atproto-reply-children .atproto-post__header{margin-bottom:6px}.atproto-embed--discussion .atproto-reply-children .atproto-post__embed{margin-top:8px}.atproto-embed--discussion .atproto-post--compact .atproto-embed__quote{padding:0}.atproto-embed--discussion .atproto-post--compact .atproto-embed__quote .atproto-post{padding:10px}.atproto-embed--discussion .atproto-reply-children{margin-left:18px;border-left:2px solid var(--neutral-2);padding-left:10px;transition:border-left-color 0.1s ease}.atproto-embed--discussion .atproto-reply-children:hover{border-left-color:var(--neutral-2)}.atproto-embed--discussion .atproto-replies>.atproto-reply-thread{border-bottom:2px solid var(--atproto-border-color)}.atproto-embed--discussion .atproto-replies>.atproto-reply-thread:last-child{border-bottom:none}.atproto-embed--discussion .atproto-reply-children .atproto-reply-thread{margin-top:4px}.atproto-embed--discussion .atproto-reply-thread[data-depth=\"0\"]{margin-top:0}.atproto-embed--discussion .atproto-load-more{display:flex;justify-content:center;padding:14px;border-top:2px solid var(--atproto-border-color)}.atproto-embed--discussion .atproto-load-more button{background:var(--atproto-load-more-bg);color:var(--atproto-load-more-color);border:1px solid var(--atproto-load-more-border-color);border-radius:100px;padding:8px 24px;font-size:13px;font-weight:600;font-family:var(--atproto-font-family);cursor:pointer;transition:background 0.15s,border-color 0.15s;white-space:nowrap}@media (hover:hover){.atproto-embed--discussion .atproto-load-more button:hover{background:#f0f5ff;border-color:#b0c8e8}}.atproto-embed--discussion .atproto-load-more button:disabled{opacity:0.5;cursor:default}.atproto-embed--discussion .atproto-discussion__empty{padding:36px 24px;text-align:center;color:var(--atproto-handle-color);font-size:14px}@media (max-width:768px){.atproto-embed--discussion .atproto-discussion__comments-title{font-size:var(--font-heading6)}}@media (max-width:480px){.atproto-embed--post .atproto-post,.atproto-embed--discussion .atproto-post{padding:12px}.atproto-embed--post .atproto-post__metrics-left,.atproto-embed--discussion .atproto-post__metrics-left{gap:12px}.atproto-embed--discussion .atproto-reply-children{margin-left:8px;padding-left:6px}.atproto-embed--discussion .atproto-liked-by__avatars{gap:4px}.atproto-embed--discussion .atproto-discussion__sort-bar{justify-content:flex-start}}";
|
|
1868
|
+
root.appendChild(style);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function ensureFontLoaded() {
|
|
1872
|
+
if (typeof document === "undefined") return;
|
|
1873
|
+
if (document.getElementById("atproto-font-google-sans")) return;
|
|
1874
|
+
var link = document.createElement("link");
|
|
1875
|
+
link.id = "atproto-font-google-sans";
|
|
1876
|
+
link.setAttribute("rel", "stylesheet");
|
|
1877
|
+
link.href =
|
|
1878
|
+
"https://fonts.googleapis.com/css2?family=Google+Sans+Flex:opsz,wght@6..144,1..1000&display=swap";
|
|
1879
|
+
document.head.appendChild(link);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/* ───── Init ───── */
|
|
1883
|
+
|
|
1884
|
+
async function initContainer(container) {
|
|
1885
|
+
var raw = container.getAttribute("data-uri");
|
|
1886
|
+
if (!raw) return;
|
|
1887
|
+
|
|
1888
|
+
var previous = CONTAINER_ABORTS.get(container);
|
|
1889
|
+
if (previous) previous.abort();
|
|
1890
|
+
var controller = new AbortController();
|
|
1891
|
+
CONTAINER_ABORTS.set(container, controller);
|
|
1892
|
+
|
|
1893
|
+
var config = parseConfig(container);
|
|
1894
|
+
|
|
1895
|
+
container.classList.add("atproto-embed-host");
|
|
1896
|
+
container.classList.remove("atproto-embed-host--post", "atproto-embed-host--discussion");
|
|
1897
|
+
container.classList.add(
|
|
1898
|
+
config.mode === "discussion"
|
|
1899
|
+
? "atproto-embed-host--discussion"
|
|
1900
|
+
: "atproto-embed-host--post"
|
|
1901
|
+
);
|
|
1902
|
+
applySizeConfig(container, config);
|
|
1903
|
+
|
|
1904
|
+
var shadow = container.shadowRoot;
|
|
1905
|
+
if (!shadow) {
|
|
1906
|
+
shadow = container.attachShadow({ mode: "open" });
|
|
1907
|
+
} else {
|
|
1908
|
+
shadow.innerHTML = "";
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
ensureFontLoaded();
|
|
1912
|
+
injectStyles(shadow);
|
|
1913
|
+
|
|
1914
|
+
var wrapper = el("div", "atproto-embed-inner");
|
|
1915
|
+
shadow.appendChild(wrapper);
|
|
1916
|
+
|
|
1917
|
+
try {
|
|
1918
|
+
var resolved = await resolveUri(raw);
|
|
1919
|
+
|
|
1920
|
+
if (config.mode === "discussion") {
|
|
1921
|
+
wrapper.classList.add("atproto-embed--discussion");
|
|
1922
|
+
renderDiscussionEmbed(wrapper, resolved, config, container, controller.signal);
|
|
1923
|
+
} else {
|
|
1924
|
+
wrapper.classList.add("atproto-embed--post");
|
|
1925
|
+
renderPostEmbed(wrapper, resolved, config, controller.signal);
|
|
1926
|
+
}
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
if (err && err.name === "AbortError") return;
|
|
1929
|
+
wrapper.innerHTML = "";
|
|
1930
|
+
wrapper.appendChild(
|
|
1931
|
+
el("div", "atproto-embed--error", {
|
|
1932
|
+
textContent: "Failed to load embed",
|
|
1933
|
+
})
|
|
1934
|
+
);
|
|
1935
|
+
console.error("[atproto-embed]", err);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function init(force) {
|
|
1940
|
+
var containers = document.querySelectorAll(
|
|
1941
|
+
".atproto-embed:not([data-embed-child])"
|
|
1942
|
+
);
|
|
1943
|
+
containers.forEach(function (c) {
|
|
1944
|
+
if (!force && c.getAttribute("data-loaded")) return;
|
|
1945
|
+
var lazy = c.getAttribute("data-lazy");
|
|
1946
|
+
if (lazy === "true" && typeof IntersectionObserver !== "undefined") {
|
|
1947
|
+
if (c.getAttribute("data-loaded") === "pending") return;
|
|
1948
|
+
c.setAttribute("data-loaded", "pending");
|
|
1949
|
+
var observer = new IntersectionObserver(function (entries) {
|
|
1950
|
+
entries.forEach(function (entry) {
|
|
1951
|
+
if (!entry.isIntersecting) return;
|
|
1952
|
+
observer.disconnect();
|
|
1953
|
+
c.setAttribute("data-loaded", "true");
|
|
1954
|
+
initContainer(c);
|
|
1955
|
+
});
|
|
1956
|
+
}, { rootMargin: "200px 0px" });
|
|
1957
|
+
observer.observe(c);
|
|
1958
|
+
} else {
|
|
1959
|
+
c.setAttribute("data-loaded", "true");
|
|
1960
|
+
initContainer(c);
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
if (document.readyState === "loading") {
|
|
1966
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
1967
|
+
} else {
|
|
1968
|
+
init();
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
if (typeof window !== "undefined") {
|
|
1972
|
+
window.AtProtoEmbed = window.AtProtoEmbed || {};
|
|
1973
|
+
window.AtProtoEmbed.init = function (force) {
|
|
1974
|
+
init(!!force);
|
|
1975
|
+
};
|
|
1976
|
+
window.AtProtoEmbed.refresh = function () {
|
|
1977
|
+
init(true);
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
})();
|