feat: multiple identity profiles
Identity tab now supports multiple named profiles: - Dropdown selector to switch between profiles - "+" button to add a new profile (prompts for name) - Pencil button to rename - X button to delete (can't delete the last one) - Toggle to enable/disable each profile independently Default profile is "Personal". All active profiles are merged and substituted simultaneously — so "Personal" (your name) and "Work" (your work email, company domain) both get caught. Storage model: profiles are stored as an array under ss_identity. getIdentity() merges all active profiles into a single identity object for the substitution engine. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -276,6 +276,28 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Profile bar */
|
||||
.profile-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile-bar .select {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.profile-bar .btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Identity tab */
|
||||
.id-section {
|
||||
margin-bottom: 12px;
|
||||
|
||||
+12
-1
@@ -46,7 +46,18 @@
|
||||
|
||||
<!-- Identity Tab (Smart Patterns) -->
|
||||
<section class="tab-content active" id="tab-identity">
|
||||
<p class="help-text">Tell Silent Send who you are. It auto-catches all variations.</p>
|
||||
<!-- Profile selector -->
|
||||
<div class="profile-bar">
|
||||
<select id="profileSelect" class="select" style="flex:1"></select>
|
||||
<button class="btn-icon" id="btnAddProfile" title="Add profile">+</button>
|
||||
<button class="btn-icon" id="btnRenameProfile" title="Rename profile">✎</button>
|
||||
<button class="btn-icon" id="btnDeleteProfile" title="Delete profile">×</button>
|
||||
<label class="toggle" title="Enable/disable this profile" style="margin-left:4px">
|
||||
<input type="checkbox" id="profileActive" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-text">Each profile is a person/identity. All active profiles are protected simultaneously.</p>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Names</div>
|
||||
|
||||
+115
-38
@@ -6,7 +6,9 @@ import api from '../lib/browser-polyfill.js';
|
||||
|
||||
// --- State ---
|
||||
let mappings = [];
|
||||
let identity = {};
|
||||
let identity = {}; // merged identity (all active profiles)
|
||||
let profiles = []; // all profiles
|
||||
let currentProfileId = null; // currently selected profile for editing
|
||||
let settings = {};
|
||||
|
||||
// --- DOM refs ---
|
||||
@@ -16,12 +18,21 @@ const $$ = (sel) => document.querySelectorAll(sel);
|
||||
// --- Init ---
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
mappings = await Storage.getMappings();
|
||||
profiles = await Storage.getProfiles();
|
||||
identity = await Storage.getIdentity();
|
||||
settings = await Storage.getSettings();
|
||||
|
||||
// Initialize profiles — create default if none exist
|
||||
if (profiles.length === 0) {
|
||||
const p = await Storage.addProfile('Personal');
|
||||
profiles = [p];
|
||||
}
|
||||
currentProfileId = profiles[0].id;
|
||||
|
||||
renderProfileSelector();
|
||||
loadIdentityForm();
|
||||
renderMappings();
|
||||
renderActivity();
|
||||
loadIdentityForm();
|
||||
updateStatusDot();
|
||||
checkFirstRun();
|
||||
|
||||
@@ -37,12 +48,20 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
if (tab.dataset.tab === 'activity') renderActivity();
|
||||
if (tab.dataset.tab === 'test') {
|
||||
// Reload identity from storage in case it was just saved
|
||||
// Reload merged identity from storage in case profiles were just saved
|
||||
Storage.getIdentity().then((id) => {
|
||||
identity = id;
|
||||
updateIdentityStatus();
|
||||
});
|
||||
}
|
||||
if (tab.dataset.tab === 'identity') {
|
||||
// Reload profiles
|
||||
Storage.getProfiles().then((p) => {
|
||||
profiles = p;
|
||||
renderProfileSelector();
|
||||
loadIdentityForm();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +89,57 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
$('#btnReveal').classList.toggle('active', settings.revealMode);
|
||||
|
||||
// Profile controls
|
||||
$('#profileSelect').addEventListener('change', (e) => {
|
||||
currentProfileId = e.target.value;
|
||||
loadIdentityForm();
|
||||
});
|
||||
|
||||
$('#btnAddProfile').addEventListener('click', async () => {
|
||||
const name = prompt('Profile name:', `Profile ${profiles.length + 1}`);
|
||||
if (!name) return;
|
||||
const p = await Storage.addProfile(name);
|
||||
profiles = await Storage.getProfiles();
|
||||
currentProfileId = p.id;
|
||||
renderProfileSelector();
|
||||
loadIdentityForm();
|
||||
checkFirstRun();
|
||||
});
|
||||
|
||||
$('#btnRenameProfile').addEventListener('click', async () => {
|
||||
const profile = profiles.find(p => p.id === currentProfileId);
|
||||
if (!profile) return;
|
||||
const name = prompt('Rename profile:', profile.name);
|
||||
if (!name) return;
|
||||
await Storage.updateProfile(currentProfileId, { name });
|
||||
profiles = await Storage.getProfiles();
|
||||
renderProfileSelector();
|
||||
});
|
||||
|
||||
$('#btnDeleteProfile').addEventListener('click', async () => {
|
||||
if (profiles.length <= 1) {
|
||||
alert('Cannot delete the last profile.');
|
||||
return;
|
||||
}
|
||||
const profile = profiles.find(p => p.id === currentProfileId);
|
||||
if (!confirm(`Delete profile "${profile?.name}"?`)) return;
|
||||
await Storage.deleteProfile(currentProfileId);
|
||||
profiles = await Storage.getProfiles();
|
||||
currentProfileId = profiles[0]?.id;
|
||||
renderProfileSelector();
|
||||
loadIdentityForm();
|
||||
identity = await Storage.getIdentity();
|
||||
checkFirstRun();
|
||||
});
|
||||
|
||||
$('#profileActive').addEventListener('change', async (e) => {
|
||||
await Storage.updateProfile(currentProfileId, { active: e.target.checked });
|
||||
profiles = await Storage.getProfiles();
|
||||
identity = await Storage.getIdentity();
|
||||
renderProfileSelector();
|
||||
checkFirstRun();
|
||||
});
|
||||
|
||||
// Save identity
|
||||
$('#btnSaveIdentity').addEventListener('click', saveIdentity);
|
||||
|
||||
@@ -108,40 +178,45 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Profiles ---
|
||||
function renderProfileSelector() {
|
||||
const select = $('#profileSelect');
|
||||
select.innerHTML = profiles.map(p =>
|
||||
`<option value="${p.id}" ${p.id === currentProfileId ? 'selected' : ''}>` +
|
||||
`${escapeHtml(p.name)}${p.active ? '' : ' (off)'}` +
|
||||
`</option>`
|
||||
).join('');
|
||||
|
||||
const profile = profiles.find(p => p.id === currentProfileId);
|
||||
$('#profileActive').checked = profile?.active ?? true;
|
||||
}
|
||||
|
||||
// --- Identity ---
|
||||
function loadIdentityForm() {
|
||||
const first = (identity.names || []).find(n => n.type === 'first');
|
||||
const last = (identity.names || []).find(n => n.type === 'last');
|
||||
const email = (identity.emails || [])[0];
|
||||
const user = (identity.usernames || [])[0];
|
||||
const phone = (identity.phones || [])[0];
|
||||
const profile = profiles.find(p => p.id === currentProfileId);
|
||||
if (!profile) return;
|
||||
|
||||
if (first) {
|
||||
$('#idFirstReal').value = first.real || '';
|
||||
$('#idFirstSub').value = first.substitute || '';
|
||||
}
|
||||
if (last) {
|
||||
$('#idLastReal').value = last.real || '';
|
||||
$('#idLastSub').value = last.substitute || '';
|
||||
}
|
||||
if (email) {
|
||||
$('#idEmailReal').value = email.real || '';
|
||||
$('#idEmailSub').value = email.substitute || '';
|
||||
}
|
||||
$('#idCatchAllEmail').value = identity.catchAllEmail || '';
|
||||
if (user) {
|
||||
$('#idUserReal').value = user.real || '';
|
||||
$('#idUserSub').value = user.substitute || '';
|
||||
}
|
||||
const host = (identity.hostnames || [])[0];
|
||||
if (host) {
|
||||
$('#idHostReal').value = host.real || '';
|
||||
$('#idHostSub').value = host.substitute || '';
|
||||
}
|
||||
if (phone) {
|
||||
$('#idPhoneReal').value = phone.real || '';
|
||||
$('#idPhoneSub').value = phone.substitute || '';
|
||||
}
|
||||
const first = (profile.names || []).find(n => n.type === 'first');
|
||||
const last = (profile.names || []).find(n => n.type === 'last');
|
||||
const email = (profile.emails || [])[0];
|
||||
const user = (profile.usernames || [])[0];
|
||||
const host = (profile.hostnames || [])[0];
|
||||
const phone = (profile.phones || [])[0];
|
||||
|
||||
$('#idFirstReal').value = first?.real || '';
|
||||
$('#idFirstSub').value = first?.substitute || '';
|
||||
$('#idLastReal').value = last?.real || '';
|
||||
$('#idLastSub').value = last?.substitute || '';
|
||||
$('#idEmailReal').value = email?.real || '';
|
||||
$('#idEmailSub').value = email?.substitute || '';
|
||||
$('#idCatchAllEmail').value = profile.catchAllEmail || '';
|
||||
$('#idUserReal').value = user?.real || '';
|
||||
$('#idUserSub').value = user?.substitute || '';
|
||||
$('#idHostReal').value = host?.real || '';
|
||||
$('#idHostSub').value = host?.substitute || '';
|
||||
$('#idPhoneReal').value = phone?.real || '';
|
||||
$('#idPhoneSub').value = phone?.substitute || '';
|
||||
$('#profileActive').checked = profile.active ?? true;
|
||||
}
|
||||
|
||||
async function saveIdentity() {
|
||||
@@ -185,18 +260,20 @@ async function saveIdentity() {
|
||||
phones.push({ real: phoneReal, substitute: phoneSub });
|
||||
}
|
||||
|
||||
identity = {
|
||||
const profileData = {
|
||||
names,
|
||||
emails,
|
||||
usernames,
|
||||
hostnames,
|
||||
phones,
|
||||
catchAllEmail: $('#idCatchAllEmail').value.trim(),
|
||||
emailDomains: identity.emailDomains || [],
|
||||
enabled: identity.enabled || { emails: true, names: true, usernames: true, phones: true, paths: true },
|
||||
emailDomains: [],
|
||||
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },
|
||||
};
|
||||
|
||||
await Storage.saveIdentity(identity);
|
||||
await Storage.updateProfile(currentProfileId, profileData);
|
||||
profiles = await Storage.getProfiles();
|
||||
identity = await Storage.getIdentity();
|
||||
checkFirstRun();
|
||||
|
||||
// Flash save button
|
||||
|
||||
Reference in New Issue
Block a user