From 473753965523a8ec75b64bde6dc5c8651df2ef88 Mon Sep 17 00:00:00 2001 From: Natalie B <22222885+espeon@users.noreply.github.com> Date: Mon, 19 May 2025 23:15:11 -0500 Subject: [PATCH] simple key manager --- js/app/components/settings/keymgr.tsx | 121 +++++++++++++++++++++++ js/app/features/bluesky/blueskySlice.tsx | 100 +++++++++++++++++++ js/app/features/bluesky/blueskyTypes.tsx | 3 + js/app/src/router.tsx | 11 +++ js/app/utils/timeAgo.ts | 36 +++++++ 5 files changed, 271 insertions(+) create mode 100644 js/app/components/settings/keymgr.tsx create mode 100644 js/app/utils/timeAgo.ts diff --git a/js/app/components/settings/keymgr.tsx b/js/app/components/settings/keymgr.tsx new file mode 100644 index 00000000..c3d4b984 --- /dev/null +++ b/js/app/components/settings/keymgr.tsx @@ -0,0 +1,121 @@ +import { X } from "@tamagui/lucide-icons"; +import AQLink from "components/aqlink"; +import Loading from "components/loading/loading"; +import { + deleteStreamKeyRecord, + getStreamKeyRecords, + selectKeyRecords, +} from "features/bluesky/blueskySlice"; +import { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "store/hooks"; +import { PlaceStreamKey } from "streamplace"; +import { Button, ScrollView, Separator, Text, XStack, YStack } from "tamagui"; +import { timeAgo } from "utils/timeAgo"; + +function KeyRow({ + keyRecord, + rkey, + deleteKeyRecord, +}: { + keyRecord: PlaceStreamKey.Record; + rkey: string; + deleteKeyRecord: (rkey: string) => void; +}) { + return ( + + + {keyRecord?.signingKey && ( + + {keyRecord?.signingKey} + + )} + {keyRecord?.createdAt && ( + + made {timeAgo(new Date(keyRecord.createdAt))} + + )} + + + + ); +} + +export default function KeyManager() { + const dispatch = useAppDispatch(); + const keyRecords = useAppSelector(selectKeyRecords); + + const deleteKeyRecord = (rkey: string) => { + dispatch(deleteStreamKeyRecord({ rkey })); + dispatch(getStreamKeyRecords()); + }; + + useEffect(() => { + dispatch(getStreamKeyRecords()); + }, []); + + return ( + + + {keyRecords === null ? ( + + ) : keyRecords.records.length === 0 ? ( + <> + No keys here! + + + Go to the live dashboard to create a key. + + + + ) : ( + <> + + Existing Pubkeys + + Your private stream key is the secret credential you use to + stream. Listed are the associated public keys. + + {keyRecords.records.map((keyRecord) => ( + + ))} + + {keyRecords.records.length} key + {keyRecords.records.length > 1 && "s"} + + + + + + Go to the live dashboard to create a key. + + + )} + + + ); +} diff --git a/js/app/features/bluesky/blueskySlice.tsx b/js/app/features/bluesky/blueskySlice.tsx index 88124fc8..ed6e7992 100644 --- a/js/app/features/bluesky/blueskySlice.tsx +++ b/js/app/features/bluesky/blueskySlice.tsx @@ -55,6 +55,8 @@ const initialState: BlueskyState = { }, newKey: null, storedKey: null, + isDeletingKey: false, + streamKeysResponse: null, newLivestream: null, }; @@ -649,6 +651,100 @@ export const blueskySlice = createAppSlice({ }; }), + getStreamKeyRecords: create.asyncThunk( + async (_, thunkAPI) => { + const { bluesky } = thunkAPI.getState() as { + bluesky: BlueskyState; + }; + if (!bluesky.pdsAgent) { + throw new Error("No agent"); + } + const did = bluesky.oauthSession?.did; + if (!did) { + throw new Error("No DID"); + } + const profile = bluesky.profiles[did]; + if (!profile) { + throw new Error("No profile"); + } + if (!did) { + throw new Error("No DID"); + } + return await bluesky.pdsAgent.com.atproto.repo.listRecords({ + repo: did, + collection: "place.stream.key", + limit: 100, + }); + }, + { + pending: (state) => { + return { + ...state, + streamKeysResponse: null, + }; + }, + fulfilled: (state, action) => { + console.log(action.payload); + return { + ...state, + streamKeysResponse: action.payload.data, + }; + }, + rejected: (state, action) => { + console.error("listStreamKeyRecords rejected", action.error); + }, + }, + ), + + deleteStreamKeyRecord: create.asyncThunk( + async ({ rkey }: { rkey: string }, thunkAPI) => { + const { bluesky } = thunkAPI.getState() as { + bluesky: BlueskyState; + }; + if (!bluesky.pdsAgent) { + throw new Error("No agent"); + } + const did = bluesky.oauthSession?.did; + if (!did) { + throw new Error("No DID"); + } + const profile = bluesky.profiles[did]; + if (!profile) { + throw new Error("No profile"); + } + if (!did) { + throw new Error("No DID"); + } + + return await bluesky.pdsAgent.com.atproto.repo.deleteRecord({ + repo: did, + collection: "place.stream.key", + rkey, + }); + }, + { + pending: (state) => { + return { + ...state, + isDeletingKey: true, + }; + }, + fulfilled: (state, action) => { + return { + ...state, + isDeletingKey: false, + }; + }, + rejected: (state, action) => { + console.error("deleteStreamKeyRecord rejected", action.error); + return { + ...state, + isDeletingKey: false, + }; + }, + }, + ), + setPDS: create.asyncThunk( async (pds: string, thunkAPI) => { await Storage.setItem("pdsURL", pds); @@ -1139,6 +1235,7 @@ export const blueskySlice = createAppSlice({ selectLogin: (bluesky) => bluesky.login, selectProfiles: (bluesky) => bluesky.profiles, selectStoredKey: (bluesky) => bluesky.storedKey, + selectKeyRecords: (bluesky) => bluesky.streamKeysResponse, selectUserProfile: (bluesky) => { const did = bluesky.oauthSession?.did; if (!did) return null; @@ -1179,6 +1276,8 @@ export const { oauthError, createStreamKeyRecord, clearStreamKeyRecord, + getStreamKeyRecords, + deleteStreamKeyRecord, createLivestreamRecord, updateLivestreamRecord, createChatProfileRecord, @@ -1196,6 +1295,7 @@ export const { selectPDS, selectLogin, selectStoredKey, + selectKeyRecords, selectIsReady, selectNewLivestream, selectChatProfile, diff --git a/js/app/features/bluesky/blueskyTypes.tsx b/js/app/features/bluesky/blueskyTypes.tsx index fdec6ba3..7ee1d6f4 100644 --- a/js/app/features/bluesky/blueskyTypes.tsx +++ b/js/app/features/bluesky/blueskyTypes.tsx @@ -1,4 +1,5 @@ import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; +import { OutputSchema } from "@atproto/api/dist/client/types/com/atproto/repo/listRecords"; import { OAuthSession } from "@streamplace/atproto-oauth-client-react-native"; import { StreamKey } from "features/base/baseSlice"; import { @@ -35,6 +36,8 @@ export interface BlueskyState { }; newKey: null | StreamKey; storedKey: null | StreamKey; + isDeletingKey: boolean; + streamKeysResponse: null | OutputSchema; newLivestream: null | NewLivestream; chatProfile: { loading: boolean; diff --git a/js/app/src/router.tsx b/js/app/src/router.tsx index 4b663fc4..0d8397a8 100644 --- a/js/app/src/router.tsx +++ b/js/app/src/router.tsx @@ -76,6 +76,7 @@ import SupportScreen from "./screens/support"; // probabl should move this import SignUp from "components/login/signup"; +import KeyManager from "components/settings/keymgr"; import { loadStateFromStorage } from "features/base/sidebarSlice"; import { store } from "store/store"; import HomeScreen from "./screens/home"; @@ -105,6 +106,7 @@ type RootStackParamList = { Multi: { config: string }; Support: undefined; Settings: undefined; + KeyManagement: undefined; GoLive: undefined; LiveDashboard: undefined; Login: undefined; @@ -138,6 +140,7 @@ const linking: LinkingOptions = { Multi: "multi/:config", Support: "support", Settings: "settings", + KeyManagement: "settings/key-management", GoLive: "golive", LiveDashboard: "live", Login: "login", @@ -438,6 +441,14 @@ export function StreamplaceDrawer() { }} /> + Key Manager, + drawerItemStyle: { display: "none" }, + }} + /> 1) { + const formatter = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); + return "on " + formatter.format(date); + } + interval = Math.floor(seconds / 86400); + // return date without years + if (interval > 1) { + const formatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); + return formatter.format(date); + } + interval = Math.floor(seconds / 3600); + if (interval > 1) { + return interval + " hours ago"; + } + interval = Math.floor(seconds / 60); + if (interval > 1) { + return interval + " minutes ago"; + } + return Math.floor(seconds) + " seconds ago"; +} -- 2.51.2