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