Files
relicario/extension/src/popup/components/trash.ts
adlee-was-taken 9a8cdf8e4f fix: replace all remaining emoji with monochrome glyph constants
- trash.ts TYPE_ICONS map uses GLYPH_TYPE_* constants
- field-history.ts copy button uses GLYPH_COPY
- attachments-disclosure.ts thumbnail/icon uses GLYPH_TYPE_DOCUMENT
- settings.ts sync button uses GLYPH_SYNC
- document.ts thumb/sigblock/preview use GLYPH_TYPE_DOCUMENT + GLYPH_PREVIEW
- glyphs.ts adds GLYPH_COPY, GLYPH_SYNC, GLYPH_PREVIEW
- vault.ts adds GLYPH_DEVICES import + devices sidebar nav button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:37:23 -04:00

127 lines
4.6 KiB
TypeScript

/// Trash view — lists soft-deleted items with restore/purge actions.
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../shared/state';
import type { ItemId, ManifestEntry, VaultSettings } from '../../shared/types';
import {
GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_CARD,
GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, GLYPH_TYPE_TOTP,
} from '../../shared/glyphs';
const TYPE_ICONS: Record<string, string> = {
login: GLYPH_TYPE_LOGIN,
secure_note: GLYPH_TYPE_SECURE_NOTE,
identity: GLYPH_TYPE_IDENTITY,
card: GLYPH_TYPE_CARD,
key: GLYPH_TYPE_KEY,
document: GLYPH_TYPE_DOCUMENT,
totp: GLYPH_TYPE_TOTP,
};
function relativeTime(unixSec: number): string {
const now = Math.floor(Date.now() / 1000);
const diff = now - unixSec;
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return `${Math.floor(diff / 86400)}d ago`;
}
function daysUntilPurge(trashedAt: number, retention: VaultSettings['trash_retention']): number | null {
if (retention.kind === 'forever') return null;
const trashedDaysAgo = Math.floor((Date.now() / 1000 - trashedAt) / 86400);
return Math.max(0, retention.value - trashedDaysAgo);
}
export function teardown(): void {
// No cleanup needed
}
export async function renderTrash(app: HTMLElement): Promise<void> {
const state = getState();
// Fetch trashed items
const resp = await sendMessage({ type: 'list_trashed' });
if (!resp.ok) {
app.innerHTML = `<div class="pad"><p class="error">Failed to load trash</p></div>`;
return;
}
const items = (resp.data as { items: Array<[ItemId, ManifestEntry]> }).items;
const retention = state.vaultSettings?.trash_retention ?? { kind: 'days', value: 30 };
// Calculate days until oldest auto-purges
let oldestPurgeDays: number | null = null;
if (items.length > 0 && retention.kind === 'days') {
const oldest = items[items.length - 1][1];
oldestPurgeDays = daysUntilPurge(oldest.trashed_at ?? 0, retention);
}
const headerInfo = items.length === 0
? ''
: oldestPurgeDays !== null
? `${items.length} item${items.length === 1 ? '' : 's'} · oldest auto-purges in ${oldestPurgeDays}d`
: `${items.length} item${items.length === 1 ? '' : 's'}`;
app.innerHTML = `
<div class="pad">
<div class="trash-header">
<button class="btn" id="back-btn">← back</button>
<h3 style="margin:0;">trash</h3>
</div>
${headerInfo ? `<p class="muted" style="margin:8px 0;">${escapeHtml(headerInfo)}</p>` : ''}
${items.length === 0
? `<p class="muted" style="text-align:center;margin-top:32px;">Trash is empty</p>`
: items.map(([id, entry]) => `
<div class="trash-row" data-id="${escapeHtml(id)}">
<span class="trash-row__icon">${TYPE_ICONS[entry.type] ?? '◻'}</span>
<div class="trash-row__info">
<span class="trash-row__title">${escapeHtml(entry.title)}</span>
<span class="trash-row__meta">trashed ${relativeTime(entry.trashed_at ?? 0)}</span>
</div>
<button class="trash-row__restore" data-restore="${escapeHtml(id)}">restore</button>
</div>
`).join('')}
${items.length > 0 ? `
<div style="margin-top:16px;text-align:center;">
<button class="btn danger" id="empty-trash-btn">empty trash</button>
</div>
` : ''}
</div>
`;
// Wire handlers
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
document.querySelectorAll<HTMLButtonElement>('[data-restore]').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = btn.dataset.restore;
if (!id) return;
btn.disabled = true;
btn.textContent = '...';
const result = await sendMessage({ type: 'restore_item', id });
if (result.ok) {
await sendMessage({ type: 'sync' });
renderTrash(app);
} else {
setState({ error: result.error });
}
});
});
document.getElementById('empty-trash-btn')?.addEventListener('click', async () => {
if (!confirm(`Permanently delete ${items.length} item${items.length === 1 ? '' : 's'}? This cannot be undone.`)) {
return;
}
const btn = document.getElementById('empty-trash-btn') as HTMLButtonElement;
btn.disabled = true;
btn.textContent = 'deleting...';
const result = await sendMessage({ type: 'purge_all_trash' });
if (result.ok) {
await sendMessage({ type: 'sync' });
renderTrash(app);
} else {
setState({ error: result.error });
}
});
}