diff --git a/desktop/src/features/identity-archive/hooks.ts b/desktop/src/features/identity-archive/hooks.ts index 5f93c9f522..a00153ad51 100644 --- a/desktop/src/features/identity-archive/hooks.ts +++ b/desktop/src/features/identity-archive/hooks.ts @@ -101,7 +101,7 @@ export function useUnarchiveIdentityMutation() { export type IdentityArchiveActions = { /** Render guard only — the relay re-verifies authority on submit. */ canArchive: boolean; - /** `undefined` while the snapshot loads — defer flair + Manage until known. */ + /** `undefined` while the snapshot loads — defer flair + mutation affordances until known. */ isArchived: boolean | undefined; isPending: boolean; archive: () => void; diff --git a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx index 510814dd15..b3b87d42ce 100644 --- a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx +++ b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx @@ -63,9 +63,8 @@ export function UserProfileAgentSettingsMenu({ onToggleAutoStart !== undefined; const autoStartSwitchId = `user-profile-agent-auto-start-${actionKey}`; const hasPrimaryActions = Boolean(onDuplicatePersona || onExportPersona); - const hasArchiveAction = - archiveActions?.canArchive === true && - archiveActions.isArchived !== undefined; + const hasArchiveAction = archiveActions?.canArchive === true; + const archiveStateKnown = archiveActions?.isArchived !== undefined; const shouldConfirmAgentDelete = managedAgent !== undefined && onDelete !== undefined; const hasManageActions = hasArchiveAction || Boolean(onDelete); @@ -146,7 +145,7 @@ export function UserProfileAgentSettingsMenu({ ) : null} {hasArchiveAction && archiveActions ? ( - archiveActions.isArchived ? ( + archiveActions.isArchived === true ? ( setArchiveConfirmOpen(true)} + disabled={isPending || !archiveStateKnown} + onSelect={(event) => { + if (!archiveStateKnown) { + event.preventDefault(); + return; + } + setArchiveConfirmOpen(true); + }} > - {archiveActions.isPending ? "Archiving…" : archiveLabel} + {archiveActions.isPending + ? "Archiving…" + : archiveStateKnown + ? archiveLabel + : "Checking archive status…"} ) ) : null} @@ -186,11 +195,12 @@ export function UserProfileAgentSettingsMenu({ ) : null} - {hasArchiveAction && archiveActions ? ( + {hasArchiveAction && archiveActions && archiveStateKnown ? ( { + if (archiveActions.isArchived !== false) return; archiveActions.archive(); setArchiveConfirmOpen(false); }} @@ -245,8 +255,7 @@ export function UserProfileAgentSettingsMenuSlot({ personaActionKey?: string; viewerIsOwner: boolean; }) { - const canShowArchiveAction = - archiveActions.canArchive && archiveActions.isArchived !== undefined; + const canShowArchiveAction = archiveActions.canArchive; const settingsActionPending = isAgentActionPending || archiveActions.isPending; const sharedProps = { diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index eaaf6eaa46..0b6092921c 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -106,12 +106,13 @@ type E2eConfig = { stallWebsocketSends?: boolean; userSearchDelayMs?: number; // NIP-IA gate inputs — see tests/helpers/bridge.ts:MockBridgeOptions for - // semantics. These three drive the archive-button gate matrix in + // semantics. These options drive the archive-button gate matrix in // tests/e2e/identity-archive.spec.ts; they're plumbed into: - // - `list_archived_identities` (archivedIdentities) + // - `list_archived_identities` (archivedIdentities / archivedIdentitiesDelayMs) // - `resolve_oa_owner` (oaOwnerIsMe) // - `resetMockRelayMembers` (relayRole) archivedIdentities?: string[]; + archivedIdentitiesDelayMs?: number; oaOwnerIsMe?: boolean; relayRole?: "owner" | "admin" | "member" | null; // Descriptors returned by the mocked `pick_and_upload_media` / @@ -7649,6 +7650,10 @@ export function maybeInstallE2eTauriMocks() { return { owner, is_me: isMe }; } case "list_archived_identities": { + const delayMs = activeConfig?.mock?.archivedIdentitiesDelayMs ?? 0; + if (delayMs > 0) { + await new Promise((resolve) => window.setTimeout(resolve, delayMs)); + } const archived = activeConfig?.mock?.archivedIdentities ?? []; return { archived }; } diff --git a/desktop/tests/e2e/identity-archive.spec.ts b/desktop/tests/e2e/identity-archive.spec.ts index d6c5d0e0ab..d4992c9369 100644 --- a/desktop/tests/e2e/identity-archive.spec.ts +++ b/desktop/tests/e2e/identity-archive.spec.ts @@ -97,6 +97,26 @@ test.describe("NIP-IA archive button gate", () => { ).toBeVisible(); }); + test("case 3b — verified OA owner viewing Alice while snapshot is pending: Archive visible but disabled", async ({ + page, + }) => { + await installMockBridge(page, { + relayRole: null, + oaOwnerIsMe: true, + archivedIdentities: [], + archivedIdentitiesDelayMs: 60_000, + }); + await openAliceProfile(page); + await openProfileSettingsMenu(page); + + const archiveButton = page.getByTestId("user-profile-archive-identity"); + await expect(archiveButton).toBeVisible(); + await expect(archiveButton).toBeDisabled(); + await expect(archiveButton).toContainText("Checking archive status…"); + await archiveButton.click({ force: true }); + await expect(page.getByTestId("archive-confirm-dialog")).toHaveCount(0); + }); + test("case 4 — no authority viewing Alice: Archive hidden", async ({ page, }) => { diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 2d1cc2561f..82114a38e7 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -134,6 +134,8 @@ type MockBridgeOptions = { * "Archived on this relay" flair + Unarchive button. */ archivedIdentities?: string[]; + /** Delay (ms) for `list_archived_identities`; lets specs assert pending snapshot UI. */ + archivedIdentitiesDelayMs?: number; /** * Drives the `is_me` field of `resolve_oa_owner`. When true, the harness * reports the active identity as the verified NIP-OA owner of the viewee