fix renaming and publishlog, add 'retry' button.
This commit is contained in:
parent
b41d607fa0
commit
0652a679a5
|
@ -81,6 +81,12 @@ func saveEvent(w http.ResponseWriter, r *http.Request) {
|
|||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`, evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, evt.Ref, evt.Content, evt.Sig)
|
||||
if err != nil {
|
||||
if strings.Index(err.Error(), "UNIQUE") != -1 {
|
||||
// already exists
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
|
||||
log.Warn().Err(err).Str("pubkey", evt.PubKey).Msg("failed to save")
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
|
|
|
@ -8,14 +8,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form @submit="publishNote">
|
||||
<legend>Publishing to {{ writeServersList }}</legend>
|
||||
<label
|
||||
>Write anything:
|
||||
<input :disabled="publishing" v-model="text" />
|
||||
</label>
|
||||
<button :disabled="publishing">Publish</button>
|
||||
</form>
|
||||
<Publish />
|
||||
<p>Data providers: {{ readServersList }}</p>
|
||||
<div v-if="$store.state.home.size === 0">
|
||||
<p>Didn't find any notes to show.</p>
|
||||
|
@ -25,33 +18,11 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {text: '', publishing: false}
|
||||
},
|
||||
computed: {
|
||||
readServersList() {
|
||||
return JSON.stringify(this.$store.getters.readServers)
|
||||
.replace(/"/g, '')
|
||||
.replace(/,/g, ' ')
|
||||
},
|
||||
writeServersList() {
|
||||
return JSON.stringify(this.$store.getters.writeServers)
|
||||
.replace(/"/g, '')
|
||||
.replace(/,/g, ' ')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async publishNote(ev) {
|
||||
ev.preventDefault()
|
||||
this.publishing = true
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('publishNote', this.text)
|
||||
this.text = ''
|
||||
} catch (err) {
|
||||
console.log('error publishing', err)
|
||||
}
|
||||
this.publishing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,42 @@
|
|||
<template>
|
||||
<article :class="{ours: ours}">
|
||||
<header class="pubkey">
|
||||
<a :href="'#/' + $store.getters.keyName(pubkey)">{{ author }}</a>
|
||||
</header>
|
||||
<p>{{ content }}</p>
|
||||
<em>
|
||||
<a :href="'#/n/' + id"
|
||||
><time :datetime="isoDate(created_at)" :title="isoDate(created_at)"
|
||||
>{{ humanDate(created_at) }}</time
|
||||
></a
|
||||
>
|
||||
<span
|
||||
v-if="ours"
|
||||
class="publish-status"
|
||||
v-for="({time, status}, host) in ($store.state.publishStatus[id] || {})"
|
||||
>
|
||||
{{ host }}: {{ status }} @ {{ time }}
|
||||
</span>
|
||||
</em>
|
||||
<article class="Note" :class="{ours: ours}">
|
||||
<div v-if="reference" class="reference">
|
||||
<Note :note="reference" />
|
||||
</div>
|
||||
<header class="pubkey">
|
||||
<a :href="'#/' + pubkey">{{ $store.getters.keyName(pubkey) }}</a>
|
||||
</header>
|
||||
<p>{{ content }}</p>
|
||||
<footer>
|
||||
<div>
|
||||
<em>
|
||||
<a :href="'#/n/' + id"
|
||||
><time :datetime="isoDate(created_at)" :title="isoDate(created_at)"
|
||||
>{{ humanDate(created_at) }}</time
|
||||
></a
|
||||
>
|
||||
</em>
|
||||
</div>
|
||||
<div>
|
||||
<a @click="referencing = !referencing">Quote/Reply</a>
|
||||
</div>
|
||||
<div v-if="ours">
|
||||
<div
|
||||
class="publish-status"
|
||||
v-for="({time, status}, host) in ($store.state.publishStatus[id] || {})"
|
||||
>
|
||||
{{ host }}: {{ status }} @ {{ humanDate(time) }}
|
||||
<button v-if="status == 'failed'" @click="republish">retry</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<Publish
|
||||
v-if="referencing"
|
||||
:reference="id"
|
||||
@publish="referencing = false"
|
||||
/>
|
||||
<List v-if="isFullPage" :notes="related" />
|
||||
</article>
|
||||
<List v-if="isFullPage" :notes="related" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -30,6 +44,11 @@
|
|||
|
||||
export default {
|
||||
props: ['note'],
|
||||
data() {
|
||||
return {
|
||||
referencing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFullPage() {
|
||||
return !this.note
|
||||
|
@ -71,10 +90,16 @@
|
|||
},
|
||||
methods: {
|
||||
isoDate(d) {
|
||||
if (typeof d === 'number') d = new Date(d * 1000)
|
||||
return d && d.toISOString()
|
||||
},
|
||||
humanDate(d) {
|
||||
if (typeof d === 'number') d = new Date(d * 1000)
|
||||
return d && prettydate.format(d)
|
||||
},
|
||||
republish(e) {
|
||||
e.preventDefault()
|
||||
this.$store.dispatch('broadcastEvent', this.note)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -86,22 +111,28 @@
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
article {
|
||||
margin: 10px 0;
|
||||
.Note {
|
||||
padding: 5px 10px;
|
||||
margin: 5px 0;
|
||||
border: 2px dotted;
|
||||
}
|
||||
article.ours {
|
||||
.Note.ours {
|
||||
background-color: orange;
|
||||
}
|
||||
p {
|
||||
.Note p {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.reference {
|
||||
.Note .reference {
|
||||
background-color: lightblue;
|
||||
}
|
||||
.publish-status {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
.Note .publish-status {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
.Note footer {
|
||||
display: flex;
|
||||
}
|
||||
.Note footer > * {
|
||||
margin: 0 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,11 +27,18 @@
|
|||
</label>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>{{ $store.getters.keyName(pubkey) }}</h1>
|
||||
<button v-if="!myself" @click="editingName = name">Rename</button>
|
||||
<h1>{{ $store.getters.keyName(this.$route.params.key) }}</h1>
|
||||
<button
|
||||
v-if="!myself"
|
||||
@click="editingName = $store.getters.keyName(this.$route.params.key)"
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h2 v-if="name !== metadata.name">{{ metadata.name }}</h2>
|
||||
<h2 v-if="$store.getters.keyName(this.$route.params.key) !== metadata.name">
|
||||
{{ metadata.name }}
|
||||
</h2>
|
||||
<div v-if="!myself">
|
||||
<button v-if="following" @click="unfollow">Unfollow</button>
|
||||
<button v-else @click="follow">Follow</button>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<form @submit="publishNote">
|
||||
<legend>Publishing to {{ writeServersList }}</legend>
|
||||
<label
|
||||
>Write anything:
|
||||
<input :disabled="publishing" v-model="text" />
|
||||
</label>
|
||||
<button :disabled="publishing">Publish</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['reference'],
|
||||
data() {
|
||||
return {
|
||||
text: '',
|
||||
publishing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
writeServersList() {
|
||||
return JSON.stringify(this.$store.getters.writeServers)
|
||||
.replace(/"/g, '')
|
||||
.replace(/,/g, ' ')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async publishNote(ev) {
|
||||
ev.preventDefault()
|
||||
this.publishing = true
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('publishNote', {
|
||||
text: this.text,
|
||||
reference: this.reference
|
||||
})
|
||||
this.text = ''
|
||||
this.$emit('publish')
|
||||
} catch (err) {
|
||||
console.log('error publishing', err)
|
||||
}
|
||||
this.publishing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,6 +1,7 @@
|
|||
// vuex store actions
|
||||
|
||||
import {verifySignature, publishEvent} from './helpers'
|
||||
import {verifySignature, publishEvent, broadcastEvent} from './helpers'
|
||||
import {KIND_METADATA, KIND_TEXTNOTE, KIND_RECOMMENDSERVER} from './constants'
|
||||
import {db} from './globals'
|
||||
|
||||
export default {
|
||||
|
@ -15,8 +16,8 @@ export default {
|
|||
JSON.stringify(discardedSecretKeys)
|
||||
)
|
||||
|
||||
// save new secret key in the database
|
||||
await db.settings.put({key: 'key', value: newKey})
|
||||
// save new secret key
|
||||
localStorage.setItem('key', newKey)
|
||||
|
||||
store.commit('setSecretKey', newKey)
|
||||
},
|
||||
|
@ -27,13 +28,13 @@ export default {
|
|||
}
|
||||
|
||||
switch (event.kind) {
|
||||
case 0: // setMetadata
|
||||
case KIND_METADATA:
|
||||
store.commit('receivedSetMetadata', {event, context})
|
||||
break
|
||||
case 1: // textNote
|
||||
case KIND_TEXTNOTE:
|
||||
store.commit('receivedTextNote', {event, context})
|
||||
break
|
||||
case 2: // recommendServer
|
||||
case KIND_RECOMMENDSERVER:
|
||||
let host = event.content
|
||||
|
||||
try {
|
||||
|
@ -100,12 +101,15 @@ export default {
|
|||
})
|
||||
}
|
||||
},
|
||||
async broadcastEvent(store, event) {
|
||||
await broadcastEvent(event, store.getters.writeServers)
|
||||
},
|
||||
async publishMetadata(store, meta) {
|
||||
let event = await publishEvent(
|
||||
{
|
||||
pubkey: store.getters.pubKeyHex,
|
||||
created_at: Math.round(new Date().getTime() / 1000),
|
||||
kind: 0,
|
||||
kind: KIND_METADATA,
|
||||
content: JSON.stringify(meta)
|
||||
},
|
||||
store.state.key,
|
||||
|
@ -114,12 +118,13 @@ export default {
|
|||
|
||||
store.commit('receivedSetMetadata', {event, context: 'happening'})
|
||||
},
|
||||
async publishNote(store, text) {
|
||||
async publishNote(store, {text, reference}) {
|
||||
let event = await publishEvent(
|
||||
{
|
||||
pubkey: store.getters.pubKeyHex,
|
||||
created_at: Math.round(new Date().getTime() / 1000),
|
||||
kind: 1,
|
||||
reference,
|
||||
kind: KIND_TEXTNOTE,
|
||||
content: text.trim()
|
||||
},
|
||||
store.state.key,
|
||||
|
@ -134,7 +139,7 @@ export default {
|
|||
{
|
||||
pubkey: store.getters.pubKeyHex,
|
||||
created_at: Math.round(new Date().getTime() / 1000),
|
||||
kind: 2,
|
||||
kind: KIND_RECOMMENDSERVER,
|
||||
content: host
|
||||
},
|
||||
store.state.key,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const KIND_METADATA = 0
|
||||
export const KIND_TEXTNOTE = 1
|
||||
export const KIND_RECOMMENDSERVER = 2
|
|
@ -3,18 +3,19 @@ import Dexie from 'dexie'
|
|||
export const db = new Dexie('db')
|
||||
|
||||
db.version(1).stores({
|
||||
settings: 'key', // as in key => value
|
||||
relays: 'host',
|
||||
following: 'pubkey',
|
||||
mynotes: 'id, kind, created_at',
|
||||
cachedmetadata: 'pubkey',
|
||||
publishlog: '++index, id',
|
||||
publishlog: '[id+host]',
|
||||
contactlist: 'pubkey'
|
||||
})
|
||||
|
||||
if (localStorage.getItem('deleted') < '2') {
|
||||
if (localStorage.getItem('deleted') < '3') {
|
||||
db.delete().then(() => {
|
||||
localStorage.setItem('deleted', '2')
|
||||
localStorage.setItem('deleted', '3')
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
window.db = db
|
||||
|
|
|
@ -29,7 +29,7 @@ export function verifySignature(evt) {
|
|||
}
|
||||
}
|
||||
|
||||
export function publishEvent(evt, key, hosts) {
|
||||
export async function publishEvent(evt, key, hosts) {
|
||||
let hash = shajs('sha256').update(serializeEvent(evt)).digest()
|
||||
evt.id = hash.toString('hex')
|
||||
|
||||
|
@ -37,13 +37,12 @@ export function publishEvent(evt, key, hosts) {
|
|||
.sign(new BigInteger(key, 16), hash, makeRandom32())
|
||||
.toString('hex')
|
||||
|
||||
return await broadcastEvent(evt, hosts)
|
||||
}
|
||||
|
||||
export function broadcastEvent(evt, hosts) {
|
||||
hosts.forEach(async host => {
|
||||
if (host.length && host[host.length - 1] === '/') host = host.slice(0, -1)
|
||||
let r = await window.fetch(host + '/save_update', {
|
||||
method: 'POST',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify(evt)
|
||||
})
|
||||
|
||||
let publishLogEntry = {
|
||||
id: evt.id,
|
||||
|
@ -51,11 +50,18 @@ export function publishEvent(evt, key, hosts) {
|
|||
host
|
||||
}
|
||||
|
||||
if (!r.ok) {
|
||||
try {
|
||||
let r = await window.fetch(host + '/save_event', {
|
||||
method: 'POST',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify(evt)
|
||||
})
|
||||
if (!r.ok) throw new Error('error publishing')
|
||||
|
||||
db.publishlog.put({...publishLogEntry, status: 'succeeded'})
|
||||
} catch (err) {
|
||||
console.log(`failed to publish ${evt} to ${host}`)
|
||||
db.publishlog.put({...publishLogEntry, status: 'failed'})
|
||||
} else {
|
||||
db.publishlog.put({...publishLogEntry, status: 'succeeded'})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import Setup from './Setup.html'
|
|||
import Profile from './Profile.html'
|
||||
import Note from './Note.html'
|
||||
import List from './List.html'
|
||||
import Publish from './Publish.html'
|
||||
|
||||
import store from './store'
|
||||
|
||||
|
@ -30,4 +31,5 @@ app.component('Setup', Setup)
|
|||
app.component('Profile', Profile)
|
||||
app.component('Note', Note)
|
||||
app.component('List', List)
|
||||
app.component('Publish', Publish)
|
||||
app.mount('#app')
|
||||
|
|
|
@ -4,9 +4,8 @@ import {pubkeyFromPrivate} from './helpers'
|
|||
import {db} from './globals'
|
||||
|
||||
export default {
|
||||
setInit(state, {relays, key, following, home, metadata, petnames}) {
|
||||
setInit(state, {relays, following, home, metadata, petnames}) {
|
||||
state.relays = relays
|
||||
state.key = key
|
||||
state.following = following.concat(
|
||||
// always be following thyself
|
||||
pubkeyFromPrivate(state.key)
|
||||
|
@ -77,8 +76,11 @@ export default {
|
|||
state.home.set(evt.id + ':' + evt.created_at, evt)
|
||||
}
|
||||
},
|
||||
saveMyOwnNote() {},
|
||||
updatePublishStatus(state, {id, time, host, status}) {
|
||||
if (!(id in state.publishStatus)) state.publishStatus[id] = {}
|
||||
state.publishStatus[id][host] = {time, status}
|
||||
state.publishStatus = {
|
||||
...state.publishStatus,
|
||||
[id]: {...(state.publishStatus[id] || {}), [host]: {time, status}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,12 @@ export default createStore({
|
|||
: []
|
||||
).concat([init, listener, publishStatusLoader]),
|
||||
state() {
|
||||
let secretKey = localStorage.getItem('key')
|
||||
if (!secretKey) {
|
||||
secretKey = makeRandom32().toString('hex')
|
||||
localStorage.setItem('key', secretKey)
|
||||
}
|
||||
|
||||
let relays = [
|
||||
{
|
||||
host: 'https://nostr-relay.bigsun.xyz',
|
||||
|
@ -31,7 +37,7 @@ export default createStore({
|
|||
haveEventSource,
|
||||
session: new Date().getTime() + '' + Math.round(Math.random() * 100000),
|
||||
relays,
|
||||
key: makeRandom32().toString('hex'),
|
||||
key: secretKey,
|
||||
following: [],
|
||||
home: new SortedMap(),
|
||||
metadata: new LRU({maxSize: 100}),
|
||||
|
@ -53,7 +59,8 @@ export default createStore({
|
|||
keyName: state => pubkey =>
|
||||
state.petnames[pubkey] ||
|
||||
(state.metadata.get(pubkey) || {}).name ||
|
||||
pubkey.slice(0, 4) + '…' + pubkey.slice(-4)
|
||||
(pubkey && pubkey.slice(0, 4) + '…' + pubkey.slice(-4)) ||
|
||||
''
|
||||
},
|
||||
mutations,
|
||||
actions
|
||||
|
@ -61,16 +68,6 @@ export default createStore({
|
|||
|
||||
async function init(store) {
|
||||
let data = await Promise.all([
|
||||
db.settings.get('key').then(row => {
|
||||
if (!row) {
|
||||
// use the key we generated on startup and save it
|
||||
db.settings.put({key: 'key', value: store.state.key})
|
||||
return store.state.key
|
||||
} else {
|
||||
// use the key that was stored
|
||||
return row.value
|
||||
}
|
||||
}),
|
||||
db.relays.toArray().then(rls => {
|
||||
// if blank, use hardcoded values
|
||||
if (rls.length === 0) {
|
||||
|
@ -107,19 +104,34 @@ async function init(store) {
|
|||
])
|
||||
|
||||
store.commit('setInit', {
|
||||
key: data[0],
|
||||
relays: data[1],
|
||||
following: data[2],
|
||||
home: data[3],
|
||||
metadata: data[4],
|
||||
petnames: data[5]
|
||||
relays: data[0],
|
||||
following: data[1],
|
||||
home: data[2],
|
||||
metadata: data[3],
|
||||
petnames: data[4]
|
||||
})
|
||||
}
|
||||
|
||||
function publishStatusLoader(store) {
|
||||
db.publishlog.toArray().then(logs => {
|
||||
logs.forEach(({id, time, host, status}) => {
|
||||
if (time < new Date().getTime() / 1000 - 60 * 60 * 24 * 30) {
|
||||
// older than 30 days, delete and ignore
|
||||
db.publishlog.delete([id, host])
|
||||
return
|
||||
}
|
||||
|
||||
store.commit('updatePublishStatus', {id, time, host, status})
|
||||
})
|
||||
})
|
||||
|
||||
db.publishlog.hook('creating', (_, {id, time, host, status}) => {
|
||||
store.commit('updatePublishStatus', {id, time, host, status})
|
||||
})
|
||||
db.publishlog.hook('updating', (mod, _, prev) => {
|
||||
let {id, time, host, status} = {...prev, ...mod}
|
||||
store.commit('updatePublishStatus', {id, time, host, status})
|
||||
})
|
||||
}
|
||||
|
||||
function listener(store) {
|
||||
|
|
Loading…
Reference in New Issue