@streamplace/components 0.7.34 → 0.7.35

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.34",
3
+ "version": "0.7.35",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -54,5 +54,5 @@
54
54
  "start": "tsc --watch --preserveWatchOutput",
55
55
  "prepare": "tsc"
56
56
  },
57
- "gitHead": "8d33e4f6129af71ccf3c5d959e347f78b6eb609a"
57
+ "gitHead": "eaefc85434e81e296c7248f6e6965a2f31d50ff8"
58
58
  }
@@ -1,5 +1,5 @@
1
- import { Car, Radio, Users } from "lucide-react-native";
2
- import { Text, View } from "react-native";
1
+ import { AlertCircle, Car, Radio, Users } from "lucide-react-native";
2
+ import { Pressable, Text, View } from "react-native";
3
3
  import * as zero from "../../ui";
4
4
 
5
5
  const { bg, r, borders, px, py, text, layout, gap } = zero;
@@ -103,6 +103,8 @@ interface HeaderProps {
103
103
  bitrate?: string;
104
104
  timeBetweenSegments?: number;
105
105
  connectionStatus?: "excellent" | "good" | "poor" | "offline";
106
+ problemsCount?: number;
107
+ onProblemsPress?: () => void;
106
108
  }
107
109
 
108
110
  export default function Header({
@@ -113,6 +115,8 @@ export default function Header({
113
115
  bitrate = "0 mbps",
114
116
  timeBetweenSegments = 0,
115
117
  connectionStatus = "offline",
118
+ problemsCount = 0,
119
+ onProblemsPress,
116
120
  }: HeaderProps) {
117
121
  const getConnectionQuality = (): "good" | "warning" | "error" => {
118
122
  if (timeBetweenSegments <= 1500) return "good";
@@ -139,7 +143,37 @@ export default function Header({
139
143
  <Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
140
144
  {streamTitle}
141
145
  </Text>
142
- <StatusIndicator status={connectionStatus} isLive={isLive} />
146
+ <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[3]]}>
147
+ <StatusIndicator status={connectionStatus} isLive={isLive} />
148
+ {problemsCount > 0 && (
149
+ <Pressable onPress={onProblemsPress}>
150
+ <View
151
+ style={[
152
+ layout.flex.row,
153
+ layout.flex.alignCenter,
154
+ gap.all[1],
155
+ px[2],
156
+ py[1],
157
+ r.md,
158
+ bg.orange[900],
159
+ borders.width.thin,
160
+ borders.color.orange[700],
161
+ { marginVertical: -8 },
162
+ ]}
163
+ >
164
+ <AlertCircle size={14} color="#fb923c" />
165
+ <Text
166
+ style={[
167
+ text.orange[400],
168
+ { fontSize: 11, fontWeight: "600" },
169
+ ]}
170
+ >
171
+ {problemsCount} {problemsCount === 1 ? "Issue" : "Issues"}
172
+ </Text>
173
+ </View>
174
+ </Pressable>
175
+ )}
176
+ </View>
143
177
  </View>
144
178
  </View>
145
179
 
@@ -2,4 +2,4 @@ export { default as ChatPanel } from "./chat-panel";
2
2
  export { default as Header } from "./header";
3
3
  export { default as InformationWidget } from "./information-widget";
4
4
  export { default as ModActions } from "./mod-actions";
5
- export { default as Problems } from "./problems";
5
+ export { default as Problems, ProblemsWrapperRef } from "./problems";
@@ -1,12 +1,32 @@
1
- import { ExternalLink } from "lucide-react-native";
2
- import { useState } from "react";
3
- import { Linking, Pressable, Text, View } from "react-native";
1
+ import {
2
+ CircleAlert,
3
+ CircleX,
4
+ ExternalLink,
5
+ Info,
6
+ Sparkle,
7
+ } from "lucide-react-native";
8
+ import { forwardRef, useImperativeHandle, useState } from "react";
9
+ import { Linking, Pressable, View } from "react-native";
4
10
  import { useLivestreamStore } from "../../livestream-store";
5
11
  import { LivestreamProblem } from "../../livestream-store/livestream-state";
6
12
  import * as zero from "../../ui";
13
+ import { Button, Text } from "../ui";
7
14
 
8
15
  const { bg, r, borders, p, text, layout, gap } = zero;
9
16
 
17
+ const getIcon = (severity: string) => {
18
+ switch (severity) {
19
+ case "error":
20
+ return <CircleX size={24} color="white" />;
21
+ case "warning":
22
+ return <CircleAlert size={24} color="white" />;
23
+ case "info":
24
+ return <Info size={24} color="white" />;
25
+ default:
26
+ return <Sparkle size={24} color="white" />;
27
+ }
28
+ };
29
+
10
30
  const Problems = ({
11
31
  probs,
12
32
  onIgnore,
@@ -15,9 +35,9 @@ const Problems = ({
15
35
  onIgnore: () => void;
16
36
  }) => {
17
37
  return (
18
- <View style={[gap.all[3]]}>
19
- <View>
20
- <Text style={[text.white, { fontSize: 24, fontWeight: "bold" }]}>
38
+ <View style={[gap.all[4]]}>
39
+ <View style={[gap.all[2]]}>
40
+ <Text size="2xl" style={[text.white, { fontWeight: "600" }]}>
21
41
  Optimize Your Stream
22
42
  </Text>
23
43
  <Text style={[text.gray[300]]}>
@@ -34,26 +54,22 @@ const Problems = ({
34
54
  { gap: 8, alignItems: "flex-start" },
35
55
  ]}
36
56
  >
37
- <Text
57
+ <View
38
58
  style={[
39
- r.sm,
40
- p[2],
59
+ zero.r.full,
60
+ zero.p[1],
41
61
  {
42
- width: 82,
43
- textAlign: "center",
44
62
  backgroundColor:
45
63
  p.severity === "error"
46
64
  ? "#7f1d1d"
47
65
  : p.severity === "warning"
48
66
  ? "#7c2d12"
49
67
  : "#1e3a8a",
50
- color: "white",
51
- fontSize: 12,
52
68
  },
53
69
  ]}
54
70
  >
55
- {p.severity}
56
- </Text>
71
+ {getIcon(p.severity)}
72
+ </View>
57
73
  <View style={[{ flex: 1 }, gap.all[1]]}>
58
74
  <Text style={[text.white, { fontWeight: "600" }]}>{p.code}</Text>
59
75
  <Text style={[text.gray[400], { fontSize: 14 }]}>
@@ -79,39 +95,34 @@ const Problems = ({
79
95
  </View>
80
96
  </View>
81
97
  ))}
82
-
83
- <Pressable
84
- onPress={onIgnore}
85
- style={[
86
- bg.blue[600],
87
- r.md,
88
- p[3],
89
- layout.flex.center,
90
- { marginTop: 16 },
91
- ]}
92
- >
93
- <Text style={[text.white, { fontWeight: "600" }]}>Ignore</Text>
94
- </Pressable>
98
+ <View style={[layout.flex.row, layout.flex.justify.end]}>
99
+ <Button onPress={onIgnore} variant="secondary">
100
+ <Text style={[text.white, { fontWeight: "600" }]}>Acknowledge</Text>
101
+ </Button>
102
+ </View>
95
103
  </View>
96
104
  );
97
105
  };
98
106
 
99
- export const ProblemsWrapper = ({
100
- children,
101
- }: {
102
- children: React.ReactElement;
103
- }) => {
107
+ export interface ProblemsWrapperRef {
108
+ setDismiss: (value: boolean) => void;
109
+ }
110
+
111
+ export const ProblemsWrapper = forwardRef<
112
+ ProblemsWrapperRef,
113
+ {
114
+ children: React.ReactElement;
115
+ }
116
+ >(({ children }, ref) => {
104
117
  const problems = useLivestreamStore((x) => x.problems);
105
118
  const [dismiss, setDismiss] = useState(false);
106
119
 
120
+ useImperativeHandle(ref, () => ({
121
+ setDismiss,
122
+ }));
123
+
107
124
  return (
108
- <View
109
- style={[
110
- { position: "relative", flex: 1 },
111
- layout.flex.center,
112
- { flexBasis: 0 },
113
- ]}
114
- >
125
+ <>
115
126
  {children}
116
127
  {problems.length > 0 && !dismiss && (
117
128
  <View
@@ -127,16 +138,16 @@ export const ProblemsWrapper = ({
127
138
  },
128
139
  layout.flex.center,
129
140
  { justifyContent: "flex-start" },
130
- p[8],
141
+ p[12],
131
142
  ]}
132
143
  >
133
144
  <View
134
145
  style={[
135
- bg.gray[900],
136
- borders.color.gray[700],
146
+ bg.neutral[900],
147
+ borders.color.neutral[700],
137
148
  borders.width.thin,
138
149
  r.lg,
139
- p[4],
150
+ p[8],
140
151
  { maxWidth: 700, width: "100%" },
141
152
  ]}
142
153
  >
@@ -144,8 +155,8 @@ export const ProblemsWrapper = ({
144
155
  </View>
145
156
  </View>
146
157
  )}
147
- </View>
158
+ </>
148
159
  );
149
- };
160
+ });
150
161
 
151
162
  export default Problems;
@@ -1,9 +1,11 @@
1
1
  import { useRootContext } from "@rn-primitives/dropdown-menu";
2
2
  import { Menu } from "lucide-react-native";
3
- import { Platform, View } from "react-native";
3
+ import { Image, Linking, Platform, Pressable, View } from "react-native";
4
+ import { useAvatars, useLivestreamInfo, zero } from "../../..";
4
5
  import { colors } from "../../../lib/theme";
5
6
  import { useLivestreamStore } from "../../../livestream-store";
6
7
  import { PlayerProtocol, usePlayerStore } from "../../../player-store/";
8
+ import { useGraphManager } from "../../../streamplace-store/graph";
7
9
  import {
8
10
  DropdownMenu,
9
11
  DropdownMenuCheckboxItem,
@@ -14,6 +16,7 @@ import {
14
16
  DropdownMenuPortal,
15
17
  DropdownMenuRadioGroup,
16
18
  DropdownMenuRadioItem,
19
+ DropdownMenuSeparator,
17
20
  DropdownMenuTrigger,
18
21
  ResponsiveDropdownMenuContent,
19
22
  Text,
@@ -38,6 +41,12 @@ export function ContextMenu({
38
41
  const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
39
42
  const setReportSubject = usePlayerStore((x) => x.setReportSubject);
40
43
 
44
+ const { profile } = useLivestreamInfo();
45
+ const avatars = useAvatars(profile?.did ? [profile?.did] : []);
46
+ const ls = useLivestreamStore((x) => x.livestream);
47
+
48
+ let graphManager = useGraphManager(profile?.did);
49
+
41
50
  const lowLatency = protocol === "webrtc";
42
51
  const setLowLatency = (value: boolean) => {
43
52
  setProtocol(value ? PlayerProtocol.WEBRTC : PlayerProtocol.HLS);
@@ -61,6 +70,96 @@ export function ContextMenu({
61
70
  </DropdownMenuTrigger>
62
71
  <Portal container={dropdownPortalContainer}>
63
72
  <DropdownMenuContent side="top" align="end">
73
+ {Platform.OS !== "web" && (
74
+ <DropdownMenuGroup title="Streamer">
75
+ <View
76
+ style={[
77
+ zero.layout.flex.row,
78
+ zero.layout.flex.center,
79
+ zero.gap.all[3],
80
+ { flex: 1, minWidth: 0 },
81
+ ]}
82
+ >
83
+ {profile?.did && avatars[profile?.did]?.avatar && (
84
+ <Image
85
+ key="avatar"
86
+ source={{
87
+ uri: avatars[profile?.did]?.avatar,
88
+ }}
89
+ style={{ width: 42, height: 42, borderRadius: 999 }}
90
+ resizeMode="cover"
91
+ />
92
+ )}
93
+ <View style={{ flex: 1, minWidth: 0 }}>
94
+ <View
95
+ style={[
96
+ zero.layout.flex.row,
97
+ zero.layout.flex.alignCenter,
98
+ zero.gap.all[2],
99
+ ]}
100
+ >
101
+ <Pressable
102
+ onPress={() => {
103
+ if (profile?.handle) {
104
+ const url = `https://bsky.app/profile/${profile.handle}`;
105
+ Linking.openURL(url);
106
+ }
107
+ }}
108
+ >
109
+ <Text>@{profile?.handle || "user"}</Text>
110
+ </Pressable>
111
+ {/*{did && profile && (
112
+ <FollowButton streamerDID={profile?.did} currentUserDID={did} />
113
+ )}*/}
114
+ </View>
115
+ <Text
116
+ color="muted"
117
+ size="sm"
118
+ numberOfLines={2}
119
+ ellipsizeMode="tail"
120
+ >
121
+ {ls?.record.title || "Stream Title"}
122
+ </Text>
123
+ </View>
124
+ </View>
125
+ <DropdownMenuSeparator />
126
+ <DropdownMenuItem
127
+ disabled={graphManager.isLoading || !profile?.did}
128
+ onPress={async () => {
129
+ try {
130
+ if (graphManager.isFollowing) {
131
+ await graphManager.unfollow();
132
+ } else {
133
+ await graphManager.follow();
134
+ }
135
+ } catch (err) {
136
+ console.error("Follow/unfollow error:", err);
137
+ }
138
+ }}
139
+ >
140
+ <Text
141
+ color={graphManager.isFollowing ? "destructive" : "default"}
142
+ >
143
+ {graphManager.isLoading
144
+ ? "Loading..."
145
+ : graphManager.isFollowing
146
+ ? "Unfollow"
147
+ : "Follow"}
148
+ </Text>
149
+ </DropdownMenuItem>
150
+ <DropdownMenuSeparator />
151
+ <DropdownMenuItem
152
+ onPress={() => {
153
+ if (profile?.handle) {
154
+ const url = `https://bsky.app/profile/${profile.handle}`;
155
+ Linking.openURL(url);
156
+ }
157
+ }}
158
+ >
159
+ <Text>View Profile on Bluesky</Text>
160
+ </DropdownMenuItem>
161
+ </DropdownMenuGroup>
162
+ )}
64
163
  <DropdownMenuGroup title="Resolution">
65
164
  <DropdownMenuRadioGroup value={quality} onValueChange={setQuality}>
66
165
  <DropdownMenuRadioItem value="source">
@@ -109,7 +109,7 @@ export const Button = forwardRef<any, ButtonProps>(
109
109
  { borderRadius: zero.borderRadius.md },
110
110
  ],
111
111
  inner: { gap: 4 },
112
- text: zt.text.sm,
112
+ text: zero.typography.universal.sm,
113
113
  };
114
114
  case "lg":
115
115
  return {
@@ -118,8 +118,8 @@ export const Button = forwardRef<any, ButtonProps>(
118
118
  zero.py[3],
119
119
  { borderRadius: zero.borderRadius.md },
120
120
  ],
121
- inner: { gap: 8 },
122
- text: zt.text.lg,
121
+ inner: { gap: 12 },
122
+ text: zero.typography.universal.lg,
123
123
  };
124
124
  case "xl":
125
125
  return {
@@ -129,17 +129,17 @@ export const Button = forwardRef<any, ButtonProps>(
129
129
  { borderRadius: zero.borderRadius.lg },
130
130
  ],
131
131
  inner: { gap: 12 },
132
- text: zt.text.xl,
132
+ text: zero.typography.universal.xl,
133
133
  };
134
134
  case "pill":
135
135
  return {
136
136
  button: [
137
- zero.px[4],
138
- zero.py[2],
137
+ zero.px[2],
138
+ zero.py[1],
139
139
  { borderRadius: zero.borderRadius.full },
140
140
  ],
141
141
  inner: { gap: 4 },
142
- text: zt.text.sm,
142
+ text: zero.typography.universal.xs,
143
143
  };
144
144
  case "md":
145
145
  default:
@@ -150,7 +150,7 @@ export const Button = forwardRef<any, ButtonProps>(
150
150
  { borderRadius: zero.borderRadius.md },
151
151
  ],
152
152
  inner: { gap: 6 },
153
- text: zt.text.md,
153
+ text: zero.typography.universal.sm,
154
154
  };
155
155
  }
156
156
  }, [size, zt]);
@@ -211,15 +211,12 @@ export const Button = forwardRef<any, ButtonProps>(
211
211
  <ActivityIndicator size={spinnerSize} color={spinnerColor} />
212
212
  </ButtonPrimitive.Icon>
213
213
  ) : leftIcon ? (
214
- <ButtonPrimitive.Icon
215
- position="left"
216
- style={{ width: iconSize, height: iconSize }}
217
- >
214
+ <ButtonPrimitive.Icon position="left">
218
215
  {leftIcon}
219
216
  </ButtonPrimitive.Icon>
220
217
  ) : null}
221
218
 
222
- <TextPrimitive.Root style={[textStyle, sizeStyles.text]}>
219
+ <TextPrimitive.Root style={[textStyle as any, sizeStyles.text]}>
223
220
  {loading && loadingText ? loadingText : children}
224
221
  </TextPrimitive.Root>
225
222
 
@@ -13,7 +13,6 @@ import {
13
13
  Platform,
14
14
  Pressable,
15
15
  StyleSheet,
16
- Text,
17
16
  useWindowDimensions,
18
17
  View,
19
18
  } from "react-native";
@@ -39,6 +38,7 @@ import {
39
38
  objectFromObjects,
40
39
  TextContext as TextClassContext,
41
40
  } from "./primitives/text";
41
+ import { Text } from "./text";
42
42
 
43
43
  export const DropdownMenu = DropdownMenuPrimitive.Root;
44
44
  export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
@@ -87,7 +87,7 @@ export const DropdownMenuBottomSheet = forwardRef<
87
87
  zt.bg.mutedForeground,
88
88
  ]}
89
89
  >
90
- <BottomSheetView style={[px[2]]}>
90
+ <BottomSheetView style={[px[4]]}>
91
91
  {typeof children === "function"
92
92
  ? children({ pressed: true })
93
93
  : children}
@@ -285,9 +285,15 @@ export const DropdownMenuItem = forwardRef<
285
285
  pr[2],
286
286
  ]}
287
287
  >
288
- {typeof children === "function"
289
- ? children({ pressed: true })
290
- : children}
288
+ {typeof children === "function" ? (
289
+ children({ pressed: true })
290
+ ) : typeof children === "string" ? (
291
+ <Text style={[inset && gap[2], disabled && { opacity: 0.5 }]}>
292
+ {children}
293
+ </Text>
294
+ ) : (
295
+ children
296
+ )}
291
297
  </View>
292
298
  </TextClassContext.Provider>
293
299
  </Pressable>
@@ -384,13 +390,15 @@ export const DropdownMenuLabel = forwardRef<
384
390
  return (
385
391
  <Text
386
392
  ref={ref}
387
- style={[
388
- px[2],
389
- py[2],
390
- { color: theme.colors.textMuted },
391
- a.fontSize.base,
392
- inset && gap[2],
393
- ]}
393
+ style={
394
+ [
395
+ px[2],
396
+ py[2],
397
+ { color: theme.colors.textMuted },
398
+ a.fontSize.base,
399
+ (inset && gap[2]) as any,
400
+ ] as any
401
+ }
394
402
  {...props}
395
403
  />
396
404
  );
@@ -404,7 +412,13 @@ export const DropdownMenuSeparator = forwardRef<
404
412
  return (
405
413
  <View
406
414
  ref={ref}
407
- style={[{ height: 0.5 }, { backgroundColor: theme.colors.border }]}
415
+ style={[
416
+ {
417
+ borderBottomWidth: 1,
418
+ borderBottomColor: theme.colors.border,
419
+ marginVertical: -0.5,
420
+ },
421
+ ]}
408
422
  {...props}
409
423
  />
410
424
  );
@@ -48,3 +48,17 @@ export function createThemedIcon(
48
48
  );
49
49
  };
50
50
  }
51
+
52
+ // usage of createThemedIcon
53
+ export function Icon({
54
+ icon,
55
+ variant = "default",
56
+ size = "md",
57
+ color,
58
+ ...restProps
59
+ }: { icon: React.ComponentType<LucideProps> } & IconProps) {
60
+ const ThemedIcon = createThemedIcon(icon);
61
+ return (
62
+ <ThemedIcon variant={variant} size={size} color={color} {...restProps} />
63
+ );
64
+ }
@@ -244,7 +244,6 @@ const primitiveStyles = StyleSheet.create({
244
244
  flexDirection: "row",
245
245
  alignItems: "center",
246
246
  justifyContent: "center",
247
- minHeight: 44, // iOS minimum touch target
248
247
  },
249
248
  disabled: {
250
249
  opacity: 0.5,
@@ -264,12 +263,6 @@ const primitiveStyles = StyleSheet.create({
264
263
  alignItems: "center",
265
264
  justifyContent: "center",
266
265
  },
267
- iconLeft: {
268
- marginRight: 8,
269
- },
270
- iconRight: {
271
- marginLeft: 8,
272
- },
273
266
  iconDisabled: {
274
267
  opacity: 0.5,
275
268
  },
@@ -590,52 +590,61 @@ export const typography = {
590
590
  },
591
591
 
592
592
  // Universal typography scale
593
+ // Atkinson's center is weird so the marginBottom is there to correct it?
593
594
  universal: {
594
595
  xs: {
595
596
  fontSize: 12,
596
597
  lineHeight: 16,
598
+ marginBottom: -0.7,
597
599
  fontWeight: "400" as const,
598
600
  fontFamily: "AtkinsonHyperlegibleNext-Regular",
599
601
  },
600
602
  sm: {
601
603
  fontSize: 14,
602
604
  lineHeight: 20,
605
+ marginBottom: -1,
603
606
  fontWeight: "400" as const,
604
607
  fontFamily: "AtkinsonHyperlegibleNext-Regular",
605
608
  },
606
609
  base: {
607
610
  fontSize: 16,
608
611
  lineHeight: 24,
612
+ marginBottom: -1.2,
609
613
  fontWeight: "400" as const,
610
614
  fontFamily: "AtkinsonHyperlegibleNext-Regular",
611
615
  },
612
616
  lg: {
613
617
  fontSize: 18,
614
618
  lineHeight: 28,
619
+ marginBottom: -1.5,
615
620
  fontWeight: "400" as const,
616
621
  fontFamily: "AtkinsonHyperlegibleNext-Regular",
617
622
  },
618
623
  xl: {
619
624
  fontSize: 20,
620
625
  lineHeight: 28,
626
+ marginBottom: -1.75,
621
627
  fontWeight: "500" as const,
622
628
  fontFamily: "AtkinsonHyperlegibleNext-Medium",
623
629
  },
624
630
  "2xl": {
625
631
  fontSize: 24,
626
632
  lineHeight: 32,
633
+ marginBottom: -2,
627
634
  fontWeight: "600" as const,
628
635
  fontFamily: "AtkinsonHyperlegibleNext-SemiBold",
629
636
  },
630
637
  "3xl": {
631
638
  fontSize: 30,
632
639
  lineHeight: 36,
640
+ marginBottom: -2.5,
633
641
  fontWeight: "700" as const,
634
642
  fontFamily: "AtkinsonHyperlegibleNext-Bold",
635
643
  },
636
644
  "4xl": {
637
645
  fontSize: 36,
638
646
  lineHeight: 40,
647
+ marginBottom: -3,
639
648
  fontWeight: "700" as const,
640
649
  fontFamily: "AtkinsonHyperlegibleNext-ExtraBold",
641
650
  },