Identity IPC commands + frontend identity management UI
New Tauri commands: list_identities, create_identity, switch_identity, delete_identity, import_identity_key, get_active_identity. Settings UI: Identities section with list (active indicator, switch/delete buttons), Create New Identity lightbox, Import Identity Key lightbox. Export/Import buttons as placeholders for Phase 2/3. Identity switch does hot-swap: tears down old Node, starts new one with all background tasks, swaps AppNode RwLock. Frontend reloads after switch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
75ce965b63
commit
18a40756d8
3 changed files with 250 additions and 1 deletions
117
frontend/app.js
117
frontend/app.js
|
|
@ -2857,7 +2857,7 @@ document.querySelectorAll('.tab').forEach(tab => {
|
|||
loadMessages(true); loadDmRecipientOptions();
|
||||
clearNotifications('msg-');
|
||||
}
|
||||
if (target === 'settings') { loadRedundancy(); loadPublicVisible(); loadCacheSizeSetting(); }
|
||||
if (target === 'settings') { loadIdentities(); loadRedundancy(); loadPublicVisible(); loadCacheSizeSetting(); }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3199,6 +3199,121 @@ document.querySelectorAll('.text-size-opt').forEach(btn => {
|
|||
});
|
||||
});
|
||||
|
||||
// --- Identity management ---
|
||||
async function loadIdentities() {
|
||||
const list = $('#identities-list');
|
||||
if (!list) return;
|
||||
try {
|
||||
const identities = await invoke('list_identities');
|
||||
if (identities.length === 0) {
|
||||
list.innerHTML = '<p class="empty-hint">No identities</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = identities.map(id => {
|
||||
const active = id.isActive ? ' <span style="color:#7fdbca;font-size:0.7rem">(active)</span>' : '';
|
||||
const switchBtn = id.isActive ? '' : `<button class="btn btn-ghost btn-sm switch-id-btn" data-id="${id.nodeId}" style="font-size:0.65rem">Switch</button>`;
|
||||
const deleteBtn = id.isActive ? '' : `<button class="btn btn-ghost btn-sm delete-id-btn" data-id="${id.nodeId}" style="font-size:0.65rem;color:#e74c3c">Delete</button>`;
|
||||
return `<div style="display:flex;align-items:center;justify-content:space-between;padding:0.3rem 0;border-bottom:1px solid #222">
|
||||
<div>
|
||||
<span style="font-weight:600">${escapeHtml(id.displayName)}</span>${active}
|
||||
<div style="font-size:0.6rem;color:#666">${id.nodeId.substring(0, 16)}...</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.3rem">${switchBtn}${deleteBtn}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Wire switch buttons
|
||||
list.querySelectorAll('.switch-id-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Switching...';
|
||||
try {
|
||||
await invoke('switch_identity', { nodeIdHex: btn.dataset.id });
|
||||
toast('Identity switched — reloading...');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} catch (e) { toast('Error: ' + e); btn.disabled = false; btn.textContent = 'Switch'; }
|
||||
});
|
||||
});
|
||||
|
||||
// Wire delete buttons
|
||||
list.querySelectorAll('.delete-id-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Delete this identity? This cannot be undone.')) return;
|
||||
try {
|
||||
await invoke('delete_identity', { nodeIdHex: btn.dataset.id });
|
||||
toast('Identity deleted');
|
||||
loadIdentities();
|
||||
} catch (e) { toast('Error: ' + e); }
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
list.innerHTML = `<p class="empty-hint">Error: ${e}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
$('#create-identity-btn').addEventListener('click', () => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'image-lightbox';
|
||||
overlay.style.cursor = 'default';
|
||||
overlay.innerHTML = `
|
||||
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:350px;width:90%;text-align:center">
|
||||
<h3 style="color:#7fdbca;margin:0 0 0.75rem">New Identity</h3>
|
||||
<input id="new-id-name" type="text" placeholder="Display name" maxlength="50" style="width:100%;margin-bottom:0.75rem" />
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center">
|
||||
<button class="btn btn-primary btn-sm" id="new-id-create">Create</button>
|
||||
<button class="btn btn-ghost btn-sm" id="new-id-cancel">Cancel</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.querySelector('#new-id-create').addEventListener('click', async () => {
|
||||
const name = overlay.querySelector('#new-id-name').value.trim();
|
||||
if (!name) { toast('Name is required'); return; }
|
||||
try {
|
||||
const nodeId = await invoke('create_identity', { name });
|
||||
toast(`Identity created: ${nodeId.substring(0, 12)}`);
|
||||
overlay.remove();
|
||||
loadIdentities();
|
||||
} catch (e) { toast('Error: ' + e); }
|
||||
});
|
||||
overlay.querySelector('#new-id-cancel').addEventListener('click', () => overlay.remove());
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
});
|
||||
|
||||
$('#import-identity-btn').addEventListener('click', () => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'image-lightbox';
|
||||
overlay.style.cursor = 'default';
|
||||
overlay.innerHTML = `
|
||||
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:400px;width:90%;text-align:center">
|
||||
<h3 style="color:#7fdbca;margin:0 0 0.75rem">Import Identity</h3>
|
||||
<p style="font-size:0.75rem;color:#888;margin-bottom:0.5rem">Paste the 64-character hex key from an identity export.</p>
|
||||
<input id="import-id-key" type="text" placeholder="Identity key (64 hex chars)" maxlength="64" style="width:100%;margin-bottom:0.5rem;font-family:monospace;font-size:0.7rem" />
|
||||
<input id="import-id-name" type="text" placeholder="Display name" maxlength="50" style="width:100%;margin-bottom:0.75rem" />
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center">
|
||||
<button class="btn btn-primary btn-sm" id="import-id-go">Import</button>
|
||||
<button class="btn btn-ghost btn-sm" id="import-id-cancel">Cancel</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.querySelector('#import-id-go').addEventListener('click', async () => {
|
||||
const keyHex = overlay.querySelector('#import-id-key').value.trim();
|
||||
const name = overlay.querySelector('#import-id-name').value.trim() || 'Imported';
|
||||
if (keyHex.length !== 64) { toast('Key must be 64 hex characters'); return; }
|
||||
try {
|
||||
const nodeId = await invoke('import_identity_key', { keyHex, name });
|
||||
toast(`Identity imported: ${nodeId.substring(0, 12)}`);
|
||||
overlay.remove();
|
||||
loadIdentities();
|
||||
} catch (e) { toast('Error: ' + e); }
|
||||
});
|
||||
overlay.querySelector('#import-id-cancel').addEventListener('click', () => overlay.remove());
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
});
|
||||
|
||||
// Export/Import buttons (placeholder — will be implemented in Phase 2/3)
|
||||
$('#export-btn').addEventListener('click', () => { toast('Export coming soon'); });
|
||||
$('#import-btn').addEventListener('click', () => { toast('Import coming soon'); });
|
||||
|
||||
$('#notifications-btn').addEventListener('click', async () => {
|
||||
// Load current settings
|
||||
const msgVal = await invoke('get_setting', { key: 'notif_messages' }).catch(() => null) || 'on';
|
||||
|
|
|
|||
|
|
@ -182,6 +182,22 @@
|
|||
<div id="circle-profiles-body" class="hidden"><div id="circle-profiles-list"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" style="text-align:center">
|
||||
<h3 style="margin-bottom:0.5rem">Identities</h3>
|
||||
<div id="identities-list" style="margin-bottom:0.5rem"></div>
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
|
||||
<button id="create-identity-btn" class="btn btn-ghost btn-sm">New Identity</button>
|
||||
<button id="import-identity-btn" class="btn btn-ghost btn-sm">Import Key</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" style="text-align:center">
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
|
||||
<button id="export-btn" class="btn btn-ghost btn-sm">Export</button>
|
||||
<button id="import-btn" class="btn btn-ghost btn-sm">Import</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<button id="notifications-btn" class="btn btn-ghost btn-full">Notifications</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue