@streamplace/components 0.7.2 → 0.7.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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./countdown"), exports);
5
5
  tslib_1.__exportStar(require("./input"), exports);
6
+ tslib_1.__exportStar(require("./loading"), exports);
6
7
  tslib_1.__exportStar(require("./metrics"), exports);
7
8
  tslib_1.__exportStar(require("./streamer-context-menu"), exports);
8
9
  tslib_1.__exportStar(require("./viewer-context-menu"), exports);
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoadingOverlay = LoadingOverlay;
4
+ const tslib_1 = require("tslib");
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_native_reanimated_1 = tslib_1.__importStar(require("react-native-reanimated"));
8
+ const atoms_1 = require("../../../lib/theme/atoms");
9
+ const defaultMessages = [
10
+ "Creating your stream",
11
+ "Uploading thumbnails",
12
+ "Getting things ready",
13
+ "Doing some magic",
14
+ "Preparing something special",
15
+ "Reticulating splines",
16
+ "Making it nice",
17
+ "Flipping some switches",
18
+ "Adding good vibes",
19
+ "Almost there",
20
+ "Summoning your Persona",
21
+ "Awakening our true selves",
22
+ "Fusion in progress",
23
+ "Equipping the right materia",
24
+ ];
25
+ function LoadingOverlay({ visible, width, height, subtitle, messages = defaultMessages, interval = 3000, }) {
26
+ const [currentIndex, setCurrentIndex] = (0, react_1.useState)(0);
27
+ const [shouldRender, setShouldRender] = (0, react_1.useState)(visible);
28
+ // Animation values
29
+ const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
30
+ const opacity = (0, react_native_reanimated_1.useSharedValue)(1);
31
+ const wholeOpacity = (0, react_native_reanimated_1.useSharedValue)(0);
32
+ // Handle fade-in and fade-out animations
33
+ (0, react_1.useEffect)(() => {
34
+ if (visible) {
35
+ setShouldRender(true); // Ensure the component is mounted
36
+ wholeOpacity.value = (0, react_native_reanimated_1.withTiming)(1, { duration: 500 }); // Fade in
37
+ }
38
+ else {
39
+ wholeOpacity.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 500 }, () => {
40
+ // Unmount after fade-out
41
+ (0, react_native_reanimated_1.runOnJS)(setShouldRender)(false);
42
+ });
43
+ }
44
+ }, [visible]);
45
+ // Cycle messages on a timer
46
+ (0, react_1.useEffect)(() => {
47
+ if (!visible) {
48
+ setCurrentIndex(0);
49
+ return;
50
+ }
51
+ const timeout = setTimeout(() => {
52
+ setCurrentIndex((prev) => (prev + 1) % messages.length);
53
+ }, interval);
54
+ return () => clearTimeout(timeout);
55
+ }, [visible, currentIndex, interval, messages.length]);
56
+ // Trigger animation on each message change
57
+ (0, react_1.useEffect)(() => {
58
+ if (!visible)
59
+ return;
60
+ const fadeDuration = Math.min(interval / 2, 250); // Simplified fade duration
61
+ // Reset animation values
62
+ translateY.value = 20;
63
+ opacity.value = 0;
64
+ // Sequential fade-in and fade-out
65
+ translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: fadeDuration });
66
+ opacity.value = (0, react_native_reanimated_1.withTiming)(1, { duration: fadeDuration }, () => {
67
+ // add a delay for interval - (fadeDuration*2)
68
+ translateY.value = (0, react_native_reanimated_1.withDelay)(interval - fadeDuration * 2, (0, react_native_reanimated_1.withTiming)(-10, { duration: fadeDuration }));
69
+ opacity.value = (0, react_native_reanimated_1.withDelay)(interval - fadeDuration * 2, (0, react_native_reanimated_1.withTiming)(0, { duration: fadeDuration }));
70
+ });
71
+ }, [currentIndex, visible]);
72
+ const animatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
73
+ return {
74
+ transform: [{ translateY: translateY.value }],
75
+ opacity: opacity.value,
76
+ };
77
+ });
78
+ const wholeAnimatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => ({
79
+ opacity: wholeOpacity.value,
80
+ }));
81
+ if (!shouldRender)
82
+ return null;
83
+ return ((0, jsx_runtime_1.jsxs)(react_native_reanimated_1.default.View, { style: [
84
+ {
85
+ position: "absolute",
86
+ top: 0,
87
+ left: 0,
88
+ width,
89
+ height,
90
+ backgroundColor: "rgba(0,0,0,0.7)",
91
+ alignItems: "center",
92
+ justifyContent: "center",
93
+ zIndex: 1000,
94
+ },
95
+ wholeAnimatedStyle,
96
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.Text, { style: [
97
+ {
98
+ color: "white",
99
+ fontSize: 24,
100
+ fontWeight: "bold",
101
+ },
102
+ animatedStyle,
103
+ ], children: messages[currentIndex] }), (0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.Text, { style: [atoms_1.pt[5], { color: "#a0a0a0" }], children: subtitle })] }));
104
+ }
@@ -97,16 +97,25 @@ function Resizable({ startingPercentage, isPlayerRatioGreater, style = {}, child
97
97
  minWidth: "100%",
98
98
  },
99
99
  style,
100
- ], children: [(0, jsx_runtime_1.jsx)(view_1.View, { style: [atoms_1.layout.flex.row, atoms_1.layout.flex.justifyCenter], children: (0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.GestureDetector, { gesture: panGesture, children: (0, jsx_runtime_1.jsx)(view_1.View, { hitSlop: { top: 20, bottom: 20, left: 20, right: 20 }, style: [
101
- atoms_1.w[32],
102
- {
103
- height: 6,
104
- transform: [{ translateY: -10 }],
105
- backgroundColor: "#eeeeee66",
106
- alignItems: "center",
107
- justifyContent: "center",
108
- borderRadius: 999,
109
- },
110
- ] }) }) }), children] })] }));
100
+ ], children: [(0, jsx_runtime_1.jsx)(view_1.View, { style: [atoms_1.layout.flex.row, atoms_1.layout.flex.justifyCenter, atoms_1.h[2]], children: (0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.GestureDetector, { gesture: panGesture, children: (0, jsx_runtime_1.jsx)(view_1.View
101
+ // Make the touch area much larger, but keep the visible handle small
102
+ , {
103
+ // Make the touch area much larger, but keep the visible handle small
104
+ style: {
105
+ height: 30, // Large touch area
106
+ width: 120, // Wide enough for thumbs
107
+ alignItems: "center",
108
+ justifyContent: "center",
109
+ //backgroundColor: "rgba(0,255,255,0.1)",
110
+ transform: [{ translateY: -30 }],
111
+ }, children: (0, jsx_runtime_1.jsx)(view_1.View, { style: [
112
+ atoms_1.w[32],
113
+ {
114
+ height: 6,
115
+ backgroundColor: "#eeeeee66",
116
+ borderRadius: 999,
117
+ transform: [{ translateY: 5 }],
118
+ },
119
+ ] }) }) }) }), children] })] }));
111
120
  }
112
121
  Resizable.displayName = "ResizableChatSheet";
@@ -47,6 +47,9 @@ const makePlayerStore = (id) => {
47
47
  setVideoRef: (videoRef) => set(() => ({ videoRef })),
48
48
  pipMode: false,
49
49
  setPipMode: (pipMode) => set(() => ({ pipMode })),
50
+ // Picture-in-Picture action function (set by player component)
51
+ pipAction: undefined,
52
+ setPipAction: (action) => set(() => ({ pipAction: action })),
50
53
  // Player element width/height setters for global sync
51
54
  playerWidth: undefined,
52
55
  setPlayerWidth: (playerWidth) => set(() => ({ playerWidth })),
@@ -5,29 +5,48 @@ exports.useUpdateStreamRecord = useUpdateStreamRecord;
5
5
  const api_1 = require("@atproto/api");
6
6
  const streamplace_store_1 = require("./streamplace-store");
7
7
  const xrpc_1 = require("./xrpc");
8
- const uploadThumbnail = async (pdsAgent, customThumbnail) => {
9
- if (customThumbnail) {
10
- let tries = 0;
11
- try {
12
- let thumbnail = await pdsAgent.uploadBlob(customThumbnail);
13
- while (thumbnail.data.blob.size === 0 &&
14
- customThumbnail.size !== 0 &&
15
- tries < 3) {
16
- console.warn("Reuploading blob as blob sizes don't match! Blob size recieved is", thumbnail.data.blob.size, "and sent blob size is", customThumbnail.size);
17
- thumbnail = await pdsAgent.uploadBlob(customThumbnail);
18
- }
19
- if (tries === 3) {
20
- throw new Error("Could not successfully upload blob (tried thrice)");
8
+ const react_1 = require("react");
9
+ const useUploadThumbnail = () => {
10
+ const abortRef = (0, react_1.useRef)(null);
11
+ (0, react_1.useEffect)(() => {
12
+ return () => {
13
+ // On unmount, abort any ongoing upload
14
+ abortRef.current?.abort();
15
+ };
16
+ }, []);
17
+ const uploadThumbnail = async (pdsAgent, customThumbnail) => {
18
+ if (!customThumbnail)
19
+ return undefined;
20
+ abortRef.current = new AbortController();
21
+ const { signal } = abortRef.current;
22
+ const maxTries = 3;
23
+ let lastError = null;
24
+ for (let tries = 0; tries < maxTries; tries++) {
25
+ try {
26
+ const thumbnail = await pdsAgent.uploadBlob(customThumbnail, {
27
+ signal,
28
+ });
29
+ if (thumbnail.success &&
30
+ thumbnail.data.blob.size === customThumbnail.size) {
31
+ console.log("Successfully uploaded thumbnail");
32
+ return thumbnail.data.blob;
33
+ }
34
+ else {
35
+ console.warn(`Blob size mismatch (attempt ${tries + 1}): received ${thumbnail.data.blob.size}, expected ${customThumbnail.size}`);
36
+ }
21
37
  }
22
- if (thumbnail.success) {
23
- console.log("Successfully uploaded thumbnail");
24
- return thumbnail.data.blob;
38
+ catch (e) {
39
+ if (signal.aborted) {
40
+ console.warn("Upload aborted");
41
+ return undefined;
42
+ }
43
+ lastError = e;
44
+ console.warn(`Error uploading thumbnail (attempt ${tries + 1}): ${e}`);
25
45
  }
26
46
  }
27
- catch (e) {
28
- throw new Error("Error uploading thumbnail: " + e);
29
- }
30
- }
47
+ throw new Error(`Could not successfully upload blob after ${maxTries} attempts. Last error: ${lastError}`);
48
+ };
49
+ return uploadThumbnail;
31
50
  };
32
51
  async function createNewPost(agent, record) {
33
52
  try {
@@ -39,7 +58,7 @@ async function createNewPost(agent, record) {
39
58
  throw error;
40
59
  }
41
60
  }
42
- function buildGoLivePost(text, url, profile, params, thumbnail) {
61
+ async function buildGoLivePost(text, url, profile, params, thumbnail, agent) {
43
62
  const now = new Date();
44
63
  const linkUrl = `${url.protocol}//${url.host}/${profile.handle}?${params.toString()}`;
45
64
  const prefix = `🔴 LIVE `;
@@ -47,7 +66,7 @@ function buildGoLivePost(text, url, profile, params, thumbnail) {
47
66
  const suffix = ` ${text}`;
48
67
  const content = prefix + textUrl + suffix;
49
68
  const rt = new api_1.RichText({ text: content });
50
- rt.detectFacetsWithoutResolution();
69
+ await rt.detectFacets(agent);
51
70
  const record = {
52
71
  $type: "app.bsky.feed.post",
53
72
  text: content,
@@ -72,6 +91,7 @@ function buildGoLivePost(text, url, profile, params, thumbnail) {
72
91
  function useCreateStreamRecord() {
73
92
  let agent = (0, xrpc_1.usePDSAgent)();
74
93
  let url = (0, streamplace_store_1.useUrl)();
94
+ const uploadThumbnail = useUploadThumbnail();
75
95
  return async (title, customThumbnail, submitPost = true) => {
76
96
  if (!agent) {
77
97
  throw new Error("No PDS agent found");
@@ -130,7 +150,7 @@ function useCreateStreamRecord() {
130
150
  did: did,
131
151
  time: new Date().toISOString(),
132
152
  });
133
- let post = buildGoLivePost(title, u, profile.data, params, thumbnail);
153
+ let post = await buildGoLivePost(title, u, profile.data, params, thumbnail, agent);
134
154
  newPost = await createNewPost(agent, post);
135
155
  if (!newPost.uri || !newPost.cid) {
136
156
  throw new Error("Cannot read properties of undefined (reading 'uri' or 'cid')");
@@ -154,6 +174,7 @@ function useCreateStreamRecord() {
154
174
  function useUpdateStreamRecord() {
155
175
  let agent = (0, xrpc_1.usePDSAgent)();
156
176
  let url = (0, streamplace_store_1.useUrl)();
177
+ const uploadThumbnail = useUploadThumbnail();
157
178
  return async (title, livestream, customThumbnail) => {
158
179
  if (!agent) {
159
180
  throw new Error("No PDS agent found");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -50,5 +50,5 @@
50
50
  "peerDependencies": {
51
51
  "react": "*"
52
52
  },
53
- "gitHead": "b07a68160ae6da34cf47aa397b4ef452248d79e5"
53
+ "gitHead": "70367834f75a94f074bd81299c2e72c36b0dbbf0"
54
54
  }
@@ -1,5 +1,6 @@
1
1
  export * from "./countdown";
2
2
  export * from "./input";
3
+ export * from "./loading";
3
4
  export * from "./metrics";
4
5
  export * from "./streamer-context-menu";
5
6
  export * from "./viewer-context-menu";
@@ -0,0 +1,154 @@
1
+ import { useEffect, useState } from "react";
2
+ import Animated, {
3
+ runOnJS,
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withDelay,
7
+ withTiming,
8
+ } from "react-native-reanimated";
9
+ import { pt } from "../../../lib/theme/atoms";
10
+
11
+ type LoadingOverlayProps = {
12
+ visible: boolean;
13
+ width: number;
14
+ height: number;
15
+ subtitle?: string;
16
+ messages?: string[];
17
+ interval?: number; // in milliseconds
18
+ };
19
+
20
+ const defaultMessages = [
21
+ "Creating your stream",
22
+ "Uploading thumbnails",
23
+ "Getting things ready",
24
+ "Doing some magic",
25
+ "Preparing something special",
26
+ "Reticulating splines",
27
+ "Making it nice",
28
+ "Flipping some switches",
29
+ "Adding good vibes",
30
+ "Almost there",
31
+ "Summoning your Persona",
32
+ "Awakening our true selves",
33
+ "Fusion in progress",
34
+ "Equipping the right materia",
35
+ ];
36
+
37
+ export function LoadingOverlay({
38
+ visible,
39
+ width,
40
+ height,
41
+ subtitle,
42
+ messages = defaultMessages,
43
+ interval = 3000,
44
+ }: LoadingOverlayProps) {
45
+ const [currentIndex, setCurrentIndex] = useState(0);
46
+ const [shouldRender, setShouldRender] = useState(visible);
47
+
48
+ // Animation values
49
+ const translateY = useSharedValue(0);
50
+ const opacity = useSharedValue(1);
51
+
52
+ const wholeOpacity = useSharedValue(0);
53
+
54
+ // Handle fade-in and fade-out animations
55
+ useEffect(() => {
56
+ if (visible) {
57
+ setShouldRender(true); // Ensure the component is mounted
58
+ wholeOpacity.value = withTiming(1, { duration: 500 }); // Fade in
59
+ } else {
60
+ wholeOpacity.value = withTiming(0, { duration: 500 }, () => {
61
+ // Unmount after fade-out
62
+ runOnJS(setShouldRender)(false);
63
+ });
64
+ }
65
+ }, [visible]);
66
+
67
+ // Cycle messages on a timer
68
+ useEffect(() => {
69
+ if (!visible) {
70
+ setCurrentIndex(0);
71
+ return;
72
+ }
73
+
74
+ const timeout = setTimeout(() => {
75
+ setCurrentIndex((prev) => (prev + 1) % messages.length);
76
+ }, interval);
77
+
78
+ return () => clearTimeout(timeout);
79
+ }, [visible, currentIndex, interval, messages.length]);
80
+
81
+ // Trigger animation on each message change
82
+ useEffect(() => {
83
+ if (!visible) return;
84
+
85
+ const fadeDuration = Math.min(interval / 2, 250); // Simplified fade duration
86
+
87
+ // Reset animation values
88
+ translateY.value = 20;
89
+ opacity.value = 0;
90
+
91
+ // Sequential fade-in and fade-out
92
+ translateY.value = withTiming(0, { duration: fadeDuration });
93
+ opacity.value = withTiming(1, { duration: fadeDuration }, () => {
94
+ // add a delay for interval - (fadeDuration*2)
95
+
96
+ translateY.value = withDelay(
97
+ interval - fadeDuration * 2,
98
+ withTiming(-10, { duration: fadeDuration }),
99
+ );
100
+ opacity.value = withDelay(
101
+ interval - fadeDuration * 2,
102
+ withTiming(0, { duration: fadeDuration }),
103
+ );
104
+ });
105
+ }, [currentIndex, visible]);
106
+
107
+ const animatedStyle = useAnimatedStyle(() => {
108
+ return {
109
+ transform: [{ translateY: translateY.value }],
110
+ opacity: opacity.value,
111
+ };
112
+ });
113
+
114
+ const wholeAnimatedStyle = useAnimatedStyle(() => ({
115
+ opacity: wholeOpacity.value,
116
+ }));
117
+
118
+ if (!shouldRender) return null;
119
+
120
+ return (
121
+ <Animated.View
122
+ style={[
123
+ {
124
+ position: "absolute",
125
+ top: 0,
126
+ left: 0,
127
+ width,
128
+ height,
129
+ backgroundColor: "rgba(0,0,0,0.7)",
130
+ alignItems: "center",
131
+ justifyContent: "center",
132
+ zIndex: 1000,
133
+ },
134
+ wholeAnimatedStyle,
135
+ ]}
136
+ >
137
+ <Animated.Text
138
+ style={[
139
+ {
140
+ color: "white",
141
+ fontSize: 24,
142
+ fontWeight: "bold",
143
+ },
144
+ animatedStyle,
145
+ ]}
146
+ >
147
+ {messages[currentIndex]}
148
+ </Animated.Text>
149
+ <Animated.Text style={[pt[5], { color: "#a0a0a0" }]}>
150
+ {subtitle}
151
+ </Animated.Text>
152
+ </Animated.View>
153
+ );
154
+ }
@@ -15,7 +15,7 @@ import Animated, {
15
15
  } from "react-native-reanimated";
16
16
  import { useSafeAreaInsets } from "react-native-safe-area-context";
17
17
  import { useKeyboardSlide } from "../../hooks";
18
- import { bottom, layout, p, w, zIndex } from "../../lib/theme/atoms";
18
+ import { bottom, h, layout, p, w, zIndex } from "../../lib/theme/atoms";
19
19
  import { View } from "./view";
20
20
 
21
21
  const AnimatedView = Animated.createAnimatedComponent(View);
@@ -154,24 +154,35 @@ export function Resizable({
154
154
  style,
155
155
  ]}
156
156
  >
157
- <View style={[layout.flex.row, layout.flex.justifyCenter]}>
157
+ <View style={[layout.flex.row, layout.flex.justifyCenter, h[2]]}>
158
158
  <GestureDetector gesture={panGesture}>
159
159
  <View
160
- hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
161
- style={[
162
- w[32],
163
- {
164
- height: 6,
165
- transform: [{ translateY: -10 }],
166
- backgroundColor: "#eeeeee66",
167
- alignItems: "center",
168
- justifyContent: "center",
169
- borderRadius: 999,
170
- },
171
- ]}
172
- />
160
+ // Make the touch area much larger, but keep the visible handle small
161
+ style={{
162
+ height: 30, // Large touch area
163
+ width: 120, // Wide enough for thumbs
164
+ alignItems: "center",
165
+ justifyContent: "center",
166
+ //backgroundColor: "rgba(0,255,255,0.1)",
167
+ transform: [{ translateY: -30 }],
168
+ }}
169
+ >
170
+ <View
171
+ style={[
172
+ w[32],
173
+ {
174
+ height: 6,
175
+ backgroundColor: "#eeeeee66",
176
+ borderRadius: 999,
177
+
178
+ transform: [{ translateY: 5 }],
179
+ },
180
+ ]}
181
+ />
182
+ </View>
173
183
  </GestureDetector>
174
184
  </View>
185
+
175
186
  {children}
176
187
  </AnimatedView>
177
188
  </>
@@ -119,6 +119,10 @@ export interface PlayerState {
119
119
  | undefined,
120
120
  ) => void;
121
121
 
122
+ pipAction: (() => void) | undefined;
123
+ /** Function to set the Picture-in-Picture action */
124
+ setPipAction: (action: (() => void) | undefined) => void;
125
+
122
126
  /** Player element width (CSS value or number) */
123
127
  playerWidth?: string | number;
124
128
  /** Function to set the player width */
@@ -83,6 +83,11 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
83
83
  pipMode: false,
84
84
  setPipMode: (pipMode: boolean) => set(() => ({ pipMode })),
85
85
 
86
+ // Picture-in-Picture action function (set by player component)
87
+ pipAction: undefined,
88
+ setPipAction: (action: (() => void) | undefined) =>
89
+ set(() => ({ pipAction: action })),
90
+
86
91
  // Player element width/height setters for global sync
87
92
  playerWidth: undefined,
88
93
  setPlayerWidth: (playerWidth: number) => set(() => ({ playerWidth })),