sequoia-cli 0.5.5 → 0.5.7
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/dist/components/sequoia-comments.js +201 -47
- package/dist/components/sequoia-subscribe.js +401 -104
- package/dist/index.js +212 -108
- package/package.json +4 -4
|
@@ -11,7 +11,16 @@
|
|
|
11
11
|
* 1. The `document-uri` attribute on the element
|
|
12
12
|
* 2. A <link rel="site.standard.document" href="at://..."> tag in the document head
|
|
13
13
|
*
|
|
14
|
+
* Custom reply button:
|
|
15
|
+
* Place any element with slot="reply-button" to replace the default Bluesky/Blacksky buttons.
|
|
16
|
+
* It stays in the light DOM, so your page CSS applies to it normally.
|
|
17
|
+
* Only practical with post-uri, since that's the only time the URL is known at authoring time:
|
|
18
|
+
* <sequoia-comments post-uri="https://bsky.app/profile/.../post/...">
|
|
19
|
+
* <a slot="reply-button" href="https://bsky.app/profile/.../post/...">Reply</a>
|
|
20
|
+
* </sequoia-comments>
|
|
21
|
+
*
|
|
14
22
|
* Attributes:
|
|
23
|
+
* - post-uri: Bluesky post as AT-URI (at://...) or bsky.app URL — skips PDS document lookup
|
|
15
24
|
* - document-uri: AT Protocol URI for the document (optional if link tag exists)
|
|
16
25
|
* - depth: Maximum depth of nested replies to fetch (default: 6)
|
|
17
26
|
* - hide: Set to "auto" to hide if no document link is detected
|
|
@@ -22,6 +31,7 @@
|
|
|
22
31
|
* - --sequoia-border-color: Border color (default: #e5e7eb)
|
|
23
32
|
* - --sequoia-accent-color: Accent/link color (default: #2563eb)
|
|
24
33
|
* - --sequoia-secondary-color: Secondary text color (default: #6b7280)
|
|
34
|
+
* - --sequoia-font-family: Font family (default: system-ui stack)
|
|
25
35
|
* - --sequoia-border-radius: Border radius (default: 8px)
|
|
26
36
|
*/
|
|
27
37
|
|
|
@@ -32,7 +42,7 @@
|
|
|
32
42
|
const styles = `
|
|
33
43
|
:host {
|
|
34
44
|
display: block;
|
|
35
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
45
|
+
font-family: var(--sequoia-font-family, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
36
46
|
color: var(--sequoia-fg-color, #1f2937);
|
|
37
47
|
line-height: 1.5;
|
|
38
48
|
}
|
|
@@ -250,17 +260,17 @@ const styles = `
|
|
|
250
260
|
white-space: nowrap;
|
|
251
261
|
}
|
|
252
262
|
|
|
263
|
+
.sequoia-comment-handle::after {
|
|
264
|
+
content: "·";
|
|
265
|
+
margin-left: 0.5rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
253
268
|
.sequoia-comment-time {
|
|
254
269
|
font-size: 0.875rem;
|
|
255
270
|
color: var(--sequoia-secondary-color, #6b7280);
|
|
256
271
|
flex-shrink: 0;
|
|
257
272
|
}
|
|
258
273
|
|
|
259
|
-
.sequoia-comment-time::before {
|
|
260
|
-
content: "·";
|
|
261
|
-
margin-right: 0.5rem;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
274
|
.sequoia-comment-text {
|
|
265
275
|
margin: 0;
|
|
266
276
|
white-space: pre-wrap;
|
|
@@ -280,6 +290,30 @@ const styles = `
|
|
|
280
290
|
width: 1rem;
|
|
281
291
|
height: 1rem;
|
|
282
292
|
}
|
|
293
|
+
|
|
294
|
+
.sequoia-quotes-section {
|
|
295
|
+
margin-top: 1.75rem;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.sequoia-quotes-header {
|
|
299
|
+
font-size: 0.75rem;
|
|
300
|
+
font-weight: 600;
|
|
301
|
+
color: var(--sequoia-secondary-color, #6b7280);
|
|
302
|
+
letter-spacing: 0.05em;
|
|
303
|
+
text-transform: uppercase;
|
|
304
|
+
margin: 0;
|
|
305
|
+
padding-bottom: 0.75rem;
|
|
306
|
+
border-bottom: 1px solid var(--sequoia-border-color, #e5e7eb);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
a.sequoia-comment-time {
|
|
310
|
+
text-decoration: none;
|
|
311
|
+
color: var(--sequoia-secondary-color, #6b7280);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
a.sequoia-comment-time:hover {
|
|
315
|
+
text-decoration: underline;
|
|
316
|
+
}
|
|
283
317
|
`;
|
|
284
318
|
|
|
285
319
|
// ============================================================================
|
|
@@ -583,6 +617,72 @@ function isThreadViewPost(post) {
|
|
|
583
617
|
return post?.$type === "app.bsky.feed.defs#threadViewPost";
|
|
584
618
|
}
|
|
585
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Fetch all quote posts for a given post URI, paginating through all results.
|
|
622
|
+
* Uses the public Bluesky AppView — gaps are expected for posts from
|
|
623
|
+
* less-connected PDS instances.
|
|
624
|
+
* @param {string} postUri - AT Protocol URI for the post
|
|
625
|
+
* @returns {Promise<Array>} Array of PostView objects
|
|
626
|
+
*/
|
|
627
|
+
/**
|
|
628
|
+
* Normalise a user-supplied post reference to an AT-URI.
|
|
629
|
+
* Accepts:
|
|
630
|
+
* - AT-URIs as-is: at://did:plc:.../app.bsky.feed.post/rkey
|
|
631
|
+
* - bsky.app post URLs: https://bsky.app/profile/<handle-or-did>/post/<rkey>
|
|
632
|
+
* When the profile segment is already a DID no network request is made.
|
|
633
|
+
* @param {string} uriOrUrl
|
|
634
|
+
* @returns {Promise<string>} AT-URI
|
|
635
|
+
*/
|
|
636
|
+
async function resolvePostUri(uriOrUrl) {
|
|
637
|
+
if (uriOrUrl.startsWith("at://")) return uriOrUrl;
|
|
638
|
+
|
|
639
|
+
const match = uriOrUrl.match(
|
|
640
|
+
/bsky\.app\/profile\/([^/?#]+)\/post\/([^/?#]+)/,
|
|
641
|
+
);
|
|
642
|
+
if (!match) throw new Error(`Cannot parse Bluesky URL: ${uriOrUrl}`);
|
|
643
|
+
|
|
644
|
+
const [, handleOrDid, rkey] = match;
|
|
645
|
+
|
|
646
|
+
let did = handleOrDid;
|
|
647
|
+
if (!handleOrDid.startsWith("did:")) {
|
|
648
|
+
const url = new URL(
|
|
649
|
+
"https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle",
|
|
650
|
+
);
|
|
651
|
+
url.searchParams.set("handle", handleOrDid);
|
|
652
|
+
const response = await fetch(url.toString());
|
|
653
|
+
if (!response.ok)
|
|
654
|
+
throw new Error(`Failed to resolve handle: ${response.status}`);
|
|
655
|
+
did = (await response.json()).did;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return `at://${did}/app.bsky.feed.post/${rkey}`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function getQuotes(postUri) {
|
|
662
|
+
const quotes = [];
|
|
663
|
+
let cursor;
|
|
664
|
+
|
|
665
|
+
do {
|
|
666
|
+
const url = new URL(
|
|
667
|
+
"https://public.api.bsky.app/xrpc/app.bsky.feed.getQuotes",
|
|
668
|
+
);
|
|
669
|
+
url.searchParams.set("uri", postUri);
|
|
670
|
+
url.searchParams.set("limit", "100");
|
|
671
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
672
|
+
|
|
673
|
+
const response = await fetch(url.toString());
|
|
674
|
+
if (!response.ok) {
|
|
675
|
+
throw new Error(`Failed to fetch quotes: ${response.status}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const data = await response.json();
|
|
679
|
+
quotes.push(...(data.posts ?? []));
|
|
680
|
+
cursor = data.cursor;
|
|
681
|
+
} while (cursor);
|
|
682
|
+
|
|
683
|
+
return quotes;
|
|
684
|
+
}
|
|
685
|
+
|
|
586
686
|
// ============================================================================
|
|
587
687
|
// Bluesky Icon
|
|
588
688
|
// ============================================================================
|
|
@@ -620,10 +720,11 @@ class SequoiaComments extends BaseElement {
|
|
|
620
720
|
}
|
|
621
721
|
|
|
622
722
|
static get observedAttributes() {
|
|
623
|
-
return ["document-uri", "depth", "hide"];
|
|
723
|
+
return ["post-uri", "document-uri", "depth", "hide"];
|
|
624
724
|
}
|
|
625
725
|
|
|
626
726
|
connectedCallback() {
|
|
727
|
+
this.initialized = true;
|
|
627
728
|
this.render();
|
|
628
729
|
this.loadComments();
|
|
629
730
|
}
|
|
@@ -633,7 +734,10 @@ class SequoiaComments extends BaseElement {
|
|
|
633
734
|
}
|
|
634
735
|
|
|
635
736
|
attributeChangedCallback() {
|
|
636
|
-
|
|
737
|
+
// attributeChangedCallback fires for pre-existing attributes during
|
|
738
|
+
// element upgrade, *before* connectedCallback — skip until we've done
|
|
739
|
+
// the initial load, otherwise every attribute triggers a duplicate fetch.
|
|
740
|
+
if (this.initialized) {
|
|
637
741
|
this.loadComments();
|
|
638
742
|
}
|
|
639
743
|
}
|
|
@@ -670,39 +774,54 @@ class SequoiaComments extends BaseElement {
|
|
|
670
774
|
this.state = { type: "loading" };
|
|
671
775
|
this.render();
|
|
672
776
|
|
|
673
|
-
const docUri = this.documentUri;
|
|
674
|
-
if (!docUri) {
|
|
675
|
-
this.state = { type: "no-document" };
|
|
676
|
-
this.render();
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
777
|
try {
|
|
681
|
-
//
|
|
682
|
-
|
|
778
|
+
// Resolve the post URI — either directly from the attribute or via the
|
|
779
|
+
// document record (which requires a PDS roundtrip)
|
|
780
|
+
const rawPostUri = this.getAttribute("post-uri");
|
|
781
|
+
let postUri = rawPostUri ? await resolvePostUri(rawPostUri) : null;
|
|
782
|
+
if (!postUri) {
|
|
783
|
+
const docUri = this.documentUri;
|
|
784
|
+
if (!docUri) {
|
|
785
|
+
this.state = { type: "no-document" };
|
|
786
|
+
this.render();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
683
789
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
790
|
+
const document = await getDocument(docUri);
|
|
791
|
+
if (!document.bskyPostRef) {
|
|
792
|
+
this.state = { type: "no-comments-enabled" };
|
|
793
|
+
this.render();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
postUri = document.bskyPostRef.uri;
|
|
689
798
|
}
|
|
690
799
|
|
|
691
|
-
const postUrl = buildBskyAppUrl(
|
|
692
|
-
const blackskyPostUrl = buildBlackskyAppUrl(
|
|
800
|
+
const postUrl = buildBskyAppUrl(postUri);
|
|
801
|
+
const blackskyPostUrl = buildBlackskyAppUrl(postUri);
|
|
802
|
+
|
|
803
|
+
// Fetch thread and quotes in parallel; quote failures degrade gracefully
|
|
804
|
+
const [threadResult, quotesResult] = await Promise.allSettled([
|
|
805
|
+
getPostThread(postUri, this.depth),
|
|
806
|
+
getQuotes(postUri),
|
|
807
|
+
]);
|
|
808
|
+
|
|
809
|
+
if (threadResult.status === "rejected") {
|
|
810
|
+
throw threadResult.reason;
|
|
811
|
+
}
|
|
693
812
|
|
|
694
|
-
|
|
695
|
-
const
|
|
813
|
+
const thread = threadResult.value;
|
|
814
|
+
const quotes =
|
|
815
|
+
quotesResult.status === "fulfilled" ? quotesResult.value : [];
|
|
696
816
|
|
|
697
|
-
// Check if there are any replies
|
|
698
817
|
const replies = thread.replies?.filter(isThreadViewPost) ?? [];
|
|
699
|
-
if (replies.length === 0) {
|
|
818
|
+
if (replies.length === 0 && quotes.length === 0) {
|
|
700
819
|
this.state = { type: "empty", postUrl, blackskyPostUrl };
|
|
701
820
|
this.render();
|
|
702
821
|
return;
|
|
703
822
|
}
|
|
704
823
|
|
|
705
|
-
this.state = { type: "loaded", thread, postUrl, blackskyPostUrl };
|
|
824
|
+
this.state = { type: "loaded", thread, quotes, postUrl, blackskyPostUrl };
|
|
706
825
|
this.render();
|
|
707
826
|
} catch (error) {
|
|
708
827
|
const message =
|
|
@@ -746,14 +865,7 @@ class SequoiaComments extends BaseElement {
|
|
|
746
865
|
this.commentsContainer.innerHTML = `
|
|
747
866
|
<div class="sequoia-comments-header">
|
|
748
867
|
<h3 class="sequoia-comments-title">Comments</h3>
|
|
749
|
-
<div>
|
|
750
|
-
<a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-bluesky">
|
|
751
|
-
${BLUESKY_ICON}
|
|
752
|
-
</a>
|
|
753
|
-
<a href="${this.state.blackskyPostUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-blacksky">
|
|
754
|
-
${BLACKSKY_ICON}
|
|
755
|
-
</a>
|
|
756
|
-
</div>
|
|
868
|
+
<div>${this.renderReplyButtons(this.state.postUrl, this.state.blackskyPostUrl)}</div>
|
|
757
869
|
</div>
|
|
758
870
|
<div class="sequoia-empty">
|
|
759
871
|
No comments yet. Be the first to reply on Bluesky!
|
|
@@ -772,26 +884,26 @@ class SequoiaComments extends BaseElement {
|
|
|
772
884
|
case "loaded": {
|
|
773
885
|
const replies =
|
|
774
886
|
this.state.thread.replies?.filter(isThreadViewPost) ?? [];
|
|
887
|
+
const quotes = this.state.quotes ?? [];
|
|
775
888
|
const threadsHtml = replies
|
|
776
889
|
.map((reply) => this.renderThread(reply))
|
|
777
890
|
.join("");
|
|
778
891
|
const commentCount = this.countComments(replies);
|
|
892
|
+
const titleText =
|
|
893
|
+
commentCount > 0
|
|
894
|
+
? `${commentCount} Comment${commentCount !== 1 ? "s" : ""}`
|
|
895
|
+
: "Comments";
|
|
896
|
+
const quotesHtml = this.renderQuotesSection(quotes);
|
|
779
897
|
|
|
780
898
|
this.commentsContainer.innerHTML = `
|
|
781
899
|
<div class="sequoia-comments-header">
|
|
782
|
-
<h3 class="sequoia-comments-title">${
|
|
783
|
-
<div>
|
|
784
|
-
<a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-bluesky">
|
|
785
|
-
${BLUESKY_ICON}
|
|
786
|
-
</a>
|
|
787
|
-
<a href="${this.state.blackskyPostUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-blacksky">
|
|
788
|
-
${BLACKSKY_ICON}
|
|
789
|
-
</a>
|
|
790
|
-
</div>
|
|
900
|
+
<h3 class="sequoia-comments-title">${titleText}</h3>
|
|
901
|
+
<div>${this.renderReplyButtons(this.state.postUrl, this.state.blackskyPostUrl)}</div>
|
|
791
902
|
</div>
|
|
792
903
|
<div class="sequoia-comments-list">
|
|
793
904
|
${threadsHtml}
|
|
794
905
|
</div>
|
|
906
|
+
${quotesHtml}
|
|
795
907
|
`;
|
|
796
908
|
break;
|
|
797
909
|
}
|
|
@@ -820,6 +932,24 @@ class SequoiaComments extends BaseElement {
|
|
|
820
932
|
return result;
|
|
821
933
|
}
|
|
822
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Render the reply-button slot. Any element with slot="reply-button" in the
|
|
937
|
+
* light DOM is projected here and remains styleable by external CSS.
|
|
938
|
+
* The default Bluesky/Blacksky buttons are used as fallback content.
|
|
939
|
+
*/
|
|
940
|
+
renderReplyButtons(postUrl, blackskyPostUrl) {
|
|
941
|
+
return `
|
|
942
|
+
<slot name="reply-button">
|
|
943
|
+
<a href="${escapeHtml(postUrl)}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-bluesky">
|
|
944
|
+
${BLUESKY_ICON}
|
|
945
|
+
</a>
|
|
946
|
+
<a href="${escapeHtml(blackskyPostUrl)}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-blacksky">
|
|
947
|
+
${BLACKSKY_ICON}
|
|
948
|
+
</a>
|
|
949
|
+
</slot>
|
|
950
|
+
`;
|
|
951
|
+
}
|
|
952
|
+
|
|
823
953
|
/**
|
|
824
954
|
* Render a complete thread (top-level comment + all nested replies)
|
|
825
955
|
*/
|
|
@@ -834,6 +964,29 @@ class SequoiaComments extends BaseElement {
|
|
|
834
964
|
return `<div class="sequoia-thread">${commentsHtml}</div>`;
|
|
835
965
|
}
|
|
836
966
|
|
|
967
|
+
/**
|
|
968
|
+
* Render a section of quote posts below the replies
|
|
969
|
+
* @param {Array} quotes - Array of PostView objects from getQuotes
|
|
970
|
+
*/
|
|
971
|
+
renderQuotesSection(quotes) {
|
|
972
|
+
if (quotes.length === 0) return "";
|
|
973
|
+
|
|
974
|
+
const quotesHtml = quotes
|
|
975
|
+
.map((post) => {
|
|
976
|
+
return `<div class="sequoia-thread">${this.renderComment(post, false, 0)}</div>`;
|
|
977
|
+
})
|
|
978
|
+
.join("");
|
|
979
|
+
|
|
980
|
+
return `
|
|
981
|
+
<div class="sequoia-quotes-section">
|
|
982
|
+
<h4 class="sequoia-quotes-header">Quotes (${quotes.length})</h4>
|
|
983
|
+
<div class="sequoia-comments-list">
|
|
984
|
+
${quotesHtml}
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
`;
|
|
988
|
+
}
|
|
989
|
+
|
|
837
990
|
/**
|
|
838
991
|
* Render a single comment
|
|
839
992
|
* @param {any} post - Post data
|
|
@@ -850,6 +1003,7 @@ class SequoiaComments extends BaseElement {
|
|
|
850
1003
|
const profileUrl = `https://bsky.app/profile/${author.did}`;
|
|
851
1004
|
const textHtml = renderTextWithFacets(post.record.text, post.record.facets);
|
|
852
1005
|
const timeAgo = formatRelativeTime(post.record.createdAt);
|
|
1006
|
+
const timeHtml = `<a href="${escapeHtml(buildBskyAppUrl(post.uri))}" target="_blank" rel="noopener noreferrer" class="sequoia-comment-time">${timeAgo}</a>`;
|
|
853
1007
|
const threadLineHtml = showThreadLine
|
|
854
1008
|
? '<div class="sequoia-thread-line"></div>'
|
|
855
1009
|
: "";
|
|
@@ -866,7 +1020,7 @@ class SequoiaComments extends BaseElement {
|
|
|
866
1020
|
${escapeHtml(displayName)}
|
|
867
1021
|
</a>
|
|
868
1022
|
<span class="sequoia-comment-handle">@${escapeHtml(author.handle)}</span>
|
|
869
|
-
|
|
1023
|
+
${timeHtml}
|
|
870
1024
|
</div>
|
|
871
1025
|
<p class="sequoia-comment-text">${textHtml}</p>
|
|
872
1026
|
</div>
|