chore: comment refinements
30d12405
3 file(s) · +72 −61
| 139 | 139 | <div className="flex gap-3 flex-wrap"> |
|
| 140 | 140 | <button |
|
| 141 | 141 | onClick={handleLogin} |
|
| 142 | - | className="px-4 py-2 border border-white hover:bg-white hover:text-black transition-colors text-sm" |
|
| 142 | + | className="px-2 py-0.5 border border-white hover:border-gray-400 hover:text-gray-400 transition-colors text-xs" |
|
| 143 | 143 | > |
|
| 144 | 144 | Sign in with ATProto |
|
| 145 | 145 | </button> |
|
| 146 | 146 | <a |
|
| 147 | 147 | href={mailtoLink} |
|
| 148 | - | className="px-4 py-2 border border-white hover:bg-white hover:text-black transition-colors text-sm inline-block" |
|
| 148 | + | className="px-4 py-0.5 border border-white hover:border-gray-400 hover:text-gray-400 transition-colors text-xs" |
|
| 149 | 149 | > |
|
| 150 | 150 | Reply via Email |
|
| 151 | 151 | </a> |
|
| 179 | 179 | <div className="flex items-center gap-3"> |
|
| 180 | 180 | {error && <span className="text-sm text-red-500">{error}</span>} |
|
| 181 | 181 | {success && ( |
|
| 182 | - | <span className="text-sm text-green-500"> |
|
| 182 | + | <span className="text-sm text-white"> |
|
| 183 | 183 | Reply posted successfully! |
|
| 184 | 184 | </span> |
|
| 185 | 185 | )} |
|
| 186 | 186 | </div> |
|
| 187 | 187 | ||
| 188 | 188 | <div className="flex gap-3"> |
|
| 189 | - | <a |
|
| 190 | - | href={mailtoLink} |
|
| 191 | - | className="text-sm text-gray-500 hover:text-gray-300 transition-colors" |
|
| 192 | - | > |
|
| 193 | - | or email |
|
| 194 | - | </a> |
|
| 195 | 189 | <button |
|
| 196 | 190 | type="submit" |
|
| 197 | 191 | disabled={isSubmitting || !replyContent.trim()} |
|
| 198 | - | className="px-4 py-2 border border-white hover:bg-white hover:text-black disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm" |
|
| 192 | + | className="px-4 py-0.5 border border-white hover:border-gray-400 hover:text-gray-400 disabled:border-opacity-50 disabled:cursor-not-allowed transition-colors text-xs" |
|
| 199 | 193 | > |
|
| 200 | 194 | {isSubmitting ? "Posting..." : "Post Reply"} |
|
| 201 | 195 | </button> |
|
| 106 | 106 | ||
| 107 | 107 | return ( |
|
| 108 | 108 | <div className="mt-8"> |
|
| 109 | - | <h3 className="text-lg font-bold mb-4">Replies ({replies.length})</h3> |
|
| 110 | - | <div className="space-y-4"> |
|
| 109 | + | <h3 className="text-lg font-bold mb-4">Replies</h3> |
|
| 110 | + | <div className="space-y-6"> |
|
| 111 | 111 | {replies.map((reply) => ( |
|
| 112 | - | <div |
|
| 113 | - | key={reply.uri} |
|
| 114 | - | className="border border-gray-700 rounded-lg p-4 hover:border-gray-600 transition-colors" |
|
| 115 | - | > |
|
| 112 | + | <div key={reply.uri}> |
|
| 116 | 113 | <div className="flex items-start gap-3"> |
|
| 117 | 114 | {reply.author.avatar ? ( |
|
| 118 | 115 | <img |
|
| 133 | 130 | <span className="font-semibold text-sm"> |
|
| 134 | 131 | {reply.author.displayName || reply.author.handle} |
|
| 135 | 132 | </span> |
|
| 133 | + | {reply.author.displayName && ( |
|
| 134 | + | <a |
|
| 135 | + | href={`https://pdsls.dev/at://${reply.author.did}`} |
|
| 136 | + | target="_blank" |
|
| 137 | + | rel="noopener noreferrer" |
|
| 138 | + | className="text-xs text-gray-400 hover:text-gray-300" |
|
| 139 | + | > |
|
| 140 | + | @{reply.author.handle} |
|
| 141 | + | </a> |
|
| 142 | + | )} |
|
| 136 | 143 | <a |
|
| 137 | - | href={`https://bsky.app/profile/${reply.author.handle}`} |
|
| 138 | - | target="_blank" |
|
| 139 | - | rel="noopener noreferrer" |
|
| 144 | + | href={`https://pdsls.dev/${reply.uri}`} |
|
| 140 | 145 | className="text-xs text-gray-400 hover:text-gray-300" |
|
| 141 | 146 | > |
|
| 142 | - | @{reply.author.handle} |
|
| 147 | + | {formatDate(reply.record.createdAt)} |
|
| 143 | 148 | </a> |
|
| 144 | - | <span className="text-xs text-gray-500"> |
|
| 145 | - | {formatDate(reply.record.createdAt)} |
|
| 146 | - | </span> |
|
| 147 | 149 | </div> |
|
| 148 | 150 | ||
| 149 | 151 | <p className="mt-2 text-sm whitespace-pre-wrap break-words"> |
|
| 150 | 152 | {reply.record.text} |
|
| 151 | 153 | </p> |
|
| 152 | 154 | ||
| 153 | - | <div className="mt-3 flex items-center gap-4 text-xs text-gray-500"> |
|
| 154 | - | {reply.replyCount > 0 && ( |
|
| 155 | - | <span>{reply.replyCount} replies</span> |
|
| 156 | - | )} |
|
| 157 | - | {reply.likeCount > 0 && <span>{reply.likeCount} likes</span>} |
|
| 158 | - | <a |
|
| 159 | - | href={`https://bsky.app/profile/${reply.author.handle}/post/${reply.uri.split("/").pop()}`} |
|
| 160 | - | target="_blank" |
|
| 161 | - | rel="noopener noreferrer" |
|
| 162 | - | className="hover:text-gray-300" |
|
| 163 | - | > |
|
| 164 | - | View on Bluesky |
|
| 165 | - | </a> |
|
| 166 | - | </div> |
|
| 155 | + | {reply.replyCount > 0 && ( |
|
| 156 | + | <div className="mt-3 flex items-center gap-4 text-xs text-gray-500"> |
|
| 157 | + | {reply.replyCount > 0 && ( |
|
| 158 | + | <span>{reply.replyCount} replies</span> |
|
| 159 | + | )} |
|
| 160 | + | </div> |
|
| 161 | + | )} |
|
| 167 | 162 | </div> |
|
| 168 | 163 | </div> |
|
| 169 | 164 | </div> |
|
| 395 | 395 | return c.json({ error: "Failed to fetch parent post" }, 400); |
|
| 396 | 396 | } |
|
| 397 | 397 | ||
| 398 | - | const parentData = await parentResponse.json(); |
|
| 398 | + | const parentData = (await parentResponse.json()) as { cid: string }; |
|
| 399 | 399 | const parentCid = parentData.cid; |
|
| 400 | 400 | ||
| 401 | + | // Fetch author profile to get handle, displayName, and avatar from Bluesky public API |
|
| 402 | + | let authorHandle = session.did; |
|
| 403 | + | let authorDisplayName: string | undefined; |
|
| 404 | + | let authorAvatar: string | undefined; |
|
| 405 | + | ||
| 406 | + | try { |
|
| 407 | + | const profileUrl = `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${session.did}`; |
|
| 408 | + | const profileResponse = await fetch(profileUrl); |
|
| 409 | + | if (profileResponse.ok) { |
|
| 410 | + | const profileData = (await profileResponse.json()) as { |
|
| 411 | + | handle?: string; |
|
| 412 | + | displayName?: string; |
|
| 413 | + | avatar?: string; |
|
| 414 | + | }; |
|
| 415 | + | authorHandle = profileData.handle || session.did; |
|
| 416 | + | authorDisplayName = profileData.displayName; |
|
| 417 | + | authorAvatar = profileData.avatar; |
|
| 418 | + | } |
|
| 419 | + | } catch (err) { |
|
| 420 | + | console.error("Failed to fetch author profile:", err); |
|
| 421 | + | } |
|
| 422 | + | ||
| 401 | 423 | // Create the comment record using site.standard.document.comment lexicon |
|
| 402 | 424 | const createRecordUrl = `${c.env.PDS_URL}/xrpc/com.atproto.repo.createRecord`; |
|
| 403 | 425 | ||
| 415 | 437 | cid: parentCid, |
|
| 416 | 438 | }, |
|
| 417 | 439 | content: body.content.trim(), |
|
| 440 | + | author: { |
|
| 441 | + | did: session.did, |
|
| 442 | + | handle: authorHandle, |
|
| 443 | + | ...(authorDisplayName && { displayName: authorDisplayName }), |
|
| 444 | + | ...(authorAvatar && { avatar: authorAvatar }), |
|
| 445 | + | }, |
|
| 418 | 446 | createdAt: new Date().toISOString(), |
|
| 419 | 447 | }, |
|
| 420 | 448 | }; |
|
| 503 | 531 | return c.json({ replies: [] }); |
|
| 504 | 532 | } |
|
| 505 | 533 | ||
| 506 | - | const parentData = await parentResponse.json(); |
|
| 534 | + | const parentData = (await parentResponse.json()) as { cid: string }; |
|
| 507 | 535 | const parentCid = parentData.cid; |
|
| 508 | 536 | ||
| 509 | 537 | // Fetch all site.standard.document.comment records |
|
| 524 | 552 | return c.json({ replies: [] }); |
|
| 525 | 553 | } |
|
| 526 | 554 | ||
| 527 | - | const data = await response.json(); |
|
| 555 | + | interface CommentRecord { |
|
| 556 | + | uri: string; |
|
| 557 | + | cid: string; |
|
| 558 | + | value: { |
|
| 559 | + | parent?: { uri?: string; cid?: string }; |
|
| 560 | + | content: string; |
|
| 561 | + | createdAt: string; |
|
| 562 | + | author?: { handle?: string; displayName?: string; avatar?: string }; |
|
| 563 | + | }; |
|
| 564 | + | indexedAt?: string; |
|
| 565 | + | } |
|
| 566 | + | ||
| 567 | + | const data = (await response.json()) as { records: CommentRecord[] }; |
|
| 528 | 568 | ||
| 529 | 569 | // Filter comments that match the parent URI |
|
| 530 | 570 | const replies: any[] = []; |
|
| 533 | 573 | const comment = record.value; |
|
| 534 | 574 | // Check if this comment's parent matches our URI |
|
| 535 | 575 | if (comment.parent?.uri === uri || comment.parent?.cid === parentCid) { |
|
| 536 | - | // Fetch author profile info |
|
| 537 | - | let handle = record.uri.split("/")[2]; // DID as fallback |
|
| 538 | - | let displayName = undefined; |
|
| 539 | - | let avatar = undefined; |
|
| 540 | - | ||
| 541 | - | try { |
|
| 542 | - | const profileUrl = `${PDS_URL}/xrpc/app.bsky.actor.getProfile?actor=${handle}`; |
|
| 543 | - | const profileResponse = await fetch(profileUrl); |
|
| 544 | - | if (profileResponse.ok) { |
|
| 545 | - | const profileData = await profileResponse.json(); |
|
| 546 | - | handle = profileData.handle || handle; |
|
| 547 | - | displayName = profileData.displayName; |
|
| 548 | - | avatar = profileData.avatar; |
|
| 549 | - | } |
|
| 550 | - | } catch (err) { |
|
| 551 | - | console.error("Failed to fetch profile:", err); |
|
| 552 | - | } |
|
| 553 | - | ||
| 554 | 576 | replies.push({ |
|
| 555 | 577 | uri: record.uri, |
|
| 556 | 578 | cid: record.cid, |
|
| 557 | 579 | author: { |
|
| 558 | 580 | did: record.uri.split("/")[2], |
|
| 559 | - | handle: handle, |
|
| 560 | - | displayName: displayName, |
|
| 561 | - | avatar: avatar, |
|
| 581 | + | handle: comment.author?.handle || record.uri.split("/")[2], |
|
| 582 | + | displayName: comment.author?.displayName, |
|
| 583 | + | avatar: comment.author?.avatar, |
|
| 562 | 584 | }, |
|
| 563 | 585 | record: { |
|
| 564 | 586 | text: comment.content, |
|