···
1
1
"use client";
2
2
3
3
-
import { use, createContext } from "react";
3
3
+
import { use, createContext, lazy } from "react";
4
4
import type { ExternalEmbed } from "@/data/queries";
5
5
import { LinkPreview } from "./LinkPreview";
6
6
-
import { BlueskyPostEmbed } from "./embeds/BlueskyPostEmbed";
7
6
import { loadTrailCard } from "@/app/loadTrailCard";
8
8
-
import {
9
9
-
getBlueskyPost,
10
10
-
getLinkMetadata,
11
11
-
type BlueskyPost,
12
12
-
type LinkMetadata,
13
13
-
} from "./utils/embed-resolver";
7
7
+
import type { BlueskyPost, LinkMetadata } from "./utils/embed-resolver";
14
8
import "./embeds/TrailEmbed.css";
15
9
import "@/app/TrailCard.css";
10
10
+
11
11
+
const BlueskyPostEmbed = lazy(() =>
12
12
+
import("./embeds/BlueskyPostEmbed").then((m) => ({ default: m.BlueskyPostEmbed })),
13
13
+
);
14
14
+
15
15
+
async function fetchBlueskyPost(uri: string): Promise<BlueskyPost | null> {
16
16
+
const { getBlueskyPost } = await import("./utils/embed-resolver");
17
17
+
return getBlueskyPost(uri);
18
18
+
}
19
19
+
20
20
+
async function fetchLinkMetadata(uri: string): Promise<LinkMetadata | null> {
21
21
+
const { getLinkMetadata } = await import("./utils/embed-resolver");
22
22
+
return getLinkMetadata(uri);
23
23
+
}
16
24
17
25
type EmbedPromise = Promise<BlueskyPost | LinkMetadata | React.ReactElement | null>;
18
26
export type EmbedCache = Map<string, EmbedPromise>;
···
44
52
if (isTrailUri(uri)) {
45
53
promise = Promise.resolve().then(() => loadTrailCard(uri));
46
54
} else if (isBlueskyPostUri(uri)) {
47
47
-
promise = getBlueskyPost(uri);
55
55
+
promise = fetchBlueskyPost(uri);
48
56
} else {
49
49
-
promise = getLinkMetadata(uri);
57
57
+
promise = fetchLinkMetadata(uri);
50
58
}
51
59
52
60
cache.set(uri, promise);
···
1
1
"use client";
2
2
3
3
-
import { useRef, useState } from "react";
3
3
+
import { useRef, useState, lazy } from "react";
4
4
import {
5
5
parseEmbedPlayerFromUrl,
6
6
getPlayerAspect,
···
15
15
import * as embedVideo from "@/lib/lexicons/app/bsky/embed/video";
16
16
import * as embedRecord from "@/lib/lexicons/app/bsky/embed/record";
17
17
import * as embedRecordWithMedia from "@/lib/lexicons/app/bsky/embed/recordWithMedia";
18
18
-
import { BlueskyVideoPlayer } from "./BlueskyVideoPlayer";
19
18
import "./BlueskyPostEmbed.css";
19
19
+
20
20
+
const BlueskyVideoPlayer = lazy(() =>
21
21
+
import("./BlueskyVideoPlayer").then((m) => ({ default: m.BlueskyVideoPlayer })),
22
22
+
);
20
23
21
24
interface BlueskyPostEmbedProps {
22
25
post: BlueskyPost;
···
1
1
-
import { RichText } from "@atproto/api";
1
1
+
import { RichText } from "../utils/rich-text";
2
2
import type * as feedPost from "@/lib/lexicons/app/bsky/feed/post";
3
3
import * as facet from "@/lib/lexicons/app/bsky/richtext/facet";
4
4
···
1
1
+
/**
2
2
+
* Minimal RichText implementation for rendering Bluesky post text.
3
3
+
* Vendored from @atproto/api to avoid pulling in the full SDK (~150KB).
4
4
+
*
5
5
+
* Only implements segment iteration for read-only rendering.
6
6
+
* Does not include text manipulation or facet detection.
7
7
+
*/
8
8
+
9
9
+
import type * as facet from "@/lib/lexicons/app/bsky/richtext/facet";
10
10
+
11
11
+
export type Facet = facet.Main;
12
12
+
export type FacetLink = facet.Link;
13
13
+
export type FacetMention = facet.Mention;
14
14
+
export type FacetTag = facet.Tag;
15
15
+
16
16
+
type Feature = Facet["features"][number];
17
17
+
18
18
+
const encoder = new TextEncoder();
19
19
+
const decoder = new TextDecoder();
20
20
+
21
21
+
class UnicodeString {
22
22
+
utf16: string;
23
23
+
utf8: Uint8Array;
24
24
+
25
25
+
constructor(utf16: string) {
26
26
+
this.utf16 = utf16;
27
27
+
this.utf8 = encoder.encode(utf16);
28
28
+
}
29
29
+
30
30
+
get length() {
31
31
+
return this.utf8.byteLength;
32
32
+
}
33
33
+
34
34
+
slice(start?: number, end?: number): string {
35
35
+
return decoder.decode(this.utf8.slice(start, end));
36
36
+
}
37
37
+
}
38
38
+
39
39
+
function isLink(f: Feature): f is FacetLink & { $type: "app.bsky.richtext.facet#link" } {
40
40
+
return f.$type === "app.bsky.richtext.facet#link";
41
41
+
}
42
42
+
43
43
+
function isMention(f: Feature): f is FacetMention & { $type: "app.bsky.richtext.facet#mention" } {
44
44
+
return f.$type === "app.bsky.richtext.facet#mention";
45
45
+
}
46
46
+
47
47
+
function isTag(f: Feature): f is FacetTag & { $type: "app.bsky.richtext.facet#tag" } {
48
48
+
return f.$type === "app.bsky.richtext.facet#tag";
49
49
+
}
50
50
+
51
51
+
export class RichTextSegment {
52
52
+
constructor(
53
53
+
public text: string,
54
54
+
public facet?: Facet,
55
55
+
) {}
56
56
+
57
57
+
get link(): FacetLink | undefined {
58
58
+
const found = this.facet?.features.find(isLink);
59
59
+
return found ? { uri: found.uri } : undefined;
60
60
+
}
61
61
+
62
62
+
get mention(): FacetMention | undefined {
63
63
+
const found = this.facet?.features.find(isMention);
64
64
+
return found ? { did: found.did } : undefined;
65
65
+
}
66
66
+
67
67
+
get tag(): FacetTag | undefined {
68
68
+
const found = this.facet?.features.find(isTag);
69
69
+
return found ? { tag: found.tag } : undefined;
70
70
+
}
71
71
+
}
72
72
+
73
73
+
export interface RichTextProps {
74
74
+
text: string;
75
75
+
facets?: Facet[];
76
76
+
}
77
77
+
78
78
+
export class RichText {
79
79
+
private unicodeText: UnicodeString;
80
80
+
private facets?: Facet[];
81
81
+
82
82
+
constructor(props: RichTextProps) {
83
83
+
this.unicodeText = new UnicodeString(props.text);
84
84
+
if (props.facets?.length) {
85
85
+
this.facets = props.facets
86
86
+
.filter((f) => f.index.byteStart <= f.index.byteEnd)
87
87
+
.sort((a, b) => a.index.byteStart - b.index.byteStart);
88
88
+
}
89
89
+
}
90
90
+
91
91
+
*segments(): Generator<RichTextSegment, void, void> {
92
92
+
const facets = this.facets || [];
93
93
+
if (!facets.length) {
94
94
+
yield new RichTextSegment(this.unicodeText.utf16);
95
95
+
return;
96
96
+
}
97
97
+
98
98
+
let textCursor = 0;
99
99
+
let facetCursor = 0;
100
100
+
101
101
+
do {
102
102
+
const currFacet = facets[facetCursor];
103
103
+
if (textCursor < currFacet.index.byteStart) {
104
104
+
yield new RichTextSegment(this.unicodeText.slice(textCursor, currFacet.index.byteStart));
105
105
+
} else if (textCursor > currFacet.index.byteStart) {
106
106
+
facetCursor++;
107
107
+
continue;
108
108
+
}
109
109
+
if (currFacet.index.byteStart < currFacet.index.byteEnd) {
110
110
+
const subtext = this.unicodeText.slice(currFacet.index.byteStart, currFacet.index.byteEnd);
111
111
+
if (!subtext.trim()) {
112
112
+
yield new RichTextSegment(subtext);
113
113
+
} else {
114
114
+
yield new RichTextSegment(subtext, currFacet);
115
115
+
}
116
116
+
}
117
117
+
textCursor = currFacet.index.byteEnd;
118
118
+
facetCursor++;
119
119
+
} while (facetCursor < facets.length);
120
120
+
121
121
+
if (textCursor < this.unicodeText.length) {
122
122
+
yield new RichTextSegment(this.unicodeText.slice(textCursor, this.unicodeText.length));
123
123
+
}
124
124
+
}
125
125
+
}