Compare commits
3 Commits
06067c0ff3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
13f7f12499
|
|||
|
bf72b74a01
|
|||
|
3950a65783
|
@@ -123,7 +123,8 @@ final class BlueskyThreadsExtension extends Minz_Extension {
|
|||||||
return $fallback;
|
return $fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = $this->renderThread($data['thread'], true);
|
$rootAuthorDid = $data['thread']['post']['author']['did'] ?? null;
|
||||||
|
$html = $this->renderThread($data['thread'], true, $rootAuthorDid);
|
||||||
$this->writeCache($url, $html);
|
$this->writeCache($url, $html);
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
@@ -221,7 +222,7 @@ final class BlueskyThreadsExtension extends Minz_Extension {
|
|||||||
// Thread rendering
|
// Thread rendering
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private function renderThread(array $node, bool $isRoot): string {
|
private function renderThread(array $node, bool $isRoot, ?string $rootAuthorDid = null): string {
|
||||||
if (!isset($node['post'])) {
|
if (!isset($node['post'])) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -229,14 +230,22 @@ final class BlueskyThreadsExtension extends Minz_Extension {
|
|||||||
$html = $this->renderPost($node['post'], $isRoot);
|
$html = $this->renderPost($node['post'], $isRoot);
|
||||||
|
|
||||||
if (!empty($node['replies'])) {
|
if (!empty($node['replies'])) {
|
||||||
$html .= '<div class="bsky-replies" style="border-left:2px solid #e1e8ed;margin-left:24px;padding-left:0;">';
|
$repliesHtml = '';
|
||||||
foreach ($node['replies'] as $reply) {
|
foreach ($node['replies'] as $reply) {
|
||||||
if (isset($reply['post'])) {
|
if (!isset($reply['post'])) {
|
||||||
$html .= $this->renderThread($reply, false);
|
continue;
|
||||||
}
|
}
|
||||||
|
if ($rootAuthorDid !== null && ($reply['post']['author']['did'] ?? null) !== $rootAuthorDid) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
$repliesHtml .= $this->renderThread($reply, false, $rootAuthorDid);
|
||||||
|
}
|
||||||
|
if ($repliesHtml !== '') {
|
||||||
|
$html .= '<div class="bsky-replies" style="border-left:2px solid #e1e8ed;margin-left:24px;padding-left:12px;">';
|
||||||
|
$html .= $repliesHtml;
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
@@ -254,7 +263,7 @@ final class BlueskyThreadsExtension extends Minz_Extension {
|
|||||||
$embedHtml = $embed !== null ? $this->renderEmbed($embed) : '';
|
$embedHtml = $embed !== null ? $this->renderEmbed($embed) : '';
|
||||||
|
|
||||||
$rootStyle = 'border:1px solid #cfd9de;border-radius:12px;padding:12px 16px;margin-bottom:12px;';
|
$rootStyle = 'border:1px solid #cfd9de;border-radius:12px;padding:12px 16px;margin-bottom:12px;';
|
||||||
$replyStyle = 'padding:10px 12px;margin:4px 0;';
|
$replyStyle = 'padding:10px 12px;margin:8px 0;';
|
||||||
$style = $isRoot ? $rootStyle : $replyStyle;
|
$style = $isRoot ? $rootStyle : $replyStyle;
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
@@ -367,17 +376,14 @@ final class BlueskyThreadsExtension extends Minz_Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<div style="border:1px solid #0085ff;border-radius:12px;padding:10px 12px;margin-top:8px;">
|
<blockquote style="border:1px solid #0085ff;border-radius:12px;padding:10px 12px;margin:8px 0 0 0;">
|
||||||
<div style="font-size:0.85em;margin-bottom:6px;">
|
<div style="font-size:0.85em;margin-bottom:6px;">
|
||||||
<strong>{$displayName}</strong>
|
<strong>{$displayName}</strong>
|
||||||
<span style="color:#536471;margin-left:4px;">@{$this->e($handle)}</span>
|
<a href="{$this->e($postUrl)}" style="color:#536471;margin-left:4px;text-decoration:none;">@{$this->e($handle)}</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="white-space:pre-wrap;word-break:break-word;">{$text}</div>
|
<div style="white-space:pre-wrap;word-break:break-word;">{$text}</div>
|
||||||
{$embedsHtml}
|
{$embedsHtml}
|
||||||
<div style="margin-top:6px;font-size:0.75em;">
|
</blockquote>
|
||||||
<a href="{$this->e($postUrl)}" style="color:#536471;">View on Bluesky ↗</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,14 +164,34 @@ class BlueskyThreadsTest extends TestCase {
|
|||||||
'Quoted post text must be rendered as an embed');
|
'Quoted post text must be rendered as an embed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Quoted posts include a "View on Bluesky" link; thread posts do not. */
|
/** Quoted posts use a blockquote with a handle permalink; thread posts have no permalink. */
|
||||||
public function testViewOnBskyLinksArePresent(): void {
|
public function testViewOnBskyLinksArePresent(): void {
|
||||||
$data = json_decode(file_get_contents(__DIR__ . '/fixtures/thread_3mhtk7awhrp26.json'), true);
|
$data = json_decode(file_get_contents(__DIR__ . '/fixtures/thread_3mhtk7awhrp26.json'), true);
|
||||||
$html = $this->call('renderThread', $data['thread'], true);
|
$html = $this->call('renderThread', $data['thread'], true);
|
||||||
|
|
||||||
$this->assertStringNotContainsString('3mhtk7awhrp26', $html, 'Thread post rkey must not appear as a permalink');
|
$this->assertStringNotContainsString('3mhtk7awhrp26', $html, 'Thread post rkey must not appear as a permalink');
|
||||||
$this->assertStringContainsString('3mhtjo3rtkn26', $html, 'Quoted post rkey must appear in its View on Bluesky link');
|
$this->assertStringContainsString('3mhtjo3rtkn26', $html, 'Quoted post rkey must appear in the handle permalink');
|
||||||
$this->assertStringContainsString('View on Bluesky', $html);
|
$this->assertStringContainsString('<blockquote', $html, 'Quoted post must be wrapped in a blockquote');
|
||||||
|
$this->assertStringNotContainsString('View on Bluesky', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A post with only a reply from another user (3mhypinzezo2l fixture) must
|
||||||
|
* render just the root post — the other-author reply is filtered out.
|
||||||
|
*/
|
||||||
|
public function testOtherAuthorRepliesAreExcluded(): void {
|
||||||
|
$data = json_decode(file_get_contents(__DIR__ . '/fixtures/thread_3mhypinzezo2l.json'), true);
|
||||||
|
$thread = $data['thread'];
|
||||||
|
$rootAuthorDid = $thread['post']['author']['did'];
|
||||||
|
|
||||||
|
$html = $this->call('renderThread', $thread, true, $rootAuthorDid);
|
||||||
|
|
||||||
|
$this->assertSame(1, substr_count($html, 'bsky-post'),
|
||||||
|
'Only the root post should be rendered; other-author reply must be excluded');
|
||||||
|
$this->assertSame(0, substr_count($html, 'bsky-replies'),
|
||||||
|
'No replies wrapper should be rendered when all replies are from other authors');
|
||||||
|
$this->assertStringNotContainsString('lasagnazero.bsky.social', $html,
|
||||||
|
'Reply author handle must not appear in output');
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
93
tests/fixtures/thread_3mhypinzezo2l.json
vendored
Normal file
93
tests/fixtures/thread_3mhypinzezo2l.json
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"thread": {
|
||||||
|
"$type": "app.bsky.feed.defs#threadViewPost",
|
||||||
|
"post": {
|
||||||
|
"uri": "at://did:plc:d4324t32vfi5xzydqbh2qdj3/app.bsky.feed.post/3mhypinzezo2l",
|
||||||
|
"cid": "bafyreialfdaqkwykiinxzy3xb6qhcenuwsaxi7d6qwzx4zfdyhprzo4s2e",
|
||||||
|
"author": {
|
||||||
|
"did": "did:plc:d4324t32vfi5xzydqbh2qdj3",
|
||||||
|
"handle": "hockeyviz.com",
|
||||||
|
"displayName": "Micah McCurdy ",
|
||||||
|
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:d4324t32vfi5xzydqbh2qdj3/bafkreiddyjdi3hty4z73vdoyxja3hj5n7izrsdmeoif2ppkqidbt4nsluq",
|
||||||
|
"associated": {
|
||||||
|
"chat": {
|
||||||
|
"allowIncoming": "all"
|
||||||
|
},
|
||||||
|
"activitySubscription": {
|
||||||
|
"allowSubscriptions": "followers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"labels": [],
|
||||||
|
"createdAt": "2023-05-28T17:07:32.578Z"
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"$type": "app.bsky.feed.post",
|
||||||
|
"createdAt": "2026-03-26T23:01:26.959Z",
|
||||||
|
"facets": [],
|
||||||
|
"langs": [
|
||||||
|
"en"
|
||||||
|
],
|
||||||
|
"tags": [],
|
||||||
|
"text": "Any good musician can rerecord somebody else's song but what really makes a cover worth doing is that little note of jealousy nestled in the blanket of admiration. It's the same with reproving somebody else's result or redoing a viz. Shoulda been me, the grudgingist yet in fact highest respect."
|
||||||
|
},
|
||||||
|
"bookmarkCount": 0,
|
||||||
|
"replyCount": 1,
|
||||||
|
"repostCount": 0,
|
||||||
|
"likeCount": 21,
|
||||||
|
"quoteCount": 0,
|
||||||
|
"indexedAt": "2026-03-26T23:01:27.160Z",
|
||||||
|
"labels": []
|
||||||
|
},
|
||||||
|
"replies": [
|
||||||
|
{
|
||||||
|
"$type": "app.bsky.feed.defs#threadViewPost",
|
||||||
|
"post": {
|
||||||
|
"uri": "at://did:plc:vfi77nom5cwpnhurnxhy7ltj/app.bsky.feed.post/3mhyw2ijjc22a",
|
||||||
|
"cid": "bafyreif6evbxbqcf5t3t6i4apm6pertxy4mylleet6a2d2fgzwqnz6inly",
|
||||||
|
"author": {
|
||||||
|
"did": "did:plc:vfi77nom5cwpnhurnxhy7ltj",
|
||||||
|
"handle": "lasagnazero.bsky.social",
|
||||||
|
"displayName": "L0",
|
||||||
|
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:vfi77nom5cwpnhurnxhy7ltj/bafkreigjurbghqtq3iogsqarnxldhl54zqdmi657pflwrrqwa2wisteq6i",
|
||||||
|
"associated": {
|
||||||
|
"activitySubscription": {
|
||||||
|
"allowSubscriptions": "followers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"labels": [],
|
||||||
|
"createdAt": "2024-11-07T14:26:50.985Z"
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"$type": "app.bsky.feed.post",
|
||||||
|
"createdAt": "2026-03-27T00:58:47.713Z",
|
||||||
|
"langs": [
|
||||||
|
"en",
|
||||||
|
"ru"
|
||||||
|
],
|
||||||
|
"reply": {
|
||||||
|
"parent": {
|
||||||
|
"cid": "bafyreialfdaqkwykiinxzy3xb6qhcenuwsaxi7d6qwzx4zfdyhprzo4s2e",
|
||||||
|
"uri": "at://did:plc:d4324t32vfi5xzydqbh2qdj3/app.bsky.feed.post/3mhypinzezo2l"
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"cid": "bafyreialfdaqkwykiinxzy3xb6qhcenuwsaxi7d6qwzx4zfdyhprzo4s2e",
|
||||||
|
"uri": "at://did:plc:d4324t32vfi5xzydqbh2qdj3/app.bsky.feed.post/3mhypinzezo2l"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text": "Something about imitation and flattery\u2026"
|
||||||
|
},
|
||||||
|
"bookmarkCount": 0,
|
||||||
|
"replyCount": 0,
|
||||||
|
"repostCount": 0,
|
||||||
|
"likeCount": 0,
|
||||||
|
"quoteCount": 0,
|
||||||
|
"indexedAt": "2026-03-27T00:58:48.952Z",
|
||||||
|
"labels": []
|
||||||
|
},
|
||||||
|
"replies": [],
|
||||||
|
"threadContext": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"threadContext": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user