@streamplace/components 0.8.0 → 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.
@@ -6,10 +6,19 @@ import {
6
6
  } from "../../lib/metadata-constants";
7
7
 
8
8
  import {
9
+ PlaceStreamMetadataConfiguration,
10
+ PlaceStreamMetadataContentRights,
11
+ PlaceStreamMetadataDistributionPolicy,
12
+ } from "streamplace";
13
+ import {
14
+ useGetBroadcasterDID,
9
15
  useGetContentMetadata,
10
16
  useSaveContentMetadata,
11
17
  } from "../../streamplace-store/content-metadata-actions";
12
- import { useDID } from "../../streamplace-store/streamplace-store";
18
+ import {
19
+ useDID,
20
+ useStreamplaceStore,
21
+ } from "../../streamplace-store/streamplace-store";
13
22
  import { usePDSAgent } from "../../streamplace-store/xrpc";
14
23
  import * as zero from "../../ui";
15
24
  import { Button } from "../ui/button";
@@ -23,29 +32,12 @@ import { Tooltip } from "../ui/tooltip";
23
32
 
24
33
  const { p, r, bg, borders, w, text, layout, gap, flex } = zero;
25
34
 
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
35
  export interface ContentMetadataFormProps {
46
36
  showUpdateButton?: boolean;
47
- onMetadataChange?: (metadata: ContentMetadata) => void;
48
- initialMetadata?: ContentMetadata;
37
+ onMetadataChange?: (
38
+ metadata: PlaceStreamMetadataConfiguration.Record,
39
+ ) => void;
40
+ initialMetadata?: PlaceStreamMetadataConfiguration.Record;
49
41
  style?: any;
50
42
  }
51
43
 
@@ -105,11 +97,11 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
105
97
  // Local state for metadata
106
98
  const [contentWarnings, setContentWarnings] = useState<string[]>([]);
107
99
  const [distributionPolicy, setDistributionPolicy] =
108
- useState<DistributionPolicy>({});
109
- const [contentRights, setContentRights] = useState<Rights>({});
100
+ useState<PlaceStreamMetadataDistributionPolicy.Main>({});
101
+ const [contentRights, setContentRights] =
102
+ useState<PlaceStreamMetadataContentRights.Main>({});
110
103
  const [selectedLicense, setSelectedLicense] = useState<string>("");
111
104
  const [customLicenseText, setCustomLicenseText] = useState<string>("");
112
- const [customDateTime, setCustomDateTime] = useState<string>("");
113
105
  const [loading, setLoading] = useState(false);
114
106
  const [hasMetadata, setHasMetadata] = useState(false);
115
107
 
@@ -119,6 +111,13 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
119
111
 
120
112
  const currentYear = new Date().getFullYear();
121
113
 
114
+ const getBroadcasterDID = useGetBroadcasterDID();
115
+ useEffect(() => {
116
+ getBroadcasterDID();
117
+ }, [getBroadcasterDID]);
118
+
119
+ const broadcasterDID = useStreamplaceStore((state) => state.broadcasterDID);
120
+
122
121
  // Load existing metadata on mount or from initialMetadata prop
123
122
  useEffect(() => {
124
123
  if (initialMetadata) {
@@ -128,11 +127,6 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
128
127
  }
129
128
  if (initialMetadata.distributionPolicy) {
130
129
  setDistributionPolicy(initialMetadata.distributionPolicy);
131
- setCustomDateTime(
132
- initialMetadata.distributionPolicy.deleteAfter
133
- ? String(initialMetadata.distributionPolicy.deleteAfter)
134
- : "",
135
- );
136
130
  }
137
131
  if (initialMetadata.contentRights) {
138
132
  setContentRights(initialMetadata.contentRights);
@@ -155,11 +149,6 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
155
149
  }
156
150
  if (metadata.record.distributionPolicy) {
157
151
  setDistributionPolicy(metadata.record.distributionPolicy);
158
- setCustomDateTime(
159
- metadata.record.distributionPolicy.deleteAfter
160
- ? String(metadata.record.distributionPolicy.deleteAfter)
161
- : "",
162
- );
163
152
  }
164
153
  if (metadata.record.contentRights) {
165
154
  setContentRights(metadata.record.contentRights);
@@ -185,6 +174,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
185
174
 
186
175
  if (onMetadataChange) {
187
176
  onMetadataChange({
177
+ $type: "place.stream.metadata.configuration",
188
178
  contentWarnings: { warnings: newWarnings },
189
179
  distributionPolicy,
190
180
  contentRights,
@@ -198,6 +188,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
198
188
  useEffect(() => {
199
189
  if (onMetadataChange) {
200
190
  onMetadataChange({
191
+ $type: "place.stream.metadata.configuration",
201
192
  contentWarnings: { warnings: contentWarnings },
202
193
  distributionPolicy,
203
194
  contentRights,
@@ -207,19 +198,40 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
207
198
 
208
199
  // Handle distribution policy changes
209
200
  const handleDistributionPolicyChange = useCallback(
210
- (deleteAfter: string) => {
211
- let duration = parseInt(deleteAfter, 10);
212
- if (isNaN(duration) || duration < 0) {
213
- duration = 0;
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
+ }
214
223
  }
215
- const newPolicy = duration > 0 ? { deleteAfter: duration } : {};
216
- console.log("newPolicy", newPolicy);
217
- setDistributionPolicy(newPolicy);
224
+ if (typeof allowedBroadcasters === "string") {
225
+ newDistributionPolicy.allowedBroadcasters =
226
+ allowedBroadcasters.split("\n");
227
+ }
228
+ setDistributionPolicy(newDistributionPolicy);
218
229
 
219
230
  if (onMetadataChange) {
220
231
  onMetadataChange({
232
+ $type: "place.stream.metadata.configuration",
221
233
  contentWarnings: { warnings: contentWarnings },
222
- distributionPolicy: newPolicy,
234
+ distributionPolicy: newDistributionPolicy,
223
235
  contentRights,
224
236
  });
225
237
  }
@@ -241,6 +253,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
241
253
 
242
254
  if (onMetadataChange) {
243
255
  onMetadataChange({
256
+ $type: "place.stream.metadata.configuration",
244
257
  contentWarnings: { warnings: contentWarnings },
245
258
  distributionPolicy,
246
259
  contentRights: newRights,
@@ -254,20 +267,13 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
254
267
  setLoading(true);
255
268
  try {
256
269
  // Build the metadata object, only including non-empty fields
257
- const metadata: any = {};
270
+ const metadata: PlaceStreamMetadataConfiguration.Record = {
271
+ $type: "place.stream.metadata.configuration",
272
+ };
258
273
 
259
274
  // Only include contentWarnings if it has values
260
275
  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("");
276
+ metadata.contentWarnings = { warnings: contentWarnings };
271
277
  }
272
278
 
273
279
  // Only include contentRights if it has actual values
@@ -301,6 +307,21 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
301
307
  metadata.contentRights = filteredRights;
302
308
  }
303
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
+
304
325
  await saveContentMetadata(metadata);
305
326
  setHasMetadata(true);
306
327
  // Show success toast
@@ -320,7 +341,6 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
320
341
  contentRights,
321
342
  selectedLicense,
322
343
  customLicenseText,
323
- customDateTime,
324
344
  hasMetadata,
325
345
  saveContentMetadata,
326
346
  ]);
@@ -670,58 +690,170 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
670
690
  optional
671
691
  </Text>
672
692
  </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
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
682
720
  style={[
683
- text.neutral[300],
684
- {
685
- minWidth: 100,
686
- textAlign: "left",
687
- paddingBottom: 8,
688
- fontSize: 14,
689
- },
721
+ layout.flex.row,
722
+ layout.flex.alignCenter,
723
+ w.percent[100],
690
724
  ]}
691
725
  >
692
- Delete After
693
- </Text>
694
- <View style={[flex.values[1]]}>
695
726
  <Text
696
727
  style={[
697
- text.gray[500],
698
- { fontSize: 12, paddingBottom: 4 },
728
+ text.neutral[300],
729
+ {
730
+ minWidth: 100,
731
+ textAlign: "left",
732
+ paddingBottom: 8,
733
+ fontSize: 14,
734
+ },
699
735
  ]}
700
736
  >
701
- Duration in seconds (e.g., 300 for 5 minutes) or 0 to
702
- allow archiving your stream
737
+ Allowed<br></br>Broadcasters
703
738
  </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],
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
+ },
720
819
  ]}
721
- />
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>
722
854
  </View>
723
855
  </View>
724
- </View>
856
+ )}
725
857
  </View>
726
858
  )}
727
859
 
@@ -5,14 +5,6 @@ export { ContentMetadataForm } from "./content-metadata-form";
5
5
  export { ContentRights } from "./content-rights";
6
6
  export { ContentWarnings } from "./content-warnings";
7
7
 
8
- // Types
9
- export type {
10
- ContentMetadata,
11
- ContentMetadataFormProps,
12
- DistributionPolicy,
13
- Rights,
14
- } from "./content-metadata-form";
15
-
16
8
  export type { ContentRightsProps } from "./content-rights";
17
9
 
18
10
  export type { ContentWarningsProps } from "./content-warnings";
@@ -1,50 +1,53 @@
1
+ import { PlaceStreamMetadataConfiguration } from "streamplace";
1
2
  import {
2
3
  ContentMetadataResult,
3
4
  useDID,
4
5
  useSetContentMetadata,
6
+ useStreamplaceStore,
5
7
  } from "./streamplace-store";
6
8
  import { usePDSAgent } from "./xrpc";
7
9
 
10
+ export const useGetBroadcasterDID = () => {
11
+ const pdsAgent = usePDSAgent();
12
+ const did = useDID();
13
+ const setBroadcasterDID = useStreamplaceStore(
14
+ (state) => state.setBroadcasterDID,
15
+ );
16
+ const setServerDID = useStreamplaceStore((state) => state.setServerDID);
17
+ return async () => {
18
+ if (!pdsAgent || !did) {
19
+ throw new Error("No PDS agent or DID available");
20
+ }
21
+
22
+ const result = await pdsAgent.place.stream.broadcast.getBroadcaster();
23
+ if (!result.success) {
24
+ throw new Error("Failed to get broadcaster DID");
25
+ }
26
+ setBroadcasterDID(result.data.broadcaster);
27
+ if (result.data.server) {
28
+ setServerDID(result.data.server);
29
+ } else {
30
+ setServerDID(null);
31
+ }
32
+ };
33
+ };
34
+
8
35
  export const useSaveContentMetadata = () => {
9
36
  const pdsAgent = usePDSAgent();
10
37
  const did = useDID();
11
38
  const setContentMetadata = useSetContentMetadata();
12
39
 
13
- return async (params: {
14
- contentWarnings?: string[];
15
- distributionPolicy?: { deleteAfter?: number };
16
- contentRights?: Record<string, any>;
17
- rkey?: string;
18
- }) => {
40
+ return async (metadataRecord: PlaceStreamMetadataConfiguration.Record) => {
19
41
  if (!pdsAgent || !did) {
20
42
  throw new Error("No PDS agent or DID available");
21
43
  }
22
44
 
23
- const metadataRecord = {
24
- $type: "place.stream.metadata.configuration",
25
- createdAt: new Date().toISOString(),
26
- ...(params.contentWarnings &&
27
- params.contentWarnings.length > 0 && {
28
- contentWarnings: { warnings: params.contentWarnings },
29
- }),
30
- ...(params.distributionPolicy &&
31
- params.distributionPolicy.deleteAfter && {
32
- distributionPolicy: params.distributionPolicy,
33
- }),
34
- ...(params.contentRights &&
35
- Object.keys(params.contentRights).length > 0 && {
36
- contentRights: params.contentRights,
37
- }),
38
- };
39
-
40
- const rkey = params.rkey || "self";
41
-
42
45
  try {
43
46
  // Try to update existing record first
44
47
  const result = await (pdsAgent as any).com.atproto.repo.putRecord({
45
48
  repo: did,
46
49
  collection: "place.stream.metadata.configuration",
47
- rkey,
50
+ rkey: "self",
48
51
  record: metadataRecord,
49
52
  });
50
53
 
@@ -52,7 +55,7 @@ export const useSaveContentMetadata = () => {
52
55
  record: metadataRecord as any,
53
56
  uri: result.data.uri,
54
57
  cid: result.data.cid || "",
55
- rkey,
58
+ rkey: "self",
56
59
  };
57
60
 
58
61
  setContentMetadata(contentMetadata);
@@ -71,7 +74,7 @@ export const useSaveContentMetadata = () => {
71
74
  ).com.atproto.repo.createRecord({
72
75
  repo: did,
73
76
  collection: "place.stream.metadata.configuration",
74
- rkey,
77
+ rkey: "self",
75
78
  record: metadataRecord,
76
79
  });
77
80
 
@@ -79,7 +82,7 @@ export const useSaveContentMetadata = () => {
79
82
  record: metadataRecord as any,
80
83
  uri: createResult.data.uri,
81
84
  cid: createResult.data.cid || "",
82
- rkey,
85
+ rkey: "self",
83
86
  };
84
87
 
85
88
  setContentMetadata(contentMetadata);
@@ -44,6 +44,11 @@ export interface StreamplaceState {
44
44
  contentMetadata: ContentMetadataResult | null;
45
45
  setContentMetadata: (metadata: ContentMetadataResult | null) => void;
46
46
 
47
+ broadcasterDID: string | null;
48
+ setBroadcasterDID: (broadcasterDID: string | null) => void;
49
+ serverDID: string | null;
50
+ setServerDID: (serverDID: string | null) => void;
51
+
47
52
  // Volume state
48
53
  volume: number;
49
54
  muted: boolean;
@@ -81,6 +86,12 @@ export const makeStreamplaceStore = ({
81
86
  handle: null,
82
87
  chatProfile: null,
83
88
 
89
+ broadcasterDID: null,
90
+ setBroadcasterDID: (broadcasterDID: string | null) =>
91
+ set({ broadcasterDID }),
92
+ serverDID: null,
93
+ setServerDID: (serverDID: string | null) => set({ serverDID }),
94
+
84
95
  // Content metadata
85
96
  contentMetadata: null,
86
97
  setContentMetadata: (metadata) => set({ contentMetadata: metadata }),