+3
-1
README.md
+3
-1
README.md
···
1
1
# atproto-ui
2
2
3
-
A React component library for rendering AT Protocol records (Bluesky, Leaflet, Tangled, and more). Handles DID resolution, PDS discovery, and record fetching automatically. [Live demo](https://atproto-ui.wisp.place).
3
+
A React component library for rendering AT Protocol records (Bluesky, Leaflet, Tangled, and more). Handles DID resolution, PDS discovery, and record fetching automatically. [Live demo](https://atproto-ui.netlify.app).
4
+
5
+
This project is mostly a wrapper on the extremely amazing work [Mary](https://mary.my.id/) has done with [atcute](https://tangled.org/@mary.my.id/atcute), please support it. I have to give thanks to [phil](https://bsky.app/profile/bad-example.com) for microcosm and slingshot. Incredible services being given for free that is responsible for why the components fetch data so quickly.
4
6
5
7
## Screenshots
6
8
+3
-1
lib/components/BlueskyPostList.tsx
+3
-1
lib/components/BlueskyPostList.tsx
···
8
8
import { useDidResolution } from "../hooks/useDidResolution";
9
9
import { BlueskyIcon } from "./BlueskyIcon";
10
10
import { parseAtUri } from "../utils/at-uri";
11
+
import { useAtProto } from "../providers/AtProtoProvider";
11
12
12
13
/**
13
14
* Options for rendering a paginated list of Bluesky posts.
···
215
216
replyParent,
216
217
hasDivider,
217
218
}) => {
219
+
const { blueskyAppBaseUrl } = useAtProto();
218
220
const text = record.text?.trim() ?? "";
219
221
const relative = record.createdAt
220
222
? formatRelativeTime(record.createdAt)
···
222
224
const absolute = record.createdAt
223
225
? new Date(record.createdAt).toLocaleString()
224
226
: undefined;
225
-
const href = `https://bsky.app/profile/${did}/post/${rkey}`;
227
+
const href = `${blueskyAppBaseUrl}/profile/${did}/post/${rkey}`;
226
228
const repostLabel =
227
229
reason?.$type === "app.bsky.feed.defs#reasonRepost"
228
230
? `${formatActor(reason.by) ?? "Someone"} reposted`
+7
-4
lib/components/RichText.tsx
+7
-4
lib/components/RichText.tsx
···
1
1
import React from "react";
2
2
import type { AppBskyRichtextFacet } from "@atcute/bluesky";
3
3
import { createTextSegments, type TextSegment } from "../utils/richtext";
4
+
import { useAtProto } from "../providers/AtProtoProvider";
4
5
5
6
export interface RichTextProps {
6
7
text: string;
···
13
14
* Properly handles byte offsets and multi-byte characters.
14
15
*/
15
16
export const RichText: React.FC<RichTextProps> = ({ text, facets, style }) => {
17
+
const { blueskyAppBaseUrl } = useAtProto();
16
18
const segments = createTextSegments(text, facets);
17
19
18
20
return (
19
21
<span style={style}>
20
22
{segments.map((segment, idx) => (
21
-
<RichTextSegment key={idx} segment={segment} />
23
+
<RichTextSegment key={idx} segment={segment} blueskyAppBaseUrl={blueskyAppBaseUrl} />
22
24
))}
23
25
</span>
24
26
);
···
26
28
27
29
interface RichTextSegmentProps {
28
30
segment: TextSegment;
31
+
blueskyAppBaseUrl: string;
29
32
}
30
33
31
-
const RichTextSegment: React.FC<RichTextSegmentProps> = ({ segment }) => {
34
+
const RichTextSegment: React.FC<RichTextSegmentProps> = ({ segment, blueskyAppBaseUrl }) => {
32
35
if (!segment.facet) {
33
36
return <>{segment.text}</>;
34
37
}
···
68
71
69
72
case "app.bsky.richtext.facet#mention": {
70
73
const mentionFeature = feature as AppBskyRichtextFacet.Mention;
71
-
const profileUrl = `https://bsky.app/profile/${mentionFeature.did}`;
74
+
const profileUrl = `${blueskyAppBaseUrl}/profile/${mentionFeature.did}`;
72
75
return (
73
76
<a
74
77
href={profileUrl}
···
92
95
93
96
case "app.bsky.richtext.facet#tag": {
94
97
const tagFeature = feature as AppBskyRichtextFacet.Tag;
95
-
const tagUrl = `https://bsky.app/hashtag/${encodeURIComponent(tagFeature.tag)}`;
98
+
const tagUrl = `${blueskyAppBaseUrl}/hashtag/${encodeURIComponent(tagFeature.tag)}`;
96
99
return (
97
100
<a
98
101
href={tagUrl}
+3
-1
lib/components/TangledString.tsx
+3
-1
lib/components/TangledString.tsx
···
2
2
import { AtProtoRecord } from "../core/AtProtoRecord";
3
3
import { TangledStringRenderer } from "../renderers/TangledStringRenderer";
4
4
import type { TangledStringRecord } from "../renderers/TangledStringRenderer";
5
+
import { useAtProto } from "../providers/AtProtoProvider";
5
6
6
7
/**
7
8
* Props for rendering Tangled String records.
···
66
67
loadingIndicator,
67
68
colorScheme,
68
69
}) => {
70
+
const { tangledBaseUrl } = useAtProto();
69
71
const Comp: React.ComponentType<TangledStringRendererInjectedProps> =
70
72
renderer ?? ((props) => <TangledStringRenderer {...props} />);
71
73
const Wrapped: React.FC<{
···
78
80
colorScheme={colorScheme}
79
81
did={did}
80
82
rkey={rkey}
81
-
canonicalUrl={`https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`}
83
+
canonicalUrl={`${tangledBaseUrl}/strings/${did}/${encodeURIComponent(rkey)}`}
82
84
/>
83
85
);
84
86
+10
-8
lib/hooks/useBlueskyAppview.ts
+10
-8
lib/hooks/useBlueskyAppview.ts
···
1
1
import { useEffect, useReducer, useRef } from "react";
2
2
import { useDidResolution } from "./useDidResolution";
3
3
import { usePdsEndpoint } from "./usePdsEndpoint";
4
-
import { createAtprotoClient, SLINGSHOT_BASE_URL } from "../utils/atproto-client";
4
+
import { createAtprotoClient } from "../utils/atproto-client";
5
5
import { useAtProto } from "../providers/AtProtoProvider";
6
6
7
7
/**
···
91
91
/** Source from which the record was successfully fetched. */
92
92
source?: "appview" | "slingshot" | "pds";
93
93
}
94
-
95
-
export const DEFAULT_APPVIEW_SERVICE = "https://public.api.bsky.app";
96
94
97
95
/**
98
96
* Maps Bluesky collection NSIDs to their corresponding appview API endpoints.
···
236
234
appviewService,
237
235
skipAppview = false,
238
236
}: UseBlueskyAppviewOptions): UseBlueskyAppviewResult<T> {
239
-
const { recordCache } = useAtProto();
237
+
const { recordCache, blueskyAppviewService, resolver } = useAtProto();
238
+
const effectiveAppviewService = appviewService ?? blueskyAppviewService;
240
239
const {
241
240
did,
242
241
error: didError,
···
326
325
did,
327
326
collection,
328
327
rkey,
329
-
appviewService ?? DEFAULT_APPVIEW_SERVICE,
328
+
effectiveAppviewService,
330
329
);
331
330
if (result) {
332
331
return result;
···
339
338
340
339
// Tier 2: Try Slingshot getRecord
341
340
try {
342
-
const result = await fetchFromSlingshot<T>(did, collection, rkey);
341
+
const slingshotUrl = resolver.getSlingshotUrl();
342
+
const result = await fetchFromSlingshot<T>(did, collection, rkey, slingshotUrl);
343
343
if (result) {
344
344
return result;
345
345
}
···
408
408
collection,
409
409
rkey,
410
410
pdsEndpoint,
411
-
appviewService,
411
+
effectiveAppviewService,
412
412
skipAppview,
413
413
resolvingDid,
414
414
resolvingEndpoint,
415
415
didError,
416
416
endpointError,
417
417
recordCache,
418
+
resolver,
418
419
]);
419
420
420
421
return state;
···
575
576
did: string,
576
577
collection: string,
577
578
rkey: string,
579
+
slingshotBaseUrl: string,
578
580
): Promise<T | undefined> {
579
-
const res = await callGetRecord<T>(SLINGSHOT_BASE_URL, did, collection, rkey);
581
+
const res = await callGetRecord<T>(slingshotBaseUrl, did, collection, rkey);
580
582
if (!res.ok) throw new Error(`Slingshot getRecord failed for ${did}/${collection}/${rkey}`);
581
583
return res.data.value;
582
584
}
+4
-6
lib/hooks/usePaginatedRecords.ts
+4
-6
lib/hooks/usePaginatedRecords.ts
···
1
1
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
import { useDidResolution } from "./useDidResolution";
3
3
import { usePdsEndpoint } from "./usePdsEndpoint";
4
-
import {
5
-
DEFAULT_APPVIEW_SERVICE,
6
-
callAppviewRpc,
7
-
callListRecords
8
-
} from "./useBlueskyAppview";
4
+
import { callAppviewRpc, callListRecords } from "./useBlueskyAppview";
5
+
import { useAtProto } from "../providers/AtProtoProvider";
9
6
10
7
/**
11
8
* Record envelope returned by paginated AT Protocol queries.
···
118
115
authorFeedService,
119
116
authorFeedActor,
120
117
}: UsePaginatedRecordsOptions): UsePaginatedRecordsResult<T> {
118
+
const { blueskyAppviewService } = useAtProto();
121
119
const {
122
120
did,
123
121
handle,
···
213
211
}
214
212
215
213
const res = await callAppviewRpc<AuthorFeedResponse>(
216
-
authorFeedService ?? DEFAULT_APPVIEW_SERVICE,
214
+
authorFeedService ?? blueskyAppviewService,
217
215
"app.bsky.feed.getAuthorFeed",
218
216
{
219
217
actor: actorIdentifier,
+78
-5
lib/providers/AtProtoProvider.tsx
+78
-5
lib/providers/AtProtoProvider.tsx
···
5
5
useMemo,
6
6
useRef,
7
7
} from "react";
8
-
import { ServiceResolver, normalizeBaseUrl } from "../utils/atproto-client";
8
+
import { ServiceResolver, normalizeBaseUrl, DEFAULT_CONFIG } from "../utils/atproto-client";
9
9
import { BlobCache, DidCache, RecordCache } from "../utils/cache";
10
10
11
11
/**
···
16
16
children: React.ReactNode;
17
17
/** Optional custom PLC directory URL. Defaults to https://plc.directory */
18
18
plcDirectory?: string;
19
+
/** Optional custom identity service URL. Defaults to https://public.api.bsky.app */
20
+
identityService?: string;
21
+
/** Optional custom Slingshot service URL. Defaults to https://slingshot.microcosm.blue */
22
+
slingshotBaseUrl?: string;
23
+
/** Optional custom Bluesky appview service URL. Defaults to https://public.api.bsky.app */
24
+
blueskyAppviewService?: string;
25
+
/** Optional custom Bluesky app base URL for links. Defaults to https://bsky.app */
26
+
blueskyAppBaseUrl?: string;
27
+
/** Optional custom Tangled base URL for links. Defaults to https://tangled.org */
28
+
tangledBaseUrl?: string;
19
29
}
20
30
21
31
/**
···
26
36
resolver: ServiceResolver;
27
37
/** Normalized PLC directory base URL. */
28
38
plcDirectory: string;
39
+
/** Normalized Bluesky appview service URL. */
40
+
blueskyAppviewService: string;
41
+
/** Normalized Bluesky app base URL for links. */
42
+
blueskyAppBaseUrl: string;
43
+
/** Normalized Tangled base URL for links. */
44
+
tangledBaseUrl: string;
29
45
/** Cache for DID documents and handle mappings. */
30
46
didCache: DidCache;
31
47
/** Cache for fetched blob data. */
···
77
93
export function AtProtoProvider({
78
94
children,
79
95
plcDirectory,
96
+
identityService,
97
+
slingshotBaseUrl,
98
+
blueskyAppviewService,
99
+
blueskyAppBaseUrl,
100
+
tangledBaseUrl,
80
101
}: AtProtoProviderProps) {
81
102
const normalizedPlc = useMemo(
82
103
() =>
83
104
normalizeBaseUrl(
84
105
plcDirectory && plcDirectory.trim()
85
106
? plcDirectory
86
-
: "https://plc.directory",
107
+
: DEFAULT_CONFIG.plcDirectory,
87
108
),
88
109
[plcDirectory],
89
110
);
111
+
const normalizedIdentity = useMemo(
112
+
() =>
113
+
normalizeBaseUrl(
114
+
identityService && identityService.trim()
115
+
? identityService
116
+
: DEFAULT_CONFIG.identityService,
117
+
),
118
+
[identityService],
119
+
);
120
+
const normalizedSlingshot = useMemo(
121
+
() =>
122
+
normalizeBaseUrl(
123
+
slingshotBaseUrl && slingshotBaseUrl.trim()
124
+
? slingshotBaseUrl
125
+
: DEFAULT_CONFIG.slingshotBaseUrl,
126
+
),
127
+
[slingshotBaseUrl],
128
+
);
129
+
const normalizedAppview = useMemo(
130
+
() =>
131
+
normalizeBaseUrl(
132
+
blueskyAppviewService && blueskyAppviewService.trim()
133
+
? blueskyAppviewService
134
+
: DEFAULT_CONFIG.blueskyAppviewService,
135
+
),
136
+
[blueskyAppviewService],
137
+
);
138
+
const normalizedBlueskyApp = useMemo(
139
+
() =>
140
+
normalizeBaseUrl(
141
+
blueskyAppBaseUrl && blueskyAppBaseUrl.trim()
142
+
? blueskyAppBaseUrl
143
+
: DEFAULT_CONFIG.blueskyAppBaseUrl,
144
+
),
145
+
[blueskyAppBaseUrl],
146
+
);
147
+
const normalizedTangled = useMemo(
148
+
() =>
149
+
normalizeBaseUrl(
150
+
tangledBaseUrl && tangledBaseUrl.trim()
151
+
? tangledBaseUrl
152
+
: DEFAULT_CONFIG.tangledBaseUrl,
153
+
),
154
+
[tangledBaseUrl],
155
+
);
90
156
const resolver = useMemo(
91
-
() => new ServiceResolver({ plcDirectory: normalizedPlc }),
92
-
[normalizedPlc],
157
+
() => new ServiceResolver({
158
+
plcDirectory: normalizedPlc,
159
+
identityService: normalizedIdentity,
160
+
slingshotBaseUrl: normalizedSlingshot,
161
+
}),
162
+
[normalizedPlc, normalizedIdentity, normalizedSlingshot],
93
163
);
94
164
const cachesRef = useRef<{
95
165
didCache: DidCache;
···
108
178
() => ({
109
179
resolver,
110
180
plcDirectory: normalizedPlc,
181
+
blueskyAppviewService: normalizedAppview,
182
+
blueskyAppBaseUrl: normalizedBlueskyApp,
183
+
tangledBaseUrl: normalizedTangled,
111
184
didCache: cachesRef.current!.didCache,
112
185
blobCache: cachesRef.current!.blobCache,
113
186
recordCache: cachesRef.current!.recordCache,
114
187
}),
115
-
[resolver, normalizedPlc],
188
+
[resolver, normalizedPlc, normalizedAppview, normalizedBlueskyApp, normalizedTangled],
116
189
);
117
190
118
191
return (
+3
-1
lib/renderers/BlueskyProfileRenderer.tsx
+3
-1
lib/renderers/BlueskyProfileRenderer.tsx
···
1
1
import React from "react";
2
2
import type { ProfileRecord } from "../types/bluesky";
3
3
import { BlueskyIcon } from "../components/BlueskyIcon";
4
+
import { useAtProto } from "../providers/AtProtoProvider";
4
5
5
6
export interface BlueskyProfileRendererProps {
6
7
record: ProfileRecord;
···
19
20
handle,
20
21
avatarUrl,
21
22
}) => {
23
+
const { blueskyAppBaseUrl } = useAtProto();
22
24
23
25
if (error)
24
26
return (
···
28
30
);
29
31
if (loading && !record) return <div style={{ padding: 8 }}>Loading…</div>;
30
32
31
-
const profileUrl = `https://bsky.app/profile/${did}`;
33
+
const profileUrl = `${blueskyAppBaseUrl}/profile/${did}`;
32
34
const rawWebsite = record.website?.trim();
33
35
const websiteHref = rawWebsite
34
36
? rawWebsite.match(/^https?:\/\//i)
+5
-3
lib/renderers/LeafletDocumentRenderer.tsx
+5
-3
lib/renderers/LeafletDocumentRenderer.tsx
···
1
1
import React, { useMemo, useRef } from "react";
2
2
import { useDidResolution } from "../hooks/useDidResolution";
3
3
import { useBlob } from "../hooks/useBlob";
4
+
import { useAtProto } from "../providers/AtProtoProvider";
4
5
import {
5
6
parseAtUri,
6
7
formatDidForLabel,
···
54
55
publicationBaseUrl,
55
56
publicationRecord,
56
57
}) => {
58
+
const { blueskyAppBaseUrl } = useAtProto();
57
59
const authorDid = record.author?.startsWith("did:")
58
60
? record.author
59
61
: undefined;
···
78
80
: undefined);
79
81
const authorLabel = resolvedPublicationLabel ?? fallbackAuthorLabel;
80
82
const authorHref = publicationUri
81
-
? `https://bsky.app/profile/${publicationUri.did}`
83
+
? `${blueskyAppBaseUrl}/profile/${publicationUri.did}`
82
84
: undefined;
83
85
84
86
if (error)
···
105
107
timeStyle: "short",
106
108
})
107
109
: undefined;
108
-
const fallbackLeafletUrl = `https://bsky.app/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
110
+
const fallbackLeafletUrl = `${blueskyAppBaseUrl}/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
109
111
const publicationRoot =
110
112
publicationBaseUrl ?? publicationRecord?.base_path ?? undefined;
111
113
const resolvedPublicationRoot = publicationRoot
···
117
119
publicationLeafletUrl ??
118
120
postUrl ??
119
121
(publicationUri
120
-
? `https://bsky.app/profile/${publicationUri.did}`
122
+
? `${blueskyAppBaseUrl}/profile/${publicationUri.did}`
121
123
: undefined) ??
122
124
fallbackLeafletUrl;
123
125
+3
-1
lib/renderers/TangledStringRenderer.tsx
+3
-1
lib/renderers/TangledStringRenderer.tsx
···
1
1
import React from "react";
2
2
import type { ShTangledString } from "@atcute/tangled";
3
+
import { useAtProto } from "../providers/AtProtoProvider";
3
4
4
5
export type TangledStringRecord = ShTangledString.Main;
5
6
···
20
21
rkey,
21
22
canonicalUrl,
22
23
}) => {
24
+
const { tangledBaseUrl } = useAtProto();
23
25
24
26
if (error)
25
27
return (
···
31
33
32
34
const viewUrl =
33
35
canonicalUrl ??
34
-
`https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`;
36
+
`${tangledBaseUrl}/strings/${did}/${encodeURIComponent(rkey)}`;
35
37
const timestamp = new Date(record.createdAt).toLocaleString(undefined, {
36
38
dateStyle: "medium",
37
39
timeStyle: "short",
+35
-4
lib/utils/atproto-client.ts
+35
-4
lib/utils/atproto-client.ts
···
13
13
export interface ServiceResolverOptions {
14
14
plcDirectory?: string;
15
15
identityService?: string;
16
+
slingshotBaseUrl?: string;
16
17
fetch?: typeof fetch;
17
18
}
18
19
19
20
const DEFAULT_PLC = "https://plc.directory";
20
21
const DEFAULT_IDENTITY_SERVICE = "https://public.api.bsky.app";
22
+
const DEFAULT_SLINGSHOT = "https://slingshot.microcosm.blue";
23
+
const DEFAULT_APPVIEW = "https://public.api.bsky.app";
24
+
const DEFAULT_BLUESKY_APP = "https://bsky.app";
25
+
const DEFAULT_TANGLED = "https://tangled.org";
26
+
21
27
const ABSOLUTE_URL_RE = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
22
28
const SUPPORTED_DID_METHODS = ["plc", "web"] as const;
23
29
type SupportedDidMethod = (typeof SUPPORTED_DID_METHODS)[number];
24
30
type SupportedDid = Did<SupportedDidMethod>;
25
31
26
-
export const SLINGSHOT_BASE_URL = "https://slingshot.microcosm.blue";
32
+
/**
33
+
* Default configuration values for AT Protocol services.
34
+
* These can be overridden via AtProtoProvider props.
35
+
*/
36
+
export const DEFAULT_CONFIG = {
37
+
plcDirectory: DEFAULT_PLC,
38
+
identityService: DEFAULT_IDENTITY_SERVICE,
39
+
slingshotBaseUrl: DEFAULT_SLINGSHOT,
40
+
blueskyAppviewService: DEFAULT_APPVIEW,
41
+
blueskyAppBaseUrl: DEFAULT_BLUESKY_APP,
42
+
tangledBaseUrl: DEFAULT_TANGLED,
43
+
} as const;
44
+
45
+
export const SLINGSHOT_BASE_URL = DEFAULT_SLINGSHOT;
27
46
28
47
export const normalizeBaseUrl = (input: string): string => {
29
48
const trimmed = input.trim();
···
38
57
39
58
export class ServiceResolver {
40
59
private plc: string;
60
+
private slingshot: string;
41
61
private didResolver: CompositeDidDocumentResolver<SupportedDidMethod>;
42
62
private handleResolver: XrpcHandleResolver;
43
63
private fetchImpl: typeof fetch;
···
50
70
opts.identityService && opts.identityService.trim()
51
71
? opts.identityService
52
72
: DEFAULT_IDENTITY_SERVICE;
73
+
const slingshotSource =
74
+
opts.slingshotBaseUrl && opts.slingshotBaseUrl.trim()
75
+
? opts.slingshotBaseUrl
76
+
: DEFAULT_SLINGSHOT;
53
77
this.plc = normalizeBaseUrl(plcSource);
54
78
const identityBase = normalizeBaseUrl(identitySource);
79
+
this.slingshot = normalizeBaseUrl(slingshotSource);
55
80
this.fetchImpl = bindFetch(opts.fetch);
56
81
const plcResolver = new PlcDidDocumentResolver({
57
82
apiUrl: this.plc,
···
97
122
return svc.serviceEndpoint.replace(/\/$/, "");
98
123
}
99
124
125
+
getSlingshotUrl(): string {
126
+
return this.slingshot;
127
+
}
128
+
100
129
async resolveHandle(handle: string): Promise<string> {
101
130
const normalized = handle.trim().toLowerCase();
102
131
if (!normalized) throw new Error("Handle cannot be empty");
···
104
133
try {
105
134
const url = new URL(
106
135
"/xrpc/com.atproto.identity.resolveHandle",
107
-
SLINGSHOT_BASE_URL,
136
+
this.slingshot,
108
137
);
109
138
url.searchParams.set("handle", normalized);
110
139
const response = await this.fetchImpl(url);
···
161
190
}
162
191
if (!service) throw new Error("service or did required");
163
192
const normalizedService = normalizeBaseUrl(service);
164
-
const handler = createSlingshotAwareHandler(normalizedService, fetchImpl);
193
+
const slingshotUrl = resolver.getSlingshotUrl();
194
+
const handler = createSlingshotAwareHandler(normalizedService, slingshotUrl, fetchImpl);
165
195
const rpc = new Client({ handler });
166
196
return { rpc, service: normalizedService, resolver };
167
197
}
···
177
207
178
208
function createSlingshotAwareHandler(
179
209
service: string,
210
+
slingshotBaseUrl: string,
180
211
fetchImpl: typeof fetch,
181
212
): FetchHandler {
182
213
const primary = simpleFetchHandler({ service, fetch: fetchImpl });
183
214
const slingshot = simpleFetchHandler({
184
-
service: SLINGSHOT_BASE_URL,
215
+
service: slingshotBaseUrl,
185
216
fetch: fetchImpl,
186
217
});
187
218
return async (pathname, init) => {
+8
-1
src/App.tsx
+8
-1
src/App.tsx
···
524
524
525
525
export const App: React.FC = () => {
526
526
return (
527
-
<AtProtoProvider>
527
+
<AtProtoProvider
528
+
plcDirectory="https://plc.wtf/"
529
+
identityService="https://api.blacksky.community"
530
+
slingshotBaseUrl="https://slingshot.microcosm.blue"
531
+
blueskyAppviewService="https://api.blacksky.community"
532
+
blueskyAppBaseUrl="https://reddwarf.app/"
533
+
tangledBaseUrl="https://tangled.org"
534
+
>
528
535
<div
529
536
style={{
530
537
maxWidth: 860,