@streamplace/components 0.7.34 → 0.8.0

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