Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

various better additions

+78 -7
+10 -3
web/src/components/common/Card.tsx
··· 1 import React, { useState } from "react"; 2 import { formatDistanceToNow } from "date-fns"; 3 import { 4 MessageSquare, 5 Heart, ··· 138 (pageUrl ? safeUrlHostname(pageUrl) : null); 139 const pageHostname = pageUrl 140 ? safeUrlHostname(pageUrl)?.replace("www.", "") 141 : null; 142 const isBookmark = type === "bookmark"; 143 ··· 284 className="inline-flex items-center gap-1 text-xs text-primary-600 dark:text-primary-400 hover:underline mt-0.5" 285 > 286 <ExternalLink size={10} /> 287 - {pageHostname} 288 </a> 289 )} 290 </div> ··· 334 )} 335 </div> 336 <span className="truncate max-w-[200px]"> 337 - {pageHostname || pageUrl} 338 </span> 339 </div> 340 </div> ··· 388 389 {item.body?.value && ( 390 <p className="text-surface-900 dark:text-surface-100 whitespace-pre-wrap leading-relaxed text-[15px]"> 391 - {item.body.value} 392 </p> 393 )} 394 </div>
··· 1 import React, { useState } from "react"; 2 import { formatDistanceToNow } from "date-fns"; 3 + import RichText from "./RichText"; 4 import { 5 MessageSquare, 6 Heart, ··· 139 (pageUrl ? safeUrlHostname(pageUrl) : null); 140 const pageHostname = pageUrl 141 ? safeUrlHostname(pageUrl)?.replace("www.", "") 142 + : null; 143 + const displayUrl = pageUrl 144 + ? pageUrl 145 + .replace(/^https?:\/\//, "") 146 + .replace(/^www\./, "") 147 + .replace(/\/$/, "") 148 : null; 149 const isBookmark = type === "bookmark"; 150 ··· 291 className="inline-flex items-center gap-1 text-xs text-primary-600 dark:text-primary-400 hover:underline mt-0.5" 292 > 293 <ExternalLink size={10} /> 294 + {displayUrl} 295 </a> 296 )} 297 </div> ··· 341 )} 342 </div> 343 <span className="truncate max-w-[200px]"> 344 + {displayUrl || pageUrl} 345 </span> 346 </div> 347 </div> ··· 395 396 {item.body?.value && ( 397 <p className="text-surface-900 dark:text-surface-100 whitespace-pre-wrap leading-relaxed text-[15px]"> 398 + <RichText text={item.body.value} /> 399 </p> 400 )} 401 </div>
+3 -2
web/src/components/common/ProfileHoverCard.tsx
··· 1 import React, { useState, useEffect, useRef } from "react"; 2 import { Link } from "react-router-dom"; 3 import Avatar from "../ui/Avatar"; 4 import { getProfile } from "../../api/client"; 5 import type { UserProfile } from "../../types"; 6 import { Loader2 } from "lucide-react"; ··· 134 </Link> 135 136 {profile.description && ( 137 - <p className="text-sm text-surface-600 dark:text-surface-300 line-clamp-3"> 138 - {profile.description} 139 </p> 140 )} 141
··· 1 import React, { useState, useEffect, useRef } from "react"; 2 import { Link } from "react-router-dom"; 3 import Avatar from "../ui/Avatar"; 4 + import RichText from "./RichText"; 5 import { getProfile } from "../../api/client"; 6 import type { UserProfile } from "../../types"; 7 import { Loader2 } from "lucide-react"; ··· 135 </Link> 136 137 {profile.description && ( 138 + <p className="text-sm text-surface-600 dark:text-surface-300 whitespace-pre-line line-clamp-3"> 139 + <RichText text={profile.description} /> 140 </p> 141 )} 142
+53
web/src/components/common/RichText.tsx
···
··· 1 + import React from "react"; 2 + import { Link } from "react-router-dom"; 3 + 4 + interface RichTextProps { 5 + text: string; 6 + className?: string; 7 + } 8 + 9 + const MENTION_REGEX = 10 + /(^|[\s(])@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)/g; 11 + 12 + export default function RichText({ text, className }: RichTextProps) { 13 + const parts: React.ReactNode[] = []; 14 + let lastIndex = 0; 15 + 16 + for (const match of text.matchAll(MENTION_REGEX)) { 17 + const fullMatch = match[0]; 18 + const prefix = match[1]; 19 + const handle = match[2]; 20 + const startIndex = match.index!; 21 + 22 + if (startIndex > lastIndex) { 23 + parts.push(text.slice(lastIndex, startIndex)); 24 + } 25 + 26 + if (prefix) { 27 + parts.push(prefix); 28 + } 29 + 30 + parts.push( 31 + <Link 32 + key={startIndex} 33 + to={`/profile/${handle}`} 34 + className="text-primary-600 dark:text-primary-400 hover:underline" 35 + onClick={(e) => e.stopPropagation()} 36 + > 37 + @{handle} 38 + </Link>, 39 + ); 40 + 41 + lastIndex = startIndex + fullMatch.length; 42 + } 43 + 44 + if (lastIndex < text.length) { 45 + parts.push(text.slice(lastIndex)); 46 + } 47 + 48 + if (parts.length === 0) { 49 + return <span className={className}>{text}</span>; 50 + } 51 + 52 + return <span className={className}>{parts}</span>; 53 + }
+12 -2
web/src/views/profile/Profile.tsx
··· 1 import React, { useEffect, useState } from "react"; 2 import { getProfile, getFeed, getCollections } from "../../api/client"; 3 import Card from "../../components/common/Card"; 4 import { 5 Edit2, 6 Github, ··· 123 }, []); 124 125 useEffect(() => { 126 const loadTabContent = async () => { 127 const isHandle = !did.startsWith("did:"); 128 const resolvedDid = isHandle ? profile?.did : did; ··· 244 </div> 245 246 {profile.description && ( 247 - <p className="text-surface-600 dark:text-surface-300 text-sm mt-3 line-clamp-2"> 248 - {profile.description} 249 </p> 250 )} 251
··· 1 import React, { useEffect, useState } from "react"; 2 import { getProfile, getFeed, getCollections } from "../../api/client"; 3 import Card from "../../components/common/Card"; 4 + import RichText from "../../components/common/RichText"; 5 import { 6 Edit2, 7 Github, ··· 124 }, []); 125 126 useEffect(() => { 127 + setProfile(null); 128 + setAnnotations([]); 129 + setHighlights([]); 130 + setBookmarks([]); 131 + setCollections([]); 132 + setActiveTab("annotations"); 133 + }, [did]); 134 + 135 + useEffect(() => { 136 const loadTabContent = async () => { 137 const isHandle = !did.startsWith("did:"); 138 const resolvedDid = isHandle ? profile?.did : did; ··· 254 </div> 255 256 {profile.description && ( 257 + <p className="text-surface-600 dark:text-surface-300 text-sm mt-3 whitespace-pre-line"> 258 + <RichText text={profile.description} /> 259 </p> 260 )} 261