update: manage jack effect

This commit is contained in:
monica 2022-12-21 15:30:07 -06:00
parent 5b89a12f39
commit 9bf4e0a6a3
31 changed files with 1246 additions and 270 deletions

View File

@ -18,8 +18,10 @@
"codemirror": "5",
"core-js": "^3.6.5",
"cross-fetch": "^3.1.5",
"dompurify": "^2.4.1",
"emoji-mart-vue-fast": "^10.2.1",
"identicon.js": "2.3",
"light-bolt11-decoder": "^2.1.0",
"markdown-it": "13.0",
"markdown-it-deflist": "2.1",
"markdown-it-emoji": "^2.0.2",
@ -28,6 +30,7 @@
"markdown-it-sup": "1.0",
"markdown-it-task-lists": "2.1",
"mergebounce": "^0.1.1",
"nayuki-qr-code-generator": "^1.8.0",
"nostr-tools": "^0.24.1",
"quasar": "2.5.5",
"readable-stream": "3.6.0",

View File

@ -9,7 +9,7 @@
:size='buttonSize'
class='button-copy'
dense
:label='verbose ? "copy" : ""'
:label='(verbose || buttonLabel) ? (buttonLabel || "copy") : ""'
align="left"
>
<q-tooltip v-if='tooltipText'>
@ -20,14 +20,21 @@
<script>
import { defineComponent } from 'vue'
import helpersMixin from '../utils/mixin'
import {Notify} from 'quasar'
export default defineComponent({
name: 'BaseButtonCopy',
mixins: [helpersMixin],
props: {
buttonText: {
type: String,
required: true
},
buttonLabel: {
type: String,
default: null
},
buttonClass: {
type: String,
required: false,
@ -52,20 +59,25 @@ export default defineComponent({
default: null
}
},
methods: {
copy() {
let text = this.copyText(this.buttonText)
console.log(text)
navigator.clipboard.writeText(text)
},
copyText(defaultText) {
computed: {
copyText() {
let selection = this.element?.getSelection()?.toString()
if (selection) {
return selection
} else return defaultText
} else return this.buttonText
},
},
methods: {
copy() {
let text = this.copyText
console.log(text)
navigator.clipboard.writeText(text)
Notify.create({
message: `copied ${this.shorten(this.copyText, 30)}`,
})
},
}
})
</script>

View File

@ -2,7 +2,6 @@
<q-btn
:class='buttonClass + (isFollowing ? "button-unfollow" : "button-follow")'
:size='buttonSize'
:disable="!$store.getters.canSignEventsAutomatically"
unelevated
:text-color='isFollowing ? "" : "secondary"'
dense

View File

@ -0,0 +1,390 @@
<template>
<div style='border: 1px solid var(--q-accent); border-radius: .5rem;'>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>learn about Nostr</span>
</template>
<q-card-section>
<p>
the <a href='https://github.com/fiatjaf/nostr' target='_blank'>Nostr</a> protocol is
a decentralized and censorship resistant distributed information network that relies on clients and relays.
relays store user data. clients communicate with the relays to save and fetch said user data.
</p>
<ul>
<li>users choose which relays to store their data on, meaning no one centralized entity has the power
to remove your data from the network (so it is recommended to use multiple relays)</li>
<li>users choose which clients to use, meaning no one centralized website can stop you from accessing the network</li>
<li>any client can be used with any relay, meaning users can choose their relays and client independently</li>
</ul>
<p>
astral is a client for Nostr. while astral is implementing a social media usecase of Nostr, the possibilities of Nostr are endless.
<a href='https://jesterui.github.io/#/game/jester1y7du0yq7uzfzhxr2xgd64lmchfpf54evjsa59ff4f2mgh83h79rs9k7ffq'>Jester</a>
is a beta peer to peer chess client implemented over Nostr.
</p>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>do I need a key?</span>
</template>
<q-card-section>
<p>
if you would just like to look around you do not need a key pair, simply close this
dialog popup. however, if you want to post or save your profile and follows you will
need to create a key pair if you don't have one already.
</p>
<p>
if you decide to just look around and want to login at a later time hit the set user <q-icon name='login'/>
button in the user menu.
</p>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<!-- <div class='flex row justify-between'> -->
<template #header>
<span class='full-width text-bold'>what is a key pair?</span>
</template>
<q-card-section>
in order to participate in the Nostr network you will need to a public key and private key pair.
this key pair can be used in any Nostr client to login.
<q-list bordered padding class="q-mt-sm q-mb-sm">
<q-item>
<q-item-section>
<q-item-label>public key</q-item-label>
<q-item-label caption>
publicly known unique ID associated with your user on the Nostr
network. can be shared freely. others can see your posts or
follow you using only your public key.
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label>private key</q-item-label>
<q-item-label caption>
<strong>KEEP THIS SECRET!</strong> secret key used to sign for
(or unlock) your public key. all content from your user public
key will need a signature derived from your private key before
being relayed. if a bad actor discovers your private key they
can impersonate you on Nostr network and see your encrypted dms.
</q-item-label>
</q-item-section>
</q-item>
</q-list>
<p>
your public key is created from your private key via a one way hash
function, meaning:
<ul>
<li>your public key can be calculated from your private
key - which is why you only need to enter your private key for astral to
know your public key</li>
<li>your private key cannot be calculated from your public key - which
is why you can freely share your public key without compromising your
private key</li>
</ul>
</p>
<p>
through the magic of cryptograpic functions, this private and public key pair
allows you to sign for Nostr events, which could represent a post, profile
settings, or follows list, in a cryptographically verifiable manner (similar to
how you sign for a bitcoin transaction)
</p>
<p>
you may see your keys displayed in a couple different key formats depending on
the Nostr client you use, please don't be alarmed. they both represent the same
byte data, they just use different encoding methods to be human readable. the
'npub' (for <span style='text-decoration: underline;'>N</span>ostr
<span style='text-decoration: underline;'>pub</span>lic key) and 'nsec'
(for <span style='text-decoration: underline;'>N</span>ostr
<span style='text-decoration: underline;'>sec</span>ret key) format that Damus
uses is preferable over the hex format that astral uses because there is a visual
indicator preventing the user from mixing up their public and private key. astral
will adopt this format in the future.
</p>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>why shouldn't I enter my private key?</span>
</template>
<q-card-section>
<p>
anytime you enter your private key into any Nostr client, you are trusting that client to:
<ol>
<li>not store your private key</li>
<li>not have any vulnerabilities that a bad actor can exploit to steal your private key</li>
</ol>
while I can promise you that astral does not store your private key (it is stored locally in your
browser and is NEVER sent to back to astral) and that I am doing my best to prevent vulnerabilities,
it is still recommeneded that you <strong>DO NOT TRUST ME</strong>.
</p>
<p>
fortunately, on desktop devices Nostr provides an easy way for you to sign into Nostr clients without
ever providing the client with your private key via browser extensions like
<a href='https://getalby.com/' target='_blank'>getAlby</a> or <a href='https://github.com/fiatjaf/nos2x#install' target='_blank'>nos2x</a>.
these browser extensions will store your private key locally
in your browser. when the client needs to send an event or decrypt your messages (ie. use your
private key), it will employ your browser extension to do the necessary cryptographic functions.
see <strong>how to get a key pair?</strong> section below for instructions on how to use these
browser extensions.
</p>
<p>
unfortunately, on mobile devices you cannot use browser extensions so it will be
necessary to enter your private key for now. this is a known and important issue
that is being worked on, and there should be some better solutions for private key
management on mobile devices coming soon.
</p>
</q-card-section>
</q-expansion-item>
<div v-if='$store.state.keys.priv' style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
v-if='$store.state.keys.priv'
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>how do I remove my private key if I already entered it?</span>
</template>
<q-card-section>
<p>
if you are on mobile you will not be able to use the directions below because browser extensions are
not supported on mobile devices. if you are on a desktop device please continue.
</p>
<ol>
<li>hit the <strong>VIEW YOUR KEYS</strong> at the bottom of the settings page and note down your private key</li>
<li>follow the instructions in <strong>how to get a key pair?</strong> section below, making sure to enter
your private key rather than generating a new one</li>
<li>hit the <strong>LOGOUT</strong> at the bottom of the settings page (<strong>LOGOUT</strong> will wipe your
user data but preserve the existing browser database, <strong>DELETE LOCAL DATA</strong> will wipe your user
data and the browser database</li>
<li>hit the <strong>USE PUBLIC KEY FROM EXTENSION</strong> option that should appear in the key input (if
astral doesn't refresh automatically please refresh the page</li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>how to get a key pair?</span>
</template>
<q-card-section>
<p>
you can use any method you want to generate your keys, as long as they
conform to the <a href='https://github.com/nostr-protocol/nips/blob/master/01.md' target='_blank'>Nostr NIP-01</a>
specification.
<ul>
<li>if you are on a desktop device the recommeneded option will be using a browser extension like
<a href='https://getalby.com/' target='_blank'>getAlby</a> or <a href='https://github.com/fiatjaf/nos2x#install' target='_blank'>nos2x</a>.
(see <strong>why shouldn't I enter my private key?</strong> section above)</li>
<li>if you are on a mobile device the easiest option will be using astral</li>
<li>if you are a technical user who is concerned about key generation security you can try the local option</li>
</ul>
</p>
<p>
**if you already have a key pair and just want to enter it into a browser extension, follow the
instructions in the applicable sub-section below but enter your private key instead of hitting
'generate' (with nos2x make sure to hit save afterword). <strong>if your existing private key begins with
'nsec' you will need to convert it to hex format</strong> before saving it in the browser extensions, which
can be done <a href='https://damus.io/key/'>here</a>.**
</p>
<p>
choose your key generation method:
</p>
<div style='border: 1px solid var(--q-accent); border-radius: .5rem;'>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>use astral</span>
</template>
<q-card-section>
<ol>
<li>hit <strong>GENERATE KEYS</strong> button below in the key input</li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<div class='full-width'>
<span class='text-bold'>use getAlby </span>
<span>longer setup than nos2x but is password protected, comes with a lightning wallet and is better maintained</span>
</div>
</template>
<q-card-section>
<ol>
<li>install <a href='https://getalby.com/' target='_blank'>getAlby</a> browser extension</li>
<li>complete alby setup (when it asks <strong>Do you have a lightning wallet?</strong> just hit <strong>Alby Wallet</strong>
if you do not have one)</li>
<li>open the getAlby extension options page (usually in dropdown when clicking on extension's icon at the top of your broswer)</li>
<li>hit the <strong>Settings</strong> tab</li>
<li>scroll down to the <strong>Nostr</strong> section and hit <strong>Generate</strong> (or if you already have a private key enter it)</li>
<li>refresh astral.ninja page and hit the <strong>USE PUBLIC KEY FROM EXTENSION</strong> option that should appear in the key input</li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>use nos2x</span>
</template>
<q-card-section>
<ol>
<li>install <a href='https://github.com/fiatjaf/nos2x#install' target='_blank'>nos2x</a> browser extension</li>
<li>open the nos2x extension options page (usually in dropdown when clicking on extension's icon at the top of your broswer)</li>
<li>hit <strong>generate</strong> button (or if you already have a private key enter it and hit <strong>save</strong>)</li>
<li>refresh astral.ninja page and hit the <strong>USE PUBLIC KEY FROM EXTENSION</strong> option that should appear in the key input</li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>use local machine</span>
</template>
<q-card-section>
<ol>
<li>open terminal</li>
<li>enter the command <pre>openssl rand -hex 32</pre></li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
</div>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>how to use astral?</span>
</template>
<q-card-section>
<ol>
<li>
<strong>enter your key</strong> if you don't have a key pair, or you have a key pair that you want to enter into
a Nostr browser extension, see <strong>how to get a key pair?</strong> section above.
</li>
<li>
<strong>choose bootstrap relays (optional)</strong> this section will only appear once a valid key has
been entered. if you are using a brand new key you don't need
to worry about this. if you are using an existing key and don't typically use any of the selected
relays, make sure to include a relay you typically use or astral may not be able to find your user.
the bootstrap relays are NOT used as your user's relay list, astral only uses these to find your
user's information and settings when loading your Nostr account.
</li>
<li>
<strong>hit PROCEED</strong> astral will login and attempt to find your user information. please
be patient as it can take a few minutes to completely sync with Nostr relays.
</li>
<li>
<strong>BACK UP YOUR KEYS!!!</strong> once you are taken to the settings page you should get a
popup displaying your Nostr keys. make sure to make a backup of your Nostr keys and keep it
somewhere safe. if you lose your Nostr keys you lose access to your Nostr user identity. there is no
astral customer service to reset your password.
</li>
<li>
<strong>edit settings</strong> hit the <strong>EDIT</strong> button for which ever section you would
like to edit.
<ul>
<li><strong>profile</strong> saving this section will broadcast your updated user profile to the
Nostr relays (if you have never set your relays on Nostr, astral will use astral's default relay
list to set and broadcast your set Nostr relays at the same time). all fields in the profile are
completely optional, you don't need to set a profile at all to use Nostr.
<ul>
<li><strong>name</strong> non-unique username that will be displayed accross Nostr</li>
<li><strong>about</strong> share a little about yourself</li>
<li><strong>picture</strong> image url for the picture you would like displayed as your Nostr
profile picture</li>
<li><strong>NIP-05 Identifier</strong> meant to be a unique, human readable identifier for
Nostr (read more <a href='https://github.com/nostr-protocol/nips/blob/master/05.md' target='_blank'>here</a>)
</li>
</ul>
</li>
<li><strong>preferences</strong> saving this section will update the look of astral for this browser.
this section is not synced to Nostr.</li>
<li><strong>relays</strong> saving this section will broadcast your updated relay list to the
Nostr relays.</li>
</ul>
</li>
</ol>
</q-card-section>
</q-expansion-item>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'BaseInformation',
})
</script>

View File

@ -0,0 +1,119 @@
<template>
<div style='border: 3px double var(--q-accent); padding: .5rem; margin: .3rem;'>
<div class='flex row no-wrap justify-between full-width'>
<div style='font-size: .8rem;' :style='showQr ? "" : "width: 60%"' class='flex column'>
<div class='text-bold' style='font-size: 1.1rem;'>lightning invoice</div>
<div v-if='description'>desc: {{description}}</div>
<div>amount: {{amount ? `${amount} sats` : 'none specified'}}</div>
<div>created date: {{dateUTC(created)}}</div>
<div>created time: {{ timeUTC(created)}}</div>
<div v-if='expires'>expires date: {{dateUTC(expires)}}</div>
<div v-if='expires'>expires time: {{ timeUTC(expires)}}</div>
<div class='flex row no-wrap justify-start q-pt-xs' style='gap: .5rem;'>
<BaseButtonCopy :button-text='request' button-label='copy invoice' outline class='q-pr-xs' @click.stop='showQr=false'/>
<q-btn label='show qr' icon='qr_code_2' outline size='sm' dense unelevated class='q-pr-xs' @click.stop='renderQr'/>
</div>
</div>
<div class='flex column' style='font-size: .7rem; padding: .4rem;' :style='showQr ? "" : "width: 40%"'>
<div v-if='!showQr' class='break-word-wrap' style='overflow-y: auto; max-height: 170px;'>{{request}}</div>
<img v-show='showQr' ref='qr' style='object-fit: cover; min-height: 200px; min-width: 200px;'/>
</div>
</div>
</div>
</template>
<script>
import helpersMixin from '../utils/mixin'
import BaseButtonCopy from '../components/BaseButtonCopy'
import qrcodegen from 'nayuki-qr-code-generator'
// import {toSvgString} from 'awesome-qr-code-generator'
export default {
name: 'BaseInvoice',
mixins: [helpersMixin],
props: {invoice: {type: Object, required: true}},
components: {
BaseButtonCopy
},
data() {
return {
showQr: false,
// links: [],
}
},
computed: {
includesAmount() {
if (this.invoice.sections[2].name === 'separator') return false
return true
},
amount() {
if (this.includesAmount) return this.invoice.sections[2].value / 1000
return null
},
description() {
if (this.includesAmount) return this.invoice.sections[6].value
return this.invoice.sections[5].value
},
created() {
if (this.includesAmount) return parseInt(this.invoice.sections[4].value)
return parseInt(this.invoice.sections[3].value)
},
expires() {
let expiresValue = this.includesAmount ? parseInt(this.invoice.sections[8].value) : parseInt(this.invoice.sections[7].value)
let parsed = parseInt(expiresValue)
if (isNaN(parsed)) return null
return this.created + parsed
},
request() {
return this.invoice.paymentRequest
}
},
mounted() {
// console.log('invoice', this.invoice)
},
methods: {
renderQr(e) {
this.showQr = true
let qr = qrcodegen.QrCode.encodeText(this.request, qrcodegen.QrCode.Ecc.MEDIUM)
let svgSrc = this.toSvgString(qr, 4)
this.$refs.qr.src = svgSrc
console.log('qr', qr)
},
toSvgString(qr, border) {
let lightColor = '#FFFFFF'
let darkColor = '#000000'
if (border < 0)
throw new RangeError('Border must be non-negative')
let parts = []
for (let y = 0; y < qr.size; y++) {
for (let x = 0; x < qr.size; x++) {
if (qr.getModule(x, y))
parts.push(`M${x + border},${y + border}h1v1h-1z`)
}
}
let svg = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 ${qr.size + border * 2} ${qr.size + border * 2}" stroke="none">
<rect width="100%" height="100%" fill="${lightColor}"/>
<path d="${parts.join(' ')}" fill="${darkColor}"/>
</svg>
`
let blob = new Blob([svg], {type: 'image/svg+xml'})
let url = URL.createObjectURL(blob)
return url
}
},
}
</script>
<style lang='scss' scoped>
.q-btn {
opacity: .7;
transition: all .3s ease-in-out;
}
.q-btn:hover {
opacity: 1;
}
</style>

View File

@ -0,0 +1,358 @@
<template>
<div style='border: 1px solid var(--q-accent); border-radius: .5rem;'>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>learn about Nostr</span>
</template>
<q-card-section>
<p>
the <a href='https://github.com/fiatjaf/nostr' target='_blank'>Nostr</a> protocol is
a decentralized and censorship resistant distributed information network that relies on clients and relays.
relays store user data. clients communicate with the relays to save and fetch said user data.
</p>
<ul>
<li>users choose which relays to store their data on, meaning no one centralized entity has the power
to remove your data from the network (so it is recommended to use multiple relays)</li>
<li>users choose which clients to use, meaning no one centralized website can stop you from accessing the network</li>
<li>any client can be used with any relay, meaning users can choose their relays and client independently</li>
</ul>
<p>
astral is a client for Nostr. while astral is implementing a social media usecase of Nostr, the possibilities of Nostr are endless.
<a href='https://jesterui.github.io/#/game/jester1y7du0yq7uzfzhxr2xgd64lmchfpf54evjsa59ff4f2mgh83h79rs9k7ffq'>Jester</a>
is a beta peer to peer chess client implemented over Nostr.
</p>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>do I need a key?</span>
</template>
<q-card-section>
<p>
if you would just like to look around you do not need a key pair, simply close this
dialog popup. however, if you want to post or save your profile and follows you will
need to create a key pair if you don't have one already.
</p>
<p>
if you decide to just look around and want to login at a later time hit the set user <q-icon name='login'/>
button in the user menu.
</p>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<!-- <div class='flex row justify-between'> -->
<template #header>
<span class='full-width text-bold'>what is a key pair?</span>
</template>
<q-card-section>
in order to participate in the Nostr network you will need to a public key and private key pair.
this key pair can be used in any Nostr client to login.
<q-list bordered padding class="q-mt-sm q-mb-sm">
<q-item>
<q-item-section>
<q-item-label>public key</q-item-label>
<q-item-label caption>
publicly known unique ID associated with your user on the Nostr
network. can be shared freely. others can see your posts or
follow you using only your public key.
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label>private key</q-item-label>
<q-item-label caption>
<strong>KEEP THIS SECRET!</strong> secret key used to sign for
(or unlock) your public key. all content from your user public
key will need a signature derived from your private key before
being relayed. if a bad actor discovers your private key they
can impersonate you on Nostr network and see your encrypted dms.
</q-item-label>
</q-item-section>
</q-item>
</q-list>
<p>
your public key is created from your private key via a one way hash
function, meaning:
<ul>
<li>your public key can be calculated from your private
key - which is why you only need to enter your private key for astral to
know your public key</li>
<li>your private key cannot be calculated from your public key - which
is why you can freely share your public key without compromising your
private key</li>
</ul>
</p>
<p>
through the magic of cryptograpic functions, this private and public key pair
allows you to sign for Nostr events, which could represent a post, profile
settings, or follows list, in a cryptographically verifiable manner (similar to
how you sign for a bitcoin transaction)
</p>
<p>
you may see your keys displayed in a couple different key formats depending on
the Nostr client you use, please don't be alarmed. they both represent the same
byte data, they just use different encoding methods to be human readable. the
'npub' (for <span style='text-decoration: underline;'>N</span>ostr
<span style='text-decoration: underline;'>pub</span>lic key) and 'nsec'
(for <span style='text-decoration: underline;'>N</span>ostr
<span style='text-decoration: underline;'>sec</span>ret key) format that Damus
uses is preferable over the hex format that astral uses because there is a visual
indicator preventing the user from mixing up their public and private key. astral
will adopt this format in the future.
</p>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>why shouldn't I enter my private key?</span>
</template>
<q-card-section>
<p>
anytime you enter your private key into any Nostr client, you are trusting that client to:
<ol>
<li>not store your private key</li>
<li>not have any vulnerabilities that a bad actor can exploit to steal your private key</li>
</ol>
while I can promise you that astral does not store your private key (it is stored locally in your
browser and is NEVER sent to back to astral) and that I am doing my best to prevent vulnerabilities,
it is still recommeneded that you <strong>DO NOT TRUST ME</strong>.
</p>
<p>
fortunately, on desktop devices Nostr provides an easy way for you to sign into Nostr clients without
ever providing the client with your private key via browser extensions like
<a href='https://getalby.com/' target='_blank'>getAlby</a> or <a href='https://github.com/fiatjaf/nos2x#install' target='_blank'>nos2x</a>.
these browser extensions will store your private key locally
in your browser. when the client needs to send an event or decrypt your messages (ie. use your
private key), it will employ your browser extension to do the necessary cryptographic functions.
see <strong>how to get a key pair?</strong> section below for instructions on how to use these
browser extensions.
</p>
<p>
unfortunately, on mobile devices you cannot use browser extensions so it will be
necessary to enter your private key for now. this is a known and important issue
that is being worked on, and there should be some better solutions for private key
management on mobile devices coming soon.
</p>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>how to get a key pair?</span>
</template>
<q-card-section>
<p>
you can use any method you want to generate your keys, as long as they
conform to the <a href='https://github.com/nostr-protocol/nips/blob/master/01.md' target='_blank'>Nostr NIP-01</a>
specification.
<ul>
<li>if you are on a desktop device the recommeneded option will be using a browser extension like
<a href='https://getalby.com/' target='_blank'>getAlby</a> or <a href='https://github.com/fiatjaf/nos2x#install' target='_blank'>nos2x</a>.
(see <strong>why shouldn't I enter my private key?</strong> section above)</li>
<li>if you are on a mobile device the easiest option will be using astral</li>
<li>if you are a technical user who is concerned about key generation security you can try the local option</li>
</ul>
</p>
<p>
if you already have a key pair and just want to enter it into a browser extension, follow the
instructions in the applicable sub-section below but enter your private key instead of hitting
'generate' (with nos2x make sure to hit save afterword)
</p>
<p>
choose your key generation method:
</p>
<div style='border: 1px solid var(--q-accent); border-radius: .5rem;'>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>use astral</span>
</template>
<q-card-section>
<ol>
<li>hit <strong>GENERATE KEYS</strong> button below in the key input</li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>use nos2x</span>
</template>
<q-card-section>
<ol>
<li>install <a href='https://github.com/fiatjaf/nos2x#install' target='_blank'>nos2x</a> browser extension</li>
<li>open the nos2x extension options page (usually in dropdown when clicking on extension's icon at the top of your broswer)</li>
<li>hit <strong>generate</strong> button</li>
<li>refresh astral.ninja page and hit the <strong>USE PUBLIC KEY FROM EXTENSION</strong> option that should appear in the key input</li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<div class='full-width'>
<span class='text-bold'>use getAlby </span>
<span>longer setup than nos2x but is password protected, comes with a lightning wallet and is better maintained</span>
</div>
</template>
<q-card-section>
<ol>
<li>install <a href='https://getalby.com/' target='_blank'>getAlby</a> browser extension</li>
<li>complete alby setup (when it asks <strong>Do you have a lightning wallet?</strong> just hit <strong>Alby Wallet</strong>
if you do not have one)</li>
<li>open the getAlby extension options page (usually in dropdown when clicking on extension's icon at the top of your broswer)</li>
<li>hit the <strong>Settings</strong> tab</li>
<li>scroll down to the <strong>Nostr</strong> section and hit <strong>Generate</strong></li>
<li>refresh astral.ninja page and hit the <strong>USE PUBLIC KEY FROM EXTENSION</strong> option that should appear in the key input</li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='generateKeys'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>use local machine</span>
</template>
<q-card-section>
<ol>
<li>open terminal</li>
<li>enter the command <pre>openssl rand -hex 32</pre></li>
<li>proceed to <strong>how to use astral?</strong> section below</li>
</ol>
</q-card-section>
</q-expansion-item>
</div>
</q-card-section>
</q-expansion-item>
<div style='max-height: 0; border-top: 1px solid var(--q-accent)'/>
<q-expansion-item
dense
dense-toggle
group='info'
class="no-padding full-width"
header-class='items-center'
>
<template #header>
<span class='full-width text-bold'>how to use astral?</span>
</template>
<q-card-section>
<ol>
<li>
<strong>enter your key</strong> if you don't have a key pair, or you have a key pair that you want to enter into
a Nostr browser extension, see <strong>how to get a key pair?</strong> section above.
</li>
<li>
<strong>choose bootstrap relays (optional)</strong> this section will only appear once a valid key has
been entered. if you are using a brand new key you don't need
to worry about this. if you are using an existing key and don't typically use any of the selected
relays, make sure to include a relay you typically use or astral may not be able to find your user.
the bootstrap relays are NOT used as your user's relay list, astral only uses these to find your
user's information and settings when loading your Nostr account.
</li>
<li>
<strong>hit PROCEED</strong> astral will login and attempt to find your user information. please
be patient as it can take a few minutes to completely sync with Nostr relays.
</li>
<li>
<strong>BACK UP YOUR KEYS!!!</strong> once you are taken to the settings page you should get a
popup displaying your Nostr keys. make sure to make a backup of your Nostr keys and keep it
somewhere safe. if you lose your Nostr keys you lose access to your Nostr user identity. there is no
astral customer service to reset your password.
</li>
<li>
<strong>edit settings</strong> hit the <strong>EDIT</strong> button for which ever section you would
like to edit.
<ul>
<li><strong>profile</strong> saving this section will broadcast your updated user profile to the
Nostr relays (if you have never set your relays on Nostr, astral will use astral's default relay
list to set and broadcast your set Nostr relays at the same time). all fields in the profile are
completely optional, you don't need to set a profile at all to use Nostr.
<ul>
<li><strong>name</strong> non-unique username that will be displayed accross Nostr</li>
<li><strong>about</strong> share a little about yourself</li>
<li><strong>picture</strong> image url for the picture you would like displayed as your Nostr
profile picture</li>
<li><strong>NIP-05 Identifier</strong> meant to be a unique, human readable identifier for
Nostr (read more <a href='https://github.com/nostr-protocol/nips/blob/master/05.md' target='_blank'>here</a>)
</li>
</ul>
</li>
<li><strong>preferences</strong> saving this section will update the look of astral for this browser.
this section is not synced to Nostr.</li>
<li><strong>relays</strong> saving this section will broadcast your updated relay list to the
Nostr relays.</li>
</ul>
</li>
</ol>
</q-card-section>
</q-expansion-item>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'BaseIssues',
})
</script>

View File

@ -14,6 +14,7 @@
label='show full post'
@click.stop="expand"
/>
<BaseInvoice v-if='invoice' :invoice='invoice'/>
<!-- <div v-if='links.length'>
<BaseLinkPreview v-for='(link, idx) of links' :key='idx' :url='link' />
</div> -->
@ -27,7 +28,8 @@ import deflist from 'markdown-it-deflist'
import taskLists from 'markdown-it-task-lists'
import emoji from 'markdown-it-emoji'
import helpersMixin from '../utils/mixin'
// import BaseLinkPreview from 'components/BaseLinkPreview.vue'
import * as bolt11Parser from 'light-bolt11-decoder'
import BaseInvoice from 'components/BaseInvoice.vue'
const md = MarkdownIt({
html: false,
@ -56,6 +58,7 @@ md.use(subscript)
trimmed.endsWith('.png') ||
trimmed.endsWith('.jpeg') ||
trimmed.endsWith('.jpg') ||
trimmed.endsWith('.svg') ||
trimmed.endsWith('.mp4') ||
trimmed.endsWith('.webm') ||
trimmed.endsWith('.ogg')
@ -76,9 +79,10 @@ md.use(subscript)
trimmed.endsWith('.gif') ||
trimmed.endsWith('.png') ||
trimmed.endsWith('.jpeg') ||
trimmed.endsWith('.jpg')
trimmed.endsWith('.jpg') ||
trimmed.endsWith('.svg')
) {
return `<img src="${src}" crossorigin async style="max-width: 90%; max-height: 50vh;">`
return `<img src="${src}" crossorigin async loading='lazy' style="max-width: 90%; max-height: 50vh;">`
} else if (
trimmed.endsWith('.mp4') ||
trimmed.endsWith('.webm') ||
@ -178,14 +182,15 @@ md.linkify
export default {
name: 'BaseMarkdown',
mixins: [helpersMixin],
emits: ['expand'],
// components: {
// BaseLinkPreview,
// },
emits: ['expand', 'resized'],
components: {
BaseInvoice,
},
data() {
return {
html: '',
invoice: null,
// links: [],
}
},
@ -201,6 +206,23 @@ export default {
},
},
computed: {
parsedContent() {
const bolt11Regex = /\b(?<i>(lnbc|LNBC)[0-9a-zA-Z]*1[0-9a-zA-Z]+)\b/g
const replacer = (match, index) => {
try {
this.invoice = bolt11Parser.decode(match)
return ''
} catch (e) {
console.log('invoice parsing error', e)
return match
}
}
let replacedContent = this.content.replace(bolt11Regex, replacer)
return replacedContent
}
},
mounted() {
this.render()
},
@ -211,7 +233,7 @@ export default {
methods: {
render() {
this.html = md.render(this.content) + this.$refs.append.innerHTML
this.html = md.render(this.parsedContent) + this.$refs.append.innerHTML
// md.render(this.$refs.src.innerHTML) + this.$refs.append.innerHTML
this.$refs.html.querySelectorAll('img').forEach(img => {
img.addEventListener('click', (e) => {
@ -221,6 +243,10 @@ export default {
} else if (document.exitFullscreen) {
document.exitFullscreen()
}
this.$emit('resized')
})
img.addEventListener('load', (e) => {
this.$emit('resized')
})
})
// if (this.links.length === 0) {

View File

@ -85,9 +85,15 @@
</a>
</q-item-label>
</div>
<BaseMarkdown v-if="event.kind === 1" :content='event.interpolated.text' :long-form='isLongForm' @expand='isLongForm = !isLongForm' />
<BaseRelayRecommend v-else-if="event.kind === 2" :url="event.content" />
<BaseMarkdown v-else> {{ cleanEvent }} </BaseMarkdown>
<BaseMarkdown
v-if="event.kind === 1"
:content='event.interpolated.text'
:long-form='isLongForm'
@expand='isLongForm = !isLongForm'
@resized='calcConnectorValues(10)'
/>
<BaseRelayRecommend v-else-if="event.kind === 2" :url="sanitize(event.content)" />
<pre v-else> {{ cleanEvent }} </pre>
<div
v-if='!isEmbeded && (isQuote || isRepost)'
class='reposts flex column'
@ -224,6 +230,7 @@ import BaseButtonInfo from 'components/BaseButtonInfo.vue'
import BaseButtonCopy from 'components/BaseButtonCopy.vue'
import BaseMarkdown from 'components/BaseMarkdown.vue'
import BaseRelayRecommend from 'components/BaseRelayRecommend.vue'
import * as DOMPurify from 'dompurify'
export default defineComponent({
name: 'BasePost',
@ -323,19 +330,18 @@ export default defineComponent({
},
cleanEvent() {
return JSON.stringify(cleanEvent(this.event), null, '\n\t')
return this.sanitize(JSON.stringify(cleanEvent(this.event), null, 2))
},
},
mounted() {
// console.log('mounted')
if (!this.isEmbeded && (this.isQuote || this.isRepost)) {
if (!Array.isArray(this.reposts)) this.reposts = []
this.processTaggedEvents(this.mentionEvents, this.reposts)
}
this.calcConnectorValues()
this.$emit('mounted')
this.isLongForm = this.event.interpolated.text.length > 500
this.isLongForm = this.event.content.length > 600
},
activated() {
@ -406,6 +412,10 @@ export default defineComponent({
console.log('post reply threads add-event', event)
this.$emit('add-event', event)
},
sanitize(text) {
return DOMPurify.sanitize(text)
}
}
})
</script>

View File

@ -14,6 +14,7 @@
import helpersMixin from '../utils/mixin'
import {cleanEvent} from '../utils/event'
import BaseButtonCopy from 'components/BaseButtonCopy.vue'
import * as DOMPurify from 'dompurify'
export default {
name: 'BaseRawEvent',
@ -25,20 +26,16 @@ export default {
computed: {
cleaned() {
if (Array.isArray(this.event)) return this.event.map(cleanEvent)
return cleanEvent(this.event)
console.log('cleaned', JSON.parse(DOMPurify.sanitize(JSON.stringify(this.event))))
if (Array.isArray(this.event)) return this.event.map(event => cleanEvent(this.sanitize(event)))
return cleanEvent(this.sanitize(this.event))
}
},
// methods: {
// copyText(defaultText) {
// console.log('defaultText: ', defaultText)
// let selection = window.getSelection().toString()
// console.log('selection: ', selection)
// if (selection) {
// return selection
// } else return defaultText
// },
// }
methods: {
sanitize(event) {
return JSON.parse(DOMPurify.sanitize(JSON.stringify(this.event)))
},
}
}
</script>

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)" crossorigin async/>
<img :src="$store.getters.avatar(pubkey)" loading='lazy' crossorigin async/>
<div :class='alignRight ? "icon-right" : "icon-left"' class='q-pt-xs'>
<BaseButtonNIP05
v-if='showVerified'

View File

@ -3,88 +3,22 @@
<!-- <div v-if="showKeyInitialization"> -->
<q-card class='relative-position full-width'>
<q-btn icon='close' size='md' flat round class='absolute-top-right z-top' @click='$emit("look-around")'/>
<h1 class="text-h6 q-pr-md">welcome to astral</h1>
<q-expansion-item
dense
dense-toggle
expand-icon='info'
expand-icon='help'
expanded-icon='expand_less'
class="intro no-padding"
class="intro no-padding full-width items-center"
header-class='items-center'
>
<!-- <div class='flex row justify-between'> -->
<template #header>
<h1 class="text-h6 q-pr-md">welcome to astral</h1>
<span class='full-width'>click here to learn about Nostr, your keys, and how to use astral</span>
</template>
<!-- </div> -->
<p>
astral is a social media client for the <a href='https://github.com/fiatjaf/nostr' target='_blank'>Nostr</a> protocol,
a decentralized and censorship resistant distributed information network that relies on clients and relays.
relays store user data. clients communicate with the relays to save and fetch said user data. users choose
which relays to store their data on, meaning no one centralized entity has the power to remove your data from the
network (so it is recommended to use multiple relays). users choose which clients to use, meaning no one centralized
website can stop you from accessing the network. any client can be used with any relay, meaming users can choose
their relays and client independently.
</p>
<p>
while astral is implementing a social media usecase of Nostr, the possibilities of Nostr are endless.
<a href='https://jesterui.github.io/#/game/jester1y7du0yq7uzfzhxr2xgd64lmchfpf54evjsa59ff4f2mgh83h79rs9k7ffq'>Jester</a>
is a beta peer to peer chess client implemented over Nostr.
</p>
</q-expansion-item>
<q-expansion-item
dense
dense-toggle
expand-icon='info'
expanded-icon='expand_less'
class="intro no-padding"
>
<!-- <div class='flex row justify-between'> -->
<template #header>
<h2 class="text-subtitle2 q-pr-md">enter your key</h2>
</template>
<q-card-section class="intro no-padding">
in order to participate in the Nostr network you will need to a public key and private key pair:
<q-list bordered padding class="q-mt-sm q-mb-sm">
<q-item>
<q-item-section>
<q-item-label>public key</q-item-label>
<q-item-label caption>
publicly known unique ID associated with your user on the Nostr
network. can be shared freely. others can see your posts or
follow you using only your public key.
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label>private key</q-item-label>
<q-item-label caption>
<strong>KEEP THIS SECRET!</strong> secret key used to sign for
(or unlock) your public key. all content from your user public
key will need a signature derived from your private key before
being relayed. if a bad actor discovers your private key they
can impersonate you on Nostr network.
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-card-section class="onboard no-padding">
<p>
if you don't have a Nostr key pair you can either generate a new key
pair below or close this popup to just look around. if you would like
to login at a later time hit the login <q-icon name='login'/> button.
</p>
<!-- <q-btn-group spread unelevated class='q-gutter-xl'>
<q-btn size="sm" outline @click="generate" color="primary">
generate
</q-btn>
<q-btn size="sm" outline color="primary" @click='$emit("look-around")'>
look around
</q-btn>
</q-btn-group> -->
</q-card-section>
<BaseInformation/>
<span style='padding: .2rem 0 0 .2rem;'>note: after login this same information can be found in
the <strong>faq</strong> section at the bottom of the settings page</span>
</q-expansion-item>
<h2 class="text-subtitle2 q-pr-md">enter your key</h2>
<q-form @submit="proceed">
<q-card-section class="key-entry no-padding">
<q-btn-group spread unelevated>
@ -163,9 +97,9 @@
</template>
</q-input>
</q-card-section>
<div v-if='isBeck32Key(key)'>
<!-- <div v-if='isBeck32Key(key)'>
{{ hexKey }}
</div>
</div> -->
</q-form>
<q-expansion-item
v-if='isKeyValid'
@ -241,6 +175,7 @@ import { validateWords } from 'nostr-tools/nip06'
import { generatePrivateKey } from 'nostr-tools'
import { decode } from 'bech32-buffer'
import BaseSelectMultiple from 'components/BaseSelectMultiple.vue'
import BaseInformation from 'components/BaseInformation.vue'
export default defineComponent({
name: 'TheKeyInitializationDialog',
@ -249,6 +184,7 @@ export default defineComponent({
components: {
BaseSelectMultiple,
BaseInformation,
},
setup() {

View File

@ -40,11 +40,11 @@
class='menu-item'
:dense='compactMode'
:style='compactMode ? "" : "min-height: 2.75rem;"'
:active="$route.name === item.title"
:active="($route.name === item.title || $route.path.split('/')[1] === item.title)"
active-class=''
@click='(event) => handleClick(event, item)'
:key='item.title'
:class="($route.name === item.title ? 'menu-item-active text-accent ' : '') +
:class="(($route.name === item.title || $route.path.split('/')[1] === item.title) ? 'menu-item-active text-accent ' : '') +
(compactMode ? 'no-margin no-padding col' : 'self-end q-px-none')"
>
<q-item-section v-if='!compactMode' class='gt-sm text-uppercase' style='font-size: 1rem;'>

View File

@ -85,6 +85,7 @@ export default {
replies: 'replies',
profile: 'profile',
relays: 'relays',
faq: 'faq',
users: 'users',
nip05Maintainer: 'NIP05 maintainer',
inactiveRelays: 'inactive relays',

View File

@ -2,7 +2,7 @@
<q-layout>
<link v-if='!updatingFont' id='font-link' rel="stylesheet" :href="`https://fonts.googleapis.com/css2?family=${googleFontsName}`" crossorigin/>
<q-dialog v-if='!$store.state.keys.pub' v-model='initializeKeys' persistent>
<TheKeyInitializationDialog style='max-height: 85vh' @look-around='lookingAround=true'/>
<TheKeyInitializationDialog style='max-height: 85vh' @look-around='setLookingAroundMode'/>
</q-dialog>
<div id='layout-container' :ripple='false'>
<div id='left-drawer' class='flex justify-end'>
@ -20,8 +20,8 @@
<q-page-container ref='pageContainer'>
<!-- <TheKeyInitializationDialog v-if='!$store.state.keys.pub && !lookingAround' @look-around='lookingAround=true'/> -->
<router-view v-slot="{ Component }">
<keep-alive >
<component :is="Component" :key='$route.path' @scroll-to-rect='scrollToRect' @reply-event='setReplyEvent' @update-font='updateFont'/>
<keep-alive :include='["Feed", "Messages", "Notifications"]'>
<component :is="Component" :key='$route.path' :looking-around='lookingAround' @scroll-to-rect='scrollToRect' @reply-event='setReplyEvent' @update-font='updateFont'/>
</keep-alive>
</router-view>
</q-page-container>
@ -161,12 +161,14 @@ export default defineComponent({
setup () {
const $q = useQuasar()
// const cachedPages = ref(['feed', 'notifications', 'messages'])
return $q
},
data() {
return {
cachedPages: ['Feed', 'Notifications', 'Messages'],
middlePagePos: {},
fabPos: [0, 10],
draggingFab: false,
@ -243,7 +245,7 @@ export default defineComponent({
},
preserveScrollPos(to, from) {
this.middlePagePos[from.fullPath] = getVerticalScrollPosition(this.scrollingContainer)
if (this.cachedPages.map(page => page.toLowerCase()).includes(from.name)) this.middlePagePos[from.fullPath] = getVerticalScrollPosition(this.scrollingContainer)
},
restoreScrollPos(to, from) {
@ -283,11 +285,8 @@ export default defineComponent({
if (this.hasLaunched) {
activateSub()
}
if (this.$store.state.keys.pub) {
this.$store.dispatch('launch')
} else {
this.$store.dispatch('launchWithoutKey')
}
if (this.$store.state.keys.pub) this.$store.dispatch('launch')
else this.$store.dispatch('launchWithoutKey')
this.hasLaunched = true
},
@ -409,6 +408,9 @@ export default defineComponent({
// }
}
// console.log('font', getCssVar('font'), this.googleFontsName)
},
setLookingAroundMode() {
this.lookingAround = true
}
},
})

View File

@ -2,7 +2,7 @@
<q-page ref='page'>
<BaseHeader>{{ $t('thread') }}</BaseHeader>
<div v-if="ancestorsCompiled.length || rootAncestor">
<div ref='ancestors' v-if="ancestorsCompiled.length || rootAncestor">
<BasePostThread :events="ancestorsCompiled" is-ancestors @add-event='addEventAncestors'/>
</div>
@ -17,7 +17,7 @@
<div v-else>
{{ $t('event') }} {{ $route.params.eventId }}
</div>
<BaseRelayList v-if="event?.seen_on?.length" :event='event' class='q-px-sm'/>
<BaseRelayList v-if="event?.seen_on?.length" :event='event' class='q-px-sm'/>
</q-item>
<q-separator color='accent' size='1px'/>
@ -28,7 +28,7 @@
<BasePostThread :events="thread" @add-event='processChildEvent'/>
</div>
</div>
<div style='min-height: 70vh;'/>
<div style='min-height: 30vh;'/>
</q-page>
</template>
@ -86,11 +86,11 @@ export default defineComponent({
}
},
activated() {
mounted() {
this.start()
},
deactivated() {
beforeUnmount() {
this.stop()
},

View File

@ -29,10 +29,10 @@
:label='"load " + unreadFeed[tab].length + " unread"'
@click='loadUnread'
/>
<BasePostThread v-for='(item, index) in feed[tab]' :key='index' :events="item" class='full-width' @add-event='processEvent'/>
<BasePostThread v-for='(item, index) in items' :key='index' :events="item" class='full-width' @add-event='processEvent'/>
<BaseButtonLoadMore
:loading-more='loadingMore'
label='load another day'
:label='items.length === feed[tab].length ? "load another day" : "load 100 more"'
@click='loadMore'
/>
</q-page>
@ -43,7 +43,7 @@ import { defineComponent } from 'vue'
import helpersMixin from '../utils/mixin'
import {addToThread} from '../utils/threads'
import {isValidEvent} from '../utils/event'
import {streamFeed, dbFeed, dbUserFollows} from '../query'
import {dbFeed, dbUserFollows} from '../query'
import BaseButtonLoadMore from 'components/BaseButtonLoadMore.vue'
import { createMetaMixin } from 'quasar'
@ -78,9 +78,23 @@ export default defineComponent({
BaseButtonLoadMore,
},
watch: {
lookingAround(curr, prev) {
if (curr) {
this.loadMore()
}
}
},
props: {
lookingAround: {
type: Boolean,
default: false,
}
},
data() {
return {
listener: null,
reachedEnd: false,
feed: {
follows: [],
@ -88,6 +102,18 @@ export default defineComponent({
AI: [],
bots: []
},
feedCounts: {
follows: 100,
global: 100,
AI: 100,
bots: 100
},
unreadCounts: {
follows: 100,
global: 100,
AI: 100,
bots: 100
},
unreadFeed: {
follows: [],
global: [],
@ -101,20 +127,21 @@ export default defineComponent({
loadingMore: true,
loadingUnread: false,
tab: 'follows',
sub: null,
since: Math.round(Date.now() / 1000) - (1 * 24 * 60 * 60),
since: Math.round(Date.now() / 1000),
profilesUsed: new Set(),
// index: 0,
active: false,
lastLoaded: Math.round(Date.now() / 1000),
refreshInterval: null,
unsubscribe: null,
}
},
computed: {
items() {
if (this.tab === 'follows') return this.feed.follows
if (this.tab === 'global') return this.feed.global
if (this.tab === 'AI') return this.feed.AI
if (this.tab === 'bots') return this.feed.bots
if (this.tab === 'follows') return this.feed.follows.slice(0, this.feedCounts['follows'])
if (this.tab === 'global') return this.feed.global.slice(0, this.feedCounts['global'])
if (this.tab === 'AI') return this.feed.AI.slice(0, this.feedCounts['AI'])
if (this.tab === 'bots') return this.feed.bots.slice(0, this.feedCounts['bots'])
return []
}
},
@ -123,76 +150,68 @@ export default defineComponent({
this.bots = await this.getFollows(this.botTracker)
this.follows = await this.getFollows(this.$store.state.keys.pub)
this.loadMore()
if (this.$store.state.keys.pub) this.loadMore()
else {
this.unsubscribe = this.$store.subscribe((mutation, state) => {
switch (mutation.type) {
case 'setKeys': {
this.loadingMore = true
setTimeout(this.loadMore(), 6)
break
}
}
})
}
if (this.follows.length === 0) {
this.tab = 'global'
}
},
activated() {
// console.log('feed activated', this.index)
// this.$refs.virtualScroll.refresh(this.index)
this.active = true
},
async beforeUnmount() {
if (this.listener) this.listener.cancel()
if (this.sub) this.sub.cancel()
this.sub = null
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
},
deactivated() {
// console.log('feed deactivated', this.index)
this.active = false
if (this.unsubscribe) this.unsubscribe()
},
methods: {
async loadMore() {
this.loadingMore = true
if (this.items.length < this.feed[this.tab].length) {
this.feedCounts[this.tab] += 100
this.loadingMore = false
return
}
let loadedFeed = {}
for (let feed of Object.keys(this.feed)) {
loadedFeed[feed] = []
}
// let timer = setTimeout(() => { this.loadingMore = false }, 1000)
if (this.sub) {
this.since = this.since - (24 * 60 * 60)
this.sub.update(this.since - (24 * 60 * 60))
} else this.sub = await streamFeed(this.since - (24 * 60 * 60), (event) => {
this.processEvent(event, this.unreadFeed)
})
this.since = this.since - (6 * 60 * 60)
let results = await dbFeed(this.since)
if (results) for (let event of results) this.processEvent(event, loadedFeed)
for (let feed of Object.keys(this.feed)) {
this.feed[feed] = this.feed[feed].concat(loadedFeed[feed])
}
this.refreshInterval = setInterval(async () => {
let results = await dbFeed(this.lastLoaded)
if (results) for (let event of results) this.processEvent(event, this.unreadFeed)
for (let feed of Object.keys(this.feed)) {
this.feed[feed] = this.feed[feed].concat(this.unreadFeed[feed])
}
}, 10000)
this.loadingMore = false
// this.sub = await dbStreamFeed(this.since, event => {
// if (!timer) {
// this.processEvent(event, this.feed)
// return
// }
// clearTimeout(timer)
// timer = setTimeout(() => {
// for (let feed of Object.keys(this.feed)) {
// this.feed[feed] = this.feed[feed].concat(loadedFeed[feed])
// }
// timer = null
// this.loadingMore = false
// }, 300)
// this.loadingMore = false
// this.processEvent(event, loadedFeed)
// })
},
loadUnread() {
this.loadingUnread = true
this.feed[this.tab] = this.unreadFeed[this.tab].concat(this.feed[this.tab])
this.unreadFeed[this.tab] = []
this.lastLoaded = Math.round(Date.now() / 1000)
this.loadingUnread = false
},

View File

@ -46,11 +46,11 @@ export default defineComponent({
}
},
activated() {
mounted() {
this.start()
},
deactivated() {
beforeUnmount() {
this.stop()
},
@ -64,9 +64,9 @@ export default defineComponent({
this.processEvent(event)
})
this.sub.hashtagOld = await dbStreamTagKind('hashtag', this.$route.params.hashtagId.toLowerCase(), 1, event => {
this.processEvent(event)
})
// this.sub.hashtagOld = await dbStreamTagKind('hashtag', this.$route.params.hashtagId.toLowerCase(), 1, event => {
// this.processEvent(event)
// })
},
stop() {

View File

@ -43,7 +43,7 @@
<script>
import {dbChats} from '../query'
import {streamMessages} from '../query'
import {listenMessages} from '../query'
import helpersMixin from '../utils/mixin'
import { createMetaMixin } from 'quasar'
@ -79,20 +79,21 @@ export default {
}
},
async activated() {
async mounted() {
console.log('route', this.$route)
this.chats = await dbChats(this.$store.state.keys.pub)
if (this.chats.length === 0) this.noChats = true
this.chats.forEach(({peer}) => this.useProfile(peer))
if (this.allChatsNeverRead) this.chats.forEach(({peer}) => this.$store.commit('haveReadMessage', peer))
this.loading = false
this.sub = await streamMessages(async event => {
this.sub = await listenMessages(async event => {
if (event.pubkey === this.$store.state.keys.pub) return
this.chats = await dbChats(this.$store.state.keys.pub)
this.useProfile(event.pubkey)
})
},
deactivated() {
beforeUnmount() {
if (this.sub) {
this.sub.cancel()
this.sub = null

View File

@ -87,7 +87,7 @@
<script>
import helpersMixin from '../utils/mixin'
import {dbMessages, streamMessages} from '../query'
import {dbMessages, listenMessages} from '../query'
import BaseMessage from 'components/BaseMessage.vue'
import { useQuasar } from 'quasar'
import { createMetaMixin } from 'quasar'
@ -179,14 +179,14 @@ export default {
let newMessages = await dbMessages(
this.$store.state.keys.pub,
this.$route.params.pubkey,
this.$store.state.unreadMessages[this.$route.params.pubkey]
50
)
let newMessagesFiltered = await this.processMessages(newMessages)
this.messages.push(...newMessagesFiltered)
}
this.$store.commit('haveReadMessage', this.$route.params.pubkey)
// this.$store.dispatch('useProfile', {pubkey: this.$route.params.pubkey, request: true})
this.sub = await streamMessages(async event => {
this.sub = await listenMessages(async event => {
let eventUserTags = event.tags
.filter(([t, v]) => t === 'p' && v)
.map(([_, v]) => v)

View File

@ -23,7 +23,7 @@
<script>
import helpersMixin from '../utils/mixin'
import {dbMentions, streamMentions} from '../query'
import {dbMentions, listenMentions} from '../query'
import { createMetaMixin } from 'quasar'
const metaData = {
@ -56,7 +56,7 @@ export default {
async activated() {
if (this.$store.state.unreadNotifications) this.loadNew()
this.sub = streamMentions(this.$store.state.keys.pub, async event => {
this.sub = listenMentions(this.$store.state.keys.pub, async event => {
let loadedNotificationsFiltered = await this.processNotifications([event])
if (loadedNotificationsFiltered.length === 0) return
this.notifications = loadedNotificationsFiltered.concat(this.notifications)

View File

@ -171,11 +171,11 @@ export default defineComponent({
}
},
activated() {
mounted() {
this.start()
},
deactivated() {
beforeUnmount() {
this.stop()
},

View File

@ -189,6 +189,21 @@
</q-form>
</div>
<q-separator color='accent'/>
<q-expansion-item
dense
expand-icon='help'
expanded-icon='expand_less'
class="full-width items-center"
header-class='items-center'
>
<template #header>
<div class="text-bold flex justify-between no-wrap full-width" style='font-size: 1.1rem;'>{{ $t('faq') }}</div>
</template>
<q-card-section>
<BaseInformation/>
</q-card-section>
</q-expansion-item>
<q-separator color='accent'/>
<div class="flex no-wrap section" style='gap: .2rem;'>
@ -204,13 +219,14 @@
<div class="text-lg text-bold tracking-wide leading-relaxed py-2">
Your keys <q-icon name="vpn_key" />
</div>
<p v-if="$store.state.keys.priv">
Make sure you back up your private key!
</p>
<p v-if="$store.state.keys.priv">Make sure you back up your private key!</p>
<p v-else>Your private key is not here!</p>
<div class="mt-1 text-xs">
Posts are published using your private key. Others can see your
posts or follow you using only your public key.
**if you entered a key that starts with 'npub' or 'nsec' these keys will look different.
it is the same key you entered, just converted to a different display format (hex)**
</div>
</q-card-section>
@ -245,6 +261,7 @@ import {dbErase} from '../query'
import { getCssVar, setCssVar } from 'quasar'
import BaseSelect from 'components/BaseSelect.vue'
import BaseSelectMultiple from 'components/BaseSelectMultiple.vue'
import BaseInformation from 'components/BaseInformation.vue'
import { createMetaMixin } from 'quasar'
const metaData = {
@ -265,7 +282,8 @@ export default {
emits: ['update-font'],
components: {
BaseSelect,
BaseSelectMultiple
BaseSelectMultiple,
BaseInformation,
},
data() {
@ -376,12 +394,10 @@ export default {
mounted() {
if (!this.$store.state.keys.pub) this.$router.push('/')
console.log('initUser', this.$route.params.initUser)
if (this.$store.state.keys.pub && this.$route.params.initUser) {
nextTick(() => {
setTimeout(() => {
this.keysDialog = true
console.log('initUser', this.$route.params.initUser)
}, 1000)
})
}
@ -449,6 +465,7 @@ export default {
if (!Object.keys(this.$store.state.relays).length) this.saveRelays()
this.$store.dispatch('setMetadata', this.metadata)
this.editingMetadata = false
},
clonePreferences() {
this.preferences = {}
@ -492,6 +509,7 @@ export default {
return
}
if (this.$store.getters.canSignEventsAutomatically) this.$store.commit('saveRelays', this.relays)
this.editingRelays = false
},
savePreferences() {
// this.loadFont(this.preferences.font)

View File

@ -130,6 +130,13 @@ export function streamFeed(
return stream('streamFeed', [since], callback)
}
export function listenFeed(
since = Math.round(Date.now() / 1000),
callback = () => { }
) {
return stream('listenFeed', [since], callback)
}
export async function dbChats(pubkey) {
return call('dbChats', [pubkey])
}
@ -142,8 +149,8 @@ export async function streamUserMessages(pubkey, callback = () => { }) {
return stream('streamUserMessages', [pubkey], callback)
}
export async function streamMessages(callback = () => { }) {
return stream('streamMessages', [], callback)
export async function listenMessages(callback = () => { }) {
return stream('listenMessages', [], callback)
}
export async function dbEvent(id) {
@ -170,8 +177,8 @@ export async function dbMentions(pubkey, limit = 50, until = Math.round(Date.now
return call('dbMentions', [pubkey, limit, until])
}
export function streamMentions(pubkey, callback = () => { }) {
return stream('streamMentions', [pubkey], callback)
export function listenMentions(pubkey, callback = () => { }) {
return stream('listenMentions', [pubkey], callback)
}
export async function dbUnreadMentionsCount(pubkey, since = Math.round(Date.now() / 1000)) {

View File

@ -6,7 +6,7 @@ import initSqlJs from '@jlongster/sql.js'
import { SQLiteFS } from 'absurd-sql'
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend'
import sqlWasm from '@jlongster/sql.js/dist/sql-wasm.wasm'
import mergebounce from 'mergebounce'
export var channel = new MessageChannel()
relay.setPort(channel)
@ -185,6 +185,12 @@ async function initDb() {
// return true
// }
let debouncedHandleInsertedEvent = mergebounce(
events => { for (let event of events) handleInsertedEvent(event) },
500,
{ 'concatArrays': true, 'promise': true, maxWait: 1500 }
)
function handleInsertedEvent(event) {
event = JSON.parse(event)
for (let id in streams) {
@ -214,7 +220,7 @@ function handleUpdatedEvent(event) {
function createTables(db, output = console.log) {
console.log('creating tables and indexes', db)
db.create_function('handleInsertedEvent', event => {
handleInsertedEvent(event)
debouncedHandleInsertedEvent([event])
})
db.create_function('handleUpdatedEvent', event => {
handleUpdatedEvent(event)
@ -330,6 +336,7 @@ function saveEventsToDb(events, output = console.log, outputTiming = console.log
if (!db) return
if (!active) return
if (saving) return
if (!events.length) return
saving = true
let start = Date.now()
console.debug(`saving ${events.length} events...`)
@ -462,6 +469,19 @@ const methods = {
}
},
listenFeed(since, callback) {
// don't need to open relay sub bc launch already subs the mentions
// also don't need date restriction as db should always be up to date
// and only new messages will be inserted
return {
filter: {
kinds: [1, 2],
since
},
callback,
}
},
dbChats(pubkey) {
let result = queryDb(`
SELECT peer, MAX(last_message) last_message
@ -541,7 +561,7 @@ const methods = {
// }
// },
streamMessages(callback) {
listenMessages(callback) {
// don't need to open relay sub bc launch already subs the mentions
// also don't need date restriction as db should always be up to date
// and only new messages will be inserted
@ -603,7 +623,7 @@ const methods = {
return result.map(row => JSON.parse(row.event))
},
streamMentions(pubkey, callback) {
listenMentions(pubkey, callback) {
// don't need to open relay sub bc launch already subs the mentions
// also don't need date restriction as db should always be up to date
// and only new messages will be inserted
@ -853,7 +873,7 @@ const methods = {
},
prune(user, pubkeys) {
let until = Math.round(Date.now() / 1000) - (10 * 24 * 60 * 60)
let until = Math.round(Date.now() / 1000) - (1 * 24 * 60 * 60)
let pubkeyList = `("${pubkeys.join('","')}")`
let result = queryDb(`
DELETE

View File

@ -1,9 +1,18 @@
import mergebounce from 'mergebounce'
import { relayPool } from 'nostr-tools'
// import {debounce} from 'quasar'
export const pool = relayPool()
let mainUserSub = null
let adhocSub = null
// let mainUserSub = null
// let adhocSub = null
let poolSubs = {}
let relays = {}
let subs = {}
let active = true
let lastSync = 0
let dbWorkerPort = null
pool.onNotice((notice, relay) => {
try {
@ -13,14 +22,15 @@ pool.onNotice((notice, relay) => {
}
})
let relays = {}
let subs = {}
let active = true
let lastSync = 0
let dbWorkerPort = null
let debounceCount = 0
// let debounceTime = Date.now()
// let debouncedEvents = []
// function emitEvents(events) {
// dbWorkerPort.postMessage({ type: 'events', events })
// debouncedEvents = []
// }
// const debouncedEmitEvents = debounce(emitEvents, 1000)
let debouncedEmitEvent = mergebounce(
events => dbWorkerPort.postMessage({ type: 'events', events }),
1000,
@ -29,6 +39,9 @@ let debouncedEmitEvent = mergebounce(
function onEvent(event, relay) {
if (!active) return
// debouncedEvents.push({ event, relay })
// if (debounceCount >= 2000 || Date.now() - debounceTime > 2000) emitEvents(debouncedEvents)
// debouncedEmitEvents(debouncedEvents)
if (debounceCount >= 2000) {
debouncedEmitEvent.flush()
debounceCount = 0
@ -43,7 +56,7 @@ function onEose(url) {
function calcFilter(subName) {
let compiledSubs = Object.values(subs)
.filter(sub => subName === sub.subName)
.filter(sub => subName === (sub.subName || 'adhoc'))
// .map(([_, sub]) => sub)
.reduce((acc, { type, value }) => {
if (type === 'user') {
@ -104,7 +117,8 @@ function calcFilter(subName) {
case 'feed':
return {
since: value,
kinds: [1, 2]
kinds: [1, 2],
limit: 5000
}
case 'event':
return {
@ -117,6 +131,7 @@ function calcFilter(subName) {
}
}
})
console.log('filter', subName, filter)
return filter
}
@ -124,17 +139,22 @@ function cancelSub(id) {
let cancelledSub = subs[id]
delete subs[id]
if (!active) return
if (cancelledSub.subName === 'mainUser') {
if (mainUserSub) {
if (Object.keys(subs).filter(id => cancelledSub.subName === 'mainUser').length === 0) mainUserSub.unsub()
else mainUserSub.sub({filter: calcFilter('mainUser')})
}
} else {
if (adhocSub) {
if (Object.keys(subs).filter(id => !cancelledSub.subName).length === 0) adhocSub.unsub()
else adhocSub.sub({filter: calcFilter()})
}
if (!cancelledSub.subName) cancelledSub.subName = 'adhoc'
if (poolSubs[cancelledSub.subName]) {
if (Object.keys(subs).filter(id => cancelledSub.subName === (subs[id].subName || 'adhoc')).length === 0) poolSubs[cancelledSub.subName].unsub()
else poolSubs[cancelledSub.subName].sub({filter: calcFilter(cancelledSub.subName)})
}
// if (cancelledSub.subName === 'mainUser') {
// if (mainUserSub) {
// if (Object.keys(subs).filter(id => cancelledSub.subName === 'mainUser').length === 0) mainUserSub.unsub()
// else mainUserSub.sub({filter: calcFilter('mainUser')})
// }
// } else {
// if (adhocSub) {
// if (Object.keys(subs).filter(id => !cancelledSub.subName).length === 0) adhocSub.unsub()
// else adhocSub.sub({filter: calcFilter()})
// }
// }
}
const methods = {
@ -145,15 +165,18 @@ const methods = {
activateSub() {
active = true
if (mainUserSub && Object.keys(subs).filter(id => subs[id].subName === 'mainUser').length) mainUserSub.sub({filter: calcFilter('mainUser')})
if (adhocSub && Object.keys(subs).filter(id => !subs[id].subName).length) adhocSub.sub({filter: calcFilter()})
Object.keys(poolSubs).forEach(subName => poolSubs[subName].sub({filter: calcFilter(subName)}))
// if (mainUserSub && Object.keys(subs).filter(id => subs[id].subName === 'mainUser').length) mainUserSub.sub({filter: calcFilter('mainUser')})
// if (adhocSub && Object.keys(subs).filter(id => !subs[id].subName).length) adhocSub.sub({filter: calcFilter()})
return
},
deactivateSub() {
active = false
if (mainUserSub) mainUserSub.unsub()
if (adhocSub) adhocSub.unsub()
Object.keys(poolSubs).forEach(subName => poolSubs[subName].unsub())
// if (mainUserSub) mainUserSub.unsub()
// if (adhocSub) adhocSub.unsub()
// emitEvents(debouncedEvents)
debouncedEmitEvent.flush()
return
},
@ -229,7 +252,7 @@ const methods = {
return {
type: 'feed',
value: Math.max(since, 0),
subName: 'mainUser'
subName: 'mainFeed'
}
},
@ -285,13 +308,16 @@ function handleMessage(ev) {
} else if (sub) {
subs[id] = methods[name](...args)
if (!active) return
if (subs[id].subName === 'mainUser') {
if (mainUserSub) mainUserSub.sub({filter: calcFilter('mainUser')})
else mainUserSub = pool.sub({ cb: onEvent, filter: calcFilter('mainUser')}, 'mainUser', onEose)
} else {
if (adhocSub) adhocSub.sub({filter: calcFilter()})
else adhocSub = pool.sub({ cb: onEvent, filter: calcFilter()}, 'adhoc', onEose)
}
let subName = subs[id].subName || 'adhoc'
if (poolSubs[subName]) poolSubs[subName].sub({filter: calcFilter(subName)})
else poolSubs[subName] = pool.sub({ cb: onEvent, filter: calcFilter(subName)}, subName, onEose)
// if (subs[id].subName === 'mainUser') {
// if (mainUserSub) mainUserSub.sub({filter: calcFilter('mainUser')})
// else mainUserSub = pool.sub({ cb: onEvent, filter: calcFilter('mainUser')}, 'mainUser', onEose)
// } else {
// if (adhocSub) adhocSub.sub({filter: calcFilter()})
// else adhocSub = pool.sub({ cb: onEvent, filter: calcFilter()}, 'adhoc', onEose)
// }
} else {
var reply = { id }
let data

View File

@ -111,7 +111,7 @@ export async function restartMainSubscription(store) {
// setup pool
let relays = Object.keys(store.state.relays).length ? store.state.relays : store.state.defaultRelays
await setRelays(relays, lastUserMainSync - (7 * 24 * 60 * 60))
await setRelays(relays, lastUserMainSync - (1 * 24 * 60 * 60))
// sub to bot tracker follows (to filter out bots in feed)
let botTracker = '29f63b70d8961835b14062b195fc7d84fa810560b36dde0749e4bc084f0f8952'
@ -121,7 +121,7 @@ export async function restartMainSubscription(store) {
}, 60 * 1000)
// sub feed
if (!mainSub.streamFeed) mainSub.streamFeed = await streamFeed(Math.round(Date.now() / 1000) - (5 * 24 * 60 * 60))
if (!mainSub.streamFeed) mainSub.streamFeed = await streamFeed(Math.round(Date.now() / 1000) - (1 * 24 * 60 * 60))
// thats all if no pubkey entered
if (!store.state.keys.pub) return
@ -133,7 +133,7 @@ export async function restartMainSubscription(store) {
let config = LocalStorage.getItem('config') || {}
config.timestamps = {lastUserMainSync: Object.keys(store.state.relays).length ? Math.round(Date.now() / 1000) : 0 }
LocalStorage.set('config', config)
}, 3 * 60 * 1000)
}, 5 * 60 * 1000)
if (store.state.follows.length)
store.state.follows.forEach(pubkey => store.dispatch('useProfile', {pubkey}))
@ -369,25 +369,19 @@ export async function recommendRelay(store, url) {
const debouncedStreamUserProfile = debounce(async (store, users) => {
if (!mainSub.streamUserProfile) {
mainSub.streamUserProfile = await streamUserProfile(
users,
users.slice(0, 500),
async event => {
if (event.pubkey in store.state.profilesCache) return
let metadata = metadataFromEvent(event)
store.commit('addProfileToCache', metadata)
store.dispatch('useNip05', {metadata})
store.dispatch('cancelUseProfile', {pubkey: event.pubkey})
}
)
} else {
if (Object.keys(users).length > 500) {
for (let pubkey of users) {
if (pubkey in store.state.profilesCache) {
store.dispatch('cancelUseProfile', {pubkey})
}
}
}
mainSub.streamUserProfile.update(users)
mainSub.streamUserProfile.update(users.slice(0, 500))
}
}, 100)
}, 3000)
let profilesInUse = {}
export async function useProfile(store, {pubkey}) {
@ -403,18 +397,21 @@ export async function useProfile(store, {pubkey}) {
if (event) {
let metadata = metadataFromEvent(event)
store.dispatch('useNip05', {metadata})
} else {
profilesInUse[pubkey] = profilesInUse[pubkey] || { count: 0, since: Date.now() }
profilesInUse[pubkey].count++
for (let pubkey of Object.keys(profilesInUse)) {
if (profilesInUse[pubkey].since && profilesInUse[pubkey].since < Date.now() - (0.5 * 60 * 1000)) delete profilesInUse[pubkey]
}
if (profilesInUse[pubkey].count === 1) debouncedStreamUserProfile(store, Object.keys(profilesInUse))
}
}
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) {
profilesInUse[pubkey].count--
if (profilesInUse[pubkey].count <= 0) {
delete profilesInUse[pubkey]
debouncedStreamUserProfile(store, Object.keys(profilesInUse))
}

View File

@ -10,6 +10,7 @@ const mainnetDefaultRelays = {
'wss://relay.damus.io': {read: true, write: true},
'wss://nostr.zebedee.cloud': {read: true, write: false},
'wss://relay.nostr.info': {read: true, write: false},
'wss://nostr-pub.semisol.dev': {read: true, write: false},
}
// const default = [
// ['wss://nostr.rocks', {read: true, write: true}],
@ -26,6 +27,7 @@ const mainnetDefaultRelays = {
'wss://nostr.rocks',
'wss://rsslay.fiatjaf.com',
'wss://nostr.zebedee.cloud',
'wss://nostr-2.zebedee.cloud',
'wss://expensive-relay.fiatjaf.com',
'wss://freedom-relay.herokuapp.com/ws',
'wss://nostr-relay.freeberty.net',

View File

@ -1,3 +1,5 @@
import * as DOMPurify from 'dompurify'
export function cleanEvent(event) {
return {
id: event.id,
@ -13,6 +15,7 @@ export function cleanEvent(event) {
export function metadataFromEvent(event) {
try {
let metadata = JSON.parse(event.content)
for (let key of Object.keys(metadata)) metadata[key] = DOMPurify.sanitize(metadata[key])
metadata.pubkey = event.pubkey
return metadata
} catch (_) {

View File

@ -1,7 +1,7 @@
import {dbUserProfile, dbEvent} from '../query'
export function shorten(str) {
return str ? str.slice(0, 5) + '…' + str.slice(-5) : ''
export function shorten(str, number = 5) {
return str ? str.slice(0, number) + '…' + str.slice(-(number)) : ''
}
export function getElementFullHeight(element) {

View File

@ -5,6 +5,7 @@ import {date} from 'quasar'
import { dbStreamEvent } from 'src/query'
import {decrypt} from 'nostr-tools/nip04'
import { decode } from 'bech32-buffer'
import * as DOMPurify from 'dompurify'
const { formatDate } = date
@ -119,8 +120,8 @@ export default {
return `[@${displayName}](/${profile})`
}
}
const hashtagReplacer = (match, hashtag) => {
return `[${match}](/hashtag/${hashtag})`
const hashtagReplacer = (match, startWhitespace, hashtag) => {
return `${startWhitespace}[${match}](/hashtag/${hashtag})`
}
const untaggedProfileReplacer = (match, profile) => {
const displayName = this.$store.getters.displayName(profile)
@ -128,7 +129,7 @@ export default {
}
let replacedText = text.replace(/#\[(\d+)\]/g, replacer)
let hashtagReplacedText = replacedText.replace(/#([\w]{1,63})/g, hashtagReplacer)
let hashtagReplacedText = replacedText.replace(/(?<s>[\s]?)#([\w]{1,63})\b/g, hashtagReplacer)
let untaggedProfileReplacedText = hashtagReplacedText.replace(/@([\w]{64})/g, untaggedProfileReplacer)
let replacedTextFinal = untaggedProfileReplacedText
@ -154,7 +155,7 @@ export default {
})
return {
text: replacedTextFinal,
text: DOMPurify.sanitize(replacedTextFinal),
replyEvents: mentions.replyEvents,
mentionEvents: mentions.mentionEvents
}

View File

@ -2434,6 +2434,11 @@ bech32-buffer@^0.2.0:
resolved "https://registry.npmjs.org/bech32-buffer/-/bech32-buffer-0.2.0.tgz"
integrity sha512-Ez8s82a+Xnn/m3/ftGaQJUSFG4EwNIj9adIJBw8OrHASQsXgvwLSducbcJ9El0rsrwJYJ71yBhC/hZzz3FPSCQ==
bech32@^1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz"
@ -2453,6 +2458,11 @@ bl@^4.0.3, bl@^4.1.0:
inherits "^2.0.4"
readable-stream "^3.4.0"
bn.js@^4.11.8:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
body-parser@1.20.0:
version "1.20.0"
resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz"
@ -2574,7 +2584,7 @@ buffer-xor@^1.0.3:
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==
buffer@>=5:
buffer@>=5, buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
@ -3288,6 +3298,11 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
dependencies:
domelementtype "^2.2.0"
dompurify@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.1.tgz#f9cb1a275fde9af6f2d0a2644ef648dd6847b631"
integrity sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA==
domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz"
@ -4774,6 +4789,15 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
light-bolt11-decoder@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/light-bolt11-decoder/-/light-bolt11-decoder-2.1.0.tgz#46b122790ae0eb415841227eba770eae7303ecf5"
integrity sha512-/AaSWTldx3aaFD7DgMVbX77MVEgLEPI0Zyx4Fjg23u3WpEoc536vz5LTXBU8oXAcrEcyDyn5GpBi2pEYuL351Q==
dependencies:
bech32 "^1.1.2"
bn.js "^4.11.8"
buffer "^6.0.3"
lilconfig@^2.0.3:
version "2.0.6"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz"
@ -5188,6 +5212,11 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
nayuki-qr-code-generator@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/nayuki-qr-code-generator/-/nayuki-qr-code-generator-1.8.0.tgz#dbeebd7b3d2b53119c51a596ad43199ef6798916"
integrity sha512-wnpXdJ+zJ+8QzzGJTEnESaRYfNIUHSgX4ykvYuaomqqUbhJdun3v0sK/39BQjw3on/vf7ujPKIg2V07WejjmLw==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"