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
add pubicon to atmention mark
awarm.space
3 months ago
76f18bcd
86f92eed
+154
-1
2 changed files
expand all
collapse all
unified
split
app
api
pub_icon
route.ts
components
Blocks
TextBlock
schema.ts
+121
app/api/pub_icon/route.ts
···
1
1
+
import { AtUri } from "@atproto/syntax";
2
2
+
import { IdResolver } from "@atproto/identity";
3
3
+
import { NextRequest, NextResponse } from "next/server";
4
4
+
import { PubLeafletPublication } from "lexicons/api";
5
5
+
import { supabaseServerClient } from "supabase/serverClient";
6
6
+
import sharp from "sharp";
7
7
+
8
8
+
const idResolver = new IdResolver();
9
9
+
10
10
+
export const runtime = "nodejs";
11
11
+
12
12
+
export async function GET(req: NextRequest) {
13
13
+
try {
14
14
+
const searchParams = req.nextUrl.searchParams;
15
15
+
const at_uri = searchParams.get("at_uri");
16
16
+
17
17
+
if (!at_uri) {
18
18
+
return new NextResponse(null, { status: 400 });
19
19
+
}
20
20
+
21
21
+
// Parse the AT URI
22
22
+
let uri: AtUri;
23
23
+
try {
24
24
+
uri = new AtUri(at_uri);
25
25
+
} catch (e) {
26
26
+
return new NextResponse(null, { status: 400 });
27
27
+
}
28
28
+
29
29
+
let publicationRecord: PubLeafletPublication.Record | null = null;
30
30
+
let publicationUri: string;
31
31
+
32
32
+
// Check if it's a document or publication
33
33
+
if (uri.collection === "pub.leaflet.document") {
34
34
+
// Query the documents_in_publications table to get the publication
35
35
+
const { data: docInPub } = await supabaseServerClient
36
36
+
.from("documents_in_publications")
37
37
+
.select("publication, publications(record)")
38
38
+
.eq("document", at_uri)
39
39
+
.single();
40
40
+
41
41
+
if (!docInPub || !docInPub.publications) {
42
42
+
return new NextResponse(null, { status: 404 });
43
43
+
}
44
44
+
45
45
+
publicationUri = docInPub.publication;
46
46
+
publicationRecord = docInPub.publications.record as PubLeafletPublication.Record;
47
47
+
} else if (uri.collection === "pub.leaflet.publication") {
48
48
+
// Query the publications table directly
49
49
+
const { data: publication } = await supabaseServerClient
50
50
+
.from("publications")
51
51
+
.select("record, uri")
52
52
+
.eq("uri", at_uri)
53
53
+
.single();
54
54
+
55
55
+
if (!publication || !publication.record) {
56
56
+
return new NextResponse(null, { status: 404 });
57
57
+
}
58
58
+
59
59
+
publicationUri = publication.uri;
60
60
+
publicationRecord = publication.record as PubLeafletPublication.Record;
61
61
+
} else {
62
62
+
// Not a supported collection
63
63
+
return new NextResponse(null, { status: 404 });
64
64
+
}
65
65
+
66
66
+
// Check if the publication has an icon
67
67
+
if (!publicationRecord?.icon) {
68
68
+
return new NextResponse(null, { status: 404 });
69
69
+
}
70
70
+
71
71
+
// Parse the publication URI to get the DID
72
72
+
const pubUri = new AtUri(publicationUri);
73
73
+
74
74
+
// Get the CID from the icon blob
75
75
+
const cid = (publicationRecord.icon.ref as unknown as { $link: string })["$link"];
76
76
+
77
77
+
// Fetch the blob from the PDS
78
78
+
const identity = await idResolver.did.resolve(pubUri.host);
79
79
+
const service = identity?.service?.find((f) => f.id === "#atproto_pds");
80
80
+
if (!service) return new NextResponse(null, { status: 404 });
81
81
+
82
82
+
const blobResponse = await fetch(
83
83
+
`${service.serviceEndpoint}/xrpc/com.atproto.sync.getBlob?did=${pubUri.host}&cid=${cid}`,
84
84
+
{
85
85
+
headers: {
86
86
+
"Accept-Encoding": "gzip, deflate, br, zstd",
87
87
+
},
88
88
+
},
89
89
+
);
90
90
+
91
91
+
if (!blobResponse.ok) {
92
92
+
return new NextResponse(null, { status: 404 });
93
93
+
}
94
94
+
95
95
+
// Get the image buffer
96
96
+
const imageBuffer = await blobResponse.arrayBuffer();
97
97
+
98
98
+
// Resize to 96x96 using Sharp
99
99
+
const resizedImage = await sharp(Buffer.from(imageBuffer))
100
100
+
.resize(96, 96, {
101
101
+
fit: "cover",
102
102
+
position: "center",
103
103
+
})
104
104
+
.webp({ quality: 90 })
105
105
+
.toBuffer();
106
106
+
107
107
+
// Return with aggressive caching headers
108
108
+
return new NextResponse(resizedImage, {
109
109
+
headers: {
110
110
+
"Content-Type": "image/webp",
111
111
+
// Cache indefinitely on CDN, icons don't change
112
112
+
"Cache-Control":
113
113
+
"public, max-age=31536000, immutable, s-maxage=31536000, stale-while-revalidate=604800",
114
114
+
"CDN-Cache-Control": "s-maxage=31536000, stale-while-revalidate=604800",
115
115
+
},
116
116
+
});
117
117
+
} catch (error) {
118
118
+
console.error("Error fetching publication icon:", error);
119
119
+
return new NextResponse(null, { status: 500 });
120
120
+
}
121
121
+
}
+33
-1
components/Blocks/TextBlock/schema.ts
···
1
1
+
import { AtUri } from "@atproto/api";
1
2
import { Schema, Node, MarkSpec } from "prosemirror-model";
2
3
import { marks } from "prosemirror-schema-basic";
3
4
import { theme } from "tailwind.config";
···
119
120
},
120
121
],
121
122
toDOM(node) {
123
123
+
let className = "atMention text-accent-contrast";
124
124
+
let aturi = new AtUri(node.attrs.atURI);
125
125
+
if (aturi.collection === "pub.leaflet.publication")
126
126
+
className += " font-bold";
127
127
+
if (aturi.collection === "pub.leaflet.document") className += " italic";
128
128
+
129
129
+
// For publications and documents, show icon
130
130
+
if (
131
131
+
aturi.collection === "pub.leaflet.publication" ||
132
132
+
aturi.collection === "pub.leaflet.document"
133
133
+
) {
134
134
+
return [
135
135
+
"span",
136
136
+
{
137
137
+
class: className,
138
138
+
"data-at-uri": node.attrs.atURI,
139
139
+
},
140
140
+
[
141
141
+
"img",
142
142
+
{
143
143
+
src: `/api/pub_icon?at_uri=${encodeURIComponent(node.attrs.atURI)}`,
144
144
+
class:
145
145
+
"inline-block w-4 h-4 rounded-full ml-1 align-text-bottom",
146
146
+
alt: "Publication icon",
147
147
+
loading: "lazy",
148
148
+
},
149
149
+
],
150
150
+
["span", 0],
151
151
+
];
152
152
+
}
153
153
+
122
154
return [
123
155
"span",
124
156
{
125
125
-
class: "atMention text-accent-contrast",
157
157
+
class: className,
126
158
"data-at-uri": node.attrs.atURI,
127
159
},
128
160
0,