diff --git a/js/web/src/routeTree.gen.ts b/js/web/src/routeTree.gen.ts index fff41273f..447f5d5a7 100644 --- a/js/web/src/routeTree.gen.ts +++ b/js/web/src/routeTree.gen.ts @@ -21,6 +21,7 @@ import { Route as SettingsRecommendationsRouteImport } from './routes/settings/r import { Route as SettingsPrivacyRouteImport } from './routes/settings/privacy' import { Route as SettingsMultistreamRouteImport } from './routes/settings/multistream' import { Route as SettingsLanguagesRouteImport } from './routes/settings/languages' +import { Route as SettingsKeysRouteImport } from './routes/settings/keys' import { Route as SettingsBrandingRouteImport } from './routes/settings/branding' import { Route as SettingsBadgesRouteImport } from './routes/settings/badges' import { Route as SettingsBadgeIssuerRouteImport } from './routes/settings/badge-issuer' @@ -90,6 +91,11 @@ const SettingsLanguagesRoute = SettingsLanguagesRouteImport.update({ path: '/languages', getParentRoute: () => SettingsRoute, } as any) +const SettingsKeysRoute = SettingsKeysRouteImport.update({ + id: '/keys', + path: '/keys', + getParentRoute: () => SettingsRoute, +} as any) const SettingsBrandingRoute = SettingsBrandingRouteImport.update({ id: '/branding', path: '/branding', @@ -144,6 +150,7 @@ export interface FileRoutesByFullPath { '/settings/badge-issuer': typeof SettingsBadgeIssuerRoute '/settings/badges': typeof SettingsBadgesRoute '/settings/branding': typeof SettingsBrandingRoute + '/settings/keys': typeof SettingsKeysRoute '/settings/languages': typeof SettingsLanguagesRoute '/settings/multistream': typeof SettingsMultistreamRoute '/settings/privacy': typeof SettingsPrivacyRoute @@ -165,6 +172,7 @@ export interface FileRoutesByTo { '/settings/badge-issuer': typeof SettingsBadgeIssuerRoute '/settings/badges': typeof SettingsBadgesRoute '/settings/branding': typeof SettingsBrandingRoute + '/settings/keys': typeof SettingsKeysRoute '/settings/languages': typeof SettingsLanguagesRoute '/settings/multistream': typeof SettingsMultistreamRoute '/settings/privacy': typeof SettingsPrivacyRoute @@ -188,6 +196,7 @@ export interface FileRoutesById { '/settings/badge-issuer': typeof SettingsBadgeIssuerRoute '/settings/badges': typeof SettingsBadgesRoute '/settings/branding': typeof SettingsBrandingRoute + '/settings/keys': typeof SettingsKeysRoute '/settings/languages': typeof SettingsLanguagesRoute '/settings/multistream': typeof SettingsMultistreamRoute '/settings/privacy': typeof SettingsPrivacyRoute @@ -212,6 +221,7 @@ export interface FileRouteTypes { | '/settings/badge-issuer' | '/settings/badges' | '/settings/branding' + | '/settings/keys' | '/settings/languages' | '/settings/multistream' | '/settings/privacy' @@ -233,6 +243,7 @@ export interface FileRouteTypes { | '/settings/badge-issuer' | '/settings/badges' | '/settings/branding' + | '/settings/keys' | '/settings/languages' | '/settings/multistream' | '/settings/privacy' @@ -255,6 +266,7 @@ export interface FileRouteTypes { | '/settings/badge-issuer' | '/settings/badges' | '/settings/branding' + | '/settings/keys' | '/settings/languages' | '/settings/multistream' | '/settings/privacy' @@ -359,6 +371,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsLanguagesRouteImport parentRoute: typeof SettingsRoute } + '/settings/keys': { + id: '/settings/keys' + path: '/keys' + fullPath: '/settings/keys' + preLoaderRoute: typeof SettingsKeysRouteImport + parentRoute: typeof SettingsRoute + } '/settings/branding': { id: '/settings/branding' path: '/branding' @@ -436,6 +455,7 @@ interface SettingsRouteChildren { SettingsBadgeIssuerRoute: typeof SettingsBadgeIssuerRoute SettingsBadgesRoute: typeof SettingsBadgesRoute SettingsBrandingRoute: typeof SettingsBrandingRoute + SettingsKeysRoute: typeof SettingsKeysRoute SettingsLanguagesRoute: typeof SettingsLanguagesRoute SettingsMultistreamRoute: typeof SettingsMultistreamRoute SettingsPrivacyRoute: typeof SettingsPrivacyRoute @@ -453,6 +473,7 @@ const SettingsRouteChildren: SettingsRouteChildren = { SettingsBadgeIssuerRoute: SettingsBadgeIssuerRoute, SettingsBadgesRoute: SettingsBadgesRoute, SettingsBrandingRoute: SettingsBrandingRoute, + SettingsKeysRoute: SettingsKeysRoute, SettingsLanguagesRoute: SettingsLanguagesRoute, SettingsMultistreamRoute: SettingsMultistreamRoute, SettingsPrivacyRoute: SettingsPrivacyRoute, diff --git a/js/web/src/routes/settings/keys.tsx b/js/web/src/routes/settings/keys.tsx new file mode 100644 index 000000000..48a8627d8 --- /dev/null +++ b/js/web/src/routes/settings/keys.tsx @@ -0,0 +1,152 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useStore } from "../../lib/store"; +import { useKeyRecords } from "../../lib/store/hooks"; + +export const Route = createFileRoute("/settings/keys")({ + component: KeyManager, +}); + +function timeAgo(date: Date): string { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function KeyManager() { + const { t } = useTranslation("settings"); + const navigate = useNavigate(); + + const deleteStreamKeyRecord = useStore((s) => s.deleteStreamKeyRecord); + const getStreamKeyRecords = useStore((s) => s.getStreamKeyRecords); + const keyObj = useKeyRecords(); + const keyRecords = keyObj?.records ?? null; + + const [deletingKeys, setDeletingKeys] = useState>(new Set()); + + const handleDelete = async (rkey: string) => { + if (deletingKeys.has(rkey)) return; + setDeletingKeys((prev) => new Set(prev).add(rkey)); + try { + await deleteStreamKeyRecord(rkey); + } catch (error: any) { + toast.error(error.message || "Failed to delete key"); + } finally { + setDeletingKeys((prev) => { + const next = new Set(prev); + next.delete(rkey); + return next; + }); + } + }; + + useEffect(() => { + const timeout = setTimeout(() => { + getStreamKeyRecords(); + }, 500); + return () => clearTimeout(timeout); + }, []); + + return ( +
+ + +

{t("key-manager")}

+ + {keyRecords === null || keyObj === null ? ( +
Loading…
+ ) : keyRecords.records.length === 0 ? ( +
+

{t("no-keys")}

+ +
+ ) : ( +
+
+

{t("your-stream-pubkeys")}

+

+ {t("pubkey-description")} +

+
+ +
+ {keyRecords.records.map((keyRecord) => { + const rkey = keyRecord.uri.split("/").pop() as string; + const value = keyRecord.value as { + signingKey?: string; + createdAt?: string; + createdBy?: string; + }; + const isDeleting = deletingKeys.has(rkey); + + return ( +
+
+ {value.signingKey && ( +
+ {value.signingKey} +
+ )} + {value.createdAt && ( +
+ made {timeAgo(new Date(value.createdAt))} + {value.createdBy && ` by ${value.createdBy}`} +
+ )} +
+ +
+ ); + })} +
+ +

+ {t("keys-count", { count: keyRecords.records.length })} +

+
+ )} +
+ ); +} diff --git a/js/web/src/routes/settings/streaming.tsx b/js/web/src/routes/settings/streaming.tsx index c774522da..420805383 100644 --- a/js/web/src/routes/settings/streaming.tsx +++ b/js/web/src/routes/settings/streaming.tsx @@ -92,7 +92,11 @@ function StreamingSettings() {

{t("streaming")}

- +