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) =>
|
signerApprove: (id: string, approved: boolean) =>
|
||||||
call<SignerStatus>('signer_approve', { id, approved }),
|
call<SignerStatus>('signer_approve', { id, approved }),
|
||||||
|
|
||||||
deleteProfile: (npub: string) => call<AppState>('delete_profile', { npub }),
|
deleteProfile: (npub: string) => call<ProfileSummary>('delete_profile', { npub }),
|
||||||
undoDelete: () => call<AppState>('undo_delete'),
|
undoDelete: () => call<ProfileSummary>('undo_delete'),
|
||||||
|
|
||||||
copyText: (text: string) => window.backend.copyText(text),
|
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 { Avatar } from '../components/Avatar';
|
||||||
import { Badge } from '../components/Badge';
|
import { Badge } from '../components/Badge';
|
||||||
import { Button } from '../components/Button';
|
import { Button } from '../components/Button';
|
||||||
|
|
@ -17,14 +17,7 @@ interface ProfilesScreenProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
|
export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
|
||||||
const {
|
const { state, selectProfile, deleteProfile, undoDelete, publishProfileMetadata } = useApp();
|
||||||
state,
|
|
||||||
selectProfile,
|
|
||||||
deleteProfile,
|
|
||||||
undoDelete,
|
|
||||||
publishProfileMetadata,
|
|
||||||
clearLastDeleted,
|
|
||||||
} = useApp();
|
|
||||||
const [selecting, setSelecting] = useState<string | null>(null);
|
const [selecting, setSelecting] = useState<string | null>(null);
|
||||||
const [publishing, setPublishing] = useState<string | null>(null);
|
const [publishing, setPublishing] = useState<string | null>(null);
|
||||||
const [error, setError] = 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 undoHistory = state?.undo_history ?? [];
|
||||||
const lastDeleted = undoHistory[undoHistory.length - 1] ?? null;
|
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) => {
|
const onPublishName = async (npub: string, label: string) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setNotice(null);
|
setNotice(null);
|
||||||
|
|
@ -78,34 +63,19 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
|
||||||
className="undo-bar"
|
className="undo-bar"
|
||||||
style={{
|
style={{
|
||||||
margin: '12px 0',
|
margin: '12px 0',
|
||||||
padding: '10px 14px',
|
padding: '8px 12px',
|
||||||
background: 'var(--primary-soft)',
|
background: 'var(--token-item-bg, #f0f0f0)',
|
||||||
border: '1px solid var(--border)',
|
borderRadius: '4px',
|
||||||
borderRadius: '8px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: 'var(--text)', fontWeight: 500 }}>
|
<Button
|
||||||
Profile “{lastDeleted.label}” deleted.
|
variant="secondary"
|
||||||
</span>
|
style={{ marginRight: '8px' }}
|
||||||
<span style={{ color: 'var(--text-muted)' }}>—</span>
|
size="sm"
|
||||||
<button
|
|
||||||
onClick={() => void undoDelete()}
|
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
|
<Icon name="refresh" size={14} /> Restore {lastDeleted.label}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
|
@ -160,43 +130,6 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) {
|
||||||
</p>
|
</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">
|
<div className="profile-grid">
|
||||||
{profiles.map((profile) => (
|
{profiles.map((profile) => (
|
||||||
<article
|
<article
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,8 @@ interface AppContextValue {
|
||||||
signerDisconnect: () => Promise<SignerStatus>;
|
signerDisconnect: () => Promise<SignerStatus>;
|
||||||
signerStatus: () => Promise<SignerStatus>;
|
signerStatus: () => Promise<SignerStatus>;
|
||||||
signerApprove: (id: string, approved: boolean) => Promise<SignerStatus>;
|
signerApprove: (id: string, approved: boolean) => Promise<SignerStatus>;
|
||||||
deleteProfile: (npub: string) => Promise<AppState>;
|
deleteProfile: (npub: string) => Promise<ProfileSummary>;
|
||||||
undoDelete: () => Promise<AppState>;
|
undoDelete: () => Promise<ProfileSummary>;
|
||||||
clearLastDeleted: () => void;
|
|
||||||
copyText: (text: string) => Promise<void>;
|
copyText: (text: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,18 +234,11 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
||||||
return api.signerApprove(id, approved);
|
return api.signerApprove(id, approved);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteProfile = useCallback(
|
const deleteProfile = useCallback((npub: string) => api.deleteProfile(npub), []);
|
||||||
(npub: string) => applyState(api.deleteProfile(npub)),
|
const undoDelete = useCallback(() => api.undoDelete(), []);
|
||||||
[applyState],
|
|
||||||
);
|
|
||||||
const undoDelete = useCallback(() => applyState(api.undoDelete()), [applyState]);
|
|
||||||
|
|
||||||
const copyText = useCallback((text: string) => api.copyText(text), []);
|
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(() => {
|
useEffect(() => {
|
||||||
if (state?.settings.theme) {
|
if (state?.settings.theme) {
|
||||||
applyTheme(state.settings.theme);
|
applyTheme(state.settings.theme);
|
||||||
|
|
@ -292,7 +284,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
||||||
setProfilePicture,
|
setProfilePicture,
|
||||||
renameProfile,
|
renameProfile,
|
||||||
setNip05,
|
setNip05,
|
||||||
clearLastDeleted,
|
|
||||||
copyText,
|
copyText,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
|
@ -307,7 +298,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
||||||
setProfilePicture,
|
setProfilePicture,
|
||||||
renameProfile,
|
renameProfile,
|
||||||
setNip05,
|
setNip05,
|
||||||
clearLastDeleted,
|
|
||||||
publishNote,
|
publishNote,
|
||||||
deleteProfile,
|
deleteProfile,
|
||||||
undoDelete,
|
undoDelete,
|
||||||
|
|
|
||||||
|
|
@ -451,47 +451,6 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
|
||||||
return { hex, nsec: `nsec1${npub.slice(5)}` };
|
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:
|
default:
|
||||||
throw new Error(`Unknown method: ${method}`);
|
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.
|
/// `true` to run the request, `false` to reject it.
|
||||||
approved: bool,
|
approved: bool,
|
||||||
},
|
},
|
||||||
DeleteProfile {
|
|
||||||
npub: String,
|
|
||||||
},
|
|
||||||
UndoDelete,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A reply envelope carrying either data or a safe user-facing error.
|
/// 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()?;
|
app.save_settings()?;
|
||||||
Ok(json!(app.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
|
// Signer control requests are handled by `run` before this function is
|
||||||
// reached; keeping a wildcard arm keeps the match exhaustive here.
|
// reached; keeping a wildcard arm keeps the match exhaustive here.
|
||||||
_ => Err(AppError::internal("Unexpected signer request.")),
|
_ => 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}")))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -1082,4 +1060,27 @@ mod tests {
|
||||||
.expect_err("missing profile must error");
|
.expect_err("missing profile must error");
|
||||||
assert_eq!(err.kind(), crate::errors::ErrorKind::ProfileNotFound);
|
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