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