fix renaming and publishlog, add 'retry' button.

This commit is contained in:
fiatjaf 2020-12-03 13:28:49 -03:00
parent b41d607fa0
commit 0652a679a5
12 changed files with 199 additions and 106 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export const KIND_METADATA = 0
export const KIND_TEXTNOTE = 1
export const KIND_RECOMMENDSERVER = 2

View File

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

View File

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

View File

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

View File

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

View File

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