Add NIP-65 relay list display and improve identity UI
- Add NIP-65 relay list service to fetch kind 10002 events from relays - Replace configurable relay page with read-only NIP-65 relay display - Update identity page to show display name and username in same badge - Use reglisse heading font for titles throughout the UI - Navigate to You page after vault unlock instead of identities list - Add autofocus to vault password input field - Add profile metadata service for fetching kind 0 events - Add readonly mode to relay-rw component Files modified: - package.json (version bump to 0.0.6) - projects/common/src/lib/services/relay-list/relay-list.service.ts (new) - projects/common/src/lib/services/profile-metadata/profile-metadata.service.ts (new) - projects/common/src/lib/constants/fallback-relays.ts (new) - projects/*/src/app/components/home/identity/* (UI improvements) - projects/*/src/app/components/edit-identity/relays/* (NIP-65 display) - projects/*/src/app/components/vault-login/* (autofocus, navigation) - projects/common/src/lib/styles/* (heading fonts) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,11 @@
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.is-readonly {
|
||||
cursor: default;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.read {
|
||||
&:not(.is-selected) {
|
||||
border: 1px solid var(--bs-green);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
export class RelayRwComponent {
|
||||
@Input({ required: true }) type!: 'read' | 'write';
|
||||
@Input({ required: true }) model!: boolean;
|
||||
@Input() readonly = false;
|
||||
@Output() modelChange = new EventEmitter<boolean>();
|
||||
|
||||
@HostBinding('class.read') get isRead() {
|
||||
@@ -27,7 +28,14 @@ export class RelayRwComponent {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
@HostBinding('class.is-readonly') get isReadonly() {
|
||||
return this.readonly;
|
||||
}
|
||||
|
||||
@HostListener('click') onClick() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this.model = !this.model;
|
||||
this.modelChange.emit(this.model);
|
||||
}
|
||||
|
||||
11
projects/common/src/lib/constants/fallback-relays.ts
Normal file
11
projects/common/src/lib/constants/fallback-relays.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Fallback relays used for fetching profile metadata (kind 0 events).
|
||||
* These are well-known relays that aggregate profile data.
|
||||
*/
|
||||
export const FALLBACK_PROFILE_RELAYS = [
|
||||
'wss://relay.nostr.band/',
|
||||
'wss://nostr.wine/',
|
||||
'wss://nos.lol/',
|
||||
'wss://relay.primal.net/',
|
||||
'wss://purplepag.es/',
|
||||
];
|
||||
@@ -0,0 +1,359 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import { FALLBACK_PROFILE_RELAYS } from '../../constants/fallback-relays';
|
||||
import { ProfileMetadata, ProfileMetadataCache } from '../storage/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
declare const chrome: any;
|
||||
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const FETCH_TIMEOUT_MS = 10000; // 10 seconds
|
||||
const STORAGE_KEY = 'profileMetadataCache';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProfileMetadataService {
|
||||
#cache: ProfileMetadataCache = {};
|
||||
#pool: SimplePool | null = null;
|
||||
#fetchPromises = new Map<string, Promise<ProfileMetadata | null>>();
|
||||
#initialized = false;
|
||||
#initPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the service by loading cache from session storage
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.#initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#initPromise) {
|
||||
return this.#initPromise;
|
||||
}
|
||||
|
||||
this.#initPromise = this.#loadCacheFromStorage();
|
||||
await this.#initPromise;
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cache from browser session storage
|
||||
*/
|
||||
async #loadCacheFromStorage(): Promise<void> {
|
||||
try {
|
||||
// Use chrome API (works in both Chrome and Firefox with polyfill)
|
||||
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||
const result = await chrome.storage.session.get(STORAGE_KEY);
|
||||
if (result[STORAGE_KEY]) {
|
||||
this.#cache = result[STORAGE_KEY];
|
||||
// Clean up stale entries
|
||||
this.#pruneStaleCache();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load profile cache from storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cache to browser session storage
|
||||
*/
|
||||
async #saveCacheToStorage(): Promise<void> {
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||
await chrome.storage.session.set({ [STORAGE_KEY]: this.#cache });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save profile cache to storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove stale entries from cache
|
||||
*/
|
||||
#pruneStaleCache(): void {
|
||||
const now = Date.now();
|
||||
for (const pubkey of Object.keys(this.#cache)) {
|
||||
if (now - this.#cache[pubkey].fetchedAt > CACHE_TTL_MS) {
|
||||
delete this.#cache[pubkey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SimplePool instance, creating it if necessary
|
||||
*/
|
||||
#getPool(): SimplePool {
|
||||
if (!this.#pool) {
|
||||
this.#pool = new SimplePool();
|
||||
}
|
||||
return this.#pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached profile metadata for a pubkey
|
||||
*/
|
||||
getCachedProfile(pubkey: string): ProfileMetadata | null {
|
||||
const cached = this.#cache[pubkey];
|
||||
if (!cached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if cache is still valid
|
||||
if (Date.now() - cached.fetchedAt > CACHE_TTL_MS) {
|
||||
delete this.#cache[pubkey];
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch profile metadata for a single pubkey
|
||||
*/
|
||||
async fetchProfile(pubkey: string): Promise<ProfileMetadata | null> {
|
||||
// Ensure initialized
|
||||
await this.initialize();
|
||||
|
||||
// Check cache first
|
||||
const cached = this.getCachedProfile(pubkey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Check if already fetching
|
||||
const existingPromise = this.#fetchPromises.get(pubkey);
|
||||
if (existingPromise) {
|
||||
return existingPromise;
|
||||
}
|
||||
|
||||
// Start new fetch
|
||||
const fetchPromise = this.#doFetchProfile(pubkey);
|
||||
this.#fetchPromises.set(pubkey, fetchPromise);
|
||||
|
||||
try {
|
||||
const result = await fetchPromise;
|
||||
return result;
|
||||
} finally {
|
||||
this.#fetchPromises.delete(pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch profiles for multiple pubkeys in parallel
|
||||
*/
|
||||
async fetchProfiles(pubkeys: string[]): Promise<Map<string, ProfileMetadata | null>> {
|
||||
// Ensure initialized
|
||||
await this.initialize();
|
||||
|
||||
const results = new Map<string, ProfileMetadata | null>();
|
||||
|
||||
// Filter out pubkeys we already have cached
|
||||
const uncachedPubkeys: string[] = [];
|
||||
for (const pubkey of pubkeys) {
|
||||
const cached = this.getCachedProfile(pubkey);
|
||||
if (cached) {
|
||||
results.set(pubkey, cached);
|
||||
} else {
|
||||
uncachedPubkeys.push(pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
if (uncachedPubkeys.length === 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// Fetch all uncached profiles
|
||||
const pool = this.#getPool();
|
||||
|
||||
try {
|
||||
const events = await this.#queryWithTimeout(
|
||||
pool,
|
||||
FALLBACK_PROFILE_RELAYS,
|
||||
[{ kinds: [0], authors: uncachedPubkeys }],
|
||||
FETCH_TIMEOUT_MS
|
||||
);
|
||||
|
||||
// Process events - keep only the most recent event per pubkey
|
||||
const latestEvents = new Map<string, { created_at: number; content: string }>();
|
||||
|
||||
for (const event of events) {
|
||||
const existing = latestEvents.get(event.pubkey);
|
||||
if (!existing || event.created_at > existing.created_at) {
|
||||
latestEvents.set(event.pubkey, {
|
||||
created_at: event.created_at,
|
||||
content: event.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and cache the profiles
|
||||
for (const [pubkey, eventData] of latestEvents) {
|
||||
try {
|
||||
const content = JSON.parse(eventData.content);
|
||||
const profile: ProfileMetadata = {
|
||||
pubkey,
|
||||
name: content.name,
|
||||
display_name: content.display_name,
|
||||
displayName: content.displayName,
|
||||
picture: content.picture,
|
||||
banner: content.banner,
|
||||
about: content.about,
|
||||
website: content.website,
|
||||
nip05: content.nip05,
|
||||
lud06: content.lud06,
|
||||
lud16: content.lud16,
|
||||
fetchedAt: Date.now(),
|
||||
};
|
||||
this.#cache[pubkey] = profile;
|
||||
results.set(pubkey, profile);
|
||||
} catch {
|
||||
console.error(`Failed to parse profile for ${pubkey}`);
|
||||
results.set(pubkey, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Set null for pubkeys we didn't find
|
||||
for (const pubkey of uncachedPubkeys) {
|
||||
if (!results.has(pubkey)) {
|
||||
results.set(pubkey, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated cache to storage
|
||||
await this.#saveCacheToStorage();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profiles:', error);
|
||||
// Set null for all unfetched pubkeys on error
|
||||
for (const pubkey of uncachedPubkeys) {
|
||||
if (!results.has(pubkey)) {
|
||||
results.set(pubkey, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to fetch a single profile
|
||||
*/
|
||||
async #doFetchProfile(pubkey: string): Promise<ProfileMetadata | null> {
|
||||
const pool = this.#getPool();
|
||||
|
||||
try {
|
||||
const events = await this.#queryWithTimeout(
|
||||
pool,
|
||||
FALLBACK_PROFILE_RELAYS,
|
||||
[{ kinds: [0], authors: [pubkey] }],
|
||||
FETCH_TIMEOUT_MS
|
||||
);
|
||||
|
||||
if (events.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the most recent event
|
||||
const latestEvent = events.reduce((latest, event) =>
|
||||
event.created_at > latest.created_at ? event : latest
|
||||
);
|
||||
|
||||
try {
|
||||
const content = JSON.parse(latestEvent.content);
|
||||
const profile: ProfileMetadata = {
|
||||
pubkey,
|
||||
name: content.name,
|
||||
display_name: content.display_name,
|
||||
displayName: content.displayName,
|
||||
picture: content.picture,
|
||||
banner: content.banner,
|
||||
about: content.about,
|
||||
website: content.website,
|
||||
nip05: content.nip05,
|
||||
lud06: content.lud06,
|
||||
lud16: content.lud16,
|
||||
fetchedAt: Date.now(),
|
||||
};
|
||||
this.#cache[pubkey] = profile;
|
||||
|
||||
// Save updated cache to storage
|
||||
await this.#saveCacheToStorage();
|
||||
|
||||
return profile;
|
||||
} catch {
|
||||
console.error(`Failed to parse profile content for ${pubkey}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch profile for ${pubkey}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query relays with a timeout
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async #queryWithTimeout(pool: SimplePool, relays: string[], filters: any[], timeoutMs: number): Promise<any[]> {
|
||||
return new Promise((resolve) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const events: any[] = [];
|
||||
let settled = false;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
resolve(events);
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
const sub = pool.subscribeMany(relays, filters, {
|
||||
onevent(event) {
|
||||
events.push(event);
|
||||
},
|
||||
oneose() {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
sub.close();
|
||||
resolve(events);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache
|
||||
*/
|
||||
async clearCache(): Promise<void> {
|
||||
this.#cache = {};
|
||||
await this.#saveCacheToStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for a specific pubkey
|
||||
*/
|
||||
async clearCacheForPubkey(pubkey: string): Promise<void> {
|
||||
delete this.#cache[pubkey];
|
||||
await this.#saveCacheToStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name for a profile (prioritizes display_name over name)
|
||||
*/
|
||||
getDisplayName(profile: ProfileMetadata | null): string | undefined {
|
||||
if (!profile) return undefined;
|
||||
return profile.display_name || profile.displayName || profile.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username for a profile (prioritizes name over display_name)
|
||||
*/
|
||||
getUsername(profile: ProfileMetadata | null): string | undefined {
|
||||
if (!profile) return undefined;
|
||||
return profile.name || profile.display_name || profile.displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import { FALLBACK_PROFILE_RELAYS } from '../../constants/fallback-relays';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
declare const chrome: any;
|
||||
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const FETCH_TIMEOUT_MS = 10000; // 10 seconds
|
||||
const STORAGE_KEY = 'relayListCache';
|
||||
|
||||
/**
|
||||
* NIP-65 Relay List entry
|
||||
*/
|
||||
export interface Nip65Relay {
|
||||
url: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached relay list for a pubkey
|
||||
*/
|
||||
export interface RelayListCache {
|
||||
pubkey: string;
|
||||
relays: Nip65Relay[];
|
||||
fetchedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache for relay lists, stored in session storage
|
||||
*/
|
||||
type RelayListCacheMap = Record<string, RelayListCache>;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RelayListService {
|
||||
#cache: RelayListCacheMap = {};
|
||||
#pool: SimplePool | null = null;
|
||||
#fetchPromises = new Map<string, Promise<Nip65Relay[]>>();
|
||||
#initialized = false;
|
||||
#initPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the service by loading cache from session storage
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.#initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#initPromise) {
|
||||
return this.#initPromise;
|
||||
}
|
||||
|
||||
this.#initPromise = this.#loadCacheFromStorage();
|
||||
await this.#initPromise;
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cache from browser session storage
|
||||
*/
|
||||
async #loadCacheFromStorage(): Promise<void> {
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||
const result = await chrome.storage.session.get(STORAGE_KEY);
|
||||
if (result[STORAGE_KEY]) {
|
||||
this.#cache = result[STORAGE_KEY];
|
||||
this.#pruneStaleCache();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load relay list cache from storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cache to browser session storage
|
||||
*/
|
||||
async #saveCacheToStorage(): Promise<void> {
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||
await chrome.storage.session.set({ [STORAGE_KEY]: this.#cache });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save relay list cache to storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove stale entries from cache
|
||||
*/
|
||||
#pruneStaleCache(): void {
|
||||
const now = Date.now();
|
||||
for (const pubkey of Object.keys(this.#cache)) {
|
||||
if (now - this.#cache[pubkey].fetchedAt > CACHE_TTL_MS) {
|
||||
delete this.#cache[pubkey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SimplePool instance, creating it if necessary
|
||||
*/
|
||||
#getPool(): SimplePool {
|
||||
if (!this.#pool) {
|
||||
this.#pool = new SimplePool();
|
||||
}
|
||||
return this.#pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached relay list for a pubkey
|
||||
*/
|
||||
getCachedRelayList(pubkey: string): Nip65Relay[] | null {
|
||||
const cached = this.#cache[pubkey];
|
||||
if (!cached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Date.now() - cached.fetchedAt > CACHE_TTL_MS) {
|
||||
delete this.#cache[pubkey];
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached.relays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch NIP-65 relay list for a single pubkey
|
||||
*/
|
||||
async fetchRelayList(pubkey: string): Promise<Nip65Relay[]> {
|
||||
await this.initialize();
|
||||
|
||||
// Check cache first
|
||||
const cached = this.getCachedRelayList(pubkey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Check if already fetching
|
||||
const existingPromise = this.#fetchPromises.get(pubkey);
|
||||
if (existingPromise) {
|
||||
return existingPromise;
|
||||
}
|
||||
|
||||
// Start new fetch
|
||||
const fetchPromise = this.#doFetchRelayList(pubkey);
|
||||
this.#fetchPromises.set(pubkey, fetchPromise);
|
||||
|
||||
try {
|
||||
const result = await fetchPromise;
|
||||
return result;
|
||||
} finally {
|
||||
this.#fetchPromises.delete(pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to fetch a single relay list
|
||||
*/
|
||||
async #doFetchRelayList(pubkey: string): Promise<Nip65Relay[]> {
|
||||
const pool = this.#getPool();
|
||||
|
||||
try {
|
||||
const events = await this.#queryWithTimeout(
|
||||
pool,
|
||||
FALLBACK_PROFILE_RELAYS,
|
||||
[{ kinds: [10002], authors: [pubkey] }],
|
||||
FETCH_TIMEOUT_MS
|
||||
);
|
||||
|
||||
if (events.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get the most recent event (kind 10002 is replaceable)
|
||||
const latestEvent = events.reduce((latest, event) =>
|
||||
event.created_at > latest.created_at ? event : latest
|
||||
);
|
||||
|
||||
// Parse relay tags
|
||||
const relays: Nip65Relay[] = [];
|
||||
for (const tag of latestEvent.tags) {
|
||||
if (tag[0] === 'r' && tag[1]) {
|
||||
const url = tag[1];
|
||||
const marker = tag[2]; // Optional: "read" or "write"
|
||||
|
||||
let read = true;
|
||||
let write = true;
|
||||
|
||||
if (marker === 'read') {
|
||||
write = false;
|
||||
} else if (marker === 'write') {
|
||||
read = false;
|
||||
}
|
||||
// No marker means both read and write
|
||||
|
||||
relays.push({ url, read, write });
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
this.#cache[pubkey] = {
|
||||
pubkey,
|
||||
relays,
|
||||
fetchedAt: Date.now(),
|
||||
};
|
||||
await this.#saveCacheToStorage();
|
||||
|
||||
return relays;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch relay list for ${pubkey}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query relays with a timeout
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async #queryWithTimeout(pool: SimplePool, relays: string[], filters: any[], timeoutMs: number): Promise<any[]> {
|
||||
return new Promise((resolve) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const events: any[] = [];
|
||||
let settled = false;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
resolve(events);
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
const sub = pool.subscribeMany(relays, filters, {
|
||||
onevent(event) {
|
||||
events.push(event);
|
||||
},
|
||||
oneose() {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
sub.close();
|
||||
resolve(events);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache
|
||||
*/
|
||||
async clearCache(): Promise<void> {
|
||||
this.#cache = {};
|
||||
await this.#saveCacheToStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for a specific pubkey
|
||||
*/
|
||||
async clearCacheForPubkey(pubkey: string): Promise<void> {
|
||||
delete this.#cache[pubkey];
|
||||
await this.#saveCacheToStorage();
|
||||
}
|
||||
}
|
||||
@@ -93,3 +93,26 @@ export interface SignerMetaData {
|
||||
|
||||
vaultSnapshots?: SignerMetaData_VaultSnapshot[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached profile metadata from kind 0 events
|
||||
*/
|
||||
export interface ProfileMetadata {
|
||||
pubkey: string;
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
displayName?: string; // Some clients use this instead
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
about?: string;
|
||||
website?: string;
|
||||
nip05?: string;
|
||||
lud06?: string;
|
||||
lud16?: string;
|
||||
fetchedAt: number; // Timestamp when this was fetched
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache for profile metadata, stored in session storage
|
||||
*/
|
||||
export type ProfileMetadataCache = Record<string, ProfileMetadata>;
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,70 +17,130 @@
|
||||
--font-heading: 'reglisse', sans-serif;
|
||||
--font-theylive: 'theylive', sans-serif;
|
||||
|
||||
// Background colors (dark theme based on market)
|
||||
--background: #0a0a0a;
|
||||
--background-light: #131313;
|
||||
--background-light-hover: #1d1d1d;
|
||||
--foreground: #fafafa;
|
||||
|
||||
// Border colors (adapted from market --border: #dac8d3)
|
||||
--border: #3d3d3d;
|
||||
--border-light: #4d4d4d;
|
||||
|
||||
// Primary colors (dark buttons like market, inverted for dark theme)
|
||||
--primary: #fafafa;
|
||||
--primary-hover: #ff3eb5;
|
||||
--primary-foreground: #1d1d1d;
|
||||
--primary-foreground-hover: #ff3eb5;
|
||||
--primary-border: #fafafa;
|
||||
--primary-border-hover: #ff3eb5;
|
||||
|
||||
// Secondary colors (pink accent - the main brand color from market)
|
||||
--secondary: #ff3eb5;
|
||||
--secondary-hover: #0a0a0a;
|
||||
--secondary-foreground: #0a0a0a;
|
||||
--secondary-foreground-hover: #ff3eb5;
|
||||
--secondary-border: #ff3eb5;
|
||||
--secondary-border-hover: #ff3eb5;
|
||||
|
||||
// Focus colors (yellow from market)
|
||||
--focus: #ffd53d;
|
||||
--focus-hover: #0a0a0a;
|
||||
--focus-foreground: #0a0a0a;
|
||||
--focus-foreground-hover: #ffd53d;
|
||||
--focus-border: #ffd53d;
|
||||
--focus-border-hover: #ffd53d;
|
||||
|
||||
// Muted colors
|
||||
--muted: #1d1d1d;
|
||||
--muted-foreground: #666666;
|
||||
|
||||
// Accent colors
|
||||
--accent: #2a2a2a;
|
||||
--accent-foreground: #fafafa;
|
||||
|
||||
// Destructive colors
|
||||
--destructive: #bf4040;
|
||||
--destructive-foreground: #fafafa;
|
||||
|
||||
// Additional brand colors (from market)
|
||||
--off-black: #0a0a0a;
|
||||
--neo-purple: #ff3eb5;
|
||||
--secondary-black: #131313;
|
||||
--tertiary-black: #1d1d1d;
|
||||
--neo-blue: #18b9fe;
|
||||
--neo-yellow: #ffd53d;
|
||||
--light-gray: #ebebeb;
|
||||
--neo-gray: #f8f8f8;
|
||||
|
||||
// Ring color for focus states
|
||||
--ring: #ff3eb5;
|
||||
--input: #996685;
|
||||
|
||||
// Border radius (from market)
|
||||
--radius: 0.25rem;
|
||||
--radius-sm: 2px;
|
||||
--radius-md: 4px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 24px;
|
||||
|
||||
// Primary colors - Pink action buttons (Plebeian Market style)
|
||||
--primary: #ff3eb5;
|
||||
--primary-hover: #e6359f;
|
||||
--primary-foreground: #ffffff;
|
||||
--primary-foreground-hover: #ffffff;
|
||||
--primary-border: #ff3eb5;
|
||||
--primary-border-hover: #e6359f;
|
||||
|
||||
// Secondary colors (pink accent)
|
||||
--secondary: #ff3eb5;
|
||||
--secondary-border: #ff3eb5;
|
||||
--secondary-border-hover: #ff3eb5;
|
||||
|
||||
// Focus colors (yellow from market)
|
||||
--focus: #ffd53d;
|
||||
--focus-border: #ffd53d;
|
||||
--focus-border-hover: #ffd53d;
|
||||
|
||||
// Brand colors
|
||||
--neo-purple: #ff3eb5;
|
||||
--neo-blue: #18b9fe;
|
||||
--neo-yellow: #ffd53d;
|
||||
|
||||
// Ring color for focus states
|
||||
--ring: #ff3eb5;
|
||||
|
||||
// Destructive colors
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DARK THEME (default)
|
||||
// ============================================
|
||||
:root {
|
||||
// Background colors
|
||||
--background: #0a0a0a;
|
||||
--background-light: #131313;
|
||||
--background-light-hover: #1d1d1d;
|
||||
--foreground: #fafafa;
|
||||
|
||||
// Border colors
|
||||
--border: #3d3d3d;
|
||||
--border-light: #4d4d4d;
|
||||
|
||||
// Secondary theme-dependent colors
|
||||
--secondary-hover: #0a0a0a;
|
||||
--secondary-foreground: #0a0a0a;
|
||||
--secondary-foreground-hover: #ff3eb5;
|
||||
|
||||
// Focus theme-dependent colors
|
||||
--focus-hover: #0a0a0a;
|
||||
--focus-foreground: #0a0a0a;
|
||||
--focus-foreground-hover: #ffd53d;
|
||||
|
||||
// Muted colors
|
||||
--muted: #1d1d1d;
|
||||
--muted-foreground: #a1a1a1;
|
||||
|
||||
// Accent colors
|
||||
--accent: #2a2a2a;
|
||||
--accent-foreground: #fafafa;
|
||||
|
||||
// Input colors
|
||||
--input: #2a2a2a;
|
||||
--input-border: #3d3d3d;
|
||||
|
||||
// Additional brand colors
|
||||
--off-black: #0a0a0a;
|
||||
--secondary-black: #131313;
|
||||
--tertiary-black: #1d1d1d;
|
||||
--light-gray: #ebebeb;
|
||||
--neo-gray: #f8f8f8;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LIGHT THEME (follows browser preference)
|
||||
// ============================================
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
// Background colors
|
||||
--background: #ffffff;
|
||||
--background-light: #f5f5f5;
|
||||
--background-light-hover: #ebebeb;
|
||||
--foreground: #0a0a0a;
|
||||
|
||||
// Border colors
|
||||
--border: #e0e0e0;
|
||||
--border-light: #d0d0d0;
|
||||
|
||||
// Secondary theme-dependent colors
|
||||
--secondary-hover: #fce7f3;
|
||||
--secondary-foreground: #ffffff;
|
||||
--secondary-foreground-hover: #ff3eb5;
|
||||
|
||||
// Focus theme-dependent colors
|
||||
--focus-hover: #fef9c3;
|
||||
--focus-foreground: #0a0a0a;
|
||||
--focus-foreground-hover: #ca8a04;
|
||||
|
||||
// Muted colors
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #737373;
|
||||
|
||||
// Accent colors
|
||||
--accent: #f5f5f5;
|
||||
--accent-foreground: #0a0a0a;
|
||||
|
||||
// Input colors
|
||||
--input: #ffffff;
|
||||
--input-border: #d0d0d0;
|
||||
|
||||
// Additional brand colors (inverted for light)
|
||||
--off-black: #ffffff;
|
||||
--secondary-black: #f5f5f5;
|
||||
--tertiary-black: #ebebeb;
|
||||
--light-gray: #404040;
|
||||
--neo-gray: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user