tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
restore last title and add button to restore last image
Natalie B.
3 weeks ago
d77dfdff
ef36430c
+101
-13
1 changed file
expand all
collapse all
unified
split
js
app
components
live-dashboard
livestream-panel.tsx
+101
-13
js/app/components/live-dashboard/livestream-panel.tsx
···
6
formatHandle,
7
formatHandleWithAt,
8
Input,
0
9
Textarea,
10
Tooltip,
11
useCreateStreamRecord,
···
21
Image,
22
Platform,
23
ScrollView,
24
-
Text,
25
TouchableOpacity,
26
View,
27
} from "react-native";
···
80
selectedImage,
81
onImageSelect,
82
onImageRemove,
0
0
0
83
}: {
84
selectedImage?: string | File | Blob;
85
onImageSelect?: () => void;
86
onImageRemove?: () => void;
0
0
0
87
}) => {
88
const imageUrl = useMemo(() => {
89
if (!selectedImage) return undefined;
···
150
</TouchableOpacity>
151
</View>
152
) : (
153
-
<TouchableOpacity onPress={onImageSelect} style={containerStyle}>
154
-
<ImagePlus size={48} color="#6b7280" />
155
-
<Text style={[text.gray[400], { marginTop: 8, fontSize: 14 }]}>
156
-
Add thumbnail image
157
-
</Text>
158
-
<Text style={[text.gray[500], { fontSize: 12, marginTop: 4 }]}>
159
-
Optional • JPG, PNG up to 975KB
160
-
</Text>
161
-
</TouchableOpacity>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
162
)}
163
</View>
164
);
···
185
186
const [createPost, setCreatePost] = useState(true);
187
const [sendPushNotification, setSendPushNotification] = useState(true);
188
-
const [canonicalUrl, setCanonicalUrl] = useState<string>(
189
-
livestream?.record.canonicalUrl || "",
190
-
);
191
const defaultCanonicalUrl = useMemo(() => {
192
return `${url}/${profile && formatHandle(profile)}`;
193
}, [url, profile?.handle]);
···
196
if (!livestream) {
197
return;
198
}
0
0
0
0
0
0
0
199
if (
200
livestream.record.canonicalUrl &&
201
livestream.record.canonicalUrl !== defaultCanonicalUrl
202
) {
203
setCanonicalUrl(livestream.record.canonicalUrl);
204
}
0
0
205
if (
206
typeof livestream.record.notificationSettings?.pushNotification ===
207
"boolean"
···
210
livestream.record.notificationSettings.pushNotification,
211
);
212
}
0
0
213
setCreatePost(typeof livestream.record.post !== "undefined");
214
}, [livestream, defaultCanonicalUrl]);
215
···
320
setSelectedImage(undefined);
321
}, []);
322
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
323
const disabled = useMemo(
324
() => !userIsLive || loading || title.trim() === "",
325
[userIsLive, loading, title],
···
569
selectedImage={selectedImage}
570
onImageSelect={handleImageSelect}
571
onImageRemove={handleImageRemove}
0
0
0
572
/>
573
)}
574
···
6
formatHandle,
7
formatHandleWithAt,
8
Input,
9
+
Text,
10
Textarea,
11
Tooltip,
12
useCreateStreamRecord,
···
22
Image,
23
Platform,
24
ScrollView,
0
25
TouchableOpacity,
26
View,
27
} from "react-native";
···
80
selectedImage,
81
onImageSelect,
82
onImageRemove,
83
+
onUseLastImage,
84
+
hasLastImage,
85
+
onGoToMetadata,
86
}: {
87
selectedImage?: string | File | Blob;
88
onImageSelect?: () => void;
89
onImageRemove?: () => void;
90
+
onUseLastImage?: () => void;
91
+
hasLastImage?: boolean;
92
+
onGoToMetadata?: () => void;
93
}) => {
94
const imageUrl = useMemo(() => {
95
if (!selectedImage) return undefined;
···
156
</TouchableOpacity>
157
</View>
158
) : (
159
+
<>
160
+
<TouchableOpacity onPress={onImageSelect} style={containerStyle}>
161
+
<ImagePlus size={48} color="#6b7280" />
162
+
<Text style={[text.gray[400], { marginTop: 8, fontSize: 14 }]}>
163
+
Add thumbnail image
164
+
</Text>
165
+
<Text style={[text.gray[500], { fontSize: 12, marginTop: 4 }]}>
166
+
Optional • JPG, PNG up to 975KB
167
+
</Text>
168
+
</TouchableOpacity>
169
+
{hasLastImage && (
170
+
<Button
171
+
variant="secondary"
172
+
size="sm"
173
+
onPress={onUseLastImage}
174
+
style={[{ marginTop: 8 }]}
175
+
>
176
+
<Text style={[text.gray[300], { fontSize: 14 }]}>
177
+
Use Last Image
178
+
</Text>
179
+
</Button>
180
+
)}
181
+
</>
182
)}
183
</View>
184
);
···
205
206
const [createPost, setCreatePost] = useState(true);
207
const [sendPushNotification, setSendPushNotification] = useState(true);
208
+
const [canonicalUrl, setCanonicalUrl] = useState<string>("");
0
0
209
const defaultCanonicalUrl = useMemo(() => {
210
return `${url}/${profile && formatHandle(profile)}`;
211
}, [url, profile?.handle]);
···
214
if (!livestream) {
215
return;
216
}
217
+
218
+
// Prefill title with previous stream's title
219
+
if (livestream.record.title) {
220
+
setTitle(livestream.record.title);
221
+
}
222
+
223
+
// Prefill canonical URL
224
if (
225
livestream.record.canonicalUrl &&
226
livestream.record.canonicalUrl !== defaultCanonicalUrl
227
) {
228
setCanonicalUrl(livestream.record.canonicalUrl);
229
}
230
+
231
+
// Prefill notification settings
232
if (
233
typeof livestream.record.notificationSettings?.pushNotification ===
234
"boolean"
···
237
livestream.record.notificationSettings.pushNotification,
238
);
239
}
240
+
241
+
// Prefill post creation preference
242
setCreatePost(typeof livestream.record.post !== "undefined");
243
}, [livestream, defaultCanonicalUrl]);
244
···
349
setSelectedImage(undefined);
350
}, []);
351
352
+
const handleUseLastImage = useCallback(async () => {
353
+
if (!livestream?.record.thumb) return;
354
+
355
+
try {
356
+
const did = livestream.uri.split("/")[2];
357
+
const cid = (livestream.record.thumb.ref as any).$link;
358
+
359
+
let didDoc;
360
+
361
+
// Resolve the DID document based on DID method
362
+
if (did.startsWith("did:web:")) {
363
+
// For did:web, construct the URL directly
364
+
const domain = did.replace("did:web:", "").replace(/:/g, "/");
365
+
const didDocUrl = `https://${domain}/.well-known/did.json`;
366
+
const didResponse = await fetch(didDocUrl);
367
+
if (!didResponse.ok) {
368
+
throw new Error("Failed to resolve did:web document");
369
+
}
370
+
didDoc = await didResponse.json();
371
+
} else if (did.startsWith("did:plc:")) {
372
+
// For did:plc, use plc.directory
373
+
const didResponse = await fetch(`https://plc.directory/${did}`);
374
+
if (!didResponse.ok) {
375
+
throw new Error("Failed to resolve DID document");
376
+
}
377
+
didDoc = await didResponse.json();
378
+
} else {
379
+
throw new Error(`Unsupported DID method: ${did}`);
380
+
}
381
+
382
+
const pdsService = didDoc.service?.find(
383
+
(s: any) => s.id === "#atproto_pds",
384
+
);
385
+
386
+
if (!pdsService?.serviceEndpoint) {
387
+
throw new Error("No PDS service endpoint found in DID document");
388
+
}
389
+
390
+
// Construct the blob URL using the PDS endpoint
391
+
const thumbnailUrl = `${pdsService.serviceEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`;
392
+
393
+
// Fetch the image and convert to blob
394
+
const response = await fetch(thumbnailUrl);
395
+
if (!response.ok) {
396
+
throw new Error(`Failed to fetch blob: ${response.status}`);
397
+
}
398
+
const blob = await response.blob();
399
+
setSelectedImage(blob);
400
+
} catch (error) {
401
+
console.error("Failed to fetch last image:", error);
402
+
toast.show("Error", "Failed to load previous thumbnail", {
403
+
duration: 3,
404
+
});
405
+
}
406
+
}, [livestream, toast]);
407
+
408
const disabled = useMemo(
409
() => !userIsLive || loading || title.trim() === "",
410
[userIsLive, loading, title],
···
654
selectedImage={selectedImage}
655
onImageSelect={handleImageSelect}
656
onImageRemove={handleImageRemove}
657
+
onUseLastImage={handleUseLastImage}
658
+
hasLastImage={!!livestream?.record.thumb}
659
+
onGoToMetadata={() => handleModeChange("metadata")}
660
/>
661
)}
662