mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
update: manage jack effect
This commit is contained in:
parent
5b89a12f39
commit
9bf4e0a6a3
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
390
src/components/BaseInformation.vue
Normal file
390
src/components/BaseInformation.vue
Normal 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>
|
119
src/components/BaseInvoice.vue
Normal file
119
src/components/BaseInvoice.vue
Normal 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>
|
||||
|
358
src/components/BaseIssues.vue
Normal file
358
src/components/BaseIssues.vue
Normal 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>
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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() {
|
||||
|
@ -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;'>
|
||||
|
@ -85,6 +85,7 @@ export default {
|
||||
replies: 'replies',
|
||||
profile: 'profile',
|
||||
relays: 'relays',
|
||||
faq: 'faq',
|
||||
users: 'users',
|
||||
nip05Maintainer: 'NIP05 maintainer',
|
||||
inactiveRelays: 'inactive relays',
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -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()
|
||||
},
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -171,11 +171,11 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
activated() {
|
||||
mounted() {
|
||||
this.start()
|
||||
},
|
||||
|
||||
deactivated() {
|
||||
beforeUnmount() {
|
||||
this.stop()
|
||||
},
|
||||
|
||||
|
@ -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)
|
||||
|
15
src/query.js
15
src/query.js
@ -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)) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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 (_) {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
31
yarn.lock
31
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user