+59
-15
src/components/Login.tsx
+59
-15
src/components/Login.tsx
···
1
1
// src/components/Login.tsx
2
2
import AtpAgent, { Agent } from "@atproto/api";
3
+
import { useAtom } from "jotai";
3
4
import React, { useEffect, useRef, useState } from "react";
4
5
5
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { imgCDNAtom } from "~/utils/atoms";
6
8
import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery";
7
9
8
10
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
···
190
192
<p className="text-xs text-gray-500 dark:text-gray-400">
191
193
Sign in with AT. Your password is never shared.
192
194
</p>
193
-
<input
195
+
{/* <input
194
196
type="text"
195
197
placeholder="handle.bsky.social"
196
198
value={handle}
197
199
onChange={(e) => setHandle(e.target.value)}
198
200
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
199
-
/>
200
-
<button
201
-
type="submit"
202
-
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
203
-
>
204
-
Log in
205
-
</button>
201
+
/> */}
202
+
<div className="flex flex-col gap-3">
203
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
204
+
<input
205
+
type="text"
206
+
placeholder=" "
207
+
value={handle}
208
+
onChange={(e) => setHandle(e.target.value)}
209
+
/>
210
+
<label>AT Handle</label>
211
+
</div>
212
+
<button
213
+
type="submit"
214
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
215
+
>
216
+
Log in
217
+
</button>
218
+
</div>
206
219
</form>
207
220
);
208
221
};
···
235
248
<p className="text-xs text-red-500 dark:text-red-400">
236
249
Warning: Less secure. Use an App Password.
237
250
</p>
238
-
<input
251
+
{/* <input
239
252
type="text"
240
253
placeholder="handle.bsky.social"
241
254
value={user}
···
257
270
value={serviceURL}
258
271
onChange={(e) => setServiceURL(e.target.value)}
259
272
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
260
-
/>
273
+
/> */}
274
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
275
+
<input
276
+
type="text"
277
+
placeholder=" "
278
+
value={user}
279
+
onChange={(e) => setUser(e.target.value)}
280
+
/>
281
+
<label>AT Handle</label>
282
+
</div>
283
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
284
+
<input
285
+
type="text"
286
+
placeholder=" "
287
+
value={password}
288
+
onChange={(e) => setPassword(e.target.value)}
289
+
/>
290
+
<label>App Password</label>
291
+
</div>
292
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
293
+
<input
294
+
type="text"
295
+
placeholder=" "
296
+
value={serviceURL}
297
+
onChange={(e) => setServiceURL(e.target.value)}
298
+
/>
299
+
<label>PDS</label>
300
+
</div>
261
301
{error && <p className="text-xs text-red-500">{error}</p>}
262
302
<button
263
303
type="submit"
···
278
318
large?: boolean;
279
319
}) => {
280
320
const { agent } = useAuth();
281
-
const did = ((agent as AtpAgent).session?.did ?? (agent as AtpAgent)?.assertDid ?? agent?.did) as
282
-
| string
283
-
| undefined;
321
+
const did = ((agent as AtpAgent).session?.did ??
322
+
(agent as AtpAgent)?.assertDid ??
323
+
agent?.did) as string | undefined;
284
324
const { data: identity } = useQueryIdentity(did);
285
-
const { data: profiledata } = useQueryProfile(`at://${did}/app.bsky.actor.profile/self`);
325
+
const { data: profiledata } = useQueryProfile(
326
+
`at://${did}/app.bsky.actor.profile/self`
327
+
);
286
328
const profile = profiledata?.value;
287
329
330
+
const [imgcdn] = useAtom(imgCDNAtom)
331
+
288
332
function getAvatarUrl(p: typeof profile) {
289
333
const link = p?.avatar?.ref?.["$link"];
290
334
if (!link || !did) return null;
291
-
return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`;
335
+
return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`;
292
336
}
293
337
294
338
if (!profiledata) {
+16
-13
src/components/UniversalPostRenderer.tsx
+16
-13
src/components/UniversalPostRenderer.tsx
···
5
5
import * as React from "react";
6
6
import { type SVGProps } from "react";
7
7
8
-
import { composerAtom, constellationURLAtom, likedPostsAtom } from "~/utils/atoms";
8
+
import { composerAtom, constellationURLAtom, imgCDNAtom, likedPostsAtom } from "~/utils/atoms";
9
9
import { useHydratedEmbed } from "~/utils/useHydrated";
10
10
import {
11
11
useQueryConstellation,
···
599
599
);
600
600
}
601
601
602
-
function getAvatarUrl(opProfile: any, did: string) {
602
+
function getAvatarUrl(opProfile: any, did: string, cdn: string) {
603
603
const link = opProfile?.value?.avatar?.ref?.["$link"];
604
604
if (!link) return null;
605
-
return `https://cdn.bsky.app/img/avatar/plain/${did}/${link}@jpeg`;
605
+
return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`;
606
606
}
607
607
608
608
export function UniversalPostRendererRawRecordShim({
···
723
723
error: embedError,
724
724
} = useHydratedEmbed(postRecord?.value?.embed, resolved?.did);
725
725
726
+
const [imgcdn] = useAtom(imgCDNAtom)
727
+
726
728
const parsedaturi = new AtUri(aturi); //parseAtUri(aturi);
727
729
728
730
const fakepost = React.useMemo<AppBskyFeedDefs.PostView>(
···
734
736
did: resolved?.did || "",
735
737
handle: resolved?.handle || "",
736
738
displayName: profileRecord?.value?.displayName || "",
737
-
avatar: getAvatarUrl(profileRecord, resolved?.did) || "",
739
+
avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "",
738
740
viewer: undefined,
739
741
labels: profileRecord?.labels || undefined,
740
742
verification: undefined,
···
762
764
repliesCount,
763
765
repostsCount,
764
766
likesCount,
767
+
imgcdn
765
768
]
766
769
);
767
770
···
886
889
{...props}
887
890
>
888
891
<path
889
-
fill="oklch(0.704 0.05 28)"
892
+
fill="var(--color-gray-400)"
890
893
d="M9 22a1 1 0 0 1-1-1v-3H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6.1l-3.7 3.71c-.2.19-.45.29-.7.29zm1-6v3.08L13.08 16H20V4H4v12z"
891
894
></path>
892
895
</svg>
···
903
906
{...props}
904
907
>
905
908
<path
906
-
fill="oklch(0.704 0.05 28)"
909
+
fill="var(--color-gray-400)"
907
910
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
908
911
></path>
909
912
</svg>
···
954
957
{...props}
955
958
>
956
959
<path
957
-
fill="oklch(0.704 0.05 28)"
960
+
fill="var(--color-gray-400)"
958
961
d="m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3"
959
962
></path>
960
963
</svg>
···
971
974
{...props}
972
975
>
973
976
<path
974
-
fill="oklch(0.704 0.05 28)"
977
+
fill="var(--color-gray-400)"
975
978
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3a3 3 0 0 0-3-3a3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"
976
979
></path>
977
980
</svg>
···
988
991
{...props}
989
992
>
990
993
<path
991
-
fill="oklch(0.704 0.05 28)"
994
+
fill="var(--color-gray-400)"
992
995
d="M16 12a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2m-6 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"
993
996
></path>
994
997
</svg>
···
1005
1008
{...props}
1006
1009
>
1007
1010
<path
1008
-
fill="oklch(0.704 0.05 28)"
1011
+
fill="var(--color-gray-400)"
1009
1012
d="M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"
1010
1013
></path>
1011
1014
</svg>
···
1039
1042
{...props}
1040
1043
>
1041
1044
<path
1042
-
fill="oklch(0.704 0.05 28)"
1045
+
fill="var(--color-gray-400)"
1043
1046
d="M10 9V5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11"
1044
1047
></path>
1045
1048
</svg>
···
1093
1096
{...props}
1094
1097
>
1095
1098
<path
1096
-
fill="oklch(0.704 0.05 28)"
1099
+
fill="var(--color-gray-400)"
1097
1100
d="M17 17H7v-3l-4 4l4 4v-3h12v-6h-2M7 7h10v3l4-4l-4-4v3H5v6h2z"
1098
1101
></path>
1099
1102
</svg>
···
1110
1113
{...props}
1111
1114
>
1112
1115
<path
1113
-
fill="oklch(0.704 0.05 28)"
1116
+
fill="var(--color-gray-400)"
1114
1117
d="M6 5.75L10.25 10H7v6h6.5l2 2H7a2 2 0 0 1-2-2v-6H1.75zm12 12.5L13.75 14H17V8h-6.5l-2-2H17a2 2 0 0 1 2 2v6h3.25z"
1115
1118
></path>
1116
1119
</svg>
+6
-2
src/routes/profile.$did/index.tsx
+6
-2
src/routes/profile.$did/index.tsx
···
1
1
import { useQueryClient } from "@tanstack/react-query";
2
2
import { createFileRoute } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
3
4
import React from "react";
4
5
5
6
import { Header } from "~/components/Header";
6
7
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
7
8
import { useAuth } from "~/providers/UnifiedAuthProvider";
9
+
import { imgCDNAtom } from "~/utils/atoms";
8
10
import { toggleFollow, useGetFollowState } from "~/utils/followState";
9
11
import {
10
12
useInfiniteQueryAuthorFeed,
···
66
68
() => postsData?.pages.flatMap((page) => page.records) ?? [],
67
69
[postsData]
68
70
);
71
+
72
+
const [imgcdn] = useAtom(imgCDNAtom)
69
73
70
74
function getAvatarUrl(p: typeof profile) {
71
75
const link = p?.avatar?.ref?.["$link"];
72
76
if (!link || !resolvedDid) return null;
73
-
return `https://cdn.bsky.app/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
77
+
return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`;
74
78
}
75
79
function getBannerUrl(p: typeof profile) {
76
80
const link = p?.banner?.ref?.["$link"];
77
81
if (!link || !resolvedDid) return null;
78
-
return `https://cdn.bsky.app/img/banner/plain/${resolvedDid}/${link}@jpeg`;
82
+
return `https://${imgcdn}/img/banner/plain/${resolvedDid}/${link}@jpeg`;
79
83
}
80
84
81
85
const displayName =
+32
-9
src/routes/settings.tsx
+32
-9
src/routes/settings.tsx
···
6
6
import {
7
7
constellationURLAtom,
8
8
defaultconstellationURL,
9
+
defaultImgCDN,
9
10
defaultslingshotURL,
11
+
defaultVideoCDN,
12
+
imgCDNAtom,
10
13
slingshotURLAtom,
14
+
videoCDNAtom,
11
15
} from "~/utils/atoms";
12
16
13
17
export const Route = createFileRoute("/settings")({
···
27
31
}
28
32
}}
29
33
/>
30
-
<Login />
34
+
<div className="lg:hidden"><Login /></div>
35
+
<div className="h-4" />
31
36
<TextInputSetting
32
37
atom={constellationURLAtom}
33
38
title={"Constellation"}
···
42
47
description={"Customize the Slingshot instance to be used by Red Dwarf"}
43
48
init={defaultslingshotURL}
44
49
/>
45
-
<span className="text-gray-500 dark:text-gray-400 py-4 px-6">please restart/refresh the app if changes arent applying correctly</span>
50
+
<TextInputSetting
51
+
atom={imgCDNAtom}
52
+
title={"Image CDN"}
53
+
description={
54
+
"Customize the Constellation instance to be used by Red Dwarf"
55
+
}
56
+
init={defaultImgCDN}
57
+
/>
58
+
<TextInputSetting
59
+
atom={videoCDNAtom}
60
+
title={"Video CDN"}
61
+
description={"Customize the Slingshot instance to be used by Red Dwarf"}
62
+
init={defaultVideoCDN}
63
+
/>
64
+
<p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm">please restart/refresh the app if changes arent applying correctly</p>
46
65
</>
47
66
);
48
67
}
···
60
79
}) {
61
80
const [value, setValue] = useAtom(atom);
62
81
return (
63
-
<div className="flex flex-col gap-2 p-4 rounded-2xl border border-gray-200 dark:border-gray-800 ">
64
-
<div>
82
+
<div className="flex flex-col gap-2 px-4 py-2">
83
+
{/* <div>
65
84
{title && (
66
85
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
67
86
{title}
···
72
91
{description}
73
92
</p>
74
93
)}
75
-
</div>
94
+
</div> */}
76
95
77
96
<div className="flex flex-row gap-2 items-center">
78
-
<input
97
+
<div className="m3input-field m3input-label m3input-border size-md flex-1">
98
+
<input type="text" placeholder=" " value={value} onChange={(e) => setValue(e.target.value)}/>
99
+
<label>{title}</label>
100
+
</div>
101
+
{/* <input
79
102
type="text"
80
103
value={value}
81
104
onChange={(e) => setValue(e.target.value)}
···
83
106
text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400
84
107
focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600"
85
108
placeholder="Enter value..."
86
-
/>
109
+
/> */}
87
110
<button
88
111
onClick={() => setValue(init ?? "")}
89
-
className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-800
112
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
90
113
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
91
114
>
92
115
Reset
···
94
117
</div>
95
118
</div>
96
119
);
97
-
}
120
+
}
+113
src/styles/app.css
+113
src/styles/app.css
···
105
105
:root {
106
106
--shadow-opacity: calc(1 - var(--is-top));
107
107
--tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15));
108
+
}
109
+
110
+
111
+
/* m3 input */
112
+
:root {
113
+
--m3input-radius: 6px;
114
+
--m3input-border-width: .0625rem;
115
+
--m3input-font-size: 16px;
116
+
--m3input-transition: 150ms cubic-bezier(.2, .8, .2, 1);
117
+
/* light theme */
118
+
--m3input-bg: var(--color-gray-50);
119
+
--m3input-border-color: var(--color-gray-400);
120
+
--m3input-label-color: var(--color-gray-500);
121
+
--m3input-text-color: var(--color-gray-900);
122
+
--m3input-focus-color: var(--color-gray-600);
123
+
}
124
+
125
+
@media (prefers-color-scheme: dark) {
126
+
:root {
127
+
--m3input-bg: var(--color-gray-950);
128
+
--m3input-border-color: var(--color-gray-700);
129
+
--m3input-label-color: var(--color-gray-400);
130
+
--m3input-text-color: var(--color-gray-50);
131
+
--m3input-focus-color: var(--color-gray-400);
132
+
}
133
+
}
134
+
135
+
/* reset page *//*
136
+
html,
137
+
body {
138
+
background: var(--m3input-bg);
139
+
margin: 0;
140
+
padding: 1rem;
141
+
color: var(--m3input-text-color);
142
+
font-family: system-ui, sans-serif;
143
+
font-size: var(--m3input-font-size);
144
+
}*/
145
+
146
+
/* base wrapper */
147
+
.m3input-field.m3input-label.m3input-border {
148
+
position: relative;
149
+
display: inline-block;
150
+
width: 100%;
151
+
/*max-width: 400px;*/
152
+
}
153
+
154
+
/* size variants */
155
+
.m3input-field.size-sm {
156
+
--m3input-h: 40px;
157
+
}
158
+
159
+
.m3input-field.size-md {
160
+
--m3input-h: 48px;
161
+
}
162
+
163
+
.m3input-field.size-lg {
164
+
--m3input-h: 56px;
165
+
}
166
+
167
+
.m3input-field.size-xl {
168
+
--m3input-h: 64px;
169
+
}
170
+
171
+
.m3input-field.m3input-label.m3input-border:not(.size-sm):not(.size-md):not(.size-lg):not(.size-xl) {
172
+
--m3input-h: 48px;
173
+
}
174
+
175
+
/* outlined input */
176
+
.m3input-field.m3input-label.m3input-border input {
177
+
width: 100%;
178
+
height: var(--m3input-h);
179
+
border: var(--m3input-border-width) solid var(--m3input-border-color);
180
+
border-radius: var(--m3input-radius);
181
+
background: var(--m3input-bg);
182
+
color: var(--m3input-text-color);
183
+
font-size: var(--m3input-font-size);
184
+
padding: 0 12px;
185
+
box-sizing: border-box;
186
+
outline: none;
187
+
transition: border-color var(--m3input-transition), box-shadow var(--m3input-transition);
188
+
}
189
+
190
+
/* focus ring */
191
+
.m3input-field.m3input-label.m3input-border input:focus {
192
+
border-color: var(--m3input-focus-color);
193
+
/*box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-color) 20%, transparent);*/
194
+
}
195
+
196
+
/* label */
197
+
.m3input-field.m3input-label.m3input-border label {
198
+
position: absolute;
199
+
left: 12px;
200
+
top: 50%;
201
+
transform: translateY(-50%);
202
+
background: var(--m3input-bg);
203
+
padding: 0 .25em;
204
+
color: var(--m3input-label-color);
205
+
pointer-events: none;
206
+
transition: all var(--m3input-transition);
207
+
}
208
+
209
+
/* float on focus or when filled */
210
+
.m3input-field.m3input-label.m3input-border input:focus+label,
211
+
.m3input-field.m3input-label.m3input-border input:not(:placeholder-shown)+label {
212
+
top: 0;
213
+
transform: translateY(-50%) scale(.78);
214
+
left: 0;
215
+
color: var(--m3input-focus-color);
216
+
}
217
+
218
+
/* placeholder trick */
219
+
.m3input-field.m3input-label.m3input-border input::placeholder {
220
+
color: transparent;
108
221
}
+10
src/utils/atoms.ts
+10
src/utils/atoms.ts
···
31
31
'slingshotURL',
32
32
defaultslingshotURL
33
33
)
34
+
export const defaultImgCDN = 'cdn.bsky.app'
35
+
export const imgCDNAtom = atomWithStorage<string>(
36
+
'imgcdnurl',
37
+
defaultImgCDN
38
+
)
39
+
export const defaultVideoCDN = 'video.bsky.app'
40
+
export const videoCDNAtom = atomWithStorage<string>(
41
+
'videocdnurl',
42
+
defaultVideoCDN
43
+
)
34
44
35
45
export const isAtTopAtom = atom<boolean>(true);
36
46
+53
-23
src/utils/useHydrated.ts
+53
-23
src/utils/useHydrated.ts
···
9
9
AppBskyFeedPost,
10
10
AtUri,
11
11
} from "@atproto/api";
12
+
import { useAtom } from "jotai";
12
13
import { useMemo } from "react";
13
14
14
-
import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery";
15
+
import { imgCDNAtom, videoCDNAtom } from "./atoms";
16
+
import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery";
15
17
16
-
type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends
17
-
| { data: infer D }
18
-
| undefined
19
-
? D
20
-
: never;
18
+
type QueryResultData<T extends (...args: any) => any> =
19
+
ReturnType<T> extends { data: infer D } | undefined ? D : never;
21
20
22
21
function asTyped<T extends { $type: string }>(obj: T): $Typed<T> {
23
22
return obj as $Typed<T>;
···
26
25
export function hydrateEmbedImages(
27
26
embed: AppBskyEmbedImages.Main,
28
27
did: string,
28
+
cdn: string
29
29
): $Typed<AppBskyEmbedImages.View> {
30
30
return asTyped({
31
31
$type: "app.bsky.embed.images#view" as const,
···
34
34
const link = img.image.ref?.["$link"];
35
35
if (!link) return null;
36
36
return {
37
-
thumb: `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
-
fullsize: `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${link}@jpeg`,
37
+
thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`,
38
+
fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`,
39
39
alt: img.alt || "",
40
40
aspectRatio: img.aspectRatio,
41
41
};
···
47
47
export function hydrateEmbedExternal(
48
48
embed: AppBskyEmbedExternal.Main,
49
49
did: string,
50
+
cdn: string
50
51
): $Typed<AppBskyEmbedExternal.View> {
51
52
return asTyped({
52
53
$type: "app.bsky.embed.external#view" as const,
···
55
56
title: embed.external.title,
56
57
description: embed.external.description,
57
58
thumb: embed.external.thumb?.ref?.$link
58
-
? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
+
? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg`
59
60
: undefined,
60
61
},
61
62
});
···
64
65
export function hydrateEmbedVideo(
65
66
embed: AppBskyEmbedVideo.Main,
66
67
did: string,
68
+
videocdn: string
67
69
): $Typed<AppBskyEmbedVideo.View> {
68
70
const videoLink = embed.video.ref.$link;
69
71
return asTyped({
70
72
$type: "app.bsky.embed.video#view" as const,
71
-
playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`,
72
-
thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`,
73
+
playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`,
74
+
thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`,
73
75
aspectRatio: embed.aspectRatio,
74
76
cid: videoLink,
75
77
});
···
80
82
quotedPost: QueryResultData<typeof useQueryPost>,
81
83
quotedProfile: QueryResultData<typeof useQueryProfile>,
82
84
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
85
+
cdn: string
83
86
): $Typed<AppBskyEmbedRecord.View> | undefined {
84
87
if (!quotedPost || !quotedProfile || !quotedIdentity) {
85
88
return undefined;
···
91
94
handle: quotedIdentity.handle,
92
95
displayName: quotedProfile.value.displayName ?? quotedIdentity.handle,
93
96
avatar: quotedProfile.value.avatar?.ref?.$link
94
-
? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
97
+
? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg`
95
98
: undefined,
96
99
viewer: {},
97
100
labels: [],
···
122
125
quotedPost: QueryResultData<typeof useQueryPost>,
123
126
quotedProfile: QueryResultData<typeof useQueryProfile>,
124
127
quotedIdentity: QueryResultData<typeof useQueryIdentity>,
128
+
cdn: string
125
129
): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined {
126
130
const hydratedRecord = hydrateEmbedRecord(
127
131
embed.record,
128
132
quotedPost,
129
133
quotedProfile,
130
134
quotedIdentity,
135
+
cdn
131
136
);
132
137
133
138
if (!hydratedRecord) return undefined;
···
148
153
149
154
export function useHydratedEmbed(
150
155
embed: AppBskyFeedPost.Record["embed"],
151
-
postAuthorDid: string | undefined,
156
+
postAuthorDid: string | undefined
152
157
) {
153
158
const recordInfo = useMemo(() => {
154
159
if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
···
181
186
error: profileError,
182
187
} = useQueryProfile(profileUri);
183
188
189
+
const [imgcdn] = useAtom(imgCDNAtom);
190
+
const [videocdn] = useAtom(videoCDNAtom);
191
+
184
192
const queryidentityresult = useQueryIdentity(quotedAuthorDid);
185
193
186
194
const hydratedEmbed: HydratedEmbedView | undefined = (() => {
187
195
if (!embed || !postAuthorDid) return undefined;
188
196
189
-
if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) {
197
+
if (
198
+
isRecordType &&
199
+
(!usequerypostresults?.data ||
200
+
!quotedProfile ||
201
+
!queryidentityresult?.data)
202
+
) {
190
203
return undefined;
191
204
}
192
205
193
206
try {
194
207
if (AppBskyEmbedImages.isMain(embed)) {
195
-
return hydrateEmbedImages(embed, postAuthorDid);
208
+
return hydrateEmbedImages(embed, postAuthorDid, imgcdn);
196
209
} else if (AppBskyEmbedExternal.isMain(embed)) {
197
-
return hydrateEmbedExternal(embed, postAuthorDid);
210
+
return hydrateEmbedExternal(embed, postAuthorDid, imgcdn);
198
211
} else if (AppBskyEmbedVideo.isMain(embed)) {
199
-
return hydrateEmbedVideo(embed, postAuthorDid);
212
+
return hydrateEmbedVideo(embed, postAuthorDid, videocdn);
200
213
} else if (AppBskyEmbedRecord.isMain(embed)) {
201
214
return hydrateEmbedRecord(
202
215
embed,
203
216
usequerypostresults?.data,
204
217
quotedProfile,
205
218
queryidentityresult?.data,
219
+
imgcdn
206
220
);
207
221
} else if (AppBskyEmbedRecordWithMedia.isMain(embed)) {
208
222
let hydratedMedia:
···
212
226
| undefined;
213
227
214
228
if (AppBskyEmbedImages.isMain(embed.media)) {
215
-
hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid);
229
+
hydratedMedia = hydrateEmbedImages(
230
+
embed.media,
231
+
postAuthorDid,
232
+
imgcdn
233
+
);
216
234
} else if (AppBskyEmbedExternal.isMain(embed.media)) {
217
-
hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid);
235
+
hydratedMedia = hydrateEmbedExternal(
236
+
embed.media,
237
+
postAuthorDid,
238
+
imgcdn
239
+
);
218
240
} else if (AppBskyEmbedVideo.isMain(embed.media)) {
219
-
hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid);
241
+
hydratedMedia = hydrateEmbedVideo(
242
+
embed.media,
243
+
postAuthorDid,
244
+
videocdn
245
+
);
220
246
}
221
247
222
248
if (hydratedMedia) {
···
226
252
usequerypostresults?.data,
227
253
quotedProfile,
228
254
queryidentityresult?.data,
255
+
imgcdn
229
256
);
230
257
}
231
258
}
···
236
263
})();
237
264
238
265
const isLoading = isRecordType
239
-
? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading
266
+
? usequerypostresults?.isLoading ||
267
+
isLoadingProfile ||
268
+
queryidentityresult?.isLoading
240
269
: false;
241
270
242
-
const error = usequerypostresults?.error || profileError || queryidentityresult?.error;
271
+
const error =
272
+
usequerypostresults?.error || profileError || queryidentityresult?.error;
243
273
244
274
return { data: hydratedEmbed, isLoading, error };
245
-
}
275
+
}