mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
Basic Profile page
This commit is contained in:
parent
533f5e662a
commit
3c66a50046
@ -83,6 +83,7 @@ export default {
|
||||
.create-post-dialog {
|
||||
position: relative;
|
||||
background-color: $color-bg;
|
||||
box-shadow: $shadow-white;
|
||||
padding: 3rem 1rem 1rem;
|
||||
min-width: 660px;
|
||||
.icon {
|
||||
|
@ -20,7 +20,7 @@
|
||||
<MenuItem
|
||||
v-if="!hideItemsRequiringSignIn || app.isSignedIn"
|
||||
icon="profile"
|
||||
:to="`/profile/${app.myPubkey}`"
|
||||
:to="`/profile/${hexToBech32(app.myPubkey, 'npub')}`"
|
||||
:enabled="app.isSignedIn"
|
||||
@click="$emit('mobile-menu-close')"
|
||||
>
|
||||
@ -71,6 +71,7 @@ import ProfilePopup from 'components/MainMenu/ProfilePopup'
|
||||
import Logo from 'components/Logo.vue'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {MENU_ITEMS} from 'components/MainMenu/constants.js'
|
||||
import {hexToBech32} from 'src/utils/utils'
|
||||
|
||||
export default {
|
||||
name: 'MainMenu',
|
||||
@ -105,7 +106,8 @@ export default {
|
||||
signIn() {
|
||||
this.$emit('mobile-menu-close')
|
||||
this.app.signIn()
|
||||
}
|
||||
},
|
||||
hexToBech32
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -48,7 +48,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
titleFromRoute() {
|
||||
const route = this.$route.name?.toLowerCase()
|
||||
return route.charAt(0).toUpperCase() + route.substring(1)
|
||||
return route?.charAt(0).toUpperCase() + route?.substring(1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<q-dialog v-model="dialogOpen">
|
||||
<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>
|
||||
<p>
|
||||
Do you really want to log out from <UserName :pubkey="pubkey" />?
|
||||
@ -77,11 +79,25 @@ export default {
|
||||
padding: 1rem;
|
||||
min-width: 440px;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: .5rem;
|
||||
left: .5rem;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-top: 3rem;
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
> p {
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $color-primary;
|
||||
display: flex;
|
||||
background-color: rgba($color: $color-dark-gray, $alpha: 0.1);
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
@ -102,13 +118,9 @@ export default {
|
||||
padding: 12px 20px;
|
||||
border-radius: 0 0 1rem 1rem;
|
||||
margin: 0 auto 1rem;
|
||||
outline: none;
|
||||
background-color: rgba($color: $color-dark-gray, $alpha: 0.3);
|
||||
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 {
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<q-avatar
|
||||
@click="clickable && linkToProfile(pubkey)"
|
||||
class="relative-position"
|
||||
:class="{'cursor-pointer': clickable}"
|
||||
:size="size"
|
||||
@click="clickable && linkToProfile(pubkey)"
|
||||
>
|
||||
<img
|
||||
v-if="hasAvatar && !avatarFetchFailed"
|
||||
@ -38,6 +38,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
36
src/components/User/UserCard.vue
Normal file
36
src/components/User/UserCard.vue
Normal 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>
|
@ -4,17 +4,12 @@
|
||||
:class="{'two-line': twoLine, clickable}"
|
||||
>
|
||||
<a @click="clickable && linkToProfile(pubkey)">
|
||||
<span
|
||||
v-if="profile?.name"
|
||||
class="name"
|
||||
>
|
||||
{{ profile.name }}
|
||||
<q-icon v-if="showFollowing && isFollow" name="visibility" color="secondary">
|
||||
<q-tooltip>
|
||||
following
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</span>
|
||||
<span v-if="profile?.name" class="name">{{ profile.name }}</span>
|
||||
<!-- <q-icon v-if="showFollowing && isFollow" name="visibility" color="secondary">-->
|
||||
<!-- <q-tooltip>-->
|
||||
<!-- following-->
|
||||
<!-- </q-tooltip>-->
|
||||
<!-- </q-icon>-->
|
||||
<Bech32Label v-if="twoLine || !profile?.name" prefix="npub" :hex="pubkey" class="pubkey" />
|
||||
</a>
|
||||
|
||||
@ -88,7 +83,6 @@ export default {
|
||||
@import "assets/theme/colors.scss";
|
||||
|
||||
.username {
|
||||
cursor: pointer;
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -109,6 +103,7 @@ export default {
|
||||
}
|
||||
}
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
.name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
34
src/nostr/Account.js
Normal file
34
src/nostr/Account.js
Normal 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
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ export default class FetchQueue extends Observable {
|
||||
this.fnGetId = fnGetId
|
||||
this.fnCreateFilter = fnCreateFilter
|
||||
this.throttle = opts.throttle || 250
|
||||
this.batchSize = opts.batchSize || 20
|
||||
this.batchSize = opts.batchSize || 50
|
||||
this.retryDelay = opts.retryDelay || 3000
|
||||
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)
|
||||
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.
|
||||
const filteredIds = []
|
||||
|
@ -1,24 +1,18 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {markRaw} from 'vue'
|
||||
import {EventKind} from 'src/nostr/model/Event'
|
||||
import NostrClient from 'src/nostr/NostrClient'
|
||||
import FetchQueue from 'src/nostr/FetchQueue'
|
||||
import {NoteOrder, useNoteStore} from 'src/nostr/store/NoteStore'
|
||||
import {useProfileStore} from 'src/nostr/store/ProfileStore'
|
||||
import {markRaw} from 'vue'
|
||||
import FetchQueue from 'src/nostr/FetchQueue'
|
||||
|
||||
// TODO Move to settings
|
||||
const RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nostr-relay.wlvs.space',
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
'wss://nostr.oxtr.dev',
|
||||
]
|
||||
import {useContactStore} from 'src/nostr/store/ContactStore'
|
||||
import {useSettingsStore} from 'stores/Settings'
|
||||
|
||||
export const Feeds = {
|
||||
GLOBAL: {
|
||||
name: 'global',
|
||||
filters: {
|
||||
kinds: [EventKind.NOTE, EventKind.DELETE],
|
||||
kinds: [EventKind.NOTE], // TODO Deletions
|
||||
},
|
||||
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', {
|
||||
state: () => ({
|
||||
// TODO Limit size. Remove oldest.
|
||||
@ -50,7 +54,9 @@ export const useNostrStore = defineStore('nostr', {
|
||||
}),
|
||||
actions: {
|
||||
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.profileQueue = profileQueue(this.client)
|
||||
@ -58,6 +64,9 @@ export const useNostrStore = defineStore('nostr', {
|
||||
|
||||
this.noteQueue = eventQueue(this.client, 'note')
|
||||
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) {
|
||||
// console.log(`[EVENT] from ${relay}`, event)
|
||||
@ -83,8 +92,10 @@ export const useNostrStore = defineStore('nostr', {
|
||||
}
|
||||
case EventKind.RELAY:
|
||||
break
|
||||
case EventKind.CONTACT:
|
||||
break
|
||||
case EventKind.CONTACT: {
|
||||
const contacts = useContactStore()
|
||||
return contacts.addEvent(event)
|
||||
}
|
||||
case EventKind.DM:
|
||||
break
|
||||
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) {
|
||||
return this.streamEvents(
|
||||
{
|
||||
@ -199,6 +235,7 @@ export const useNostrStore = defineStore('nostr', {
|
||||
|
||||
fetchMultiple(filters, limit = 100) {
|
||||
const filtersWithLimit = Object.assign({}, filters, {limit})
|
||||
let unsubscribeTimeout
|
||||
return new Promise(resolve => {
|
||||
const objects = []
|
||||
this.client.subscribe(
|
||||
@ -216,8 +253,11 @@ export const useNostrStore = defineStore('nostr', {
|
||||
},
|
||||
{
|
||||
eoseCallback: (_relay, subId) => {
|
||||
this.client.unsubscribe(subId)
|
||||
resolve(objects)
|
||||
if (unsubscribeTimeout) clearTimeout(unsubscribeTimeout)
|
||||
unsubscribeTimeout = setTimeout(() => {
|
||||
this.client.unsubscribe(subId)
|
||||
resolve(objects)
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -169,7 +169,7 @@ class ReconnectingWebSocket extends Observable {
|
||||
}
|
||||
|
||||
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)
|
||||
if (this.opts.reconnect) this.reconnect()
|
||||
|
66
src/nostr/store/ContactStore.js
Normal file
66
src/nostr/store/ContactStore.js
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
@ -10,7 +10,7 @@
|
||||
<div>
|
||||
<div
|
||||
v-for="feed in availableFeeds"
|
||||
:key="feed.name"
|
||||
:key="feed"
|
||||
@click="switchFeed(feed)"
|
||||
class="popup-header"
|
||||
v-close-popup>
|
||||
@ -30,7 +30,7 @@
|
||||
</div>
|
||||
|
||||
<div class="feed">
|
||||
<div class="load-more-container" :class="{'more-available': /* FIXME */false}">
|
||||
<div class="load-more-container" :class="{'more-available': numUnreads}">
|
||||
<ButtonLoadMore
|
||||
v-if="numUnreads"
|
||||
:label="`Load ${numUnreads} unread`"
|
||||
@ -78,15 +78,15 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
availableFeeds: [Feeds.GLOBAL],
|
||||
selectedFeed: Feeds.GLOBAL,
|
||||
availableFeeds: ['global'],
|
||||
selectedFeed: 'global',
|
||||
feeds: {},
|
||||
initialLoadComplete: false,
|
||||
recentlyLoaded: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeFeed() {
|
||||
return this.feeds[this.selectedFeed.name]
|
||||
return this.feeds[this.selectedFeed]
|
||||
},
|
||||
feedItems() {
|
||||
return this.activeFeed?.items
|
||||
@ -95,15 +95,15 @@ export default defineComponent({
|
||||
return this.activeFeed?.unreads
|
||||
},
|
||||
numUnreads() {
|
||||
if (!this.initialLoadComplete) return 0
|
||||
if (this.recentlyLoaded) return 0
|
||||
return this.activeFeed?.unreads.length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initFeed(feed) {
|
||||
if (this.feeds[feed.name]) return
|
||||
initFeed(feedId) {
|
||||
if (this.feeds[feedId]) return
|
||||
|
||||
this.feeds[feed.name] = {
|
||||
this.feeds[feedId] = {
|
||||
items: [],
|
||||
unreads: [],
|
||||
}
|
||||
@ -111,34 +111,39 @@ export default defineComponent({
|
||||
let initialFetchComplete = false
|
||||
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(
|
||||
feed,
|
||||
Feeds[feedId.toUpperCase()],
|
||||
event => {
|
||||
const target = initialFetchComplete
|
||||
? this.feeds[feed.name].unreads
|
||||
? this.feeds[feedId].unreads
|
||||
: initialItems
|
||||
target.push([event]) // FIXME Single element thread
|
||||
},
|
||||
() => {
|
||||
initialItems.sort(feedOrder)
|
||||
this.feeds[feed.name].items = initialItems
|
||||
this.feeds[feedId].items = initialItems
|
||||
initialFetchComplete = true
|
||||
|
||||
// Wait a bit before showing the first unreads
|
||||
setTimeout(() => this.initialLoadComplete = true, 5000)
|
||||
setTimeout(() => this.recentlyLoaded = false, 5000)
|
||||
}
|
||||
)
|
||||
},
|
||||
switchFeed(feed) {
|
||||
this.initFeed(feed)
|
||||
this.selectedFeed = feed
|
||||
switchFeed(feedId) {
|
||||
this.initFeed(feedId)
|
||||
this.selectedFeed = feedId
|
||||
},
|
||||
loadUnreads() {
|
||||
const items = this.feedUnreads.concat(this.feedItems)
|
||||
items.sort(feedOrder)
|
||||
this.activeFeed.items = items
|
||||
this.activeFeed.unreads = []
|
||||
|
||||
// Wait a bit before showing unreads again
|
||||
this.recentlyLoaded = true
|
||||
setTimeout(() => this.recentlyLoaded = false, 5000)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -166,6 +171,9 @@ export default defineComponent({
|
||||
.addon-menu {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
&-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
&-popup {
|
||||
min-width: 150px;
|
||||
border-radius: 1rem;
|
||||
|
@ -3,9 +3,18 @@
|
||||
<PageHeader back-button />
|
||||
|
||||
<div class="profile-header">
|
||||
<UserAvatar :pubkey="pubkey" />
|
||||
<UserAvatar :pubkey="pubkey" class="profile-header-avatar" />
|
||||
<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>
|
||||
|
||||
@ -18,7 +27,8 @@
|
||||
:breakpoint="0"
|
||||
>
|
||||
<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="relays" label="Relays" />
|
||||
</q-tabs>
|
||||
@ -34,6 +44,31 @@
|
||||
actions
|
||||
/>
|
||||
</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-page>
|
||||
</template>
|
||||
@ -44,6 +79,7 @@ import PageHeader from 'components/PageHeader.vue'
|
||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||
import UserName from 'components/User/UserName.vue'
|
||||
import ListPost from 'components/Post/ListPost.vue'
|
||||
import UserCard from 'components/User/UserCard.vue'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||
import {bech32ToHex} from 'src/utils/utils'
|
||||
@ -51,6 +87,7 @@ import {bech32ToHex} from 'src/utils/utils'
|
||||
export default defineComponent({
|
||||
name: 'Profile',
|
||||
components: {
|
||||
UserCard,
|
||||
PageHeader,
|
||||
UserAvatar,
|
||||
UserName,
|
||||
@ -71,12 +108,28 @@ export default defineComponent({
|
||||
pubkey() {
|
||||
return bech32ToHex(this.$route.params.pubkey)
|
||||
},
|
||||
posts() {
|
||||
profile() {
|
||||
return this.nostr.getProfile(this.pubkey)
|
||||
},
|
||||
notes() {
|
||||
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() {
|
||||
this.nostr.fetchNotesByAuthor(this.pubkey)
|
||||
this.nostr.fetchFollowers(this.pubkey)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -85,9 +138,29 @@ export default defineComponent({
|
||||
.profile {
|
||||
&-header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
&-avatar {
|
||||
height: 128px;
|
||||
width: 128px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&-content {
|
||||
.followers {
|
||||
span + span {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&-tab-panels {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.profile-header-content .username {
|
||||
.name, .pubkey:first-child {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,44 +1,22 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {getEventHash, getPublicKey, signEvent} from 'nostr-tools'
|
||||
import Nip07 from 'src/utils/Nip07'
|
||||
import {Account} from 'src/nostr/Account'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
const RELAYS = [
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
'wss://nostr.onsats.org',
|
||||
'wss://nostr-relay.wlvs.space',
|
||||
'wss://nostr.bitcoiner.social',
|
||||
'wss://relay.damus.io',
|
||||
'wss://nostr.zebedee.cloud',
|
||||
'wss://relay.nostr.info',
|
||||
'wss://nostr-pub.semisol.dev',
|
||||
]
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
accounts: {},
|
||||
pubkey: null,
|
||||
relays: []
|
||||
relays: RELAYS,
|
||||
}),
|
||||
getters: {
|
||||
activeAccount(state) {
|
||||
|
Loading…
Reference in New Issue
Block a user