@streamplace/components 0.7.35 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/components/content-metadata/content-metadata-form.js +467 -0
  2. package/dist/components/content-metadata/content-rights.js +78 -0
  3. package/dist/components/content-metadata/content-warnings.js +68 -0
  4. package/dist/components/content-metadata/index.js +11 -0
  5. package/dist/components/mobile-player/player.js +4 -0
  6. package/dist/components/mobile-player/ui/report-modal.js +3 -2
  7. package/dist/components/ui/checkbox.js +87 -0
  8. package/dist/components/ui/dialog.js +188 -83
  9. package/dist/components/ui/primitives/input.js +13 -1
  10. package/dist/components/ui/primitives/modal.js +2 -2
  11. package/dist/components/ui/select.js +89 -0
  12. package/dist/components/ui/textarea.js +23 -4
  13. package/dist/components/ui/toast.js +464 -114
  14. package/dist/components/ui/tooltip.js +103 -0
  15. package/dist/index.js +2 -0
  16. package/dist/lib/metadata-constants.js +157 -0
  17. package/dist/lib/theme/theme.js +5 -3
  18. package/dist/streamplace-provider/index.js +14 -4
  19. package/dist/streamplace-store/content-metadata-actions.js +124 -0
  20. package/dist/streamplace-store/streamplace-store.js +22 -5
  21. package/dist/streamplace-store/user.js +67 -7
  22. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  23. package/package.json +3 -3
  24. package/src/components/content-metadata/content-metadata-form.tsx +893 -0
  25. package/src/components/content-metadata/content-rights.tsx +104 -0
  26. package/src/components/content-metadata/content-warnings.tsx +100 -0
  27. package/src/components/content-metadata/index.tsx +10 -0
  28. package/src/components/mobile-player/player.tsx +5 -0
  29. package/src/components/mobile-player/ui/report-modal.tsx +13 -7
  30. package/src/components/ui/checkbox.tsx +147 -0
  31. package/src/components/ui/dialog.tsx +319 -99
  32. package/src/components/ui/primitives/input.tsx +19 -2
  33. package/src/components/ui/primitives/modal.tsx +4 -2
  34. package/src/components/ui/select.tsx +175 -0
  35. package/src/components/ui/textarea.tsx +47 -29
  36. package/src/components/ui/toast.tsx +785 -179
  37. package/src/components/ui/tooltip.tsx +131 -0
  38. package/src/index.tsx +3 -0
  39. package/src/lib/metadata-constants.ts +180 -0
  40. package/src/lib/theme/theme.tsx +10 -6
  41. package/src/streamplace-provider/index.tsx +20 -2
  42. package/src/streamplace-store/content-metadata-actions.tsx +145 -0
  43. package/src/streamplace-store/streamplace-store.tsx +41 -4
  44. package/src/streamplace-store/user.tsx +71 -7
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,893 @@
1
+ import { forwardRef, useCallback, useEffect, useState } from "react";
2
+ import { ScrollView, View } from "react-native";
3
+ import {
4
+ CONTENT_WARNINGS,
5
+ LICENSE_OPTIONS,
6
+ } from "../../lib/metadata-constants";
7
+
8
+ import {
9
+ PlaceStreamMetadataConfiguration,
10
+ PlaceStreamMetadataContentRights,
11
+ PlaceStreamMetadataDistributionPolicy,
12
+ } from "streamplace";
13
+ import {
14
+ useGetBroadcasterDID,
15
+ useGetContentMetadata,
16
+ useSaveContentMetadata,
17
+ } from "../../streamplace-store/content-metadata-actions";
18
+ import {
19
+ useDID,
20
+ useStreamplaceStore,
21
+ } from "../../streamplace-store/streamplace-store";
22
+ import { usePDSAgent } from "../../streamplace-store/xrpc";
23
+ import * as zero from "../../ui";
24
+ import { Button } from "../ui/button";
25
+ import { Checkbox } from "../ui/checkbox";
26
+ import { Input } from "../ui/input";
27
+ import { Select } from "../ui/select";
28
+ import { Text } from "../ui/text";
29
+ import { Textarea } from "../ui/textarea";
30
+ import { useToast } from "../ui/toast";
31
+ import { Tooltip } from "../ui/tooltip";
32
+
33
+ const { p, r, bg, borders, w, text, layout, gap, flex } = zero;
34
+
35
+ export interface ContentMetadataFormProps {
36
+ showUpdateButton?: boolean;
37
+ onMetadataChange?: (
38
+ metadata: PlaceStreamMetadataConfiguration.Record,
39
+ ) => void;
40
+ initialMetadata?: PlaceStreamMetadataConfiguration.Record;
41
+ style?: any;
42
+ }
43
+
44
+ // ButtonSelector component (same as in livestream-panel)
45
+ const ButtonSelector = ({
46
+ values,
47
+ selectedValue,
48
+ setSelectedValue,
49
+ disabledValues = [],
50
+ style = [],
51
+ }: {
52
+ values: { label: string; value: string }[];
53
+ selectedValue: string;
54
+ setSelectedValue: (value: string) => void;
55
+ disabledValues?: string[];
56
+ style?: any[];
57
+ }) => (
58
+ <View style={[layout.flex.row, gap.all[1], ...style]}>
59
+ {values.map(({ label, value }) => (
60
+ <Button
61
+ key={value}
62
+ variant={selectedValue === value ? "primary" : "secondary"}
63
+ size="pill"
64
+ disabled={disabledValues.includes(value)}
65
+ onPress={() => setSelectedValue(value)}
66
+ style={[
67
+ r.md,
68
+ {
69
+ opacity: disabledValues.includes(value) ? 0.5 : 1,
70
+ },
71
+ ]}
72
+ >
73
+ <Text
74
+ style={[
75
+ selectedValue === value ? text.white : text.gray[300],
76
+ { fontSize: 14, fontWeight: "600" },
77
+ ]}
78
+ >
79
+ {label}
80
+ </Text>
81
+ </Button>
82
+ ))}
83
+ </View>
84
+ );
85
+
86
+ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
87
+ (
88
+ { showUpdateButton = false, onMetadataChange, initialMetadata, style },
89
+ ref,
90
+ ) => {
91
+ const pdsAgent = usePDSAgent();
92
+ const did = useDID();
93
+ const getContentMetadata = useGetContentMetadata();
94
+ const saveContentMetadata = useSaveContentMetadata();
95
+ const toast = useToast();
96
+
97
+ // Local state for metadata
98
+ const [contentWarnings, setContentWarnings] = useState<string[]>([]);
99
+ const [distributionPolicy, setDistributionPolicy] =
100
+ useState<PlaceStreamMetadataDistributionPolicy.Main>({});
101
+ const [contentRights, setContentRights] =
102
+ useState<PlaceStreamMetadataContentRights.Main>({});
103
+ const [selectedLicense, setSelectedLicense] = useState<string>("");
104
+ const [customLicenseText, setCustomLicenseText] = useState<string>("");
105
+ const [loading, setLoading] = useState(false);
106
+ const [hasMetadata, setHasMetadata] = useState(false);
107
+
108
+ // State for section toggles
109
+ const [activeSection, setActiveSection] =
110
+ useState<string>("contentWarnings");
111
+
112
+ const currentYear = new Date().getFullYear();
113
+
114
+ const getBroadcasterDID = useGetBroadcasterDID();
115
+ useEffect(() => {
116
+ getBroadcasterDID();
117
+ }, [getBroadcasterDID]);
118
+
119
+ const broadcasterDID = useStreamplaceStore((state) => state.broadcasterDID);
120
+
121
+ // Load existing metadata on mount or from initialMetadata prop
122
+ useEffect(() => {
123
+ if (initialMetadata) {
124
+ // Use provided initial metadata
125
+ if (initialMetadata.contentWarnings?.warnings) {
126
+ setContentWarnings(initialMetadata.contentWarnings.warnings);
127
+ }
128
+ if (initialMetadata.distributionPolicy) {
129
+ setDistributionPolicy(initialMetadata.distributionPolicy);
130
+ }
131
+ if (initialMetadata.contentRights) {
132
+ setContentRights(initialMetadata.contentRights);
133
+ setSelectedLicense(initialMetadata.contentRights.license || "");
134
+ }
135
+ return;
136
+ }
137
+
138
+ const loadMetadata = async () => {
139
+ if (!pdsAgent || !did) return;
140
+
141
+ try {
142
+ const metadata = await getContentMetadata();
143
+ if (metadata?.record) {
144
+ setHasMetadata(true);
145
+ if (metadata.record.contentWarnings?.warnings) {
146
+ setContentWarnings(
147
+ metadata.record.contentWarnings.warnings as string[],
148
+ );
149
+ }
150
+ if (metadata.record.distributionPolicy) {
151
+ setDistributionPolicy(metadata.record.distributionPolicy);
152
+ }
153
+ if (metadata.record.contentRights) {
154
+ setContentRights(metadata.record.contentRights);
155
+ setSelectedLicense(metadata.record.contentRights.license || "");
156
+ }
157
+ }
158
+ } catch (error) {
159
+ // No existing metadata is fine
160
+ console.log("No existing metadata found");
161
+ }
162
+ };
163
+
164
+ loadMetadata();
165
+ }, [pdsAgent, did, initialMetadata]);
166
+
167
+ const handleContentWarningChange = useCallback(
168
+ (warning: string, checked: boolean) => {
169
+ const newWarnings = checked
170
+ ? [...contentWarnings, warning]
171
+ : contentWarnings.filter((w) => w !== warning);
172
+
173
+ setContentWarnings(newWarnings);
174
+
175
+ if (onMetadataChange) {
176
+ onMetadataChange({
177
+ $type: "place.stream.metadata.configuration",
178
+ contentWarnings: { warnings: newWarnings },
179
+ distributionPolicy,
180
+ contentRights,
181
+ });
182
+ }
183
+ },
184
+ [contentWarnings, distributionPolicy, contentRights, onMetadataChange],
185
+ );
186
+
187
+ // Notify parent component when metadata changes
188
+ useEffect(() => {
189
+ if (onMetadataChange) {
190
+ onMetadataChange({
191
+ $type: "place.stream.metadata.configuration",
192
+ contentWarnings: { warnings: contentWarnings },
193
+ distributionPolicy,
194
+ contentRights,
195
+ });
196
+ }
197
+ }, [contentWarnings, distributionPolicy, contentRights, onMetadataChange]);
198
+
199
+ // Handle distribution policy changes
200
+ const handleDistributionPolicyChange = useCallback(
201
+ ({
202
+ deleteAfter,
203
+ allowedBroadcasters,
204
+ }: {
205
+ deleteAfter?: string;
206
+ allowedBroadcasters?: string;
207
+ }) => {
208
+ let newDistributionPolicy: PlaceStreamMetadataDistributionPolicy.Main =
209
+ {
210
+ ...distributionPolicy,
211
+ };
212
+ if (typeof deleteAfter === "string") {
213
+ let duration = parseInt(deleteAfter, 10);
214
+ if (isNaN(duration)) {
215
+ newDistributionPolicy.deleteAfter = undefined;
216
+ } else {
217
+ if (isNaN(duration) || duration < 0) {
218
+ duration = -1;
219
+ }
220
+ newDistributionPolicy.deleteAfter =
221
+ duration === 0 ? undefined : duration;
222
+ }
223
+ }
224
+ if (typeof allowedBroadcasters === "string") {
225
+ newDistributionPolicy.allowedBroadcasters =
226
+ allowedBroadcasters.split("\n");
227
+ }
228
+ setDistributionPolicy(newDistributionPolicy);
229
+
230
+ if (onMetadataChange) {
231
+ onMetadataChange({
232
+ $type: "place.stream.metadata.configuration",
233
+ contentWarnings: { warnings: contentWarnings },
234
+ distributionPolicy: newDistributionPolicy,
235
+ contentRights,
236
+ });
237
+ }
238
+ },
239
+ [
240
+ contentWarnings,
241
+ contentRights,
242
+ onMetadataChange,
243
+ distributionPolicy,
244
+ setDistributionPolicy,
245
+ ],
246
+ );
247
+
248
+ // Handle content rights changes
249
+ const handleContentRightsChange = useCallback(
250
+ (field: string, value: any) => {
251
+ const newRights = { ...contentRights, [field]: value };
252
+ setContentRights(newRights);
253
+
254
+ if (onMetadataChange) {
255
+ onMetadataChange({
256
+ $type: "place.stream.metadata.configuration",
257
+ contentWarnings: { warnings: contentWarnings },
258
+ distributionPolicy,
259
+ contentRights: newRights,
260
+ });
261
+ }
262
+ },
263
+ [contentWarnings, distributionPolicy, contentRights, onMetadataChange],
264
+ );
265
+
266
+ const handleSave = useCallback(async () => {
267
+ setLoading(true);
268
+ try {
269
+ // Build the metadata object, only including non-empty fields
270
+ const metadata: PlaceStreamMetadataConfiguration.Record = {
271
+ $type: "place.stream.metadata.configuration",
272
+ };
273
+
274
+ // Only include contentWarnings if it has values
275
+ if (contentWarnings && contentWarnings.length > 0) {
276
+ metadata.contentWarnings = { warnings: contentWarnings };
277
+ }
278
+
279
+ // Only include contentRights if it has actual values
280
+ const rightsWithLicense = {
281
+ ...contentRights,
282
+ license:
283
+ selectedLicense === "custom"
284
+ ? customLicenseText
285
+ : selectedLicense || undefined,
286
+ };
287
+
288
+ // Filter out empty values from contentRights and convert copyrightYear to number
289
+ const filteredRights = Object.fromEntries(
290
+ Object.entries(rightsWithLicense)
291
+ .filter(
292
+ ([_, value]) =>
293
+ value !== undefined && value !== null && value !== "",
294
+ )
295
+ .map(([key, value]) => {
296
+ // Convert copyrightYear to integer as per lexicon
297
+ if (key === "copyrightYear" && typeof value === "string") {
298
+ const year = parseInt(value, 10);
299
+ return [key, isNaN(year) ? undefined : year];
300
+ }
301
+ return [key, value];
302
+ })
303
+ .filter(([_, value]) => value !== undefined),
304
+ );
305
+
306
+ if (Object.keys(filteredRights).length > 0) {
307
+ metadata.contentRights = filteredRights;
308
+ }
309
+
310
+ metadata.distributionPolicy = {
311
+ ...distributionPolicy,
312
+ };
313
+
314
+ if (distributionPolicy?.allowedBroadcasters) {
315
+ const filteredBs = distributionPolicy.allowedBroadcasters.filter(
316
+ (broadcaster) => broadcaster !== "",
317
+ );
318
+ metadata.distributionPolicy.allowedBroadcasters = filteredBs;
319
+ setDistributionPolicy({
320
+ ...distributionPolicy,
321
+ allowedBroadcasters: filteredBs,
322
+ });
323
+ }
324
+
325
+ await saveContentMetadata(metadata);
326
+ setHasMetadata(true);
327
+ // Show success toast
328
+ toast.show(
329
+ hasMetadata ? "Content metadata updated" : "Content metadata created",
330
+ "Your settings have been saved successfully",
331
+ );
332
+ } catch (error) {
333
+ console.error("Failed to save metadata:", error);
334
+ // Show error toast
335
+ toast.show("Failed to save metadata", "Please try again later");
336
+ } finally {
337
+ setLoading(false);
338
+ }
339
+ }, [
340
+ contentWarnings,
341
+ contentRights,
342
+ selectedLicense,
343
+ customLicenseText,
344
+ hasMetadata,
345
+ saveContentMetadata,
346
+ ]);
347
+
348
+ return (
349
+ <>
350
+ <ScrollView
351
+ ref={ref}
352
+ style={[{ flex: 1 }, style]}
353
+ showsVerticalScrollIndicator={false}
354
+ contentContainerStyle={{ flexGrow: 1 }}
355
+ >
356
+ <View style={[gap.all[8], w.percent[100], { alignItems: "stretch" }]}>
357
+ {/* Section Selector */}
358
+ <View style={[gap.all[4], w.percent[100]]}>
359
+ <ButtonSelector
360
+ values={[
361
+ { label: "Content Warnings", value: "contentWarnings" },
362
+ { label: "Content Rights", value: "contentRights" },
363
+ { label: "Distribution", value: "distribution" },
364
+ ]}
365
+ selectedValue={activeSection}
366
+ setSelectedValue={setActiveSection}
367
+ style={[{ marginVertical: -2, flexDirection: "column" }]}
368
+ />
369
+ </View>
370
+
371
+ {/* Content Warnings Section */}
372
+ {activeSection === "contentWarnings" && (
373
+ <View style={[gap.all[3], w.percent[100]]}>
374
+ <View
375
+ style={[
376
+ layout.flex.row,
377
+ layout.flex.alignCenter,
378
+ w.percent[100],
379
+ ]}
380
+ >
381
+ <Text
382
+ style={[
383
+ text.neutral[300],
384
+ {
385
+ minWidth: 100,
386
+ textAlign: "left",
387
+ paddingBottom: 8,
388
+ fontSize: 14,
389
+ },
390
+ ]}
391
+ >
392
+ Content Warnings
393
+ </Text>
394
+ <Text
395
+ style={[text.gray[500], { fontSize: 12, paddingBottom: 8 }]}
396
+ >
397
+ optional
398
+ </Text>
399
+ </View>
400
+ <View style={[gap.all[2], w.percent[100]]}>
401
+ {CONTENT_WARNINGS.map((warning) => (
402
+ <View key={warning.value} style={[w.percent[100]]}>
403
+ <Tooltip content={warning.description} position="top">
404
+ <Checkbox
405
+ checked={contentWarnings.includes(warning.value)}
406
+ onCheckedChange={(checked) =>
407
+ handleContentWarningChange(warning.value, checked)
408
+ }
409
+ label={warning.label}
410
+ style={[{ fontSize: 12 }]}
411
+ />
412
+ </Tooltip>
413
+ </View>
414
+ ))}
415
+ </View>
416
+ </View>
417
+ )}
418
+
419
+ {/* Content Rights Section */}
420
+ {activeSection === "contentRights" && (
421
+ <View style={[gap.all[3], w.percent[100]]}>
422
+ <View
423
+ style={[
424
+ layout.flex.row,
425
+ layout.flex.alignCenter,
426
+ w.percent[100],
427
+ ]}
428
+ >
429
+ <Text
430
+ style={[
431
+ text.neutral[300],
432
+ {
433
+ minWidth: 100,
434
+ textAlign: "left",
435
+ paddingBottom: 8,
436
+ fontSize: 14,
437
+ },
438
+ ]}
439
+ >
440
+ Content Rights
441
+ </Text>
442
+ <Text
443
+ style={[text.gray[500], { fontSize: 12, paddingBottom: 8 }]}
444
+ >
445
+ optional
446
+ </Text>
447
+ </View>
448
+
449
+ <View style={[gap.all[3], w.percent[100]]}>
450
+ <View
451
+ style={[
452
+ layout.flex.row,
453
+ layout.flex.alignCenter,
454
+ w.percent[100],
455
+ ]}
456
+ >
457
+ <Text
458
+ style={[
459
+ text.neutral[300],
460
+ {
461
+ minWidth: 100,
462
+ textAlign: "left",
463
+ paddingBottom: 8,
464
+ fontSize: 14,
465
+ },
466
+ ]}
467
+ >
468
+ Copyright Year
469
+ </Text>
470
+ <View style={[flex.values[1]]}>
471
+ <Input
472
+ value={contentRights.copyrightYear?.toString() || ""}
473
+ onChange={(value) =>
474
+ handleContentRightsChange("copyrightYear", value)
475
+ }
476
+ placeholder={currentYear.toString()}
477
+ variant="filled"
478
+ inputStyle={[
479
+ p[3],
480
+ r.md,
481
+ bg.neutral[800],
482
+ text.white,
483
+ borders.width.thin,
484
+ borders.color.neutral[600],
485
+ w.percent[100],
486
+ ]}
487
+ />
488
+ </View>
489
+ </View>
490
+
491
+ <View
492
+ style={[
493
+ layout.flex.row,
494
+ layout.flex.alignCenter,
495
+ w.percent[100],
496
+ ]}
497
+ >
498
+ <Text
499
+ style={[
500
+ text.neutral[300],
501
+ {
502
+ minWidth: 100,
503
+ textAlign: "left",
504
+ paddingBottom: 8,
505
+ fontSize: 14,
506
+ },
507
+ ]}
508
+ >
509
+ License
510
+ </Text>
511
+ <View style={[flex.values[1]]}>
512
+ <Select
513
+ value={selectedLicense}
514
+ onValueChange={(value) => {
515
+ setSelectedLicense(value);
516
+ handleContentRightsChange(
517
+ "license",
518
+ value === "custom" ? customLicenseText : value,
519
+ );
520
+ }}
521
+ placeholder="Select a license"
522
+ items={LICENSE_OPTIONS.map((opt) => ({
523
+ label: opt.label,
524
+ value: opt.value,
525
+ description: opt.description,
526
+ }))}
527
+ style={[
528
+ p[3],
529
+ r.md,
530
+ bg.neutral[800],
531
+ text.white,
532
+ borders.width.thin,
533
+ borders.color.neutral[600],
534
+ w.percent[100],
535
+ ]}
536
+ />
537
+ </View>
538
+ </View>
539
+
540
+ {/* Custom License Text Input */}
541
+ {selectedLicense === "custom" && (
542
+ <View
543
+ style={[
544
+ layout.flex.row,
545
+ layout.flex.alignCenter,
546
+ w.percent[100],
547
+ ]}
548
+ >
549
+ <Text
550
+ style={[
551
+ text.neutral[300],
552
+ {
553
+ minWidth: 100,
554
+ textAlign: "left",
555
+ paddingBottom: 8,
556
+ fontSize: 14,
557
+ },
558
+ ]}
559
+ >
560
+ Custom License
561
+ </Text>
562
+ <View style={[flex.values[1]]}>
563
+ <Textarea
564
+ value={customLicenseText}
565
+ onChangeText={(value) => {
566
+ setCustomLicenseText(value);
567
+ if (selectedLicense === "custom") {
568
+ handleContentRightsChange("license", value);
569
+ }
570
+ }}
571
+ placeholder="Enter your custom license terms..."
572
+ style={[
573
+ p[3],
574
+ r.md,
575
+ bg.neutral[800],
576
+ text.white,
577
+ borders.width.thin,
578
+ borders.color.neutral[600],
579
+ w.percent[100],
580
+ ]}
581
+ />
582
+ </View>
583
+ </View>
584
+ )}
585
+
586
+ <View
587
+ style={[
588
+ layout.flex.row,
589
+ layout.flex.alignCenter,
590
+ w.percent[100],
591
+ ]}
592
+ >
593
+ <Text
594
+ style={[
595
+ text.neutral[300],
596
+ {
597
+ minWidth: 100,
598
+ textAlign: "left",
599
+ paddingBottom: 8,
600
+ fontSize: 14,
601
+ },
602
+ ]}
603
+ >
604
+ Copyright Notice
605
+ </Text>
606
+ <View style={[flex.values[1]]}>
607
+ <Textarea
608
+ value={contentRights.copyrightNotice || ""}
609
+ onChangeText={(value: string) =>
610
+ handleContentRightsChange("copyrightNotice", value)
611
+ }
612
+ placeholder="Enter your copyright notice..."
613
+ style={[
614
+ p[3],
615
+ r.md,
616
+ bg.neutral[800],
617
+ text.white,
618
+ borders.width.thin,
619
+ borders.color.neutral[600],
620
+ w.percent[100],
621
+ ]}
622
+ />
623
+ </View>
624
+ </View>
625
+
626
+ <View
627
+ style={[
628
+ layout.flex.row,
629
+ layout.flex.alignCenter,
630
+ w.percent[100],
631
+ ]}
632
+ >
633
+ <Text
634
+ style={[
635
+ text.neutral[300],
636
+ {
637
+ minWidth: 100,
638
+ textAlign: "left",
639
+ paddingBottom: 8,
640
+ fontSize: 14,
641
+ },
642
+ ]}
643
+ >
644
+ Credit Line
645
+ </Text>
646
+ <View style={[flex.values[1]]}>
647
+ <Textarea
648
+ value={contentRights.creditLine || ""}
649
+ onChangeText={(value: string) =>
650
+ handleContentRightsChange("creditLine", value)
651
+ }
652
+ placeholder="Enter your credit line..."
653
+ style={[
654
+ p[3],
655
+ r.md,
656
+ bg.neutral[800],
657
+ text.white,
658
+ borders.width.thin,
659
+ borders.color.neutral[600],
660
+ w.percent[100],
661
+ ]}
662
+ />
663
+ </View>
664
+ </View>
665
+ </View>
666
+ </View>
667
+ )}
668
+
669
+ {/* Distribution Section */}
670
+ {activeSection === "distribution" && (
671
+ <View style={[gap.all[3], w.percent[100]]}>
672
+ <View
673
+ style={[
674
+ layout.flex.row,
675
+ layout.flex.alignCenter,
676
+ w.percent[100],
677
+ ]}
678
+ >
679
+ <Text
680
+ style={[
681
+ text.neutral[300],
682
+ { minWidth: 100, textAlign: "left", paddingBottom: 8 },
683
+ ]}
684
+ >
685
+ Distribution
686
+ </Text>
687
+ <Text
688
+ style={[text.gray[500], { fontSize: 12, paddingBottom: 8 }]}
689
+ >
690
+ optional
691
+ </Text>
692
+ </View>
693
+
694
+ {/* allow everyone to distribute your content */}
695
+ <Tooltip
696
+ content="Distribution of your content is unlimited, but they still have to respect the deleteAfter policy below."
697
+ position="top"
698
+ >
699
+ <Checkbox
700
+ checked={
701
+ distributionPolicy.allowedBroadcasters?.includes("*") ||
702
+ false
703
+ }
704
+ onCheckedChange={(checked) =>
705
+ handleDistributionPolicyChange({
706
+ allowedBroadcasters: checked
707
+ ? "*"
708
+ : broadcasterDID || "",
709
+ })
710
+ }
711
+ label={"Allow everyone to distribute your content"}
712
+ style={[{ fontSize: 12 }]}
713
+ />
714
+ </Tooltip>
715
+
716
+ {/* allowedBroadcasters */}
717
+ {!distributionPolicy.allowedBroadcasters?.includes("*") && (
718
+ <View style={[gap.all[3], w.percent[100]]}>
719
+ <View
720
+ style={[
721
+ layout.flex.row,
722
+ layout.flex.alignCenter,
723
+ w.percent[100],
724
+ ]}
725
+ >
726
+ <Text
727
+ style={[
728
+ text.neutral[300],
729
+ {
730
+ minWidth: 100,
731
+ textAlign: "left",
732
+ paddingBottom: 8,
733
+ fontSize: 14,
734
+ },
735
+ ]}
736
+ >
737
+ Allowed<br></br>Broadcasters
738
+ </Text>
739
+ <View style={[flex.values[1]]}>
740
+ <Text
741
+ style={[
742
+ text.gray[500],
743
+ { fontSize: 12, paddingBottom: 4 },
744
+ ]}
745
+ >
746
+ Enter the did:webs of the broadcasters you want to
747
+ allow to distribute your content, one per line.
748
+ </Text>
749
+ <Input
750
+ multiline={true}
751
+ numberOfLines={4}
752
+ value={
753
+ distributionPolicy.allowedBroadcasters?.join(
754
+ "\n",
755
+ ) || ""
756
+ }
757
+ onChange={(value) => {
758
+ handleDistributionPolicyChange({
759
+ allowedBroadcasters: value,
760
+ });
761
+ }}
762
+ variant="filled"
763
+ inputStyle={[
764
+ p[3],
765
+ r.md,
766
+ bg.neutral[800],
767
+ text.white,
768
+ borders.width.thin,
769
+ borders.color.neutral[600],
770
+ w.percent[100],
771
+ ]}
772
+ />
773
+ </View>
774
+ </View>
775
+ </View>
776
+ )}
777
+
778
+ <Tooltip
779
+ content="Anyone may archive your content indefinitely."
780
+ position="top"
781
+ >
782
+ <Checkbox
783
+ checked={distributionPolicy.deleteAfter === -1}
784
+ onCheckedChange={(checked) => {
785
+ if (checked) {
786
+ handleDistributionPolicyChange({
787
+ deleteAfter: "-1",
788
+ });
789
+ } else {
790
+ handleDistributionPolicyChange({
791
+ deleteAfter: "300",
792
+ });
793
+ }
794
+ }}
795
+ label={"Allow everyone to archive your content"}
796
+ style={[{ fontSize: 12 }]}
797
+ />
798
+ </Tooltip>
799
+
800
+ {/* deleteAfter */}
801
+ {distributionPolicy.deleteAfter !== -1 && (
802
+ <View style={[gap.all[3], w.percent[100]]}>
803
+ <View
804
+ style={[
805
+ layout.flex.row,
806
+ layout.flex.alignCenter,
807
+ w.percent[100],
808
+ ]}
809
+ >
810
+ <Text
811
+ style={[
812
+ text.neutral[300],
813
+ {
814
+ minWidth: 100,
815
+ textAlign: "left",
816
+ paddingBottom: 8,
817
+ fontSize: 14,
818
+ },
819
+ ]}
820
+ >
821
+ Delete After
822
+ </Text>
823
+ <View style={[flex.values[1]]}>
824
+ <Text
825
+ style={[
826
+ text.gray[500],
827
+ { fontSize: 12, paddingBottom: 4 },
828
+ ]}
829
+ >
830
+ Duration in seconds (e.g., 300 for 5 minutes)
831
+ </Text>
832
+ <Input
833
+ value={
834
+ distributionPolicy.deleteAfter?.toString() || ""
835
+ }
836
+ onChange={(value) => {
837
+ handleDistributionPolicyChange({
838
+ deleteAfter: value,
839
+ });
840
+ }}
841
+ keyboardType="numeric"
842
+ variant="filled"
843
+ inputStyle={[
844
+ p[3],
845
+ r.md,
846
+ bg.neutral[800],
847
+ text.white,
848
+ borders.width.thin,
849
+ borders.color.neutral[600],
850
+ w.percent[100],
851
+ ]}
852
+ />
853
+ </View>
854
+ </View>
855
+ </View>
856
+ )}
857
+ </View>
858
+ )}
859
+
860
+ {/* Save Button - Always visible */}
861
+ <View style={[layout.flex.center, w.percent[100]]}>
862
+ <Button
863
+ onPress={handleSave}
864
+ loading={loading}
865
+ disabled={loading}
866
+ style={[
867
+ bg.primary[500],
868
+ r.md,
869
+ gap.all[3],
870
+ { minWidth: 200 },
871
+ layout.flex.center,
872
+ { opacity: loading ? 0.5 : 1 },
873
+ ]}
874
+ >
875
+ <Text
876
+ style={[
877
+ text.white,
878
+ w.percent[100],
879
+ { fontSize: 16, fontWeight: "bold" },
880
+ ]}
881
+ >
882
+ {hasMetadata ? "Update Metadata" : "Save Metadata"}
883
+ </Text>
884
+ </Button>
885
+ </View>
886
+ </View>
887
+ </ScrollView>
888
+ </>
889
+ );
890
+ },
891
+ );
892
+
893
+ ContentMetadataForm.displayName = "ContentMetadataForm";