refactor: migrate to applesauce-accounts for proper account management

- Replace manual WebSocket connections with applesauce-accounts
- Use ExtensionAccount.fromExtension() for nostr browser extension integration
- Add AccountsProvider to component tree for account management
- Use useActiveAccount hook to get current user account
- Simplify user display to show formatted public key (profile fetching TODO)
- Remove manual profile fetching WebSocket code
- Improve architecture with proper applesauce account system
- Add applesauce-accounts dependency to package.json

This provides a more robust and standardized approach to nostr account
management using the applesauce ecosystem.
This commit is contained in:
Gigi
2025-10-02 07:24:37 +02:00
parent 496b19f021
commit 0369ece6f4
7 changed files with 52 additions and 145 deletions

2
dist/index.html vendored
View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Markr - Nostr Bookmarks</title> <title>Markr - Nostr Bookmarks</title>
<script type="module" crossorigin src="/assets/index-Dh663TVM.js"></script> <script type="module" crossorigin src="/assets/index-Dgtlx6sm.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D18sZheu.css"> <link rel="stylesheet" crossorigin href="/assets/index-D18sZheu.css">
</head> </head>
<body> <body>

2
node_modules/.package-lock.json generated vendored
View File

@@ -1,6 +1,6 @@
{ {
"name": "markr", "name": "markr",
"version": "0.1.0", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

5
package-lock.json generated
View File

@@ -1,13 +1,14 @@
{ {
"name": "markr", "name": "markr",
"version": "0.1.0", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "markr", "name": "markr",
"version": "0.1.0", "version": "0.0.1",
"dependencies": { "dependencies": {
"applesauce-accounts": "^3.1.0",
"applesauce-core": "^3.1.0", "applesauce-core": "^3.1.0",
"applesauce-react": "^3.1.0", "applesauce-react": "^3.1.0",
"nostr-tools": "^2.4.0", "nostr-tools": "^2.4.0",

View File

@@ -10,6 +10,7 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
}, },
"dependencies": { "dependencies": {
"applesauce-accounts": "^3.1.0",
"applesauce-core": "^3.1.0", "applesauce-core": "^3.1.0",
"applesauce-react": "^3.1.0", "applesauce-react": "^3.1.0",
"nostr-tools": "^2.4.0", "nostr-tools": "^2.4.0",

View File

@@ -1,27 +1,30 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { EventStoreProvider } from 'applesauce-react' import { EventStoreProvider, AccountsProvider } from 'applesauce-react'
import { EventStore } from 'applesauce-core' import { EventStore } from 'applesauce-core'
import { AccountManager } from 'applesauce-accounts'
import Login from './components/Login' import Login from './components/Login'
import Bookmarks from './components/Bookmarks' import Bookmarks from './components/Bookmarks'
function App() { function App() {
const [eventStore, setEventStore] = useState<EventStore | null>(null) const [eventStore, setEventStore] = useState<EventStore | null>(null)
const [accountManager, setAccountManager] = useState<AccountManager | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState(false) const [isAuthenticated, setIsAuthenticated] = useState(false)
const [userPublicKey, setUserPublicKey] = useState<string | null>(null)
const [userProfile, setUserProfile] = useState<{name?: string, username?: string, nip05?: string} | null>(null)
useEffect(() => { useEffect(() => {
// Initialize event store // Initialize event store and account manager
const store = new EventStore() const store = new EventStore()
const accounts = new AccountManager()
setEventStore(store) setEventStore(store)
setAccountManager(accounts)
}, []) }, [])
if (!eventStore) { if (!eventStore || !accountManager) {
return <div>Loading...</div> return <div>Loading...</div>
} }
return ( return (
<EventStoreProvider eventStore={eventStore}> <EventStoreProvider eventStore={eventStore}>
<AccountsProvider manager={accountManager}>
<div className="app"> <div className="app">
<header> <header>
<h1>Markr</h1> <h1>Markr</h1>
@@ -29,23 +32,12 @@ function App() {
</header> </header>
{!isAuthenticated ? ( {!isAuthenticated ? (
<Login onLogin={(publicKey, profile) => { <Login onLogin={() => setIsAuthenticated(true)} />
setIsAuthenticated(true)
setUserPublicKey(publicKey)
setUserProfile(profile)
}} />
) : ( ) : (
<Bookmarks <Bookmarks onLogout={() => setIsAuthenticated(false)} />
userPublicKey={userPublicKey}
userProfile={userProfile}
onLogout={() => {
setIsAuthenticated(false)
setUserPublicKey(null)
setUserProfile(null)
}}
/>
)} )}
</div> </div>
</AccountsProvider>
</EventStoreProvider> </EventStoreProvider>
) )
} }

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useContext } from 'react' import { useState, useEffect, useContext } from 'react'
import { EventStoreContext } from 'applesauce-react' import { EventStoreContext, Hooks } from 'applesauce-react'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
interface Bookmark { interface Bookmark {
@@ -11,31 +11,24 @@ interface Bookmark {
tags: string[][] tags: string[][]
} }
interface UserProfile {
name?: string
username?: string
nip05?: string
}
interface BookmarksProps { interface BookmarksProps {
userPublicKey: string | null
userProfile: UserProfile | null
onLogout: () => void onLogout: () => void
} }
const Bookmarks: React.FC<BookmarksProps> = ({ userPublicKey, userProfile, onLogout }) => { const Bookmarks: React.FC<BookmarksProps> = ({ onLogout }) => {
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]) const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const eventStore = useContext(EventStoreContext) const eventStore = useContext(EventStoreContext)
const activeAccount = Hooks.useActiveAccount()
useEffect(() => { useEffect(() => {
if (eventStore && userPublicKey) { if (eventStore && activeAccount) {
fetchBookmarks() fetchBookmarks()
} }
}, [eventStore, userPublicKey]) }, [eventStore, activeAccount])
const fetchBookmarks = async () => { const fetchBookmarks = async () => {
if (!eventStore || !userPublicKey) return if (!eventStore || !activeAccount) return
try { try {
setLoading(true) setLoading(true)
@@ -46,7 +39,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ userPublicKey, userProfile, onLog
const events = eventStore.getByFilters([ const events = eventStore.getByFilters([
{ {
kinds: [10003, 30003], kinds: [10003, 30003],
authors: [userPublicKey] authors: [activeAccount.pubkey]
} }
]) ])
@@ -111,23 +104,12 @@ const Bookmarks: React.FC<BookmarksProps> = ({ userPublicKey, userProfile, onLog
return new Date(timestamp * 1000).toLocaleDateString() return new Date(timestamp * 1000).toLocaleDateString()
} }
const formatUserDisplay = (profile: UserProfile | null, publicKey: string | null) => { const formatUserDisplay = () => {
if (!profile || (!profile.name && !profile.username && !profile.nip05)) { if (!activeAccount) return 'Unknown User'
return publicKey ? `${publicKey.slice(0, 8)}...${publicKey.slice(-8)}` : 'Unknown User'
}
// Priority: NIP-05 > name > username // For now, just show the formatted public key
if (profile.nip05) { // TODO: Implement profile fetching through applesauce system
return profile.nip05 return `${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}`
}
if (profile.name) {
return profile.name
}
if (profile.username) {
return `@${profile.username}`
}
return publicKey ? `${publicKey.slice(0, 8)}...${publicKey.slice(-8)}` : 'Unknown User'
} }
if (loading) { if (loading) {
@@ -136,8 +118,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ userPublicKey, userProfile, onLog
<div className="bookmarks-header"> <div className="bookmarks-header">
<div> <div>
<h2>Your Bookmarks</h2> <h2>Your Bookmarks</h2>
{userPublicKey && ( {activeAccount && (
<p className="user-info">Logged in as: {formatUserDisplay(userProfile, userPublicKey)}</p> <p className="user-info">Logged in as: {formatUserDisplay()}</p>
)} )}
</div> </div>
<button onClick={onLogout} className="logout-button"> <button onClick={onLogout} className="logout-button">
@@ -154,8 +136,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ userPublicKey, userProfile, onLog
<div className="bookmarks-header"> <div className="bookmarks-header">
<div> <div>
<h2>Your Bookmarks ({bookmarks.length})</h2> <h2>Your Bookmarks ({bookmarks.length})</h2>
{userPublicKey && ( {activeAccount && (
<p className="user-info">Logged in as: {formatUserDisplay(userProfile, userPublicKey)}</p> <p className="user-info">Logged in as: {formatUserDisplay()}</p>
)} )}
</div> </div>
<button onClick={onLogout} className="logout-button"> <button onClick={onLogout} className="logout-button">

View File

@@ -1,93 +1,24 @@
import { useState } from 'react' import { useState } from 'react'
import { Hooks } from 'applesauce-react'
interface UserProfile { import { Accounts } from 'applesauce-accounts'
name?: string
username?: string
nip05?: string
}
interface LoginProps { interface LoginProps {
onLogin: (publicKey: string, profile: UserProfile) => void onLogin: () => void
} }
const Login: React.FC<LoginProps> = ({ onLogin }) => { const Login: React.FC<LoginProps> = ({ onLogin }) => {
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const accountManager = Hooks.useAccountManager()
const fetchUserProfile = async (publicKey: string): Promise<UserProfile> => {
try {
// Create a simple relay connection to fetch profile
const relay = new WebSocket('wss://relay.damus.io')
return new Promise((resolve) => {
const timeout = setTimeout(() => {
relay.close()
resolve({}) // Return empty profile if timeout
}, 5000)
relay.onopen = () => {
// Request profile event (kind 0)
relay.send(JSON.stringify([
"REQ",
"profile",
{
"kinds": [0],
"authors": [publicKey]
}
]))
}
relay.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
if (data[0] === 'EVENT' && data[2]?.kind === 0) {
const profileEvent = data[2]
const profileContent = JSON.parse(profileEvent.content || '{}')
clearTimeout(timeout)
relay.close()
resolve({
name: profileContent.name,
username: profileContent.username,
nip05: profileContent.nip05
})
}
} catch (error) {
console.error('Error parsing profile:', error)
}
}
relay.onerror = () => {
clearTimeout(timeout)
relay.close()
resolve({}) // Return empty profile on error
}
})
} catch (error) {
console.error('Error fetching profile:', error)
return {}
}
}
const handleLogin = async () => { const handleLogin = async () => {
try { try {
setIsConnecting(true) setIsConnecting(true)
// Check if nostr is available in the browser // Create account from nostr extension
if (!window.nostr) { const account = await Accounts.ExtensionAccount.fromExtension()
throw new Error('Nostr extension not found. Please install a nostr browser extension.') accountManager.addAccount(account)
} accountManager.setActive(account)
onLogin()
// Request public key from nostr extension
const publicKey = await window.nostr.getPublicKey()
if (publicKey) {
// Fetch user profile from nostr
const profile = await fetchUserProfile(publicKey)
onLogin(publicKey, profile)
} else {
throw new Error('Failed to get public key')
}
} catch (error) { } catch (error) {
console.error('Login failed:', error) console.error('Login failed:', error)
alert('Login failed. Please install a nostr browser extension and try again.') alert('Login failed. Please install a nostr browser extension and try again.')