@streamplace/components 0.8.6 → 0.8.9

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.
@@ -1,10 +1,12 @@
1
- import { forwardRef } from "react";
2
- import { StyleSheet, View } from "react-native";
1
+ import { forwardRef, type ComponentProps } from "react";
2
+ import { StyleSheet } from "react-native";
3
+ import { useTheme, zero } from "../..";
3
4
  import { LICENSE_URL_LABELS } from "../../lib/metadata-constants";
4
- import { useTheme } from "../../lib/theme/theme";
5
5
  import { Text } from "../ui/text";
6
6
 
7
- export interface ContentRightsProps {
7
+ const { layout, gap, mt, text: textStyles } = zero;
8
+
9
+ export interface ContentRightsProps extends ComponentProps<typeof Text> {
8
10
  contentRights: {
9
11
  creator?: string;
10
12
  copyrightNotice?: string;
@@ -15,16 +17,13 @@ export interface ContentRightsProps {
15
17
  compact?: boolean;
16
18
  }
17
19
 
18
- export const ContentRights = forwardRef<any, ContentRightsProps>(
19
- ({ contentRights }, ref) => {
20
- const { theme } = useTheme();
21
-
20
+ export const ContentRights = forwardRef<Text, ContentRightsProps>(
21
+ ({ contentRights, compact, ...rest }, ref) => {
22
+ const { zero } = useTheme();
22
23
  if (!contentRights || Object.keys(contentRights).length === 0) {
23
24
  return null;
24
25
  }
25
26
 
26
- const styles = createStyles(theme);
27
-
28
27
  const formatLicense = (license: string) => {
29
28
  return LICENSE_URL_LABELS[license] || license;
30
29
  };
@@ -38,7 +37,11 @@ export const ContentRights = forwardRef<any, ContentRightsProps>(
38
37
  // }
39
38
 
40
39
  if (contentRights.copyrightYear) {
41
- elements.push(`© ${contentRights.copyrightYear.toString()}`);
40
+ elements.push(
41
+ `© ${contentRights.copyrightYear.toString()}${contentRights.creditLine ? " " + contentRights.creditLine : ""}`,
42
+ );
43
+ } else if (contentRights.creditLine) {
44
+ elements.push(contentRights.creditLine);
42
45
  }
43
46
 
44
47
  if (contentRights.license) {
@@ -49,56 +52,28 @@ export const ContentRights = forwardRef<any, ContentRightsProps>(
49
52
  elements.push(contentRights.copyrightNotice);
50
53
  }
51
54
 
52
- if (contentRights.creditLine) {
53
- elements.push(contentRights.creditLine);
55
+ if (elements.length > 0) {
56
+ elements[0] = "Stream content is " + elements[0];
57
+ }
58
+
59
+ if (elements.length == 0) {
60
+ return null;
54
61
  }
55
62
 
56
63
  return (
57
- <View ref={ref} style={styles.compactContainer}>
58
- <Text style={styles.compactText}>{elements.join(" • ")}</Text>
59
- </View>
64
+ <Text
65
+ ref={ref}
66
+ style={[
67
+ zero.text.mutedForeground,
68
+ mt[1],
69
+ StyleSheet.flatten(rest.style),
70
+ ]}
71
+ {...rest}
72
+ >
73
+ {elements.join(" • ")}
74
+ </Text>
60
75
  );
61
76
  },
62
77
  );
63
78
 
64
79
  ContentRights.displayName = "ContentRights";
65
-
66
- function createStyles(theme: any) {
67
- return StyleSheet.create({
68
- container: {
69
- paddingVertical: theme.spacing[3],
70
- },
71
- title: {
72
- fontSize: 14,
73
- fontWeight: "600",
74
- color: theme.colors.text,
75
- marginBottom: theme.spacing[2],
76
- },
77
- content: {
78
- gap: theme.spacing[2],
79
- },
80
- row: {
81
- flexDirection: "row",
82
- gap: theme.spacing[2],
83
- },
84
- label: {
85
- fontSize: 13,
86
- color: theme.colors.textMuted,
87
- },
88
- value: {
89
- fontSize: 13,
90
- color: theme.colors.text,
91
- },
92
- compactContainer: {
93
- flexDirection: "row",
94
- gap: theme.spacing[2],
95
- flexWrap: "wrap",
96
- marginTop: theme.spacing[1],
97
- },
98
- compactText: {
99
- fontSize: 14,
100
- fontWeight: "500",
101
- color: theme.colors.text,
102
- },
103
- });
104
- }
@@ -0,0 +1,94 @@
1
+ import { AlertTriangle, ChevronDown } from "lucide-react-native";
2
+ import { View } from "react-native";
3
+ import { zero } from "../..";
4
+ import { C2PA_WARNING_LABELS } from "../../lib/metadata-constants";
5
+ import { useTheme } from "../../lib/theme/theme";
6
+ import { pt, r } from "../../ui";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuTrigger,
10
+ ResponsiveDropdownMenuContent,
11
+ } from "../ui/dropdown";
12
+ import { Text } from "../ui/text";
13
+
14
+ const { px, py, gap, layout } = zero;
15
+
16
+ export interface ContentWarningBadgeProps {
17
+ warnings: string[];
18
+ }
19
+
20
+ export function ContentWarningBadge({ warnings }: ContentWarningBadgeProps) {
21
+ const { theme } = useTheme();
22
+
23
+ const getWarningLabel = (warning: string): string => {
24
+ return C2PA_WARNING_LABELS[warning] || warning;
25
+ };
26
+
27
+ if (!warnings || warnings.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <DropdownMenu>
33
+ <DropdownMenuTrigger>
34
+ <View
35
+ style={[
36
+ layout.flex.row,
37
+ layout.flex.align.center,
38
+ gap.all[2],
39
+ px[3],
40
+ py[2],
41
+ r.md,
42
+ { backgroundColor: theme.colors.warning + "20" },
43
+ ]}
44
+ >
45
+ <AlertTriangle size={14} color={theme.colors.warningForeground} />
46
+ <Text
47
+ size="sm"
48
+ weight="semibold"
49
+ style={{ color: theme.colors.warningForeground }}
50
+ >
51
+ Intended for certain audiences
52
+ </Text>
53
+ <ChevronDown size={14} color={theme.colors.warningForeground} />
54
+ </View>
55
+ </DropdownMenuTrigger>
56
+
57
+ <ResponsiveDropdownMenuContent>
58
+ <View style={[layout.flex.column, px[2], pt[2]]}>
59
+ <Text>Heads up!</Text>
60
+ <Text>This stream may contain:</Text>
61
+ </View>
62
+ <View
63
+ style={[
64
+ layout.flex.row,
65
+ { flexWrap: "wrap" },
66
+ gap.all[2],
67
+ px[2],
68
+ py[2],
69
+ ]}
70
+ >
71
+ {warnings.map((warning, index) => (
72
+ <View
73
+ key={index}
74
+ style={[
75
+ { backgroundColor: theme.colors.warning },
76
+ px[3],
77
+ py[1],
78
+ r.full,
79
+ ]}
80
+ >
81
+ <Text
82
+ size="sm"
83
+ weight="semibold"
84
+ style={{ color: theme.colors.warningForeground }}
85
+ >
86
+ {getWarningLabel(warning)}
87
+ </Text>
88
+ </View>
89
+ ))}
90
+ </View>
91
+ </ResponsiveDropdownMenuContent>
92
+ </DropdownMenu>
93
+ );
94
+ }
@@ -1,34 +1,50 @@
1
1
  import { forwardRef } from "react";
2
- import { StyleSheet, View } from "react-native";
2
+ import { View } from "react-native";
3
+ import { zero } from "../..";
3
4
  import { C2PA_WARNING_LABELS } from "../../lib/metadata-constants";
4
5
  import { useTheme } from "../../lib/theme/theme";
5
6
  import { Text } from "../ui/text";
6
7
 
8
+ const { layout, gap, bg, r, p, px, py, text: textStyles, borders } = zero;
9
+
7
10
  export interface ContentWarningsProps {
8
11
  warnings: string[];
9
12
  compact?: boolean;
10
13
  }
11
14
 
12
- export const ContentWarnings = forwardRef<any, ContentWarningsProps>(
13
- ({ warnings, compact = false }, ref) => {
15
+ export const ContentWarnings = forwardRef<View, ContentWarningsProps>(
16
+ ({ warnings, compact = false, ...rest }, ref) => {
14
17
  const { theme } = useTheme();
15
18
 
16
19
  if (!warnings || warnings.length === 0) {
17
20
  return null;
18
21
  }
19
22
 
20
- const styles = createStyles(theme, compact);
21
-
22
23
  const getWarningLabel = (warning: string): string => {
23
24
  return C2PA_WARNING_LABELS[warning] || warning;
24
25
  };
25
26
 
26
27
  if (compact) {
27
28
  return (
28
- <View ref={ref} style={styles.compactContainer}>
29
+ <View
30
+ ref={ref}
31
+ style={[layout.flex.row, layout.flex.wrap.wrap, gap.all[1]]}
32
+ {...rest}
33
+ >
29
34
  {warnings.map((warning, index) => (
30
- <View key={index} style={styles.compactWarning}>
31
- <Text style={styles.compactWarningText}>
35
+ <View
36
+ key={index}
37
+ style={[
38
+ { backgroundColor: theme.colors.warning },
39
+ r.full,
40
+ px[2],
41
+ { paddingVertical: 2 },
42
+ ]}
43
+ >
44
+ <Text
45
+ size="sm"
46
+ style={[{ color: theme.colors.warningForeground }]}
47
+ >
32
48
  {getWarningLabel(warning)}
33
49
  </Text>
34
50
  </View>
@@ -38,12 +54,29 @@ export const ContentWarnings = forwardRef<any, ContentWarningsProps>(
38
54
  }
39
55
 
40
56
  return (
41
- <View ref={ref} style={styles.container}>
42
- <Text style={styles.title}>Content Warnings</Text>
43
- <View style={styles.warningsContainer}>
57
+ <View ref={ref} style={[layout.flex.column, gap.all[2]]} {...rest}>
58
+ <Text
59
+ style={[{ fontSize: 14, fontWeight: "600" }, textStyles.gray[900]]}
60
+ >
61
+ Content Warnings
62
+ </Text>
63
+ <View style={[layout.flex.row, layout.flex.wrap.wrap, gap.all[2]]}>
44
64
  {warnings.map((warning, index) => (
45
- <View key={index} style={styles.warning}>
46
- <Text style={styles.warningText}>{getWarningLabel(warning)}</Text>
65
+ <View
66
+ key={index}
67
+ style={[
68
+ { backgroundColor: theme.colors.warning },
69
+ r.full,
70
+ px[3],
71
+ py[1],
72
+ ]}
73
+ >
74
+ <Text
75
+ size="sm"
76
+ style={[{ color: theme.colors.warningForeground }]}
77
+ >
78
+ {getWarningLabel(warning)}
79
+ </Text>
47
80
  </View>
48
81
  ))}
49
82
  </View>
@@ -53,48 +86,3 @@ export const ContentWarnings = forwardRef<any, ContentWarningsProps>(
53
86
  );
54
87
 
55
88
  ContentWarnings.displayName = "ContentWarnings";
56
-
57
- function createStyles(theme: any, compact: boolean) {
58
- return StyleSheet.create({
59
- container: {
60
- flexDirection: "column",
61
- gap: theme.spacing[2],
62
- },
63
- title: {
64
- fontSize: 14,
65
- fontWeight: "600",
66
- color: theme.colors.text,
67
- },
68
- warningsContainer: {
69
- flexDirection: "row",
70
- flexWrap: "wrap",
71
- gap: theme.spacing[2],
72
- },
73
- warning: {
74
- backgroundColor: theme.colors.warning,
75
- borderRadius: theme.borderRadius.md,
76
- padding: theme.spacing[2],
77
- },
78
- warningText: {
79
- color: theme.colors.warningForeground,
80
- fontSize: 12,
81
- fontWeight: "500",
82
- },
83
- compactContainer: {
84
- flexDirection: "row",
85
- flexWrap: "wrap",
86
- gap: theme.spacing[1],
87
- },
88
- compactWarning: {
89
- backgroundColor: theme.colors.warning,
90
- borderRadius: theme.borderRadius.full,
91
- paddingHorizontal: 10,
92
- paddingVertical: 4,
93
- },
94
- compactWarningText: {
95
- color: theme.colors.warningForeground,
96
- fontSize: 14,
97
- fontWeight: "600",
98
- },
99
- });
100
- }
@@ -3,8 +3,10 @@ export { ContentMetadataForm } from "./content-metadata-form";
3
3
 
4
4
  // Display components
5
5
  export { ContentRights } from "./content-rights";
6
+ export { ContentWarningBadge } from "./content-warning-badge";
6
7
  export { ContentWarnings } from "./content-warnings";
7
8
 
8
9
  export type { ContentRightsProps } from "./content-rights";
9
10
 
11
+ export type { ContentWarningBadgeProps } from "./content-warning-badge";
10
12
  export type { ContentWarningsProps } from "./content-warnings";
@@ -1,15 +1,21 @@
1
1
  import { useRootContext } from "@rn-primitives/dropdown-menu";
2
2
  import { Menu } from "lucide-react-native";
3
3
  import { Image, Linking, Platform, Pressable, View } from "react-native";
4
- import { useAvatars, useLivestreamInfo, zero } from "../../..";
4
+ import {
5
+ ContentRights,
6
+ ContentWarnings,
7
+ useAvatars,
8
+ useLivestreamInfo,
9
+ zero,
10
+ } from "../../..";
5
11
  import { colors } from "../../../lib/theme";
6
12
  import { useLivestreamStore } from "../../../livestream-store";
7
13
  import { PlayerProtocol, usePlayerStore } from "../../../player-store/";
8
14
  import { useGraphManager } from "../../../streamplace-store/graph";
15
+ import { gap, pt, px } from "../../../ui";
9
16
  import {
10
17
  DropdownMenu,
11
18
  DropdownMenuCheckboxItem,
12
- DropdownMenuContentWithoutPortal,
13
19
  DropdownMenuGroup,
14
20
  DropdownMenuInfo,
15
21
  DropdownMenuItem,
@@ -17,6 +23,9 @@ import {
17
23
  DropdownMenuRadioGroup,
18
24
  DropdownMenuRadioItem,
19
25
  DropdownMenuSeparator,
26
+ DropdownMenuSub,
27
+ DropdownMenuSubContent,
28
+ DropdownMenuSubTrigger,
20
29
  DropdownMenuTrigger,
21
30
  ResponsiveDropdownMenuContent,
22
31
  Text,
@@ -42,8 +51,14 @@ export function ContextMenu({
42
51
  const setReportSubject = usePlayerStore((x) => x.setReportSubject);
43
52
 
44
53
  const { profile } = useLivestreamInfo();
54
+
45
55
  const avatars = useAvatars(profile?.did ? [profile?.did] : []);
46
56
  const ls = useLivestreamStore((x) => x.livestream);
57
+ const segment = useLivestreamStore((x) => x.segment);
58
+
59
+ // Get content rights from the latest segment
60
+ const contentRights = segment?.contentRights;
61
+ const contentWarnings = segment?.contentWarnings?.warnings || [];
47
62
 
48
63
  let graphManager = useGraphManager(profile?.did);
49
64
 
@@ -58,10 +73,7 @@ export function ContextMenu({
58
73
  // dummy portal for mobile
59
74
  const Portal = isMobile ? View : DropdownMenuPortal;
60
75
 
61
- // render the responsive version on mobile as we can't fullscreen there
62
- const DropdownMenuContent = isMobile
63
- ? ResponsiveDropdownMenuContent
64
- : DropdownMenuContentWithoutPortal;
76
+ const DropdownMenuContent = ResponsiveDropdownMenuContent;
65
77
 
66
78
  return (
67
79
  <DropdownMenu>
@@ -160,28 +172,54 @@ export function ContextMenu({
160
172
  </DropdownMenuItem>
161
173
  </DropdownMenuGroup>
162
174
  )}
163
- <DropdownMenuGroup title="Resolution">
164
- <DropdownMenuRadioGroup value={quality} onValueChange={setQuality}>
165
- <DropdownMenuRadioItem value="source">
166
- <Text>Source (Original Quality)</Text>
167
- </DropdownMenuRadioItem>
168
- {qualities.map((r) => (
169
- <DropdownMenuRadioItem value={r.name}>
170
- <Text>{r.name}</Text>
171
- </DropdownMenuRadioItem>
172
- ))}
173
- </DropdownMenuRadioGroup>
175
+
176
+ <DropdownMenuGroup>
177
+ <DropdownMenuSub>
178
+ <DropdownMenuSubTrigger subMenuTitle="Quality">
179
+ <View
180
+ style={[
181
+ zero.flex.values[1],
182
+ zero.layout.flex.row,
183
+ zero.layout.flex.spaceBetween,
184
+ zero.pr[4],
185
+ ]}
186
+ >
187
+ <Text>Quality</Text>
188
+ <Text muted>
189
+ ({quality}, {lowLatency ? "low latency" : "regular latency"}
190
+ )
191
+ </Text>
192
+ </View>
193
+ </DropdownMenuSubTrigger>
194
+ <DropdownMenuSubContent>
195
+ <DropdownMenuGroup title="Resolution">
196
+ <DropdownMenuRadioGroup
197
+ value={quality}
198
+ onValueChange={setQuality}
199
+ >
200
+ <DropdownMenuRadioItem value="source">
201
+ <Text>Source (Original Quality)</Text>
202
+ </DropdownMenuRadioItem>
203
+ {qualities.map((r) => (
204
+ <DropdownMenuRadioItem key={r.name} value={r.name}>
205
+ <Text>{r.name}</Text>
206
+ </DropdownMenuRadioItem>
207
+ ))}
208
+ </DropdownMenuRadioGroup>
209
+ </DropdownMenuGroup>
210
+ <DropdownMenuGroup>
211
+ <DropdownMenuCheckboxItem
212
+ checked={lowLatency}
213
+ onCheckedChange={() => setLowLatency(!lowLatency)}
214
+ >
215
+ <Text>Low Latency</Text>
216
+ </DropdownMenuCheckboxItem>
217
+ </DropdownMenuGroup>
218
+ <DropdownMenuInfo description="Reduces the delay between video and chat for a more real-time experience." />
219
+ </DropdownMenuSubContent>
220
+ </DropdownMenuSub>
174
221
  </DropdownMenuGroup>
175
222
  <DropdownMenuGroup title="Advanced">
176
- <DropdownMenuCheckboxItem
177
- checked={lowLatency}
178
- onCheckedChange={() => setLowLatency(!lowLatency)}
179
- >
180
- <Text>Low Latency</Text>
181
- </DropdownMenuCheckboxItem>
182
- </DropdownMenuGroup>
183
- <DropdownMenuInfo description="Reduces the delay between video and chat for a more real-time experience." />
184
- <DropdownMenuGroup>
185
223
  <DropdownMenuCheckboxItem
186
224
  checked={debugInfo}
187
225
  onCheckedChange={() => setShowDebugInfo(!debugInfo)}
@@ -196,6 +234,23 @@ export function ContextMenu({
196
234
  setReportSubject={setReportSubject}
197
235
  />
198
236
  </DropdownMenuGroup>
237
+ <View style={[pt[3], px[2], gap.all[2]]}>
238
+ {contentWarnings && contentWarnings.length > 0 && (
239
+ <View style={[gap.all[1]]}>
240
+ <Text size="base" color="muted">
241
+ Stream may contain
242
+ </Text>
243
+ <ContentWarnings warnings={contentWarnings} compact={true} />
244
+ </View>
245
+ )}
246
+ {contentRights && Object.keys(contentRights).length > 0 && (
247
+ <ContentRights
248
+ contentRights={contentRights}
249
+ size="xs"
250
+ color="muted"
251
+ />
252
+ )}
253
+ </View>
199
254
  </DropdownMenuContent>
200
255
  </Portal>
201
256
  </DropdownMenu>
@@ -1,7 +1,7 @@
1
1
  import { Check } from "lucide-react-native";
2
2
  import { forwardRef } from "react";
3
3
  import { StyleSheet, TouchableOpacity, View } from "react-native";
4
- import { useTheme } from "../../lib/theme/theme";
4
+ import { Theme, useTheme } from "../../lib/theme/theme";
5
5
  import { Text } from "./text";
6
6
 
7
7
  export interface CheckboxProps {
@@ -60,9 +60,23 @@ export const Checkbox = forwardRef<any, CheckboxProps>(
60
60
  </View>
61
61
  {(label || description) && (
62
62
  <View style={styles.content}>
63
- {label && <Text style={styles.label}>{label}</Text>}
63
+ {label && (
64
+ <Text
65
+ size={size === "sm" ? "sm" : size === "lg" ? "lg" : "base"}
66
+ color={disabled ? "muted" : "default"}
67
+ leading="snug"
68
+ >
69
+ {label}
70
+ </Text>
71
+ )}
64
72
  {description && (
65
- <Text style={styles.description}>{description}</Text>
73
+ <Text
74
+ size={size === "sm" ? "xs" : size === "lg" ? "base" : "sm"}
75
+ color={disabled ? "muted" : "muted"}
76
+ style={{ marginTop: theme.spacing[1] }}
77
+ >
78
+ {description}
79
+ </Text>
66
80
  )}
67
81
  </View>
68
82
  )}
@@ -74,7 +88,7 @@ export const Checkbox = forwardRef<any, CheckboxProps>(
74
88
  Checkbox.displayName = "Checkbox";
75
89
 
76
90
  function createStyles(
77
- theme: any,
91
+ theme: Theme,
78
92
  size: string,
79
93
  disabled: boolean,
80
94
  checked: boolean,
@@ -90,13 +104,13 @@ function createStyles(
90
104
  checkboxSize: 20,
91
105
  borderRadius: 4,
92
106
  padding: theme.spacing[1],
93
- gap: theme.spacing[2],
107
+ gap: theme.spacing[1],
94
108
  },
95
109
  lg: {
96
110
  checkboxSize: 24,
97
111
  borderRadius: 6,
98
- padding: theme.spacing[2],
99
- gap: theme.spacing[3],
112
+ padding: theme.spacing[1],
113
+ gap: theme.spacing[2],
100
114
  },
101
115
  };
102
116
 
@@ -116,7 +130,7 @@ function createStyles(
116
130
  ? theme.colors.border
117
131
  : checked
118
132
  ? theme.colors.primary
119
- : theme.colors.border,
133
+ : theme.colors.textMuted,
120
134
  borderRadius: currentSize.borderRadius,
121
135
  backgroundColor: disabled
122
136
  ? theme.colors.muted
@@ -128,20 +142,8 @@ function createStyles(
128
142
  },
129
143
  content: {
130
144
  flex: 1,
131
- paddingTop: currentSize.padding * 0.5,
145
+ paddingTop: currentSize.padding * 0.25,
132
146
  paddingLeft: theme.spacing[2],
133
147
  },
134
- label: {
135
- fontSize: size === "sm" ? 14 : size === "lg" ? 18 : 16,
136
- fontWeight: "500",
137
- color: disabled ? theme.colors.textDisabled : theme.colors.text,
138
- lineHeight: size === "sm" ? 18 : size === "lg" ? 22 : 20,
139
- },
140
- description: {
141
- fontSize: size === "sm" ? 12 : size === "lg" ? 16 : 14,
142
- color: disabled ? theme.colors.textDisabled : theme.colors.textMuted,
143
- marginTop: theme.spacing[1],
144
- lineHeight: size === "sm" ? 16 : size === "lg" ? 20 : 18,
145
- },
146
148
  });
147
149
  }