fix: restore profile delete/undo and themed auto-dismiss undo bar

- ipc: add DeleteProfile/UndoDelete requests returning state_view
- profiles: expose delete_profile outside tests
- api: deleteProfile/undoDelete now return AppState; AppProvider uses applyState
- fakeBackend: implement delete/undo handlers
- ProfilesScreen: themed 'Profile deleted — Undo and restore profile' (primary color, primary-soft) with 5s auto-dismiss, shown in both empty and grid states
This commit is contained in:
Avi 2026-08-27 12:07:13 -05:00
commit 9e635e7a93
6 changed files with 172 additions and 40 deletions

View file

@ -106,8 +106,8 @@ export const api = {
signerApprove: (id: string, approved: boolean) =>
call<SignerStatus>('signer_approve', { id, approved }),
deleteProfile: (npub: string) => call<ProfileSummary>('delete_profile', { npub }),
undoDelete: () => call<ProfileSummary>('undo_delete'),
deleteProfile: (npub: string) => call<AppState>('delete_profile', { npub }),
undoDelete: () => call<AppState>('undo_delete'),
copyText: (text: string) => window.backend.copyText(text),
};

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Avatar } from '../components/Avatar';
import { Badge } from '../components/Badge';
import { Button } from '../components/Button';
@ -17,7 +17,14 @@ interface ProfilesScreenProps {
}
export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
const { state, selectProfile, deleteProfile, undoDelete, publishProfileMetadata } = useApp();
const {
state,
selectProfile,
deleteProfile,
undoDelete,
publishProfileMetadata,
clearLastDeleted,
} = useApp();
const [selecting, setSelecting] = useState<string | null>(null);
const [publishing, setPublishing] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
@ -33,6 +40,14 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
const undoHistory = state?.undo_history ?? [];
const lastDeleted = undoHistory[undoHistory.length - 1] ?? null;
useEffect(() => {
if (!lastDeleted) return;
const timeout = setTimeout(() => {
clearLastDeleted();
}, 5000);
return () => clearTimeout(timeout);
}, [lastDeleted, clearLastDeleted]);
const onPublishName = async (npub: string, label: string) => {
setError(null);
setNotice(null);
@ -63,19 +78,34 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
className="undo-bar"
style={{
margin: '12px 0',
padding: '8px 12px',
background: 'var(--token-item-bg, #f0f0f0)',
borderRadius: '4px',
padding: '10px 14px',
background: 'var(--primary-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<Button
variant="secondary"
style={{ marginRight: '8px' }}
size="sm"
<span style={{ color: 'var(--text)', fontWeight: 500 }}>
Profile “{lastDeleted.label}” deleted.
</span>
<span style={{ color: 'var(--text-muted)' }}>—</span>
<button
onClick={() => void undoDelete()}
style={{
background: 'none',
border: 'none',
padding: 0,
color: 'var(--primary)',
fontWeight: 600,
cursor: 'pointer',
textDecoration: 'underline',
textUnderlineOffset: '2px',
}}
>
<Icon name="refresh" size={14} /> Restore {lastDeleted.label}
</Button>
Undo and restore profile
</button>
</div>
)}
<EmptyState
@ -130,6 +160,43 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
</p>
)}
{lastDeleted && (
<div
className="undo-bar"
style={{
margin: '12px 0',
padding: '10px 14px',
background: 'var(--primary-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
gap: '8px',
flexWrap: 'wrap',
}}
>
<span style={{ color: 'var(--text)', fontWeight: 500 }}>
Profile “{lastDeleted.label}” deleted.
</span>
<span style={{ color: 'var(--text-muted)' }}>—</span>
<button
onClick={() => void undoDelete()}
style={{
background: 'none',
border: 'none',
padding: 0,
color: 'var(--primary)',
fontWeight: 600,
cursor: 'pointer',
textDecoration: 'underline',
textUnderlineOffset: '2px',
}}
>
Undo and restore profile
</button>
</div>
)}
<div className="profile-grid">
{profiles.map((profile) => (
<article

View file

@ -71,8 +71,9 @@ interface AppContextValue {
signerDisconnect: () => Promise<SignerStatus>;
signerStatus: () => Promise<SignerStatus>;
signerApprove: (id: string, approved: boolean) => Promise<SignerStatus>;
deleteProfile: (npub: string) => Promise<ProfileSummary>;
undoDelete: () => Promise<ProfileSummary>;
deleteProfile: (npub: string) => Promise<AppState>;
undoDelete: () => Promise<AppState>;
clearLastDeleted: () => void;
copyText: (text: string) => Promise<void>;
}
@ -234,11 +235,18 @@ export function AppProvider({ children }: { children: ReactNode }) {
return api.signerApprove(id, approved);
}, []);
const deleteProfile = useCallback((npub: string) => api.deleteProfile(npub), []);
const undoDelete = useCallback(() => api.undoDelete(), []);
const deleteProfile = useCallback(
(npub: string) => applyState(api.deleteProfile(npub)),
[applyState],
);
const undoDelete = useCallback(() => applyState(api.undoDelete()), [applyState]);
const copyText = useCallback((text: string) => api.copyText(text), []);
const clearLastDeleted = useCallback(() => {
setState((prev) => (prev ? { ...prev, undo_history: prev.undo_history?.slice(0, -1) } : prev));
}, []);
useEffect(() => {
if (state?.settings.theme) {
applyTheme(state.settings.theme);
@ -284,6 +292,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
setProfilePicture,
renameProfile,
setNip05,
clearLastDeleted,
copyText,
}),
[
@ -298,6 +307,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
setProfilePicture,
renameProfile,
setNip05,
clearLastDeleted,
publishNote,
deleteProfile,
undoDelete,

View file

@ -451,6 +451,47 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
return { hex, nsec: `nsec1${npub.slice(5)}` };
}
case 'delete_profile': {
const npub = String(params.npub);
const idx = state.profiles.findIndex((p) => p.npub === npub);
if (idx === -1) throw new Error('That profile is not stored on this computer.');
const [deleted] = state.profiles.splice(idx, 1);
const wasActive = state.active_profile?.npub === npub;
const next: AppState = {
...state,
profiles: [...state.profiles],
active_profile: wasActive ? null : state.active_profile,
undo_history: [...(state.undo_history ?? []), { ...deleted, is_active: false }],
};
// clear active flag on remaining profiles if needed
if (wasActive) {
next.profiles = next.profiles.map((p) => ({ ...p, is_active: false }));
}
backend.setState(next);
return next;
}
case 'undo_delete': {
const hist = state.undo_history ?? [];
if (hist.length === 0) throw new Error('No profile deletions to undo.');
const restored = hist[hist.length - 1];
const next: AppState = {
...state,
profiles: [...state.profiles, { ...restored, is_active: state.active_profile === null }],
active_profile: state.active_profile ?? { ...restored, is_active: true },
undo_history: hist.slice(0, -1),
};
// ensure only one active
if (state.active_profile === null) {
next.profiles = next.profiles.map((p) => ({
...p,
is_active: p.npub === restored.npub,
}));
}
backend.setState(next);
return next;
}
default:
throw new Error(`Unknown method: ${method}`);
}