@streamplace/components 0.7.2 → 0.7.7
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/dist/components/chat/chat-box.js +212 -24
- package/dist/components/chat/chat-message.js +5 -5
- package/dist/components/chat/chat.js +83 -5
- package/dist/components/chat/emoji-suggestions.js +35 -0
- package/dist/components/chat/mod-view.js +59 -8
- package/dist/components/chat/system-message.js +19 -0
- package/dist/components/icons/bluesky-icon.js +9 -0
- package/dist/components/keep-awake.js +7 -0
- package/dist/components/keep-awake.native.js +16 -0
- package/dist/components/mobile-player/fullscreen.js +2 -1
- package/dist/components/mobile-player/fullscreen.native.js +3 -3
- package/dist/components/mobile-player/player.js +15 -30
- package/dist/components/mobile-player/ui/index.js +2 -0
- package/dist/components/mobile-player/ui/report-modal.js +90 -0
- package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
- package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
- package/dist/components/mobile-player/use-webrtc.js +7 -1
- package/dist/components/mobile-player/video-retry.js +29 -0
- package/dist/components/mobile-player/video.js +84 -9
- package/dist/components/mobile-player/video.native.js +24 -10
- package/dist/components/share/sharesheet.js +91 -0
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/dropdown.js +6 -6
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/primitives/modal.js +0 -1
- package/dist/components/ui/resizeable.js +20 -11
- package/dist/components/ui/slider.js +5 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/usePointerDevice.js +71 -0
- package/dist/index.js +10 -3
- package/dist/lib/system-messages.js +101 -0
- package/dist/livestream-store/chat.js +111 -18
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/problems.js +76 -0
- package/dist/livestream-store/websocket-consumer.js +39 -4
- package/dist/player-store/player-store.js +33 -4
- package/dist/streamplace-store/block.js +51 -12
- package/dist/streamplace-store/stream.js +44 -23
- package/dist/ui/index.js +79 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/67b1eb60 +0 -0
- package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/7c275f90 +0 -0
- package/package.json +6 -2
- package/src/components/chat/chat-box.tsx +295 -25
- package/src/components/chat/chat-message.tsx +6 -7
- package/src/components/chat/chat.tsx +192 -41
- package/src/components/chat/emoji-suggestions.tsx +94 -0
- package/src/components/chat/mod-view.tsx +119 -40
- package/src/components/chat/system-message.tsx +38 -0
- package/src/components/icons/bluesky-icon.tsx +9 -0
- package/src/components/keep-awake.native.tsx +13 -0
- package/src/components/keep-awake.tsx +3 -0
- package/src/components/mobile-player/fullscreen.native.tsx +12 -3
- package/src/components/mobile-player/fullscreen.tsx +10 -3
- package/src/components/mobile-player/player.tsx +28 -36
- package/src/components/mobile-player/props.tsx +1 -0
- package/src/components/mobile-player/ui/index.ts +2 -0
- package/src/components/mobile-player/ui/report-modal.tsx +195 -0
- package/src/components/mobile-player/ui/streamer-loading-overlay.tsx +154 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
- package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
- package/src/components/mobile-player/use-webrtc.tsx +10 -2
- package/src/components/mobile-player/video-retry.tsx +28 -0
- package/src/components/mobile-player/video.native.tsx +24 -10
- package/src/components/mobile-player/video.tsx +100 -21
- package/src/components/share/sharesheet.tsx +185 -0
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/dropdown.tsx +13 -13
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/primitives/modal.tsx +0 -1
- package/src/components/ui/resizeable.tsx +26 -15
- package/src/components/ui/slider.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/usePointerDevice.ts +89 -0
- package/src/index.tsx +11 -2
- package/src/lib/system-messages.ts +135 -0
- package/src/livestream-store/chat.tsx +145 -17
- package/src/livestream-store/livestream-state.tsx +10 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/problems.tsx +96 -0
- package/src/livestream-store/websocket-consumer.tsx +44 -4
- package/src/player-store/player-state.tsx +25 -4
- package/src/player-store/player-store.tsx +43 -5
- package/src/streamplace-store/block.tsx +55 -13
- package/src/streamplace-store/stream.tsx +66 -35
- package/src/ui/index.ts +86 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-92db9086-0/56540125 +0 -0
|
@@ -4,13 +4,18 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { BackHandler, Dimensions, StyleSheet, View } from "react-native";
|
|
5
5
|
import { SystemBars } from "react-native-edge-to-edge";
|
|
6
6
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
PlayerProtocol,
|
|
9
|
+
useLivestreamStore,
|
|
10
|
+
usePlayerStore,
|
|
11
|
+
VideoRetry,
|
|
12
|
+
} from "../..";
|
|
8
13
|
import Video from "./video.native";
|
|
9
14
|
|
|
10
15
|
// Standard 16:9 video aspect ratio
|
|
11
16
|
const VIDEO_ASPECT_RATIO = 16 / 9;
|
|
12
17
|
|
|
13
|
-
export function Fullscreen(props: { src: string }) {
|
|
18
|
+
export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
|
|
14
19
|
const ref = useRef<VideoView>(null);
|
|
15
20
|
const insets = useSafeAreaInsets();
|
|
16
21
|
const navigation = useNavigation();
|
|
@@ -157,6 +162,7 @@ export function Fullscreen(props: { src: string }) {
|
|
|
157
162
|
]}
|
|
158
163
|
>
|
|
159
164
|
<Video />
|
|
165
|
+
{props.children}
|
|
160
166
|
</View>
|
|
161
167
|
</View>
|
|
162
168
|
);
|
|
@@ -165,7 +171,10 @@ export function Fullscreen(props: { src: string }) {
|
|
|
165
171
|
// Normal non-fullscreen mode
|
|
166
172
|
return (
|
|
167
173
|
<>
|
|
168
|
-
<
|
|
174
|
+
<VideoRetry>
|
|
175
|
+
<Video />
|
|
176
|
+
</VideoRetry>
|
|
177
|
+
{props.children}
|
|
169
178
|
</>
|
|
170
179
|
);
|
|
171
180
|
}
|
|
@@ -3,8 +3,9 @@ import { View as RNView } from "react-native";
|
|
|
3
3
|
import { getFirstPlayerID, usePlayerStore } from "../..";
|
|
4
4
|
import { View } from "../../components/ui";
|
|
5
5
|
import Video from "./video";
|
|
6
|
+
import VideoRetry from "./video-retry";
|
|
6
7
|
|
|
7
|
-
export function Fullscreen(props: { src: string }) {
|
|
8
|
+
export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
|
|
8
9
|
const playerId = getFirstPlayerID();
|
|
9
10
|
const protocol = usePlayerStore((x) => x.protocol, playerId);
|
|
10
11
|
const fullscreen = usePlayerStore((x) => x.fullscreen, playerId);
|
|
@@ -72,8 +73,14 @@ export function Fullscreen(props: { src: string }) {
|
|
|
72
73
|
}, []);
|
|
73
74
|
|
|
74
75
|
return (
|
|
75
|
-
<View
|
|
76
|
-
|
|
76
|
+
<View
|
|
77
|
+
ref={divRef}
|
|
78
|
+
style={{ width: "100%", height: "100%", overflow: "hidden" }}
|
|
79
|
+
>
|
|
80
|
+
<VideoRetry>
|
|
81
|
+
<Video />
|
|
82
|
+
</VideoRetry>
|
|
83
|
+
{props.children}
|
|
77
84
|
</View>
|
|
78
85
|
);
|
|
79
86
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import { flex, layout, w, zIndex } from "../../lib/theme/atoms";
|
|
3
|
-
import { useSegment } from "../../livestream-store";
|
|
2
|
+
import { flex, h, layout, w, zIndex } from "../../lib/theme/atoms";
|
|
4
3
|
import {
|
|
5
4
|
PlayerStatus,
|
|
6
5
|
PlayerStatusTracker,
|
|
@@ -10,19 +9,29 @@ import { useStreamplaceStore } from "../../streamplace-store";
|
|
|
10
9
|
import { Text, View } from "../ui";
|
|
11
10
|
import { Fullscreen } from "./fullscreen";
|
|
12
11
|
import { PlayerProps } from "./props";
|
|
12
|
+
import ReportModal from "./ui/report-modal";
|
|
13
13
|
|
|
14
14
|
const OFFLINE_THRESHOLD = 10000;
|
|
15
15
|
|
|
16
16
|
export * as PlayerUI from "./ui";
|
|
17
17
|
|
|
18
|
-
export function Player(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const setOffline = usePlayerStore((x) => x.setOffline);
|
|
18
|
+
export function Player(
|
|
19
|
+
props: Partial<PlayerProps> & { children?: React.ReactNode },
|
|
20
|
+
) {
|
|
22
21
|
const setIngest = usePlayerStore((x) => x.setIngestConnectionState);
|
|
23
22
|
|
|
24
23
|
const clearControlsTimeout = usePlayerStore((x) => x.clearControlsTimeout);
|
|
25
24
|
|
|
25
|
+
const setReportingURL = usePlayerStore((x) => x.setReportingURL);
|
|
26
|
+
|
|
27
|
+
const reportModalOpen = usePlayerStore((x) => x.reportModalOpen);
|
|
28
|
+
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
29
|
+
const reportSubject = usePlayerStore((x) => x.reportSubject);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setReportingURL(props.reportingURL ?? null);
|
|
33
|
+
}, [props.reportingURL]);
|
|
34
|
+
|
|
26
35
|
// Will call back every few seconds to send health updates
|
|
27
36
|
usePlayerStatus();
|
|
28
37
|
|
|
@@ -44,40 +53,23 @@ export function Player(props: Partial<PlayerProps>) {
|
|
|
44
53
|
};
|
|
45
54
|
}, []);
|
|
46
55
|
|
|
47
|
-
const segment = useSegment();
|
|
48
|
-
const [lastCheck, setLastCheck] = useState(0);
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (playing) {
|
|
52
|
-
setOffline(false);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (!segment) {
|
|
56
|
-
setOffline(false);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const startTime = Date.parse(segment.startTime);
|
|
60
|
-
if (!startTime) {
|
|
61
|
-
console.error("startTime is not a number", segment.startTime);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const timeSinceStart = Date.now() - startTime;
|
|
65
|
-
if (timeSinceStart > OFFLINE_THRESHOLD) {
|
|
66
|
-
setOffline(true);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const handle = setTimeout(() => {
|
|
70
|
-
setLastCheck(Date.now());
|
|
71
|
-
}, 1000);
|
|
72
|
-
return () => clearTimeout(handle);
|
|
73
|
-
}, [segment, playing, lastCheck]);
|
|
74
|
-
|
|
75
56
|
return (
|
|
76
57
|
<>
|
|
77
58
|
<View
|
|
78
|
-
style={[
|
|
59
|
+
style={[
|
|
60
|
+
zIndex[0],
|
|
61
|
+
w.percent[100],
|
|
62
|
+
h.percent[100],
|
|
63
|
+
flex.shrink[1],
|
|
64
|
+
layout.flex.center,
|
|
65
|
+
]}
|
|
79
66
|
>
|
|
80
|
-
<
|
|
67
|
+
<ReportModal
|
|
68
|
+
open={reportModalOpen}
|
|
69
|
+
onOpenChange={setReportModalOpen}
|
|
70
|
+
subject={reportSubject!}
|
|
71
|
+
/>
|
|
72
|
+
<Fullscreen src={props.src}>{props.children}</Fullscreen>
|
|
81
73
|
</View>
|
|
82
74
|
</>
|
|
83
75
|
);
|
|
@@ -2,5 +2,7 @@ export * from "./countdown";
|
|
|
2
2
|
export * from "./input";
|
|
3
3
|
export * from "./metrics";
|
|
4
4
|
export * from "./streamer-context-menu";
|
|
5
|
+
export * from "./streamer-loading-overlay";
|
|
5
6
|
export * from "./viewer-context-menu";
|
|
7
|
+
export * from "./viewer-loading-overlay";
|
|
6
8
|
export * from "./viewers";
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComAtprotoModerationCreateReport,
|
|
3
|
+
ComAtprotoModerationDefs,
|
|
4
|
+
} from "@atproto/api";
|
|
5
|
+
import { CheckCircle, Circle, Loader2 } from "lucide-react-native";
|
|
6
|
+
import React, { useState } from "react";
|
|
7
|
+
import { TouchableOpacity, View } from "react-native";
|
|
8
|
+
import { zero } from "../../..";
|
|
9
|
+
import { useSubmitReport } from "../../../livestream-store";
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
ModalContent,
|
|
15
|
+
Text,
|
|
16
|
+
Textarea,
|
|
17
|
+
} from "../../ui";
|
|
18
|
+
|
|
19
|
+
// AT Protocol moderation reason types with proper labels
|
|
20
|
+
const REPORT_REASONS = [
|
|
21
|
+
{
|
|
22
|
+
value: ComAtprotoModerationDefs.REASONSPAM,
|
|
23
|
+
label: "Spam",
|
|
24
|
+
description: "Excessive unwanted promotion, replies, mentions",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
value: ComAtprotoModerationDefs.REASONVIOLATION,
|
|
28
|
+
label: "Rule Violation",
|
|
29
|
+
description: "Direct, blatant violation of laws or terms of service",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: ComAtprotoModerationDefs.REASONMISLEADING,
|
|
33
|
+
label: "Misleading Content",
|
|
34
|
+
description: "Misleading identity, affiliation, or content",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
value: ComAtprotoModerationDefs.REASONSEXUAL,
|
|
38
|
+
label: "Sexual Content",
|
|
39
|
+
description: "Unwanted or mislabeled sexual content",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: ComAtprotoModerationDefs.REASONRUDE,
|
|
43
|
+
label: "Harassment",
|
|
44
|
+
description: "Rude, harassing, explicit, or otherwise unwelcoming behavior",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: ComAtprotoModerationDefs.REASONOTHER,
|
|
48
|
+
label: "Other",
|
|
49
|
+
description: "Reports not falling under another report category",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
interface ReportModalProps {
|
|
54
|
+
open: boolean;
|
|
55
|
+
onOpenChange: (open: boolean) => void;
|
|
56
|
+
onSubmit?: (reason: string, additionalComments?: string) => void;
|
|
57
|
+
subject: ComAtprotoModerationCreateReport.InputSchema["subject"];
|
|
58
|
+
title?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const ReportModal: React.FC<ReportModalProps> = ({
|
|
63
|
+
open,
|
|
64
|
+
onOpenChange,
|
|
65
|
+
onSubmit,
|
|
66
|
+
subject,
|
|
67
|
+
title = "Report",
|
|
68
|
+
description = "Why are you submitting this report?",
|
|
69
|
+
}) => {
|
|
70
|
+
const [selectedReason, setSelectedReason] = useState<string | null>(null);
|
|
71
|
+
const [additionalComments, setAdditionalComments] = useState<string>("");
|
|
72
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
73
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
74
|
+
|
|
75
|
+
const submitReport = useSubmitReport();
|
|
76
|
+
|
|
77
|
+
const handleCancel = () => {
|
|
78
|
+
setSelectedReason(null);
|
|
79
|
+
setAdditionalComments("");
|
|
80
|
+
setSubmitError(null);
|
|
81
|
+
onOpenChange(false);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleSubmit = async () => {
|
|
85
|
+
if (!selectedReason) return;
|
|
86
|
+
|
|
87
|
+
setIsSubmitting(true);
|
|
88
|
+
setSubmitError(null);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
submitReport(
|
|
92
|
+
subject,
|
|
93
|
+
selectedReason,
|
|
94
|
+
additionalComments.trim() || undefined,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Reset form and close modal on success
|
|
98
|
+
setSelectedReason(null);
|
|
99
|
+
setAdditionalComments("");
|
|
100
|
+
onOpenChange(false);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Failed to submit report:", error);
|
|
103
|
+
setSubmitError("Failed to submit report. Please try again.");
|
|
104
|
+
} finally {
|
|
105
|
+
setIsSubmitting(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Dialog
|
|
111
|
+
open={open}
|
|
112
|
+
onOpenChange={onOpenChange}
|
|
113
|
+
title={title}
|
|
114
|
+
description={description}
|
|
115
|
+
showCloseButton
|
|
116
|
+
variant="default"
|
|
117
|
+
size="md"
|
|
118
|
+
dismissible={false}
|
|
119
|
+
position="center"
|
|
120
|
+
>
|
|
121
|
+
<ModalContent style={[zero.pb[2]]}>
|
|
122
|
+
{REPORT_REASONS.map((reason) => (
|
|
123
|
+
<TouchableOpacity
|
|
124
|
+
key={reason.value}
|
|
125
|
+
onPress={() => setSelectedReason(reason.value)}
|
|
126
|
+
style={[
|
|
127
|
+
zero.layout.flex.row,
|
|
128
|
+
zero.gap.all[2],
|
|
129
|
+
zero.py[3],
|
|
130
|
+
zero.px[3],
|
|
131
|
+
zero.borderRadius[8],
|
|
132
|
+
zero.layout.flex.alignCenter,
|
|
133
|
+
selectedReason === reason.value && {
|
|
134
|
+
backgroundColor: "rgba(0, 122, 255, 0.1)",
|
|
135
|
+
},
|
|
136
|
+
]}
|
|
137
|
+
>
|
|
138
|
+
<View>
|
|
139
|
+
{selectedReason === reason.value ? <CheckCircle /> : <Circle />}
|
|
140
|
+
</View>
|
|
141
|
+
<View
|
|
142
|
+
style={[zero.layout.flex.column, zero.gap.all[1], zero.flex[1]]}
|
|
143
|
+
>
|
|
144
|
+
<Text style={[{ fontWeight: "600" }]}>{reason.label}</Text>
|
|
145
|
+
<Text style={[{ fontSize: 14, color: "rgba(255,255,255,0.7)" }]}>
|
|
146
|
+
{reason.description}
|
|
147
|
+
</Text>
|
|
148
|
+
</View>
|
|
149
|
+
</TouchableOpacity>
|
|
150
|
+
))}
|
|
151
|
+
|
|
152
|
+
<View style={[zero.pb[4], zero.mt[4], zero.px[2]]}>
|
|
153
|
+
<Text style={[zero.mb[2]]}>Additional Comments (optional)</Text>
|
|
154
|
+
<Textarea
|
|
155
|
+
maxLength={500}
|
|
156
|
+
numberOfLines={3}
|
|
157
|
+
value={additionalComments}
|
|
158
|
+
onChangeText={setAdditionalComments}
|
|
159
|
+
placeholder="Provide additional context for this report..."
|
|
160
|
+
/>
|
|
161
|
+
{submitError && (
|
|
162
|
+
<Text style={[zero.mt[2], { color: "red", fontSize: 14 }]}>
|
|
163
|
+
{submitError}
|
|
164
|
+
</Text>
|
|
165
|
+
)}
|
|
166
|
+
</View>
|
|
167
|
+
</ModalContent>
|
|
168
|
+
<DialogFooter>
|
|
169
|
+
<Button
|
|
170
|
+
variant="secondary"
|
|
171
|
+
onPress={handleCancel}
|
|
172
|
+
disabled={isSubmitting}
|
|
173
|
+
>
|
|
174
|
+
<Text>Cancel</Text>
|
|
175
|
+
</Button>
|
|
176
|
+
<Button
|
|
177
|
+
variant="primary"
|
|
178
|
+
onPress={handleSubmit}
|
|
179
|
+
disabled={!selectedReason || isSubmitting}
|
|
180
|
+
>
|
|
181
|
+
{isSubmitting ? (
|
|
182
|
+
<>
|
|
183
|
+
<Loader2 style={[{ marginRight: 8 }]} />
|
|
184
|
+
<Text>Submitting...</Text>
|
|
185
|
+
</>
|
|
186
|
+
) : (
|
|
187
|
+
<Text>Submit</Text>
|
|
188
|
+
)}
|
|
189
|
+
</Button>
|
|
190
|
+
</DialogFooter>
|
|
191
|
+
</Dialog>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export default ReportModal;
|
|
@@ -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 the 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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRootContext } from "@rn-primitives/dropdown-menu";
|
|
2
|
+
import { Settings } from "lucide-react-native";
|
|
2
3
|
import { colors } from "../../../lib/theme";
|
|
3
4
|
import { useLivestreamStore } from "../../../livestream-store";
|
|
4
5
|
import { PlayerProtocol, usePlayerStore } from "../../../player-store/";
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
DropdownMenuCheckboxItem,
|
|
8
9
|
DropdownMenuGroup,
|
|
9
10
|
DropdownMenuInfo,
|
|
11
|
+
DropdownMenuItem,
|
|
10
12
|
DropdownMenuRadioGroup,
|
|
11
13
|
DropdownMenuRadioItem,
|
|
12
14
|
DropdownMenuTrigger,
|
|
@@ -33,9 +35,9 @@ export function ContextMenu() {
|
|
|
33
35
|
return (
|
|
34
36
|
<DropdownMenu>
|
|
35
37
|
<DropdownMenuTrigger>
|
|
36
|
-
<
|
|
38
|
+
<Settings color={colors.gray[200]} />
|
|
37
39
|
</DropdownMenuTrigger>
|
|
38
|
-
<ResponsiveDropdownMenuContent>
|
|
40
|
+
<ResponsiveDropdownMenuContent side="top" align="end">
|
|
39
41
|
<DropdownMenuGroup title="Resolution">
|
|
40
42
|
<DropdownMenuRadioGroup value={quality} onValueChange={setQuality}>
|
|
41
43
|
<DropdownMenuRadioItem value="source">
|
|
@@ -65,7 +67,33 @@ export function ContextMenu() {
|
|
|
65
67
|
<Text>Show Debug Info</Text>
|
|
66
68
|
</DropdownMenuCheckboxItem>
|
|
67
69
|
</DropdownMenuGroup>
|
|
70
|
+
<DropdownMenuGroup title="Report">
|
|
71
|
+
<ReportButton />
|
|
72
|
+
</DropdownMenuGroup>
|
|
68
73
|
</ResponsiveDropdownMenuContent>
|
|
69
74
|
</DropdownMenu>
|
|
70
75
|
);
|
|
71
76
|
}
|
|
77
|
+
|
|
78
|
+
export function ReportButton() {
|
|
79
|
+
const livestream = useLivestreamStore((x) => x.livestream);
|
|
80
|
+
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
81
|
+
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
82
|
+
const { onOpenChange } = useRootContext();
|
|
83
|
+
return (
|
|
84
|
+
<DropdownMenuItem
|
|
85
|
+
onPress={() => {
|
|
86
|
+
if (!livestream) return;
|
|
87
|
+
onOpenChange?.(false);
|
|
88
|
+
setReportModalOpen(true);
|
|
89
|
+
setReportSubject({
|
|
90
|
+
$type: "com.atproto.repo.strongRef",
|
|
91
|
+
uri: livestream.uri,
|
|
92
|
+
cid: livestream.cid,
|
|
93
|
+
});
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Text>Report Livestream...</Text>
|
|
97
|
+
</DropdownMenuItem>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Play } from "lucide-react-native";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import {
|
|
9
|
+
KeepAwake,
|
|
10
|
+
Loader,
|
|
11
|
+
PlayerStatus,
|
|
12
|
+
usePlayerStore,
|
|
13
|
+
useTheme,
|
|
14
|
+
} from "../../..";
|
|
15
|
+
|
|
16
|
+
export function ViewerLoadingOverlay() {
|
|
17
|
+
const status = usePlayerStore((x) => x.status);
|
|
18
|
+
const theme = useTheme();
|
|
19
|
+
const opacity = useSharedValue(0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (status === PlayerStatus.PLAYING || status === PlayerStatus.SUSPEND) {
|
|
23
|
+
opacity.value = withTiming(0, { duration: 300 });
|
|
24
|
+
} else {
|
|
25
|
+
opacity.value = withTiming(1, { duration: 300 });
|
|
26
|
+
}
|
|
27
|
+
}, [status, opacity]);
|
|
28
|
+
|
|
29
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
30
|
+
return {
|
|
31
|
+
opacity: opacity.value,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (status === PlayerStatus.PLAYING) {
|
|
36
|
+
return <KeepAwake />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (status === PlayerStatus.SUSPEND) {
|
|
40
|
+
return null; // No overlay when stopped
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let spinner = <Loader size="large" />;
|
|
44
|
+
if (status === PlayerStatus.PAUSE) {
|
|
45
|
+
spinner = <Play size="$12" color={theme.styles.text.primary["color"]} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Animated.View
|
|
50
|
+
style={[
|
|
51
|
+
{
|
|
52
|
+
position: "absolute",
|
|
53
|
+
width: "100%",
|
|
54
|
+
height: "100%",
|
|
55
|
+
zIndex: 998,
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
justifyContent: "center",
|
|
58
|
+
backgroundColor: "rgba(0,0,0,0.3)",
|
|
59
|
+
},
|
|
60
|
+
animatedStyle,
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
{spinner}
|
|
64
|
+
</Animated.View>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { usePlayerStore, useStreamKey } from "../..";
|
|
2
|
+
import { PlayerStatus, usePlayerStore, useStreamKey } from "../..";
|
|
3
3
|
import { RTCPeerConnection, RTCSessionDescription } from "./webrtc-primitives";
|
|
4
4
|
|
|
5
5
|
export default function useWebRTC(
|
|
@@ -7,6 +7,7 @@ export default function useWebRTC(
|
|
|
7
7
|
): [MediaStream | null, boolean] {
|
|
8
8
|
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
|
|
9
9
|
const [stuck, setStuck] = useState<boolean>(false);
|
|
10
|
+
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
10
11
|
|
|
11
12
|
const lastChange = useRef<number>(0);
|
|
12
13
|
|
|
@@ -29,7 +30,12 @@ export default function useWebRTC(
|
|
|
29
30
|
});
|
|
30
31
|
peerConnection.addEventListener("connectionstatechange", () => {
|
|
31
32
|
console.log("connection state change", peerConnection.connectionState);
|
|
32
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
peerConnection.connectionState === "closed" ||
|
|
35
|
+
peerConnection.connectionState === "failed" ||
|
|
36
|
+
peerConnection.connectionState === "disconnected"
|
|
37
|
+
) {
|
|
38
|
+
console.log("setting stuck to true", peerConnection.connectionState);
|
|
33
39
|
setStuck(true);
|
|
34
40
|
}
|
|
35
41
|
if (peerConnection.connectionState !== "connected") {
|
|
@@ -52,6 +58,7 @@ export default function useWebRTC(
|
|
|
52
58
|
if (lastAudioFramesReceived !== audioFramesReceived) {
|
|
53
59
|
lastAudioFramesReceived = audioFramesReceived;
|
|
54
60
|
lastChange.current = Date.now();
|
|
61
|
+
setStatus(PlayerStatus.PLAYING);
|
|
55
62
|
setStuck(false);
|
|
56
63
|
}
|
|
57
64
|
}
|
|
@@ -60,6 +67,7 @@ export default function useWebRTC(
|
|
|
60
67
|
if (lastFramesReceived !== framesReceived) {
|
|
61
68
|
lastFramesReceived = framesReceived;
|
|
62
69
|
lastChange.current = Date.now();
|
|
70
|
+
setStatus(PlayerStatus.PLAYING);
|
|
63
71
|
setStuck(false);
|
|
64
72
|
}
|
|
65
73
|
}
|