fix(profile): show archive control while snapshot loads

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-06-29 16:04:26 -07:00
parent 15a73aa27f
commit 02ff18b469
5 changed files with 49 additions and 13 deletions

View File

@@ -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;

View File

@@ -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({
<DropdownMenuSeparator />
) : null}
{hasArchiveAction && archiveActions ? (
archiveActions.isArchived ? (
archiveActions.isArchived === true ? (
<DropdownMenuItem
data-testid="user-profile-unarchive-identity"
disabled={isPending}
@@ -158,11 +157,21 @@ export function UserProfileAgentSettingsMenu({
) : (
<DropdownMenuItem
data-testid="user-profile-archive-identity"
disabled={isPending}
onSelect={() => setArchiveConfirmOpen(true)}
disabled={isPending || !archiveStateKnown}
onSelect={(event) => {
if (!archiveStateKnown) {
event.preventDefault();
return;
}
setArchiveConfirmOpen(true);
}}
>
<Archive className="h-4 w-4" />
{archiveActions.isPending ? "Archiving…" : archiveLabel}
{archiveActions.isPending
? "Archiving…"
: archiveStateKnown
? archiveLabel
: "Checking archive status…"}
</DropdownMenuItem>
)
) : null}
@@ -186,11 +195,12 @@ export function UserProfileAgentSettingsMenu({
) : null}
</DropdownMenuContent>
</DropdownMenu>
{hasArchiveAction && archiveActions ? (
{hasArchiveAction && archiveActions && archiveStateKnown ? (
<ArchiveConfirmDialog
isBot={isBot}
isPending={archiveActions.isPending}
onConfirm={() => {
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 = {

View File

@@ -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 };
}

View File

@@ -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,
}) => {

View File

@@ -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