chore: comment refinements 30d12405
Steve · 2026-01-09 22:51 3 file(s) · +72 −61
packages/client/src/components/post/GuestReply.tsx +4 −10
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>
packages/client/src/components/post/ReplyList.tsx +22 −27
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>
packages/server/src/routes/now.ts +46 −24
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,