update: absurd-sql

This commit is contained in:
monica 2022-09-04 12:06:12 -05:00
parent acecce96b8
commit cc15c9b54c
48 changed files with 8466 additions and 9885 deletions

2337
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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',

View File

@ -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

View 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>

View File

@ -29,7 +29,6 @@
<script>
import { defineComponent } from 'vue'
// import {onEventUpdate} from '../db'
import BaseRelayList from 'components/BaseRelayList.vue'
export default defineComponent({

View File

@ -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') ||

View File

@ -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 {

View File

@ -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;

View File

@ -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&nbsp;</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

View File

@ -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)

View File

@ -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'"

View File

@ -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)
}
},

View File

@ -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>

View File

@ -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)
}
}
}

View File

@ -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'

View File

@ -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

View File

@ -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
View File

@ -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])
}

View File

@ -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',
}

View File

@ -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')
}

View File

@ -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>

View File

@ -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})
},
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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) ||

View File

@ -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>

View File

@ -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
}
}
})

View File

@ -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
View 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>

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

149
src/relay.js Normal file
View 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
View 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))
}
}

View File

@ -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'),

View File

@ -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)
}

View File

@ -7,6 +7,7 @@ export default function (store) {
case 'addRelay':
case 'removeRelay':
case 'setRelayOpt':
case 'saveRelays':
case 'follow':
case 'unfollow':
case 'reorderFollows':

View File

@ -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]
}

View File

@ -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')

View File

@ -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}]

View File

@ -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) {

View File

@ -16,6 +16,7 @@ export function metadataFromEvent(event) {
metadata.pubkey = event.pubkey
return metadata
} catch (_) {
console.log('metadataFromEvent error', _)
return {}
}
}

View File

@ -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)

View File

@ -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 '???'
}
},
}
}

View File

@ -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)

View File

@ -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))
}
}

10819
yarn.lock

File diff suppressed because it is too large Load Diff