From 8493b1efc100e72b2db1f06016532943d32d353f Mon Sep 17 00:00:00 2001 From: "Natalie B." <22222885+espeon@users.noreply.github.com> Date: Sat, 22 Nov 2025 16:42:55 -0600 Subject: [PATCH] modalize login component, handle returning to previous page, move account options to settings --- js/app/components/aqlink.tsx | 41 +++- .../live-dashboard/live-selector.tsx | 22 +- .../components/live-dashboard/stream-key.tsx | 17 +- js/app/components/login/login-modal.tsx | 192 +++++++++++++++++ js/app/components/login/login.tsx | 147 +++++-------- js/app/components/mobile/chat.tsx | 15 +- .../name-color-picker/name-color-picker.tsx | 196 ++++++++++++++++++ .../settings/account-category-settings.tsx | 115 ++++++++++ .../components/settings-navigation-item.tsx | 6 + js/app/components/settings/settings.tsx | 94 +++++---- js/app/features/bluesky/blueskyProvider.tsx | 64 +++++- js/app/src/router.tsx | 18 +- js/app/src/screens/live-dashboard.tsx | 15 +- js/app/src/screens/mobile-go-live.tsx | 15 +- js/app/src/screens/oauth-callback.tsx | 43 ++++ js/app/store/slices/blueskySlice.ts | 30 +++ js/app/utils/navigation.ts | 58 ++++++ js/components/locales/en-US/settings.ftl | 7 + js/components/locales/es-ES/settings.ftl | 7 + js/components/locales/fr-FR/settings.ftl | 7 + js/components/locales/pt-BR/settings.ftl | 7 + js/components/locales/zh-Hant/settings.ftl | 7 + 22 files changed, 966 insertions(+), 157 deletions(-) create mode 100644 js/app/components/login/login-modal.tsx create mode 100644 js/app/components/settings/account-category-settings.tsx create mode 100644 js/app/src/screens/oauth-callback.tsx create mode 100644 js/app/utils/navigation.ts diff --git a/js/app/components/aqlink.tsx b/js/app/components/aqlink.tsx index f40f510cd..ed875f3ef 100644 --- a/js/app/components/aqlink.tsx +++ b/js/app/components/aqlink.tsx @@ -4,10 +4,13 @@ import { ParamListBase, useLinkBuilder, useNavigation, + useNavigationState, + useRoute, } from "@react-navigation/native"; import usePlatform from "hooks/usePlatform"; import { useEffect } from "react"; import { Pressable, StyleProp, ViewStyle } from "react-native"; +import { useStore } from "store"; import Loading from "./loading/loading"; export type LinkParams = { screen: string; params?: Record }; @@ -25,11 +28,44 @@ export default function AQLink({ }) { const { isWeb } = usePlatform(); const navigation = useNavigation>(); + const route = useRoute(); + const openLoginModal = useStore((state) => state.openLoginModal); + + // get the deepest active route for nested navigators + const currentRoute = useNavigationState((state) => { + let route: any = state.routes[state.index]; + while (route.state?.index !== undefined) { + route = route.state.routes[route.state.index]; + } + return { name: route.name, params: route.params }; + }); + const baseStyle: StyleProp = { display: "flex", }; + const handlePress = () => { + // intercept login navigation and show modal instead + if (to.screen === "Login") { + console.log( + "AQLink intercepting login navigation, current route:", + currentRoute, + ); + openLoginModal(currentRoute as any); + return; + } + navigation.navigate(to.screen, to.params); + }; + if (isWeb) { + // on web, intercept login links with onClick handler + if (to.screen === "Login") { + return ( + + {children} + + ); + } return ( {children} @@ -38,10 +74,7 @@ export default function AQLink({ } return ( - navigation.navigate(to.screen, to.params)} - > + {children} ); diff --git a/js/app/components/live-dashboard/live-selector.tsx b/js/app/components/live-dashboard/live-selector.tsx index 67ae8b0d4..1972e4af7 100644 --- a/js/app/components/live-dashboard/live-selector.tsx +++ b/js/app/components/live-dashboard/live-selector.tsx @@ -1,10 +1,10 @@ -import { useNavigation } from "@react-navigation/native"; +import { useNavigation, useRoute } from "@react-navigation/native"; import { Button, Text, View, zero } from "@streamplace/components"; import { flex } from "@streamplace/components/src/ui"; -import { Redirect } from "components/aqlink"; import Loading from "components/loading/loading"; import { Camera, FerrisWheel } from "lucide-react-native"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +import { useStore } from "store"; import { useIsReady, useUserProfile } from "store/hooks"; import { StreamKeyScreen } from "./stream-key"; @@ -28,12 +28,26 @@ export default function StreamScreen({ route }) { const isReady = useIsReady(); const userProfile = useUserProfile(); const navigation = useNavigation(); + const openLoginModal = useStore((state) => state.openLoginModal); + const currentRoute = useRoute(); + + useEffect(() => { + if (isReady && !userProfile) { + openLoginModal({ name: currentRoute.name, params: currentRoute.params }); + } + }, [ + isReady, + userProfile, + openLoginModal, + currentRoute.name, + currentRoute.params, + ]); if (!isReady) { return ; } if (!userProfile) { - return ; + return ; } if (selectedMode === "webcam") { diff --git a/js/app/components/live-dashboard/stream-key.tsx b/js/app/components/live-dashboard/stream-key.tsx index 1fe623e52..990178590 100644 --- a/js/app/components/live-dashboard/stream-key.tsx +++ b/js/app/components/live-dashboard/stream-key.tsx @@ -1,3 +1,4 @@ +import { useRoute } from "@react-navigation/native"; import { Body, Button, @@ -8,7 +9,6 @@ import { useToast, View, } from "@streamplace/components"; -import { Redirect } from "components/aqlink"; import Loading from "components/loading/loading"; import { Clipboard, ClipboardCheck } from "lucide-react-native"; import { useEffect, useState } from "react"; @@ -43,18 +43,25 @@ const Content = ({ children }: { children: React.ReactNode }) => { export function StreamKeyScreen() { const [protocol, setProtocol] = useState<"whip" | "rtmp">("rtmp"); const isReady = useIsReady(); + const userProfile = useUserProfile(); + const openLoginModal = useStore((state) => state.openLoginModal); + const route = useRoute(); + const url = useStore((state) => state.url); + + useEffect(() => { + if (isReady && !userProfile) { + openLoginModal({ name: route.name, params: route.params }); + } + }, [isReady, userProfile, openLoginModal, route.name, route.params]); if (!isReady) { return ; } - const userProfile = useUserProfile(); if (!userProfile) { - return ; + return ; } - const url = useStore((state) => state.url); - return ( diff --git a/js/app/components/login/login-modal.tsx b/js/app/components/login/login-modal.tsx new file mode 100644 index 000000000..d911984ac --- /dev/null +++ b/js/app/components/login/login-modal.tsx @@ -0,0 +1,192 @@ +import { Button, Text, useTheme, zero } from "@streamplace/components"; +import { Info, X } from "lucide-react-native"; +import { useEffect, useState } from "react"; +import { + ActivityIndicator, + Alert, + Linking, + Modal, + Pressable, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { useStore } from "store"; +import { useLogin } from "store/hooks"; + +interface LoginModalProps { + visible: boolean; + onClose: () => void; +} + +export default function LoginModal({ visible, onClose }: LoginModalProps) { + const { theme } = useTheme(); + const loginAction = useStore((state) => state.login); + const openLoginLink = useStore((state) => state.openLoginLink); + const loginState = useLogin(); + const [handle, setHandle] = useState(""); + + const submit = () => { + let clean = handle; + if (handle.startsWith("@")) clean = handle.slice(1); + loginAction(clean, openLoginLink); + }; + + const onSignup = () => { + loginAction("https://bsky.social", openLoginLink); + }; + + const onEnterPress = (e: any) => { + if (e.nativeEvent.key === "Enter") { + submit(); + } + }; + + useEffect(() => { + if (loginState?.error) { + Alert.alert("Login error", loginState.error); + } + }, [loginState?.error]); + + return ( + + + e.stopPropagation()} + > + + + Log in + + + + + + + + + Sign in using your handle on the AT Protocol + + { + const u = new URL( + "https://atproto.academy/docs/Authentication/why", + ); + Linking.openURL(u.toString()); + }} + > + + + + (e.g. your Bluesky handle) + + + + + Handle + + setHandle( + text + .toLowerCase() + .replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "") + .trim(), + ) + } + style={[ + { + backgroundColor: "#1a1a1a", + borderWidth: 1, + borderColor: "#333", + borderRadius: 8, + padding: 12, + color: "white", + }, + ]} + onSubmitEditing={onEnterPress} + autoCapitalize="none" + autoCorrect={false} + keyboardType="url" + placeholderTextColor="#666" + /> + + + + + + + + + + ); +} diff --git a/js/app/components/login/login.tsx b/js/app/components/login/login.tsx index 2568a2b64..603928308 100644 --- a/js/app/components/login/login.tsx +++ b/js/app/components/login/login.tsx @@ -1,8 +1,8 @@ import { useNavigation } from "@react-navigation/native"; -import { Button, Text, useTheme, zero } from "@streamplace/components"; +import { Button, storage, Text, useTheme, zero } from "@streamplace/components"; +import { Redirect } from "components/aqlink"; import Loading from "components/loading/loading"; -import NameColorPicker from "components/name-color-picker/name-color-picker"; -import { Info, LogOut, UserRoundPen } from "lucide-react-native"; +import { Info } from "lucide-react-native"; import { useEffect, useState } from "react"; import { ActivityIndicator, @@ -16,24 +16,49 @@ import { View, } from "react-native"; import { useStore } from "store"; -import { - useChatProfile, - useIsReady, - useLogin, - useUserProfile, -} from "store/hooks"; +import { useIsReady, useLogin, useUserProfile } from "store/hooks"; +import { navigateToRoute } from "../../utils/navigation"; export default function Login() { const { theme } = useTheme(); const loginAction = useStore((state) => state.login); - const logout = useStore((state) => state.logout); const openLoginLink = useStore((state) => state.openLoginLink); - const chatProfile = useChatProfile(); + const closeLoginModal = useStore((state) => state.closeLoginModal); const userProfile = useUserProfile(); const loginState = useLogin(); + const navigation = useNavigation(); const [handle, setHandle] = useState(""); const isReady = useIsReady(); - const navigation = useNavigation(); + // null: no return route, undefined: hasn't checked yet + const [localReturnRoute, setLocalReturnRoute] = useState< + | { + name: string; + params?: any; + } + | null + | undefined + >(); + + // check for stored return route on mount + useEffect(() => { + storage.getItem("returnRoute").then((stored) => { + if (stored) { + try { + const route = JSON.parse(stored); + console.log("Login page - found stored returnRoute:", route); + setLocalReturnRoute(route); + storage.removeItem("returnRoute"); + closeLoginModal(); + navigateToRoute(navigation, route); + } catch (e) { + console.error("Failed to parse returnRoute from storage", e); + setLocalReturnRoute(null); + } + } else { + setLocalReturnRoute(null); + } + }); + }, [navigation, closeLoginModal]); const submit = () => { let clean = handle; @@ -55,7 +80,7 @@ export default function Login() { } }, [loginState?.error]); - if (!isReady) { + if (!isReady || localReturnRoute === undefined) { return ( ; + } return ( - - - Hey,{" "} - - @{userProfile.handle} - - . - - - - - - {/* link to bsky.app/settings */} - - - - + ); } @@ -251,13 +201,14 @@ export default function Login() { zero.gap.all[3], ]} > -