Compare commits
No commits in common. "832e1144f03d0199b033ff17a9dcffddac61fbc2" and "56c2a6d836e34ce1a126575ec21fb77f69c15fa4" have entirely different histories.
832e1144f0
...
56c2a6d836
7 changed files with 1477 additions and 189 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -106,8 +106,8 @@ export const api = {
|
|||
signerApprove: (id: string, approved: boolean) =>
|
||||
call<SignerStatus>('signer_approve', { id, approved }),
|
||||
|
||||
deleteProfile: (npub: string) => call<AppState>('delete_profile', { npub }),
|
||||
undoDelete: () => call<AppState>('undo_delete'),
|
||||
deleteProfile: (npub: string) => call<ProfileSummary>('delete_profile', { npub }),
|
||||
undoDelete: () => call<ProfileSummary>('undo_delete'),
|
||||
|
||||
copyText: (text: string) => window.backend.copyText(text),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Avatar } from '../components/Avatar';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { Button } from '../components/Button';
|
||||
|
|
@ -17,14 +17,7 @@ interface ProfilesScreenProps {
|
|||
}
|
||||
|
||||
export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
|
||||
const {
|
||||
state,
|
||||
selectProfile,
|
||||
deleteProfile,
|
||||
undoDelete,
|
||||
publishProfileMetadata,
|
||||
clearLastDeleted,
|
||||
} = useApp();
|
||||
const { state, selectProfile, deleteProfile, undoDelete, publishProfileMetadata } = useApp();
|
||||
const [selecting, setSelecting] = useState<string | null>(null);
|
||||
const [publishing, setPublishing] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -40,14 +33,6 @@ 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);
|
||||
|
|
@ -78,34 +63,19 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
|
|||
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',
|
||||
padding: '8px 12px',
|
||||
background: 'var(--token-item-bg, #f0f0f0)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--text)', fontWeight: 500 }}>
|
||||
Profile “{lastDeleted.label}” deleted.
|
||||
</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>—</span>
|
||||
<button
|
||||
<Button
|
||||
variant="secondary"
|
||||
style={{ marginRight: '8px' }}
|
||||
size="sm"
|
||||
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>
|
||||
<Icon name="refresh" size={14} /> Restore {lastDeleted.label}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<EmptyState
|
||||
|
|
@ -160,43 +130,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -71,9 +71,8 @@ interface AppContextValue {
|
|||
signerDisconnect: () => Promise<SignerStatus>;
|
||||
signerStatus: () => Promise<SignerStatus>;
|
||||
signerApprove: (id: string, approved: boolean) => Promise<SignerStatus>;
|
||||
deleteProfile: (npub: string) => Promise<AppState>;
|
||||
undoDelete: () => Promise<AppState>;
|
||||
clearLastDeleted: () => void;
|
||||
deleteProfile: (npub: string) => Promise<ProfileSummary>;
|
||||
undoDelete: () => Promise<ProfileSummary>;
|
||||
copyText: (text: string) => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
@ -235,18 +234,11 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
return api.signerApprove(id, approved);
|
||||
}, []);
|
||||
|
||||
const deleteProfile = useCallback(
|
||||
(npub: string) => applyState(api.deleteProfile(npub)),
|
||||
[applyState],
|
||||
);
|
||||
const undoDelete = useCallback(() => applyState(api.undoDelete()), [applyState]);
|
||||
const deleteProfile = useCallback((npub: string) => api.deleteProfile(npub), []);
|
||||
const undoDelete = useCallback(() => api.undoDelete(), []);
|
||||
|
||||
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);
|
||||
|
|
@ -292,7 +284,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
setProfilePicture,
|
||||
renameProfile,
|
||||
setNip05,
|
||||
clearLastDeleted,
|
||||
copyText,
|
||||
}),
|
||||
[
|
||||
|
|
@ -307,7 +298,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
setProfilePicture,
|
||||
renameProfile,
|
||||
setNip05,
|
||||
clearLastDeleted,
|
||||
publishNote,
|
||||
deleteProfile,
|
||||
undoDelete,
|
||||
|
|
|
|||
|
|
@ -451,47 +451,6 @@ 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}`);
|
||||
}
|
||||
|
|
|
|||
15
src/ipc.rs
15
src/ipc.rs
|
|
@ -146,10 +146,6 @@ pub enum Request {
|
|||
/// `true` to run the request, `false` to reject it.
|
||||
approved: bool,
|
||||
},
|
||||
DeleteProfile {
|
||||
npub: String,
|
||||
},
|
||||
UndoDelete,
|
||||
}
|
||||
|
||||
/// A reply envelope carrying either data or a safe user-facing error.
|
||||
|
|
@ -532,17 +528,6 @@ async fn run_with_app(app: &mut App, request: Request) -> Result<serde_json::Val
|
|||
app.save_settings()?;
|
||||
Ok(json!(app.settings))
|
||||
}
|
||||
Request::DeleteProfile { npub } => {
|
||||
let deleted = profiles::delete_profile(&mut app.vault, &npub)?;
|
||||
app.save_vault()?;
|
||||
app.undo_history.push(deleted.clone());
|
||||
Ok(json!(app.state_view()))
|
||||
}
|
||||
Request::UndoDelete => {
|
||||
let restored = app.undo_delete()?;
|
||||
app.save_vault()?;
|
||||
Ok(json!(app.state_view()))
|
||||
}
|
||||
// Signer control requests are handled by `run` before this function is
|
||||
// reached; keeping a wildcard arm keeps the match exhaustive here.
|
||||
_ => Err(AppError::internal("Unexpected signer request.")),
|
||||
|
|
|
|||
|
|
@ -596,28 +596,6 @@ pub fn parse_secret_key(hex_str: &str) -> Result<SecretKey, AppError> {
|
|||
SecretKey::from_slice(&bytes).map_err(|e| AppError::invalid_secret(format!("{e}")))
|
||||
}
|
||||
|
||||
/// Delete a profile by npub, returning the deleted profile for undo.
|
||||
/// The vault must not be encrypted, or the key must be provided.
|
||||
pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result<ProfileSummary, AppError> {
|
||||
let pos = vault
|
||||
.profiles
|
||||
.iter()
|
||||
.position(|p| p.public_key == npub)
|
||||
.ok_or_else(|| AppError::profile_not_found(npub))?;
|
||||
let stored = vault.profiles.remove(pos);
|
||||
if vault.active_profile.as_deref() == Some(npub) {
|
||||
vault.active_profile = None;
|
||||
}
|
||||
Ok(ProfileSummary {
|
||||
label: stored.label,
|
||||
npub: stored.public_key,
|
||||
created_at: stored.created_at,
|
||||
is_active: false,
|
||||
picture: stored.picture,
|
||||
nip05: stored.nip05,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -1082,4 +1060,27 @@ mod tests {
|
|||
.expect_err("missing profile must error");
|
||||
assert_eq!(err.kind(), crate::errors::ErrorKind::ProfileNotFound);
|
||||
}
|
||||
|
||||
/// Delete a profile by npub, returning the deleted profile for undo.
|
||||
/// The vault must not be encrypted, or the key must be provided.
|
||||
pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result<ProfileSummary, AppError> {
|
||||
let pos = vault
|
||||
.profiles
|
||||
.iter()
|
||||
.position(|p| p.public_key == npub)
|
||||
.ok_or_else(|| AppError::profile_not_found(npub))?;
|
||||
let stored = vault.profiles.remove(pos);
|
||||
// Clear the active_profile if it was the one deleted
|
||||
if vault.active_profile.as_deref() == Some(npub) {
|
||||
vault.active_profile = None;
|
||||
}
|
||||
Ok(ProfileSummary {
|
||||
label: stored.label,
|
||||
npub: stored.public_key,
|
||||
created_at: stored.created_at,
|
||||
is_active: false,
|
||||
picture: stored.picture,
|
||||
nip05: stored.nip05,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue