Load user profile from Nostr, display name and avatar
This commit is contained in:
@@ -7,10 +7,13 @@ import Icon from '#components/icon';
|
|||||||
import UserMenu from '#components/user-menu';
|
import UserMenu from '#components/user-menu';
|
||||||
import SearchBox from '#components/search-box';
|
import SearchBox from '#components/search-box';
|
||||||
import CategoryChips from '#components/category-chips';
|
import CategoryChips from '#components/category-chips';
|
||||||
|
import { and } from 'ember-truth-helpers';
|
||||||
|
|
||||||
export default class AppHeaderComponent extends Component {
|
export default class AppHeaderComponent extends Component {
|
||||||
@service storage;
|
@service storage;
|
||||||
@service settings;
|
@service settings;
|
||||||
|
@service nostrAuth;
|
||||||
|
@service nostrData;
|
||||||
@tracked isUserMenuOpen = false;
|
@tracked isUserMenuOpen = false;
|
||||||
@tracked searchQuery = '';
|
@tracked searchQuery = '';
|
||||||
|
|
||||||
@@ -64,9 +67,19 @@ export default class AppHeaderComponent extends Component {
|
|||||||
aria-label="User Menu"
|
aria-label="User Menu"
|
||||||
{{on "click" this.toggleUserMenu}}
|
{{on "click" this.toggleUserMenu}}
|
||||||
>
|
>
|
||||||
<div class="user-avatar-placeholder">
|
{{#if
|
||||||
<Icon @name="user" @size={{20}} @color="white" />
|
(and this.nostrAuth.isConnected this.nostrData.profile.picture)
|
||||||
</div>
|
}}
|
||||||
|
<img
|
||||||
|
src={{this.nostrData.profile.picture}}
|
||||||
|
class="user-avatar"
|
||||||
|
alt="User Avatar"
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<div class="user-avatar-placeholder">
|
||||||
|
<Icon @name="user" @size={{20}} @color="white" />
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{{#if this.isUserMenuOpen}}
|
{{#if this.isUserMenuOpen}}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default class UserMenuComponent extends Component {
|
|||||||
@service storage;
|
@service storage;
|
||||||
@service osmAuth;
|
@service osmAuth;
|
||||||
@service nostrAuth;
|
@service nostrAuth;
|
||||||
|
@service nostrData;
|
||||||
|
|
||||||
@tracked isNostrConnectModalOpen = false;
|
@tracked isNostrConnectModalOpen = false;
|
||||||
|
|
||||||
@@ -135,7 +136,7 @@ export default class UserMenuComponent extends Component {
|
|||||||
<div class="account-status">
|
<div class="account-status">
|
||||||
{{#if this.nostrAuth.isConnected}}
|
{{#if this.nostrAuth.isConnected}}
|
||||||
<strong title={{this.nostrAuth.pubkey}}>
|
<strong title={{this.nostrAuth.pubkey}}>
|
||||||
{{this.nostrAuth.pubkey}}
|
{{this.nostrData.userDisplayName}}
|
||||||
</strong>
|
</strong>
|
||||||
{{else}}
|
{{else}}
|
||||||
Not connected
|
Not connected
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const DEFAULT_CONNECT_RELAY = 'wss://relay.nsec.app';
|
|||||||
|
|
||||||
export default class NostrAuthService extends Service {
|
export default class NostrAuthService extends Service {
|
||||||
@service nostrRelay;
|
@service nostrRelay;
|
||||||
|
@service nostrData;
|
||||||
|
|
||||||
@tracked pubkey = null;
|
@tracked pubkey = null;
|
||||||
@tracked signerType = null; // 'extension' or 'connect'
|
@tracked signerType = null; // 'extension' or 'connect'
|
||||||
@@ -56,6 +57,7 @@ export default class NostrAuthService extends Service {
|
|||||||
this.pubkey = extensionPubkey;
|
this.pubkey = extensionPubkey;
|
||||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||||
}
|
}
|
||||||
|
this.nostrData.loadProfile(this.pubkey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to verify extension nostr pubkey, logging out', e);
|
console.warn('Failed to verify extension nostr pubkey, logging out', e);
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
@@ -112,6 +114,7 @@ export default class NostrAuthService extends Service {
|
|||||||
this.signerType = 'extension';
|
this.signerType = 'extension';
|
||||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||||
localStorage.setItem(STORAGE_KEY_TYPE, 'extension');
|
localStorage.setItem(STORAGE_KEY_TYPE, 'extension');
|
||||||
|
this.nostrData.loadProfile(this.pubkey);
|
||||||
return this.pubkey;
|
return this.pubkey;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get public key from extension:', 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._signerInstance.remote
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.nostrData.loadProfile(this.pubkey);
|
||||||
|
|
||||||
return this.pubkey;
|
return this.pubkey;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.connectStatus = null;
|
this.connectStatus = null;
|
||||||
@@ -253,6 +258,7 @@ export default class NostrAuthService extends Service {
|
|||||||
if (pubkey !== this.pubkey) {
|
if (pubkey !== this.pubkey) {
|
||||||
throw new Error('Remote signer pubkey mismatch');
|
throw new Error('Remote signer pubkey mismatch');
|
||||||
}
|
}
|
||||||
|
this.nostrData.loadProfile(this.pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async signEvent(event) {
|
async signEvent(event) {
|
||||||
@@ -266,6 +272,7 @@ export default class NostrAuthService extends Service {
|
|||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
this.pubkey = null;
|
this.pubkey = null;
|
||||||
|
this.nostrData?.loadProfile(null);
|
||||||
this.signerType = null;
|
this.signerType = null;
|
||||||
this.connectStatus = null;
|
this.connectStatus = null;
|
||||||
this.connectUri = null;
|
this.connectUri = null;
|
||||||
|
|||||||
158
app/services/nostr-data.js
Normal file
158
app/services/nostr-data.js
Normal file
@@ -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<NostrEvent>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -190,7 +190,16 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 */
|
/* User Menu Popover */
|
||||||
|
|||||||
Reference in New Issue
Block a user