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:
2025-12-17 15:21:57 +01:00
parent fe886d2101
commit 578f3e08ff
39 changed files with 1900 additions and 536 deletions

View File

@@ -3,19 +3,14 @@ import { Router } from '@angular/router';
import {
Identity_DECRYPTED,
NostrHelper,
ProfileMetadata,
ProfileMetadataService,
PubkeyComponent,
StorageService,
ToastComponent,
VisualNip05Pipe,
} from '@common';
import NDK, { NDKUserProfile } from '@nostr-dev-kit/ndk';
interface LoadedData {
profile: NDKUserProfile | undefined;
nip05: string | undefined;
nip05isValidated: boolean | undefined;
validating: boolean;
}
import NDK from '@nostr-dev-kit/ndk';
@Component({
selector: 'app-identity',
@@ -26,20 +21,35 @@ interface LoadedData {
export class IdentityComponent implements OnInit {
selectedIdentity: Identity_DECRYPTED | undefined;
selectedIdentityNpub: string | undefined;
loadedData: LoadedData = {
profile: undefined,
nip05: undefined,
nip05isValidated: undefined,
validating: false,
};
profile: ProfileMetadata | null = null;
nip05isValidated: boolean | undefined;
validating = false;
loading = true;
readonly #storage = inject(StorageService);
readonly #router = inject(Router);
readonly #profileMetadata = inject(ProfileMetadataService);
ngOnInit(): void {
this.#loadData();
}
get displayName(): string | undefined {
return this.#profileMetadata.getDisplayName(this.profile);
}
get username(): string | undefined {
return this.#profileMetadata.getUsername(this.profile);
}
get avatarUrl(): string | undefined {
return this.profile?.picture;
}
get bannerUrl(): string | undefined {
return this.profile?.banner;
}
copyToClipboard(pubkey: string | undefined) {
if (!pubkey) {
return;
@@ -70,6 +80,7 @@ export class IdentityComponent implements OnInit {
);
if (!identity) {
this.loading = false;
return;
}
@@ -77,41 +88,66 @@ export class IdentityComponent implements OnInit {
const pubkey = NostrHelper.pubkeyFromPrivkey(identity.privkey);
this.selectedIdentityNpub = NostrHelper.pubkey2npub(pubkey);
// Determine the user's relays to check for his profile.
// Initialize the profile metadata service (loads cache from storage)
await this.#profileMetadata.initialize();
// Check if we have cached profile data
const cachedProfile = this.#profileMetadata.getCachedProfile(pubkey);
if (cachedProfile) {
this.profile = cachedProfile;
this.loading = false;
// Validate NIP-05 if present (in background)
if (cachedProfile.nip05) {
this.#validateNip05(pubkey, cachedProfile.nip05);
}
return; // Use cached data, don't fetch again
}
// No cached data, fetch from relays
this.loading = true;
const fetchedProfile = await this.#profileMetadata.fetchProfile(pubkey);
if (fetchedProfile) {
this.profile = fetchedProfile;
// Validate NIP-05 if present
if (fetchedProfile.nip05) {
this.#validateNip05(pubkey, fetchedProfile.nip05);
}
}
this.loading = false;
} catch (error) {
console.error(error);
this.loading = false;
}
}
async #validateNip05(pubkey: string, nip05: string) {
try {
this.validating = true;
// Get relays for validation
const relays =
this.#storage
.getBrowserSessionHandler()
.browserSessionData?.relays.filter(
(x) => x.identityId === identity.id
(x) => x.identityId === this.selectedIdentity?.id
) ?? [];
if (relays.length === 0) {
return;
}
const relevantRelays = relays.filter((x) => x.write).map((x) => x.url);
// Fetch the user's profile.
const ndk = new NDK({
explicitRelayUrls: relevantRelays,
});
await ndk.connect();
const user = ndk.getUser({
pubkey: NostrHelper.pubkeyFromPrivkey(identity.privkey),
//relayUrls: relevantRelays,
});
this.loadedData.profile = (await user.fetchProfile()) ?? undefined;
if (this.loadedData.profile?.nip05) {
this.loadedData.validating = true;
this.loadedData.nip05isValidated =
(await user.validateNip05(this.loadedData.profile.nip05)) ??
undefined;
this.loadedData.validating = false;
if (relevantRelays.length > 0) {
const ndk = new NDK({
explicitRelayUrls: relevantRelays,
});
await ndk.connect();
const user = ndk.getUser({ pubkey });
this.nip05isValidated = (await user.validateNip05(nip05)) ?? undefined;
}
this.validating = false;
} catch (error) {
console.error(error);
// TODO
console.error('NIP-05 validation failed:', error);
this.validating = false;
}
}
}