mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 13:33:22 +00:00
* Basic tag rendering in posts
* Refactor nostr fetch logic
This commit is contained in:
parent
2ac3c9ab7d
commit
90a2abc7a3
@ -6,7 +6,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {hexToBech32} from 'src/utils/utils'
|
import {hexToBech32, shortenBech32} from 'src/utils/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Bech32Label',
|
name: 'Bech32Label',
|
||||||
@ -34,14 +34,9 @@ export default {
|
|||||||
const value = this.bech32
|
const value = this.bech32
|
||||||
? this.bech32.substring(4)
|
? this.bech32.substring(4)
|
||||||
: hexToBech32(this.hex, '')
|
: hexToBech32(this.hex, '')
|
||||||
return this.shorten(value)
|
return shortenBech32(value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
shorten(str) {
|
|
||||||
return str.substr(0, 5) + '…' + str.substr(-5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ export default {
|
|||||||
const p = []
|
const p = []
|
||||||
|
|
||||||
if (this.ancestor) {
|
if (this.ancestor) {
|
||||||
if (this.ancestor.isReply()) {
|
if (this.ancestor.hasAncestor()) {
|
||||||
e.push(this.ancestor.root())
|
e.push(this.ancestor.root())
|
||||||
}
|
}
|
||||||
e.push(this.ancestor.id)
|
e.push(this.ancestor.id)
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<div class="addon">
|
<div class="addon">
|
||||||
<slot name="addon" />
|
<slot name="addon" />
|
||||||
</div>
|
</div>
|
||||||
<router-link class="logo" to="/">
|
<router-link v-if="logo" class="logo" to="/">
|
||||||
<Logo />
|
<Logo />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@ -45,7 +45,11 @@ export default defineComponent({
|
|||||||
backButton: {
|
backButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
|
logo: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
titleFromRoute() {
|
titleFromRoute() {
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<div class="post-content-header">
|
<div class="post-content-header">
|
||||||
<p v-if="note.isReply()" class="in-reply-to">
|
<p v-if="note.hasAncestor()" class="in-reply-to">
|
||||||
Replying to
|
Replying to
|
||||||
<a @click.stop="goToProfile(ancestor?.author)">
|
<a @click.stop="goToProfile(ancestor?.author)">
|
||||||
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
||||||
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="post-content-body">
|
<div class="post-content-body">
|
||||||
<p>
|
<p>
|
||||||
<BaseMarkdown :content="note.content" />
|
<PostRenderer :note="note" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content-footer">
|
<div class="post-content-footer">
|
||||||
@ -59,9 +59,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BaseIcon from 'components/BaseIcon'
|
import BaseIcon from 'components/BaseIcon'
|
||||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
|
||||||
import UserName from 'components/User/UserName.vue'
|
import UserName from 'components/User/UserName.vue'
|
||||||
import BaseMarkdown from 'components/Post/BaseMarkdown.vue'
|
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||||
|
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
||||||
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||||
import {useAppStore} from 'stores/App'
|
import {useAppStore} from 'stores/App'
|
||||||
@ -73,11 +73,11 @@ export default {
|
|||||||
name: 'HeroPost',
|
name: 'HeroPost',
|
||||||
mixins: [routerMixin],
|
mixins: [routerMixin],
|
||||||
components: {
|
components: {
|
||||||
BaseMarkdown,
|
BaseIcon,
|
||||||
UserName,
|
UserName,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
BaseIcon,
|
|
||||||
PostEditor,
|
PostEditor,
|
||||||
|
PostRenderer,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
@ -98,7 +98,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
ancestor() {
|
ancestor() {
|
||||||
return this.note.isReply()
|
return this.note.hasAncestor()
|
||||||
? this.nostr.getNote(this.note.ancestor())
|
? this.nostr.getNote(this.note.ancestor())
|
||||||
: null
|
: null
|
||||||
},
|
},
|
||||||
@ -149,6 +149,7 @@ export default {
|
|||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
a {
|
a {
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span class="created-at">{{ formatPostDate(note.createdAt) }}</span>
|
<span class="created-at">{{ formatPostDate(note.createdAt) }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="note.isReply()" class="in-reply-to">
|
<p v-if="note.hasAncestor()" class="in-reply-to">
|
||||||
Replying to
|
Replying to
|
||||||
<a @click.stop="goToProfile(ancestor?.author)">
|
<a @click.stop="goToProfile(ancestor?.author)">
|
||||||
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content-body">
|
<div class="post-content-body">
|
||||||
<BaseMarkdown :content="note.content" />
|
<PostRenderer :note="note" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showActions" class="post-content-actions">
|
<div v-if="showActions" class="post-content-actions">
|
||||||
<div class="action-item comment" @click.stop="app.createPost({ancestor: note.id})">
|
<div class="action-item comment" @click.stop="app.createPost({ancestor: note.id})">
|
||||||
@ -52,12 +52,12 @@
|
|||||||
import BaseIcon from 'components/BaseIcon'
|
import BaseIcon from 'components/BaseIcon'
|
||||||
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 BaseMarkdown from 'components/Post/BaseMarkdown.vue'
|
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
||||||
import routerMixin from 'src/router/mixin'
|
|
||||||
import {useAppStore} from 'stores/App'
|
import {useAppStore} from 'stores/App'
|
||||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||||
import DateUtils from 'src/utils/DateUtils'
|
|
||||||
import {useStatStore} from 'src/nostr/store/StatStore'
|
import {useStatStore} from 'src/nostr/store/StatStore'
|
||||||
|
import routerMixin from 'src/router/mixin'
|
||||||
|
import DateUtils from 'src/utils/DateUtils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListPost',
|
name: 'ListPost',
|
||||||
@ -65,7 +65,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
UserName,
|
UserName,
|
||||||
BaseMarkdown,
|
PostRenderer,
|
||||||
BaseIcon,
|
BaseIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -99,7 +99,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
ancestor() {
|
ancestor() {
|
||||||
return this.note.isReply()
|
return this.note.hasAncestor()
|
||||||
? this.nostr.getNote(this.note.ancestor())
|
? this.nostr.getNote(this.note.ancestor())
|
||||||
: null
|
: null
|
||||||
},
|
},
|
||||||
@ -165,6 +165,7 @@ export default {
|
|||||||
color: $color-dark-gray;
|
color: $color-dark-gray;
|
||||||
a {
|
a {
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ import superscript from 'markdown-it-sup'
|
|||||||
import deflist from 'markdown-it-deflist'
|
import deflist from 'markdown-it-deflist'
|
||||||
import emoji from 'markdown-it-emoji'
|
import emoji from 'markdown-it-emoji'
|
||||||
import * as Bolt11Decoder from 'light-bolt11-decoder'
|
import * as Bolt11Decoder from 'light-bolt11-decoder'
|
||||||
import BaseInvoice from 'components/post/BaseInvoice.vue'
|
import BaseInvoice from 'components/Post/Renderer/BaseInvoice.vue'
|
||||||
|
|
||||||
const md = MarkdownIt({
|
const md = MarkdownIt({
|
||||||
html: false,
|
html: false,
|
||||||
@ -194,7 +194,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
content: {
|
content: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'todo needs to be updated'
|
required: true,
|
||||||
},
|
},
|
||||||
longForm: {
|
longForm: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -227,6 +227,12 @@ export default {
|
|||||||
this.render()
|
this.render()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
content() {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
render() {
|
render() {
|
||||||
this.html = md.render(this.parsedContent) + this.$refs.append.innerHTML
|
this.html = md.render(this.parsedContent) + this.$refs.append.innerHTML
|
100
src/components/Post/Renderer/PostRenderer.vue
Normal file
100
src/components/Post/Renderer/PostRenderer.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<BaseMarkdown :content="content" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseMarkdown from 'components/Post/Renderer/BaseMarkdown.vue'
|
||||||
|
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||||
|
import {TagType} from 'src/nostr/model/Event'
|
||||||
|
import {hexToBech32, shortenBech32} from 'src/utils/utils'
|
||||||
|
import routerMixin from 'src/router/mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PostRenderer',
|
||||||
|
components: {BaseMarkdown},
|
||||||
|
mixins: [routerMixin],
|
||||||
|
props: {
|
||||||
|
note: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
nostr: useNostrStore(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
content() {
|
||||||
|
let content = this.note.content
|
||||||
|
content = this.replaceBech32Refs(content)
|
||||||
|
content = this.replaceTagRefs(content)
|
||||||
|
return content
|
||||||
|
},
|
||||||
|
profiles() {
|
||||||
|
const profiles = {}
|
||||||
|
for (const pubkey of this.note.pubkeyRefs()) {
|
||||||
|
// TODO batch request
|
||||||
|
profiles[hexToBech32(pubkey, 'npub')] = this.nostr.getProfile(pubkey)
|
||||||
|
}
|
||||||
|
return profiles
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
replaceTagRefs(str) {
|
||||||
|
if (!str) return str
|
||||||
|
return str.replace(/#\[([0-9]+)]/ig, (match, index) => {
|
||||||
|
const tag = this.note.tags[index]
|
||||||
|
if (!tag) return match
|
||||||
|
const rendered = this.renderTag(tag)
|
||||||
|
if (!rendered) return match
|
||||||
|
return rendered
|
||||||
|
})
|
||||||
|
},
|
||||||
|
renderTag(tag) {
|
||||||
|
switch (tag.type) {
|
||||||
|
case TagType.PUBKEY: {
|
||||||
|
const bech32 = hexToBech32(tag.ref, 'npub')
|
||||||
|
return this.renderProfileRef(bech32)
|
||||||
|
}
|
||||||
|
case TagType.EVENT: {
|
||||||
|
const bech32 = hexToBech32(tag.ref, 'note')
|
||||||
|
return this.renderNoteRef(bech32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replaceBech32Refs(str) {
|
||||||
|
if (!str) return str
|
||||||
|
return str.replace(/@((npub|note)[a-z0-9]{59})/ig, (match, bech32, prefix) => {
|
||||||
|
switch (prefix) {
|
||||||
|
case 'npub':
|
||||||
|
return this.renderProfileRef(bech32)
|
||||||
|
case 'note':
|
||||||
|
return this.renderNoteRef(bech32)
|
||||||
|
default:
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
renderProfileRef(bech32) {
|
||||||
|
const profile = this.profiles[bech32]
|
||||||
|
const text = profile
|
||||||
|
? profile.name
|
||||||
|
: shortenBech32(bech32)
|
||||||
|
if (!bech32 || !text) return
|
||||||
|
const link = this.linkToProfile(bech32)
|
||||||
|
return `[${text}](${link})`
|
||||||
|
},
|
||||||
|
renderNoteRef(bech32) {
|
||||||
|
if (!bech32) return
|
||||||
|
const text = shortenBech32(bech32)
|
||||||
|
const link = this.linkToThread(bech32)
|
||||||
|
return `[${text}](${link})`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
v-if="app.isSignedIn"
|
v-if="app.isSignedIn"
|
||||||
class="btn"
|
class="btn btn-sm"
|
||||||
:class="{'btn-primary': !isFollowing}"
|
:class="{'btn-primary': !isFollowing}"
|
||||||
>
|
>
|
||||||
{{ isFollowing ? 'Unfollow' : 'Follow' }}
|
{{ isFollowing ? 'Unfollow' : 'Follow' }}
|
||||||
|
@ -31,10 +31,15 @@ h3 {
|
|||||||
letter-spacing: unset;
|
letter-spacing: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
height: fit-content;
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -68,6 +73,7 @@ h3 {
|
|||||||
|
|
||||||
&-sm {
|
&-sm {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export default class FetchQueue extends Observable {
|
|||||||
this.fnCreateFilter = fnCreateFilter
|
this.fnCreateFilter = fnCreateFilter
|
||||||
this.throttle = opts.throttle || 250
|
this.throttle = opts.throttle || 250
|
||||||
this.batchSize = opts.batchSize || 50
|
this.batchSize = opts.batchSize || 50
|
||||||
this.retryDelay = opts.retryDelay || 3000
|
this.retryDelay = opts.retryDelay || 5000
|
||||||
this.maxRetries = opts.maxRetries || 3
|
this.maxRetries = opts.maxRetries || 3
|
||||||
|
|
||||||
this.queue = {}
|
this.queue = {}
|
||||||
@ -19,7 +19,7 @@ export default class FetchQueue extends Observable {
|
|||||||
this.retryInterval = null
|
this.retryInterval = null
|
||||||
|
|
||||||
// XXX
|
// XXX
|
||||||
setInterval(() => this.failed = {}, 5000)
|
setInterval(() => this.failed = {}, 10000)
|
||||||
}
|
}
|
||||||
|
|
||||||
add(id) {
|
add(id) {
|
||||||
@ -29,9 +29,7 @@ export default class FetchQueue extends Observable {
|
|||||||
if (this.failed[id]) return // TODO improve this
|
if (this.failed[id]) return // TODO improve this
|
||||||
this.queue[id] = 0
|
this.queue[id] = 0
|
||||||
|
|
||||||
if (!this.fetching) {
|
if (!this.fetching && !this.fetchQueued) {
|
||||||
this.fetch()
|
|
||||||
} else if (!this.fetchQueued) {
|
|
||||||
setTimeout(this.fetch.bind(this), this.throttle)
|
setTimeout(this.fetch.bind(this), this.throttle)
|
||||||
this.fetchQueued = true
|
this.fetchQueued = true
|
||||||
}
|
}
|
||||||
@ -44,8 +42,6 @@ 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}/${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 = []
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
@ -58,38 +54,51 @@ export default class FetchQueue extends Observable {
|
|||||||
filteredIds.push(id)
|
filteredIds.push(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filteredIds.length) return
|
if (!filteredIds.length) return
|
||||||
|
|
||||||
|
console.log(`Fetching ${ids.length}/${Object.keys(this.queue).length} ${this.subId}s`, ids)
|
||||||
|
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
this.retryInterval = setInterval(this.fetch.bind(this), this.retryDelay)
|
this.retryInterval = setInterval(this.fetch.bind(this), this.retryDelay)
|
||||||
|
|
||||||
// XXX Needed for some relays?
|
// XXX Needed for some relays?
|
||||||
this.client.unsubscribe(this.subId)
|
//this.client.unsubscribe(this.subId)
|
||||||
|
|
||||||
this.client.subscribe(
|
const sub = this.client.subscribe(this.fnCreateFilter(filteredIds), this.subId)
|
||||||
this.fnCreateFilter(filteredIds),
|
sub.on('event', (event, relay, subId) => {
|
||||||
(event, relay, subId) => {
|
|
||||||
const id = this.fnGetId(event)
|
const id = this.fnGetId(event)
|
||||||
|
if (!this.queue[id]) return
|
||||||
|
|
||||||
delete this.queue[id]
|
delete this.queue[id]
|
||||||
filteredIds.splice(filteredIds.indexOf(id), 1)
|
filteredIds.splice(filteredIds.indexOf(id), 1)
|
||||||
|
|
||||||
// console.log(`Fetched ${this.subId} ${id}, ${filteredIds.length} remaining`)
|
console.log(`Fetched ${this.subId} ${id}, ${filteredIds.length} remaining`)
|
||||||
|
|
||||||
this.emit('event', event, relay)
|
this.emit('event', event, relay)
|
||||||
|
|
||||||
if (Object.keys(this.queue).length === 0) {
|
if (Object.keys(this.queue).length === 0) {
|
||||||
// console.log(`Fetched all ${this.subId}s`)
|
|
||||||
if (this.retryInterval) clearInterval(this.retryInterval)
|
if (this.retryInterval) clearInterval(this.retryInterval)
|
||||||
this.client.unsubscribe(subId)
|
|
||||||
this.fetching = false
|
this.fetching = false
|
||||||
|
sub.close()
|
||||||
} else if (filteredIds.length === 0) {
|
} else if (filteredIds.length === 0) {
|
||||||
// console.log(`Batch ${this.subId} fetched, requesting more (${Object.keys(this.queue).length} remain)`)
|
|
||||||
this.fetch()
|
this.fetch()
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
{
|
sub.on('complete', () => {
|
||||||
subId: this.subId,
|
if (this.fetching && Object.keys(this.queue).length > 0) {
|
||||||
|
this.fetch()
|
||||||
|
} else {
|
||||||
|
console.log('[COMPLETE]', this)
|
||||||
|
if (this.retryInterval) clearInterval(this.retryInterval)
|
||||||
|
this.fetching = false
|
||||||
|
sub.close()
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
sub.on('close', () => {
|
||||||
|
if (this.fetching && Object.keys(this.queue).length > 0) {
|
||||||
|
this.fetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,11 @@
|
|||||||
import RelayPool from 'src/nostr/RelayPool'
|
import RelayPool from 'src/nostr/RelayPool'
|
||||||
import Event from 'src/nostr/model/Event'
|
import {CloseAfter} from 'src/nostr/Relay'
|
||||||
|
|
||||||
export const CancelAfter = {
|
|
||||||
SINGLE: 'single',
|
|
||||||
EOSE: 'eose',
|
|
||||||
NEVER: 'never',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class NostrClient {
|
export default class NostrClient {
|
||||||
constructor(relays) {
|
constructor(relays) {
|
||||||
this.pool = new RelayPool(relays)
|
this.pool = new RelayPool(relays)
|
||||||
this.pool.on('event', this.onEvent.bind(this))
|
|
||||||
this.pool.on('eose', this.onEose.bind(this))
|
|
||||||
this.pool.on('notice', this.onNotice.bind(this))
|
this.pool.on('notice', this.onNotice.bind(this))
|
||||||
this.pool.on('ok', this.onOk.bind(this))
|
this.pool.on('ok', this.onOk.bind(this))
|
||||||
|
|
||||||
this.subs = {}
|
|
||||||
this.nextSubId = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
@ -31,70 +20,57 @@ export default class NostrClient {
|
|||||||
return this.pool.connectedRelays()
|
return this.pool.connectedRelays()
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(filters, callback, opts = {}) {
|
subscribe(filters, subId = null) {
|
||||||
let subId
|
return this.pool.subscribe(filters, subId)
|
||||||
if (opts?.subId) {
|
|
||||||
//if (this.subs[opts.subId]) throw new Error(`SubId '${opts.subId}' already exists`)
|
|
||||||
subId = opts.subId
|
|
||||||
} else {
|
|
||||||
subId = `sub${this.nextSubId++}`
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subs[subId] = {
|
|
||||||
eventCallback: callback,
|
|
||||||
eoseCallback: opts.eoseCallback,
|
|
||||||
cancelAfter: opts.cancelAfter || CancelAfter.NEVER,
|
|
||||||
}
|
|
||||||
this.pool.subscribe(subId, filters)
|
|
||||||
|
|
||||||
return subId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(subId) {
|
unsubscribe(subId) {
|
||||||
this.pool.unsubscribe(subId)
|
this.pool.unsubscribe(subId)
|
||||||
delete this.subs[subId]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send(event) {
|
publish(event) {
|
||||||
return this.pool.send(event)
|
return this.pool.publish(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(relay, subId, ev) {
|
// fetchSingle(filters) {
|
||||||
const event = Event.from(ev)
|
// const filtersWithLimit = Object.assign({}, filters, {limit: 1})
|
||||||
if (!event.validate()) {
|
// return new Promise(resolve => {
|
||||||
// TODO Close relay?
|
// const sub = this.pool.subscribe(filters)
|
||||||
console.error(`Invalid event from ${relay}`, event)
|
// sub.on('event')
|
||||||
return
|
// this.client.subscribe(
|
||||||
}
|
// filtersWithLimit,
|
||||||
|
// (event, relay) => {
|
||||||
|
// resolve(this.addEvent(event, relay))
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// closeAfter: 'single'
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
const sub = this.subs[subId]
|
fetchMultiple(filters, limit = 100, timeout = 5000) {
|
||||||
if (!sub) {
|
return new Promise(resolve => {
|
||||||
//console.warn(`Event for invalid subId ${subId} from ${relay}`)
|
const objects = {}
|
||||||
return
|
const filtersWithLimit = Object.assign({}, filters, {limit})
|
||||||
}
|
const sub = this.pool.subscribe(filtersWithLimit, null, CloseAfter.EOSE)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
if (typeof sub.eventCallback === 'function') {
|
sub.close()
|
||||||
sub.eventCallback(event, relay, subId)
|
resolve(objects)
|
||||||
}
|
}, timeout)
|
||||||
|
sub.on('event', event => {
|
||||||
if (sub.cancelAfter === CancelAfter.SINGLE) {
|
objects[event.id] = event
|
||||||
this.unsubscribe(subId)
|
if (Object.keys(objects).length >= limit) {
|
||||||
}
|
clearTimeout(timer)
|
||||||
}
|
sub.close()
|
||||||
|
resolve(objects)
|
||||||
onEose(relay, subId) {
|
|
||||||
//console.log(`[EOSE] from ${relay} for ${subId}`)
|
|
||||||
|
|
||||||
const sub = this.subs[subId]
|
|
||||||
if (!sub) return
|
|
||||||
|
|
||||||
if (typeof sub.eoseCallback === 'function') {
|
|
||||||
sub.eoseCallback(relay, subId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub.cancelAfter === CancelAfter.EOSE) {
|
|
||||||
this.unsubscribe(subId)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
sub.on('close', () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve(objects)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onNotice(relay, message) {
|
onNotice(relay, message) {
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {markRaw} from 'vue'
|
import {markRaw} from 'vue'
|
||||||
import {EventKind} from 'src/nostr/model/Event'
|
import {EventKind} from 'src/nostr/model/Event'
|
||||||
import Note from 'src/nostr/model/Note'
|
|
||||||
import NostrClient from 'src/nostr/NostrClient'
|
import NostrClient from 'src/nostr/NostrClient'
|
||||||
import FetchQueue from 'src/nostr/FetchQueue'
|
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 {useContactStore} from 'src/nostr/store/ContactStore'
|
import {useContactStore} from 'src/nostr/store/ContactStore'
|
||||||
import {useSettingsStore} from 'stores/Settings'
|
import {useSettingsStore} from 'stores/Settings'
|
||||||
import {ReactionOrder, useReactionStore} from 'src/nostr/store/ReactionStore'
|
|
||||||
import {useStatStore} from 'src/nostr/store/StatStore'
|
import {useStatStore} from 'src/nostr/store/StatStore'
|
||||||
|
import {CloseAfter} from 'src/nostr/Relay'
|
||||||
|
|
||||||
export const Feeds = {
|
export const Feeds = {
|
||||||
GLOBAL: {
|
GLOBAL: {
|
||||||
@ -92,14 +91,9 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
return profiles.addEvent(event)
|
return profiles.addEvent(event)
|
||||||
}
|
}
|
||||||
case EventKind.NOTE: {
|
case EventKind.NOTE: {
|
||||||
if (Note.isReaction(event)) {
|
|
||||||
const reactions = useReactionStore()
|
|
||||||
return reactions.addEvent(event)
|
|
||||||
} else {
|
|
||||||
const notes = useNoteStore()
|
const notes = useNoteStore()
|
||||||
return notes.addEvent(event)
|
return notes.addEvent(event)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case EventKind.RELAY:
|
case EventKind.RELAY:
|
||||||
break
|
break
|
||||||
case EventKind.CONTACT: {
|
case EventKind.CONTACT: {
|
||||||
@ -113,8 +107,8 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
case EventKind.SHARE:
|
case EventKind.SHARE:
|
||||||
break
|
break
|
||||||
case EventKind.REACTION: {
|
case EventKind.REACTION: {
|
||||||
const reactions = useReactionStore()
|
const notes = useNoteStore()
|
||||||
return reactions.addEvent(event)
|
return notes.addEvent(event)
|
||||||
}
|
}
|
||||||
case EventKind.CHATROOM:
|
case EventKind.CHATROOM:
|
||||||
break
|
break
|
||||||
@ -127,7 +121,7 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
|
|
||||||
sendEvent(event) {
|
sendEvent(event) {
|
||||||
this.addEvent(event)
|
this.addEvent(event)
|
||||||
return this.client.send(event)
|
return this.client.publish(event)
|
||||||
},
|
},
|
||||||
|
|
||||||
getProfile(pubkey) {
|
getProfile(pubkey) {
|
||||||
@ -139,7 +133,7 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
|
|
||||||
getNote(id) {
|
getNote(id) {
|
||||||
const notes = useNoteStore()
|
const notes = useNoteStore()
|
||||||
const note = notes.get(id)
|
let note = notes.get(id)
|
||||||
if (!note) this.noteQueue.add(id)
|
if (!note) this.noteQueue.add(id)
|
||||||
return note
|
return note
|
||||||
},
|
},
|
||||||
@ -154,7 +148,7 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
getNotesByAuthor(pubkey, opts = {}) {
|
getNotesByAuthor(pubkey, opts = {}) {
|
||||||
const order = opts.order || NoteOrder.CREATION_DATE_DESC
|
const order = opts.order || NoteOrder.CREATION_DATE_DESC
|
||||||
const notes = useNoteStore()
|
const notes = useNoteStore()
|
||||||
return notes.allByAuthor(pubkey, order)
|
return notes.getNotesByAuthor(pubkey, order)
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchNotesByAuthor(pubkey, opts = {}) {
|
fetchNotesByAuthor(pubkey, opts = {}) {
|
||||||
@ -193,9 +187,9 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
getReactionsTo(id, order = ReactionOrder.CREATION_DATE_DESC) {
|
getReactionsTo(id, order = NoteOrder.CREATION_DATE_DESC) {
|
||||||
const store = useReactionStore()
|
const store = useNoteStore()
|
||||||
const reactions = store.allByEvent(id, order)
|
const reactions = store.reactionsTo(id, order)
|
||||||
// TODO fetch?
|
// TODO fetch?
|
||||||
return reactions
|
return reactions
|
||||||
},
|
},
|
||||||
@ -210,9 +204,9 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
getReactionsByAuthor(pubkey, order = ReactionOrder.CREATION_DATE_DESC) {
|
getReactionsByAuthor(pubkey, order = NoteOrder.CREATION_DATE_DESC) {
|
||||||
const store = useReactionStore()
|
const store = useNoteStore()
|
||||||
const reactions = store.allByAuthor(pubkey, order)
|
const reactions = store.getReactionsByAuthor(pubkey, order)
|
||||||
// TODO fetch?
|
// TODO fetch?
|
||||||
return reactions
|
return reactions
|
||||||
},
|
},
|
||||||
@ -227,6 +221,30 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchMultiple(filters, limit = 100, timeout = 5000) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const objects = {}
|
||||||
|
const filtersWithLimit = Object.assign({}, filters, {limit})
|
||||||
|
const sub = this.client.subscribe(filtersWithLimit, null, CloseAfter.EOSE)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
sub.close()
|
||||||
|
resolve(objects)
|
||||||
|
}, timeout)
|
||||||
|
sub.on('event', event => {
|
||||||
|
objects[event.id] = this.addEvent(event)
|
||||||
|
if (Object.keys(objects).length >= limit) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
sub.close()
|
||||||
|
resolve(objects)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
sub.on('close', () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve(objects)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
streamThread(rootId, eventCallback, initialFetchCompleteCallback) {
|
streamThread(rootId, eventCallback, initialFetchCompleteCallback) {
|
||||||
return this.streamEvents(
|
return this.streamEvents(
|
||||||
{
|
{
|
||||||
@ -255,102 +273,58 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
streamFullProfile(pubkey) {
|
streamFullProfile(pubkey) {
|
||||||
const handles = []
|
// FIXME
|
||||||
// Everything authored by pubkey
|
// const handles = []
|
||||||
handles.push(this.client.subscribe({
|
// // Everything authored by pubkey
|
||||||
kinds: [EventKind.NOTE],
|
|
||||||
authors: [pubkey],
|
|
||||||
limit: 200,
|
|
||||||
}, () => {}, { subId: 'foo' }))
|
|
||||||
handles.push(this.client.subscribe({
|
|
||||||
kinds: [EventKind.REACTION],
|
|
||||||
authors: [pubkey],
|
|
||||||
limit: 100,
|
|
||||||
}))
|
|
||||||
handles.push(this.client.subscribe({
|
|
||||||
kinds: [EventKind.METADATA, EventKind.CONTACT],
|
|
||||||
authors: [pubkey],
|
|
||||||
}))
|
|
||||||
// handles.push(this.client.subscribe({
|
// handles.push(this.client.subscribe({
|
||||||
// kinds: [EventKind.METADATA, EventKind.NOTE, EventKind.RELAY, EventKind.CONTACT, EventKind.REACTION],
|
// kinds: [EventKind.NOTE],
|
||||||
|
// authors: [pubkey],
|
||||||
|
// limit: 200,
|
||||||
|
// }, () => {}, { subId: 'foo' }))
|
||||||
|
// handles.push(this.client.subscribe({
|
||||||
|
// kinds: [EventKind.REACTION],
|
||||||
|
// authors: [pubkey],
|
||||||
|
// limit: 100,
|
||||||
|
// }))
|
||||||
|
// handles.push(this.client.subscribe({
|
||||||
|
// kinds: [EventKind.METADATA, EventKind.CONTACT],
|
||||||
// authors: [pubkey],
|
// authors: [pubkey],
|
||||||
// }))
|
// }))
|
||||||
// Followers
|
// // handles.push(this.client.subscribe({
|
||||||
handles.push(this.client.subscribe({
|
// // kinds: [EventKind.METADATA, EventKind.NOTE, EventKind.RELAY, EventKind.CONTACT, EventKind.REACTION],
|
||||||
kinds: [EventKind.CONTACT],
|
// // authors: [pubkey],
|
||||||
'#p': [pubkey]
|
// // }))
|
||||||
}))
|
// // Followers
|
||||||
return handles
|
// handles.push(this.client.subscribe({
|
||||||
|
// kinds: [EventKind.CONTACT],
|
||||||
|
// '#p': [pubkey],
|
||||||
|
// limit: 2000,
|
||||||
|
// }))
|
||||||
|
// return handles
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelStream(subIds) {
|
cancelStream(subIds) {
|
||||||
|
// FIXME
|
||||||
if (!Array.isArray(subIds)) subIds = [subIds]
|
if (!Array.isArray(subIds)) subIds = [subIds]
|
||||||
for (const subId of subIds) {
|
for (const subId of subIds) {
|
||||||
this.client.unsubscribe(subId)
|
this.client.unsubscribe(subId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchEvent(id) {
|
|
||||||
this.fetchSingle({
|
|
||||||
ids: [id],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchSingle(filters) {
|
|
||||||
const filtersWithLimit = Object.assign({}, filters, {limit: 1})
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.client.subscribe(
|
|
||||||
filtersWithLimit,
|
|
||||||
(event, relay) => {
|
|
||||||
resolve(this.addEvent(event, relay))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cancelAfter: 'single'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchMultiple(filters, limit = 100) {
|
|
||||||
const filtersWithLimit = Object.assign({}, filters, {limit})
|
|
||||||
let unsubscribeTimeout
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const objects = []
|
|
||||||
this.client.subscribe(
|
|
||||||
filtersWithLimit,
|
|
||||||
(event, relay, subId) => {
|
|
||||||
const obj = this.addEvent(event, relay)
|
|
||||||
if (!obj) return
|
|
||||||
|
|
||||||
// TODO Deduplicate
|
|
||||||
objects.push(obj)
|
|
||||||
if (objects.length >= limit) {
|
|
||||||
this.client.unsubscribe(subId)
|
|
||||||
resolve(objects)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eoseCallback: (_relay, subId) => {
|
|
||||||
if (unsubscribeTimeout) clearTimeout(unsubscribeTimeout)
|
|
||||||
unsubscribeTimeout = setTimeout(() => {
|
|
||||||
this.client.unsubscribe(subId)
|
|
||||||
resolve(objects)
|
|
||||||
}, 250)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
streamEvents(filters, initialFetchSize, eventCallback, initialFetchCompleteCallback, opts) {
|
streamEvents(filters, initialFetchSize, eventCallback, initialFetchCompleteCallback, opts) {
|
||||||
const filtersWithLimit = Object.assign({}, filters, {limit: initialFetchSize})
|
const filtersWithLimit = Object.assign({}, filters, {limit: initialFetchSize})
|
||||||
|
|
||||||
let numEventsSeen = 0
|
let numEventsSeen = 0
|
||||||
let initialFetchComplete = false
|
let initialFetchComplete = false
|
||||||
|
|
||||||
return this.client.subscribe(
|
const sub = this.client.subscribe(filtersWithLimit, opts.subId || null)
|
||||||
filtersWithLimit,
|
const timer = setTimeout(() => {
|
||||||
(event, relay) => {
|
if (!initialFetchComplete) {
|
||||||
|
initialFetchComplete = true
|
||||||
|
if (initialFetchCompleteCallback) initialFetchCompleteCallback()
|
||||||
|
}
|
||||||
|
}, opts.timeout || 5000)
|
||||||
|
sub.on('event', (event, relay) => {
|
||||||
const known = this.hasEvent(event.id)
|
const known = this.hasEvent(event.id)
|
||||||
const obj = this.addEvent(event, relay)
|
const obj = this.addEvent(event, relay)
|
||||||
if (!obj || known) return
|
if (!obj || known) return
|
||||||
@ -359,20 +333,19 @@ export const useNostrStore = defineStore('nostr', {
|
|||||||
|
|
||||||
if (++numEventsSeen >= initialFetchSize && !initialFetchComplete) {
|
if (++numEventsSeen >= initialFetchSize && !initialFetchComplete) {
|
||||||
initialFetchComplete = true
|
initialFetchComplete = true
|
||||||
|
clearTimeout(timer)
|
||||||
if (initialFetchCompleteCallback) initialFetchCompleteCallback()
|
if (initialFetchCompleteCallback) initialFetchCompleteCallback()
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
{
|
sub.on('complete', () => {
|
||||||
subId: opts.subId || null,
|
|
||||||
cancelAfter: 'never',
|
|
||||||
eoseCallback: () => {
|
|
||||||
if (!initialFetchComplete) {
|
if (!initialFetchComplete) {
|
||||||
initialFetchComplete = true
|
initialFetchComplete = true
|
||||||
|
clearTimeout(timer)
|
||||||
if (initialFetchCompleteCallback) initialFetchCompleteCallback()
|
if (initialFetchCompleteCallback) initialFetchCompleteCallback()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
)
|
return sub
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,51 @@
|
|||||||
import {Observable} from 'src/nostr/utils'
|
import {Observable} from 'src/nostr/utils'
|
||||||
|
import Event from 'src/nostr/model/Event'
|
||||||
|
|
||||||
|
export class Subscription extends Observable {
|
||||||
|
constructor(relay, subId, closeAfter) {
|
||||||
|
super()
|
||||||
|
this.relay = relay
|
||||||
|
this.subId = subId
|
||||||
|
this.closeAfter = closeAfter
|
||||||
|
this.closed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.closed) return
|
||||||
|
this.closed = true
|
||||||
|
this.relay.unsubscribe(this.subId)
|
||||||
|
this.emit('close', this.relay, this.subId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent(ev) {
|
||||||
|
const event = Event.from(ev)
|
||||||
|
if (!event.validate()) {
|
||||||
|
// TODO Close relay?
|
||||||
|
console.error(`Invalid event from ${this.relay}`, event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('event', event, this.relay, this.subId)
|
||||||
|
|
||||||
|
if (this.closeAfter === CloseAfter.SINGLE) {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEose() {
|
||||||
|
this.emit('eose', this.relay, this.subId)
|
||||||
|
|
||||||
|
if (this.closeAfter === CloseAfter.EOSE) {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CloseAfter = {
|
||||||
|
SINGLE: 'single',
|
||||||
|
EOSE: 'eose',
|
||||||
|
NEVER: 'never',
|
||||||
|
}
|
||||||
|
|
||||||
export class Relay extends Observable {
|
export class Relay extends Observable {
|
||||||
constructor(url, opts) {
|
constructor(url, opts) {
|
||||||
@ -10,6 +57,9 @@ export class Relay extends Observable {
|
|||||||
this.socket.on('close', this.emit.bind(this, 'close', this))
|
this.socket.on('close', this.emit.bind(this, 'close', this))
|
||||||
this.socket.on('error', this.emit.bind(this, 'error', this))
|
this.socket.on('error', this.emit.bind(this, 'error', this))
|
||||||
this.socket.on('message', this.onMessage.bind(this))
|
this.socket.on('message', this.onMessage.bind(this))
|
||||||
|
|
||||||
|
this.subs = {}
|
||||||
|
this.nextSubId = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
@ -24,16 +74,21 @@ export class Relay extends Observable {
|
|||||||
return this.socket.isConnected()
|
return this.socket.isConnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
send(event) {
|
publish(event) {
|
||||||
this.socket.send(['EVENT', event])
|
this.socket.send(['EVENT', event])
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(subId, filters) {
|
subscribe(filters, subId = null, closeAfter = CloseAfter.NEVER) {
|
||||||
console.log(`${this} subscribing to ${subId}`, filters)
|
if (!subId) subId = `sub${this.nextSubId++}`
|
||||||
|
const sub = new Subscription(this, subId, closeAfter)
|
||||||
|
this.subs[subId] = sub
|
||||||
this.socket.send(['REQ', subId, filters])
|
this.socket.send(['REQ', subId, filters])
|
||||||
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(subId) {
|
unsubscribe(subId) {
|
||||||
|
if (!this.subs[subId]) return
|
||||||
|
delete this.subs[subId]
|
||||||
this.socket.send(['CLOSE', subId])
|
this.socket.send(['CLOSE', subId])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +104,7 @@ export class Relay extends Observable {
|
|||||||
processMessage(msg) {
|
processMessage(msg) {
|
||||||
const array = JSON.parse(msg)
|
const array = JSON.parse(msg)
|
||||||
|
|
||||||
if (!Array.isArray(array) || array.length === 0) {
|
if (!Array.isArray(array) || !array.length) {
|
||||||
throw new Error('not a nostr message')
|
throw new Error('not a nostr message')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,12 +113,22 @@ export class Relay extends Observable {
|
|||||||
case 'EVENT': {
|
case 'EVENT': {
|
||||||
Relay.enforceArrayLength(array, 3)
|
Relay.enforceArrayLength(array, 3)
|
||||||
const [_, subId, event] = array
|
const [_, subId, event] = array
|
||||||
|
|
||||||
|
const sub = this.subs[subId]
|
||||||
|
if (sub) sub.onEvent(event)
|
||||||
|
|
||||||
|
// still needed?
|
||||||
this.emit('event', subId, event)
|
this.emit('event', subId, event)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'EOSE': {
|
case 'EOSE': {
|
||||||
Relay.enforceArrayLength(array, 2)
|
Relay.enforceArrayLength(array, 2)
|
||||||
const [_, subId] = array
|
const [_, subId] = array
|
||||||
|
|
||||||
|
const sub = this.subs[subId]
|
||||||
|
if (sub) sub.onEose()
|
||||||
|
|
||||||
|
// still needed?
|
||||||
this.emit('eose', subId)
|
this.emit('eose', subId)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,70 @@
|
|||||||
import {Relay} from 'src/nostr/Relay'
|
import {CloseAfter, Relay} from 'src/nostr/Relay'
|
||||||
import {Observable} from 'src/nostr/utils'
|
import {Observable} from 'src/nostr/utils'
|
||||||
|
|
||||||
export default class extends Observable {
|
class MultiSubscription extends Observable {
|
||||||
|
constructor(subId, subs) {
|
||||||
|
super()
|
||||||
|
this.subId = subId
|
||||||
|
this.subs = {}
|
||||||
|
for (const sub of subs) {
|
||||||
|
this.subs[sub.relay] = sub
|
||||||
|
this.setupListeners(sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners(sub) {
|
||||||
|
sub.on('event', this.emit.bind(this, 'event'))
|
||||||
|
sub.on('eose', this.onEose.bind(this))
|
||||||
|
sub.on('close', this.onClose.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(sub) {
|
||||||
|
console.assert(sub.subId === this.subId, 'invalid subId')
|
||||||
|
this.subs[sub.relay] = sub
|
||||||
|
this.setupListeners(sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(relay = null) {
|
||||||
|
if (relay) {
|
||||||
|
if (this.subs[relay]) this.subs[relay].close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sub of Object.values(this.subs)) {
|
||||||
|
sub.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEose(relay) {
|
||||||
|
const sub = this.subs[relay]
|
||||||
|
if (!sub) return
|
||||||
|
|
||||||
|
// still needed?
|
||||||
|
this.emit('eose', relay, this.subId)
|
||||||
|
|
||||||
|
if (!sub.eoseSeen) {
|
||||||
|
sub.eoseSeen = true
|
||||||
|
if (Object.values(this.subs).every(sub => sub.eoseSeen)) {
|
||||||
|
this.emit('complete', this.subId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(relay) {
|
||||||
|
delete this.subs[relay]
|
||||||
|
if (Object.keys(this.subs).length === 0) {
|
||||||
|
this.emit('close', this.subId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ReplayPool extends Observable {
|
||||||
constructor(urls) {
|
constructor(urls) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.relays = {}
|
this.relays = {}
|
||||||
this.subs = {}
|
this.subs = {}
|
||||||
|
this.nextSubId = 0
|
||||||
|
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
this.add(url)
|
this.add(url)
|
||||||
@ -35,25 +93,30 @@ export default class extends Observable {
|
|||||||
delete this.relays[url]
|
delete this.relays[url]
|
||||||
}
|
}
|
||||||
|
|
||||||
send(event) {
|
publish(event) {
|
||||||
for (const relay of this.connectedRelays()) {
|
for (const relay of this.connectedRelays()) {
|
||||||
relay.send(event)
|
relay.publish(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(subId, filters) {
|
subscribe(filters, subId = null, closeAfter = CloseAfter.NEVER) {
|
||||||
console.log(`Subscribing ${subId}`, filters)
|
if (!subId) subId = `sub${this.nextSubId++}`
|
||||||
this.subs[subId] = filters
|
|
||||||
|
const sub = new MultiSubscription(subId, [])
|
||||||
|
sub.on('close', this.unsubscribe.bind(this, subId))
|
||||||
|
|
||||||
|
this.subs[subId] = {sub, filters, closeAfter}
|
||||||
for (const relay of this.connectedRelays()) {
|
for (const relay of this.connectedRelays()) {
|
||||||
relay.subscribe(subId, filters)
|
sub.add(relay.subscribe(filters, subId, closeAfter))
|
||||||
}
|
}
|
||||||
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(subId) {
|
unsubscribe(subId) {
|
||||||
|
const sub = this.subs[subId]
|
||||||
|
if (!sub) return
|
||||||
|
sub.sub.close()
|
||||||
delete this.subs[subId]
|
delete this.subs[subId]
|
||||||
for (const relay of this.connectedRelays()) {
|
|
||||||
relay.unsubscribe(subId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
@ -75,8 +138,8 @@ export default class extends Observable {
|
|||||||
onOpen(relay) {
|
onOpen(relay) {
|
||||||
console.log(`Connected to ${relay}`, relay)
|
console.log(`Connected to ${relay}`, relay)
|
||||||
for (const subId of Object.keys(this.subs)) {
|
for (const subId of Object.keys(this.subs)) {
|
||||||
// console.log(`Subscribing ${subId} with ${relay}`, this.subs[subId])
|
const sub = this.subs[subId]
|
||||||
relay.subscribe(subId, this.subs[subId])
|
sub.sub.add(relay.subscribe(sub.filters, subId, sub.closeAfter))
|
||||||
}
|
}
|
||||||
this.emit('open', relay)
|
this.emit('open', relay)
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,31 @@ export const EventKind = {
|
|||||||
CHATROOM: 42,
|
CHATROOM: 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tag = {
|
export const TagType = {
|
||||||
PUBKEY: 'p',
|
PUBKEY: 'p',
|
||||||
EVENT: 'e',
|
EVENT: 'e',
|
||||||
}
|
}
|
||||||
|
|
||||||
class EventRefs extends Array {
|
export class Tag {
|
||||||
|
constructor(type, ref, relay = null, meta = null) {
|
||||||
|
this.type = type
|
||||||
|
this.ref = ref
|
||||||
|
this.relay = relay
|
||||||
|
this.meta = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(array) {
|
||||||
|
if (!array || !array[0] || !array[1]) return
|
||||||
|
return new Tag(
|
||||||
|
array[0],
|
||||||
|
array[1],
|
||||||
|
array[2] || null,
|
||||||
|
array[3] || null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventRefs extends Array {
|
||||||
constructor(refs) {
|
constructor(refs) {
|
||||||
// FIXME limit number of refs here
|
// FIXME limit number of refs here
|
||||||
super(...refs)
|
super(...refs)
|
||||||
@ -42,7 +61,7 @@ export default class Event {
|
|||||||
this.pubkey = opts.pubkey
|
this.pubkey = opts.pubkey
|
||||||
this.created_at = opts.createdAt || opts.created_at
|
this.created_at = opts.createdAt || opts.created_at
|
||||||
this.kind = opts.kind
|
this.kind = opts.kind
|
||||||
this.tags = opts.tags || []
|
this.tags = Event.parseTags(opts.tags || [])
|
||||||
this.content = opts.content
|
this.content = opts.content
|
||||||
this.sig = opts.sig
|
this.sig = opts.sig
|
||||||
}
|
}
|
||||||
@ -56,6 +75,16 @@ export default class Event {
|
|||||||
return new Event(opts)
|
return new Event(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static parseTags(tags) {
|
||||||
|
const res = []
|
||||||
|
for (const tag of tags) {
|
||||||
|
const parsed = Tag.from(tag)
|
||||||
|
if (!parsed) continue
|
||||||
|
res.push(parsed)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
get createdAt() {
|
get createdAt() {
|
||||||
return this.created_at
|
return this.created_at
|
||||||
}
|
}
|
||||||
@ -69,16 +98,19 @@ export default class Event {
|
|||||||
return getEventHash(this)
|
return getEventHash(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pubkeyTags() {
|
||||||
|
return this.tags.filter(tag => tag.type === TagType.PUBKEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTags() {
|
||||||
|
return this.tags.filter(tag => tag.type === TagType.EVENT)
|
||||||
|
}
|
||||||
|
|
||||||
pubkeyRefs() {
|
pubkeyRefs() {
|
||||||
return this.tags
|
return this.pubkeyTags().map(tag => tag.ref)
|
||||||
.filter(tag => tag[0] === Tag.PUBKEY && tag[1])
|
|
||||||
.map(tag => tag[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eventRefs() {
|
eventRefs() {
|
||||||
const refs = this.tags
|
return new EventRefs(this.eventTags().map(tag => tag.ref))
|
||||||
.filter(tag => tag[0] === Tag.EVENT && tag[1])
|
|
||||||
.map(tag => tag[1])
|
|
||||||
return new EventRefs(refs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {EventKind} from 'src/nostr/model/Event'
|
import {EventKind, EventRefs, TagType} from 'src/nostr/model/Event'
|
||||||
import {isEmoji} from 'src/utils/utils'
|
import {isEmoji} from 'src/utils/utils'
|
||||||
|
|
||||||
export default class Note {
|
export default class Note {
|
||||||
@ -7,11 +7,8 @@ export default class Note {
|
|||||||
this.kind = args.kind || EventKind.NOTE
|
this.kind = args.kind || EventKind.NOTE
|
||||||
this.author = args.author || args.pubkey
|
this.author = args.author || args.pubkey
|
||||||
this.createdAt = args.createdAt
|
this.createdAt = args.createdAt
|
||||||
this.content = args.content
|
this.content = args.content || ''
|
||||||
this.refs = {
|
this.tags = args.tags || []
|
||||||
events: args.refs?.events || [],
|
|
||||||
pubkeys: args.refs?.pubkeys || [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static from(event) {
|
static from(event) {
|
||||||
@ -24,15 +21,12 @@ export default class Note {
|
|||||||
author: event.pubkey,
|
author: event.pubkey,
|
||||||
createdAt: event.createdAt,
|
createdAt: event.createdAt,
|
||||||
content,
|
content,
|
||||||
refs: {
|
tags: event.tags,
|
||||||
events: event.eventRefs(),
|
|
||||||
pubkeys: event.pubkeyRefs(),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isReply() {
|
hasAncestor() {
|
||||||
return !this.refs.events.isEmpty()
|
return !this.eventRefs().isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
canReply() {
|
canReply() {
|
||||||
@ -40,16 +34,39 @@ export default class Note {
|
|||||||
}
|
}
|
||||||
|
|
||||||
root() {
|
root() {
|
||||||
return this.refs.events.root()
|
return this.eventRefs().root()
|
||||||
}
|
}
|
||||||
|
|
||||||
ancestor() {
|
ancestor() {
|
||||||
return this.refs.events.ancestor()
|
return this.eventRefs().ancestor()
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkeyRefs() {
|
||||||
|
return this.tags
|
||||||
|
.filter(tag => tag.type === TagType.PUBKEY)
|
||||||
|
.map(tag => tag.ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventRefs() {
|
||||||
|
const refs = this.tags
|
||||||
|
.filter(tag => tag.type === TagType.EVENT)
|
||||||
|
.map(tag => tag.ref)
|
||||||
|
return new EventRefs(refs)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTagRefs() {
|
||||||
|
const regex = /#\[([0-9]+)]/ig
|
||||||
|
let refs = []
|
||||||
|
let match
|
||||||
|
while ((match = regex.exec(this.content))) {
|
||||||
|
refs.push(match[1])
|
||||||
|
}
|
||||||
|
return refs
|
||||||
}
|
}
|
||||||
|
|
||||||
isReaction() {
|
isReaction() {
|
||||||
return this.kind === EventKind.REACTION
|
return this.kind === EventKind.REACTION
|
||||||
|| (this.isReply() && Note.isReactionContent(this.content))
|
|| (this.hasAncestor() && Note.isReactionContent(this.content))
|
||||||
}
|
}
|
||||||
|
|
||||||
static isReaction(event) {
|
static isReaction(event) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {EventKind, Tag} from 'src/nostr/model/Event'
|
import {EventKind} from 'src/nostr/model/Event'
|
||||||
|
|
||||||
export const useContactStore = defineStore('contact', {
|
export const useContactStore = defineStore('contact', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@ -26,12 +26,11 @@ export const useContactStore = defineStore('contact', {
|
|||||||
const newContacts = []
|
const newContacts = []
|
||||||
newContacts.lastUpdatedAt = event.createdAt
|
newContacts.lastUpdatedAt = event.createdAt
|
||||||
|
|
||||||
const tags = event.tags.filter(tag => tag[0] === Tag.PUBKEY && tag[1])
|
for (const tag of event.pubkeyTags()) {
|
||||||
for (const tag of tags) {
|
|
||||||
newContacts.push({
|
newContacts.push({
|
||||||
pubkey: tag[1],
|
pubkey: tag.ref,
|
||||||
relay: tag[2],
|
relay: tag.relay,
|
||||||
name: tag[3],
|
name: tag.meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,9 @@ export const useNoteStore = defineStore('note', {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
notes: {},
|
notes: {},
|
||||||
replies: {},
|
replies: {},
|
||||||
byAuthor: {},
|
reactions: {},
|
||||||
|
notesByAuthor: {},
|
||||||
|
reactionsByAuthor: {},
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
get(state) {
|
get(state) {
|
||||||
@ -19,9 +21,15 @@ export const useNoteStore = defineStore('note', {
|
|||||||
repliesTo(state) {
|
repliesTo(state) {
|
||||||
return (id, order) => (state.replies[id] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
return (id, order) => (state.replies[id] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
||||||
},
|
},
|
||||||
allByAuthor(state) {
|
reactionsTo(state) {
|
||||||
return (pubkey, order) => (state.byAuthor[pubkey] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
return (id, order) => (state.reactions[id] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
||||||
}
|
},
|
||||||
|
getNotesByAuthor(state) {
|
||||||
|
return (pubkey, order) => (state.notesByAuthor[pubkey] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
||||||
|
},
|
||||||
|
getReactionsByAuthor(state) {
|
||||||
|
return (pubkey, order) => (state.reactionsByAuthor[pubkey] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
addEvent(event) {
|
addEvent(event) {
|
||||||
@ -30,19 +38,24 @@ export const useNoteStore = defineStore('note', {
|
|||||||
|
|
||||||
// Skip if note already exists
|
// Skip if note already exists
|
||||||
if (this.notes[note.id]) return this.notes[note.id]
|
if (this.notes[note.id]) return this.notes[note.id]
|
||||||
|
|
||||||
this.notes[note.id] = note
|
this.notes[note.id] = note
|
||||||
|
|
||||||
if (!this.byAuthor[note.author]) {
|
const byAuthor = note.isReaction()
|
||||||
this.byAuthor[note.author] = []
|
? this.reactionsByAuthor
|
||||||
|
: this.notesByAuthor
|
||||||
|
if (!byAuthor[note.author]) {
|
||||||
|
byAuthor[note.author] = []
|
||||||
}
|
}
|
||||||
this.byAuthor[note.author].push(note)
|
byAuthor[note.author].push(note)
|
||||||
|
|
||||||
if (note.isReply()) {
|
if (note.hasAncestor()) {
|
||||||
if (!this.replies[note.ancestor()]) {
|
const map = note.isReaction()
|
||||||
this.replies[note.ancestor()] = []
|
? this.reactions
|
||||||
|
: this.replies
|
||||||
|
if (!map[note.ancestor()]) {
|
||||||
|
map[note.ancestor()] = []
|
||||||
}
|
}
|
||||||
this.replies[note.ancestor()].push(note)
|
map[note.ancestor()].push(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notes[note.id]
|
return this.notes[note.id]
|
||||||
|
@ -52,7 +52,7 @@ export const useReactionStore = defineStore('reaction', {
|
|||||||
actions: {
|
actions: {
|
||||||
addEvent(event) {
|
addEvent(event) {
|
||||||
const note = Note.from(event)
|
const note = Note.from(event)
|
||||||
if (!note || !note.isReply()) return false
|
if (!note || !note.hasAncestor()) return false
|
||||||
|
|
||||||
// Skip if reaction already exists
|
// Skip if reaction already exists
|
||||||
if (this.reactions[note.id]) return this.reactions[note.id]
|
if (this.reactions[note.id]) return this.reactions[note.id]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page>
|
<q-page>
|
||||||
<div class="page-header-container">
|
<div class="page-header-container">
|
||||||
<PageHeader>
|
<PageHeader logo>
|
||||||
<template #addon>
|
<template #addon>
|
||||||
<div class="addon-menu">
|
<div class="addon-menu">
|
||||||
<div class="addon-menu-icon">
|
<div class="addon-menu-icon">
|
||||||
@ -40,7 +40,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Thread v-for="thread in feedItems" :key="thread.id" :thread="thread" class="full-width" />
|
<Thread v-for="thread in feedItems" :key="thread[0].id" :thread="thread" class="full-width" />
|
||||||
|
|
||||||
<!-- <ButtonLoadMore-->
|
<!-- <ButtonLoadMore-->
|
||||||
<!-- :loading="loadingMore"-->
|
<!-- :loading="loadingMore"-->
|
||||||
@ -80,9 +80,9 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
feeds: {},
|
||||||
availableFeeds: ['global'],
|
availableFeeds: ['global'],
|
||||||
selectedFeed: 'global',
|
selectedFeed: 'global',
|
||||||
feeds: {},
|
|
||||||
recentlyLoaded: true,
|
recentlyLoaded: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -73,7 +73,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
rootId() {
|
rootId() {
|
||||||
if (!this.noteLoaded) return
|
if (!this.noteLoaded) return
|
||||||
return this.note.isReply()
|
return this.note.hasAncestor()
|
||||||
? this.note.root()
|
? this.note.root()
|
||||||
: this.note.id
|
: this.note.id
|
||||||
},
|
},
|
||||||
@ -197,7 +197,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
allAncestors(note) {
|
allAncestors(note) {
|
||||||
if (!note.isReply()) return []
|
if (!note.hasAncestor()) return []
|
||||||
const ancestor = this.nostr.getNote(note.ancestor())
|
const ancestor = this.nostr.getNote(note.ancestor())
|
||||||
if (!ancestor) {
|
if (!ancestor) {
|
||||||
console.error(`Couldn't fetch ancestor ${note.ancestor()}`)
|
console.error(`Couldn't fetch ancestor ${note.ancestor()}`)
|
||||||
|
@ -117,10 +117,10 @@ export default defineComponent({
|
|||||||
return this.nostr.getNotesByAuthor(this.pubkey)
|
return this.nostr.getNotesByAuthor(this.pubkey)
|
||||||
},
|
},
|
||||||
posts() {
|
posts() {
|
||||||
return this.notes.filter(note => !note.isReply())
|
return this.notes.filter(note => !note.hasAncestor())
|
||||||
},
|
},
|
||||||
replies() {
|
replies() {
|
||||||
return this.notes.filter(note => note.isReply())
|
return this.notes.filter(note => note.hasAncestor())
|
||||||
.map(note => [this.nostr.getNote(note.ancestor()), note])
|
.map(note => [this.nostr.getNote(note.ancestor()), note])
|
||||||
},
|
},
|
||||||
reactions() {
|
reactions() {
|
||||||
@ -143,7 +143,7 @@ export default defineComponent({
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'followers',
|
name: 'followers',
|
||||||
params: {
|
params: {
|
||||||
pubkey: hexToBech32(this.pubkey),
|
pubkey: hexToBech32(this.pubkey, 'npub'),
|
||||||
tab,
|
tab,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import {hexToBech32} from 'src/utils/utils'
|
import {hexToBech32, isBech32} from 'src/utils/utils'
|
||||||
|
|
||||||
|
function ensureBech32(str, prefix) {
|
||||||
|
return isBech32(str)
|
||||||
|
? str
|
||||||
|
: hexToBech32(str, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
@ -6,7 +12,7 @@ export default {
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
params: {
|
params: {
|
||||||
pubkey: hexToBech32(pubkey, 'npub')
|
pubkey: ensureBech32(pubkey, 'npub')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -14,9 +20,16 @@ export default {
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'thread',
|
name: 'thread',
|
||||||
params: {
|
params: {
|
||||||
id: hexToBech32(id, 'note')
|
id: ensureBech32(id, 'note')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
// TODO There must be a better way to do this
|
||||||
|
linkToProfile(pubkey) {
|
||||||
|
return `/profile/${ensureBech32(pubkey, 'npub')}`
|
||||||
|
},
|
||||||
|
linkToThread(id) {
|
||||||
|
return `/thread/${ensureBech32(id, 'note')}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,10 @@ export function isEmoji(str) {
|
|||||||
return !/\w/.test(str) && PATTERN_EMOJI.test(str)
|
return !/\w/.test(str) && PATTERN_EMOJI.test(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isBech32(str) {
|
||||||
|
return /^(note|npub|nsec)[a-z0-9]{59}$/i.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
export function bech32prefix(bech32) {
|
export function bech32prefix(bech32) {
|
||||||
if (!bech32 || bech32.length < 4) return
|
if (!bech32 || bech32.length < 4) return
|
||||||
return bech32.substr(0, 4).toLowerCase()
|
return bech32.substr(0, 4).toLowerCase()
|
||||||
@ -38,3 +42,9 @@ export function hexToBech32(hex, prefix = '') {
|
|||||||
}
|
}
|
||||||
return bech32encode(prefix, buffer)
|
return bech32encode(prefix, buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shortenBech32(str) {
|
||||||
|
if (!str) return str
|
||||||
|
let keepStart = isBech32(str) ? 9 : 5
|
||||||
|
return str.substr(0, keepStart) + '…' + str.substr(-5)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user