* Implement like button

* Implement event deletions
* Add global notification stream
This commit is contained in:
styppo 2023-01-20 18:59:39 +00:00
parent 4654e36367
commit e2e072ec02
No known key found for this signature in database
GPG Key ID: 3AAA685C50724C28
11 changed files with 449 additions and 198 deletions

View File

@ -0,0 +1,6 @@
<template>
<svg
viewBox="0 0 256 256"
class="r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-1plcrui r-lrvibr r-1hdv0qi"
><g><path d="M177.536 29.056c-24.438 0-40.864 16.875-49.547 29.12c-8.704-12.267-25.13-29.12-49.557-29.12 C47.733 29.056 20.8 57.75 20.8 90.432c0 68.032 79.499 139.861 107.051 140.373H128v-0.021h0.117 c27.553-0.501 107.062-72.331 107.062-140.341C235.179 57.75 208.256 29.056 177.536 29.056z" /></g></svg>
</template>

View File

@ -13,9 +13,20 @@ export default defineComponent({
default: 'home' default: 'home'
}, },
}, },
data() {
return {
name: this.icon,
}
},
computed: { computed: {
iconComponent() { iconComponent() {
return defineAsyncComponent(() => import(`./icons/${this.icon}.vue`)) this.name
return defineAsyncComponent(() => import(`./icons/${this.name}.vue`))
}
},
watch: {
icon(icon) {
this.name = icon
} }
} }
}) })

View File

@ -17,7 +17,7 @@
</div> </div>
<q-menu :offset="[0, 20]" target=".menu-profile" class="menu-profile-popup" > <q-menu :offset="[0, 20]" target=".menu-profile" class="menu-profile-popup" >
<div> <div>
<div v-for="(_, pk) in settings.accounts" :key="pk" class="popup-header" @click="settings.switchAccount(pk)" v-close-popup> <div v-for="(_, pk) in settings.accounts" :key="pk" class="popup-header" @click="app.switchAccount(pk)" v-close-popup>
<div class="sidebar-profile-pic"> <div class="sidebar-profile-pic">
<UserAvatar :pubkey="pk" :clickable="false"/> <UserAvatar :pubkey="pk" :clickable="false"/>
</div> </div>

View File

@ -32,24 +32,14 @@
<span>{{ formatDate(note.createdAt) }}</span> <span>{{ formatDate(note.createdAt) }}</span>
</p> </p>
<div class="post-content-actions"> <div class="post-content-actions">
<div class="action-item comment"> <PostActions :note="note" flavor="hero" @comment="$refs.editor.focus()" />
<BaseIcon icon="comment" />
<span>{{ stats.comments || '' }}</span>
</div>
<div class="action-item repost">
<BaseIcon icon="repost" />
<span>{{ stats.shares || '' }}</span>
</div>
<div class="action-item like">
<BaseIcon icon="like" />
<span>{{ stats.reactions || '' }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="app.isSignedIn" class="post-reply"> <div v-if="app.isSignedIn" class="post-reply">
<PostEditor <PostEditor
:ancestor="note" :ancestor="note"
ref="editor"
compact compact
placeholder="Post your reply" placeholder="Post your reply"
/> />
@ -58,22 +48,21 @@
</template> </template>
<script> <script>
import BaseIcon from 'components/BaseIcon'
import UserName from 'components/User/UserName.vue' import UserName from 'components/User/UserName.vue'
import UserAvatar from 'components/User/UserAvatar.vue' import UserAvatar from 'components/User/UserAvatar.vue'
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue' import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
import PostEditor from 'components/CreatePost/PostEditor.vue' import PostEditor from 'components/CreatePost/PostEditor.vue'
import {useNostrStore} from 'src/nostr/NostrStore' import {useNostrStore} from 'src/nostr/NostrStore'
import {useAppStore} from 'stores/App' import {useAppStore} from 'stores/App'
import {useStatStore} from 'src/nostr/store/StatStore'
import routerMixin from 'src/router/mixin' import routerMixin from 'src/router/mixin'
import DateUtils from 'src/utils/DateUtils' import DateUtils from 'src/utils/DateUtils'
import PostActions from 'components/Post/PostActions.vue'
export default { export default {
name: 'HeroPost', name: 'HeroPost',
mixins: [routerMixin], mixins: [routerMixin],
components: { components: {
BaseIcon, PostActions,
UserName, UserName,
UserAvatar, UserAvatar,
PostEditor, PostEditor,
@ -93,7 +82,6 @@ export default {
return { return {
app: useAppStore(), app: useAppStore(),
nostr: useNostrStore(), nostr: useNostrStore(),
stat: useStatStore(),
} }
}, },
computed: { computed: {
@ -102,9 +90,6 @@ export default {
? this.nostr.getNote(this.note.ancestor()) ? this.nostr.getNote(this.note.ancestor())
: null : null
}, },
stats() {
return this.stat.get(this.note.id)
},
}, },
methods: { methods: {
formatDate: DateUtils.formatDate, formatDate: DateUtils.formatDate,
@ -179,60 +164,6 @@ export default {
} }
} }
&-actions { &-actions {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 450px;
width: 100%;
padding: .5rem 0;
margin: auto;
.action-item {
display: flex;
align-items: center;
cursor: pointer;
svg {
padding: 8px;
border-radius: 999px;
display: block;
width: 40px;
height: 40px;
fill: $color-light-gray;
}
span {
color: $color-light-gray;
line-height: 40px;
font-weight: bold;
}
&:hover {
&.comment {
svg {
fill: $post-action-blue;
background-color: rgba($color: $post-action-blue, $alpha: 0.2);
}
span {
color: $post-action-blue;
}
}
&.repost {
svg {
fill: $post-action-green;
background-color: rgba($color: $post-action-green, $alpha: 0.2);
}
span {
color: $post-action-green;
}
}
&.like {
svg {
fill: $post-action-red;
background-color: rgba($color: $post-action-red, $alpha: 0.2);
}
span {
color: $post-action-red;
}
}
}
}
} }
} }
} }

View File

@ -31,31 +31,19 @@
<PostRenderer :note="note" /> <PostRenderer :note="note" />
</div> </div>
<div v-if="showActions" class="post-content-actions"> <div v-if="showActions" class="post-content-actions">
<div class="action-item comment" @click.stop="app.createPost({ancestor: note.id})"> <PostActions :note="note" />
<BaseIcon icon="comment" />
<span>{{ stats.comments || '' }}</span>
</div>
<div class="action-item repost" @click.stop>
<BaseIcon icon="repost" />
<span>{{ stats.shares || '' }}</span>
</div>
<div class="action-item like" @click.stop>
<BaseIcon icon="like" />
<span>{{ stats.reactions || '' }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import BaseIcon from 'components/BaseIcon'
import UserAvatar from 'components/User/UserAvatar.vue' import UserAvatar from 'components/User/UserAvatar.vue'
import UserName from 'components/User/UserName.vue' import UserName from 'components/User/UserName.vue'
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue' import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
import PostActions from 'components/Post/PostActions.vue'
import {useAppStore} from 'stores/App' import {useAppStore} from 'stores/App'
import {useNostrStore} from 'src/nostr/NostrStore' import {useNostrStore} from 'src/nostr/NostrStore'
import {useStatStore} from 'src/nostr/store/StatStore'
import routerMixin from 'src/router/mixin' import routerMixin from 'src/router/mixin'
import DateUtils from 'src/utils/DateUtils' import DateUtils from 'src/utils/DateUtils'
@ -63,10 +51,10 @@ export default {
name: 'ListPost', name: 'ListPost',
mixins: [routerMixin], mixins: [routerMixin],
components: { components: {
PostActions,
UserAvatar, UserAvatar,
UserName, UserName,
PostRenderer, PostRenderer,
BaseIcon,
}, },
props: { props: {
note: { note: {
@ -94,7 +82,6 @@ export default {
return { return {
app: useAppStore(), app: useAppStore(),
nostr: useNostrStore(), nostr: useNostrStore(),
stat: useStatStore(),
} }
}, },
data() { data() {
@ -109,9 +96,6 @@ export default {
? this.nostr.getNote(this.note.ancestor()) ? this.nostr.getNote(this.note.ancestor())
: null : null
}, },
stats() {
return this.stat.get(this.note.id)
},
showActions() { showActions() {
return this.actions && this.note.canReply() return this.actions && this.note.canReply()
}, },
@ -119,7 +103,7 @@ export default {
// Mention refreshCounter to make this property react to changes to it // Mention refreshCounter to make this property react to changes to it
this.refreshCounter this.refreshCounter
return this.formatPostDate(this.note.createdAt) return this.formatPostDate(this.note.createdAt)
} },
}, },
methods: { methods: {
formatPostDate(timestamp) { formatPostDate(timestamp) {
@ -229,57 +213,6 @@ export default {
margin-bottom: .5rem; margin-bottom: .5rem;
} }
&-actions { &-actions {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 450px;
width: calc(100% + 9px);
margin-left: -9px;
.action-item {
display: flex;
align-items: center;
cursor: pointer;
svg {
padding: 8px;
border-radius: 999px;
display: block;
width: 36px;
height: 36px;
fill: $color-light-gray;
}
span {
color: $color-light-gray;
}
&:hover {
&.comment {
svg {
fill: $post-action-blue;
background-color: rgba($color: $post-action-blue, $alpha: 0.2);
}
span {
color: $post-action-blue;
}
}
&.repost {
svg {
fill: $post-action-green;
background-color: rgba($color: $post-action-green, $alpha: 0.2);
}
span {
color: $post-action-green;
}
}
&.like {
svg {
fill: $post-action-red;
background-color: rgba($color: $post-action-red, $alpha: 0.2);
}
span {
color: $post-action-red;
}
}
}
}
} }
} }
} }

View File

@ -0,0 +1,184 @@
<template>
<div class="post-actions" :class="flavor">
<div class="action-item comment" @click.stop="comment">
<BaseIcon icon="comment" />
<span>{{ stats.comments || '' }}</span>
</div>
<div class="action-item repost" @click.stop="repost">
<BaseIcon icon="repost" />
<span>{{ stats.shares || '' }}</span>
</div>
<div class="action-item like" :class="{active: liked}" @click.stop="like">
<BaseIcon :icon="liked ? 'like_filled' : 'like'" />
<span>{{ stats.reactions || '' }}</span>
</div>
</div>
</template>
<script>
import BaseIcon from 'components/BaseIcon/index.vue'
import {useNostrStore} from 'src/nostr/NostrStore'
import {useStatStore} from 'src/nostr/store/StatStore'
import {useAppStore} from 'stores/App'
import EventBuilder from 'src/nostr/EventBuilder'
export default {
name: 'PostActions',
components: {BaseIcon},
emits: ['comment', 'repost'],
props: {
note: {
type: Object,
required: true,
},
flavor: {
type: String,
default: 'list',
}
},
setup() {
return {
app: useAppStore(),
stat: useStatStore(),
nostr: useNostrStore()
}
},
computed: {
stats() {
return this.stat.get(this.note.id)
},
ourReactions() {
return this.nostr.getOurReactionsTo(this.note.id)
},
liked() {
return this.ourReactions.length > 0
},
},
methods: {
comment() {
if (this.flavor === 'list') {
this.app.createPost({ancestor: this.note.id})
} else {
this.$emit('comment')
}
},
repost() {
if (this.flavor === 'list') {
// TODO
//this.app.createPost({ancestor: this.note.id})
} else {
this.$emit('repost')
}
},
like() {
return !this.liked ? this.publishLike() : this.deleteLike()
},
async publishLike() {
const event = EventBuilder.reaction(this.note, this.app.myPubkey).build()
if (!await this.app.signEvent(event)) return
this.nostr.publish(event)
},
async deleteLike() {
const ids = this.ourReactions.map(r => r.id)
const event = EventBuilder.delete(this.app.myPubkey, ids).build()
if (!await this.app.signEvent(event)) return
this.nostr.publish(event)
},
},
}
</script>
<style lang="scss" scoped>
@import "assets/theme/colors.scss";
@import "assets/variables.scss";
.post-actions {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 490px;
width: 100%;
margin: auto;
padding: .5rem 0 .5rem 16px;
&.list {
width: calc(100% + 9px);
margin-left: -9px;
padding: 0;
.action-item {
min-width: 56px;
svg {
width: 36px;
height: 36px;
}
}
}
.action-item {
display: flex;
align-items: center;
cursor: pointer;
min-width: 60px;
transition: 120ms ease;
svg {
padding: 8px;
border-radius: 999px;
display: block;
width: 40px;
height: 40px;
fill: $color-light-gray;
transition: 120ms ease;
}
span {
color: $color-light-gray;
}
&.active, &:hover {
&.comment {
svg {
fill: $post-action-blue;
}
span {
color: $post-action-blue;
}
}
&.repost {
svg {
fill: $post-action-green;
}
span {
color: $post-action-green;
}
}
&.like {
svg {
fill: $post-action-red;
}
span {
color: $post-action-red;
}
}
}
&:hover {
&.comment {
svg {
background-color: rgba($color: $post-action-blue, $alpha: 0.2);
}
}
&.repost {
svg {
background-color: rgba($color: $post-action-green, $alpha: 0.2);
}
}
&.like {
svg {
background-color: rgba($color: $post-action-red, $alpha: 0.2);
}
}
}
}
}
@media screen and (max-width: $phone) {
.post-actions {
max-width: unset;
}
}
</style>

View File

@ -11,6 +11,7 @@ import {useStatStore} from 'src/nostr/store/StatStore'
import {Observable} from 'src/nostr/utils' import {Observable} from 'src/nostr/utils'
import {CloseAfter} from 'src/nostr/Relay' import {CloseAfter} from 'src/nostr/Relay'
import DateUtils from 'src/utils/DateUtils' import DateUtils from 'src/utils/DateUtils'
import {useAppStore} from 'stores/App'
class Stream extends Observable { class Stream extends Observable {
constructor(sub) { constructor(sub) {
@ -58,6 +59,11 @@ export const useNostrStore = defineStore('nostr', {
// TODO Limit size. Remove oldest. // TODO Limit size. Remove oldest.
seenBy: {}, // EventId -> {RelayURL -> Timestamp, ...} seenBy: {}, // EventId -> {RelayURL -> Timestamp, ...}
}), }),
getters: {
activeUser() {
return useAppStore().myPubkey
}
},
actions: { actions: {
init() { init() {
const settings = useSettingsStore() const settings = useSettingsStore()
@ -74,7 +80,15 @@ export const useNostrStore = defineStore('nostr', {
this.contactQueue = contactQueue(this.client, 'queue') this.contactQueue = contactQueue(this.client, 'queue')
this.contactQueue.on('event', this.addEvent.bind(this)) this.contactQueue.on('event', this.addEvent.bind(this))
this.userSubs = []
// Fetch profile info for stored accounts.
this.getProfiles(Object.keys(settings.accounts)) this.getProfiles(Object.keys(settings.accounts))
// Start subscription for signed-in user
if (this.activeUser) {
this.subscribeForUser(this.activeUser)
}
}, },
addEvent(event, relay = null) { addEvent(event, relay = null) {
@ -94,31 +108,27 @@ export const useNostrStore = defineStore('nostr', {
} }
switch (event.kind) { switch (event.kind) {
case EventKind.METADATA: { case EventKind.METADATA:
const profiles = useProfileStore() return useProfileStore().addEvent(event)
return profiles.addEvent(event) case EventKind.NOTE:
} return useNoteStore().addEvent(event)
case EventKind.NOTE: {
const notes = useNoteStore()
return notes.addEvent(event)
}
case EventKind.RELAY: case EventKind.RELAY:
// TODO
break break
case EventKind.CONTACT: { case EventKind.CONTACT:
const contacts = useContactStore() return useContactStore().addEvent(event)
return contacts.addEvent(event)
}
case EventKind.DM: case EventKind.DM:
// TODO
break break
case EventKind.DELETE: case EventKind.DELETE:
break // TODO metadata, contacts?
useNoteStore().deleteEvent(event)
return event
case EventKind.SHARE: case EventKind.SHARE:
// TODO // TODO
return event return event
case EventKind.REACTION: { case EventKind.REACTION:
const notes = useNoteStore() return useNoteStore().addEvent(event)
return notes.addEvent(event)
}
case EventKind.CHATROOM: case EventKind.CHATROOM:
break break
} }
@ -129,10 +139,65 @@ export const useNostrStore = defineStore('nostr', {
}, },
publish(event) { publish(event) {
this.addEvent(event) // FIXME
console.log('publishing', event)
this.addEvent(event, {url: '<local>'})
return this.client.publish(event) return this.client.publish(event)
}, },
subscribeForUser(pubkey) {
this.unsubscribeForUser()
// Fetch our metadata once.
this.getProfile(pubkey)
this.getContacts(pubkey)
// Fetch our recent reactions once.
this.fetch({
kinds: [EventKind.REACTION],
authors: [pubkey],
limit: 50,
})
const subs = []
// Subscribe to events created by us.
const subMeta = this.client.subscribe({
kinds: [EventKind.METADATA, EventKind.CONTACT, EventKind.REACTION, EventKind.SHARE],
authors: [pubkey],
limit: 0,
}, `user:${pubkey}`)
subMeta.on('event', this.addEvent.bind(this))
subs.push(subMeta)
// Subscribe to events tagging us
const subTags = this.client.subscribe({
kinds: [EventKind.NOTE, EventKind.REACTION, EventKind.SHARE],
'#p': [pubkey],
limit: 100,
}, `notifications:${pubkey}`)
subTags.on('event', event => {
console.log('got notificaiton', event)
// this.addEvent.bind(this)
this.addEvent(event)
})
subs.push(subTags)
this.userSubs = subs
},
unsubscribeForUser() {
for (const sub of this.userSubs) {
sub.close()
}
this.userSubs = []
},
getNotifications(pubkey) {
const notes = useNoteStore()
return notes.notesByTag(pubkey, NoteOrder.CREATION_DATE_DESC)
},
getProfile(pubkey) { getProfile(pubkey) {
const profiles = useProfileStore() const profiles = useProfileStore()
const profile = profiles.get(pubkey) const profile = profiles.get(pubkey)
@ -162,13 +227,12 @@ export const useNostrStore = defineStore('nostr', {
return replies return replies
}, },
getNotesByAuthor(pubkey, opts = {}) { getPostsByAuthor(pubkey, order = NoteOrder.CREATION_DATE_DESC) {
const order = opts.order || NoteOrder.CREATION_DATE_DESC
const notes = useNoteStore() const notes = useNoteStore()
return notes.getNotesByAuthor(pubkey, order) return notes.postsByAuthor(pubkey, order)
}, },
fetchNotesByAuthor(pubkey, limit = 100) { fetchPostsByAuthor(pubkey, limit = 100) {
return this.fetch( return this.fetch(
{ {
kinds: [EventKind.NOTE], kinds: [EventKind.NOTE],
@ -209,6 +273,14 @@ export const useNostrStore = defineStore('nostr', {
return reactions return reactions
}, },
getOurReactionsTo(id, order = NoteOrder.CREATION_DATE_DESC) {
if (!this.activeUser) return []
const store = useNoteStore()
return store
.reactionsTo(id, order)
.filter(reaction => reaction.author === this.activeUser)
},
fetchReactionsTo(id, limit = 500) { fetchReactionsTo(id, limit = 500) {
return this.fetch( return this.fetch(
{ {
@ -221,7 +293,7 @@ export const useNostrStore = defineStore('nostr', {
getReactionsByAuthor(pubkey, order = NoteOrder.CREATION_DATE_DESC) { getReactionsByAuthor(pubkey, order = NoteOrder.CREATION_DATE_DESC) {
const store = useNoteStore() const store = useNoteStore()
const reactions = store.getReactionsByAuthor(pubkey, order) const reactions = store.reactionsByAuthor(pubkey, order)
// TODO fetch? // TODO fetch?
return reactions return reactions
}, },
@ -284,6 +356,7 @@ export const useNostrStore = defineStore('nostr', {
sub.on('end', () => { sub.on('end', () => {
clearTimeout(timer) clearTimeout(timer)
if (!objects) return
const values = Object.values(objects) const values = Object.values(objects)
console.log(`[COMPLETE] stream ${sub.subId} (${values.length})`, filters) console.log(`[COMPLETE] stream ${sub.subId} (${values.length})`, filters)
stream.emit('init', values) stream.emit('init', values)

View File

@ -6,29 +6,99 @@ export const NoteOrder = {
CREATION_DATE_DESC: (a, b) => b.createdAt - a.createdAt, CREATION_DATE_DESC: (a, b) => b.createdAt - a.createdAt,
} }
class NoteIndex {
constructor() {
this.byAuthor = {}
this.byAncestor = {}
this.byTag = {}
}
add(note) {
NoteIndex.addToIndex(this.byAuthor, note.author, note)
if (note.hasAncestor()) {
NoteIndex.addToIndex(this.byAncestor, note.ancestor(), note)
}
for (const pubkey of note.pubkeyRefs()) {
NoteIndex.addToIndex(this.byTag, pubkey, note)
}
}
remove(note) {
NoteIndex.removeFromIndex(this.byAuthor, note.author, note.id)
NoteIndex.removeFromIndex(this.byAncestor, note.ancestor(), note.id)
for (const pubkey of note.pubkeyRefs()) {
NoteIndex.removeFromIndex(this.byTag, pubkey, note.id)
}
}
static addToIndex(index, key, note) {
if (!index[key]) {
index[key] = []
}
index[key].push(note)
}
static removeFromIndex(index, key, noteId) {
const values = index[key]
if (!values) return
const idx = values.findIndex(note => note.id === noteId)
if (idx < 0) return
values.splice(idx, 1)
if (!values.length) delete index[key]
}
getByAuthor(pubkey) {
return this.byAuthor[pubkey]
}
getByAncestor(id) {
return this.byAncestor[id]
}
getByTag(pubkey) {
return this.byTag[pubkey]
}
}
export const useNoteStore = defineStore('note', { export const useNoteStore = defineStore('note', {
state: () => ({ state: () => ({
notes: {}, notes: {},
replies: {}, postIndex: new NoteIndex(),
reactions: {}, reactionIndex: new NoteIndex(),
notesByAuthor: {},
reactionsByAuthor: {},
}), }),
getters: { getters: {
get(state) { get(state) {
return id => state.notes[id] return id => state.notes[id]
}, },
notesByAuthor(state) {
return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.postIndex.getByAuthor(pubkey) || [])
.concat((state.reactionIndex.getByAuthor(pubkey) || []))
.sort(order)
},
notesByTag(state) {
return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.postIndex.getByTag(pubkey) || [])
.concat(state.reactionIndex.getByTag(pubkey))
.sort(order)
},
repliesTo(state) { repliesTo(state) {
return (id, order) => (state.replies[id] || []).sort(order || NoteOrder.CREATION_DATE_ASC) return (id, order = NoteOrder.CREATION_DATE_ASC) => (state.postIndex.getByAncestor(id) || []).sort(order)
},
postsByAuthor(state) {
return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.postIndex.getByAuthor(pubkey) || []).sort(order)
},
postsByTag(state) {
return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.postIndex.getByTag(pubkey) || []).sort(order)
}, },
reactionsTo(state) { reactionsTo(state) {
return (id, order) => (state.reactions[id] || []).sort(order || NoteOrder.CREATION_DATE_ASC) return (id, order = NoteOrder.CREATION_DATE_ASC) => (state.reactionIndex.getByAncestor(id) || []).sort(order)
}, },
getNotesByAuthor(state) { reactionsByAuthor(state) {
return (pubkey, order) => (state.notesByAuthor[pubkey] || []).sort(order || NoteOrder.CREATION_DATE_ASC) return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.reactionIndex.getByAuthor(pubkey) || []).sort(order)
}, },
getReactionsByAuthor(state) { reactionsByTag(state) {
return (pubkey, order) => (state.reactionsByAuthor[pubkey] || []).sort(order || NoteOrder.CREATION_DATE_ASC) return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.reactionIndex.getByTag(pubkey) || []).sort(order)
}, },
}, },
actions: { actions: {
@ -37,28 +107,35 @@ export const useNoteStore = defineStore('note', {
if (!note) return false if (!note) return false
// Skip if note already exists // Skip if note already exists
if (this.notes[note.id]) return this.notes[note.id] if (this.notes[note.id]) {
return this.notes[note.id]
}
this.notes[note.id] = note this.notes[note.id] = note
const byAuthor = note.isReaction() if (note.isReaction()) {
? this.reactionsByAuthor this.reactionIndex.add(note)
: this.notesByAuthor } else {
if (!byAuthor[note.author]) { this.postIndex.add(note)
byAuthor[note.author] = []
}
byAuthor[note.author].push(note)
if (note.hasAncestor()) {
const map = note.isReaction()
? this.reactions
: this.replies
if (!map[note.ancestor()]) {
map[note.ancestor()] = []
}
map[note.ancestor()].push(note)
} }
return this.notes[note.id] return this.notes[note.id]
},
deleteEvent(event) {
for (const id of event.eventRefs()) {
this.deleteNote(id, event.pubkey)
}
},
deleteNote(id, owner) {
const note = this.get(id)
if (!note) return
if (note.author !== owner) return
if (note.isReaction()) {
this.reactionIndex.remove(note)
} else {
this.postIndex.remove(note)
}
delete this.notes[id]
} }
} }
}) })

View File

@ -1,6 +1,7 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {EventKind} from 'src/nostr/model/Event' import {EventKind} from 'src/nostr/model/Event'
import Note from 'src/nostr/model/Note' import Note from 'src/nostr/model/Note'
import {useNoteStore} from 'src/nostr/store/NoteStore'
export const useStatStore = defineStore('stat', { export const useStatStore = defineStore('stat', {
state: () => ({ state: () => ({
@ -47,6 +48,34 @@ export const useStatStore = defineStore('stat', {
stats.shares++ stats.shares++
break break
} }
case EventKind.DELETE:
this.deleteEvent(event)
break
}
},
deleteEvent(event) {
// FIXME Shares are not correctly decremented yet as they are not yet stored in the note store
for (const id of event.eventRefs()) {
this.deleteNote(id, event.pubkey)
}
},
deleteNote(id, owner) {
const notes = useNoteStore()
const note = notes.get(id)
if (!note) return
if (note.author !== owner) return
if (note.isReaction()) {
const stats = this.getOrInit(note.ancestor())
stats.reactions--
} else if (note.isRepostOrTag()) {
const stats = this.getOrInit(note.ancestor())
stats.shares--
} else {
for (const eventId of note.eventRefs()) {
const stats = this.getOrInit(eventId)
stats.comments--
}
} }
}, },
getOrInit(id) { getOrInit(id) {

View File

@ -124,7 +124,7 @@ export default defineComponent({
return this.nostr.getProfile(this.pubkey) return this.nostr.getProfile(this.pubkey)
}, },
notes() { notes() {
return this.nostr.getNotesByAuthor(this.pubkey) return this.nostr.getPostsByAuthor(this.pubkey)
}, },
posts() { posts() {
return this.notes?.filter(note => !note.hasAncestor()).slice(0, 50) return this.notes?.filter(note => !note.hasAncestor()).slice(0, 50)
@ -171,7 +171,7 @@ export default defineComponent({
}, },
}, },
mounted() { mounted() {
this.nostr.fetchNotesByAuthor(this.pubkey, 50) this.nostr.fetchPostsByAuthor(this.pubkey, 50)
.then(() => this.loadingNotes = false) .then(() => this.loadingNotes = false)
this.nostr.fetchReactionsByAuthor(this.pubkey, 50) this.nostr.fetchReactionsByAuthor(this.pubkey, 50)
.then(() => this.loadingReactions = false) .then(() => this.loadingReactions = false)

View File

@ -1,5 +1,6 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {useSettingsStore} from 'stores/Settings' import {useSettingsStore} from 'stores/Settings'
import {useNostrStore} from 'src/nostr/NostrStore'
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: () => ({ state: () => ({
@ -37,6 +38,12 @@ export const useAppStore = defineStore('app', {
if (this.isSignedIn) return Promise.resolve(true) if (this.isSignedIn) return Promise.resolve(true)
return this.signIn(fragment) return this.signIn(fragment)
}, },
switchAccount(pubkey) {
const settings = useSettingsStore()
settings.switchAccount(pubkey)
const nostr = useNostrStore()
nostr.subscribeForUser(pubkey)
},
async createPost(options = {}) { async createPost(options = {}) {
if (!await this.signInIfNeeded()) return if (!await this.signInIfNeeded()) return
this.createPostDialog.params = options this.createPostDialog.params = options