tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
28
pulls
pipelines
use linkify js for urls in profiles
awarm.space
3 months ago
b4cca5ca
c19d446e
+58
-15
1 changed file
expand all
collapse all
unified
split
app
(home-pages)
p
[didOrHandle]
ProfileHeader.tsx
+58
-15
app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
···
11
11
import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
12
12
import { SpeedyLink } from "components/SpeedyLink";
13
13
import { ReactNode } from "react";
14
14
+
import * as linkify from "linkifyjs";
14
15
15
16
export const ProfileHeader = (props: {
16
17
profile: ProfileViewDetailed;
···
149
150
};
150
151
151
152
function parseDescription(description: string): ReactNode[] {
152
152
-
const combinedRegex = /(@\S+|https?:\/\/\S+)/g;
153
153
+
// Find all mentions using regex
154
154
+
const mentionRegex = /@\S+/g;
155
155
+
const mentions: { start: number; end: number; value: string }[] = [];
156
156
+
let mentionMatch;
157
157
+
while ((mentionMatch = mentionRegex.exec(description)) !== null) {
158
158
+
mentions.push({
159
159
+
start: mentionMatch.index,
160
160
+
end: mentionMatch.index + mentionMatch[0].length,
161
161
+
value: mentionMatch[0],
162
162
+
});
163
163
+
}
164
164
+
165
165
+
// Find all URLs using linkifyjs
166
166
+
const links = linkify.find(description).filter((link) => link.type === "url");
167
167
+
168
168
+
// Filter out URLs that overlap with mentions (mentions take priority)
169
169
+
const nonOverlappingLinks = links.filter((link) => {
170
170
+
return !mentions.some(
171
171
+
(mention) =>
172
172
+
(link.start >= mention.start && link.start < mention.end) ||
173
173
+
(link.end > mention.start && link.end <= mention.end) ||
174
174
+
(link.start <= mention.start && link.end >= mention.end),
175
175
+
);
176
176
+
});
177
177
+
178
178
+
// Combine into a single sorted list
179
179
+
const allMatches: Array<{
180
180
+
start: number;
181
181
+
end: number;
182
182
+
value: string;
183
183
+
href: string;
184
184
+
type: "url" | "mention";
185
185
+
}> = [
186
186
+
...nonOverlappingLinks.map((link) => ({
187
187
+
start: link.start,
188
188
+
end: link.end,
189
189
+
value: link.value,
190
190
+
href: link.href,
191
191
+
type: "url" as const,
192
192
+
})),
193
193
+
...mentions.map((mention) => ({
194
194
+
start: mention.start,
195
195
+
end: mention.end,
196
196
+
value: mention.value,
197
197
+
href: `/p/${mention.value.slice(1)}`,
198
198
+
type: "mention" as const,
199
199
+
})),
200
200
+
].sort((a, b) => a.start - b.start);
153
201
154
202
const parts: ReactNode[] = [];
155
203
let lastIndex = 0;
156
156
-
let match;
157
204
let key = 0;
158
205
159
159
-
while ((match = combinedRegex.exec(description)) !== null) {
206
206
+
for (const match of allMatches) {
160
207
// Add text before this match
161
161
-
if (match.index > lastIndex) {
162
162
-
parts.push(description.slice(lastIndex, match.index));
208
208
+
if (match.start > lastIndex) {
209
209
+
parts.push(description.slice(lastIndex, match.start));
163
210
}
164
211
165
165
-
const matched = match[0];
166
166
-
167
167
-
if (matched.startsWith("@")) {
168
168
-
// It's a mention
169
169
-
const handle = matched.slice(1);
212
212
+
if (match.type === "mention") {
170
213
parts.push(
171
171
-
<SpeedyLink key={key++} href={`/p/${handle}`}>
172
172
-
{matched}
214
214
+
<SpeedyLink key={key++} href={match.href}>
215
215
+
{match.value}
173
216
</SpeedyLink>,
174
217
);
175
218
} else {
176
219
// It's a URL
177
177
-
const urlWithoutProtocol = matched
220
220
+
const urlWithoutProtocol = match.value
178
221
.replace(/^https?:\/\//, "")
179
222
.replace(/\/+$/, "");
180
223
const displayText =
···
182
225
? urlWithoutProtocol.slice(0, 50) + "…"
183
226
: urlWithoutProtocol;
184
227
parts.push(
185
185
-
<a key={key++} href={matched} target="_blank" rel="noopener noreferrer">
228
228
+
<a key={key++} href={match.href} target="_blank" rel="noopener noreferrer">
186
229
{displayText}
187
230
</a>,
188
231
);
189
232
}
190
233
191
191
-
lastIndex = match.index + matched.length;
234
234
+
lastIndex = match.end;
192
235
}
193
236
194
237
// Add remaining text after last match