Release v1.0.4 - Add logging system, lock button, and emoji navigation
- Comprehensive logging system with chrome.storage.session persistence - NIP-07 action logging in background scripts with standalone functions - Vault operation logging (unlock, lock, create, reset, import, export) - Profile and bookmark operation logging - Logs page with refresh functionality and category icons - Lock button (🔒) in navigation bar to quickly lock vault - Reduced nav bar size (40px height, 16px font) with emoji icons - Reordered navigation: You, Permissions, Bookmarks, Logs, About, Lock - Bookmarks functionality for saving frequently used Nostr apps - Fixed lock/unlock flow by properly clearing in-memory session data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
<div class="icon-button">
|
||||
<i [class]="'bi bi-' + icon"></i>
|
||||
@if (isEmoji) {
|
||||
<span class="emoji">{{ icon }}</span>
|
||||
} @else {
|
||||
<i [class]="'bi bi-' + icon"></i>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -9,4 +9,9 @@ import { Component, Input } from '@angular/core';
|
||||
})
|
||||
export class IconButtonComponent {
|
||||
@Input({ required: true }) icon!: string;
|
||||
|
||||
get isEmoji(): boolean {
|
||||
// Check if the icon is an emoji (starts with a non-ASCII character)
|
||||
return this.icon.length > 0 && this.icon.charCodeAt(0) > 255;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
declare const chrome: any;
|
||||
|
||||
export type LogCategory =
|
||||
| 'nip07'
|
||||
| 'permission'
|
||||
| 'vault'
|
||||
| 'profile'
|
||||
| 'bookmark'
|
||||
| 'system';
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: Date;
|
||||
level: 'log' | 'warn' | 'error' | 'debug';
|
||||
category: LogCategory;
|
||||
icon: string;
|
||||
message: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
// Serializable format for storage
|
||||
interface StoredLogEntry {
|
||||
timestamp: string;
|
||||
level: 'log' | 'warn' | 'error' | 'debug';
|
||||
category: LogCategory;
|
||||
icon: string;
|
||||
message: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
const LOGS_STORAGE_KEY = 'extensionLogs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -20,46 +44,351 @@ export class LoggerService {
|
||||
return this.#logs;
|
||||
}
|
||||
|
||||
initialize(namespace: string): void {
|
||||
async initialize(namespace: string): Promise<void> {
|
||||
this.#namespace = namespace;
|
||||
await this.#loadLogsFromStorage();
|
||||
}
|
||||
|
||||
async #loadLogsFromStorage(): Promise<void> {
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||
const result = await chrome.storage.session.get(LOGS_STORAGE_KEY);
|
||||
if (result[LOGS_STORAGE_KEY]) {
|
||||
// Convert stored format back to LogEntry with Date objects
|
||||
this.#logs = (result[LOGS_STORAGE_KEY] as StoredLogEntry[]).map(
|
||||
(entry) => ({
|
||||
...entry,
|
||||
timestamp: new Date(entry.timestamp),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load logs from storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async #saveLogsToStorage(): Promise<void> {
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||
// Convert Date to ISO string for storage
|
||||
const storedLogs: StoredLogEntry[] = this.#logs.map((entry) => ({
|
||||
...entry,
|
||||
timestamp: entry.timestamp.toISOString(),
|
||||
}));
|
||||
await chrome.storage.session.set({ [LOGS_STORAGE_KEY]: storedLogs });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save logs to storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshLogs(): Promise<void> {
|
||||
await this.#loadLogsFromStorage();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Generic logging methods
|
||||
// ============================================
|
||||
|
||||
log(value: any, data?: any) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', value, data);
|
||||
const nowString = new Date().toLocaleString();
|
||||
console.log(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
||||
this.#addLog('log', 'system', '📝', value, data);
|
||||
this.#consoleLog('log', value);
|
||||
}
|
||||
|
||||
warn(value: any, data?: any) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('warn', value, data);
|
||||
const nowString = new Date().toLocaleString();
|
||||
console.warn(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
||||
this.#addLog('warn', 'system', '⚠️', value, data);
|
||||
this.#consoleLog('warn', value);
|
||||
}
|
||||
|
||||
error(value: any, data?: any) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('error', value, data);
|
||||
const nowString = new Date().toLocaleString();
|
||||
console.error(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
||||
this.#addLog('error', 'system', '❌', value, data);
|
||||
this.#consoleLog('error', value);
|
||||
}
|
||||
|
||||
debug(value: any, data?: any) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('debug', value, data);
|
||||
const nowString = new Date().toLocaleString();
|
||||
console.debug(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
||||
this.#addLog('debug', 'system', '🔍', value, data);
|
||||
this.#consoleLog('debug', value);
|
||||
}
|
||||
|
||||
clear() {
|
||||
// ============================================
|
||||
// NIP-07 Action Logging
|
||||
// ============================================
|
||||
|
||||
logNip07Action(
|
||||
method: string,
|
||||
host: string,
|
||||
approved: boolean,
|
||||
autoApproved: boolean,
|
||||
details?: { kind?: number; peerPubkey?: string }
|
||||
) {
|
||||
this.#assureInitialized();
|
||||
const approvalType = autoApproved ? 'auto-approved' : approved ? 'approved' : 'denied';
|
||||
const icon = approved ? '✅' : '🚫';
|
||||
|
||||
let message = `${method} from ${host} - ${approvalType}`;
|
||||
if (details?.kind !== undefined) {
|
||||
message += ` (kind: ${details.kind})`;
|
||||
}
|
||||
|
||||
this.#addLog('log', 'nip07', icon, message, {
|
||||
method,
|
||||
host,
|
||||
approved,
|
||||
autoApproved,
|
||||
...details,
|
||||
});
|
||||
this.#consoleLog('log', message);
|
||||
}
|
||||
|
||||
logNip07GetPublicKey(host: string, approved: boolean, autoApproved: boolean) {
|
||||
this.logNip07Action('getPublicKey', host, approved, autoApproved);
|
||||
}
|
||||
|
||||
logNip07SignEvent(
|
||||
host: string,
|
||||
kind: number,
|
||||
approved: boolean,
|
||||
autoApproved: boolean
|
||||
) {
|
||||
this.logNip07Action('signEvent', host, approved, autoApproved, { kind });
|
||||
}
|
||||
|
||||
logNip07Encrypt(
|
||||
method: 'nip04.encrypt' | 'nip44.encrypt',
|
||||
host: string,
|
||||
approved: boolean,
|
||||
autoApproved: boolean,
|
||||
peerPubkey?: string
|
||||
) {
|
||||
this.logNip07Action(method, host, approved, autoApproved, { peerPubkey });
|
||||
}
|
||||
|
||||
logNip07Decrypt(
|
||||
method: 'nip04.decrypt' | 'nip44.decrypt',
|
||||
host: string,
|
||||
approved: boolean,
|
||||
autoApproved: boolean,
|
||||
peerPubkey?: string
|
||||
) {
|
||||
this.logNip07Action(method, host, approved, autoApproved, { peerPubkey });
|
||||
}
|
||||
|
||||
logNip07GetRelays(host: string, approved: boolean, autoApproved: boolean) {
|
||||
this.logNip07Action('getRelays', host, approved, autoApproved);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Permission Logging
|
||||
// ============================================
|
||||
|
||||
logPermissionStored(
|
||||
host: string,
|
||||
method: string,
|
||||
policy: string,
|
||||
kind?: number
|
||||
) {
|
||||
this.#assureInitialized();
|
||||
const icon = policy === 'allow' ? '🔓' : '🔒';
|
||||
let message = `Permission stored: ${method} for ${host} - ${policy}`;
|
||||
if (kind !== undefined) {
|
||||
message += ` (kind: ${kind})`;
|
||||
}
|
||||
this.#addLog('log', 'permission', icon, message, { host, method, policy, kind });
|
||||
this.#consoleLog('log', message);
|
||||
}
|
||||
|
||||
logPermissionDeleted(host: string, method: string, kind?: number) {
|
||||
this.#assureInitialized();
|
||||
let message = `Permission deleted: ${method} for ${host}`;
|
||||
if (kind !== undefined) {
|
||||
message += ` (kind: ${kind})`;
|
||||
}
|
||||
this.#addLog('log', 'permission', '🗑️', message, { host, method, kind });
|
||||
this.#consoleLog('log', message);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Vault Operations Logging
|
||||
// ============================================
|
||||
|
||||
logVaultUnlock() {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'vault', '🔓', 'Vault unlocked', undefined);
|
||||
this.#consoleLog('log', 'Vault unlocked');
|
||||
}
|
||||
|
||||
logVaultLock() {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'vault', '🔒', 'Vault locked', undefined);
|
||||
this.#consoleLog('log', 'Vault locked');
|
||||
}
|
||||
|
||||
logVaultCreated() {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'vault', '🆕', 'Vault created', undefined);
|
||||
this.#consoleLog('log', 'Vault created');
|
||||
}
|
||||
|
||||
logVaultExport(fileName: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'vault', '📤', `Vault exported: ${fileName}`, { fileName });
|
||||
this.#consoleLog('log', `Vault exported: ${fileName}`);
|
||||
}
|
||||
|
||||
logVaultImport(fileName: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'vault', '📥', `Vault imported: ${fileName}`, { fileName });
|
||||
this.#consoleLog('log', `Vault imported: ${fileName}`);
|
||||
}
|
||||
|
||||
logVaultReset() {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('warn', 'vault', '🗑️', 'Extension reset', undefined);
|
||||
this.#consoleLog('warn', 'Extension reset');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Profile Operations Logging
|
||||
// ============================================
|
||||
|
||||
logProfileFetchError(pubkey: string, error: string) {
|
||||
this.#assureInitialized();
|
||||
const shortPubkey = pubkey.substring(0, 8) + '...';
|
||||
this.#addLog('error', 'profile', '👤', `Failed to fetch profile for ${shortPubkey}: ${error}`, {
|
||||
pubkey,
|
||||
error,
|
||||
});
|
||||
this.#consoleLog('error', `Failed to fetch profile for ${shortPubkey}: ${error}`);
|
||||
}
|
||||
|
||||
logProfileParseError(pubkey: string) {
|
||||
this.#assureInitialized();
|
||||
const shortPubkey = pubkey.substring(0, 8) + '...';
|
||||
this.#addLog('error', 'profile', '👤', `Failed to parse profile content for ${shortPubkey}`, {
|
||||
pubkey,
|
||||
});
|
||||
this.#consoleLog('error', `Failed to parse profile content for ${shortPubkey}`);
|
||||
}
|
||||
|
||||
logNip05ValidationError(nip05: string, error: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('error', 'profile', '🔗', `NIP-05 validation failed for ${nip05}: ${error}`, {
|
||||
nip05,
|
||||
error,
|
||||
});
|
||||
this.#consoleLog('error', `NIP-05 validation failed for ${nip05}: ${error}`);
|
||||
}
|
||||
|
||||
logNip05ValidationSuccess(nip05: string, pubkey: string) {
|
||||
this.#assureInitialized();
|
||||
const shortPubkey = pubkey.substring(0, 8) + '...';
|
||||
this.#addLog('log', 'profile', '✓', `NIP-05 verified: ${nip05} → ${shortPubkey}`, {
|
||||
nip05,
|
||||
pubkey,
|
||||
});
|
||||
this.#consoleLog('log', `NIP-05 verified: ${nip05} → ${shortPubkey}`);
|
||||
}
|
||||
|
||||
logProfileEdit(identityNick: string, field: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'profile', '✏️', `Profile edited: ${identityNick} - ${field}`, {
|
||||
identityNick,
|
||||
field,
|
||||
});
|
||||
this.#consoleLog('log', `Profile edited: ${identityNick} - ${field}`);
|
||||
}
|
||||
|
||||
logIdentityCreated(nick: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'profile', '🆕', `Identity created: ${nick}`, { nick });
|
||||
this.#consoleLog('log', `Identity created: ${nick}`);
|
||||
}
|
||||
|
||||
logIdentityDeleted(nick: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('warn', 'profile', '🗑️', `Identity deleted: ${nick}`, { nick });
|
||||
this.#consoleLog('warn', `Identity deleted: ${nick}`);
|
||||
}
|
||||
|
||||
logIdentitySelected(nick: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'profile', '👆', `Identity selected: ${nick}`, { nick });
|
||||
this.#consoleLog('log', `Identity selected: ${nick}`);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Bookmark Operations Logging
|
||||
// ============================================
|
||||
|
||||
logBookmarkAdded(url: string, title: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'bookmark', '🔖', `Bookmark added: ${title}`, { url, title });
|
||||
this.#consoleLog('log', `Bookmark added: ${title}`);
|
||||
}
|
||||
|
||||
logBookmarkRemoved(url: string, title: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('log', 'bookmark', '🗑️', `Bookmark removed: ${title}`, { url, title });
|
||||
this.#consoleLog('log', `Bookmark removed: ${title}`);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// System/Error Logging
|
||||
// ============================================
|
||||
|
||||
logRelayFetchError(identityNick: string, error: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('error', 'system', '📡', `Failed to fetch relays for ${identityNick}: ${error}`, {
|
||||
identityNick,
|
||||
error,
|
||||
});
|
||||
this.#consoleLog('error', `Failed to fetch relays for ${identityNick}: ${error}`);
|
||||
}
|
||||
|
||||
logStorageError(operation: string, error: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('error', 'system', '💾', `Storage error (${operation}): ${error}`, {
|
||||
operation,
|
||||
error,
|
||||
});
|
||||
this.#consoleLog('error', `Storage error (${operation}): ${error}`);
|
||||
}
|
||||
|
||||
logCryptoError(operation: string, error: string) {
|
||||
this.#assureInitialized();
|
||||
this.#addLog('error', 'system', '🔐', `Crypto error (${operation}): ${error}`, {
|
||||
operation,
|
||||
error,
|
||||
});
|
||||
this.#consoleLog('error', `Crypto error (${operation}): ${error}`);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Internal methods
|
||||
// ============================================
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.#logs = [];
|
||||
await this.#saveLogsToStorage();
|
||||
}
|
||||
|
||||
#addLog(level: LogEntry['level'], message: any, data?: any) {
|
||||
#addLog(
|
||||
level: LogEntry['level'],
|
||||
category: LogCategory,
|
||||
icon: string,
|
||||
message: any,
|
||||
data?: any
|
||||
) {
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date(),
|
||||
level,
|
||||
category,
|
||||
icon,
|
||||
message: typeof message === 'string' ? message : JSON.stringify(message),
|
||||
data,
|
||||
};
|
||||
@@ -69,6 +398,27 @@ export class LoggerService {
|
||||
if (this.#logs.length > this.#maxLogs) {
|
||||
this.#logs.pop();
|
||||
}
|
||||
|
||||
// Save to storage asynchronously (don't block)
|
||||
this.#saveLogsToStorage();
|
||||
}
|
||||
|
||||
#consoleLog(level: 'log' | 'warn' | 'error' | 'debug', message: string) {
|
||||
const nowString = new Date().toLocaleString();
|
||||
const formattedMsg = `[${this.#namespace} - ${nowString}] ${message}`;
|
||||
switch (level) {
|
||||
case 'warn':
|
||||
console.warn(formattedMsg);
|
||||
break;
|
||||
case 'error':
|
||||
console.error(formattedMsg);
|
||||
break;
|
||||
case 'debug':
|
||||
console.debug(formattedMsg);
|
||||
break;
|
||||
default:
|
||||
console.log(formattedMsg);
|
||||
}
|
||||
}
|
||||
|
||||
#assureInitialized() {
|
||||
@@ -79,3 +429,87 @@ export class LoggerService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Standalone functions for background script
|
||||
// (Background script runs in different context without Angular DI)
|
||||
// ============================================
|
||||
|
||||
export async function backgroundLog(
|
||||
category: LogCategory,
|
||||
icon: string,
|
||||
level: LogEntry['level'],
|
||||
message: string,
|
||||
data?: any
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (typeof chrome === 'undefined' || !chrome.storage?.session) {
|
||||
console.log(`[Background] ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await chrome.storage.session.get(LOGS_STORAGE_KEY);
|
||||
const existingLogs: StoredLogEntry[] = result[LOGS_STORAGE_KEY] || [];
|
||||
|
||||
const newEntry: StoredLogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
category,
|
||||
icon,
|
||||
message,
|
||||
data,
|
||||
};
|
||||
|
||||
const updatedLogs = [newEntry, ...existingLogs].slice(0, 500);
|
||||
await chrome.storage.session.set({ [LOGS_STORAGE_KEY]: updatedLogs });
|
||||
} catch (error) {
|
||||
console.error('Failed to add background log:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function backgroundLogNip07Action(
|
||||
method: string,
|
||||
host: string,
|
||||
approved: boolean,
|
||||
autoApproved: boolean,
|
||||
details?: { kind?: number; peerPubkey?: string }
|
||||
): Promise<void> {
|
||||
const approvalType = autoApproved
|
||||
? 'auto-approved'
|
||||
: approved
|
||||
? 'approved'
|
||||
: 'denied';
|
||||
const icon = approved ? '✅' : '🚫';
|
||||
|
||||
let message = `${method} from ${host} - ${approvalType}`;
|
||||
if (details?.kind !== undefined) {
|
||||
message += ` (kind: ${details.kind})`;
|
||||
}
|
||||
|
||||
await backgroundLog('nip07', icon, 'log', message, {
|
||||
method,
|
||||
host,
|
||||
approved,
|
||||
autoApproved,
|
||||
...details,
|
||||
});
|
||||
}
|
||||
|
||||
export async function backgroundLogPermissionStored(
|
||||
host: string,
|
||||
method: string,
|
||||
policy: string,
|
||||
kind?: number
|
||||
): Promise<void> {
|
||||
const icon = policy === 'allow' ? '🔓' : '🔒';
|
||||
let message = `Permission stored: ${method} for ${host} - ${policy}`;
|
||||
if (kind !== undefined) {
|
||||
message += ` (kind: ${kind})`;
|
||||
}
|
||||
await backgroundLog('permission', icon, 'log', message, {
|
||||
host,
|
||||
method,
|
||||
policy,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import { FALLBACK_PROFILE_RELAYS } from '../../constants/fallback-relays';
|
||||
import { ProfileMetadata, ProfileMetadataCache } from '../storage/types';
|
||||
import { LoggerService } from '../logger/logger.service';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
declare const chrome: any;
|
||||
@@ -14,6 +15,7 @@ const STORAGE_KEY = 'profileMetadataCache';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProfileMetadataService {
|
||||
readonly #logger = inject(LoggerService);
|
||||
#cache: ProfileMetadataCache = {};
|
||||
#pool: SimplePool | null = null;
|
||||
#fetchPromises = new Map<string, Promise<ProfileMetadata | null>>();
|
||||
@@ -52,7 +54,8 @@ export class ProfileMetadataService {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load profile cache from storage:', error);
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.#logger.logStorageError('load profile cache', errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +68,8 @@ export class ProfileMetadataService {
|
||||
await chrome.storage.session.set({ [STORAGE_KEY]: this.#cache });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save profile cache to storage:', error);
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.#logger.logStorageError('save profile cache', errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +213,7 @@ export class ProfileMetadataService {
|
||||
this.#cache[pubkey] = profile;
|
||||
results.set(pubkey, profile);
|
||||
} catch {
|
||||
console.error(`Failed to parse profile for ${pubkey}`);
|
||||
this.#logger.logProfileParseError(pubkey);
|
||||
results.set(pubkey, null);
|
||||
}
|
||||
}
|
||||
@@ -225,7 +229,8 @@ export class ProfileMetadataService {
|
||||
await this.#saveCacheToStorage();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profiles:', error);
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.#logger.logProfileFetchError('multiple', errorMsg);
|
||||
// Set null for all unfetched pubkeys on error
|
||||
for (const pubkey of uncachedPubkeys) {
|
||||
if (!results.has(pubkey)) {
|
||||
@@ -283,11 +288,12 @@ export class ProfileMetadataService {
|
||||
|
||||
return profile;
|
||||
} catch {
|
||||
console.error(`Failed to parse profile content for ${pubkey}`);
|
||||
this.#logger.logProfileParseError(pubkey);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch profile for ${pubkey}:`, error);
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.#logger.logProfileFetchError(pubkey, errorMsg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ export abstract class BrowserSessionHandler {
|
||||
this.#browserSessionData = JSON.parse(JSON.stringify(data));
|
||||
}
|
||||
|
||||
clearInMemoryData() {
|
||||
this.#browserSessionData = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the full data to the session data storage.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BrowserSyncFlow, SignerMetaData } from './types';
|
||||
import { Bookmark, BrowserSyncFlow, SignerMetaData } from './types';
|
||||
|
||||
export abstract class SignerMetaHandler {
|
||||
get signerMetaData(): SignerMetaData | undefined {
|
||||
@@ -8,7 +8,7 @@ export abstract class SignerMetaHandler {
|
||||
|
||||
#signerMetaData?: SignerMetaData;
|
||||
|
||||
readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'recklessMode', 'whitelistedHosts'];
|
||||
readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'recklessMode', 'whitelistedHosts', 'bookmarks'];
|
||||
/**
|
||||
* Load the full data from the storage. If the storage is used for storing
|
||||
* other data (e.g. browser sync data when the user decided to NOT sync),
|
||||
@@ -89,4 +89,26 @@ export abstract class SignerMetaHandler {
|
||||
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bookmarks array and immediately saves it.
|
||||
*/
|
||||
async setBookmarks(bookmarks: Bookmark[]): Promise<void> {
|
||||
if (!this.#signerMetaData) {
|
||||
this.#signerMetaData = {
|
||||
bookmarks,
|
||||
};
|
||||
} else {
|
||||
this.#signerMetaData.bookmarks = bookmarks;
|
||||
}
|
||||
|
||||
await this.saveFullData(this.#signerMetaData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current bookmarks.
|
||||
*/
|
||||
getBookmarks(): Bookmark[] {
|
||||
return this.#signerMetaData?.bookmarks ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,14 @@ export class StorageService {
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async lockVault(): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
await this.getBrowserSessionHandler().clearData();
|
||||
this.getBrowserSessionHandler().clearInMemoryData();
|
||||
// Note: We don't set isInitialized = false here because the sync data
|
||||
// (encrypted vault) is still loaded and we need it to unlock again
|
||||
}
|
||||
|
||||
async unlockVault(password: string): Promise<void> {
|
||||
await unlockVault.call(this, password);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,16 @@ export const SIGNER_META_DATA_KEY = {
|
||||
vaultSnapshots: 'vaultSnapshots',
|
||||
};
|
||||
|
||||
/**
|
||||
* Bookmark entry for storing user bookmarks
|
||||
*/
|
||||
export interface Bookmark {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface SignerMetaData {
|
||||
syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Signer sync, 3 = Custom sync (bring your own sync))
|
||||
|
||||
@@ -104,6 +114,9 @@ export interface SignerMetaData {
|
||||
|
||||
// Whitelisted hosts: auto-approve all actions from these hosts
|
||||
whitelistedHosts?: string[];
|
||||
|
||||
// User bookmarks
|
||||
bookmarks?: Bookmark[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,3 +84,11 @@ h2.font-heading {
|
||||
h3.font-heading {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
// Emoji styling
|
||||
.emoji {
|
||||
font-family: var(--font-emoji);
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--font-sans: 'IBM Plex Mono', monospace;
|
||||
--font-heading: 'reglisse', sans-serif;
|
||||
--font-theylive: 'theylive', sans-serif;
|
||||
--font-emoji: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Android Emoji', sans-serif;
|
||||
|
||||
// Border radius (from market)
|
||||
--radius: 0.25rem;
|
||||
|
||||
Reference in New Issue
Block a user