diff --git a/js/app/components/mobile/desktop-ui.tsx b/js/app/components/mobile/desktop-ui.tsx index e50aa9a63..8cf2b6b1d 100644 --- a/js/app/components/mobile/desktop-ui.tsx +++ b/js/app/components/mobile/desktop-ui.tsx @@ -254,6 +254,7 @@ export function DesktopUi({ setTitle={setTitle} ingestStarting={ingestStarting} toggleGoLive={toggleGoLive} + isLive={isActivelyLive} /> )} diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index fdad83430..aa7aee332 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -72,6 +72,7 @@ export function MobileUi({ ingestStarting, setIngestStarting, toggleGoLive, + toggleStopStream, } = useLivestreamInfo(); const { width, height } = usePlayerDimensions(); const { isPlayerRatioGreater } = useSegmentDimensions(); @@ -103,6 +104,12 @@ export function MobileUi({ const isSelfAndNotLive = ingest === "new"; const isLive = ingest !== null && ingest !== "new"; + useEffect(() => { + if (isLive && ingestStarting) { + setIngestStarting(false); + } + }, [isLive, ingestStarting, setIngestStarting]); + const FADE_OUT_DELAY = 4000; const fadeOpacity = useSharedValue(1); const fadeTimeout = useRef(null); @@ -222,7 +229,7 @@ export function MobileUi({ )} @@ -241,6 +248,7 @@ export function MobileUi({ setTitle={setTitle} ingestStarting={ingestStarting} toggleGoLive={toggleGoLive} + isLive={isLive} /> )} @@ -468,6 +476,7 @@ function RightControlsPanel({ + {Platform.OS === "web" && } )} {Platform.OS === "web" ? ( @@ -515,7 +524,11 @@ function RightControlsPanel({ )} )} - + {ingest === null ? ( + + ) : ( + + )} )} {shouldShowChatSidePanel && setShowChat && ( diff --git a/js/components/src/components/mobile-player/ui/input.tsx b/js/components/src/components/mobile-player/ui/input.tsx index e713cbb61..ac5888edf 100644 --- a/js/components/src/components/mobile-player/ui/input.tsx +++ b/js/components/src/components/mobile-player/ui/input.tsx @@ -9,6 +9,8 @@ type InputPanelProps = { setTitle: (title: string) => void; ingestStarting: boolean; toggleGoLive: () => void; + isLive: boolean; + toggleStopStream?: () => void; }; export function InputPanel({ @@ -16,6 +18,8 @@ export function InputPanel({ setTitle, ingestStarting, toggleGoLive, + isLive, + toggleStopStream, }: InputPanelProps) { const { slideKeyboard } = useKeyboardSlide(); return ( @@ -37,16 +41,46 @@ export function InputPanel({ { padding: 10 }, ]} > - - - + {!isLive && ( + + + + )} {ingestStarting ? ( Starting your stream... + ) : isLive ? ( + + + + Stop Stream + + ) : ( ; +import { ChevronRight, Cog } from "lucide-react-native"; +import { useEffect, useState } from "react"; +import Animated, { + Easing, + useAnimatedStyle, + useSharedValue, + withDelay, + withSequence, + withTiming, +} from "react-native-reanimated"; +import { useLivestreamInfo, zero } from "../../.."; +import { usePlayerStore } from "../../../player-store"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, + ResponsiveDropdownMenuContent, + Text, + useTheme, +} from "../../ui"; + +export function StreamContextMenu({ + dropdownPortalContainer, +}: { + dropdownPortalContainer?: string; +}) { + const th = useTheme(); + const debugInfo = usePlayerStore((x) => x.showDebugInfo); + const setShowDebugInfo = usePlayerStore((x) => x.setShowDebugInfo); + const { toggleStopStream } = useLivestreamInfo(); + const ingest = usePlayerStore((x) => x.ingestConnectionState); + const isLive = ingest !== null && ingest !== "new"; + + const [isOpen, setIsOpen] = useState(false); + const [hasShownTooltip, setHasShownTooltip] = useState(false); + + const tooltipOpacity = useSharedValue(0); + const tooltipTranslateX = useSharedValue(20); + + useEffect(() => { + if (isLive && !hasShownTooltip) { + tooltipOpacity.value = withDelay( + 500, + withSequence( + withTiming(1, { duration: 300 }), + withDelay(10000, withTiming(0, { duration: 300 })), + ), + ); + tooltipTranslateX.value = withDelay( + 500, + withSequence( + withTiming(0, { duration: 300 }), + withDelay(10000, withTiming(20, { duration: 300 })), + ), + ); + setHasShownTooltip(true); + } + }, [isLive, hasShownTooltip]); + + const iconRotate = useAnimatedStyle(() => { + return { + transform: [ + { + rotateZ: withTiming(isOpen ? "240deg" : "0deg", { + duration: 650, + easing: Easing.out(Easing.ease), + }), + }, + ], + }; + }); + + const tooltipStyle = useAnimatedStyle(() => { + return { + opacity: tooltipOpacity.value, + transform: [{ translateX: tooltipTranslateX.value }], + }; + }); + + return ( + + + + + + + + End stream here + + + + + + {isLive && ( + + { + toggleStopStream(); + }} + > + Stop Stream + + + )} + + setShowDebugInfo(!debugInfo)} + > + Show Debug Info + + + + + ); } diff --git a/js/components/src/hooks/useLivestreamInfo.ts b/js/components/src/hooks/useLivestreamInfo.ts index 7ce04a937..690a1142d 100644 --- a/js/components/src/hooks/useLivestreamInfo.ts +++ b/js/components/src/hooks/useLivestreamInfo.ts @@ -9,6 +9,7 @@ export function useLivestreamInfo(url?: string) { const ingestStarting = usePlayerStore((x) => x.ingestStarting); const setIngestStarting = usePlayerStore((x) => x.setIngestStarting); const setIngestLive = usePlayerStore((x) => x.setIngestLive); + const stopIngest = usePlayerStore((x) => x.stopIngest); const createStreamRecord = useCreateStreamRecord(); @@ -54,6 +55,12 @@ export function useLivestreamInfo(url?: string) { } }; + // Stop the current broadcast + const toggleStopStream = () => { + console.log("Stopping stream..."); + stopIngest(); + }; + return { ingest, profile, @@ -67,5 +74,6 @@ export function useLivestreamInfo(url?: string) { setIngestStarting, handleSubmit, toggleGoLive, + toggleStopStream, }; } diff --git a/js/components/src/player-store/player-state.tsx b/js/components/src/player-store/player-state.tsx index 7a9bd98cf..b7f76e6f5 100644 --- a/js/components/src/player-store/player-state.tsx +++ b/js/components/src/player-store/player-state.tsx @@ -63,6 +63,9 @@ export interface PlayerState { ingestAutoStart?: boolean; setIngestAutoStart?: (autoStart: boolean) => void; + /** stop ingest process, again with a slight delay to allow UI to update */ + stopIngest: () => void; + /** Timestamp (number) when ingest started, or null if not started */ ingestStarted: number | null; diff --git a/js/components/src/player-store/player-store.tsx b/js/components/src/player-store/player-store.tsx index b62b4d3c4..115dfc94e 100644 --- a/js/components/src/player-store/player-store.tsx +++ b/js/components/src/player-store/player-store.tsx @@ -53,6 +53,23 @@ export const makePlayerStore = (id?: string): StoreApi => { setIngestStarted: (timestamp: number | null) => set(() => ({ ingestStarted: timestamp })), + stopIngest: () => { + set(() => ({ + ingestLive: false, + ingestConnectionState: "new", + ingestStarted: null, + })), + setTimeout( + () => + set(() => ({ + ingestLive: false, + ingestConnectionState: "new", + ingestStarted: null, + })), + 200, + ); + }, + fullscreen: false, setFullscreen: (isFullscreen: boolean) => set(() => ({ fullscreen: isFullscreen })), -- 2.51.2 From 09af00f48ab8c7d4aea5867f4e802689408f0d84 Mon Sep 17 00:00:00 2001 From: "Natalie B." <22222885+espeon@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:53:19 +0100 Subject: [PATCH 2/2] login modal should stop blocking the screen --- js/app/components/login/login-modal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/app/components/login/login-modal.tsx b/js/app/components/login/login-modal.tsx index 02f23d120..ca0da6c83 100644 --- a/js/app/components/login/login-modal.tsx +++ b/js/app/components/login/login-modal.tsx @@ -11,6 +11,10 @@ interface LoginModalProps { export default function LoginModal({ visible, onClose }: LoginModalProps) { const { theme, zero: z } = useTheme(); + if (!visible) { + return null; + } + return (