From 8edf021e22f2547d6e2686e72ae1bd1f31d54575 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Tue, 21 Oct 2025 19:35:50 -0500 Subject: [PATCH] Migrate Redux store to Zustand Replace Redux Toolkit slices and react-redux hooks with a Zustand-based store. Add new store/index and slices, and store/hooks selector wrappers. Remove legacy Redux slices, listener and store wiring, update components to use useStore and new selector hooks, and adjust auth/Bluesky/Streamplace APIs accordingly. Update package deps and lockfile to add zustand and remove Redux-related packages. --- js/app/components/create-livestream.tsx | 24 +- js/app/components/edit-livestream.tsx | 24 +- js/app/components/follow-button.tsx | 19 +- .../live-dashboard/live-selector.tsx | 10 +- .../live-dashboard/livestream-panel.tsx | 5 +- .../components/live-dashboard/stream-key.tsx | 28 +- js/app/components/login/login.tsx | 35 +- js/app/components/mobile/player.tsx | 24 +- .../name-color-picker/name-color-picker.tsx | 25 +- .../components/provider/provider.shared.tsx | 23 +- js/app/components/settings/key-manager.tsx | 21 +- js/app/components/settings/settings.tsx | 49 +- js/app/contexts/FullscreenContext.tsx | 14 +- js/app/features/base/baseSlice.tsx | 57 - js/app/features/base/sidebarSlice.tsx | 124 -- js/app/features/bluesky/blueskyProvider.tsx | 40 +- js/app/features/bluesky/blueskySlice.tsx | 1515 ----------------- js/app/features/bluesky/blueskyTypes.tsx | 53 - .../features/bluesky/contentMetadataSlice.tsx | 385 ----- .../platform/platformSlice.native.tsx | 202 --- js/app/features/platform/platformSlice.tsx | 66 - .../streamplace/streamplaceProvider.tsx | 21 +- .../features/streamplace/streamplaceSlice.tsx | 249 --- js/app/hooks/createSlice.tsx | 6 - js/app/hooks/useAvatars.tsx | 16 +- js/app/hooks/useLiveUser.tsx | 5 +- js/app/hooks/useSidebarControl.tsx | 29 +- js/app/package.json | 5 +- js/app/src/router.tsx | 57 +- js/app/src/screens/chat-popout.native.tsx | 5 +- js/app/src/screens/chat-popout.tsx | 5 +- js/app/src/screens/embed.tsx | 13 +- js/app/src/screens/live-dashboard.tsx | 10 +- js/app/src/screens/mobile-go-live.tsx | 5 +- js/app/store/hooks.ts | 79 + js/app/store/hooks.tsx | 12 - js/app/store/index.ts | 39 + js/app/store/listener.ts | 24 - js/app/store/slices/baseSlice.ts | 32 + js/app/store/slices/blueskySlice.ts | 1141 +++++++++++++ js/app/store/slices/contentMetadataSlice.ts | 298 ++++ js/app/store/slices/platformSlice.ts | 40 + js/app/store/slices/sidebarSlice.ts | 117 ++ js/app/store/slices/streamplaceSlice.ts | 117 ++ js/app/store/store.tsx | 60 - pnpm-lock.yaml | 86 +- 46 files changed, 2095 insertions(+), 3119 deletions(-) delete mode 100644 js/app/features/base/baseSlice.tsx delete mode 100644 js/app/features/base/sidebarSlice.tsx delete mode 100644 js/app/features/bluesky/blueskySlice.tsx delete mode 100644 js/app/features/bluesky/blueskyTypes.tsx delete mode 100644 js/app/features/bluesky/contentMetadataSlice.tsx delete mode 100644 js/app/features/platform/platformSlice.native.tsx delete mode 100644 js/app/features/platform/platformSlice.tsx delete mode 100644 js/app/features/streamplace/streamplaceSlice.tsx delete mode 100644 js/app/hooks/createSlice.tsx create mode 100644 js/app/store/hooks.ts delete mode 100644 js/app/store/hooks.tsx create mode 100644 js/app/store/index.ts delete mode 100644 js/app/store/listener.ts create mode 100644 js/app/store/slices/baseSlice.ts create mode 100644 js/app/store/slices/blueskySlice.ts create mode 100644 js/app/store/slices/contentMetadataSlice.ts create mode 100644 js/app/store/slices/platformSlice.ts create mode 100644 js/app/store/slices/sidebarSlice.ts create mode 100644 js/app/store/slices/streamplaceSlice.ts delete mode 100644 js/app/store/store.tsx diff --git a/js/app/components/create-livestream.tsx b/js/app/components/create-livestream.tsx index 9849b6902..6b65a061e 100644 --- a/js/app/components/create-livestream.tsx +++ b/js/app/components/create-livestream.tsx @@ -1,10 +1,5 @@ import { zero } from "@streamplace/components"; import ThumbnailSelector from "components/thumbnail-selector"; -import { - createLivestreamRecord, - selectNewLivestream, - selectUserProfile, -} from "features/bluesky/blueskySlice"; import { useCaptureVideoFrame } from "hooks/useCaptureVideoFrame"; import { useLiveUser } from "hooks/useLiveUser"; import { useEffect, useState } from "react"; @@ -17,12 +12,16 @@ import { useWindowDimensions, View, } from "react-native"; -import { useAppDispatch, useAppSelector } from "store/hooks"; +import { useStore } from "store"; +import { useNewLivestream, useUserProfile } from "store/hooks"; const isWeb = Platform.OS === "web"; export default function CreateLivestream() { - const dispatch = useAppDispatch(); + const createLivestreamRecord = useStore( + (state) => state.createLivestreamRecord, + ); + const streamplaceUrl = useStore((state) => state.url); // Note: Toast functionality removed, would need simple alert replacement const userIsLive = useLiveUser(); const [title, setTitle] = useState(""); @@ -30,8 +29,8 @@ export default function CreateLivestream() { const [customThumbnail, setCustomThumbnail] = useState( undefined, ); - const profile = useAppSelector(selectUserProfile); - const newLivestream = useAppSelector(selectNewLivestream); + const profile = useUserProfile(); + const newLivestream = useNewLivestream(); const captureFrame = useCaptureVideoFrame(); const { width } = useWindowDimensions(); @@ -66,12 +65,7 @@ export default function CreateLivestream() { } } - await dispatch( - createLivestreamRecord({ - title, - customThumbnail: thumbnailToUse, - }), - ); + await createLivestreamRecord(title, thumbnailToUse, streamplaceUrl); } catch (error) { console.error("Error creating livestream:", error); // Would show toast: "Error creating livestream" diff --git a/js/app/components/edit-livestream.tsx b/js/app/components/edit-livestream.tsx index a68170fa7..f55c1fc64 100644 --- a/js/app/components/edit-livestream.tsx +++ b/js/app/components/edit-livestream.tsx @@ -1,23 +1,22 @@ import { Text, useLivestream, zero } from "@streamplace/components"; -import { - selectNewLivestream, - selectUserProfile, - updateLivestreamRecord, -} from "features/bluesky/blueskySlice"; import { useLiveUser } from "hooks/useLiveUser"; import { useEffect, useState } from "react"; import { Pressable, ScrollView, TextInput, View } from "react-native"; -import { useAppDispatch, useAppSelector } from "store/hooks"; +import { useStore } from "store"; +import { useNewLivestream, useUserProfile } from "store/hooks"; export default function UpdateLivestream() { - const dispatch = useAppDispatch(); + const updateLivestreamRecord = useStore( + (state) => state.updateLivestreamRecord, + ); + const streamplaceUrl = useStore((state) => state.url); // Note: Toast functionality removed, would need simple alert replacement const userIsLive = useLiveUser(); const [title, setTitle] = useState(""); const [loading, setLoading] = useState(false); - const profile = useAppSelector(selectUserProfile); + const profile = useUserProfile(); const livestream = useLivestream(); - const newLivestream = useAppSelector(selectNewLivestream); + const newLivestream = useNewLivestream(); useEffect(() => { if (newLivestream?.record) { @@ -37,12 +36,7 @@ export default function UpdateLivestream() { const handleSubmit = async () => { setLoading(true); try { - await dispatch( - updateLivestreamRecord({ - title, - livestream, - }), - ); + await updateLivestreamRecord(title, livestream, streamplaceUrl); } catch (error) { console.error("Error updating livestream:", error); // Would show toast: "Error updating livestream" diff --git a/js/app/components/follow-button.tsx b/js/app/components/follow-button.tsx index 9040a7cc7..97a443cfb 100644 --- a/js/app/components/follow-button.tsx +++ b/js/app/components/follow-button.tsx @@ -2,9 +2,8 @@ import { Button, Icon, Text, zero } from "@streamplace/components"; import { Plus } from "lucide-react-native"; import React, { useEffect, useState } from "react"; import { View } from "react-native"; -import { followUser, unfollowUser } from "../features/bluesky/blueskySlice"; -import { selectStreamplace } from "../features/streamplace/streamplaceSlice"; -import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { useStore } from "store"; +import { useStreamplaceUrl } from "store/hooks"; /** * FollowButton component for following/unfollowing a streamer. @@ -28,8 +27,9 @@ const FollowButton: React.FC = ({ const [isFollowing, setIsFollowing] = useState(null); const [error, setError] = useState(null); const [followUri, setFollowUri] = useState(null); - const { url: streamplaceUrl } = useAppSelector(selectStreamplace); - const dispatch = useAppDispatch(); + const streamplaceUrl = useStreamplaceUrl(); + const followUser = useStore((state) => state.followUser); + const unfollowUser = useStore((state) => state.unfollowUser); // Hide button if not logged in or viewing own stream if (!currentUserDID || currentUserDID === streamerDID) return null; @@ -80,7 +80,7 @@ const FollowButton: React.FC = ({ setError(null); setIsFollowing(true); // Optimistic try { - await dispatch(followUser(streamerDID)).unwrap(); + await followUser(streamerDID); setIsFollowing(true); onFollowChange?.(true); } catch (err) { @@ -95,12 +95,7 @@ const FollowButton: React.FC = ({ setError(null); setIsFollowing(false); // Optimistic try { - await dispatch( - unfollowUser({ - subjectDID: streamerDID, - ...(followUri ? { followUri } : {}), - }), - ).unwrap(); + await unfollowUser(streamerDID, followUri ?? undefined, streamplaceUrl); setIsFollowing(false); setFollowUri(null); onFollowChange?.(false); diff --git a/js/app/components/live-dashboard/live-selector.tsx b/js/app/components/live-dashboard/live-selector.tsx index c10aa1cb2..67ae8b0d4 100644 --- a/js/app/components/live-dashboard/live-selector.tsx +++ b/js/app/components/live-dashboard/live-selector.tsx @@ -3,13 +3,9 @@ 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 { - selectIsReady, - selectUserProfile, -} from "features/bluesky/blueskySlice"; import { Camera, FerrisWheel } from "lucide-react-native"; import React, { useState } from "react"; -import { useAppSelector } from "store/hooks"; +import { useIsReady, useUserProfile } from "store/hooks"; import { StreamKeyScreen } from "./stream-key"; const { layout, gap } = zero; @@ -29,8 +25,8 @@ const elems = [ export default function StreamScreen({ route }) { const [selectedMode, setSelectedMode] = useState(null); - const isReady = useAppSelector(selectIsReady); - const userProfile = useAppSelector(selectUserProfile); + const isReady = useIsReady(); + const userProfile = useUserProfile(); const navigation = useNavigation(); if (!isReady) { diff --git a/js/app/components/live-dashboard/livestream-panel.tsx b/js/app/components/live-dashboard/livestream-panel.tsx index b241d8dae..50c8bf2e5 100644 --- a/js/app/components/live-dashboard/livestream-panel.tsx +++ b/js/app/components/live-dashboard/livestream-panel.tsx @@ -22,10 +22,9 @@ import { TouchableOpacity, View, } from "react-native"; -import { selectUserProfile } from "../../features/bluesky/blueskySlice"; +import { useUserProfile } from "store/hooks"; import { useCaptureVideoFrame } from "../../hooks/useCaptureVideoFrame"; import { useLiveUser } from "../../hooks/useLiveUser"; -import { useAppSelector } from "../../store/hooks"; const { flex, p, px, py, gap, layout, bg, borders, text, r, w, typography } = zero; @@ -165,7 +164,7 @@ function LivestreamPanel() { const toast = useToast(); const userIsLive = useLiveUser(); const captureFrame = useCaptureVideoFrame(); - const profile = useAppSelector(selectUserProfile); + const profile = useUserProfile(); const livestream = useLivestream(); const createStreamRecord = useCreateStreamRecord(); const updateStreamRecord = useUpdateStreamRecord(); diff --git a/js/app/components/live-dashboard/stream-key.tsx b/js/app/components/live-dashboard/stream-key.tsx index cbc8a60c4..94bc8ea24 100644 --- a/js/app/components/live-dashboard/stream-key.tsx +++ b/js/app/components/live-dashboard/stream-key.tsx @@ -9,16 +9,11 @@ import { } from "@streamplace/components"; import { Redirect } from "components/aqlink"; import Loading from "components/loading/loading"; -import { - clearStreamKeyRecord, - createStreamKeyRecord, - selectIsReady, - selectUserProfile, -} from "features/bluesky/blueskySlice"; import { Clipboard, ClipboardCheck } from "lucide-react-native"; import { useEffect, useState } from "react"; import { ScrollView, TextInput } from "react-native"; -import { useAppDispatch, useAppSelector } from "store/hooks"; +import { useStore } from "store"; +import { useIsReady, useUserProfile } from "store/hooks"; const FormRow = ({ children }: { children: React.ReactNode }) => { return ( @@ -46,18 +41,18 @@ const Content = ({ children }: { children: React.ReactNode }) => { export function StreamKeyScreen() { const [protocol, setProtocol] = useState<"whip" | "rtmp">("rtmp"); - const isReady = useAppSelector(selectIsReady); + const isReady = useIsReady(); if (!isReady) { return ; } - const userProfile = useAppSelector(selectUserProfile); + const userProfile = useUserProfile(); if (!userProfile) { return ; } - const url = useAppSelector((state) => state.streamplace.url); + const url = useStore((state) => state.url); return ( @@ -190,11 +185,14 @@ export function StreamKey() { const theme = useTheme(); const toast = useToast(); - const dispatch = useAppDispatch(); + const createStreamKeyRecord = useStore( + (state) => state.createStreamKeyRecord, + ); + const clearStreamKeyRecord = useStore((state) => state.clearStreamKeyRecord); const [generating, setGenerating] = useState(false); const [hidekey, setHidekey] = useState(true); const [didcopy, setDidcopy] = useState(false); - const newKey = useAppSelector((state) => state.bluesky.newKey); + const newKey = useStore((state) => state.newKey); let foregroundColor = theme.theme.colors.text || "#fff"; @@ -204,9 +202,9 @@ export function StreamKey() { } return () => { - dispatch(clearStreamKeyRecord()); + clearStreamKeyRecord(); }; - }, [newKey, dispatch]); + }, [newKey]); const handleCopy = async () => { if (!newKey) { @@ -287,7 +285,7 @@ export function StreamKey() { try { setGenerating(true); setDidcopy(false); - await dispatch(createStreamKeyRecord({ store: false })); + await createStreamKeyRecord(false); } catch (e) { console.error("failed to generate stream key", e); } finally { diff --git a/js/app/components/login/login.tsx b/js/app/components/login/login.tsx index d90ae7acc..5cb247eaa 100644 --- a/js/app/components/login/login.tsx +++ b/js/app/components/login/login.tsx @@ -2,14 +2,6 @@ import { useNavigation } from "@react-navigation/native"; import { Button, Text, useTheme, zero } from "@streamplace/components"; import Loading from "components/loading/loading"; import NameColorPicker from "components/name-color-picker/name-color-picker"; -import { - login, - logout, - selectChatProfile, - selectIsReady, - selectLogin, - selectUserProfile, -} from "features/bluesky/blueskySlice"; import { Info, LogOut, UserRoundPen } from "lucide-react-native"; import { useEffect, useState } from "react"; import { @@ -23,25 +15,34 @@ import { TextInput, View, } from "react-native"; -import { useAppDispatch, useAppSelector } from "store/hooks"; +import { useStore } from "store"; +import { + useChatProfile, + useIsReady, + useLogin, + useUserProfile, +} from "store/hooks"; export default function Login() { const { theme } = useTheme(); - const dispatch = useAppDispatch(); - const chatProfile = useAppSelector(selectChatProfile); - const userProfile = useAppSelector(selectUserProfile); - const loginState = useAppSelector(selectLogin); + const loginAction = useStore((state) => state.login); + const logout = useStore((state) => state.logout); + const openLoginLink = useStore((state) => state.openLoginLink); + const streamplaceUrl = useStore((state) => state.url); + const chatProfile = useChatProfile(); + const userProfile = useUserProfile(); + const loginState = useLogin(); const [handle, setHandle] = useState(""); - const isReady = useAppSelector(selectIsReady); + const isReady = useIsReady(); const navigation = useNavigation(); const submit = () => { let clean = handle; if (handle.startsWith("@")) clean = handle.slice(1); - dispatch(login(clean)); + loginAction(clean, streamplaceUrl, openLoginLink); }; const onSignup = () => { - dispatch(login("https://bsky.social")); + loginAction("https://bsky.social", streamplaceUrl, openLoginLink); }; const onEnterPress = (e: any) => { if (e.nativeEvent.key === "Enter") { @@ -98,7 +99,7 @@ export default function Login() { ]} >