diff --git a/app/components/app-header.gjs b/app/components/app-header.gjs
index cbc771c..91ac29e 100644
--- a/app/components/app-header.gjs
+++ b/app/components/app-header.gjs
@@ -7,10 +7,13 @@ import Icon from '#components/icon';
import UserMenu from '#components/user-menu';
import SearchBox from '#components/search-box';
import CategoryChips from '#components/category-chips';
+import { and } from 'ember-truth-helpers';
export default class AppHeaderComponent extends Component {
@service storage;
@service settings;
+ @service nostrAuth;
+ @service nostrData;
@tracked isUserMenuOpen = false;
@tracked searchQuery = '';
@@ -64,9 +67,19 @@ export default class AppHeaderComponent extends Component {
aria-label="User Menu"
{{on "click" this.toggleUserMenu}}
>
-
-
-
+ {{#if
+ (and this.nostrAuth.isConnected this.nostrData.profile.picture)
+ }}
+
+ {{else}}
+
+
+
+ {{/if}}
{{#if this.isUserMenuOpen}}
diff --git a/app/components/user-menu.gjs b/app/components/user-menu.gjs
index 93be100..6b92d66 100644
--- a/app/components/user-menu.gjs
+++ b/app/components/user-menu.gjs
@@ -11,6 +11,7 @@ export default class UserMenuComponent extends Component {
@service storage;
@service osmAuth;
@service nostrAuth;
+ @service nostrData;
@tracked isNostrConnectModalOpen = false;
@@ -135,7 +136,7 @@ export default class UserMenuComponent extends Component {
{{#if this.nostrAuth.isConnected}}
- {{this.nostrAuth.pubkey}}
+ {{this.nostrData.userDisplayName}}
{{else}}
Not connected
diff --git a/app/services/nostr-auth.js b/app/services/nostr-auth.js
index ccbb4b4..cce17d4 100644
--- a/app/services/nostr-auth.js
+++ b/app/services/nostr-auth.js
@@ -16,6 +16,7 @@ const DEFAULT_CONNECT_RELAY = 'wss://relay.nsec.app';
export default class NostrAuthService extends Service {
@service nostrRelay;
+ @service nostrData;
@tracked pubkey = null;
@tracked signerType = null; // 'extension' or 'connect'
@@ -56,6 +57,7 @@ export default class NostrAuthService extends Service {
this.pubkey = extensionPubkey;
localStorage.setItem(STORAGE_KEY, this.pubkey);
}
+ this.nostrData.loadProfile(this.pubkey);
} catch (e) {
console.warn('Failed to verify extension nostr pubkey, logging out', e);
this.disconnect();
@@ -112,6 +114,7 @@ export default class NostrAuthService extends Service {
this.signerType = 'extension';
localStorage.setItem(STORAGE_KEY, this.pubkey);
localStorage.setItem(STORAGE_KEY_TYPE, 'extension');
+ this.nostrData.loadProfile(this.pubkey);
return this.pubkey;
} catch (error) {
console.error('Failed to get public key from extension:', error);
@@ -207,6 +210,8 @@ export default class NostrAuthService extends Service {
this._signerInstance.remote
);
+ this.nostrData.loadProfile(this.pubkey);
+
return this.pubkey;
} catch (error) {
this.connectStatus = null;
@@ -253,6 +258,7 @@ export default class NostrAuthService extends Service {
if (pubkey !== this.pubkey) {
throw new Error('Remote signer pubkey mismatch');
}
+ this.nostrData.loadProfile(this.pubkey);
}
async signEvent(event) {
@@ -266,6 +272,7 @@ export default class NostrAuthService extends Service {
async disconnect() {
this.pubkey = null;
+ this.nostrData?.loadProfile(null);
this.signerType = null;
this.connectStatus = null;
this.connectUri = null;
diff --git a/app/services/nostr-data.js b/app/services/nostr-data.js
new file mode 100644
index 0000000..adf99e9
--- /dev/null
+++ b/app/services/nostr-data.js
@@ -0,0 +1,158 @@
+import Service, { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { EventStore } from 'applesauce-core/event-store';
+import { ProfileModel } from 'applesauce-core/models/profile';
+import { MailboxesModel } from 'applesauce-core/models/mailboxes';
+import { npubEncode } from 'applesauce-core/helpers/pointers';
+
+const BOOTSTRAP_RELAYS = [
+ 'wss://purplepag.es',
+ 'wss://relay.damus.io',
+ 'wss://nos.lol',
+];
+
+export default class NostrDataService extends Service {
+ @service nostrRelay;
+ @service nostrAuth;
+
+ store = new EventStore();
+
+ @tracked profile = null;
+ @tracked mailboxes = null;
+ @tracked blossomServers = [];
+
+ _profileSub = null;
+ _mailboxesSub = null;
+ _blossomSub = null;
+
+ _requestSub = null;
+
+ constructor() {
+ super(...arguments);
+
+ // Feed events from the relay pool into the event store
+ this.nostrRelay.pool.relays$.subscribe(() => {
+ // Setup relay subscription tracking if needed, or we just rely on request()
+ // which returns an Observable
+ });
+ }
+
+ async loadProfile(pubkey) {
+ if (!pubkey) return;
+
+ // Reset state
+ this.profile = null;
+ this.mailboxes = null;
+ this.blossomServers = [];
+
+ this._cleanupSubscriptions();
+
+ const relays = new Set(BOOTSTRAP_RELAYS);
+
+ // Try to get extension relays
+ if (typeof window.nostr !== 'undefined' && window.nostr.getRelays) {
+ try {
+ const extRelays = await window.nostr.getRelays();
+ for (const url of Object.keys(extRelays)) {
+ relays.add(url);
+ }
+ } catch {
+ console.warn('Failed to get NIP-07 relays');
+ }
+ }
+
+ const relayList = Array.from(relays);
+
+ // Request events and dump them into the store
+ this._requestSub = this.nostrRelay.pool
+ .request(relayList, [
+ {
+ authors: [pubkey],
+ kinds: [0, 10002, 10063],
+ },
+ ])
+ .subscribe({
+ next: (event) => {
+ this.store.add(event);
+ },
+ error: (err) => {
+ console.error('Error fetching profile events:', err);
+ },
+ });
+
+ // Setup models to track state reactively
+ this._profileSub = this.store
+ .model(ProfileModel, pubkey)
+ .subscribe((profileContent) => {
+ this.profile = profileContent;
+ });
+
+ this._mailboxesSub = this.store
+ .model(MailboxesModel, pubkey)
+ .subscribe((mailboxesData) => {
+ this.mailboxes = mailboxesData;
+ });
+
+ this._blossomSub = this.store
+ .replaceable(10063, pubkey)
+ .subscribe((event) => {
+ if (event && event.tags) {
+ this.blossomServers = event.tags
+ .filter((t) => t[0] === 'server' && t[1])
+ .map((t) => t[1]);
+ } else {
+ this.blossomServers = [];
+ }
+ });
+ }
+
+ get userDisplayName() {
+ if (this.profile) {
+ if (this.profile.nip05) {
+ return this.profile.nip05;
+ }
+ if (this.profile.displayName || this.profile.display_name) {
+ return this.profile.displayName || this.profile.display_name;
+ }
+ if (this.profile.name) {
+ return this.profile.name;
+ }
+ }
+
+ // Fallback to npub
+ if (this.nostrAuth.pubkey) {
+ try {
+ const npub = npubEncode(this.nostrAuth.pubkey);
+ return `${npub.slice(0, 9)}...${npub.slice(-4)}`;
+ } catch {
+ return this.nostrAuth.pubkey;
+ }
+ }
+
+ return 'Not connected';
+ }
+
+ _cleanupSubscriptions() {
+ if (this._requestSub) {
+ this._requestSub.unsubscribe();
+ this._requestSub = null;
+ }
+ if (this._profileSub) {
+ this._profileSub.unsubscribe();
+ this._profileSub = null;
+ }
+ if (this._mailboxesSub) {
+ this._mailboxesSub.unsubscribe();
+ this._mailboxesSub = null;
+ }
+ if (this._blossomSub) {
+ this._blossomSub.unsubscribe();
+ this._blossomSub = null;
+ }
+ }
+
+ willDestroy() {
+ super.willDestroy(...arguments);
+ this._cleanupSubscriptions();
+ }
+}
diff --git a/app/styles/app.css b/app/styles/app.css
index 17591ee..d5891b3 100644
--- a/app/styles/app.css
+++ b/app/styles/app.css
@@ -190,7 +190,16 @@ body {
display: flex;
align-items: center;
justify-content: center;
- box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
+ flex-shrink: 0;
+}
+
+.user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ object-fit: cover;
+ flex-shrink: 0;
+}
}
/* User Menu Popover */