mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
update: absurd-sql
This commit is contained in:
parent
acecce96b8
commit
cc15c9b54c
2337
package-lock.json
generated
2337
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "astral",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "decentralized social platform (nostr client)",
|
||||
"productName": "astral",
|
||||
"author": "monica <monlovesmango@protonmail.com>",
|
||||
@ -11,8 +11,11 @@
|
||||
"test": "echo \"No test specified\" && exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jlongster/sql.js": "^1.6.7",
|
||||
"@quasar/extras": "^1.0.0",
|
||||
"absurd-sql": "^0.0.53",
|
||||
"bech32-buffer": "^0.2.0",
|
||||
"codemirror": "5",
|
||||
"core-js": "^3.6.5",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"emoji-mart-vue-fast": "^10.2.1",
|
||||
@ -24,14 +27,10 @@
|
||||
"markdown-it-sub": "1.0",
|
||||
"markdown-it-sup": "1.0",
|
||||
"markdown-it-task-lists": "2.1",
|
||||
"nostr-tools": "^0.22.2",
|
||||
"pouchdb-adapter-idb": "^6.4.3",
|
||||
"pouchdb-core": "^6.4.3",
|
||||
"pouchdb-mapreduce": "^6.4.3",
|
||||
"pouchdb-upsert": "^2.2.0",
|
||||
"mergebounce": "^0.1.1",
|
||||
"nostr-tools": "0.23.4",
|
||||
"quasar": "2.5.5",
|
||||
"readable-stream": "3.6.0",
|
||||
"stream": "^0.0.2",
|
||||
"tributejs": "^5.1.3",
|
||||
"vue": "^3.0.0",
|
||||
"vue-i18n": "^9.2.0-beta.40",
|
||||
|
@ -78,7 +78,9 @@ module.exports = configure(function (ctx) {
|
||||
// blergh
|
||||
extendWebpack(cfg) {
|
||||
cfg.plugins.push(
|
||||
new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'] })
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer']
|
||||
})
|
||||
)
|
||||
cfg.resolve.alias = cfg.resolve.alias || {}
|
||||
cfg.resolve.alias.stream = 'readable-stream'
|
||||
@ -86,8 +88,12 @@ module.exports = configure(function (ctx) {
|
||||
cfg.resolve.fallback.buffer = require.resolve('buffer/')
|
||||
cfg.resolve.fallback.stream = require.resolve('readable-stream')
|
||||
cfg.resolve.fallback.crypto = false
|
||||
cfg.resolve.fallback.path = false
|
||||
cfg.resolve.fallback.fs = false
|
||||
cfg.experiments = cfg.experiments || {}
|
||||
cfg.experiments.asyncWebAssembly = true
|
||||
cfg.module = cfg.module || { rules: [] }
|
||||
cfg.module.rules.push({ test: /\.wasm$/, type: 'asset/inline' })
|
||||
},
|
||||
},
|
||||
|
||||
@ -98,6 +104,10 @@ module.exports = configure(function (ctx) {
|
||||
},
|
||||
port: 8080,
|
||||
open: false, // opens browser window automatically
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
},
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'https://astral.ninja',
|
||||
|
@ -47,6 +47,10 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
element: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -57,9 +61,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
copyText(defaultText) {
|
||||
// console.log('defaultText: ', defaultText)
|
||||
let selection = window.getSelection().toString()
|
||||
// console.log('selection: ', selection)
|
||||
let selection = this.element?.getSelection()?.toString()
|
||||
if (selection) {
|
||||
return selection
|
||||
} else return defaultText
|
||||
|
56
src/components/BaseButtonLoadMore.vue
Normal file
56
src/components/BaseButtonLoadMore.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<q-separator color='accent'/>
|
||||
<q-btn-group
|
||||
flat
|
||||
spread
|
||||
dense
|
||||
text-color="accent"
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
:loading='loadingMore'
|
||||
flat
|
||||
color="accent"
|
||||
class='text-weight-light'
|
||||
style='letter-spacing: .1rem;'
|
||||
:label='reachedEnd ? "reached end" : label'
|
||||
:disable='reachedEnd'
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<template #loading>
|
||||
<div class='row justify-center'>
|
||||
<q-spinner-orbit color="accent" size='sm' />
|
||||
</div>
|
||||
</template>
|
||||
</q-btn>
|
||||
</q-btn-group>
|
||||
<q-separator color='accent'/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseButtonLoadMore',
|
||||
emits: ['click'],
|
||||
props: {
|
||||
loadingMore: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
reachedEnd: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'load more'
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -29,7 +29,6 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
// import {onEventUpdate} from '../db'
|
||||
import BaseRelayList from 'components/BaseRelayList.vue'
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -10,9 +10,7 @@ import subscript from 'markdown-it-sub'
|
||||
import superscript from 'markdown-it-sup'
|
||||
import deflist from 'markdown-it-deflist'
|
||||
import taskLists from 'markdown-it-task-lists'
|
||||
// import markdownHighlightJs from 'markdown-it-highlightjs'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
// import linkPreview from 'markdown-it-link-preview'
|
||||
|
||||
import helpersMixin from '../utils/mixin'
|
||||
|
||||
@ -58,7 +56,7 @@ md.use(subscript)
|
||||
trimmed.endsWith('.jpeg') ||
|
||||
trimmed.endsWith('.jpg')
|
||||
) {
|
||||
return `<img src="${src}" style="max-width: 90%; max-height: 30%">`
|
||||
return `<img src="${src}" crossorigin style="max-width: 90%; max-height: 30%">`
|
||||
} else if (
|
||||
trimmed.endsWith('.mp4') ||
|
||||
trimmed.endsWith('.webm') ||
|
||||
|
@ -19,12 +19,12 @@
|
||||
(idx === sequence.length - 1 ? " last-message" : "")'
|
||||
>
|
||||
<div
|
||||
v-if='!isEmbeded && evt.taggedEvents && render'
|
||||
v-if='!isEmbeded && taggedEvents[evt.id] && render'
|
||||
class='flex column text-left full-width q-pb-xs embeded-message'
|
||||
style='display: block;'
|
||||
:clickable='false'
|
||||
>
|
||||
<div v-for='(taggedEvent, index) in evt.taggedEvents' :key='taggedEvent.id + "_" + index + "_" + render'>
|
||||
<div v-for='(taggedEvent) in taggedEvents[evt.id]' :key='taggedEvent.id + "_" + taggedEvents[evt.id].length + "_" + render'>
|
||||
<div v-if='taggedEvent.kind === 1 || taggedEvent.kind === 2' class='reposts'>
|
||||
<BasePost
|
||||
:event='taggedEvent'
|
||||
@ -39,6 +39,7 @@
|
||||
class='no-padding no-margin'
|
||||
:is-embeded='true'
|
||||
clickable
|
||||
@mounted="$emit('mounted')"
|
||||
@click.capture.prevent.stop
|
||||
/>
|
||||
</div>
|
||||
@ -61,7 +62,6 @@
|
||||
>
|
||||
<q-list dense class='flex column q-gutter-xs q-pa-xs'>
|
||||
<div v-close-popup>
|
||||
<!-- <q-item-section>reply</q-item-section> -->
|
||||
<BaseButtonReply
|
||||
button-class='text-accent full-width justify-start'
|
||||
:verbose='true'
|
||||
@ -69,7 +69,6 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-close-popup>
|
||||
<!-- <div-section>copy</div-section> -->
|
||||
<BaseButtonCopy
|
||||
button-class='text-accent full-width justify-start'
|
||||
:button-text='evt.interpolated.text'
|
||||
@ -77,7 +76,6 @@
|
||||
/>
|
||||
</div>
|
||||
<div >
|
||||
<!-- <div-section>info</div-section> -->
|
||||
<BaseButtonInfo
|
||||
button-class='text-accent full-width justify-start'
|
||||
:event='evt'
|
||||
@ -87,7 +85,6 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<!-- <div-section>relays</div-section> -->
|
||||
<BaseButtonRelays
|
||||
button-class='text-accent full-width justify-start'
|
||||
:event='evt'
|
||||
@ -105,10 +102,7 @@
|
||||
|
||||
<script>
|
||||
import { useQuasar } from 'quasar'
|
||||
// import {decrypt} from 'nostr-tools/nip04'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
// import {pool} from '../pool'
|
||||
// import {dbGetEvent} from '../db'
|
||||
import BaseButtonRelays from 'components/BaseButtonRelays.vue'
|
||||
import BaseButtonInfo from 'components/BaseButtonInfo.vue'
|
||||
import BaseButtonCopy from 'components/BaseButtonCopy.vue'
|
||||
@ -117,7 +111,7 @@ import BaseMarkdown from 'components/BaseMarkdown.vue'
|
||||
|
||||
export default {
|
||||
name: 'BaseMessage',
|
||||
emits: ['reply', 'scroll-to'],
|
||||
emits: ['reply', 'mounted'],
|
||||
mixins: [helpersMixin],
|
||||
props: {
|
||||
event: {type: Object, required: true},
|
||||
@ -138,13 +132,12 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
// metadataDialog: false,
|
||||
invisible: true,
|
||||
reposts: {},
|
||||
menu: {},
|
||||
render: 1,
|
||||
contextMenus: [],
|
||||
persistentMenu: false,
|
||||
taggedEvents: {}
|
||||
}
|
||||
},
|
||||
|
||||
@ -152,11 +145,9 @@ export default {
|
||||
sequence() {
|
||||
let sequence = [this.event].concat(this.event.appended).filter(x => x)
|
||||
// this.interpolateMessageMentions(sequence)
|
||||
if (this.render) return sequence
|
||||
return sequence
|
||||
},
|
||||
// text() {
|
||||
// return this.sequence.map(evt => this.interpolateMentions(evt.text, evt.tags).text)
|
||||
// },
|
||||
|
||||
sent() {
|
||||
return this.event.pubkey === this.$store.state.keys.pub
|
||||
@ -164,23 +155,17 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// this.menu = this.menu.fill(false, 0, this.sequence.length)
|
||||
setTimeout(() => {
|
||||
this.invisible = false
|
||||
}, 20)
|
||||
if (this.event.taggedEvents)
|
||||
setTimeout(() => {
|
||||
// console.log('rerender event: ', this.event)
|
||||
this.render++
|
||||
this.$emit('scroll-to')
|
||||
}, 1000)
|
||||
// this.sequence.forEach(event => {
|
||||
// if (event.interpolated.mentionEvents) {
|
||||
// this.reposts[event.id] = []
|
||||
// this.listenReposts(event.interpolated.mentionEvents, this.reposts[event.id])
|
||||
// }
|
||||
// this.menu[event.id] = false
|
||||
// })
|
||||
for (let ev of this.sequence) {
|
||||
let tagged = ev.tags?.filter(([t, v]) => t === 'e' && v).map(([t, v]) => v) || []
|
||||
if (tagged.length) {
|
||||
this.taggedEvents[ev.id] = []
|
||||
this.processTaggedEvents(tagged, this.taggedEvents[ev.id])
|
||||
}
|
||||
}
|
||||
if (this.event.created_at < this.$store.state.lastMessageRead[this.$route.params.pubkey]) this.$emit('mounted')
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -197,21 +182,9 @@ export default {
|
||||
togglePersistentMenu(value) {
|
||||
this.persistentMenu = value
|
||||
}
|
||||
|
||||
// copyText(defaultText) {
|
||||
// let selection = window.getSelection().toString()
|
||||
// if (selection) {
|
||||
// return selection
|
||||
// } else return defaultText
|
||||
// },
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- :class='event.pubkey === $store.state.keys.pub ? "bg-primary" : "bg-secondary"'
|
||||
|
||||
margin: .8rem 0;
|
||||
gap: .25rem;
|
||||
-->
|
||||
<style lang='scss'>
|
||||
.message-sent,
|
||||
.message-received {
|
||||
|
@ -1,4 +1,3 @@
|
||||
<!-- :clickable='$route.params.eventId !== event.id && !replying' -->
|
||||
<template>
|
||||
<q-item
|
||||
color='accent'
|
||||
@ -59,7 +58,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- :style='"height: " + (childReplyContentHeight) + "px;"' -->
|
||||
<q-item-section>
|
||||
<q-item-section ref='postContent'>
|
||||
<q-item-label caption class="text-secondary" style='opacity: .7;'>
|
||||
@ -118,7 +116,6 @@
|
||||
:class='replying ? "justify-between" : "justify-end"'
|
||||
>
|
||||
<div class='text-primary text-thin col q-pl-xs' style=' font-size: 90%; font-weight: 300;'>{{replyMode}}</div>
|
||||
<!-- @reply="replying = !replying" -->
|
||||
<div class='flex row no-wrap'>
|
||||
<q-tabs
|
||||
v-model='replyMode'
|
||||
@ -177,8 +174,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <q-separator v-if='replyMode' color='primary' size='1px' /> -->
|
||||
<!-- <Reply v-if="event" :event="event"/> -->
|
||||
</q-item-section>
|
||||
<q-item-section v-if="replyMode" class='full-width new-reply-box' ref='replyContent'>
|
||||
<q-tab-panels
|
||||
@ -206,7 +201,6 @@
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-item-section>
|
||||
<!-- <q-separator v-if='replyMode' color='primary' size='1px' class='q-mt-sm'/> -->
|
||||
|
||||
<q-item v-if='hasReplyChildren' class='no-padding no-border no-margin column full-width' >
|
||||
<div v-for="thread in event.replies" :key="thread[0].id" ref="childReplyContent">
|
||||
@ -223,13 +217,9 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
// import VueForceNextTick from 'vue-force-next-tick'
|
||||
import {nextTick} from 'vue'
|
||||
import {pool} from '../pool'
|
||||
import {cleanEvent} from '../utils/event'
|
||||
import {dbGetEvent} from '../db'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
// import BaseButtonPost from 'components/BaseButtonPost.vue'
|
||||
import BaseButtonRelays from 'components/BaseButtonRelays.vue'
|
||||
import BaseButtonInfo from 'components/BaseButtonInfo.vue'
|
||||
import BaseButtonCopy from 'components/BaseButtonCopy.vue'
|
||||
@ -238,7 +228,7 @@ import BaseRelayRecommend from 'components/BaseRelayRecommend.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasePost',
|
||||
emits: ['resized', 'add-event'],
|
||||
emits: ['resized', 'add-event', 'mounted'],
|
||||
mixins: [helpersMixin],
|
||||
props: {
|
||||
event: {type: Object, required: true},
|
||||
@ -248,7 +238,6 @@ export default defineComponent({
|
||||
isEmbeded: {type: Boolean, default: false},
|
||||
},
|
||||
components: {
|
||||
// BaseButtonPost,
|
||||
BaseButtonRelays,
|
||||
BaseButtonInfo,
|
||||
BaseButtonCopy,
|
||||
@ -273,34 +262,19 @@ export default defineComponent({
|
||||
|
||||
computed: {
|
||||
tagged() {
|
||||
// let eventTags = this.event.tags.filter(([t, v]) => t === 'e').map(([t, v]) => v)
|
||||
// let lastEventTag = eventTags[eventTags.length - 1]
|
||||
// // console.log('BasePost eventTags: ', eventTags, 'return: ', lastEventTag)
|
||||
// if (lastEventTag) return lastEventTag
|
||||
// for (let i = this.event.tags.length - 1; i >= 0; i--) {
|
||||
// let tag = this.event.tags[i]
|
||||
// if (tag.length === 2 && tag[0] === 'e') {
|
||||
// return tag[1]
|
||||
// }
|
||||
// }
|
||||
let replyTags = this.event.interpolated.replyEvents
|
||||
let replyTags = this.event.interpolated?.replyEvents
|
||||
if (replyTags?.length) return replyTags[replyTags.length - 1]
|
||||
return null
|
||||
},
|
||||
// content() {
|
||||
// return this.interpolateMentions(this.event.content, this.event.tags)
|
||||
// },
|
||||
|
||||
isRepost() {
|
||||
return this.event.interpolated?.text === '' &&
|
||||
this.event.interpolated.mentionEvents.length
|
||||
// return this.content.text === '' && this.content.mentions.eventMentions.length
|
||||
},
|
||||
|
||||
isQuote() {
|
||||
return this.event.interpolated?.text &&
|
||||
this.event.interpolated.mentionEvents.length
|
||||
// return this.content.text && this.content.mentions.eventMentions.length
|
||||
},
|
||||
|
||||
mentionEvents() {
|
||||
@ -356,42 +330,24 @@ export default defineComponent({
|
||||
mounted() {
|
||||
// console.log('mounted')
|
||||
if (!this.isEmbeded && (this.isQuote || this.isRepost)) {
|
||||
this.listenReposts(this.mentionEvents)
|
||||
// console.log('eventMentions:', this.mentionEvents)
|
||||
this.processTaggedEvents(this.mentionEvents, this.reposts)
|
||||
}
|
||||
this.calcConnectorValues()
|
||||
this.$emit('mounted')
|
||||
},
|
||||
|
||||
// updated() {
|
||||
// this.calcConnectorValues()
|
||||
// },
|
||||
|
||||
activated() {
|
||||
this.calcConnectorValues()
|
||||
this.trigger++
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
if (this.reposts.length) {
|
||||
for (let event of this.reposts) this.$store.dispatch('cancelUseProfile', {pubkey: event.pubkey})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// startClicking() {
|
||||
// if (this.event.kind === 2) return
|
||||
|
||||
// this.clicking = true
|
||||
// setTimeout(() => {
|
||||
// this.clicking = false
|
||||
// }, 200)
|
||||
// },
|
||||
|
||||
// finishClicking(ev) {
|
||||
// if (ev.target.tagName === 'A') return
|
||||
|
||||
// replyingConnectorStyle() {
|
||||
// if (this.replying && ) {
|
||||
// let height = this.postHeight + this.childReplyHeights.slice(0, -1).reduce((c, p) => c + p, 0)
|
||||
// if (this.replyHeight) height += this.replyHeight
|
||||
// return 'visibility: visible; height: ' + height + 'px'
|
||||
// } else return ''
|
||||
// },
|
||||
|
||||
childReplyConnectorStyle() {
|
||||
if (this.childReplyHeights?.length) {
|
||||
let height = this.postHeight + this.childReplyHeights.slice(0, -1).reduce((c, p) => c + p, 0)
|
||||
@ -416,8 +372,6 @@ export default defineComponent({
|
||||
this.replyHeight = this.replyContentHeight
|
||||
if (this.hasReplyChildren) {
|
||||
this.childReplyHeights = this.$refs.childReplyContent?.map((div) => div.clientHeight)
|
||||
// for (let {height, i} of childReplyHeights)
|
||||
// this.set(this.childReplyHeights, i, height)
|
||||
}
|
||||
this.$emit('resized')
|
||||
}, time)
|
||||
@ -441,66 +395,13 @@ export default defineComponent({
|
||||
this.$emit('add-event', event)
|
||||
},
|
||||
|
||||
async listenReposts(eventIds) {
|
||||
let subEventIds = []
|
||||
// let this.reposts = []
|
||||
// only render first 10 reposts
|
||||
eventIds.splice(10)
|
||||
for (let eventId of eventIds) {
|
||||
let event = await dbGetEvent(eventId)
|
||||
if (event) {
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: event.pubkey,
|
||||
request: true
|
||||
})
|
||||
this.interpolateEventMentions(event)
|
||||
this.reposts.push(event)
|
||||
} else {
|
||||
subEventIds.push(eventId)
|
||||
}
|
||||
}
|
||||
// console.log('this.reposts: ', this.reposts)
|
||||
// console.log('subEventIds: ', subEventIds)
|
||||
this.eventSub = pool.sub(
|
||||
{
|
||||
filter: {ids: subEventIds},
|
||||
cb: async event => {
|
||||
this.eventSub.unsub()
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: event.pubkey,
|
||||
request: true
|
||||
})
|
||||
this.interpolateEventMentions(event)
|
||||
this.reposts.push(event)
|
||||
// this.event = event
|
||||
}
|
||||
},
|
||||
'event-browser'
|
||||
)
|
||||
},
|
||||
|
||||
niceDate(timestamp) {
|
||||
if (this.trigger) return this.niceDateUTC(timestamp)
|
||||
return this.niceDateUTC(timestamp)
|
||||
}
|
||||
|
||||
// listen to changes to the event in the db so we get .seen_on updates
|
||||
// this.eventUpdates = await onEventUpdate(
|
||||
// this.$route.params.eventId,
|
||||
// event => {
|
||||
// // once we get an update from the db we know we can stop listening for relay updates
|
||||
// if (this.eventSub) this.eventSub.unsub()
|
||||
|
||||
// // and just update our local event with the latest one from the db
|
||||
// this.event = event
|
||||
// }
|
||||
// )
|
||||
// },
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!-- background-color: rgba(255, 255, 255, 0.2); -->
|
||||
<!-- background: rgba(255, 255, 255, 0.1); -->
|
||||
<style lang="scss" scoped>
|
||||
.post-padding {
|
||||
box-sizing: border-box;
|
||||
|
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<q-item unelevated class='q-pa-none post-entry-form flex coloumn' ref='postEntry' @click.stop @keypress.enter.stop @keydown.stop @keyup.stop>
|
||||
<!-- <q-separator color='primary' size='1px'/> -->
|
||||
<q-item unelevated class='q-pa-none post-entry-form flex coloumn' ref='postEntry' @click.stop @mouseup.stop @keypress.enter.stop @keydown.stop @keyup.stop>
|
||||
|
||||
<div
|
||||
v-if='replyMode === "quote" || replyMode === "repost"'
|
||||
@ -20,7 +19,6 @@
|
||||
class='embeded-message q-px-sm q-py-xs'
|
||||
>
|
||||
<div class='relative-position'>
|
||||
<!-- <span class='text-primary text-subtitle1'> reply to </span> -->
|
||||
<q-btn icon="close" flat dense @click.stop='$emit("clear-event")' size='xs' class='absolute-top-right z-top'/>
|
||||
</div>
|
||||
<BaseMessage
|
||||
@ -42,8 +40,6 @@
|
||||
<BaseUserAvatar :pubkey='$store.state.keys.pub' class='avatar-image' />
|
||||
<span id="input-placeholder"> {{ placeholderText }}</span>
|
||||
<div id="input-readonly-highlight" contenteditable="true" spellcheck="false"></div>
|
||||
<!-- ref="zinput" -->
|
||||
<!-- @keypress.enter.exact='handleEnter' -->
|
||||
<div
|
||||
id="input-editable"
|
||||
:contenteditable="!sending && !mentionsUpdating"
|
||||
@ -63,38 +59,8 @@
|
||||
<q-spinner-orbit color="accent" size='md'/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <q-input
|
||||
ref="input"
|
||||
v-model="text"
|
||||
type='textarea'
|
||||
autogrow
|
||||
autofocus
|
||||
:label="label"
|
||||
:disable='sending || mentionsUpdating'
|
||||
:loading='mentionsUpdating'
|
||||
@keypress.ctrl.enter="send"
|
||||
@click='trigger++'
|
||||
@keyup='trigger++'
|
||||
>
|
||||
<template #loading>
|
||||
<div class='full-width row justify-center q-my-md'>
|
||||
<q-spinner-orbit color="accent" size='md'/>
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-separator color='primary' size='1px'/> -->
|
||||
</div>
|
||||
<!-- <q-list id='tribute-wrapper' class='overflow-auto' style='position: aboslute; bottom: 100%; max-height: 70vh;'> -->
|
||||
<div style='font-size: .9rem;'>
|
||||
<!-- <q-list v-if='tags.length && !sending' class='q-px-sm tagged-wrapper'>
|
||||
<div class='text-primary'>tagged<span v-if='$route.name === "messages"'>{{' **NOTE TAGS ARE NOT PRIVATE**'}}</span></div>
|
||||
<div v-for='(tag, index) in tags' :key='index' class='flex row no-wrap q-gutter-xs' style='font-size: .8rem; font-weight: 300;'>
|
||||
<div class='text-bold'>{{ "#[" + index + "] " }}</div>
|
||||
<div>{{ (tag[0] === "e" ? " event: " : "") + (tag[0] === "p" ? " user: " : "")}}</div>
|
||||
<BaseUserName v-if='tag[0] === "p"' :pubkey='tag[1]' :fallback='true'/>
|
||||
<BaseMarkdown v-if='tag[0] === "e"'> {{ `[&${shorten(tag[1])}](/event/${tag[1]})` }} </BaseMarkdown>
|
||||
</div>
|
||||
</q-list> -->
|
||||
<div v-if='links.length' class='q-pl-xs'>
|
||||
<div class='text-secondary'>links added</div>
|
||||
<ul dense style='font-size: .8rem; font-weight: 300;'>
|
||||
@ -227,11 +193,7 @@
|
||||
@click.stop='send'
|
||||
:disable='!textValid'
|
||||
>
|
||||
<!-- <q-item-label v-if='!replyMode'>relay </q-item-label> -->
|
||||
<q-icon name="send" :style='"transform: translateX(" + sendIconTranslation + "px);"'/>
|
||||
<!-- <q-tooltip>
|
||||
send
|
||||
</q-tooltip> -->
|
||||
</q-btn>
|
||||
</q-btn-group>
|
||||
</div>
|
||||
@ -242,38 +204,22 @@
|
||||
import { colors } from 'quasar'
|
||||
const { getPaletteColor } = colors
|
||||
import helpersMixin from '../utils/mixin'
|
||||
// import {getPubKeyTagWithRelay, getEventTagWithRelay, processMentions} from '../utils/helpers'
|
||||
// import {nextTick} from 'vue'
|
||||
// import {getPubKeyTagWithRelay, getEventTagWithRelay, extractMentions} from '../utils/helpers'
|
||||
import {getPubKeyTagWithRelay, getEventTagWithRelay, shorten} from '../utils/helpers'
|
||||
// import BaseButtonCopy from 'components/BaseButtonCopy.vue'
|
||||
// import BaseButtonClear from 'components/BaseButtonClear.vue'
|
||||
import BaseEmojiPicker from 'components/BaseEmojiPicker.vue'
|
||||
import BaseLinkForm from 'components/BaseLinkForm.vue'
|
||||
// import BaseMarkdown from 'components/BaseMarkdown.vue'
|
||||
import BaseMessage from 'components/BaseMessage.vue'
|
||||
|
||||
export default {
|
||||
name: 'BasePostEntry',
|
||||
mixins: [helpersMixin],
|
||||
emits: ['sent', 'resized', 'clear-event'],
|
||||
// emits: ['sent', 'resized'],
|
||||
components: {
|
||||
// BaseButtonCopy,
|
||||
// BaseButtonClear,
|
||||
BaseEmojiPicker,
|
||||
BaseLinkForm,
|
||||
// BaseMarkdown,
|
||||
BaseMessage,
|
||||
},
|
||||
|
||||
props: {
|
||||
// postEntryAlignment (post-entry-alignment) should be 'column' or 'row'
|
||||
// postEntryAlignment: {
|
||||
// type: String,
|
||||
// required: false,
|
||||
// default: 'column'
|
||||
// },
|
||||
messageMode: {
|
||||
type: String,
|
||||
required: false,
|
||||
@ -366,7 +312,6 @@ export default {
|
||||
return true
|
||||
},
|
||||
postEntryWidth() {
|
||||
// return this.$refs.input?.$el?.clientWidth
|
||||
return this.$refs.postEntry?.$el?.clientWidth
|
||||
},
|
||||
toolboxWidth() {
|
||||
@ -660,22 +605,15 @@ export default {
|
||||
|
||||
async updateMentionsTags() {
|
||||
this.trigger++
|
||||
// let curPos = this.cursorPositionStart
|
||||
// let prevTextLength = this.text.length
|
||||
let { start } = this.startEndOfRange()
|
||||
const mentionRegex = /(?<t>[@&]{1})(?<p>[a-f0-9]{64})/g
|
||||
if (this.text.toLowerCase().match(mentionRegex)) {
|
||||
this.mentionsUpdating = true
|
||||
// this.text = await this.extractMentions(this.text, this.tags)
|
||||
await this.extractMentions(this.textarea, this.tags)
|
||||
// this.textarea.innerHTML = this.text
|
||||
// this.text = this.textarea.innerHTML
|
||||
this.updateText()
|
||||
if (start.el.nodeName === '#text' && start.pos > start.el.length)
|
||||
this.setCaret(start.el, start.el.length)
|
||||
else this.setCaret(start.el, start.pos)
|
||||
// this.setCursorPosition(curPos + (this.text.length - prevTextLength))
|
||||
// this.updateReadonlyInputs()
|
||||
this.updateReadonlyInput()
|
||||
this.mentionsUpdating = false
|
||||
}
|
||||
@ -753,7 +691,7 @@ export default {
|
||||
appendHashtags(tags) {
|
||||
for (let hashtag of this.hashtags) {
|
||||
if (!tags.find(([_, v]) => v === hashtag)) {
|
||||
tags.push(['hashtag', hashtag.toLowerCase()])
|
||||
tags.push(['t', hashtag.toLowerCase()])
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -818,7 +756,6 @@ export default {
|
||||
for (let key in mentions) {
|
||||
let [_, mentionText] = key.split('_')
|
||||
const mentionAnchorRegex = new RegExp(`(?<i>${mentionText})\\b`, 'g')
|
||||
// let matches = node.textContent.matchAll(mentionAnchorRegex)
|
||||
readonlyTextareaHtml = readonlyTextareaHtml.replaceAll(
|
||||
mentionAnchorRegex,
|
||||
(_, value) => this.colorText(mentionText).outerHTML
|
||||
@ -830,7 +767,6 @@ export default {
|
||||
updateReadonlyHightlightInput() {
|
||||
// update over char limit highlighting
|
||||
if (this.overCharLimit) {
|
||||
// console.log('over char limit', this.charPos(this.charLimit))
|
||||
this.readonlyHighlightTextarea.innerHTML = this.textarea.innerHTML
|
||||
let { el, pos } = this.charPos(this.charLimit, this.readonlyHighlightTextarea)
|
||||
let midword = el.length && el.length > pos
|
||||
|
@ -22,7 +22,6 @@
|
||||
<script>
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import BaseShowMore from 'components/BaseShowMore'
|
||||
// import {getEventTagWithRelay} from '../utils/helpers'
|
||||
|
||||
export default {
|
||||
name: 'BasePostThread',
|
||||
@ -73,12 +72,12 @@ export default {
|
||||
if ((i === this.events.length - 1) && curr.replies?.length && this.threadWidth &&
|
||||
// (this.replyDepth >= 5)) {
|
||||
(this.replyDepth >= 5 || (this.replyDepth > 0 && this.threadWidth < 175))) {
|
||||
let replies = Array.from(curr.replies)
|
||||
// let replies = Array.from(curr.replies)
|
||||
let event = Object.assign({}, curr)
|
||||
event.replies = []
|
||||
// curr.replies = []
|
||||
filled.push(event)
|
||||
filled.push({id: 'FILLER', root: curr.id, replies: replies})
|
||||
filled.push({id: 'FILLER', root: curr.id})
|
||||
// filled.concat([curr, {id: 'FILLER', root: curr.id, replies: replies}])
|
||||
// console.log('filled', filled)
|
||||
} else filled.push(curr)
|
||||
|
@ -4,7 +4,7 @@
|
||||
<q-btn
|
||||
rounded
|
||||
flat
|
||||
color="secondary"
|
||||
color="primary"
|
||||
size="md"
|
||||
:icon="url in $store.state.relays ? 'check' : 'add'"
|
||||
:label="url in $store.state.relays ? 'added' : 'add relay'"
|
||||
|
@ -47,7 +47,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {pool} from '../pool'
|
||||
import {publish} from '../query'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
|
||||
export default {
|
||||
@ -68,7 +68,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
publishTo(relayURL) {
|
||||
pool.relays[relayURL]?.relay?.publish?.(this.event)
|
||||
publish(this.event, relayURL)
|
||||
}
|
||||
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex column q-gutter-md">
|
||||
<div class="" caption>added relay:</div>
|
||||
<div v-if='!listView' class="" caption>added relay:</div>
|
||||
<BaseRelayCard :url='url' />
|
||||
</div>
|
||||
</template>
|
||||
@ -16,7 +16,8 @@ export default {
|
||||
BaseRelayCard,
|
||||
},
|
||||
props: {
|
||||
url: {type: String, required: true}
|
||||
url: {type: String, required: true},
|
||||
listView: {type: Boolean, default: false},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -33,8 +33,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
showMore() {
|
||||
if (this.replies && this.replies.length) this.toEvent(this.root, this.replies)
|
||||
else this.toEvent(this.root)
|
||||
this.toEvent(this.root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div :class='(bordered ? "bordered-avatar" : "") + (hoverEffect ? " hovered-avatar" : "")'>
|
||||
<q-avatar :rounded='!round' class='relative-position' :size='size' @click.stop="toProfile(pubkey)">
|
||||
<img :src="$store.getters.avatar(pubkey)"/>
|
||||
<img :src="$store.getters.avatar(pubkey)" crossorigin/>
|
||||
<div :class='alignRight ? "icon-right" : "icon-left"' class='q-pt-xs'>
|
||||
<BaseButtonNIP05
|
||||
v-if='showVerified'
|
||||
|
@ -123,7 +123,6 @@
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<!-- <q-fab-action color="primary" label="login/create user" /> -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -142,13 +141,6 @@ export default defineComponent({
|
||||
BaseMarkdown,
|
||||
},
|
||||
|
||||
// props: {
|
||||
// initializeKeys: {
|
||||
// type: Boolean,
|
||||
// default: true
|
||||
// }
|
||||
// },
|
||||
|
||||
setup() {
|
||||
return {
|
||||
focusKeyInput() {
|
||||
@ -159,20 +151,12 @@ export default defineComponent({
|
||||
|
||||
data() {
|
||||
return {
|
||||
// initializeKeys: true,
|
||||
watchOnly: false,
|
||||
key: null,
|
||||
// hasExtension: false,
|
||||
hasExtension: false,
|
||||
}
|
||||
},
|
||||
|
||||
// watch: {
|
||||
// $route(curr, prev) {
|
||||
// if (this.showKeyInitialization) this.initializeKeys = true
|
||||
// else this.initializeKeys = false
|
||||
// },
|
||||
// },
|
||||
|
||||
computed: {
|
||||
icon() {
|
||||
return document.getElementById('icon').href
|
||||
@ -183,11 +167,6 @@ export default defineComponent({
|
||||
return true
|
||||
},
|
||||
|
||||
hasExtension() {
|
||||
if (window.nostr) return true
|
||||
return false
|
||||
},
|
||||
|
||||
isKeyKey() {
|
||||
if (this.isKey(this.hexKey)) return true
|
||||
return false
|
||||
@ -227,28 +206,18 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
// async start() {
|
||||
// if (!this.$store.state.keys.pub) {
|
||||
// // keys not set up, offer the option to try to get a pubkey from window.nostr
|
||||
// setTimeout(() => {
|
||||
// if (window.nostr) {
|
||||
// this.hasExtension = true
|
||||
// console.log('window has nostr')
|
||||
// }
|
||||
// }, 1000)
|
||||
// console.log('getFromExtension', this.getFromExtension)
|
||||
// }
|
||||
// if (this.$store.state.keys.pub) this.initializeKeys = false
|
||||
// console.log('start')
|
||||
// },
|
||||
async created() {
|
||||
if (!this.$store.state.keys.pub) {
|
||||
// keys not set up, offer the option to try to get a pubkey from window.nostr
|
||||
setTimeout(() => {
|
||||
if (window.nostr) {
|
||||
this.hasExtension = true
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// setInitializeKeys(evt) {
|
||||
// if (this.hideKeyInitialization) this.initializeKeys = false
|
||||
// else if (this.$store.state.keys.pub) this.initializeKeys = false
|
||||
// else if (!this.hideKeyInitialization) this.initializeKeys = true
|
||||
// },
|
||||
|
||||
async getFromExtension() {
|
||||
try {
|
||||
this.key = await window.nostr.getPublicKey()
|
||||
@ -262,13 +231,6 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
// getAstralPublicKey() {
|
||||
// this.key =
|
||||
// '2df69cd0c6ab95e08f466abe7b39bb64e744ee31ffc3041f270bdfec2a37ec06'
|
||||
// this.watchOnly = true
|
||||
// this.focusKeyInput()
|
||||
// },
|
||||
|
||||
generate() {
|
||||
this.key = generatePrivateKey()
|
||||
this.watchOnly = false
|
||||
|
@ -45,7 +45,6 @@
|
||||
</div>
|
||||
<q-list class='q-pt-xs q-pl-sm' style='overflow-y: auto; max-height: 40vh;'>
|
||||
<div v-for="user in domainUsers" :key="user.pubkey">
|
||||
<!-- <h2 class='text-caption text-bold q-my-none'> {{user.name}} </h2> -->
|
||||
<BaseUserCard :pubkey="user.pubkey" />
|
||||
</div>
|
||||
</q-list>
|
||||
@ -69,7 +68,7 @@
|
||||
<BaseUserCard
|
||||
v-for="pubkey in $store.state.following"
|
||||
:pubkey="pubkey"
|
||||
:key="pubkey"
|
||||
:key="pubkey + '_' + $store.state.profilesCacheToggle"
|
||||
/>
|
||||
</q-list>
|
||||
<Draggable
|
||||
@ -79,7 +78,6 @@
|
||||
@end="dragging=false"
|
||||
item-key="pubkey"
|
||||
>
|
||||
<!-- <div>{{element.name}}</div> -->
|
||||
<template #header>
|
||||
<div class='flex row justify-between items-start'>
|
||||
<span>{{ $t('dragDropReorder') }}</span>
|
||||
@ -89,7 +87,6 @@
|
||||
<template #item="{element}">
|
||||
<BaseUserCard :pubkey='element.pubkey' :action-buttons='false'/>
|
||||
</template>
|
||||
<!-- <BaseUserCard :clickable='false' :pubkey="element.pubkey" /> -->
|
||||
</Draggable>
|
||||
</div>
|
||||
<div v-else>
|
||||
@ -120,6 +117,7 @@ export default defineComponent({
|
||||
reordering: false,
|
||||
reorderedFollowing: [],
|
||||
dragging: false,
|
||||
profilesUsed: new Set(),
|
||||
}
|
||||
},
|
||||
|
||||
@ -148,6 +146,10 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async searchProfile() {
|
||||
@ -177,20 +179,8 @@ export default defineComponent({
|
||||
this.domainNames = await searchDomain(this.domain)
|
||||
// this.domainUsers
|
||||
if (this.domainUsers.length || this.domainDefaultPubkey) {
|
||||
if (this.domainDefaultPubkey) {
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: this.domainDefaultPubkey,
|
||||
request: true
|
||||
})
|
||||
}
|
||||
if (this.domainUsers.length) {
|
||||
this.domainUsers.forEach((user) => {
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: user.pubkey,
|
||||
request: true
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.domainDefaultPubkey) this.useProfile(this.domainDefaultPubkey)
|
||||
if (this.domainUsers.length) this.domainUsers.forEach((user) => this.useProfile(user.pubkey))
|
||||
this.searching = false
|
||||
this.domainMode = true
|
||||
return
|
||||
@ -228,7 +218,14 @@ export default defineComponent({
|
||||
cancelReorder() {
|
||||
this.reordering = false
|
||||
this.reorderedFollowing = []
|
||||
}
|
||||
},
|
||||
|
||||
useProfile(pubkey) {
|
||||
if (this.profilesUsed.has(pubkey)) return
|
||||
|
||||
this.profilesUsed.add(pubkey)
|
||||
this.$store.dispatch('useProfile', {pubkey})
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
106
src/db.js
106
src/db.js
@ -1,106 +0,0 @@
|
||||
const worker = new Worker(new URL('./worker-db.js', import.meta.url))
|
||||
|
||||
const hub = {}
|
||||
|
||||
worker.onmessage = ev => {
|
||||
let {id, success, error, data, stream} = JSON.parse(ev.data)
|
||||
|
||||
if (stream) {
|
||||
console.debug('🖴', id, '~>>', data)
|
||||
hub[id](data)
|
||||
return
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
hub[id].reject(new Error(error))
|
||||
delete hub[id]
|
||||
return
|
||||
}
|
||||
|
||||
if (data) console.debug('🖴', id, '->', data)
|
||||
hub[id]?.resolve?.(data)
|
||||
delete hub[id]
|
||||
}
|
||||
|
||||
function call(name, args) {
|
||||
let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
console.debug('🖴', id, '<-', args)
|
||||
worker.postMessage(JSON.stringify({id, name, args}))
|
||||
return new Promise((resolve, reject) => {
|
||||
hub[id] = {resolve, reject}
|
||||
})
|
||||
}
|
||||
|
||||
function stream(name, args, callback) {
|
||||
let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
hub[id] = callback
|
||||
console.debug('db <-', id, args)
|
||||
worker.postMessage(JSON.stringify({id, name, args, stream: true}))
|
||||
return {
|
||||
cancel() {
|
||||
worker.postMessage(JSON.stringify({id, cancel: true}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function eraseDatabase() {
|
||||
return call('eraseDatabase', [])
|
||||
}
|
||||
export async function destroyStreams() {
|
||||
return call('destroyStreams', [])
|
||||
}
|
||||
export async function dbSave(event, relay) {
|
||||
return call('dbSave', [event, relay])
|
||||
}
|
||||
export async function dbGetHomeFeedNotes(
|
||||
limit = 50,
|
||||
since = Math.round(Date.now() / 1000)
|
||||
) {
|
||||
return call('dbGetHomeFeedNotes', [limit, since])
|
||||
}
|
||||
export function onNewHomeFeedNote(callback = () => {}) {
|
||||
return stream('onNewHomeFeedNote', [], callback)
|
||||
}
|
||||
export async function dbGetChats(ourPubKey) {
|
||||
return call('dbGetChats', [ourPubKey])
|
||||
}
|
||||
export async function dbGetMessages(
|
||||
peerPubKey,
|
||||
limit = 50,
|
||||
since = Math.round(Date.now() / 1000)
|
||||
) {
|
||||
return call('dbGetMessages', [peerPubKey, limit, since])
|
||||
}
|
||||
export function onNewMessage(peerPubKey, callback = () => {}) {
|
||||
return stream('onNewMessage', [peerPubKey], callback)
|
||||
}
|
||||
export async function dbGetEvent(id) {
|
||||
return call('dbGetEvent', [id])
|
||||
}
|
||||
export async function onEventUpdate(id, callback = () => {}) {
|
||||
return stream('onEventUpdate', [id], callback)
|
||||
}
|
||||
export async function dbGetMentions(ourPubKey, limit = 40, since, until) {
|
||||
return call('dbGetMentions', [ourPubKey, limit, since, until])
|
||||
}
|
||||
export function onNewMention(ourPubKey, callback = () => {}) {
|
||||
return stream('onNewMention', [ourPubKey], callback)
|
||||
}
|
||||
export function onNewAnyMessage(callback = () => {}) {
|
||||
return stream('onNewAnyMessage', [], callback)
|
||||
}
|
||||
export async function dbGetUnreadNotificationsCount(ourPubKey, since) {
|
||||
return call('dbGetUnreadNotificationsCount', [ourPubKey, since])
|
||||
}
|
||||
export async function dbGetUnreadMessages(pubkey, since) {
|
||||
return call('dbGetUnreadMessages', [pubkey, since])
|
||||
}
|
||||
export async function dbGetProfile(pubkey) {
|
||||
return call('dbGetProfile', [pubkey])
|
||||
}
|
||||
export async function dbGetContactList(pubkey) {
|
||||
return call('dbGetContactList', [pubkey])
|
||||
}
|
||||
export async function dbGetRelayForPubKey(pubkey) {
|
||||
return call('dbGetRelayForPubKey', [pubkey])
|
||||
}
|
@ -81,6 +81,7 @@ export default {
|
||||
follows: 'follows',
|
||||
followers: 'followers',
|
||||
replies: 'replies',
|
||||
profile: 'profile',
|
||||
relays: 'relays',
|
||||
users: 'users',
|
||||
nip05Maintainer: 'NIP05 maintainer',
|
||||
@ -89,6 +90,7 @@ export default {
|
||||
// text
|
||||
noFollows: 'no follows',
|
||||
noFollowers: 'no followers',
|
||||
noRelays: 'no relays',
|
||||
dragDropReorder: 'drag and drop to reorder',
|
||||
|
||||
}
|
||||
|
@ -58,7 +58,6 @@
|
||||
>
|
||||
<q-tooltip>forward</q-tooltip>
|
||||
</q-btn>
|
||||
<!-- v-if='$route.name !== "inbox" && $route.name !== "messages"' -->
|
||||
<q-btn
|
||||
@click.stop="scrollToTop"
|
||||
color="primary"
|
||||
@ -90,7 +89,7 @@
|
||||
import { defineComponent} from 'vue'
|
||||
import { scroll } from 'quasar'
|
||||
const { getVerticalScrollPosition, setVerticalScrollPosition} = scroll
|
||||
import { destroyStreams } from '../db'
|
||||
import { destroyStreams } from '../query'
|
||||
import TheKeyInitializationDialog from 'components/TheKeyInitializationDialog.vue'
|
||||
import TheUserMenu from 'components/TheUserMenu.vue'
|
||||
import TheSearchMenu from 'components/TheSearchMenu.vue'
|
||||
@ -112,18 +111,10 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
// computed: {
|
||||
// showKeyInitialization() {
|
||||
// if (['profile', 'event', 'hashtag', 'feed'].includes(this.$route.name)) return false
|
||||
// return true
|
||||
// },
|
||||
// },
|
||||
|
||||
mounted() {
|
||||
if (this.$store.state.keys.pub) {
|
||||
// keys already set up
|
||||
this.$store.dispatch('launch')
|
||||
// this.initializeKeys = false
|
||||
} else {
|
||||
this.$store.dispatch('launchWithoutKey')
|
||||
}
|
||||
|
@ -3,8 +3,8 @@
|
||||
<q-page ref='page'>
|
||||
<div class="text-h5 text-bold q-py-md">{{ $t('thread') }}</div>
|
||||
<q-separator color='accent' size='2px'/>
|
||||
<div v-if="ancestors.length">
|
||||
<BasePostThread :events="ancestors" is-ancestors @add-event='addEventAncestors'/>
|
||||
<div v-if="ancestorsCompiled.length || rootAncestor">
|
||||
<BasePostThread :events="ancestorsCompiled" is-ancestors @add-event='addEventAncestors'/>
|
||||
</div>
|
||||
|
||||
<q-item ref="main" class='no-padding column'>
|
||||
@ -13,21 +13,20 @@
|
||||
:event='event'
|
||||
:highlighted='true'
|
||||
:position='ancestors.length ? "last" : "standalone"'
|
||||
@add-event='addEventChildren'
|
||||
@add-event='processChildEvent'
|
||||
/>
|
||||
<div v-else>
|
||||
{{ $t('event') }} {{ $route.params.eventId }}
|
||||
</div>
|
||||
<!-- style='background: rgba(255, 255, 255, 0.1);' -->
|
||||
<BaseRelayList v-if="event?.seen_on?.length" :event='event'/>
|
||||
</q-item>
|
||||
|
||||
<q-separator color='accent' size='2px'/>
|
||||
|
||||
<div v-if="childrenThreads.length">
|
||||
<div v-if="childrenThreadsFiltered.length">
|
||||
<div class="text-h6 text-bold">{{ $t('replies') }}</div>
|
||||
<div v-for="(thread) in childrenThreads" :key="thread[0].id">
|
||||
<BasePostThread :events="thread" @add-event='addEventChildren'/>
|
||||
<div v-for="(thread) in childrenThreadsFiltered" :key="thread[0].id">
|
||||
<BasePostThread :events="thread" @add-event='processChildEvent'/>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
@ -35,15 +34,9 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
// import { parse } from 'JSON'
|
||||
import {pool} from '../pool'
|
||||
import {dbGetEvent, onEventUpdate} from '../db'
|
||||
import {dbStreamEvent, dbStreamTagKind} from '../query'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import {addToThread} from '../utils/threads'
|
||||
// import { scroll } from 'quasar'
|
||||
// const { getVerticalScrollPosition, setVerticalScrollPosition} = scroll
|
||||
// import { scroll } from 'quasar'
|
||||
// const { getScrollTarget, setScrollPosition } = scroll
|
||||
import BaseRelayList from 'components/BaseRelayList.vue'
|
||||
|
||||
export default defineComponent({
|
||||
@ -58,206 +51,118 @@ export default defineComponent({
|
||||
return {
|
||||
replying: false,
|
||||
ancestors: [],
|
||||
ancestorsSet: new Set(),
|
||||
ancestorsSub: null,
|
||||
ancestorsSeen: new Map(),
|
||||
ancestorIds: [],
|
||||
rootAncestor: null,
|
||||
event: null,
|
||||
eventSub: null,
|
||||
childrenThreads: [],
|
||||
childrenSeen: new Map(),
|
||||
childrenSub: null,
|
||||
eventUpdates: null
|
||||
childrenSet: new Set(),
|
||||
sub: {},
|
||||
profilesUsed: new Set(),
|
||||
}
|
||||
},
|
||||
|
||||
// computed: {
|
||||
// content() {
|
||||
// return this.interpolateMentions(this.event.content, this.event.tags)
|
||||
// },
|
||||
// },
|
||||
computed: {
|
||||
childrenThreadsFiltered() {
|
||||
return this.childrenThreads.filter(thread => thread[0].interpolated.replyEvents.includes(this.$route.params.eventId))
|
||||
},
|
||||
ancestorsCompiled() {
|
||||
if (!this.rootAncestor) return this.ancestors
|
||||
if (this.ancestors.length && this.rootAncestor && this.ancestors[0].id === this.rootAncestor.id) return this.ancestors
|
||||
return [this.rootAncestor].concat(this.ancestors)
|
||||
}
|
||||
},
|
||||
|
||||
activated() {
|
||||
console.log('activated')
|
||||
this.start()
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
console.log('deactivated')
|
||||
this.stop()
|
||||
},
|
||||
|
||||
methods: {
|
||||
start() {
|
||||
this.listen()
|
||||
async start() {
|
||||
this.sub.event = await dbStreamEvent(this.$route.params.eventId, event => {
|
||||
let getAncestorsChildren = false
|
||||
if (!this.event) getAncestorsChildren = true
|
||||
this.interpolateEventMentions(event)
|
||||
this.event = null
|
||||
this.event = event
|
||||
if (getAncestorsChildren) {
|
||||
if (this.event.interpolated.replyEvents.length) this.subRootAncestor()
|
||||
this.subAncestorsChildren()
|
||||
}
|
||||
this.useProfile(event.pubkey)
|
||||
}, true)
|
||||
this.subAncestorsChildren()
|
||||
},
|
||||
|
||||
stop() {
|
||||
this.replying = false
|
||||
if (this.ancestorsSub) this.ancestorsSub.unsub()
|
||||
if (this.childrenSub) this.childrenSub.unsub()
|
||||
if (this.eventSub) this.eventSub.unsub()
|
||||
if (this.eventUpdates) this.eventUpdates.cancel()
|
||||
if (this.sub.event) this.sub.event.cancel()
|
||||
if (this.sub.ancestorsChildren) this.sub.ancestorsChildren.cancel()
|
||||
if (this.sub.rootAncestor) this.sub.rootAncestor.cancel()
|
||||
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
|
||||
},
|
||||
|
||||
async listen() {
|
||||
this.event = await dbGetEvent(this.$route.params.eventId)
|
||||
if (this.event) {
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: this.event.pubkey,
|
||||
request: true
|
||||
})
|
||||
this.interpolateEventMentions(this.event)
|
||||
this.listenAncestors()
|
||||
} else {
|
||||
this.eventSub = pool.sub(
|
||||
{
|
||||
filter: {ids: [this.$route.params.eventId]},
|
||||
cb: async event => {
|
||||
this.eventSub.unsub()
|
||||
this.event = event
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: this.event.pubkey,
|
||||
request: true
|
||||
})
|
||||
this.interpolateEventMentions(this.event)
|
||||
this.listenAncestors()
|
||||
}
|
||||
},
|
||||
'event-browser'
|
||||
)
|
||||
}
|
||||
async subRootAncestor() {
|
||||
console.log('subbing root ancestor', this.event.interpolated.replyEvents[0])
|
||||
this.sub.rootAncestor = await dbStreamEvent(this.event.interpolated.replyEvents[0], event => {
|
||||
this.processAncestorEvent(event)
|
||||
this.sub.rootAncestor.cancel()
|
||||
})
|
||||
},
|
||||
|
||||
// listen to changes to the event in the db so we get .seen_on updates
|
||||
this.eventUpdates = await onEventUpdate(
|
||||
this.$route.params.eventId,
|
||||
event => {
|
||||
// once we get an update from the db we know we can stop listening for relay updates
|
||||
if (this.eventSub) this.eventSub.unsub()
|
||||
async subAncestorsChildren() {
|
||||
let tags = this.event?.interpolated?.replyEvents?.length ? [this.$route.params.eventId, this.event.interpolated.replyEvents[0]] : [this.$route.params.eventId]
|
||||
|
||||
// and just update our local event with the latest one from the db
|
||||
this.event = event
|
||||
this.interpolateEventMentions(this.event)
|
||||
if (this.sub.ancestorsChildren) this.sub.ancestorsChildren.update('e', tags, 1)
|
||||
else this.sub.ancestorsChildren = await dbStreamTagKind('e', tags, 1, event => {
|
||||
if (this.event && event.created_at < this.event.created_at) {
|
||||
this.processAncestorEvent(event)
|
||||
return
|
||||
}
|
||||
)
|
||||
if (this.$route.params.childThreads) this.childrenThreads = JSON.parse(this.$route.params.childThreads)
|
||||
else this.listenChildren()
|
||||
this.processChildEvent(event)
|
||||
return
|
||||
})
|
||||
},
|
||||
|
||||
listenChildren() {
|
||||
this.childrenThreads = []
|
||||
this.childrenSeen = new Map()
|
||||
this.childrenSub = pool.sub(
|
||||
{
|
||||
filter: [
|
||||
{
|
||||
'#e': [this.$route.params.eventId],
|
||||
kinds: [1]
|
||||
}
|
||||
],
|
||||
cb: async (event, relay) => {
|
||||
let existing = this.childrenSeen.get(event.id)
|
||||
if (existing) {
|
||||
if (!Array.isArray(existing.seen_on)) existing.seen_on = []
|
||||
else if (existing.seen_on.includes(relay)) return
|
||||
existing.seen_on.push(relay)
|
||||
return
|
||||
}
|
||||
processAncestorEvent(event) {
|
||||
let currAncestor = this.ancestors.length ? this.ancestors[this.ancestors.length - 1] : this.event
|
||||
if (currAncestor.interpolated.replyEvents.length === 0) return
|
||||
|
||||
event.seen_on = [relay]
|
||||
this.childrenSeen.set(event.id, event)
|
||||
let existing = this.ancestorsSeen.get(event.id)
|
||||
if (existing) return
|
||||
|
||||
this.$store.dispatch('useProfile', {pubkey: event.pubkey})
|
||||
this.interpolateEventMentions(event)
|
||||
this.ancestorsSeen.set(event.id, event)
|
||||
if (this.event?.interpolated?.replyEvents?.[0] === event.id) this.rootAncestor = event
|
||||
|
||||
this.interpolateEventMentions(event)
|
||||
addToThread(this.childrenThreads, event)
|
||||
return
|
||||
}
|
||||
},
|
||||
'event-children'
|
||||
)
|
||||
},
|
||||
|
||||
async listenAncestors() {
|
||||
this.ancestors = []
|
||||
this.ancestorsSet = new Set()
|
||||
|
||||
let eventTags = this.event.interpolated.replyEvents
|
||||
if (eventTags.length === 2) await this.getAncestorsAncestorsFromDb(eventTags)
|
||||
if (eventTags.length) {
|
||||
this.ancestorsSub = pool.sub(
|
||||
{
|
||||
filter: [
|
||||
{
|
||||
kinds: [1],
|
||||
ids: eventTags
|
||||
}
|
||||
],
|
||||
cb: async event => {
|
||||
if (this.ancestorsSet.has(event.id)) return
|
||||
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: event.pubkey,
|
||||
request: true
|
||||
})
|
||||
this.interpolateEventMentions(event)
|
||||
this.ancestorsSet.add(event.id)
|
||||
|
||||
// manual sorting
|
||||
// older events first
|
||||
for (let i = 0; i < this.ancestors.length; i++) {
|
||||
if (event.created_at < this.ancestors[i].created_at) {
|
||||
// the new event is older than the current index,
|
||||
// so we add it at the previous index
|
||||
this.ancestors.splice(i, 0, event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// the newer event is the newest, add to end
|
||||
this.ancestors.push(event)
|
||||
this.scrollToMainEvent()
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
'event-ancestors'
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
async getAncestorsAncestorsFromDb(eventTags) {
|
||||
const initialEventId = eventTags[0]
|
||||
let lastEventId = eventTags[1]
|
||||
let addedAncestorCount = 0
|
||||
while (lastEventId !== initialEventId && addedAncestorCount <= 5) {
|
||||
// console.log('starting await, lastEventId: ', lastEventId)
|
||||
let lastEvent = await dbGetEvent(lastEventId)
|
||||
// console.log('finished await')
|
||||
if (lastEvent) {
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: lastEvent.pubkey,
|
||||
request: true
|
||||
})
|
||||
let lastEventTags = lastEvent.tags.filter(([t, _]) => t === 'e').map(([_, v]) => v)
|
||||
if (lastEventTags.length === 0) {
|
||||
// console.log(`last event ${lastEventId} has no tags prior to finding initial event ${initialEventId}`)
|
||||
break
|
||||
} else if (lastEventTags[0] !== initialEventId) {
|
||||
// console.log(`last event ${lastEventId} does not have initial event ${initialEventId} listed as initial event`)
|
||||
break
|
||||
} else if (lastEventTags.length > 2) {
|
||||
// console.log(`last event ${lastEventId} has more than 2 tags`)
|
||||
break
|
||||
}
|
||||
if (!eventTags.includes(lastEventId)) eventTags.push(lastEventId)
|
||||
lastEventId = lastEventTags[lastEventTags.length - 1]
|
||||
// console.log('eventTags: ', eventTags)
|
||||
// console.log('lastEventTags: ', lastEventTags)
|
||||
} else {
|
||||
// console.log('no event found from db')
|
||||
break
|
||||
let prevAncestorId = currAncestor.interpolated.replyEvents[currAncestor.interpolated.replyEvents.length - 1]
|
||||
if (prevAncestorId === event.id) {
|
||||
let prevAncestor = event
|
||||
while (prevAncestor) {
|
||||
this.ancestors = [prevAncestor].concat(this.ancestors)
|
||||
this.scrollToMainEvent()
|
||||
this.useProfile(prevAncestor.pubkey)
|
||||
currAncestor = prevAncestor
|
||||
prevAncestorId = currAncestor.interpolated.replyEvents[currAncestor.interpolated.replyEvents.length - 1]
|
||||
prevAncestor = this.ancestorsSeen.get(prevAncestorId)
|
||||
}
|
||||
// for (eventId in eventTags) {
|
||||
addedAncestorCount++
|
||||
}
|
||||
return eventTags
|
||||
},
|
||||
|
||||
processChildEvent(event) {
|
||||
if (event.id === this.$route.params.eventId) return
|
||||
if (this.childrenSet.has(event.id)) return
|
||||
|
||||
this.childrenSet.add(event.id)
|
||||
this.useProfile(event.pubkey)
|
||||
this.interpolateEventMentions(event)
|
||||
addToThread(this.childrenThreads, event)
|
||||
},
|
||||
|
||||
scrollToMainEvent() {
|
||||
@ -267,20 +172,17 @@ export default defineComponent({
|
||||
})
|
||||
},
|
||||
|
||||
addEventChildren(event) {
|
||||
let existing = this.childrenSeen.get(event.id)
|
||||
if (existing) {
|
||||
return
|
||||
}
|
||||
this.interpolateEventMentions(event)
|
||||
this.childrenSeen.set(event.id, event)
|
||||
addToThread(this.childrenThreads, event)
|
||||
},
|
||||
|
||||
addEventAncestors(event) {
|
||||
this.interpolateEventMentions(event)
|
||||
this.toEvent(event.id)
|
||||
},
|
||||
|
||||
useProfile(pubkey) {
|
||||
if (this.profilesUsed.has(pubkey)) return
|
||||
|
||||
this.profilesUsed.add(pubkey)
|
||||
this.$store.dispatch('useProfile', {pubkey})
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -4,9 +4,7 @@
|
||||
class='home-feed-header flex column'
|
||||
>
|
||||
<div class="text-h5 text-bold q-py-md">{{ $t('feed') }}</div>
|
||||
<!-- <BasePostEntry v-if='$store.state.keys.pub'/> -->
|
||||
</div>
|
||||
<!-- <q-separator color='accent' size='2px'/> -->
|
||||
<q-tabs
|
||||
v-model="tab"
|
||||
dense
|
||||
@ -17,118 +15,51 @@
|
||||
>
|
||||
<q-tab name="follows" label='follows' />
|
||||
<q-tab name="global" label='global' />
|
||||
<q-tab v-if='botsFeed.length' name="bots" label='bots' />
|
||||
<q-tab name="bots" label='bots' />
|
||||
</q-tabs>
|
||||
<q-tab-panels v-model="tab" animated>
|
||||
<q-tab-panel name="follows" class='no-padding'>
|
||||
<div>
|
||||
<q-virtual-scroll :items='followsFeed' virtual-scroll-item-size="110" ref='followsFeedScroll'>
|
||||
<q-virtual-scroll :items='feed.follows' virtual-scroll-item-size="110" ref='followsFeedScroll'>
|
||||
<template #default="{ item }">
|
||||
<BasePostThread :key="item[0].id" :events="item" @add-event='addEventFollows'/>
|
||||
<BasePostThread :key="item[0].id" :events="item" @add-event='processEvent'/>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
<div v-if='followsFeed.length'>
|
||||
<q-separator color='accent'/>
|
||||
<q-btn-group
|
||||
flat
|
||||
spread
|
||||
dense
|
||||
text-color="accent"
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
:loading='loadingMore'
|
||||
flat
|
||||
color="accent"
|
||||
class='text-weight-light'
|
||||
style='letter-spacing: .1rem;'
|
||||
:label='reachedEnd ? "reached end" : "load 200 more"'
|
||||
:disable='reachedEnd'
|
||||
@click="loadMoreFollowsFeed"
|
||||
>
|
||||
<template #loading>
|
||||
<div class='row justify-center q-my-md'>
|
||||
<q-spinner-orbit color="accent" size='md' />
|
||||
</div>
|
||||
</template>
|
||||
</q-btn>
|
||||
</q-btn-group>
|
||||
<q-separator color='accent'/>
|
||||
</div>
|
||||
<BaseButtonLoadMore
|
||||
:loading-more='loadingMore'
|
||||
label='load another day'
|
||||
@click='loadMore'
|
||||
/>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="global" class='no-padding'>
|
||||
<div>
|
||||
<q-virtual-scroll :items='globalFeed' virtual-scroll-item-size="110" ref='globalFeedScroll'>
|
||||
<q-virtual-scroll :items='feed.global' virtual-scroll-item-size="110" ref='globalFeedScroll'>
|
||||
<template #default="{ item }">
|
||||
<BasePostThread :key="item[0].id" :events="item" @add-event='addEventGlobal'/>
|
||||
<BasePostThread :key="item[0].id" :events="item" @add-event='processEvent'/>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
<div v-if='globalFeed.length'>
|
||||
<q-separator color='accent'/>
|
||||
<q-btn-group
|
||||
flat
|
||||
spread
|
||||
dense
|
||||
text-color="accent"
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
:loading='loadingMore'
|
||||
flat
|
||||
color="accent"
|
||||
class='text-weight-light'
|
||||
style='letter-spacing: .1rem;'
|
||||
label='load another day'
|
||||
@click="loadMoreGlobalFeed"
|
||||
>
|
||||
<template #loading>
|
||||
<div class='row justify-center q-my-md'>
|
||||
<q-spinner-orbit color="accent" size='md' />
|
||||
</div>
|
||||
</template>
|
||||
</q-btn>
|
||||
</q-btn-group>
|
||||
<q-separator color='accent'/>
|
||||
</div>
|
||||
<BaseButtonLoadMore
|
||||
:loading-more='loadingMore'
|
||||
label='load another day'
|
||||
@click='loadMore'
|
||||
/>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel v-if='botsFeed.length' name="bots" class='no-padding hide-scrollbar'>
|
||||
<q-tab-panel name="bots" class='no-padding hide-scrollbar'>
|
||||
<div>
|
||||
<q-virtual-scroll :items='botsFeed' virtual-scroll-item-size="110" ref='botsFeedScroll'>
|
||||
<q-virtual-scroll :items='feed.bots' virtual-scroll-item-size="110" ref='botsFeedScroll'>
|
||||
<template #default="{ item }">
|
||||
<BasePostThread :key="item[0].id" :events="item" @add-event='addEventGlobal'/>
|
||||
<BasePostThread :key="item[0].id" :events="item" @add-event='processEvent'/>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
<div v-if='botsFeed.length'>
|
||||
<q-separator color='accent'/>
|
||||
<q-btn-group
|
||||
flat
|
||||
spread
|
||||
dense
|
||||
text-color="accent"
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
:loading='loadingMore'
|
||||
flat
|
||||
color="accent"
|
||||
class='text-weight-light'
|
||||
style='letter-spacing: .1rem;'
|
||||
label='load another day'
|
||||
@click="loadMoreGlobalFeed"
|
||||
>
|
||||
<template #loading>
|
||||
<div class='row justify-center q-my-md'>
|
||||
<q-spinner-orbit color="accent" size='md' />
|
||||
</div>
|
||||
</template>
|
||||
</q-btn>
|
||||
</q-btn-group>
|
||||
<q-separator color='accent'/>
|
||||
</div>
|
||||
<BaseButtonLoadMore
|
||||
:loading-more='loadingMore'
|
||||
label='load another day'
|
||||
@click='loadMore'
|
||||
/>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
@ -136,163 +67,115 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {pool} from '../pool'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import {addToThread} from '../utils/threads'
|
||||
import {dbGetHomeFeedNotes, onNewHomeFeedNote} from '../db'
|
||||
import {dbStreamFeed, dbUserFollows} from '../query'
|
||||
import BaseButtonLoadMore from 'components/BaseButtonLoadMore.vue'
|
||||
|
||||
export default {
|
||||
name: 'Feed',
|
||||
mixins: [helpersMixin],
|
||||
|
||||
components: {
|
||||
BaseButtonLoadMore,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
listener: null,
|
||||
reachedEnd: false,
|
||||
followsFeed: [],
|
||||
followsFeedSet: new Set(),
|
||||
globalFeed: [],
|
||||
globalFeedSet: new Set(),
|
||||
botsFeed: [],
|
||||
botsFeedSet: new Set(),
|
||||
feed: {
|
||||
follows: [],
|
||||
global: [],
|
||||
bots: []
|
||||
},
|
||||
feedSet: new Set(),
|
||||
bots: [],
|
||||
loadingMore: false,
|
||||
follows: [],
|
||||
botTracker: '29f63b70d8961835b14062b195fc7d84fa810560b36dde0749e4bc084f0f8952',
|
||||
loadingMore: true,
|
||||
tab: 'follows',
|
||||
sub: null,
|
||||
since: null,
|
||||
since: Math.round(Date.now() / 1000) - (3 * 24 * 60 * 60),
|
||||
profilesUsed: new Set(),
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.loadMoreFollowsFeed()
|
||||
this.loadMoreGlobalFeed()
|
||||
this.bots = await this.getFollows(this.botTracker)
|
||||
this.follows = await this.getFollows(this.$store.state.keys.pub)
|
||||
|
||||
this.listener = onNewHomeFeedNote(event => {
|
||||
if (this.followsFeedSet.has(event.id)) return
|
||||
this.loadMore()
|
||||
|
||||
this.followsFeedSet.add(event.id)
|
||||
this.interpolateEventMentions(event)
|
||||
addToThread(this.followsFeed, event, 'feed')
|
||||
})
|
||||
if (this.follows.length === 0) {
|
||||
this.tab = 'global'
|
||||
}
|
||||
},
|
||||
|
||||
async beforeUnmount() {
|
||||
if (this.listener) this.listener.cancel()
|
||||
if (this.sub) this.sub.unsub()
|
||||
if (this.sub) this.sub.cancel()
|
||||
this.sub = null
|
||||
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadMoreFollowsFeed() {
|
||||
async loadMore() {
|
||||
this.loadingMore = true
|
||||
|
||||
let until = this.followsFeed.length === 0
|
||||
? Math.round(Date.now() / 1000)
|
||||
: Math.min.apply(
|
||||
Math,
|
||||
this.followsFeed.flat().map(event => event.created_at)
|
||||
) - 1
|
||||
let loadedNotes = await dbGetHomeFeedNotes(
|
||||
200,
|
||||
until
|
||||
)
|
||||
// loadedNotes = loadedNotes.filter(event => !this.followsFeedSet.has(event.id))
|
||||
if (loadedNotes.length < 200) {
|
||||
this.reachedEnd = true
|
||||
if (this.followsFeed.length === 0) {
|
||||
this.tab = 'global'
|
||||
let loadedFeed = {
|
||||
follows: [],
|
||||
global: [],
|
||||
bots: []
|
||||
}
|
||||
let timer = setTimeout(() => { this.loadingMore = false }, 1000)
|
||||
if (this.sub) {
|
||||
this.since = this.since - (24 * 60 * 60)
|
||||
this.sub.update(this.since)
|
||||
return
|
||||
}
|
||||
this.sub = await dbStreamFeed(this.since, event => {
|
||||
if (!timer) {
|
||||
this.processEvent(event, this.feed)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.interpolateEventMentions(loadedNotes)
|
||||
|
||||
let loadedThreads = []
|
||||
for (let i = loadedNotes.length - 1; i >= 0; i--) {
|
||||
let event = loadedNotes[i]
|
||||
if (this.followsFeedSet.has(event.id)) continue
|
||||
this.followsFeedSet.add(event.id)
|
||||
addToThread(loadedThreads, event, 'feed')
|
||||
// loadedThreads.sort((a, b) => a[0].latest_created_at < b[0].latest_created_at)
|
||||
}
|
||||
this.followsFeed.push(...loadedThreads)
|
||||
this.loadingMore = false
|
||||
},
|
||||
|
||||
async loadMoreGlobalFeed() {
|
||||
this.loadingMore = true
|
||||
if (this.sub) this.sub.unsub()
|
||||
|
||||
if (this.bots.length === 0) {
|
||||
await new Promise(resolve => {
|
||||
let sub = pool.sub({
|
||||
filter: [{authors: ['29f63b70d8961835b14062b195fc7d84fa810560b36dde0749e4bc084f0f8952'], kinds: [3]}],
|
||||
cb: async event => {
|
||||
this.bots = event.tags.filter(([t, v]) => t === 'p' && v).map(([_, v]) => v)
|
||||
clearTimeout(timeout)
|
||||
if (sub) sub.unsub()
|
||||
resolve()
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
for (let feed of Object.keys(this.feed)) {
|
||||
this.feed[feed] = this.feed[feed].concat(loadedFeed[feed])
|
||||
}
|
||||
})
|
||||
let timeout = setTimeout(() => {
|
||||
sub.unsub()
|
||||
sub = null
|
||||
resolve()
|
||||
}, 3000)
|
||||
timer = null
|
||||
this.loadingMore = false
|
||||
}, 300)
|
||||
this.loadingMore = false
|
||||
this.processEvent(event, loadedFeed)
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.since) this.since = Math.floor(Date.now() / 1000) - 86400
|
||||
else this.since -= 86400
|
||||
|
||||
this.sub = pool.sub(
|
||||
{
|
||||
filter: [
|
||||
{
|
||||
kinds: [1, 2],
|
||||
since: this.since,
|
||||
until: this.since + 86400,
|
||||
}
|
||||
],
|
||||
cb: async (event, relay) => {
|
||||
// if (this.globalFeedSet.has(event.id)) return
|
||||
|
||||
// // this.$store.dispatch('useProfile', {
|
||||
// // pubkey: event.pubkey,
|
||||
// // request: true
|
||||
// // })
|
||||
// this.interpolateEventMentions(event)
|
||||
// this.globalFeedSet.add(event.id)
|
||||
// if (this.bots.includes(event.pubkey)) {
|
||||
// addToThread(this.botsFeed, event)
|
||||
// this.botsFeed.sort((a, b) => a[0].latest_created_at < b[0].latest_created_at)
|
||||
// } else {
|
||||
// addToThread(this.globalFeed, event, 'feed')
|
||||
// this.globalFeed.sort((a, b) => a[0].latest_created_at < b[0].latest_created_at)
|
||||
// }
|
||||
this.addEventGlobal(event)
|
||||
return
|
||||
}
|
||||
},
|
||||
'global-feed'
|
||||
)
|
||||
|
||||
this.loadingMore = false
|
||||
},
|
||||
|
||||
addEventFollows(event) {
|
||||
if (this.followsFeedSet.has(event.id)) return
|
||||
processEvent(event, feed = this.feed) {
|
||||
if (this.feedSet.has(event.id)) return
|
||||
this.feedSet.add(event.id)
|
||||
this.interpolateEventMentions(event)
|
||||
this.followsFeedSet.add(event.id)
|
||||
addToThread(this.followsFeed, event, 'feed')
|
||||
this.useProfile(event.pubkey)
|
||||
|
||||
if (this.follows.includes(event.pubkey)) addToThread(feed.follows, Object.assign({}, event), 'feed')
|
||||
if (this.bots.includes(event.pubkey)) addToThread(feed.bots, Object.assign({}, event), 'feed')
|
||||
else addToThread(feed.global, Object.assign({}, event), 'feed')
|
||||
},
|
||||
|
||||
addEventGlobal(event) {
|
||||
if (this.globalFeedSet.has(event.id)) return
|
||||
this.interpolateEventMentions(event)
|
||||
this.globalFeedSet.add(event.id)
|
||||
if (this.bots.includes(event.pubkey)) {
|
||||
addToThread(this.botsFeed, event)
|
||||
} else {
|
||||
addToThread(this.globalFeed, event, 'feed')
|
||||
}
|
||||
async getFollows(pubkey) {
|
||||
let event = await dbUserFollows(pubkey)
|
||||
if (!event) return []
|
||||
return event.tags
|
||||
.filter(([t, v]) => t === 'p' && v)
|
||||
.map(([_, v]) => v)
|
||||
},
|
||||
|
||||
useProfile(pubkey) {
|
||||
if (this.profilesUsed.has(pubkey)) return
|
||||
|
||||
this.profilesUsed.add(pubkey)
|
||||
this.$store.dispatch('useProfile', {pubkey})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,83 +3,76 @@
|
||||
<div class="text-h5 text-bold q-py-md">{{'#' + this.$route.params.hashtagId}}</div>
|
||||
<q-separator color='accent' size='2px'/>
|
||||
<div>
|
||||
<BasePostThread v-for="thread in threads" :key="thread[0].id" :events="thread" @add-event='addEvent'/>
|
||||
<BasePostThread v-for="thread in threads" :key="thread[0].id" :events="thread" @add-event='processEvent'/>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import {pool} from '../pool'
|
||||
import {dbStreamTagKind} from '../query'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import {addToThread} from '../utils/threads'
|
||||
// import BaseUserCard from 'components/BaseUserCard.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Hashtag',
|
||||
mixins: [helpersMixin],
|
||||
|
||||
components: {
|
||||
// BaseUserCard,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
threads: [],
|
||||
eventsSet: new Set(),
|
||||
sub: null,
|
||||
sub: {},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.params.hashtagId'(curr, prev) {
|
||||
if (curr !== prev && curr && prev) {
|
||||
this.stop()
|
||||
this.start()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
activated() {
|
||||
this.listen()
|
||||
this.start()
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
if (this.sub) this.sub.unsub()
|
||||
this.stop()
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
listen() {
|
||||
async start() {
|
||||
this.threads = []
|
||||
this.eventsSet = new Set()
|
||||
|
||||
this.sub = pool.sub(
|
||||
{
|
||||
filter: [
|
||||
{
|
||||
'#hashtag': [this.$route.params.hashtagId.toLowerCase()],
|
||||
kinds: [1, 2]
|
||||
}
|
||||
],
|
||||
cb: async (event, relay) => {
|
||||
switch (event.kind) {
|
||||
case 0:
|
||||
await this.$store.dispatch('addEvent', {event, relay})
|
||||
return
|
||||
this.sub.hashtag = await dbStreamTagKind('e', this.$route.params.hashtagId.toLowerCase(), 1, event => {
|
||||
this.processEvent(event)
|
||||
})
|
||||
|
||||
case 1:
|
||||
case 2:
|
||||
if (this.eventsSet.has(event.id)) return
|
||||
|
||||
this.interpolateEventMentions(event)
|
||||
this.eventsSet.add(event.id)
|
||||
addToThread(this.threads, event)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
'hashtag-browser'
|
||||
)
|
||||
this.sub.hashtagOld = await dbStreamTagKind('hashtag', this.$route.params.hashtagId.toLowerCase(), 1, event => {
|
||||
this.processEvent(event)
|
||||
})
|
||||
},
|
||||
|
||||
addEvent(event) {
|
||||
stop() {
|
||||
if (this.sub.hashtag) this.sub.hashtag.cancel()
|
||||
if (this.sub.oldHashtag) this.sub.oldHashtag.cancel()
|
||||
this.threads = []
|
||||
this.eventsSet = new Set()
|
||||
},
|
||||
|
||||
processEvent(event) {
|
||||
if (this.eventsSet.has(event.id)) return
|
||||
|
||||
this.interpolateEventMentions(event)
|
||||
this.eventsSet.add(event.id)
|
||||
addToThread(this.threads, event)
|
||||
}
|
||||
return
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -2,15 +2,6 @@
|
||||
<q-page>
|
||||
<div class="text-h5 text-bold q-py-md full-width flex row justify-start">
|
||||
{{ $t('inbox') }}
|
||||
<!-- <q-btn
|
||||
v-if='allChatsNeverRead'
|
||||
label="mark all as read"
|
||||
color="secondary"
|
||||
class='q-ml-lg'
|
||||
outline
|
||||
dense
|
||||
@click.stop='markAllAsRead'
|
||||
/> -->
|
||||
</div>
|
||||
<q-separator color='accent' size='2px'/>
|
||||
|
||||
@ -24,18 +15,18 @@
|
||||
v-ripple
|
||||
clickable
|
||||
class='flex row no-padding no-margin justify-between items-center q-gutter-xs'
|
||||
:to="{ name: 'messages', params: { pubkey: chat.peer }}"
|
||||
@click.capture.stop="$router.push({ name: 'messages', params: { pubkey: chat.peer }})"
|
||||
>
|
||||
<div class='col q-pl-md q-pr-auto flex row' style='max-width: 350px; width: 350px;'>
|
||||
<BaseUserCard v-if='chat.peer' :pubkey='chat.peer' :action-buttons='false' class='col' :clickable='false'/>
|
||||
<q-badge
|
||||
v-if="$store.state.unreadMessages[chat.peer]"
|
||||
color="secondary"
|
||||
outline
|
||||
class='text-bold q-my-auto'
|
||||
>
|
||||
{{ $store.state.unreadMessages[chat.peer] }}
|
||||
</q-badge>
|
||||
<BaseUserCard v-if='chat.peer' :pubkey='chat.peer' :action-buttons='false' class='col' :clickable='false'/>
|
||||
<q-badge
|
||||
v-if="$store.state.unreadMessages[chat.peer]"
|
||||
color="secondary"
|
||||
outline
|
||||
class='text-bold q-my-auto'
|
||||
>
|
||||
{{ $store.state.unreadMessages[chat.peer] }}
|
||||
</q-badge>
|
||||
</div>
|
||||
<label class='no-padding text-right'>
|
||||
{{ niceDateUTC(chat.lastMessage) }}
|
||||
@ -52,10 +43,9 @@
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
<!-- :button-to="'/messages/' + pubkey" -->
|
||||
|
||||
<script>
|
||||
import {dbGetChats} from '../db'
|
||||
import {dbChats} from '../query'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
|
||||
export default {
|
||||
@ -67,6 +57,7 @@ export default {
|
||||
chats: [],
|
||||
loading: true,
|
||||
noChats: false,
|
||||
profilesUsed: new Set(),
|
||||
}
|
||||
},
|
||||
|
||||
@ -76,22 +67,31 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.chats = await dbGetChats(this.$store.state.keys.pub)
|
||||
async activated() {
|
||||
this.chats = await dbChats(this.$store.state.keys.pub)
|
||||
if (this.chats.length === 0) this.noChats = true
|
||||
this.chats.forEach(({peer}) =>
|
||||
this.$store.dispatch('useProfile', {pubkey: peer})
|
||||
)
|
||||
this.chats.forEach(({peer}) => this.useProfile(peer))
|
||||
if (this.allChatsNeverRead) this.chats.forEach(({peer}) => this.$store.commit('haveReadMessage', peer))
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
|
||||
},
|
||||
|
||||
methods: {
|
||||
markAllAsRead() {
|
||||
this.chats.forEach(chat => {
|
||||
this.$store.commit('haveReadMessage', chat.peer)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
useProfile(pubkey) {
|
||||
if (this.profilesUsed.has(pubkey)) return
|
||||
|
||||
this.profilesUsed.add(pubkey)
|
||||
this.$store.dispatch('useProfile', {pubkey})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -29,12 +29,9 @@
|
||||
</div>
|
||||
<div ref='messageScroll' class='col overflow-auto' @scroll='updateCurrentDatestamp'>
|
||||
<q-infinite-scroll @load="loadMore" reverse ref='messagesScroll'>
|
||||
<!-- <q-intersection
|
||||
@visibility='test'
|
||||
> -->
|
||||
<div
|
||||
v-for="(event, index) in messages"
|
||||
:key="event.id"
|
||||
:key="event.id + '_' + event.taggedEvents?.length"
|
||||
class='flex column items-self'
|
||||
>
|
||||
<div
|
||||
@ -48,10 +45,9 @@
|
||||
:id="event.id"
|
||||
:event="event"
|
||||
v-scroll-fire='markAsRead'
|
||||
@scroll-to='scrollToBottom'
|
||||
@mounted='scrollToBottom'
|
||||
@reply='reply'
|
||||
/>
|
||||
<!-- </q-intersection > -->
|
||||
</div>
|
||||
<template #loading>
|
||||
<div v-if='canLoadMore' class='row justify-center q-my-md'>
|
||||
@ -72,7 +68,6 @@
|
||||
size='sm'
|
||||
@click.stop='scrollToBottom()'
|
||||
/>
|
||||
<!-- <q-separator v-if='Object.keys(replyEvent).length' color='primary' size='1px'/> -->
|
||||
<BasePostEntry
|
||||
:message-mode='replyEvent? "reply" : "message"'
|
||||
:event='replyEvent'
|
||||
@ -84,16 +79,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {decrypt} from 'nostr-tools/nip04'
|
||||
|
||||
import helpersMixin from '../utils/mixin'
|
||||
// import {getElementFullHeight, isElementFullyScrolled} from '../utils/helpers'
|
||||
// import {isElementFullyScrolled} from '../utils/helpers'
|
||||
// import { scroll } from 'quasar'
|
||||
// const { getVerticalScrollPosition, setVerticalScrollPosition} = scroll
|
||||
// import {dbGetEvent} from '../db'
|
||||
import {pool} from '../pool'
|
||||
import {dbGetMessages, onNewMessage, dbGetEvent} from '../db'
|
||||
import {dbMessages, streamMessages} from '../query'
|
||||
import BaseMessage from 'components/BaseMessage.vue'
|
||||
|
||||
export default {
|
||||
@ -106,35 +93,31 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
listener: null,
|
||||
sub: null,
|
||||
messages: [],
|
||||
canLoadMore: true,
|
||||
text: '',
|
||||
// sending: null,
|
||||
messagesSet: new Set(),
|
||||
unreadMessagesSet: new Set(),
|
||||
unlock: () => {},
|
||||
mutex: null,
|
||||
eventSubs: {},
|
||||
replyEvent: null,
|
||||
currentDatestamp: null,
|
||||
}
|
||||
},
|
||||
|
||||
async activated() {
|
||||
// load peer profile if it exists
|
||||
this.$store.dispatch('useProfile', {pubkey: this.$route.params.pubkey})
|
||||
|
||||
// load saved messages and start listening for new ones
|
||||
await this.start()
|
||||
this.scrollToBottom()
|
||||
},
|
||||
|
||||
async deactivated() {
|
||||
if (this.listener) {
|
||||
this.listener.cancel()
|
||||
this.listener = null
|
||||
if (this.sub) {
|
||||
this.sub.cancel()
|
||||
this.sub = null
|
||||
}
|
||||
this.$store.dispatch('cancelUseProfile', {pubkey: this.$route.params.pubkey})
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -150,10 +133,14 @@ export default {
|
||||
|
||||
async start() {
|
||||
// this.messagesSet = new Set()
|
||||
if (this.listener) this.listener.cancel()
|
||||
if (this.sub) this.sub.cancel()
|
||||
|
||||
// load peer profile if it exists
|
||||
this.$store.dispatch('useProfile', {pubkey: this.$route.params.pubkey})
|
||||
|
||||
if (this.$store.state.unreadMessages[this.$route.params.pubkey]) {
|
||||
let newMessages = await dbGetMessages(
|
||||
let newMessages = await dbMessages(
|
||||
this.$store.state.keys.pub,
|
||||
this.$route.params.pubkey,
|
||||
this.$store.state.unreadMessages[this.$route.params.pubkey]
|
||||
)
|
||||
@ -161,9 +148,15 @@ export default {
|
||||
this.messages.push(...newMessagesFiltered)
|
||||
}
|
||||
this.$store.commit('haveReadMessage', this.$route.params.pubkey)
|
||||
this.$store.dispatch('useProfile', {pubkey: this.$route.params.pubkey})
|
||||
this.listener = onNewMessage(this.$route.params.pubkey, async event => {
|
||||
this.addMessage(event)
|
||||
// this.$store.dispatch('useProfile', {pubkey: this.$route.params.pubkey, request: true})
|
||||
this.sub = await streamMessages(async event => {
|
||||
let eventUserTags = event.tags
|
||||
.filter(([t, v]) => t === 'p' && v)
|
||||
.map(([_, v]) => v)
|
||||
if ((event.pubkey === this.$route.params.pubkey && eventUserTags.includes(this.$store.state.keys.pub)) ||
|
||||
(event.pubkey === this.$store.state.keys.pub && eventUserTags.includes(this.$route.params.pubkey))
|
||||
)
|
||||
this.addMessage(event)
|
||||
})
|
||||
},
|
||||
|
||||
@ -172,6 +165,7 @@ export default {
|
||||
return new Promise(resolve =>
|
||||
setTimeout(() => {
|
||||
this.$refs.messageScroll.scrollTop = this.$refs.messageScroll.scrollHeight
|
||||
this.$store.commit('haveReadMessage', this.$route.params.pubkey)
|
||||
this.unreadMessagesSet.clear()
|
||||
resolve()
|
||||
}, 10)
|
||||
@ -179,11 +173,13 @@ export default {
|
||||
},
|
||||
|
||||
async loadMore(_, done) {
|
||||
let loadedMessages = await dbGetMessages(
|
||||
let loadedMessages = await dbMessages(
|
||||
this.$store.state.keys.pub,
|
||||
this.$route.params.pubkey,
|
||||
50,
|
||||
this.messages[0]?.created_at - 1 || ''
|
||||
this.messages[0]?.created_at - 1 || Math.round(Date.now() / 1000)
|
||||
)
|
||||
// console.log('loadedMessages', loadedMessages)
|
||||
|
||||
if (loadedMessages.length < 50) {
|
||||
this.canLoadMore = false
|
||||
@ -192,62 +188,25 @@ export default {
|
||||
// newMessages = newMessages.filter(event => !this.messagesSet.has(event.id))
|
||||
let loadedMessagesFiltered = await this.processMessages(loadedMessages)
|
||||
|
||||
// for (let i = 0; i < newMessages.length; i++) {
|
||||
// // await newMessages.forEach(async (event) => {
|
||||
// let event = newMessages[i]
|
||||
// if (this.messagesSet.has(event.id)) return
|
||||
|
||||
// this.messagesSet.add(event.id)
|
||||
// event.text = await this.getPlaintext(event)
|
||||
// this.interpolateMessageMentions(event)
|
||||
// if (event.tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event)
|
||||
// if (event.appended) {
|
||||
// for (let j = 0; j < event.appended.length; j++) {
|
||||
// this.messagesSet.add(event.appended[j].id)
|
||||
// event.appended[j].text = await this.getPlaintext(event.appended[j])
|
||||
// this.interpolateMessageMentions(event.appended[j])
|
||||
// if (event.appended[j].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event.appended[j])
|
||||
// }
|
||||
// }
|
||||
// newMessagesFiltered.push(event)
|
||||
// }
|
||||
// this.messagesSet.add(newMessages[i].id)
|
||||
// newMessages[i].text = await this.getPlaintext(newMessages[i])
|
||||
// this.interpolateMessageMentions(newMessages[i])
|
||||
// if (newMessages[i].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(newMessages[i])
|
||||
// if (newMessages[i].appended) {
|
||||
// for (let j = 0; j < newMessages[i].appended.length; j++) {
|
||||
// this.messagesSet.add(newMessages[i].appended[j].id)
|
||||
// newMessages[i].appended[j].text = await this.getPlaintext(newMessages[i].appended[j])
|
||||
// this.interpolateMessageMentions(newMessages[i].appended[j])
|
||||
// if (newMessages[i].appended[j].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(newMessages[i].appended[j])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// this.messages = newMessages.concat(this.messages)
|
||||
this.messages = loadedMessagesFiltered.concat(this.messages)
|
||||
done(!this.canLoadMore)
|
||||
},
|
||||
|
||||
async processMessages(messages) {
|
||||
let messagesFiltered = []
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
// await messages.forEach(async (event) => {
|
||||
let event = messages[i]
|
||||
if (this.messagesSet.has(event.id)) return
|
||||
if (this.messagesSet.has(event.id)) continue
|
||||
|
||||
this.messagesSet.add(event.id)
|
||||
event.text = await this.getPlaintext(event)
|
||||
this.interpolateMessageMentions(event)
|
||||
if (event.tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event)
|
||||
if (event.appended) {
|
||||
for (let j = 0; j < event.appended.length; j++) {
|
||||
this.messagesSet.add(event.appended[j].id)
|
||||
event.appended[j].text = await this.getPlaintext(event.appended[j])
|
||||
this.interpolateMessageMentions(event.appended[j])
|
||||
if (event.appended[j].tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event.appended[j])
|
||||
}
|
||||
}
|
||||
messagesFiltered.push(event)
|
||||
@ -255,39 +214,6 @@ export default {
|
||||
return messagesFiltered
|
||||
},
|
||||
|
||||
async getPlaintext(event) {
|
||||
if (
|
||||
event.tags.find(
|
||||
([tag, value]) => tag === 'p' && value === this.$store.state.keys.pub
|
||||
)
|
||||
) {
|
||||
// it is addressed to us
|
||||
// decrypt it
|
||||
return await this.decrypt(event.pubkey, event.content)
|
||||
} else if (event.pubkey === this.$store.state.keys.pub) {
|
||||
// it is coming from us
|
||||
let [_, target] = event.tags.find(([tag]) => tag === 'p')
|
||||
// decrypt it
|
||||
return await this.decrypt(target, event.content)
|
||||
}
|
||||
},
|
||||
|
||||
async decrypt(peer, ciphertext) {
|
||||
try {
|
||||
if (this.$store.state.keys.priv) {
|
||||
return decrypt(this.$store.state.keys.priv, peer, ciphertext)
|
||||
} else if (
|
||||
(await window?.nostr?.getPublicKey?.()) === this.$store.state.keys.pub
|
||||
) {
|
||||
return await window.nostr.nip04.decrypt(peer, ciphertext)
|
||||
} else {
|
||||
throw new Error('no private key available to decrypt!')
|
||||
}
|
||||
} catch (err) {
|
||||
return '???'
|
||||
}
|
||||
},
|
||||
|
||||
markAsRead(element) {
|
||||
if (this.unreadMessagesSet.size === 0) return
|
||||
if (!this.unreadMessagesSet.has(element.id)) return
|
||||
@ -297,57 +223,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async processTaggedEvents(event) {
|
||||
let tagged = event.tags.filter(([t, v]) => t === 'e' && v).map(([t, v]) => v)
|
||||
// console.log('processing tagged events for: ', event, tagged)
|
||||
tagged.splice(10)
|
||||
event.taggedEvents = []
|
||||
this.listenReposts(tagged, event.taggedEvents)
|
||||
},
|
||||
|
||||
async listenReposts(eventIds, events) {
|
||||
// let subEventIds = []
|
||||
// let this.events = []
|
||||
for (let eventId of eventIds) {
|
||||
let event = await dbGetEvent(eventId)
|
||||
if (event) {
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: event.pubkey,
|
||||
request: true
|
||||
})
|
||||
if (event.kind === 1 || event.kind === 2) this.interpolateEventMentions(event)
|
||||
else if (event.kind === 4) {
|
||||
event.text = await this.getPlaintext(event)
|
||||
this.interpolateMessageMentions(event)
|
||||
}
|
||||
events.push(event)
|
||||
// } else {
|
||||
// subEventIds.push(eventId)
|
||||
} else this.eventSubs[eventId] = pool.sub(
|
||||
{
|
||||
filter: {ids: eventId},
|
||||
cb: async event => {
|
||||
this.eventSubs[eventId].unsub()
|
||||
this.$store.dispatch('useProfile', {
|
||||
pubkey: event.pubkey,
|
||||
request: true
|
||||
})
|
||||
if (event.kind === 1 || event.kind === 2) this.interpolateEventMentions(event)
|
||||
else if (event.kind === 4) {
|
||||
event.text = await this.getPlaintext(event)
|
||||
this.interpolateMessageMentions(event)
|
||||
}
|
||||
events.push(event)
|
||||
// this.event = event
|
||||
}
|
||||
},
|
||||
'event-browser'
|
||||
)
|
||||
}
|
||||
// console.log('this.events: ', this.events)
|
||||
// console.log('subEventIds: ', subEventIds)
|
||||
},
|
||||
|
||||
reply(event) {
|
||||
this.replyEvent = null
|
||||
setTimeout(() => {
|
||||
@ -361,21 +236,10 @@ export default {
|
||||
// console.log('scrolled', event)
|
||||
let messageScroll = this.$refs.messageScroll
|
||||
let datestamps = Array.from(messageScroll.querySelectorAll('.datestamp'))
|
||||
// let inView = (messageScroll.scrollTop < )
|
||||
// console.log('datestamps', datestamps)
|
||||
// console.log('datestamps', datestamps.map((node) => {
|
||||
// return {
|
||||
// offsetHeight: node.offsetHeight,
|
||||
// offsetTop: node.offsetTop,
|
||||
// inView: (messageScroll.scrollTop < node.offsetTop && messageScroll.scrollTop + messageScroll.clientHeight > node.offsetTop)
|
||||
// }
|
||||
// }))
|
||||
this.currentDatestamp = datestamps.reduce((p, c) => {
|
||||
if (c.offsetTop < messageScroll.scrollTop + c.offsetHeight) return c.innerText
|
||||
else return p
|
||||
}, datestamps[0].innerText)
|
||||
// console.log(messageScroll.scrollHeight, messageScroll.clientHeight, messageScroll.scrollTop)
|
||||
// console.log('currentDatestamp', this.currentDatestamp)
|
||||
},
|
||||
|
||||
async messageSent(event) {
|
||||
@ -391,7 +255,6 @@ export default {
|
||||
event.text = await this.getPlaintext(event)
|
||||
this.unlock()
|
||||
this.interpolateMessageMentions(event)
|
||||
if (event.tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event)
|
||||
|
||||
let messageScroll = this.$refs.messageScroll
|
||||
let scrollToBottom = 100 > Math.abs((messageScroll.scrollHeight - messageScroll.clientHeight) - messageScroll.scrollTop) ||
|
||||
|
@ -13,7 +13,6 @@
|
||||
:event="event"
|
||||
:highlighted="$store.state.lastNotificationRead < event.created_at"
|
||||
/>
|
||||
<!-- v-scroll-fire='markAsRead' -->
|
||||
</div>
|
||||
<template #loading>
|
||||
<div v-if='!reachedEnd' class='row justify-center q-my-md'>
|
||||
@ -26,7 +25,7 @@
|
||||
|
||||
<script>
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import {dbGetMentions, onNewMention} from '../db'
|
||||
import {dbMentions, streamMentions} from '../query'
|
||||
|
||||
export default {
|
||||
name: 'Notifications',
|
||||
@ -37,99 +36,78 @@ export default {
|
||||
notifications: [],
|
||||
notificationsSet: new Set(),
|
||||
reachedEnd: false,
|
||||
listener: null,
|
||||
reading: false
|
||||
sub: null,
|
||||
reading: false,
|
||||
profilesUsed: new Set(),
|
||||
}
|
||||
},
|
||||
|
||||
async activated() {
|
||||
if (this.$store.state.unreadNotifications) this.loadNew()
|
||||
|
||||
this.listener = onNewMention(this.$store.state.keys.pub, async event => {
|
||||
if (this.notificationsSet.has(event.id)) return
|
||||
this.interpolateEventMentions(event)
|
||||
this.addNotificationEvent(event)
|
||||
this.notificationsSet.add(event.id)
|
||||
this.sub = streamMentions(this.$store.state.keys.pub, async event => {
|
||||
let loadedNotificationsFiltered = await this.processNotifications([event])
|
||||
if (loadedNotificationsFiltered.length === 0) return
|
||||
this.notifications = loadedNotificationsFiltered.concat(this.notifications)
|
||||
this.highlightUnreadNotifications()
|
||||
})
|
||||
},
|
||||
|
||||
async deactivated() {
|
||||
this.$store.commit('haveReadNotifications')
|
||||
if (this.listener) this.listener.cancel()
|
||||
if (this.sub) this.sub.cancel()
|
||||
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadMore(_, done) {
|
||||
// if (this.notifications.length === 0) {
|
||||
// this.reachedEnd = true
|
||||
// done()
|
||||
// return
|
||||
// }
|
||||
// this.notifications = await dbGetMentions(
|
||||
// this.$store.state.keys.pub,
|
||||
// 40,
|
||||
// 0,
|
||||
// Math.round(Date.now() / 1000)
|
||||
// )
|
||||
// if (this.notifications.length > 0) {
|
||||
// this.reachedEnd = false
|
||||
// }
|
||||
|
||||
// this.notifications.forEach(({pubkey}) => {
|
||||
// this.$store.dispatch('useProfile', {pubkey, request: true})
|
||||
// })
|
||||
let until
|
||||
if (this.notifications.length) until = this.notifications[this.notifications.length - 1].created_at - 1
|
||||
else until = Math.round(Date.now() / 1000)
|
||||
let loadedNotifications = await dbGetMentions(
|
||||
let loadedNotifications = await dbMentions(
|
||||
this.$store.state.keys.pub,
|
||||
40,
|
||||
0,
|
||||
until
|
||||
)
|
||||
if (loadedNotifications.length < 40) {
|
||||
this.reachedEnd = true
|
||||
}
|
||||
// loadedNotifications = loadedNotifications.filter(event => !this.notificationsSet.has(event.id))
|
||||
this.interpolateEventMentions(loadedNotifications)
|
||||
loadedNotifications.forEach(event => {
|
||||
if (this.notificationsSet.has(event.id)) return
|
||||
|
||||
this.notificationsSet.add(event.id)
|
||||
this.addNotificationEvent(event)
|
||||
this.$store.dispatch('useProfile', {pubkey: event.pubkey, request: true})
|
||||
})
|
||||
// this.notifications = this.notifications.concat(loadedNotifications)
|
||||
let loadedNotificationsFiltered = await this.processNotifications(loadedNotifications)
|
||||
this.notifications = this.notifications.concat(loadedNotificationsFiltered)
|
||||
// will mark notifications as read after 3 * unread count seconds in the page
|
||||
if (
|
||||
this.notifications.length > 0 &&
|
||||
this.notifications[0].created_at > this.$store.state.lastNotificationRead
|
||||
) {
|
||||
setTimeout(() => {
|
||||
this.$store.commit('haveReadNotifications')
|
||||
}, 3000 * this.notifications.filter(n => n.created_at > this.$store.state.lastNotificationRead).length)
|
||||
}
|
||||
this.highlightUnreadNotifications()
|
||||
done(this.reachedEnd)
|
||||
},
|
||||
|
||||
async loadNew() {
|
||||
let until = Math.round(Date.now() / 1000)
|
||||
let since = this.$store.state.lastNotificationRead
|
||||
let loadedNotifications = await dbGetMentions(
|
||||
let loadedNotifications = await dbMentions(
|
||||
this.$store.state.keys.pub,
|
||||
40,
|
||||
since,
|
||||
until
|
||||
40
|
||||
)
|
||||
loadedNotifications = loadedNotifications.filter(event => !this.notificationsSet.has(event.id))
|
||||
this.interpolateEventMentions(loadedNotifications)
|
||||
loadedNotifications.forEach(event => {
|
||||
let loadedNotificationsFiltered = await this.processNotifications(loadedNotifications)
|
||||
this.notifications = loadedNotificationsFiltered.concat(this.notifications)
|
||||
// will mark notifications as read after 3 * unread count seconds in the page
|
||||
this.highlightUnreadNotifications()
|
||||
},
|
||||
|
||||
processNotifications(notifications) {
|
||||
let notificationsFiltered = []
|
||||
for (let i = 0; i < notifications.length; i++) {
|
||||
// await notifications.forEach(async (event) => {
|
||||
let event = notifications[i]
|
||||
if (this.notificationsSet.has(event.id)) continue
|
||||
|
||||
this.notificationsSet.add(event.id)
|
||||
this.addNotificationEvent(event)
|
||||
this.$store.dispatch('useProfile', {pubkey: event.pubkey, request: true})
|
||||
})
|
||||
// this.notifications = loadedNotifications.concat(this.notifications)
|
||||
// will mark notifications as read after 3 * unread count seconds in the page
|
||||
this.interpolateEventMentions(event)
|
||||
// if (event.tags.filter(([t, v]) => t === 'e' && v).length) this.processTaggedEvents(event)
|
||||
notificationsFiltered.push(event)
|
||||
this.useProfile(event.pubkey)
|
||||
}
|
||||
return notificationsFiltered
|
||||
},
|
||||
|
||||
highlightUnreadNotifications() {
|
||||
if (
|
||||
this.notifications.length > 0 &&
|
||||
this.notifications[0].created_at > this.$store.state.lastNotificationRead
|
||||
@ -141,15 +119,6 @@ export default {
|
||||
},
|
||||
|
||||
addNotificationEvent(event) {
|
||||
// manual sorting
|
||||
// for (let i = 0; i < this.notifications.length; i++) {
|
||||
// if (event.created_at < this.notifications[i].created_at) {
|
||||
// // the new event is older than the current index,
|
||||
// // so we add it at the previous index
|
||||
// this.notifications.splice(i, 0, event)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
if (this.notifications.length === 0) {
|
||||
this.notifications.push(event)
|
||||
return
|
||||
@ -176,7 +145,14 @@ export default {
|
||||
|
||||
// the event is the oldest, add to end
|
||||
this.notifications.push(event)
|
||||
}
|
||||
},
|
||||
|
||||
useProfile(pubkey) {
|
||||
if (this.profilesUsed.has(pubkey)) return
|
||||
|
||||
this.profilesUsed.add(pubkey)
|
||||
this.$store.dispatch('useProfile', {pubkey})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -11,44 +11,54 @@
|
||||
>
|
||||
<q-tab name="posts" label='posts' />
|
||||
<q-tab name="follows" label='follows' />
|
||||
<q-tab name="followers" label='followers' />
|
||||
<q-tab name="relays" label='relays' />
|
||||
</q-tabs>
|
||||
<q-tab-panels v-model="tab" animated>
|
||||
<q-tab-panel name="posts" class='no-padding'>
|
||||
<div>
|
||||
<BasePostThread v-for="thread in threads" :key="thread[0].id" :events="thread" @add-event='addEvent'/>
|
||||
<BaseButtonLoadMore :loading-more='loadingMore' :reached-end='reachedEnd' @click='loadMore' />
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="follows" class='no-padding'>
|
||||
<div v-if="!follows">{{ $t('noFollows') }}</div>
|
||||
<div v-else class="flex column relative">
|
||||
<!-- <q-btn
|
||||
v-if="$store.getters.hasMoreContacts($route.params.pubkey)"
|
||||
:name="showAllContacts ? 'show less' : 'show all'"
|
||||
:label="showAllContacts ? 'show less' : 'show all'"
|
||||
:icon-right="showAllContacts ? 'expand_less' : 'expand_more'"
|
||||
color="secondary"
|
||||
class='q-ma-sm'
|
||||
outline
|
||||
size='sm'
|
||||
@click="showAllContacts = !showAllContacts"
|
||||
/> -->
|
||||
<div class='q-pl-sm'>
|
||||
<BaseUserCard
|
||||
v-for="(user) in follows"
|
||||
:key="user.pubkey"
|
||||
:pubkey="user.pubkey"
|
||||
v-for="(pubkey) in follows"
|
||||
:key="pubkey"
|
||||
:pubkey="pubkey"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="followers" class='no-padding'>
|
||||
<div v-if="!followers">{{ $t('noFollowers') }}</div>
|
||||
<div v-else class="flex column relative">
|
||||
<div class='q-pl-sm'>
|
||||
<BaseUserCard
|
||||
v-for="(pubkey) in Object.keys(followers)"
|
||||
:key="pubkey"
|
||||
:pubkey="pubkey"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="relays" class='no-padding'>
|
||||
<div v-if="!relays">{{ $t('noRelays') }}</div>
|
||||
<div v-else class="flex column relative">
|
||||
<div class='q-pl-sm'>
|
||||
<BaseRelayRecommend
|
||||
v-for="(relay) in Object.keys(relays)"
|
||||
:key="relay"
|
||||
:url="relay"
|
||||
:list-view='true'
|
||||
/>
|
||||
</div>
|
||||
<!-- <q-btn
|
||||
v-if='!showAllContacts && $store.getters.hasMoreContacts($route.params.pubkey)'
|
||||
icon='more_vert'
|
||||
size='xl'
|
||||
class='q-pa-md justify-start items-start'
|
||||
flat
|
||||
dense
|
||||
@click="showAllContacts = true"
|
||||
/> -->
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
@ -57,10 +67,12 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import {pool} from '../pool'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import {addToThread} from '../utils/threads'
|
||||
import BaseUserCard from 'components/BaseUserCard.vue'
|
||||
import { dbStreamUserFollows, dbStreamUserFollowers, streamUserNotes, dbUserNotes } from '../query'
|
||||
import BaseRelayRecommend from 'components/BaseRelayRecommend.vue'
|
||||
import BaseButtonLoadMore from 'components/BaseButtonLoadMore.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Profile',
|
||||
@ -68,21 +80,23 @@ export default defineComponent({
|
||||
|
||||
components: {
|
||||
BaseUserCard,
|
||||
BaseRelayRecommend,
|
||||
BaseButtonLoadMore,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
threads: [],
|
||||
eventsSet: new Set(),
|
||||
sub: null,
|
||||
showAllContacts: false,
|
||||
tab: 'posts'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
follows() {
|
||||
return this.$store.getters.contacts(this.$route.params.pubkey)
|
||||
sub: {},
|
||||
tab: 'posts',
|
||||
followsEvent: null,
|
||||
follows: [],
|
||||
followers: [],
|
||||
relays: {},
|
||||
profilesUsed: new Set(),
|
||||
loadingMore: true,
|
||||
reachedEnd: false,
|
||||
}
|
||||
},
|
||||
|
||||
@ -91,58 +105,79 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
if (this.sub) this.sub.unsub()
|
||||
this.stop()
|
||||
},
|
||||
|
||||
methods: {
|
||||
start() {
|
||||
this.listen()
|
||||
this.$store.dispatch('useProfile', {pubkey: this.$route.params.pubkey, request: true})
|
||||
this.$store.dispatch('useContacts', {pubkey: this.$route.params.pubkey, request: true})
|
||||
this.$store.getters
|
||||
.contacts(this.$route.params.pubkey)
|
||||
?.forEach(pubkey => this.$store.dispatch('useProfile', {pubkey}))
|
||||
async start() {
|
||||
this.useProfile(this.$route.params.pubkey)
|
||||
this.loadingMore = true
|
||||
|
||||
let timer = setTimeout(async() => {
|
||||
this.loadMore()
|
||||
}, 4000)
|
||||
this.sub.streamUserNotes = streamUserNotes(this.$route.params.pubkey, event => {
|
||||
if (!timer) this.processUserNotes([event], this.threads)
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(async() => {
|
||||
this.loadMore()
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}, 500)
|
||||
})
|
||||
this.sub.dbStreamUserFollows = dbStreamUserFollows(this.$route.params.pubkey, event => {
|
||||
if (this.followsEvent && event.created_at < this.followsEvent.created_at) return
|
||||
this.followsEvent = event
|
||||
this.follows = event.tags
|
||||
.filter(([t, v]) => t === 'p' && v)
|
||||
.map(([_, v]) => v)
|
||||
this.relays = JSON.parse(event.content)
|
||||
if (this.follows.length)
|
||||
this.follows.forEach(pubkey => this.useProfile(pubkey))
|
||||
})
|
||||
this.sub.dbStreamUserFollowers = dbStreamUserFollowers(this.$route.params.pubkey, event => {
|
||||
this.followers[event.pubkey] = true
|
||||
this.useProfile(event.pubkey)
|
||||
})
|
||||
},
|
||||
|
||||
listen() {
|
||||
this.threads = []
|
||||
this.eventsSet = new Set()
|
||||
stop() {
|
||||
if (this.sub.streamUserNotes) this.sub.streamUserNotes.cancel()
|
||||
if (this.sub.dbStreamUserFollows) this.sub.dbStreamUserFollows.cancel()
|
||||
if (this.sub.dbStreamUserFollowers) this.sub.dbStreamUserFollowers.cancel()
|
||||
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
|
||||
},
|
||||
|
||||
this.sub = pool.sub(
|
||||
{
|
||||
filter: [
|
||||
{
|
||||
authors: [this.$route.params.pubkey],
|
||||
kinds: [0, 1, 2]
|
||||
}
|
||||
],
|
||||
cb: async (event, relay) => {
|
||||
switch (event.kind) {
|
||||
case 0:
|
||||
await this.$store.dispatch('addEvent', {event, relay})
|
||||
return
|
||||
processUserNotes(events, threads) {
|
||||
for (let event of events) {
|
||||
if (this.eventsSet.has(event.id)) continue
|
||||
|
||||
case 1:
|
||||
case 2:
|
||||
if (this.eventsSet.has(event.id)) return
|
||||
this.interpolateEventMentions(event)
|
||||
this.eventsSet.add(event.id)
|
||||
addToThread(threads, event)
|
||||
}
|
||||
},
|
||||
|
||||
this.interpolateEventMentions(event)
|
||||
this.eventsSet.add(event.id)
|
||||
addToThread(this.threads, event)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
'profile-browser'
|
||||
)
|
||||
useProfile(pubkey) {
|
||||
if (this.profilesUsed.has(pubkey)) return
|
||||
|
||||
this.profilesUsed.add(pubkey)
|
||||
this.$store.dispatch('useProfile', {pubkey})
|
||||
},
|
||||
|
||||
addEvent(event) {
|
||||
if (this.eventsSet.has(event.id)) return
|
||||
this.processUserNotes([event], this.threads)
|
||||
},
|
||||
|
||||
this.interpolateEventMentions(event)
|
||||
this.eventsSet.add(event.id)
|
||||
addToThread(this.threads, event)
|
||||
async loadMore() {
|
||||
this.loadingMore = true
|
||||
let until = this.threads.length ? this.threads[this.threads.length - 1][0].created_at : Math.round(Date.now() / 1000)
|
||||
let notes = await dbUserNotes(this.$route.params.pubkey, until, 50)
|
||||
if (notes.length < 50) this.reachedEnd = true
|
||||
let threads = []
|
||||
this.processUserNotes(notes, threads)
|
||||
this.threads = this.threads.concat(threads)
|
||||
this.loadingMore = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -2,15 +2,23 @@
|
||||
<q-page>
|
||||
<div class="text-h5 text-bold q-py-md">{{ $t('settings') }}</div>
|
||||
<q-separator color='accent' size='2px'/>
|
||||
<q-form class="q-gutter-md" @submit="setMetadata">
|
||||
<!-- <div class="text-lg p-4">Profile</div> -->
|
||||
<q-input v-model="metadata.name" filled type="text" label="Name">
|
||||
<q-form class="q-gutter-md q-pt-sm" @submit="setMetadata">
|
||||
<div v-if='editingMetadata' class='flex justify-between' style='display: flex; gap: .2rem;'>
|
||||
<q-btn label="save" color="primary" size="sm" type="submit"/>
|
||||
<q-btn label="cancel" color="negative" size="sm" @click='cancel("metadata")'/>
|
||||
</div>
|
||||
<div class="text-bold flex justify-between no-wrap" style='font-size: 1.1rem;'>
|
||||
{{ $t('profile') }}
|
||||
<q-btn v-if='!editingMetadata' label="edit" color="primary" size="sm" @click='editingMetadata = true'/>
|
||||
</div>
|
||||
<q-input v-model="metadata.name" filled type="text" label="Name" :disable='!editingMetadata'>
|
||||
<template #before>
|
||||
<q-icon name="alternate_email" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="metadata.about"
|
||||
:disable='!editingMetadata'
|
||||
filled
|
||||
autogrow
|
||||
type="text"
|
||||
@ -19,6 +27,7 @@
|
||||
/>
|
||||
<q-input
|
||||
v-model.trim="metadata.picture"
|
||||
:disable='!editingMetadata'
|
||||
filled
|
||||
type="text"
|
||||
label="Picture URL"
|
||||
@ -30,30 +39,38 @@
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model.trim="metadata.nip05"
|
||||
:disable='!editingMetadata'
|
||||
filled
|
||||
type="text"
|
||||
label="NIP-05 Identifier"
|
||||
maxlength="50"
|
||||
/>
|
||||
<q-btn label="Save" type="submit" color="primary" />
|
||||
</q-form>
|
||||
<q-separator color='accent' spaced/>
|
||||
<div class="my-8">
|
||||
<div>
|
||||
<div v-if='editingRelays' class='flex justify-between' style='display: flex; gap: .2rem;'>
|
||||
<q-btn label="save" color="primary" size="sm" @click='saveRelays'/>
|
||||
<q-btn label="cancel" color="negative" size="sm" @click='cancel("relays")'/>
|
||||
</div>
|
||||
<div class="text-bold flex justify-between no-wrap" style='font-size: 1.1rem;'>
|
||||
{{ $t('relays') }}
|
||||
<div class="text-normal flex row no-wrap" style='font-size: .9rem;'>
|
||||
<div style='width: 3.4em; text-align: center;'>read</div>
|
||||
<div style='width: 3.4em; text-align: center;'>write</div>
|
||||
<div class="text-normal flex row no-wrap" style='font-size: .9rem; gap: .4rem;'>
|
||||
<q-btn v-if='!editingRelays' label="edit" color="primary" size="sm" @click='editingRelays = true'/>
|
||||
<div v-if='editingRelays'>read</div>
|
||||
<div v-if='editingRelays'>write</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-list class="mb-3">
|
||||
<q-item v-for="([url]) in activeRelays" :key="url" class='flex justify-between items-center no-wrap no-padding'>
|
||||
<q-list class='flex column q-pt-xs' style='gap: .2rem;'>
|
||||
<q-item
|
||||
v-for="(url) in Object.keys(relays)"
|
||||
:key="url"
|
||||
class='flex justify-between items-center no-wrap no-padding'
|
||||
style='min-height: 1.2rem'
|
||||
>
|
||||
<div>
|
||||
{{ url }}
|
||||
</div>
|
||||
<div class="flex no-wrap items-center">
|
||||
<q-btn
|
||||
color="primary"
|
||||
v-if='relays[url].read || relays[url].write'
|
||||
color="secondary"
|
||||
size="sm"
|
||||
label="Share"
|
||||
:disable="
|
||||
@ -62,92 +79,42 @@
|
||||
"
|
||||
@click="shareRelay(url)"
|
||||
/>
|
||||
<q-toggle
|
||||
v-model='editedRelays[url].read'
|
||||
color='secondary'
|
||||
size='sm'
|
||||
class='no-padding'
|
||||
@click='toggleEditingRelays'
|
||||
/>
|
||||
<q-toggle
|
||||
v-model='editedRelays[url].write'
|
||||
color='secondary'
|
||||
size='sm'
|
||||
class='no-padding'
|
||||
@click='toggleEditingRelays'
|
||||
/>
|
||||
<!-- <span
|
||||
class="cursor-pointer tracking-wide"
|
||||
:class="{'font-bold': opts.read, 'text-secondary': opts.read}"
|
||||
@click="
|
||||
$store.getters.canSignEventsAutomatically
|
||||
? setRelayOpt(url, 'read', !opts.read)
|
||||
: null
|
||||
"
|
||||
>
|
||||
{{ $t('read') }}
|
||||
</span>
|
||||
<span
|
||||
class="cursor-pointer tracking-wide"
|
||||
:class="{'font-bold': opts.write, 'text-secondary': opts.write}"
|
||||
@click="
|
||||
$store.getters.canSignEventsAutomatically
|
||||
? setRelayOpt(url, 'write', !opts.write)
|
||||
: null
|
||||
"
|
||||
>
|
||||
{{ $t('write') }}
|
||||
</span> -->
|
||||
</div>
|
||||
</q-item>
|
||||
<q-item v-for="([url]) in inactiveRelays" :key="url" class='flex justify-between items-center no-wrap no-padding'>
|
||||
<div>
|
||||
{{ url }}
|
||||
</div>
|
||||
<div class="flex no-wrap items-center">
|
||||
<!-- <q-btn
|
||||
color="primary"
|
||||
size="sm"
|
||||
label="Share"
|
||||
:disable="
|
||||
hasJustSharedRelay ||
|
||||
!$store.getters.canSignEventsAutomatically
|
||||
"
|
||||
@click="shareRelay(url)"
|
||||
/> -->
|
||||
<q-btn
|
||||
v-if='editingRelays && !relays[url].read && !relays[url].write'
|
||||
color="negative"
|
||||
label='remove'
|
||||
size="sm"
|
||||
:disable="!$store.getters.canSignEventsAutomatically"
|
||||
@click="removeRelay(url)"
|
||||
/>
|
||||
{{ url }}
|
||||
</div>
|
||||
<div class="flex no-wrap items-center" style='gap: .6rem;'>
|
||||
<q-toggle
|
||||
v-model='editedRelays[url].read'
|
||||
color='secondary'
|
||||
v-if='editingRelays'
|
||||
v-model='relays[url].read'
|
||||
color='primary'
|
||||
size='sm'
|
||||
dense
|
||||
class='no-padding'
|
||||
@click='toggleEditingRelays'
|
||||
/>
|
||||
<q-toggle
|
||||
v-model='editedRelays[url].write'
|
||||
color='secondary'
|
||||
v-if='editingRelays'
|
||||
v-model='relays[url].write'
|
||||
color='primary'
|
||||
size='sm'
|
||||
dense
|
||||
class='no-padding'
|
||||
@click='toggleEditingRelays'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div>
|
||||
<q-btn label="save" color="primary" :disable='!editingRelays' @click='setRelayOpt'/>
|
||||
<q-btn label="reset" color="secondary" :disable='!editingRelays' @click='cloneRelays'/>
|
||||
</div>
|
||||
<q-form @submit="addRelay">
|
||||
<q-form v-if='editingRelays' class='q-py-xs' @submit="addRelay">
|
||||
<q-input
|
||||
v-model="addingRelay"
|
||||
class="mx-3"
|
||||
filled
|
||||
dense
|
||||
autofocus
|
||||
label="Add a relay"
|
||||
:disable="!$store.getters.canSignEventsAutomatically"
|
||||
>
|
||||
@ -156,41 +123,20 @@
|
||||
label="Add"
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="ml-3"
|
||||
size="sm"
|
||||
@click="addRelay"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
<!-- <div class="text-bold" style='font-size: 1.1rem;'>{{ $t('inactiveRelays') }}</div>
|
||||
<q-list class="mb-3">
|
||||
<q-item v-for="([url]) in inactiveRelays" :key="url">
|
||||
<q-item-section>
|
||||
<div class="flex justify-between">
|
||||
{{ url }}
|
||||
<q-btn
|
||||
color="negative"
|
||||
label='remove'
|
||||
size="sm"
|
||||
:disable="!$store.getters.canSignEventsAutomatically"
|
||||
@click="removeRelay(url)"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list> -->
|
||||
</div>
|
||||
|
||||
<q-separator color='accent' spaced/>
|
||||
|
||||
<div class="my-8">
|
||||
<div class="flex no-wrap" style='gap: .2rem;'>
|
||||
<q-btn label="Delete Local Data" color="negative" @click="hardReset" />
|
||||
<q-btn
|
||||
class="q-ml-md"
|
||||
label="View your keys"
|
||||
color="primary"
|
||||
@click="keysDialog = true"
|
||||
/>
|
||||
<q-btn label="View your keys" color="primary" @click="keysDialog = true" />
|
||||
<q-btn label="dev tools" color='secondary' :to='{ name: "devTools"}' />
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="keysDialog">
|
||||
@ -235,7 +181,7 @@ import {nextTick} from 'vue'
|
||||
import {queryName} from 'nostr-tools/nip05'
|
||||
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import {eraseDatabase} from '../db'
|
||||
import {dbErase} from '../query'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
@ -247,16 +193,16 @@ export default {
|
||||
|
||||
return {
|
||||
keysDialog: false,
|
||||
relays: {},
|
||||
editedRelays: {},
|
||||
editingRelays: false,
|
||||
addingRelay: '',
|
||||
editingMetadata: false,
|
||||
metadata: {
|
||||
name,
|
||||
picture,
|
||||
about,
|
||||
nip05
|
||||
},
|
||||
relays: {},
|
||||
editingRelays: false,
|
||||
addingRelay: '',
|
||||
unsubscribe: null,
|
||||
hasJustSharedRelay: false
|
||||
}
|
||||
@ -265,35 +211,7 @@ export default {
|
||||
watch: {
|
||||
'$store.state.relays'(curr, prev) {
|
||||
if (curr !== prev) this.cloneRelays()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
storeRelays() {
|
||||
// if (Object.keys(this.$store.state.relays).length) return this.$store.state.relays
|
||||
// return {}
|
||||
return this.$store.state.relays || {}
|
||||
},
|
||||
activeRelays() {
|
||||
return Object.entries(this.relays).filter(([url, opts]) => opts.read === true || opts.write === true)
|
||||
// return Object.entries(this.relays).filter(([url, opts]) => opts.read === true || opts.write === true)
|
||||
},
|
||||
inactiveRelays() {
|
||||
return Object.entries(this.relays).filter(([url, opts]) => opts.read === false && opts.write === false)
|
||||
// return Object.entries(this.relays).filter(([url, opts]) => opts.read === false && opts.write === false)
|
||||
},
|
||||
activeRelaysCopy() {
|
||||
return Object.entries(this.storeRelays).filter(([url, opts]) => opts.read === true || opts.write === true)
|
||||
},
|
||||
inactiveRelaysCopy() {
|
||||
return Object.entries(this.storeRelays).filter(([url, opts]) => opts.read === false && opts.write === false)
|
||||
},
|
||||
// editingRelays() {
|
||||
// if (this.activeRelays.filter(([url, opts]) =>
|
||||
// this.editedRelays[url].read !== opts.read ||
|
||||
// this.editedRelays[url].write !== opts.write).length) return true
|
||||
// return false
|
||||
// }
|
||||
},
|
||||
|
||||
mounted() {
|
||||
@ -330,7 +248,6 @@ export default {
|
||||
}
|
||||
})
|
||||
this.cloneRelays()
|
||||
console.log(this.relays)
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
@ -338,15 +255,13 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
cloneMetadata() {
|
||||
let {name, picture, about, nip05} = this.$store.state.profilesCache[this.$store.state.keys.pub]
|
||||
this.metadata = {name, picture, about, nip05}
|
||||
console.log('cloneMeta', this.metadata)
|
||||
},
|
||||
cloneRelays() {
|
||||
this.relays = JSON.parse(JSON.stringify(this.$store.state.relays))
|
||||
this.editedRelays = JSON.parse(JSON.stringify(this.$store.state.relays))
|
||||
},
|
||||
toggleEditingRelays(value, evt) {
|
||||
if (Object.entries(this.editedRelays).filter(([url, opts]) =>
|
||||
this.relays[url].read !== opts.read ||
|
||||
this.relays[url].write !== opts.write).length) this.editingRelays = true
|
||||
else this.editingRelays = false
|
||||
},
|
||||
async setMetadata() {
|
||||
if (this.metadata.nip05 === '') this.metadata.nip05 = undefined
|
||||
@ -368,7 +283,7 @@ export default {
|
||||
this.$store.dispatch('setMetadata', this.metadata)
|
||||
},
|
||||
addRelay() {
|
||||
this.$store.commit('addRelay', this.addingRelay)
|
||||
this.relays[this.addingRelay] = { read: true, write: true }
|
||||
this.addingRelay = ''
|
||||
},
|
||||
removeRelay(url) {
|
||||
@ -379,23 +294,27 @@ export default {
|
||||
cancel: true
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$store.commit('removeRelay', url)
|
||||
delete this.relays[url]
|
||||
})
|
||||
},
|
||||
setRelayOpt() {
|
||||
console.log('setRelayOpt')
|
||||
if (this.$store.getters.canSignEventsAutomatically) Object.entries(this.editedRelays)
|
||||
.forEach(([url, opts]) => {
|
||||
if (this.relays[url].read !== opts.read) this.$store.commit('setRelayOpt', {url, opt: 'read', value: opts.read})
|
||||
if (this.relays[url].write !== opts.write) this.$store.commit('setRelayOpt', {url, opt: 'write', value: opts.write})
|
||||
})
|
||||
saveRelays() {
|
||||
if (this.$store.getters.canSignEventsAutomatically) this.$store.commit('saveRelays', this.relays)
|
||||
},
|
||||
cancel(section) {
|
||||
if (section === 'metadata') {
|
||||
this.editingMetadata = false
|
||||
this.cloneMetadata()
|
||||
return
|
||||
}
|
||||
if (section === 'relays') {
|
||||
this.editingRelays = false
|
||||
this.cloneRelays()
|
||||
return
|
||||
}
|
||||
},
|
||||
// setRelayOpt(url, opt, value) {
|
||||
// this.$store.commit('setRelayOpt', {url, opt, value})
|
||||
// },
|
||||
shareRelay(url) {
|
||||
this.hasJustSharedRelay = true
|
||||
this.$store.dispatch('recommendServer', url)
|
||||
this.$store.dispatch('recommendRelay', url)
|
||||
setTimeout(() => {
|
||||
this.hasJustSharedRelay = false
|
||||
}, 5000)
|
||||
@ -409,7 +328,8 @@ export default {
|
||||
})
|
||||
.onOk(async () => {
|
||||
LocalStorage.clear()
|
||||
await eraseDatabase()
|
||||
// await eraseDatabase()
|
||||
await dbErase()
|
||||
window.location.reload()
|
||||
})
|
||||
},
|
||||
|
192
src/pages/devTools.vue
Normal file
192
src/pages/devTools.vue
Normal file
@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<q-page>
|
||||
<div class="text-h5 text-bold q-py-md">dev tools</div>
|
||||
<q-separator color='accent' size='2px'/>
|
||||
<div class="text-bold q-py-md">sql query</div>
|
||||
|
||||
<!-- <h2> sql query </h2> -->
|
||||
<!-- <label for='commands'>Enter some SQL</label> -->
|
||||
<!-- <br> -->
|
||||
|
||||
<textarea v-model='sql' id="editor"/>
|
||||
<div style='display: flex; flex-direction: row; gap: 1rem; padding: .5rem 0; align-items: center'>
|
||||
<q-btn id="execute" color='primary' @click='execEditorContents'>Execute</q-btn>
|
||||
<!-- <button id='savedb' class="btn btn-secondary btn-sm">Save the db</button> -->
|
||||
<!-- <label class="button">Load an SQLite database file: <input type='file' id='dbfile' /></label> -->
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
<q-table
|
||||
v-if='rows.length'
|
||||
:rows='rows'
|
||||
dense
|
||||
wrap-cells
|
||||
:rows-per-page-options='[10, 50, 100, 0]'
|
||||
/>
|
||||
<pre id="output">Results will be displayed here</pre>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import helpersMixin from '../utils/mixin'
|
||||
import {dbQuery} from '../query'
|
||||
import CodeMirror from 'codemirror/lib/codemirror.js'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/mode/sql/sql.js'
|
||||
import 'codemirror/theme/dracula.css'
|
||||
// import CodeMirror from 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.58.1/codemirror.js'
|
||||
// import(/* webpackIgnore: true */ 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.58.1/codemirror.js')
|
||||
|
||||
|
||||
// import BaseUserCard from 'components/BaseUserCard.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DevTools',
|
||||
mixins: [helpersMixin],
|
||||
|
||||
components: {
|
||||
// BaseUserCard,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
codeEditor: null,
|
||||
tictime: null,
|
||||
sql: 'SELECT * FROM nostr_events;\n-- SELECT * FROM nostr_users;',
|
||||
// sql: 'SELECT * FROM nostr;',
|
||||
// results: [],
|
||||
rows: [],
|
||||
// rowKey: 'id',
|
||||
// columns: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
editor() {
|
||||
return document.getElementById('editor')
|
||||
},
|
||||
commands() {
|
||||
return document.getElementById('commands')
|
||||
},
|
||||
executeButton() {
|
||||
return document.getElementById('execute')
|
||||
},
|
||||
output() {
|
||||
return document.getElementById('output')
|
||||
},
|
||||
error() {
|
||||
return document.getElementById('error')
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.codeEditor = CodeMirror.fromTextArea(this.editor, {
|
||||
mode: 'text/x-sql',
|
||||
theme: 'dracula',
|
||||
viewportMargin: Infinity,
|
||||
indentWithTabs: true,
|
||||
smartIndent: true,
|
||||
lineNumbers: true,
|
||||
matchBrackets: true,
|
||||
autofocus: true,
|
||||
extraKeys: {
|
||||
'Ctrl-Enter': this.execEditorContents,
|
||||
// "Ctrl-S": savedb,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
activated() {
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Execute the commands when the button is clicked
|
||||
execEditorContents() {
|
||||
console.log('sql', this.codeEditor.getValue())
|
||||
this.noerror()
|
||||
this.rows = []
|
||||
this.execute(this.codeEditor.getValue() + ';')
|
||||
},
|
||||
|
||||
// Run a command in the database
|
||||
async execute(sql) {
|
||||
this.tic()
|
||||
this.output.textContent = 'Fetching results...'
|
||||
let results
|
||||
results = await dbQuery(sql)
|
||||
// try {
|
||||
// results = await dbQuery(sql)
|
||||
// } catch (e) {
|
||||
// this.displayError(e)
|
||||
// }
|
||||
this.toc('Executing SQL')
|
||||
|
||||
this.tic()
|
||||
this.output.innerHTML = ''
|
||||
if (!results || !results.length) {
|
||||
this.print('0 rows returned')
|
||||
return
|
||||
}
|
||||
this.rows = results
|
||||
// this.output.appendChild(this.createTable(results))
|
||||
// console.log(results)
|
||||
this.toc('Displaying results')
|
||||
},
|
||||
|
||||
// Connect to the HTML element we 'print' to
|
||||
print(text) {
|
||||
this.output.innerHTML = text.replace(/\n/g, '<br>')
|
||||
},
|
||||
displayError(e) {
|
||||
console.log(e)
|
||||
this.error.style.height = '2em'
|
||||
this.error.textContent = e.message
|
||||
},
|
||||
|
||||
noerror() {
|
||||
this.error.style.height = '0'
|
||||
},
|
||||
|
||||
// concatTableValues(vals, tagName) {
|
||||
// if (vals.length === 0) return ''
|
||||
// var open = '<' + tagName + '>', close = '</' + tagName + '>'
|
||||
// return open + vals.join(close + open) + close
|
||||
// },
|
||||
// Create an HTML table
|
||||
// createTable(data) {
|
||||
// if (data.length === 0) return
|
||||
// console.log(data)
|
||||
// let columns = Object.keys(data[0])
|
||||
// // this.rowKey = columns[0]
|
||||
// // this.columns = columns
|
||||
// let values = data.map(row => Object.values(row))
|
||||
// console.log('columns:', columns, 'values:', values)
|
||||
// var tbl = document.createElement('table')
|
||||
// var html = '<thead>' + this.concatTableValues(columns, 'th') + '</thead>'
|
||||
// var rows = values.map(v => this.concatTableValues(v, 'td'))
|
||||
// // this.rows = values
|
||||
// html += '<tbody>' + this.concatTableValues(rows, 'tr') + '</tbody>'
|
||||
// tbl.innerHTML = html
|
||||
// return tbl
|
||||
// },
|
||||
|
||||
// Performance measurement functions
|
||||
tic() { this.tictime = Date.now() },
|
||||
|
||||
toc(msg) {
|
||||
let took = Date.now() - this.tictime
|
||||
console.log((msg || 'toc') + ': ' + took + 'ms')
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.q-tabs {
|
||||
border-bottom: 1px solid $accent
|
||||
}
|
||||
</style>
|
19
src/pool.js
19
src/pool.js
@ -1,9 +1,22 @@
|
||||
import {relayPool} from 'nostr-tools'
|
||||
// import {Dialog} from 'quasar'
|
||||
import {Dialog} from 'quasar'
|
||||
// import {pool} from './relay.worker'
|
||||
import * as relayWorker from './relay.worker'
|
||||
// import {relayPool} from 'nostr-tools'
|
||||
|
||||
export const pool = relayPool()
|
||||
// export const pool = relayPool()
|
||||
|
||||
pool.setPolicy('randomChoice', 3)
|
||||
// relayWorker.pool.setPolicy('randomChoice', 3)
|
||||
// relayWorker.pool.onNotice((notice, relay) => {
|
||||
// Notify.create({
|
||||
// message: `Relay ${relay.url} says: ${notice}`,
|
||||
// color: 'info'
|
||||
// })
|
||||
// })
|
||||
|
||||
// const tempPool = pool
|
||||
// delete pool
|
||||
export const pool = relayWorker.pool
|
||||
|
||||
// this will try to sign either with window.nostr or using a manual prompt
|
||||
export async function signAsynchronously(event) {
|
||||
|
254
src/query.js
Normal file
254
src/query.js
Normal file
@ -0,0 +1,254 @@
|
||||
import {Notify} from 'quasar'
|
||||
import {initBackend} from 'absurd-sql/dist/indexeddb-main-thread'
|
||||
// import { channel } from './relay'
|
||||
const worker = new Worker(new URL('./query.worker.js', import.meta.url))
|
||||
initBackend(worker)
|
||||
// worker.postMessage({ name: 'setPort' }, [ channel.port2 ])
|
||||
const hub = {}
|
||||
// initializeDatabase()
|
||||
|
||||
|
||||
worker.onmessage = ev => {
|
||||
// let { id, success, error, data, stream, type } = JSON.parse(ev.data)
|
||||
// let { id, success, error, data, stream, type } = ev.data
|
||||
let { id, success, error, data, stream, type, notice } = typeof ev.data === 'string' ? JSON.parse(ev.data) : ev.data
|
||||
|
||||
if (type) {
|
||||
// console.debug(ev.data)
|
||||
return
|
||||
}
|
||||
|
||||
if (notice) {
|
||||
Notify.create({
|
||||
message: `Relay ${notice.relay.url} says: ${notice.message}`,
|
||||
color: 'info'
|
||||
})
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
// console.debug('🖴', id, '~>>', data)
|
||||
if (hub[id]) hub[id](data)
|
||||
return
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
hub[id].reject(new Error(error))
|
||||
delete hub[id]
|
||||
return
|
||||
}
|
||||
|
||||
// if (data) console.debug('🖴', id, '->', data)
|
||||
// console.log('🖴', id, '->', data)
|
||||
hub[id]?.resolve?.(data)
|
||||
delete hub[id]
|
||||
}
|
||||
|
||||
function call(name, args) {
|
||||
let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
// console.debug('🖴', id, '<-', args)
|
||||
// console.log('🖴', id, '<-', args)
|
||||
worker.postMessage({ id, name, args })
|
||||
// worker.postMessage(JSON.stringify({ id, name, args }))
|
||||
return new Promise((resolve, reject) => {
|
||||
hub[id] = { resolve, reject }
|
||||
})
|
||||
}
|
||||
|
||||
function stream(name, args, callback) {
|
||||
let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
hub[id] = callback
|
||||
// console.debug('db <-', id, args)
|
||||
worker.postMessage(JSON.stringify({ id, name, args, stream: true }))
|
||||
return {
|
||||
update(...args) {
|
||||
worker.postMessage(JSON.stringify({ id, name, args, stream: true }))
|
||||
},
|
||||
cancel() {
|
||||
worker.postMessage(JSON.stringify({ id, cancel: true }))
|
||||
delete hub[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// function sub(name, args) {
|
||||
// let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
// // hub[id] = callback
|
||||
// console.debug('relay sub', id, args)
|
||||
// worker.postMessage(JSON.stringify({ id, name, args, sub: true }))
|
||||
// return {
|
||||
// update(...args) {
|
||||
// worker.postMessage(JSON.stringify({ id, name, args, sub: true }))
|
||||
// },
|
||||
// cancel() {
|
||||
// worker.postMessage(JSON.stringify({ id, cancel: true }))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// async function initializeDatabase() {
|
||||
// return call('initializeDatabase', [])
|
||||
// }
|
||||
export async function destroyStreams() {
|
||||
return call('destroyStreams', [])
|
||||
}
|
||||
|
||||
export async function dbErase() {
|
||||
return call('dbErase', [])
|
||||
}
|
||||
|
||||
export async function dbSave(event) {
|
||||
return call('dbSave', [event])
|
||||
}
|
||||
|
||||
export function dbStreamFeed(
|
||||
since = Math.round(Date.now() / 1000),
|
||||
callback = () => { }
|
||||
) {
|
||||
return stream('dbStreamFeed', [since], callback)
|
||||
}
|
||||
|
||||
export async function dbChats(pubkey) {
|
||||
return call('dbChats', [pubkey])
|
||||
}
|
||||
|
||||
export async function dbMessages(userPubkey, peerPubkey, limit = 50, until = Math.round(Date.now() / 1000)) {
|
||||
return call('dbMessages', [userPubkey, peerPubkey, limit, until])
|
||||
}
|
||||
|
||||
export async function streamUserMessages(pubkey, callback = () => { }) {
|
||||
return stream('streamUserMessages', [pubkey], callback)
|
||||
}
|
||||
|
||||
export async function streamMessages(callback = () => { }) {
|
||||
return stream('streamMessages', [], callback)
|
||||
}
|
||||
|
||||
export async function dbEvent(id) {
|
||||
return call('dbEvent', [id])
|
||||
}
|
||||
|
||||
export async function dbStreamTagKind(type, value, kind, callback = () => { }) {
|
||||
return stream('dbStreamTagKind', [type, value, kind], callback)
|
||||
}
|
||||
|
||||
// note abnormal behavior for dbStreamEvent
|
||||
// for querying with one event id:
|
||||
// -by default will not create sub if event found in db
|
||||
// -extra 'updates' variable will create sub and push seen_on updates
|
||||
// -still need to manually cancel stream
|
||||
// for querying with multiple event ids (normal behavior):
|
||||
// -stream kept open for all events
|
||||
// -no seen_on updates are pushed (even with updates = true)
|
||||
export async function dbStreamEvent(id, callback = () => { }, updates = false) {
|
||||
return stream('dbStreamEvent', [id, updates], callback)
|
||||
}
|
||||
|
||||
export async function dbMentions(pubkey, limit = 50, until = Math.round(Date.now() / 1000)) {
|
||||
return call('dbMentions', [pubkey, limit, until])
|
||||
}
|
||||
|
||||
export function streamMentions(pubkey, callback = () => { }) {
|
||||
return stream('streamMentions', [pubkey], callback)
|
||||
}
|
||||
|
||||
export async function dbUnreadMentionsCount(pubkey, since = Math.round(Date.now() / 1000)) {
|
||||
return call('dbUnreadMentionsCount', [pubkey, since])
|
||||
}
|
||||
|
||||
export async function dbUnreadMessagesCount(userPubkey, peerPubkey, since = Math.round(Date.now() / 1000)) {
|
||||
return call('dbUnreadMessagesCount', [userPubkey, peerPubkey, since])
|
||||
}
|
||||
|
||||
export async function dbUserProfile(pubkey) {
|
||||
return call('dbUserProfile', [pubkey])
|
||||
}
|
||||
|
||||
export async function dbUserFollows(pubkey) {
|
||||
return call('dbUserFollows', [pubkey])
|
||||
}
|
||||
|
||||
export async function dbUserNotes(pubkey, until = Math.round(Date.now() / 1000), limit = 50) {
|
||||
return call('dbUserNotes', [pubkey, until, limit])
|
||||
}
|
||||
|
||||
export function streamUser(pubkey, callback = () => { }) {
|
||||
return stream('streamUser', [pubkey], callback)
|
||||
}
|
||||
|
||||
export function streamUserNotes(pubkey, callback = () => { }) {
|
||||
return stream('streamUserNotes', [pubkey], callback)
|
||||
}
|
||||
|
||||
export function streamUserProfile(pubkey, callback = () => { }) {
|
||||
return stream('streamUserProfile', [pubkey], callback)
|
||||
}
|
||||
|
||||
export function dbStreamUserProfile(pubkey, callback = () => { }) {
|
||||
return stream('dbStreamUserProfile', [pubkey], callback)
|
||||
}
|
||||
|
||||
export function streamUserFollows(pubkey, callback = () => { }) {
|
||||
return stream('dbStreamUserFollows', [pubkey], callback)
|
||||
}
|
||||
|
||||
export function dbStreamUserFollows(pubkey, callback = () => { }) {
|
||||
return stream('dbStreamUserFollows', [pubkey], callback)
|
||||
}
|
||||
|
||||
export function dbStreamUserFollowers(pubkey, callback = () => { }) {
|
||||
return stream('dbStreamUserFollowers', [pubkey], callback)
|
||||
}
|
||||
|
||||
export function streamTag(type, value, callback = () => { }) {
|
||||
return stream('streamTag', [type, value], callback)
|
||||
}
|
||||
|
||||
// export async function dbGetRelayForPubKey(pubkey) {
|
||||
// return call('dbGetRelayForPubKey', [pubkey])
|
||||
// }
|
||||
|
||||
export async function prune(user, pubkeys) {
|
||||
return call('prune', [user, pubkeys])
|
||||
}
|
||||
|
||||
export async function dbQuery(sql) {
|
||||
return call('dbQuery', [sql])
|
||||
}
|
||||
|
||||
export function setRelays(relays) {
|
||||
return call('setRelays', [JSON.parse(JSON.stringify(relays))])
|
||||
}
|
||||
|
||||
export function publish(event, relayURL) {
|
||||
return call('publish', [JSON.parse(JSON.stringify(event)), relayURL])
|
||||
}
|
||||
|
||||
|
||||
// export function relaySubUser(pubkey) {
|
||||
// return sub('relaySubUser', [pubkey])
|
||||
// }
|
||||
|
||||
// export function relaySubUserNotes(pubkey) {
|
||||
// return sub('relaySubUserNotes', [pubkey])
|
||||
// }
|
||||
|
||||
// export function relaySubUserInfo(pubkey) {
|
||||
// return sub('relaySubUserInfo', [pubkey])
|
||||
// }
|
||||
|
||||
// export function relaySubTag(type, value) {
|
||||
// return sub('relaySubTag', [type, value])
|
||||
// }
|
||||
|
||||
// export function relaySubFeed(since) {
|
||||
// return sub('relaySubFeed', [since])
|
||||
// }
|
||||
|
||||
// export function relaySubEvent(id) {
|
||||
// return sub('relaySubEvent', [id])
|
||||
// }
|
||||
|
||||
// export function relayUnsub() {
|
||||
// return call('relayUnsub', [])
|
||||
// }
|
||||
|
1012
src/query.worker.js
Normal file
1012
src/query.worker.js
Normal file
File diff suppressed because it is too large
Load Diff
149
src/relay.js
Normal file
149
src/relay.js
Normal file
@ -0,0 +1,149 @@
|
||||
const worker = new Worker(new URL('./relay.worker.js', import.meta.url))
|
||||
|
||||
const hub = {}
|
||||
|
||||
worker.onmessage = ev => {
|
||||
let { id, success, error, data } = typeof ev.data === 'string' ? JSON.parse(ev.data) : ev.data
|
||||
|
||||
if (!success) {
|
||||
hub[id].reject(new Error(id + ':' + error))
|
||||
delete hub[id]
|
||||
return
|
||||
}
|
||||
|
||||
// if (data) console.debug('🖴', id, '->', data)
|
||||
hub[id]?.resolve?.(data)
|
||||
delete hub[id]
|
||||
}
|
||||
|
||||
function call(name, args) {
|
||||
let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
// console.debug('🖴', id, '<-', args)
|
||||
worker.postMessage(JSON.stringify({ id, name, args }))
|
||||
return new Promise((resolve, reject) => {
|
||||
hub[id] = { resolve, reject }
|
||||
})
|
||||
}
|
||||
|
||||
// function stream(name, args, callback) {
|
||||
// let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
// hub[id] = callback
|
||||
// console.debug('db <-', id, args)
|
||||
// worker.postMessage(JSON.stringify({ id, name, args, stream: true }))
|
||||
// return {
|
||||
// cancel() {
|
||||
// worker.postMessage(JSON.stringify({ id, cancel: true }))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
function sub(name, args) {
|
||||
let id = name + ' ' + Math.random().toString().slice(-4)
|
||||
// hub[id] = callback
|
||||
console.debug('sub', id, args)
|
||||
worker.postMessage(JSON.stringify({ id, name, args, sub: true }))
|
||||
return {
|
||||
update(...args) {
|
||||
worker.postMessage(JSON.stringify({ id, name, args, sub: true }))
|
||||
},
|
||||
cancel() {
|
||||
worker.postMessage(JSON.stringify({ id, sub: true, cancel: true }))
|
||||
delete hub[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function subUser(pubkey) {
|
||||
return sub('subUser', [pubkey])
|
||||
}
|
||||
|
||||
export function subUserNotes(pubkey) {
|
||||
return sub('subUserNotes', [pubkey])
|
||||
}
|
||||
|
||||
export function subUserProfile(pubkey) {
|
||||
return sub('subUserProfile', [pubkey])
|
||||
}
|
||||
|
||||
export function subUserFollows(pubkey) {
|
||||
return sub('subUserFollows', [pubkey])
|
||||
}
|
||||
|
||||
export function subUserFollowers(pubkey) {
|
||||
return sub('subUserFollowers', [pubkey])
|
||||
}
|
||||
|
||||
export function subUserMessages(pubkey) {
|
||||
return sub('subUserMessages', [pubkey])
|
||||
}
|
||||
|
||||
export function subTag(type, value) {
|
||||
return sub('subTag', [type, value])
|
||||
}
|
||||
|
||||
export function subFeed(since) {
|
||||
return sub('subFeed', [since])
|
||||
}
|
||||
|
||||
export function subEvent(id) {
|
||||
return sub('subEvent', [id])
|
||||
}
|
||||
|
||||
export function unsub() {
|
||||
return call('unsub', [])
|
||||
}
|
||||
|
||||
export function close() {
|
||||
return call('close', [])
|
||||
}
|
||||
|
||||
export function setRelays(relays) {
|
||||
return call('setRelays', [relays])
|
||||
}
|
||||
|
||||
export function setPort(channel) {
|
||||
worker.postMessage({ name: 'setPort' }, [ channel.port2 ])
|
||||
}
|
||||
|
||||
export function publish(event, relayURL) {
|
||||
return call('publish', [event, relayURL])
|
||||
}
|
||||
|
||||
|
||||
// // this will try to sign either with window.nostr or using a manual prompt
|
||||
// export async function signAsynchronously(event) {
|
||||
// if (window.nostr) {
|
||||
// let signatureOrEvent = await window.nostr.signEvent(event)
|
||||
// switch (typeof signatureOrEvent) {
|
||||
// case 'string':
|
||||
// return signatureOrEvent
|
||||
// case 'object':
|
||||
// return signatureOrEvent.sig
|
||||
// default:
|
||||
// throw new Error('Failed to sign with Nostr extension.')
|
||||
// }
|
||||
// } else {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// Dialog.create({
|
||||
// class: 'px-6 py-1 overflow-hidden',
|
||||
// title: 'Sign this event manually',
|
||||
// message: `<pre class="font-mono">${JSON.stringify(
|
||||
// event,
|
||||
// null,
|
||||
// 2
|
||||
// )}</pre>`,
|
||||
// html: true,
|
||||
// prompt: {
|
||||
// model: '',
|
||||
// type: 'text',
|
||||
// isValid: val => !!val.toLowerCase().match(/^[a-z0-9]{128}$/),
|
||||
// attrs: {autocomplete: 'off'},
|
||||
// label: 'Paste the signature here (as hex)'
|
||||
// }
|
||||
// })
|
||||
// .onOk(resolve)
|
||||
// .onCancel(() => reject('Canceled.'))
|
||||
// .onDismiss(() => reject('Canceled.'))
|
||||
// })
|
||||
// }
|
||||
// }
|
372
src/relay.worker.js
Normal file
372
src/relay.worker.js
Normal file
@ -0,0 +1,372 @@
|
||||
import mergebounce from 'mergebounce'
|
||||
// import {pool} from './pool'
|
||||
// import {pool} from './relay'
|
||||
import { relayPool } from 'nostr-tools'
|
||||
// import { relayPool } from './test.js'
|
||||
|
||||
export const pool = relayPool()
|
||||
let poolSub = null
|
||||
|
||||
pool.onNotice((notice, relay) => {
|
||||
dbWorkerPort.postMessage({type: 'notice', notice, relay})
|
||||
})
|
||||
|
||||
let relays = {}
|
||||
let subs = {}
|
||||
|
||||
let dbWorkerPort = null
|
||||
|
||||
let debounceCount = 0
|
||||
let debouncedEmitEvent = mergebounce(
|
||||
events => dbWorkerPort.postMessage({ type: 'events', events }),
|
||||
300,
|
||||
{ 'concatArrays': true, 'promise': true, maxWait: 3000 }
|
||||
)
|
||||
|
||||
function onEvent(event, relay) {
|
||||
// postMessage(`[RELAY WORKER] Web worker got this event from ${relay}: ${JSON.stringify(event, null, 2)}`)
|
||||
// dbWorkerPort.postMessage({ type: 'event', event, relay })
|
||||
if (debounceCount >= 2000) {
|
||||
debouncedEmitEvent.flush()
|
||||
debounceCount = 0
|
||||
// console.log('flushing mergebounce')
|
||||
}
|
||||
debouncedEmitEvent([{ event, relay }])
|
||||
debounceCount++
|
||||
// if (![
|
||||
// 'wss://rsslay.fiatjaf.com',
|
||||
// 'wss://nostr-pub.wellorder.net',
|
||||
// 'wss://expensive-relay.fiatjaf.com'
|
||||
// ].includes(relay)) console.log('onEvent', event.kind, { event, relay })
|
||||
// if ([0, 3].includes(event.kind)) console.log('onEvent', event.kind, { event, relay })
|
||||
// postMessage({ type: 'event', event, relay })
|
||||
}
|
||||
|
||||
function calcFilter() {
|
||||
let compiledSubs = Object.entries(subs)//.filter(([id, value]) => type === 'ids')
|
||||
.map(([_, sub]) => sub)
|
||||
.reduce((acc, { type, value }) => {
|
||||
if (type === 'user') {
|
||||
acc[type] = [value]
|
||||
return acc
|
||||
// } else if (type === 'user_tagged') {
|
||||
// acc[type] = value
|
||||
// return acc
|
||||
} else if (type === 'feed') {
|
||||
acc[type] = value
|
||||
return acc
|
||||
} else if (type === 'tag') {
|
||||
let tagType = value.tagType
|
||||
let tagValues = value.tagValues
|
||||
acc[`#${tagType}`] = (acc[`#${tagType}`] || []).concat(tagValues)
|
||||
return acc
|
||||
}
|
||||
acc[type] = (acc[type] || []).concat(value)
|
||||
return acc
|
||||
}, {})
|
||||
let filter = Object.entries(compiledSubs)
|
||||
.map(([type, value]) => {
|
||||
switch (type) {
|
||||
case 'user':
|
||||
return {
|
||||
authors: value,
|
||||
kinds: [0, 1, 2, 3, 4]
|
||||
}
|
||||
case 'userNotes':
|
||||
return {
|
||||
authors: value,
|
||||
kinds: [1]
|
||||
}
|
||||
case 'userProfile':
|
||||
return {
|
||||
authors: value,
|
||||
kinds: [0]
|
||||
}
|
||||
case 'userFollows':
|
||||
return {
|
||||
authors: value,
|
||||
kinds: [3]
|
||||
}
|
||||
case 'userFollowers':
|
||||
return {
|
||||
'#p': value,
|
||||
kinds: [3]
|
||||
}
|
||||
case 'userMessages':
|
||||
return {
|
||||
authors: value,
|
||||
kinds: [4]
|
||||
}
|
||||
case 'feed':
|
||||
return {
|
||||
since: value
|
||||
}
|
||||
case 'event':
|
||||
return {
|
||||
ids: value
|
||||
}
|
||||
default:
|
||||
return {
|
||||
[type]: value
|
||||
}
|
||||
}
|
||||
})
|
||||
// relayWorker.postMessage({ type: 'setFilter', filter })
|
||||
// console.log('relaysSet', relaysSet, filter)
|
||||
// if (relaysSet) {
|
||||
// console.log('setFilter', filter)
|
||||
// poolSub = poolSub.sub({ cb: onEvent, filter })
|
||||
return filter
|
||||
// if (!poolSub) poolSub = pool.sub({ cb: onEvent, filter })
|
||||
// else poolSub.sub({ filter })
|
||||
// poolSub.main.sub({ filter })
|
||||
// }
|
||||
}
|
||||
|
||||
function cancelSub(id) {
|
||||
delete subs[id]
|
||||
if (poolSub) {
|
||||
if (Object.keys(subs).length === 0) poolSub.unsub()
|
||||
else poolSub.sub({ cb: onEvent, filter: calcFilter()})
|
||||
}
|
||||
}
|
||||
|
||||
const methods = {
|
||||
close() {
|
||||
self.close()
|
||||
return
|
||||
},
|
||||
|
||||
unsub() {
|
||||
// relayWorker.postMessage({ type: 'unsub' })
|
||||
// pool.unsub()
|
||||
// subs = {}
|
||||
return
|
||||
},
|
||||
|
||||
subUser(pubkey) {
|
||||
return {
|
||||
type: 'user',
|
||||
value: pubkey
|
||||
}
|
||||
},
|
||||
|
||||
subUserNotes(pubkey) {
|
||||
return {
|
||||
type: 'userNotes',
|
||||
value: pubkey
|
||||
}
|
||||
},
|
||||
|
||||
subUserProfile(pubkey) {
|
||||
let pubkeys = Array.isArray(pubkey) ? pubkey : [pubkey]
|
||||
return {
|
||||
type: 'userProfile',
|
||||
value: pubkeys
|
||||
}
|
||||
},
|
||||
|
||||
subUserFollows(pubkey) {
|
||||
let pubkeys = Array.isArray(pubkey) ? pubkey : [pubkey]
|
||||
return {
|
||||
type: 'userFollows',
|
||||
value: pubkeys
|
||||
}
|
||||
},
|
||||
|
||||
subUserFollowers(pubkey) {
|
||||
let pubkeys = Array.isArray(pubkey) ? pubkey : [pubkey]
|
||||
return {
|
||||
type: 'userFollowers',
|
||||
value: pubkeys
|
||||
}
|
||||
},
|
||||
|
||||
subUserMessages(pubkey) {
|
||||
let pubkeys = Array.isArray(pubkey) ? pubkey : [pubkey]
|
||||
return {
|
||||
type: 'userMessages',
|
||||
value: pubkeys
|
||||
}
|
||||
},
|
||||
|
||||
subTag(type, value) {
|
||||
let values = Array.isArray(value) ? value : [value]
|
||||
return {
|
||||
type: 'tag',
|
||||
value: {
|
||||
tagType: type,
|
||||
tagValues: values
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
subFeed(since) {
|
||||
return {
|
||||
type: 'feed',
|
||||
value: since
|
||||
}
|
||||
},
|
||||
|
||||
subEvent(id) {
|
||||
let ids = Array.isArray(id) ? id : [id]
|
||||
return {
|
||||
type: 'event',
|
||||
value: ids
|
||||
}
|
||||
},
|
||||
|
||||
setRelays(newRelays) {
|
||||
for (let url in newRelays) {
|
||||
if (!relays[url]) pool.addRelay(url, newRelays[url])
|
||||
else if (relays[url].read !== newRelays[url].read || relays[url].write !== newRelays[url].write) {
|
||||
pool.removeRelay(url)
|
||||
pool.addRelay(url, newRelays[url])
|
||||
}
|
||||
}
|
||||
for (let url in relays) {
|
||||
if (!newRelays[url]) pool.removeRelay(url)
|
||||
if (!newRelays[url]) console.log('removing relay', url)
|
||||
}
|
||||
// relaysSet = true
|
||||
// if (Object.keys(subs).length) calcFilter()
|
||||
// console.log('queue', queue)
|
||||
// if (queue.length) {
|
||||
// queue.forEach(ev => handleMessage(ev))
|
||||
// queue = null
|
||||
// }
|
||||
// await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
// poolSub = poolSub.sub({ cb: onEvent, filter: [{
|
||||
// // return pool.sub({ cb: onEvent, filter: [{
|
||||
// authors: ['8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168'],
|
||||
// kinds: [0]
|
||||
// }] })
|
||||
// if (Object.keys(subs).length) calcFilter()
|
||||
relays = newRelays
|
||||
// console.log('subs', subs, 'poolSub', poolSub)
|
||||
// if (!poolSub) poolSub = pool
|
||||
// else {
|
||||
// filters = calcFilter()
|
||||
// poolSub.sub({ cb: onEvent, filter: filters})
|
||||
// }
|
||||
return relays
|
||||
},
|
||||
|
||||
publish(event, relayURL) {
|
||||
if (relayURL) return pool.relays[relayURL]?.relay?.publish?.(event)
|
||||
else pool.publish(event, (status, url) => {
|
||||
if (status === 0) {
|
||||
console.log(`publish request sent to ${url}`)
|
||||
}
|
||||
if (status === 1) {
|
||||
console.log(`event published by ${url}`, event)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// let poolRelays = {}
|
||||
// var poolSub = pool
|
||||
// var poolSub = {}
|
||||
// let relaysSet = false
|
||||
// var queue = []
|
||||
// async function run() {
|
||||
// // let queue = []
|
||||
// // db is not initialized, collect all requests in a queue
|
||||
// self.onmessage = async function (ev) {
|
||||
// let { name } = typeof ev.data === 'string' ? JSON.parse(ev.data) : ev.data
|
||||
// console.log('ev queue', name, ev)
|
||||
// if (name !== 'setRelays') queue.push(ev)
|
||||
// else {
|
||||
// console.log('ev setRelays', ev)
|
||||
// handleMessage(ev)
|
||||
// // relaysSet = true
|
||||
// }
|
||||
// }
|
||||
|
||||
// // while (!relaysSet) {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
// if (poolSub) calcFilter()
|
||||
// // console.log('waiting', relaysSet, queue)
|
||||
// // }
|
||||
|
||||
// self.onmessage = handleMessage
|
||||
// queue.forEach(ev => handleMessage(ev))
|
||||
// queue = null
|
||||
// }
|
||||
|
||||
// run()
|
||||
self.onmessage = handleMessage
|
||||
|
||||
function handleMessage(ev) {
|
||||
// self.onmessage = async function (ev) {
|
||||
// let { name, args, id, stream, cancel } = JSON.parse(ev.data)
|
||||
// let { name, args, id, cancel, sub } = JSON.parse(ev.data)
|
||||
// console.log('ev.data', ev.data, 'subs', subs)
|
||||
// let { name, args, id, cancel, sub } = ev.data
|
||||
let { name, args, id, cancel, sub } = typeof ev.data === 'string' ? JSON.parse(ev.data) : ev.data
|
||||
if (ev.ports.length && name === 'setPort') {
|
||||
dbWorkerPort = ev.ports[0]
|
||||
return
|
||||
} else if (ev.data.type) return
|
||||
// console.log('poolRelays', poolRelays)
|
||||
// if (Object.keys(poolRelays).length === 0 && name !== 'setRelays') {
|
||||
// queue.push(ev)
|
||||
// return
|
||||
// }
|
||||
|
||||
if (cancel) {
|
||||
// subs[id].cancel()
|
||||
// delete subs[id]
|
||||
cancelSub(id)
|
||||
} else if (sub) {
|
||||
subs[id] = methods[name](...args)
|
||||
// console.log(id, 'poolSub', poolSub, 'subs', subs, 'relays', relays)
|
||||
// if (!poolSub) {
|
||||
// // for (let url in relays) {
|
||||
// // pool.addRelay(url, relays[url])
|
||||
// // // await poolSub.addRelay(url, relays[url])
|
||||
// // console.log('addRelay var', url, relays[url])
|
||||
// // // poolRelays[url] = relays[url]
|
||||
// // }
|
||||
// filters = calcFilter()
|
||||
// poolSub = pool.sub({ cb: onEvent, filter: filters })
|
||||
// }
|
||||
if (poolSub) poolSub.sub({ cb: onEvent, filter: calcFilter()})
|
||||
else poolSub = pool.sub({ cb: onEvent, filter: calcFilter()})
|
||||
// console.log('calcFilter()', calcFilter())
|
||||
// if (poolSub.main) calcFilter()
|
||||
// if (poolSub) calcFilter()
|
||||
} else {
|
||||
var reply = { id }
|
||||
let data
|
||||
try {
|
||||
// if (name === 'setRelays') {
|
||||
// // poolSub['main'] = data
|
||||
// let relays = args[0]
|
||||
// for (let url in relays) {
|
||||
// await pool.addRelay(url, relays[url])
|
||||
// console.log('addRelay var', url, relays[url])
|
||||
// // poolRelays[url] = relays[url]
|
||||
// }
|
||||
// poolSub['main'] = pool.sub({ cb: onEvent, filter: [{
|
||||
// authors: ['8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168'],
|
||||
// kinds: [0, 3]
|
||||
// }] })
|
||||
// console.log('poolSub', poolSub, 'subs', subs)
|
||||
// data = poolSub
|
||||
// } else {
|
||||
// let data = await methods[name](...args)
|
||||
data = methods[name](...args)
|
||||
// data = await methods[name](...args)
|
||||
// }
|
||||
reply.success = true
|
||||
reply.data = data
|
||||
} catch (err) {
|
||||
reply.success = false
|
||||
reply.error = err.message
|
||||
}
|
||||
self.postMessage(JSON.stringify(reply))
|
||||
}
|
||||
}
|
@ -9,11 +9,6 @@ const routes = [
|
||||
component: () => import('pages/Feed.vue'),
|
||||
name: 'feed',
|
||||
},
|
||||
// {
|
||||
// path: '/home',
|
||||
// component: () => import('pages/Home.vue'),
|
||||
// name: 'home',
|
||||
// },
|
||||
{
|
||||
path: '/follow',
|
||||
component: () => import('pages/SearchFollow.vue'),
|
||||
@ -28,19 +23,6 @@ const routes = [
|
||||
path: '/messages/inbox',
|
||||
component: () => import('pages/Inbox.vue'),
|
||||
name: 'inbox',
|
||||
// children: [
|
||||
// {
|
||||
// path: 'inbox',
|
||||
// component: () => import('pages/Inbox.vue'),
|
||||
// name: 'inbox'
|
||||
// },
|
||||
// {
|
||||
// path: '/messages/:pubkey',
|
||||
// component: () => import('pages/Messages.vue'),
|
||||
// name: 'messages'
|
||||
// }
|
||||
// ],
|
||||
// redirect: { name: 'inbox' }
|
||||
},
|
||||
{
|
||||
path: '/messages/:pubkey([a-f0-9A-F]{64})',
|
||||
@ -67,6 +49,11 @@ const routes = [
|
||||
component: () => import('pages/Hashtag.vue'),
|
||||
name: 'hashtag',
|
||||
},
|
||||
{
|
||||
path: '/devTools',
|
||||
component: () => import('pages/devTools.vue'),
|
||||
name: 'devTools',
|
||||
},
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
component: () => import('pages/ErrorNotFound.vue'),
|
||||
|
@ -1,10 +1,19 @@
|
||||
import {encrypt} from 'nostr-tools/nip04'
|
||||
import {queryName} from 'nostr-tools/nip05'
|
||||
import {Notify, LocalStorage} from 'quasar'
|
||||
|
||||
import {Notify, LocalStorage, debounce} from 'quasar'
|
||||
import {pool, signAsynchronously} from '../pool'
|
||||
import {dbSave, dbGetProfile, dbGetContactList} from '../db'
|
||||
// import {processMentions, getPubKeyTagWithRelay} from '../utils/helpers'
|
||||
import {
|
||||
dbSave,
|
||||
dbUserProfile,
|
||||
dbUserFollows,
|
||||
streamUserProfile,
|
||||
streamUserFollows,
|
||||
streamUser,
|
||||
dbQuery,
|
||||
setRelays,
|
||||
publish,
|
||||
prune
|
||||
} from '../query'
|
||||
import {getPubKeyTagWithRelay} from '../utils/helpers'
|
||||
import {metadataFromEvent} from '../utils/event'
|
||||
|
||||
@ -16,6 +25,7 @@ export function initKeys(store, keys) {
|
||||
}
|
||||
|
||||
export async function launch(store) {
|
||||
console.log('launch for ', store.state.keys.pub)
|
||||
if (!store.state.keys.pub) {
|
||||
store.commit('setKeys') // passing no arguments will cause a new seed to be generated
|
||||
|
||||
@ -31,7 +41,7 @@ export async function launch(store) {
|
||||
}
|
||||
|
||||
// translate localStorage into a kind3 event -- or load relays and following from event
|
||||
let contactList = await dbGetContactList(store.state.keys.pub)
|
||||
let contactList = await dbUserFollows(store.state.keys.pub)
|
||||
var {relays, following} = store.state
|
||||
if (contactList) {
|
||||
try {
|
||||
@ -53,117 +63,86 @@ export async function launch(store) {
|
||||
store.commit('setFollowing', following)
|
||||
store.commit('setRelays', relays)
|
||||
|
||||
// setup pool
|
||||
for (let url in store.state.relays) {
|
||||
pool.addRelay(url, store.state.relays[url])
|
||||
}
|
||||
pool.onNotice((notice, relay) => {
|
||||
Notify.create({
|
||||
message: `Relay ${relay.url} says: ${notice}`,
|
||||
color: 'info'
|
||||
})
|
||||
})
|
||||
// preload our own profile from the db
|
||||
await store.dispatch('useProfile', {pubkey: store.state.keys.pub})
|
||||
|
||||
// start listening for nostr events
|
||||
store.dispatch('restartMainSubscription')
|
||||
|
||||
// preload our own profile from the db
|
||||
store.dispatch('useProfile', {pubkey: store.state.keys.pub})
|
||||
|
||||
// preload our follows profiles from the db
|
||||
for (let pubkey of following) store.dispatch('useProfile', {pubkey})
|
||||
}
|
||||
|
||||
export async function launchWithoutKey(store) {
|
||||
// var {relays} = store.state
|
||||
|
||||
// // update store state
|
||||
// store.commit('setRelays', relays)
|
||||
|
||||
// setup pool
|
||||
for (let url in store.state.relays) {
|
||||
pool.addRelay(url, store.state.relays[url])
|
||||
}
|
||||
pool.onNotice((notice, relay) => {
|
||||
Notify.create({
|
||||
message: `Relay ${relay.url} says: ${notice}`,
|
||||
color: 'info'
|
||||
})
|
||||
})
|
||||
store.dispatch('restartMainSubscription')
|
||||
}
|
||||
|
||||
var mainSub = pool
|
||||
let mainSub = {}
|
||||
export async function restartMainSubscription(store) {
|
||||
// console.log('restart main subscription for', [store.state.keys.pub].concat(store.state.following), store.state.relays)
|
||||
|
||||
export function restartMainSubscription(store) {
|
||||
mainSub = mainSub.sub(
|
||||
{
|
||||
filter: [
|
||||
// notes, profiles and contact lists of people we follow (and ourselves)
|
||||
{
|
||||
kinds: [0, 1, 2, 3],
|
||||
authors: store.state.following.concat(store.state.keys.pub)
|
||||
},
|
||||
// setup pool
|
||||
await setRelays(store.state.relays)
|
||||
|
||||
// posts mentioning us and direct messages to us
|
||||
{
|
||||
kinds: [1, 4],
|
||||
'#p': [store.state.keys.pub]
|
||||
},
|
||||
let botTracker = '29f63b70d8961835b14062b195fc7d84fa810560b36dde0749e4bc084f0f8952'
|
||||
let botTrackerSub = await streamUserFollows(botTracker)
|
||||
setTimeout(() => {
|
||||
botTrackerSub.cancel()
|
||||
}, 60 * 1000)
|
||||
|
||||
// our own direct messages to other people
|
||||
{
|
||||
kinds: [4],
|
||||
authors: [store.state.keys.pub]
|
||||
}
|
||||
],
|
||||
cb: async (event, relay) => {
|
||||
switch (event.kind) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
break
|
||||
case 2:
|
||||
break
|
||||
case 3: {
|
||||
if (event.pubkey === store.state.keys.pub) {
|
||||
// we got a new contact list from ourselves
|
||||
// we must update our local relays and following lists
|
||||
// if we don't have any local lists yet
|
||||
let local = await dbGetContactList(store.state.keys.pub)
|
||||
if (!local || local.created_at < event.created_at) {
|
||||
var relays, following
|
||||
try {
|
||||
relays = JSON.parse(event.content)
|
||||
store.commit('setRelays', relays)
|
||||
} catch (err) {
|
||||
/***/
|
||||
}
|
||||
if (!store.state.keys.pub) return
|
||||
|
||||
following = event.tags
|
||||
.filter(([t, v]) => t === 'p' && v)
|
||||
.map(([_, v]) => v)
|
||||
store.commit('setFollowing', following)
|
||||
setTimeout(() => {
|
||||
prune(store.state.keys.pub, [botTracker, store.state.keys.pub].concat(store.state.following))
|
||||
}, 5 * 60 * 1000)
|
||||
|
||||
following.forEach(f =>
|
||||
store.dispatch('useProfile', {pubkey: f})
|
||||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 4:
|
||||
break
|
||||
}
|
||||
if (store.state.following.length)
|
||||
store.state.following.forEach(pubkey => store.dispatch('useProfile', {pubkey}))
|
||||
if (!mainSub.streamUser) mainSub.streamUser = await streamUser(
|
||||
store.state.keys.pub,
|
||||
async event => {
|
||||
if (event.kind === 3) {
|
||||
let result = await dbQuery(`
|
||||
SELECT json_extract(event,'$.created_at') created_at
|
||||
FROM nostr
|
||||
WHERE json_extract(event,'$.kind') = 3 AND
|
||||
json_extract(event,'$.pubkey') = '${store.state.keys.pub}'
|
||||
LIMIT 1
|
||||
`)
|
||||
if (result.length && event.created_at < result[0].created_at) return
|
||||
let relays = JSON.parse(event.content)
|
||||
store.commit('setRelays', relays)
|
||||
|
||||
store.dispatch('addEvent', {event, relay})
|
||||
let follows = event.tags
|
||||
.filter(([t, v]) => t === 'p' && v)
|
||||
.map(([_, v]) => v)
|
||||
store.commit('setFollowing', follows)
|
||||
store.dispatch('restartMainSubscription')
|
||||
} else if (event.kind === 0) {
|
||||
let result = await dbQuery(`
|
||||
SELECT json_extract(event,'$.created_at') created_at
|
||||
FROM nostr
|
||||
WHERE json_extract(event,'$.kind') = 0 AND
|
||||
json_extract(event,'$.pubkey') = '${store.state.keys.pub}'
|
||||
LIMIT 1
|
||||
`)
|
||||
if (result.length && event.created_at < result[0].created_at) return
|
||||
|
||||
let metadata = metadataFromEvent(event)
|
||||
store.commit('addProfileToCache', metadata)
|
||||
}
|
||||
},
|
||||
'main-channel'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function addEvent(store, {event, relay = null}) {
|
||||
await dbSave(event, relay)
|
||||
}
|
||||
|
||||
export async function sendPost(store, {message, tags = [], kind = 1}) {
|
||||
if (message.length === 0) return
|
||||
|
||||
let event
|
||||
try {
|
||||
// const unpublishedEvent = await processMentions({
|
||||
const unpublishedEvent = {
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
@ -171,46 +150,23 @@ export async function sendPost(store, {message, tags = [], kind = 1}) {
|
||||
tags,
|
||||
content: message
|
||||
}
|
||||
// console.log('unpublishedEvent: ', unpublishedEvent)
|
||||
event = await pool.publish(unpublishedEvent)
|
||||
} catch (err) {
|
||||
|
||||
let event = await pool.publish(unpublishedEvent)
|
||||
if (!event) throw new Error('could not create post for publishing')
|
||||
|
||||
let publishResult = await publish(event)
|
||||
if (!publishResult) throw new Error('could not publish post')
|
||||
console.log('sendPost', event, publishResult)
|
||||
|
||||
store.dispatch('addEvent', {event})
|
||||
return event
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
message: `Did not publish: ${err}`,
|
||||
message: `could not publish post: ${error}`,
|
||||
color: 'negative'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
// aborted
|
||||
return
|
||||
}
|
||||
|
||||
store.dispatch('addEvent', {event})
|
||||
return event
|
||||
}
|
||||
|
||||
export async function setMetadata(store, metadata) {
|
||||
let event = await pool.publish({
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
content: JSON.stringify(metadata)
|
||||
})
|
||||
|
||||
store.dispatch('addEvent', {event})
|
||||
store.commit('addProfileToCache', { pubkey: store.state.keys.pub, ...metadata })
|
||||
}
|
||||
|
||||
export async function recommendServer(store, url) {
|
||||
await pool.publish({
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: Math.round(Date.now() / 1000),
|
||||
kind: 2,
|
||||
tags: [],
|
||||
content: url
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendChatMessage(store, {now, pubkey, text, tags}) {
|
||||
@ -227,12 +183,7 @@ export async function sendChatMessage(store, {now, pubkey, text, tags}) {
|
||||
} else {
|
||||
throw new Error('no private key available to encrypt!')
|
||||
}
|
||||
} catch (err) {
|
||||
/***/
|
||||
}
|
||||
|
||||
let event
|
||||
try {
|
||||
let unpublishedEvent = {
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: now,
|
||||
@ -240,163 +191,28 @@ export async function sendChatMessage(store, {now, pubkey, text, tags}) {
|
||||
tags: tags.map(([t, v]) => [t, v]),
|
||||
content: ciphertext
|
||||
}
|
||||
// console.log('unpublishedEvent: ', unpublishedEvent)
|
||||
// if (replyTo) {
|
||||
// unpublishedEvent.tags.push(['e', replyTo])
|
||||
// }
|
||||
event = await pool.publish(unpublishedEvent)
|
||||
} catch (err) {
|
||||
|
||||
let event = await pool.publish(unpublishedEvent)
|
||||
if (!event) throw new Error('could not create message for publishing')
|
||||
|
||||
let publishResult = await publish(event)
|
||||
if (!publishResult) throw new Error('could not publish message')
|
||||
|
||||
store.dispatch('addEvent', {event})
|
||||
return event
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
message: `Did not publish: ${err}`,
|
||||
message: `could not publish message: ${error}`,
|
||||
color: 'negative'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
// aborted
|
||||
return
|
||||
}
|
||||
|
||||
store.dispatch('addEvent', {event})
|
||||
return event
|
||||
}
|
||||
|
||||
export async function addEvent(store, {event, relay = null}) {
|
||||
await dbSave(event, relay)
|
||||
|
||||
// do things after the event is saved
|
||||
switch (event.kind) {
|
||||
case 0:
|
||||
// this will reset the profile cache for this URL
|
||||
store.dispatch('useProfile', {pubkey: event.pubkey})
|
||||
break
|
||||
case 1:
|
||||
break
|
||||
case 2:
|
||||
break
|
||||
case 3:
|
||||
// this will reset the profile cache for this URL
|
||||
store.dispatch('useContacts', event.pubkey)
|
||||
break
|
||||
case 4:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export async function useProfile(store, {pubkey, request = false}) {
|
||||
let metadata
|
||||
if (pubkey in store.state.profilesCache) {
|
||||
// we don't fetch again, but we do commit this so the LRU gets updated
|
||||
store.commit('addProfileToCache', {
|
||||
pubkey,
|
||||
...store.state.profilesCache[pubkey]
|
||||
}) // (just the pubkey is enough)
|
||||
return
|
||||
}
|
||||
|
||||
// fetch from db and add to cache
|
||||
let event = await dbGetProfile(pubkey)
|
||||
if (event) {
|
||||
metadata = metadataFromEvent(event)
|
||||
} else if (request) {
|
||||
// try to request from a relay
|
||||
await new Promise(resolve => {
|
||||
let sub = pool.sub({
|
||||
filter: [{authors: [pubkey], kinds: [0]}],
|
||||
cb: async event => {
|
||||
metadata = metadataFromEvent(event)
|
||||
clearTimeout(timeout)
|
||||
if (sub) sub.unsub()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
let timeout = setTimeout(() => {
|
||||
sub.unsub()
|
||||
sub = null
|
||||
resolve()
|
||||
}, 6000)
|
||||
})
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
store.commit('addProfileToCache', metadata)
|
||||
|
||||
if (metadata.nip05) {
|
||||
if (metadata.nip05 === '') delete metadata.nip05
|
||||
|
||||
let cached = store.state.nip05VerificationCache[metadata.nip05]
|
||||
if (cached && cached.when > Date.now() / 1000 - 60 * 60) {
|
||||
if (cached.pubkey !== pubkey) delete metadata.nip05
|
||||
} else {
|
||||
let checked = await queryName(metadata.nip05)
|
||||
store.commit('addToNIP05VerificationCache', {
|
||||
pubkey: checked,
|
||||
identifier: metadata.nip05
|
||||
})
|
||||
if (pubkey !== checked) delete metadata.nip05
|
||||
}
|
||||
|
||||
store.commit('addProfileToCache', metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function useContacts(store, {pubkey, request = false}) {
|
||||
if (pubkey in store.state.contactListCache) {
|
||||
// we don't fetch again, but we do commit this so the LRU gets updated
|
||||
store.commit('addContactListToCache', store.state.contactListCache[pubkey])
|
||||
return
|
||||
}
|
||||
|
||||
// fetch from db and add to cache
|
||||
let event = await dbGetContactList(pubkey)
|
||||
if (event) {
|
||||
store.commit('addContactListToCache', event)
|
||||
} else if (request) {
|
||||
// try to request from a relay
|
||||
await new Promise(resolve => {
|
||||
let sub = pool.sub({
|
||||
filter: [{authors: [pubkey], kinds: [3]}],
|
||||
cb: async event => {
|
||||
store.commit('addContactListToCache', event)
|
||||
// store.dispatch('addEvent', {event})
|
||||
clearTimeout(timeout)
|
||||
if (sub) sub.unsub()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
let timeout = setTimeout(() => {
|
||||
sub.unsub()
|
||||
sub = null
|
||||
resolve()
|
||||
}, 6000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// export async function useContacts(store, pubkey) {
|
||||
// if (pubkey in store.state.contactListCache) {
|
||||
// // we don't fetch again, but we do commit this so the LRU gets updated
|
||||
// store.commit('addContactListToCache', store.state.contactListCache[pubkey])
|
||||
// } else {
|
||||
// // fetch from db and add to cache
|
||||
// let event = await dbGetContactList(pubkey)
|
||||
// if (event) {
|
||||
// store.commit('addContactListToCache', event)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function publishContactList(store) {
|
||||
// extend the existing tags
|
||||
let event = await dbGetContactList(store.state.keys.pub)
|
||||
var tags = event?.tags || []
|
||||
|
||||
// remove contacts that we're not following anymore
|
||||
// tags = tags.filter(
|
||||
// ([t, v]) => t === 'p' && store.state.following.find(f => f === v)
|
||||
// )
|
||||
let oldEvent = await dbUserFollows(store.state.keys.pub)
|
||||
var tags = oldEvent?.tags || []
|
||||
|
||||
// check existing event because it might contain more data in the
|
||||
// tags that we don't want to replace, if so push existing event tag,
|
||||
@ -412,36 +228,165 @@ export async function publishContactList(store) {
|
||||
}
|
||||
})
|
||||
)
|
||||
// now we merely add to the existing event because it might contain more data in the
|
||||
// tags that we don't want to replace
|
||||
// await Promise.all(
|
||||
// store.state.following.map(async pubkey => {
|
||||
// if (!tags.find(([t, v]) => t === 'p' && v === pubkey)) {
|
||||
// tags.push(await getPubKeyTagWithRelay(pubkey))
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
|
||||
// event = {
|
||||
// pubkey: store.state.keys.pub,
|
||||
// created_at: Math.floor(Date.now() / 1000),
|
||||
// kind: 3,
|
||||
// tags,
|
||||
// newTags,
|
||||
// content: JSON.stringify(store.state.relays)
|
||||
// }
|
||||
event = await pool.publish({
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 3,
|
||||
tags: newTags,
|
||||
content: JSON.stringify(store.state.relays)
|
||||
})
|
||||
try {
|
||||
let event = await pool.publish({
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 3,
|
||||
tags: newTags,
|
||||
content: JSON.stringify(store.state.relays)
|
||||
})
|
||||
|
||||
await store.dispatch('addEvent', {event})
|
||||
if (!event) throw new Error('could not create updated list of followed keys and relays')
|
||||
|
||||
Notify.create({
|
||||
message: 'Updated and published list of followed keys and relays.',
|
||||
color: 'positive'
|
||||
})
|
||||
let publishResult = await publish(event)
|
||||
if (!publishResult) throw new Error('could not publish updated list of followed keys and relays')
|
||||
|
||||
let relays, follows
|
||||
relays = JSON.parse(event.content)
|
||||
follows = event.tags
|
||||
.filter(([t, v]) => t === 'p' && v)
|
||||
.map(([_, v]) => v)
|
||||
|
||||
// update store state
|
||||
store.commit('setFollowing', follows)
|
||||
store.commit('setRelays', relays)
|
||||
|
||||
await store.dispatch('addEvent', {event})
|
||||
|
||||
Notify.create({
|
||||
message: 'updated and published list of followed keys and relays.',
|
||||
color: 'positive'
|
||||
})
|
||||
return event
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
message: `could not publish updated list of followed keys and relays: ${error}`,
|
||||
color: 'negative'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export async function setMetadata(store, metadata) {
|
||||
try {
|
||||
let event = await pool.publish({
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
content: JSON.stringify(metadata)
|
||||
})
|
||||
if (!event) throw new Error('could not create updated profile event')
|
||||
|
||||
let publishResult = await publish(event)
|
||||
if (!publishResult) throw new Error('could not publish update profile event')
|
||||
|
||||
store.dispatch('addEvent', {event})
|
||||
store.commit('addProfileToCache', { pubkey: store.state.keys.pub, ...metadata })
|
||||
|
||||
Notify.create({
|
||||
message: 'updated and published profile',
|
||||
color: 'positive'
|
||||
})
|
||||
return event
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
message: `could not publish updated profile: ${error}`,
|
||||
color: 'negative'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export async function recommendRelay(store, url) {
|
||||
try {
|
||||
let event = await pool.publish({
|
||||
pubkey: store.state.keys.pub,
|
||||
created_at: Math.round(Date.now() / 1000),
|
||||
kind: 2,
|
||||
tags: [],
|
||||
content: url
|
||||
})
|
||||
if (!event) throw new Error('could not create recommend relay event')
|
||||
|
||||
let publishResult = await publish(event)
|
||||
if (!publishResult) throw new Error('could not publish recommend relay event')
|
||||
|
||||
store.dispatch('addEvent', {event})
|
||||
return event
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
message: `could not publish recommend relay event: ${error}`,
|
||||
color: 'negative'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedStreamUserProfile = debounce(async (store, users) => {
|
||||
if (!mainSub.streamUserProfile) {
|
||||
mainSub.streamUserProfile = await streamUserProfile(
|
||||
users,
|
||||
async event => {
|
||||
if (event.pubkey in store.state.profilesCache) return
|
||||
let metadata = metadataFromEvent(event)
|
||||
store.commit('addProfileToCache', metadata)
|
||||
store.dispatch('useNip05', {metadata})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
mainSub.streamUserProfile.update(users)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
let profilesInUse = {}
|
||||
export async function useProfile(store, {pubkey}) {
|
||||
if (pubkey in store.state.profilesCache) {
|
||||
// we don't fetch again, but we do commit this so the LRU gets updated
|
||||
store.commit('addProfileToCache', {
|
||||
pubkey,
|
||||
...store.state.profilesCache[pubkey]
|
||||
}) // (just the pubkey is enough)
|
||||
} else {
|
||||
// fetch from db and add to cache
|
||||
let event = await dbUserProfile(pubkey)
|
||||
if (event) {
|
||||
let metadata = metadataFromEvent(event)
|
||||
store.dispatch('useNip05', {metadata})
|
||||
}
|
||||
}
|
||||
|
||||
profilesInUse[pubkey] = profilesInUse[pubkey] || 0
|
||||
profilesInUse[pubkey]++
|
||||
if (profilesInUse[pubkey] === 1) debouncedStreamUserProfile(store, Object.keys(profilesInUse))
|
||||
}
|
||||
|
||||
export async function cancelUseProfile(store, {pubkey}) {
|
||||
if (!profilesInUse[pubkey]) return
|
||||
profilesInUse[pubkey]--
|
||||
if (profilesInUse[pubkey] === 0) {
|
||||
delete profilesInUse[pubkey]
|
||||
debouncedStreamUserProfile(store, Object.keys(profilesInUse))
|
||||
}
|
||||
}
|
||||
|
||||
export async function useNip05(store, {metadata}) {
|
||||
if (metadata.nip05 === '') delete metadata.nip05
|
||||
|
||||
if (metadata.nip05) {
|
||||
let cached = store.state.nip05VerificationCache[metadata.nip05]
|
||||
if (cached && cached.when > Date.now() / 1000 - 60 * 60) {
|
||||
if (cached.pubkey !== metadata.pubkey) delete metadata.nip05
|
||||
} else {
|
||||
let checked = await queryName(metadata.nip05)
|
||||
store.commit('addToNIP05VerificationCache', {
|
||||
pubkey: checked,
|
||||
identifier: metadata.nip05
|
||||
})
|
||||
if (metadata.pubkey !== checked) delete metadata.nip05
|
||||
}
|
||||
}
|
||||
store.commit('addProfileToCache', metadata)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export default function (store) {
|
||||
case 'addRelay':
|
||||
case 'removeRelay':
|
||||
case 'setRelayOpt':
|
||||
case 'saveRelays':
|
||||
case 'follow':
|
||||
case 'unfollow':
|
||||
case 'reorderFollows':
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
generateSeedWords,
|
||||
privateKeyFromSeed
|
||||
} from 'nostr-tools/nip06'
|
||||
// import Vuex from 'vuex'
|
||||
|
||||
export function setKeys(state, {mnemonic, priv, pub} = {}) {
|
||||
if (!mnemonic && !priv && !pub) {
|
||||
@ -51,6 +52,11 @@ export function setRelayOpt(state, {url, opt, value}) {
|
||||
}
|
||||
}
|
||||
|
||||
export function saveRelays(state, relays) {
|
||||
console.log('mutations save relays')
|
||||
state.relays = relays
|
||||
}
|
||||
|
||||
export function setFollowing(state, following) {
|
||||
state.following = following
|
||||
}
|
||||
@ -90,7 +96,7 @@ export function addProfileToCache(
|
||||
}
|
||||
|
||||
// removing older stuff if necessary
|
||||
if (state.profilesCacheLRU.length > 150) {
|
||||
if (state.profilesCacheLRU.length > 1500) {
|
||||
let oldest = state.profilesCacheLRU.shift()
|
||||
delete state.profilesCache[oldest]
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export default function (store) {
|
||||
replaceRelay(store, payload.url, state.relays[payload.url])
|
||||
break
|
||||
|
||||
case 'saveRelays':
|
||||
case 'follow':
|
||||
case 'unfollow':
|
||||
store.dispatch('restartMainSubscription')
|
||||
|
@ -12,7 +12,7 @@ const getMainnetRelays = () => {
|
||||
['wss://nostr.rocks', {read: true, write: true}],
|
||||
['wss://relayer.fiatjaf.com', {read: true, write: true}],
|
||||
['wss://nostr.onsats.org', {read: true, write: true}],
|
||||
['wss://nostr-relay.untethr.me ', {read: true, write: true}],
|
||||
['wss://nostr-relay.untethr.me', {read: true, write: true}],
|
||||
['wss://nostr-relay.wlvs.space', {read: true, write: true}],
|
||||
['wss://nostr.bitcoiner.social', {read: true, write: true}],
|
||||
['wss://nostr-relay.freeberty.net', {read: true, write: true}]
|
||||
|
@ -1,16 +1,15 @@
|
||||
import {
|
||||
onNewMention,
|
||||
onNewAnyMessage,
|
||||
dbGetChats,
|
||||
dbGetUnreadMessages,
|
||||
dbGetUnreadNotificationsCount
|
||||
} from '../db'
|
||||
dbChats,
|
||||
dbUnreadMessagesCount,
|
||||
dbUnreadMentionsCount,
|
||||
streamTag
|
||||
} from '../query'
|
||||
|
||||
export default function (store) {
|
||||
const setUnreadNotifications = async () => {
|
||||
store.commit(
|
||||
'setUnreadNotifications',
|
||||
await dbGetUnreadNotificationsCount(
|
||||
await dbUnreadMentionsCount(
|
||||
store.state.keys.pub,
|
||||
store.state.lastNotificationRead
|
||||
)
|
||||
@ -20,25 +19,32 @@ export default function (store) {
|
||||
const setUnreadMessages = async peer => {
|
||||
store.commit('setUnreadMessages', {
|
||||
peer,
|
||||
count: await dbGetUnreadMessages(
|
||||
count: await dbUnreadMessagesCount(
|
||||
store.state.keys.pub,
|
||||
peer,
|
||||
store.state.lastMessageRead[peer] || 0
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
onNewMention(store.state.keys.pub, setUnreadNotifications)
|
||||
onNewAnyMessage(event => {
|
||||
if (event.pubkey === store.state.keys.pub) return
|
||||
setUnreadMessages(event.pubkey)
|
||||
if (store.state.keys.pub) streamTag('p', store.state.keys.pub, event => {
|
||||
if (event.kind === 1) setUnreadNotifications
|
||||
else if (event.kind === 4) setUnreadMessages(event.pubkey)
|
||||
})
|
||||
else {
|
||||
let interval = setInterval(() => {
|
||||
if (store.state.keys.pub) {
|
||||
streamTag('p', store.state.keys.pub, event => {
|
||||
if (event.kind === 1) setUnreadNotifications
|
||||
else if (event.kind === 4) setUnreadMessages(event.pubkey)
|
||||
})
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
setUnreadNotifications()
|
||||
dbGetChats().then(chats => {
|
||||
chats.forEach(chat => {
|
||||
setUnreadMessages(chat.peer)
|
||||
})
|
||||
})
|
||||
dbChats(store.state.keys.pub).then(chats => { chats.forEach(chat => { setUnreadMessages(chat.peer) }) })
|
||||
|
||||
store.subscribe(({type, payload}, state) => {
|
||||
switch (type) {
|
||||
|
@ -16,6 +16,7 @@ export function metadataFromEvent(event) {
|
||||
metadata.pubkey = event.pubkey
|
||||
return metadata
|
||||
} catch (_) {
|
||||
console.log('metadataFromEvent error', _)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {dbGetProfile} from '../db'
|
||||
import {dbUserProfile} from '../query'
|
||||
|
||||
export function shorten(str) {
|
||||
return str ? str.slice(0, 5) + '…' + str.slice(-5) : ''
|
||||
@ -96,7 +96,7 @@ export async function processMentions(event) {
|
||||
|
||||
export async function getPubKeyTagWithRelay(pubkey) {
|
||||
var base = ['p', pubkey]
|
||||
let event = await dbGetProfile(pubkey)
|
||||
let event = await dbUserProfile(pubkey)
|
||||
if (event && event.seen_on && event.seen_on.length) {
|
||||
let random = event.seen_on[Math.floor(Math.random() * event.seen_on.length)]
|
||||
base.push(random)
|
||||
|
@ -2,6 +2,8 @@ import Tribute from 'tributejs'
|
||||
import {shorten} from './helpers'
|
||||
// import { stringify } from 'JSON'
|
||||
import {date} from 'quasar'
|
||||
import { dbStreamEvent } from 'src/query'
|
||||
import {decrypt} from 'nostr-tools/nip04'
|
||||
const { formatDate } = date
|
||||
|
||||
|
||||
@ -169,7 +171,7 @@ export default {
|
||||
return `
|
||||
<div class="flex row no-wrap items-center" style="gap: .2rem; width: 100%;">
|
||||
<div style="border-radius: 10px">
|
||||
<img src=${this.$store.getters.avatar(item.original.value.pubkey)} style="object-fit: cover; height: 1.5rem; width: 1.5rem;"/>
|
||||
<img src=${this.$store.getters.avatar(item.original.value.pubkey)} crossorigin style="object-fit: cover; height: 1.5rem; width: 1.5rem;"/>
|
||||
</div>
|
||||
<div class="text-bold">${item.string}</div>
|
||||
${item.original.value.nip05
|
||||
@ -199,5 +201,65 @@ export default {
|
||||
detach: element => tribute.detach(element),
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async processTaggedEvents(ids, events) {
|
||||
// let tagged = event.tags.filter(([t, v]) => t === 'e' && v).map(([t, v]) => v)
|
||||
// // console.log('processing tagged events for: ', event, tagged)
|
||||
// tagged.splice(10)
|
||||
// event.taggedEvents = []
|
||||
if (!Array.isArray(events)) throw new Error('no array supplied')
|
||||
ids.splice(10)
|
||||
// this.subTaggedEvents(tagged, event.taggedEvents)
|
||||
let eventSubs = {}
|
||||
for (let id of ids) {
|
||||
eventSubs[id] = await dbStreamEvent(id, async ev => {
|
||||
// ev = JSON.parse(ev)
|
||||
this.$store.dispatch('useProfile', { pubkey: ev.pubkey })
|
||||
if (ev.kind === 1 || ev.kind === 2) this.interpolateEventMentions(ev)
|
||||
else if (ev.kind === 4) {
|
||||
ev.text = await this.getPlaintext(ev)
|
||||
this.interpolateMessageMentions(ev)
|
||||
}
|
||||
events = events.push(ev)
|
||||
// event.taggedEvents.push(ev)
|
||||
eventSubs[id].cancel()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async getPlaintext(event) {
|
||||
if (
|
||||
event.tags.find(
|
||||
([tag, value]) => tag === 'p' && value === this.$store.state.keys.pub
|
||||
)
|
||||
) {
|
||||
// it is addressed to us
|
||||
// decrypt it
|
||||
return await this.decrypt(event.pubkey, event.content)
|
||||
} else if (event.pubkey === this.$store.state.keys.pub) {
|
||||
// it is coming from us
|
||||
let [_, target] = event.tags.find(([tag]) => tag === 'p')
|
||||
// decrypt it
|
||||
return await this.decrypt(target, event.content)
|
||||
}
|
||||
},
|
||||
|
||||
async decrypt(peer, ciphertext) {
|
||||
try {
|
||||
if (this.$store.state.keys.priv) {
|
||||
return decrypt(this.$store.state.keys.priv, peer, ciphertext)
|
||||
} else if (
|
||||
(await window?.nostr?.getPublicKey?.()) === this.$store.state.keys.pub
|
||||
) {
|
||||
return await window.nostr.nip04.decrypt(peer, ciphertext)
|
||||
} else {
|
||||
throw new Error('no private key available to decrypt!')
|
||||
}
|
||||
} catch (err) {
|
||||
return '???'
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
// import { search } from 'core-js/fn/symbol'
|
||||
import {addSorted} from './helpers'
|
||||
|
||||
function calcReplyTags(event, route) {
|
||||
@ -7,10 +6,6 @@ function calcReplyTags(event, route) {
|
||||
else return []
|
||||
}
|
||||
|
||||
// function log(desc, ...v) {
|
||||
// console.log(desc, ...v)
|
||||
// }
|
||||
|
||||
function searchAndUpdateThreads(threads, route, ...events) {
|
||||
// scenarios:
|
||||
// 1) new post event
|
||||
@ -91,7 +86,6 @@ function searchAndUpdateThreads(threads, route, ...events) {
|
||||
if (events.length > 1) {
|
||||
event.replies.push(events.slice(1))
|
||||
}
|
||||
unshiftThreads.sort()
|
||||
for (let j = unshiftThreads.length - 1; j >= 0; j--) {
|
||||
event.replies.push(threads[unshiftThreads[j]])
|
||||
threads.splice(unshiftThreads[j], 1)
|
||||
|
453
src/worker-db.js
453
src/worker-db.js
@ -1,453 +0,0 @@
|
||||
/* global emit */
|
||||
|
||||
import PouchDB from 'pouchdb-core'
|
||||
import PouchDBUpsert from 'pouchdb-upsert'
|
||||
import PouchDBMapReduce from 'pouchdb-mapreduce'
|
||||
import PouchDBAdapterIDB from 'pouchdb-adapter-idb'
|
||||
|
||||
import {cleanEvent} from './utils/event'
|
||||
|
||||
PouchDB.plugin(PouchDBAdapterIDB).plugin(PouchDBMapReduce).plugin(PouchDBUpsert)
|
||||
|
||||
// instantiate db (every doc will be an event, that's it)
|
||||
// ~
|
||||
const db = new PouchDB('nostr-events', {
|
||||
auto_compaction: true,
|
||||
revs_limit: 1
|
||||
})
|
||||
|
||||
// db schema (views)
|
||||
// ~
|
||||
const DESIGN_VERSION = 7
|
||||
db.upsert('_design/main', current => {
|
||||
if (current && current.version >= DESIGN_VERSION) return false
|
||||
|
||||
return {
|
||||
version: DESIGN_VERSION,
|
||||
views: {
|
||||
profiles: {
|
||||
map: function (event) {
|
||||
if (event.kind === 0) {
|
||||
emit(event.pubkey)
|
||||
}
|
||||
}.toString()
|
||||
},
|
||||
homefeed: {
|
||||
map: function (event) {
|
||||
if (event.kind === 1 || event.kind === 2) {
|
||||
emit(event.created_at)
|
||||
}
|
||||
}.toString()
|
||||
},
|
||||
mentions: {
|
||||
map: function (event) {
|
||||
if (event.kind === 1) {
|
||||
for (var i = 0; i < event.tags.length; i++) {
|
||||
var tag = event.tags[i]
|
||||
if (tag[0] === 'p') emit([tag[1], event.created_at])
|
||||
if (tag[0] === 'e') emit([tag[1], event.created_at])
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
},
|
||||
messages: {
|
||||
map: function (event) {
|
||||
if (event.kind === 4) {
|
||||
for (var i = 0; i < event.tags.length; i++) {
|
||||
var tag = event.tags[i]
|
||||
if (tag[0] === 'p') {
|
||||
emit([tag[1], event.created_at])
|
||||
break
|
||||
}
|
||||
}
|
||||
emit([event.pubkey, event.created_at])
|
||||
}
|
||||
}.toString()
|
||||
},
|
||||
contactlists: {
|
||||
map: function (event) {
|
||||
if (event.kind === 3) {
|
||||
emit(event.pubkey)
|
||||
}
|
||||
}.toString()
|
||||
},
|
||||
followers: {
|
||||
map: function (event) {
|
||||
if (event.kind === 3) {
|
||||
for (let i = 0; i < event.tags.length; i++) {
|
||||
var tag = event.tags[i]
|
||||
if (tag.length >= 2 && tag[0] === 'p') {
|
||||
emit(tag[1], event.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
},
|
||||
petnames: {
|
||||
map: function (event) {
|
||||
if (event.kind === 3) {
|
||||
for (let i = 0; i < event.tags.length; i++) {
|
||||
var tag = event.tags[i]
|
||||
if (tag.length >= 4 && tag[0] === 'p') {
|
||||
emit(tag[1], [event.pubkey, tag[3]])
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
// cleanup old views after a design doc change
|
||||
db.viewCleanup().then(r => console.log('view cleanup done', r))
|
||||
})
|
||||
|
||||
// delete old events after the first 1000 (this is slow, so do it after a while)
|
||||
//
|
||||
setTimeout(async () => {
|
||||
let result = await db.query('main/homefeed', {
|
||||
descending: true,
|
||||
skip: 1000,
|
||||
include_docs: true
|
||||
})
|
||||
result.rows.forEach(row => db.remove(row.doc))
|
||||
}, 1000 * 60 * 15 /* 15 minutes */)
|
||||
|
||||
const methods = {
|
||||
// delete everything
|
||||
//
|
||||
async eraseDatabase() {
|
||||
return await db.destroy()
|
||||
},
|
||||
|
||||
async destroyStreams() {
|
||||
for (let id in streams) {
|
||||
streams[id].cancel()
|
||||
delete streams[id]
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// general function for saving an event, with granular logic for each kind
|
||||
//
|
||||
async dbSave(event, relay) {
|
||||
switch (event.kind) {
|
||||
case 0: {
|
||||
// first check if we don't have a newer metadata for this user
|
||||
let current = await methods.dbGetProfile(event.pubkey)
|
||||
if (current && current.created_at >= event.created_at) {
|
||||
// don't save
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
case 1:
|
||||
break
|
||||
case 2:
|
||||
break
|
||||
case 3: {
|
||||
// first check if we don't have a newer contact list for this user
|
||||
let current = await methods.dbGetContactList(event.pubkey)
|
||||
if (current && current.created_at >= event.created_at) {
|
||||
// don't save
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
case 4: {
|
||||
// cleanup extra fields if somehow they manage to get in here (they shouldn't)
|
||||
delete event.appended
|
||||
delete event.plaintext
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
event._id = event.id
|
||||
|
||||
try {
|
||||
await db.upsert(event.id, current => {
|
||||
if (
|
||||
(current.seen_on && current.seen_on.indexOf(relay) !== -1) ||
|
||||
!relay
|
||||
) {
|
||||
// return falsey so the document won't be updated
|
||||
return false
|
||||
}
|
||||
|
||||
// otherwise update with the relay this was seen on
|
||||
let updated = cleanEvent(event)
|
||||
updated.seen_on = current.seen_on || []
|
||||
updated.seen_on.push(relay)
|
||||
return updated
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('unexpected error saving event', event, err)
|
||||
}
|
||||
},
|
||||
|
||||
// db queries
|
||||
// ~
|
||||
async dbGetHomeFeedNotes(limit = 50, until = Math.round(Date.now() / 1000)) {
|
||||
let result = await db.query('main/homefeed', {
|
||||
include_docs: true,
|
||||
descending: true,
|
||||
limit,
|
||||
startkey: until
|
||||
})
|
||||
return result.rows.map(r => r.doc)
|
||||
},
|
||||
|
||||
onNewHomeFeedNote(callback = () => {}) {
|
||||
let changes = db.changes({
|
||||
live: true,
|
||||
since: 'now',
|
||||
include_docs: true,
|
||||
filter: '_view',
|
||||
view: 'main/homefeed'
|
||||
})
|
||||
|
||||
changes.on('change', change => callback(change.doc))
|
||||
|
||||
return changes
|
||||
},
|
||||
|
||||
async dbGetChats(ourPubKey) {
|
||||
let result = await db.query('main/messages')
|
||||
|
||||
let chats = result.rows
|
||||
.map(r => r.key)
|
||||
.reduce((acc, [peer, date]) => {
|
||||
acc[peer] = acc[peer] || 0
|
||||
if (date > acc[peer]) acc[peer] = date
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
delete chats[ourPubKey]
|
||||
|
||||
return Object.entries(chats)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([peer, lastMessage]) => ({peer, lastMessage}))
|
||||
},
|
||||
|
||||
async dbGetMessages(
|
||||
peerPubKey,
|
||||
limit = 50,
|
||||
since = Math.round(Date.now() / 1000)
|
||||
) {
|
||||
let result = await db.query('main/messages', {
|
||||
include_docs: true,
|
||||
descending: true,
|
||||
startkey: [peerPubKey, since],
|
||||
endkey: [peerPubKey, 0],
|
||||
limit
|
||||
})
|
||||
return result.rows
|
||||
.map(r => r.doc)
|
||||
.reverse()
|
||||
.reduce((acc, event) => {
|
||||
if (!acc.length) return [event]
|
||||
let last = acc[acc.length - 1]
|
||||
if (
|
||||
last.pubkey === event.pubkey &&
|
||||
last.created_at + 120 >= event.created_at
|
||||
) {
|
||||
last.appended = last.appended || []
|
||||
last.appended.push(event)
|
||||
} else {
|
||||
acc.push(event)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
},
|
||||
|
||||
onNewMessage(peerPubKey, callback = () => {}) {
|
||||
// listen for changes
|
||||
let changes = db.changes({
|
||||
live: true,
|
||||
since: 'now',
|
||||
include_docs: true,
|
||||
filter: '_view',
|
||||
view: 'main/messages'
|
||||
})
|
||||
|
||||
changes.on('change', change => {
|
||||
if (
|
||||
change.doc.pubkey === peerPubKey ||
|
||||
change.doc.tags.find(([t, v]) => t === 'p' && v === peerPubKey)
|
||||
) {
|
||||
callback(change.doc)
|
||||
}
|
||||
})
|
||||
|
||||
return changes
|
||||
},
|
||||
|
||||
async dbGetEvent(id) {
|
||||
try {
|
||||
return await db.get(id)
|
||||
} catch (err) {
|
||||
if (err.name === 'not_found') return null
|
||||
else throw err
|
||||
}
|
||||
},
|
||||
|
||||
onEventUpdate(id, callback = () => {}) {
|
||||
let changes = db.changes({
|
||||
live: true,
|
||||
since: 'now',
|
||||
include_docs: true,
|
||||
doc_ids: [id]
|
||||
})
|
||||
|
||||
changes.on('change', change => callback(change.doc))
|
||||
|
||||
return changes
|
||||
},
|
||||
|
||||
async dbGetMentions(ourPubKey, limit = 40, since, until) {
|
||||
let result = await db.query('main/mentions', {
|
||||
include_docs: true,
|
||||
descending: true,
|
||||
startkey: [ourPubKey, until],
|
||||
endkey: [ourPubKey, since],
|
||||
limit
|
||||
})
|
||||
return result.rows.map(r => r.doc)
|
||||
},
|
||||
|
||||
onNewMention(ourPubKey, callback = () => {}) {
|
||||
// listen for changes
|
||||
let changes = db.changes({
|
||||
live: true,
|
||||
since: 'now',
|
||||
include_docs: true,
|
||||
filter: '_view',
|
||||
view: 'main/mentions'
|
||||
})
|
||||
|
||||
changes.on('change', change => {
|
||||
if (change.doc.tags.find(([t, v]) => t === 'p' && v === ourPubKey)) {
|
||||
callback(change.doc)
|
||||
}
|
||||
})
|
||||
|
||||
return changes
|
||||
},
|
||||
|
||||
onNewAnyMessage(callback = () => {}) {
|
||||
// listen for changes
|
||||
let changes = db.changes({
|
||||
live: true,
|
||||
since: 'now',
|
||||
include_docs: true,
|
||||
filter: '_view',
|
||||
view: 'main/messages'
|
||||
})
|
||||
|
||||
changes.on('change', change => {
|
||||
callback(change.doc)
|
||||
})
|
||||
|
||||
return changes
|
||||
},
|
||||
|
||||
async dbGetUnreadNotificationsCount(ourPubKey, since) {
|
||||
let result = await db.query('main/mentions', {
|
||||
include_docs: false,
|
||||
descending: true,
|
||||
startkey: [ourPubKey, Math.round(Date.now() / 1000)],
|
||||
endkey: [ourPubKey, since]
|
||||
})
|
||||
return result.rows.filter((v, i, a) => a.indexOf(v) === i).length
|
||||
},
|
||||
|
||||
async dbGetUnreadMessages(pubkey, since) {
|
||||
let result = await db.query('main/messages', {
|
||||
include_docs: true,
|
||||
descending: true,
|
||||
startkey: [pubkey, Math.round(Date.now() / 1000)],
|
||||
endkey: [pubkey, since]
|
||||
})
|
||||
return result.rows.filter(r => r.doc.pubkey === pubkey).length
|
||||
},
|
||||
|
||||
async dbGetProfile(pubkey) {
|
||||
let result = await db.query('main/profiles', {
|
||||
include_docs: true,
|
||||
key: pubkey
|
||||
})
|
||||
switch (result.rows.length) {
|
||||
case 0:
|
||||
return null
|
||||
case 1:
|
||||
return result.rows[0].doc
|
||||
default: {
|
||||
let sorted = result.rows.sort(
|
||||
(a, b) => (b.doc?.created_at || 0) - (a.doc?.created_at || 0)
|
||||
)
|
||||
sorted
|
||||
.slice(1)
|
||||
.filter(row => row.doc)
|
||||
.forEach(row => db.remove(row.doc))
|
||||
return sorted[0].doc
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async dbGetContactList(pubkey) {
|
||||
let result = await db.query('main/contactlists', {
|
||||
include_docs: true,
|
||||
key: pubkey
|
||||
})
|
||||
switch (result.rows.length) {
|
||||
case 0:
|
||||
return null
|
||||
case 1:
|
||||
return result.rows[0].doc
|
||||
default: {
|
||||
let sorted = result.rows.sort(
|
||||
(a, b) => (b.doc?.created_at || 0) - (a.doc?.created_at || 0)
|
||||
)
|
||||
sorted
|
||||
.slice(1)
|
||||
.filter(row => row.doc)
|
||||
.forEach(row => db.remove(row.doc))
|
||||
return sorted[0].doc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var streams = {}
|
||||
|
||||
self.onmessage = async function (ev) {
|
||||
let {name, args, id, stream, cancel} = JSON.parse(ev.data)
|
||||
|
||||
if (stream) {
|
||||
let changes = methods[name](...args, data => {
|
||||
self.postMessage(
|
||||
JSON.stringify({
|
||||
id,
|
||||
data,
|
||||
stream: true
|
||||
})
|
||||
)
|
||||
})
|
||||
streams[id] = changes
|
||||
} else if (cancel) {
|
||||
streams[id].cancel()
|
||||
delete streams[id]
|
||||
} else {
|
||||
var reply = {id}
|
||||
try {
|
||||
let data = await methods[name](...args)
|
||||
reply.success = true
|
||||
reply.data = data
|
||||
} catch (err) {
|
||||
reply.success = false
|
||||
reply.error = err.message
|
||||
}
|
||||
|
||||
self.postMessage(JSON.stringify(reply))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user