Basic Profile page

This commit is contained in:
styppo 2023-01-09 22:24:24 +00:00
parent 533f5e662a
commit 3c66a50046
No known key found for this signature in database
GPG Key ID: 3AAA685C50724C28
15 changed files with 347 additions and 98 deletions

View File

@ -83,6 +83,7 @@ export default {
.create-post-dialog { .create-post-dialog {
position: relative; position: relative;
background-color: $color-bg; background-color: $color-bg;
box-shadow: $shadow-white;
padding: 3rem 1rem 1rem; padding: 3rem 1rem 1rem;
min-width: 660px; min-width: 660px;
.icon { .icon {

View File

@ -20,7 +20,7 @@
<MenuItem <MenuItem
v-if="!hideItemsRequiringSignIn || app.isSignedIn" v-if="!hideItemsRequiringSignIn || app.isSignedIn"
icon="profile" icon="profile"
:to="`/profile/${app.myPubkey}`" :to="`/profile/${hexToBech32(app.myPubkey, 'npub')}`"
:enabled="app.isSignedIn" :enabled="app.isSignedIn"
@click="$emit('mobile-menu-close')" @click="$emit('mobile-menu-close')"
> >
@ -71,6 +71,7 @@ import ProfilePopup from 'components/MainMenu/ProfilePopup'
import Logo from 'components/Logo.vue' import Logo from 'components/Logo.vue'
import {useAppStore} from 'stores/App' import {useAppStore} from 'stores/App'
import {MENU_ITEMS} from 'components/MainMenu/constants.js' import {MENU_ITEMS} from 'components/MainMenu/constants.js'
import {hexToBech32} from 'src/utils/utils'
export default { export default {
name: 'MainMenu', name: 'MainMenu',
@ -105,7 +106,8 @@ export default {
signIn() { signIn() {
this.$emit('mobile-menu-close') this.$emit('mobile-menu-close')
this.app.signIn() this.app.signIn()
} },
hexToBech32
} }
} }
</script> </script>

View File

@ -48,7 +48,7 @@ export default defineComponent({
methods: { methods: {
titleFromRoute() { titleFromRoute() {
const route = this.$route.name?.toLowerCase() const route = this.$route.name?.toLowerCase()
return route.charAt(0).toUpperCase() + route.substring(1) return route?.charAt(0).toUpperCase() + route?.substring(1)
} }
} }
}) })

View File

@ -1,6 +1,8 @@
<template> <template>
<q-dialog v-model="dialogOpen"> <q-dialog v-model="dialogOpen">
<div class="logout-dialog"> <div class="logout-dialog">
<q-btn icon="close" size="md" class="icon" flat round v-close-popup/>
<h3>Log out from <UserName :pubkey="pubkey" /></h3> <h3>Log out from <UserName :pubkey="pubkey" /></h3>
<p> <p>
Do you really want to log out from <UserName :pubkey="pubkey" />? Do you really want to log out from <UserName :pubkey="pubkey" />?
@ -77,11 +79,25 @@ export default {
padding: 1rem; padding: 1rem;
min-width: 440px; min-width: 440px;
.icon {
position: absolute;
width: 16px;
height: 16px;
top: .5rem;
left: .5rem;
fill: #fff;
}
h3 { h3 {
margin-top: 0; margin-top: 3rem;
padding: 0 .5rem;
}
> p {
padding: 0 .5rem;
} }
.warning { .warning {
color: $color-primary;
display: flex; display: flex;
background-color: rgba($color: $color-dark-gray, $alpha: 0.1); background-color: rgba($color: $color-dark-gray, $alpha: 0.1);
border-radius: 1rem 1rem 0 0; border-radius: 1rem 1rem 0 0;
@ -102,13 +118,9 @@ export default {
padding: 12px 20px; padding: 12px 20px;
border-radius: 0 0 1rem 1rem; border-radius: 0 0 1rem 1rem;
margin: 0 auto 1rem; margin: 0 auto 1rem;
outline: none;
background-color: rgba($color: $color-dark-gray, $alpha: 0.3); background-color: rgba($color: $color-dark-gray, $alpha: 0.3);
border: 1px solid rgba($color: $color-dark-gray, $alpha: 0); border: 1px solid rgba($color: $color-dark-gray, $alpha: 0);
transition: border 150ms ease;
&:focus {
border: 1px solid rgba($color: $color-primary, $alpha: 1);
outline: none;
}
} }
.buttons { .buttons {

View File

@ -1,8 +1,8 @@
<template> <template>
<q-avatar <q-avatar
@click="clickable && linkToProfile(pubkey)"
class="relative-position"
:class="{'cursor-pointer': clickable}" :class="{'cursor-pointer': clickable}"
:size="size"
@click="clickable && linkToProfile(pubkey)"
> >
<img <img
v-if="hasAvatar && !avatarFetchFailed" v-if="hasAvatar && !avatarFetchFailed"
@ -38,6 +38,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
size: {
type: String,
default: '',
}
}, },
data() { data() {
return { return {

View File

@ -0,0 +1,36 @@
<template>
<div class="user-card">
<UserAvatar :pubkey="pubkey" :clickable="clickable" />
<UserName :pubkey="pubkey" :clickable="clickable" two-line />
</div>
</template>
<script>
import UserAvatar from 'components/User/UserAvatar.vue'
import UserName from 'components/User/UserName.vue'
export default {
name: 'UserCard',
components: {UserName, UserAvatar},
props: {
pubkey: {
type: String,
required: true,
},
clickable: {
type: Boolean,
default: false,
}
}
}
</script>
<style lang="scss" scoped>
.user-card {
display: flex;
margin: 1rem;
* + * {
margin-left: 1rem;
}
}
</style>

View File

@ -4,17 +4,12 @@
:class="{'two-line': twoLine, clickable}" :class="{'two-line': twoLine, clickable}"
> >
<a @click="clickable && linkToProfile(pubkey)"> <a @click="clickable && linkToProfile(pubkey)">
<span <span v-if="profile?.name" class="name">{{ profile.name }}</span>
v-if="profile?.name" <!-- <q-icon v-if="showFollowing && isFollow" name="visibility" color="secondary">-->
class="name" <!-- <q-tooltip>-->
> <!-- following-->
{{ profile.name }} <!-- </q-tooltip>-->
<q-icon v-if="showFollowing && isFollow" name="visibility" color="secondary"> <!-- </q-icon>-->
<q-tooltip>
following
</q-tooltip>
</q-icon>
</span>
<Bech32Label v-if="twoLine || !profile?.name" prefix="npub" :hex="pubkey" class="pubkey" /> <Bech32Label v-if="twoLine || !profile?.name" prefix="npub" :hex="pubkey" class="pubkey" />
</a> </a>
@ -88,7 +83,6 @@ export default {
@import "assets/theme/colors.scss"; @import "assets/theme/colors.scss";
.username { .username {
cursor: pointer;
.name { .name {
font-weight: bold; font-weight: bold;
} }
@ -109,6 +103,7 @@ export default {
} }
} }
&.clickable { &.clickable {
cursor: pointer;
.name:hover { .name:hover {
text-decoration: underline; text-decoration: underline;
} }

34
src/nostr/Account.js Normal file
View File

@ -0,0 +1,34 @@
import {getEventHash, getPublicKey, signEvent} from 'nostr-tools'
import Nip07 from 'src/utils/Nip07'
export class Account {
constructor(opts) {
this.pubkey = opts.pubkey || null
this.privkey = opts.privkey || null
this.useExtension = opts.useExtension || false
if (this.privkey && !this.pubkey) {
this.pubkey = getPublicKey(this.privkey)
}
if (!this.pubkey) throw new Error('pubkey is required')
}
canSign() {
return !!this.privkey || (this.useExtension && Nip07.isAvailable())
}
async sign(event) {
event.id = getEventHash(event)
if (this.privkey) {
event.sig = signEvent(event, this.privkey)
} else if (this.useExtension && Nip07.isAvailable()) {
let {sig} = await Nip07.signEvent(event)
event.sig = sig
} else {
// TODO
throw new Error('cannot sign')
}
return event
}
}

View File

@ -8,7 +8,7 @@ export default class FetchQueue extends Observable {
this.fnGetId = fnGetId this.fnGetId = fnGetId
this.fnCreateFilter = fnCreateFilter this.fnCreateFilter = fnCreateFilter
this.throttle = opts.throttle || 250 this.throttle = opts.throttle || 250
this.batchSize = opts.batchSize || 20 this.batchSize = opts.batchSize || 50
this.retryDelay = opts.retryDelay || 3000 this.retryDelay = opts.retryDelay || 3000
this.maxRetries = opts.maxRetries || 3 this.maxRetries = opts.maxRetries || 3
@ -41,7 +41,7 @@ export default class FetchQueue extends Observable {
const ids = Object.keys(this.queue).slice(0, this.batchSize) const ids = Object.keys(this.queue).slice(0, this.batchSize)
if (!ids.length) return if (!ids.length) return
console.log(`Fetching ${ids.length} ${this.subId}s`, ids) console.log(`Fetching ${ids.length}/${Object.keys(this.queue).length} ${this.subId}s`, ids)
// Remove ids that we have tried too many times. // Remove ids that we have tried too many times.
const filteredIds = [] const filteredIds = []

View File

@ -1,24 +1,18 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {markRaw} from 'vue'
import {EventKind} from 'src/nostr/model/Event' import {EventKind} from 'src/nostr/model/Event'
import NostrClient from 'src/nostr/NostrClient' import NostrClient from 'src/nostr/NostrClient'
import FetchQueue from 'src/nostr/FetchQueue'
import {NoteOrder, useNoteStore} from 'src/nostr/store/NoteStore' import {NoteOrder, useNoteStore} from 'src/nostr/store/NoteStore'
import {useProfileStore} from 'src/nostr/store/ProfileStore' import {useProfileStore} from 'src/nostr/store/ProfileStore'
import {markRaw} from 'vue' import {useContactStore} from 'src/nostr/store/ContactStore'
import FetchQueue from 'src/nostr/FetchQueue' import {useSettingsStore} from 'stores/Settings'
// TODO Move to settings
const RELAYS = [
'wss://relay.damus.io',
'wss://nostr-relay.wlvs.space',
'wss://nostr-pub.wellorder.net',
'wss://nostr.oxtr.dev',
]
export const Feeds = { export const Feeds = {
GLOBAL: { GLOBAL: {
name: 'global', name: 'global',
filters: { filters: {
kinds: [EventKind.NOTE, EventKind.DELETE], kinds: [EventKind.NOTE], // TODO Deletions
}, },
initialFetchSize: 100, initialFetchSize: 100,
}, },
@ -43,6 +37,16 @@ const profileQueue = client => new FetchQueue(
}) })
) )
const contactQueue = client => new FetchQueue(
client,
'contact',
event => event.pubkey,
pubkeys => ({
kinds: [EventKind.CONTACT],
authors: pubkeys
})
)
export const useNostrStore = defineStore('nostr', { export const useNostrStore = defineStore('nostr', {
state: () => ({ state: () => ({
// TODO Limit size. Remove oldest. // TODO Limit size. Remove oldest.
@ -50,7 +54,9 @@ export const useNostrStore = defineStore('nostr', {
}), }),
actions: { actions: {
init() { init() {
this.client = markRaw(new NostrClient(RELAYS)) const settings = useSettingsStore()
console.log(settings.relays)
this.client = markRaw(new NostrClient(settings.relays))
this.client.connect() this.client.connect()
this.profileQueue = profileQueue(this.client) this.profileQueue = profileQueue(this.client)
@ -58,6 +64,9 @@ export const useNostrStore = defineStore('nostr', {
this.noteQueue = eventQueue(this.client, 'note') this.noteQueue = eventQueue(this.client, 'note')
this.noteQueue.on('event', this.addEvent.bind(this)) this.noteQueue.on('event', this.addEvent.bind(this))
this.contactQueue = contactQueue(this.client, 'queue')
this.contactQueue.on('event', this.addEvent.bind(this))
}, },
addEvent(event, relay = null) { addEvent(event, relay = null) {
// console.log(`[EVENT] from ${relay}`, event) // console.log(`[EVENT] from ${relay}`, event)
@ -83,8 +92,10 @@ export const useNostrStore = defineStore('nostr', {
} }
case EventKind.RELAY: case EventKind.RELAY:
break break
case EventKind.CONTACT: case EventKind.CONTACT: {
break const contacts = useContactStore()
return contacts.addEvent(event)
}
case EventKind.DM: case EventKind.DM:
break break
case EventKind.DELETE: case EventKind.DELETE:
@ -145,6 +156,31 @@ export const useNostrStore = defineStore('nostr', {
) )
}, },
getContacts(pubkey) {
const store = useContactStore()
const contacts = store.getContacts(pubkey)
if (!contacts) this.contactQueue.add(pubkey)
return contacts
},
getFollowers(pubkey) {
const store = useContactStore()
const followers = store.getFollowers(pubkey)
// TODO fetch
return followers
},
fetchFollowers(pubkey, opts = {}) {
const limit = opts.limit || 500
return this.fetchMultiple(
{
kinds: [EventKind.CONTACT],
'#p': [pubkey],
},
limit
)
},
streamThread(rootId, eventCallback, initialFetchCompleteCallback) { streamThread(rootId, eventCallback, initialFetchCompleteCallback) {
return this.streamEvents( return this.streamEvents(
{ {
@ -199,6 +235,7 @@ export const useNostrStore = defineStore('nostr', {
fetchMultiple(filters, limit = 100) { fetchMultiple(filters, limit = 100) {
const filtersWithLimit = Object.assign({}, filters, {limit}) const filtersWithLimit = Object.assign({}, filters, {limit})
let unsubscribeTimeout
return new Promise(resolve => { return new Promise(resolve => {
const objects = [] const objects = []
this.client.subscribe( this.client.subscribe(
@ -216,8 +253,11 @@ export const useNostrStore = defineStore('nostr', {
}, },
{ {
eoseCallback: (_relay, subId) => { eoseCallback: (_relay, subId) => {
if (unsubscribeTimeout) clearTimeout(unsubscribeTimeout)
unsubscribeTimeout = setTimeout(() => {
this.client.unsubscribe(subId) this.client.unsubscribe(subId)
resolve(objects) resolve(objects)
}, 250)
} }
} }
) )

View File

@ -169,7 +169,7 @@ class ReconnectingWebSocket extends Observable {
} }
onError(error) { onError(error) {
console.log(`Socket error from relay ${this.url}: ${error.message || error}`) console.log(`Socket error from relay ${this.url}`, error)
this.emit('error', error, this) this.emit('error', error, this)
if (this.opts.reconnect) this.reconnect() if (this.opts.reconnect) this.reconnect()

View File

@ -0,0 +1,66 @@
import {defineStore} from 'pinia'
import {EventKind, Tag} from 'src/nostr/model/Event'
export const useContactStore = defineStore('contact', {
state: () => ({
contacts: {},
followers: {},
}),
getters: {
getContacts(state) {
return pubkey => state.contacts[pubkey]
},
getFollowers(state) {
return pubkey => state.followers[pubkey]
}
},
actions: {
addEvent(event) {
console.assert(event.kind === EventKind.CONTACT)
const existingContacts = this.contacts[event.pubkey]
if (existingContacts && existingContacts.lastUpdatedAt >= event.createdAt) {
return
}
const newContacts = []
newContacts.lastUpdatedAt = event.createdAt
const tags = event.tags.filter(tag => tag[0] === Tag.PUBKEY && tag[1])
for (const tag of tags) {
newContacts.push({
pubkey: tag[1],
relay: tag[2],
name: tag[3],
})
}
if (existingContacts) {
for (const contact of existingContacts) {
this.removeFollower(contact.pubkey, event.pubkey)
}
}
for (const contact of newContacts) {
this.addFollower(contact.pubkey, event.pubkey)
}
this.contacts[event.pubkey] = newContacts
return event
},
addFollower(to, follower) {
if (this.followers[to]) {
this.followers[to].push(follower)
} else {
this.followers[to] = [follower]
}
},
removeFollower(from, follower) {
const followers = this.followers[from]
if (!followers) return
const index = followers.indexOf(follower)
if (index < 0) return
followers.splice(index, 1)
}
}
})

View File

@ -10,7 +10,7 @@
<div> <div>
<div <div
v-for="feed in availableFeeds" v-for="feed in availableFeeds"
:key="feed.name" :key="feed"
@click="switchFeed(feed)" @click="switchFeed(feed)"
class="popup-header" class="popup-header"
v-close-popup> v-close-popup>
@ -30,7 +30,7 @@
</div> </div>
<div class="feed"> <div class="feed">
<div class="load-more-container" :class="{'more-available': /* FIXME */false}"> <div class="load-more-container" :class="{'more-available': numUnreads}">
<ButtonLoadMore <ButtonLoadMore
v-if="numUnreads" v-if="numUnreads"
:label="`Load ${numUnreads} unread`" :label="`Load ${numUnreads} unread`"
@ -78,15 +78,15 @@ export default defineComponent({
}, },
data() { data() {
return { return {
availableFeeds: [Feeds.GLOBAL], availableFeeds: ['global'],
selectedFeed: Feeds.GLOBAL, selectedFeed: 'global',
feeds: {}, feeds: {},
initialLoadComplete: false, recentlyLoaded: true,
} }
}, },
computed: { computed: {
activeFeed() { activeFeed() {
return this.feeds[this.selectedFeed.name] return this.feeds[this.selectedFeed]
}, },
feedItems() { feedItems() {
return this.activeFeed?.items return this.activeFeed?.items
@ -95,15 +95,15 @@ export default defineComponent({
return this.activeFeed?.unreads return this.activeFeed?.unreads
}, },
numUnreads() { numUnreads() {
if (!this.initialLoadComplete) return 0 if (this.recentlyLoaded) return 0
return this.activeFeed?.unreads.length return this.activeFeed?.unreads.length
}, },
}, },
methods: { methods: {
initFeed(feed) { initFeed(feedId) {
if (this.feeds[feed.name]) return if (this.feeds[feedId]) return
this.feeds[feed.name] = { this.feeds[feedId] = {
items: [], items: [],
unreads: [], unreads: [],
} }
@ -111,34 +111,39 @@ export default defineComponent({
let initialFetchComplete = false let initialFetchComplete = false
let initialItems = [] let initialItems = []
console.log(`subscribing to feed ${feed.name}`, this.feeds[feed.name]) console.log(`subscribing to feed ${feedId}`, this.feeds[feedId])
this.nostr.streamFeed( this.nostr.streamFeed(
feed, Feeds[feedId.toUpperCase()],
event => { event => {
const target = initialFetchComplete const target = initialFetchComplete
? this.feeds[feed.name].unreads ? this.feeds[feedId].unreads
: initialItems : initialItems
target.push([event]) // FIXME Single element thread target.push([event]) // FIXME Single element thread
}, },
() => { () => {
initialItems.sort(feedOrder) initialItems.sort(feedOrder)
this.feeds[feed.name].items = initialItems this.feeds[feedId].items = initialItems
initialFetchComplete = true initialFetchComplete = true
// Wait a bit before showing the first unreads // Wait a bit before showing the first unreads
setTimeout(() => this.initialLoadComplete = true, 5000) setTimeout(() => this.recentlyLoaded = false, 5000)
} }
) )
}, },
switchFeed(feed) { switchFeed(feedId) {
this.initFeed(feed) this.initFeed(feedId)
this.selectedFeed = feed this.selectedFeed = feedId
}, },
loadUnreads() { loadUnreads() {
const items = this.feedUnreads.concat(this.feedItems) const items = this.feedUnreads.concat(this.feedItems)
items.sort(feedOrder) items.sort(feedOrder)
this.activeFeed.items = items this.activeFeed.items = items
this.activeFeed.unreads = [] this.activeFeed.unreads = []
// Wait a bit before showing unreads again
this.recentlyLoaded = true
setTimeout(() => this.recentlyLoaded = false, 5000)
}, },
}, },
mounted() { mounted() {
@ -166,6 +171,9 @@ export default defineComponent({
.addon-menu { .addon-menu {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
&-icon {
cursor: pointer;
}
&-popup { &-popup {
min-width: 150px; min-width: 150px;
border-radius: 1rem; border-radius: 1rem;

View File

@ -3,9 +3,18 @@
<PageHeader back-button /> <PageHeader back-button />
<div class="profile-header"> <div class="profile-header">
<UserAvatar :pubkey="pubkey" /> <UserAvatar :pubkey="pubkey" class="profile-header-avatar" />
<div class="profile-header-content"> <div class="profile-header-content">
<UserName :pubkey="pubkey" two-line show-verified /> <p class="username"><UserName :pubkey="pubkey" two-line show-verified /></p>
<p class="about">{{ profile?.about }}</p>
<p class="followers">
<span>
<strong>{{ contacts?.length || 0 }}</strong> Following
</span>
<span>
<strong>{{ followers?.length || 0 }}</strong> Followers
</span>
</p>
</div> </div>
</div> </div>
@ -18,7 +27,8 @@
:breakpoint="0" :breakpoint="0"
> >
<q-tab name="posts" label="Posts" /> <q-tab name="posts" label="Posts" />
<q-tab name="follows" label="Follows" /> <q-tab name="replies" label="Replies" />
<q-tab name="following" label="Following" />
<q-tab name="followers" label="Followers" /> <q-tab name="followers" label="Followers" />
<q-tab name="relays" label="Relays" /> <q-tab name="relays" label="Relays" />
</q-tabs> </q-tabs>
@ -34,6 +44,31 @@
actions actions
/> />
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="replies" class="no-padding">
<ListPost
v-for="note in replies"
:key="note.id"
:note="note"
clickable
actions
/>
</q-tab-panel>
<q-tab-panel name="following" class="no-padding">
<UserCard
v-for="contact in contacts"
:key="contact.pubkey"
:pubkey="contact.pubkey"
clickable
/>
</q-tab-panel>
<q-tab-panel name="followers" class="no-padding">
<UserCard
v-for="follower in followers"
:key="follower"
:pubkey="follower"
clickable
/>
</q-tab-panel>
</q-tab-panels> </q-tab-panels>
</q-page> </q-page>
</template> </template>
@ -44,6 +79,7 @@ import PageHeader from 'components/PageHeader.vue'
import UserAvatar from 'components/User/UserAvatar.vue' import UserAvatar from 'components/User/UserAvatar.vue'
import UserName from 'components/User/UserName.vue' import UserName from 'components/User/UserName.vue'
import ListPost from 'components/Post/ListPost.vue' import ListPost from 'components/Post/ListPost.vue'
import UserCard from 'components/User/UserCard.vue'
import {useAppStore} from 'stores/App' import {useAppStore} from 'stores/App'
import {useNostrStore} from 'src/nostr/NostrStore' import {useNostrStore} from 'src/nostr/NostrStore'
import {bech32ToHex} from 'src/utils/utils' import {bech32ToHex} from 'src/utils/utils'
@ -51,6 +87,7 @@ import {bech32ToHex} from 'src/utils/utils'
export default defineComponent({ export default defineComponent({
name: 'Profile', name: 'Profile',
components: { components: {
UserCard,
PageHeader, PageHeader,
UserAvatar, UserAvatar,
UserName, UserName,
@ -71,12 +108,28 @@ export default defineComponent({
pubkey() { pubkey() {
return bech32ToHex(this.$route.params.pubkey) return bech32ToHex(this.$route.params.pubkey)
}, },
posts() { profile() {
return this.nostr.getProfile(this.pubkey)
},
notes() {
return this.nostr.getNotesByAuthor(this.pubkey) return this.nostr.getNotesByAuthor(this.pubkey)
}, },
posts() {
return this.notes.filter(note => !note.isReply())
},
replies() {
return this.notes.filter(note => note.isReply())
},
contacts() {
return this.nostr.getContacts(this.pubkey)
},
followers() {
return this.nostr.getFollowers(this.pubkey)
},
}, },
mounted() { mounted() {
this.nostr.fetchNotesByAuthor(this.pubkey) this.nostr.fetchNotesByAuthor(this.pubkey)
this.nostr.fetchFollowers(this.pubkey)
} }
}) })
</script> </script>
@ -85,9 +138,29 @@ export default defineComponent({
.profile { .profile {
&-header { &-header {
display: flex; display: flex;
padding: 1rem;
&-avatar {
height: 128px;
width: 128px;
margin-right: 1rem;
}
&-content {
.followers {
span + span {
margin-left: 1rem;
}
}
}
} }
&-tab-panels { &-tab-panels {
background-color: unset; background-color: unset;
} }
} }
</style> </style>
<style lang="scss">
.profile-header-content .username {
.name, .pubkey:first-child {
font-size: 1.4rem;
}
}
</style>

View File

@ -1,44 +1,22 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {getEventHash, getPublicKey, signEvent} from 'nostr-tools' import {Account} from 'src/nostr/Account'
import Nip07 from 'src/utils/Nip07'
export class Account { const RELAYS = [
constructor(opts) { 'wss://nostr-pub.wellorder.net',
this.pubkey = opts.pubkey || null 'wss://nostr.onsats.org',
this.privkey = opts.privkey || null 'wss://nostr-relay.wlvs.space',
this.useExtension = opts.useExtension || false 'wss://nostr.bitcoiner.social',
'wss://relay.damus.io',
if (this.privkey && !this.pubkey) { 'wss://nostr.zebedee.cloud',
this.pubkey = getPublicKey(this.privkey) 'wss://relay.nostr.info',
} 'wss://nostr-pub.semisol.dev',
]
if (!this.pubkey) throw new Error('pubkey is required')
}
canSign() {
return !!this.privkey || (this.useExtension && Nip07.isAvailable())
}
async sign(event) {
event.id = getEventHash(event)
if (this.privkey) {
event.sig = signEvent(event, this.privkey)
} else if (this.useExtension && Nip07.isAvailable()) {
let {sig} = await Nip07.signEvent(event)
event.sig = sig
} else {
// TODO
throw new Error('cannot sign')
}
return event
}
}
export const useSettingsStore = defineStore('settings', { export const useSettingsStore = defineStore('settings', {
state: () => ({ state: () => ({
accounts: {}, accounts: {},
pubkey: null, pubkey: null,
relays: [] relays: RELAYS,
}), }),
getters: { getters: {
activeAccount(state) { activeAccount(state) {