mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
* Implement like button
* Implement event deletions * Add global notification stream
This commit is contained in:
parent
4654e36367
commit
e2e072ec02
6
src/components/BaseIcon/icons/like_filled.vue
Normal file
6
src/components/BaseIcon/icons/like_filled.vue
Normal 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>
|
@ -13,9 +13,20 @@ export default defineComponent({
|
||||
default: 'home'
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: this.icon,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconComponent() {
|
||||
return defineAsyncComponent(() => import(`./icons/${this.icon}.vue`))
|
||||
this.name
|
||||
return defineAsyncComponent(() => import(`./icons/${this.name}.vue`))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
icon(icon) {
|
||||
this.name = icon
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<q-menu :offset="[0, 20]" target=".menu-profile" class="menu-profile-popup" >
|
||||
<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">
|
||||
<UserAvatar :pubkey="pk" :clickable="false"/>
|
||||
</div>
|
||||
|
@ -32,24 +32,14 @@
|
||||
<span>{{ formatDate(note.createdAt) }}</span>
|
||||
</p>
|
||||
<div class="post-content-actions">
|
||||
<div class="action-item comment">
|
||||
<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>
|
||||
<PostActions :note="note" flavor="hero" @comment="$refs.editor.focus()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="app.isSignedIn" class="post-reply">
|
||||
<PostEditor
|
||||
:ancestor="note"
|
||||
ref="editor"
|
||||
compact
|
||||
placeholder="Post your reply"
|
||||
/>
|
||||
@ -58,22 +48,21 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseIcon from 'components/BaseIcon'
|
||||
import UserName from 'components/User/UserName.vue'
|
||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
||||
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {useStatStore} from 'src/nostr/store/StatStore'
|
||||
import routerMixin from 'src/router/mixin'
|
||||
import DateUtils from 'src/utils/DateUtils'
|
||||
import PostActions from 'components/Post/PostActions.vue'
|
||||
|
||||
export default {
|
||||
name: 'HeroPost',
|
||||
mixins: [routerMixin],
|
||||
components: {
|
||||
BaseIcon,
|
||||
PostActions,
|
||||
UserName,
|
||||
UserAvatar,
|
||||
PostEditor,
|
||||
@ -93,7 +82,6 @@ export default {
|
||||
return {
|
||||
app: useAppStore(),
|
||||
nostr: useNostrStore(),
|
||||
stat: useStatStore(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -102,9 +90,6 @@ export default {
|
||||
? this.nostr.getNote(this.note.ancestor())
|
||||
: null
|
||||
},
|
||||
stats() {
|
||||
return this.stat.get(this.note.id)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatDate: DateUtils.formatDate,
|
||||
@ -179,60 +164,6 @@ export default {
|
||||
}
|
||||
}
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,31 +31,19 @@
|
||||
<PostRenderer :note="note" />
|
||||
</div>
|
||||
<div v-if="showActions" class="post-content-actions">
|
||||
<div class="action-item comment" @click.stop="app.createPost({ancestor: note.id})">
|
||||
<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>
|
||||
<PostActions :note="note" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseIcon from 'components/BaseIcon'
|
||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||
import UserName from 'components/User/UserName.vue'
|
||||
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
|
||||
import PostActions from 'components/Post/PostActions.vue'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||
import {useStatStore} from 'src/nostr/store/StatStore'
|
||||
import routerMixin from 'src/router/mixin'
|
||||
import DateUtils from 'src/utils/DateUtils'
|
||||
|
||||
@ -63,10 +51,10 @@ export default {
|
||||
name: 'ListPost',
|
||||
mixins: [routerMixin],
|
||||
components: {
|
||||
PostActions,
|
||||
UserAvatar,
|
||||
UserName,
|
||||
PostRenderer,
|
||||
BaseIcon,
|
||||
},
|
||||
props: {
|
||||
note: {
|
||||
@ -94,7 +82,6 @@ export default {
|
||||
return {
|
||||
app: useAppStore(),
|
||||
nostr: useNostrStore(),
|
||||
stat: useStatStore(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -109,9 +96,6 @@ export default {
|
||||
? this.nostr.getNote(this.note.ancestor())
|
||||
: null
|
||||
},
|
||||
stats() {
|
||||
return this.stat.get(this.note.id)
|
||||
},
|
||||
showActions() {
|
||||
return this.actions && this.note.canReply()
|
||||
},
|
||||
@ -119,7 +103,7 @@ export default {
|
||||
// Mention refreshCounter to make this property react to changes to it
|
||||
this.refreshCounter
|
||||
return this.formatPostDate(this.note.createdAt)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatPostDate(timestamp) {
|
||||
@ -229,57 +213,6 @@ export default {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
184
src/components/Post/PostActions.vue
Normal file
184
src/components/Post/PostActions.vue
Normal 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>
|
@ -11,6 +11,7 @@ import {useStatStore} from 'src/nostr/store/StatStore'
|
||||
import {Observable} from 'src/nostr/utils'
|
||||
import {CloseAfter} from 'src/nostr/Relay'
|
||||
import DateUtils from 'src/utils/DateUtils'
|
||||
import {useAppStore} from 'stores/App'
|
||||
|
||||
class Stream extends Observable {
|
||||
constructor(sub) {
|
||||
@ -58,6 +59,11 @@ export const useNostrStore = defineStore('nostr', {
|
||||
// TODO Limit size. Remove oldest.
|
||||
seenBy: {}, // EventId -> {RelayURL -> Timestamp, ...}
|
||||
}),
|
||||
getters: {
|
||||
activeUser() {
|
||||
return useAppStore().myPubkey
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
init() {
|
||||
const settings = useSettingsStore()
|
||||
@ -74,7 +80,15 @@ export const useNostrStore = defineStore('nostr', {
|
||||
this.contactQueue = contactQueue(this.client, 'queue')
|
||||
this.contactQueue.on('event', this.addEvent.bind(this))
|
||||
|
||||
this.userSubs = []
|
||||
|
||||
// Fetch profile info for stored accounts.
|
||||
this.getProfiles(Object.keys(settings.accounts))
|
||||
|
||||
// Start subscription for signed-in user
|
||||
if (this.activeUser) {
|
||||
this.subscribeForUser(this.activeUser)
|
||||
}
|
||||
},
|
||||
|
||||
addEvent(event, relay = null) {
|
||||
@ -94,31 +108,27 @@ export const useNostrStore = defineStore('nostr', {
|
||||
}
|
||||
|
||||
switch (event.kind) {
|
||||
case EventKind.METADATA: {
|
||||
const profiles = useProfileStore()
|
||||
return profiles.addEvent(event)
|
||||
}
|
||||
case EventKind.NOTE: {
|
||||
const notes = useNoteStore()
|
||||
return notes.addEvent(event)
|
||||
}
|
||||
case EventKind.METADATA:
|
||||
return useProfileStore().addEvent(event)
|
||||
case EventKind.NOTE:
|
||||
return useNoteStore().addEvent(event)
|
||||
case EventKind.RELAY:
|
||||
// TODO
|
||||
break
|
||||
case EventKind.CONTACT: {
|
||||
const contacts = useContactStore()
|
||||
return contacts.addEvent(event)
|
||||
}
|
||||
case EventKind.CONTACT:
|
||||
return useContactStore().addEvent(event)
|
||||
case EventKind.DM:
|
||||
// TODO
|
||||
break
|
||||
case EventKind.DELETE:
|
||||
break
|
||||
// TODO metadata, contacts?
|
||||
useNoteStore().deleteEvent(event)
|
||||
return event
|
||||
case EventKind.SHARE:
|
||||
// TODO
|
||||
return event
|
||||
case EventKind.REACTION: {
|
||||
const notes = useNoteStore()
|
||||
return notes.addEvent(event)
|
||||
}
|
||||
case EventKind.REACTION:
|
||||
return useNoteStore().addEvent(event)
|
||||
case EventKind.CHATROOM:
|
||||
break
|
||||
}
|
||||
@ -129,10 +139,65 @@ export const useNostrStore = defineStore('nostr', {
|
||||
},
|
||||
|
||||
publish(event) {
|
||||
this.addEvent(event)
|
||||
// FIXME
|
||||
console.log('publishing', event)
|
||||
this.addEvent(event, {url: '<local>'})
|
||||
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) {
|
||||
const profiles = useProfileStore()
|
||||
const profile = profiles.get(pubkey)
|
||||
@ -162,13 +227,12 @@ export const useNostrStore = defineStore('nostr', {
|
||||
return replies
|
||||
},
|
||||
|
||||
getNotesByAuthor(pubkey, opts = {}) {
|
||||
const order = opts.order || NoteOrder.CREATION_DATE_DESC
|
||||
getPostsByAuthor(pubkey, order = NoteOrder.CREATION_DATE_DESC) {
|
||||
const notes = useNoteStore()
|
||||
return notes.getNotesByAuthor(pubkey, order)
|
||||
return notes.postsByAuthor(pubkey, order)
|
||||
},
|
||||
|
||||
fetchNotesByAuthor(pubkey, limit = 100) {
|
||||
fetchPostsByAuthor(pubkey, limit = 100) {
|
||||
return this.fetch(
|
||||
{
|
||||
kinds: [EventKind.NOTE],
|
||||
@ -209,6 +273,14 @@ export const useNostrStore = defineStore('nostr', {
|
||||
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) {
|
||||
return this.fetch(
|
||||
{
|
||||
@ -221,7 +293,7 @@ export const useNostrStore = defineStore('nostr', {
|
||||
|
||||
getReactionsByAuthor(pubkey, order = NoteOrder.CREATION_DATE_DESC) {
|
||||
const store = useNoteStore()
|
||||
const reactions = store.getReactionsByAuthor(pubkey, order)
|
||||
const reactions = store.reactionsByAuthor(pubkey, order)
|
||||
// TODO fetch?
|
||||
return reactions
|
||||
},
|
||||
@ -284,6 +356,7 @@ export const useNostrStore = defineStore('nostr', {
|
||||
|
||||
sub.on('end', () => {
|
||||
clearTimeout(timer)
|
||||
if (!objects) return
|
||||
const values = Object.values(objects)
|
||||
console.log(`[COMPLETE] stream ${sub.subId} (${values.length})`, filters)
|
||||
stream.emit('init', values)
|
||||
|
@ -6,29 +6,99 @@ export const NoteOrder = {
|
||||
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', {
|
||||
state: () => ({
|
||||
notes: {},
|
||||
replies: {},
|
||||
reactions: {},
|
||||
notesByAuthor: {},
|
||||
reactionsByAuthor: {},
|
||||
postIndex: new NoteIndex(),
|
||||
reactionIndex: new NoteIndex(),
|
||||
}),
|
||||
getters: {
|
||||
get(state) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
return (pubkey, order) => (state.notesByAuthor[pubkey] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
||||
reactionsByAuthor(state) {
|
||||
return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.reactionIndex.getByAuthor(pubkey) || []).sort(order)
|
||||
},
|
||||
getReactionsByAuthor(state) {
|
||||
return (pubkey, order) => (state.reactionsByAuthor[pubkey] || []).sort(order || NoteOrder.CREATION_DATE_ASC)
|
||||
reactionsByTag(state) {
|
||||
return (pubkey, order = NoteOrder.CREATION_DATE_ASC) => (state.reactionIndex.getByTag(pubkey) || []).sort(order)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -37,28 +107,35 @@ export const useNoteStore = defineStore('note', {
|
||||
if (!note) return false
|
||||
|
||||
// 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
|
||||
|
||||
const byAuthor = note.isReaction()
|
||||
? this.reactionsByAuthor
|
||||
: this.notesByAuthor
|
||||
if (!byAuthor[note.author]) {
|
||||
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)
|
||||
if (note.isReaction()) {
|
||||
this.reactionIndex.add(note)
|
||||
} else {
|
||||
this.postIndex.add(note)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {EventKind} from 'src/nostr/model/Event'
|
||||
import Note from 'src/nostr/model/Note'
|
||||
import {useNoteStore} from 'src/nostr/store/NoteStore'
|
||||
|
||||
export const useStatStore = defineStore('stat', {
|
||||
state: () => ({
|
||||
@ -47,6 +48,34 @@ export const useStatStore = defineStore('stat', {
|
||||
stats.shares++
|
||||
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) {
|
||||
|
@ -124,7 +124,7 @@ export default defineComponent({
|
||||
return this.nostr.getProfile(this.pubkey)
|
||||
},
|
||||
notes() {
|
||||
return this.nostr.getNotesByAuthor(this.pubkey)
|
||||
return this.nostr.getPostsByAuthor(this.pubkey)
|
||||
},
|
||||
posts() {
|
||||
return this.notes?.filter(note => !note.hasAncestor()).slice(0, 50)
|
||||
@ -171,7 +171,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.nostr.fetchNotesByAuthor(this.pubkey, 50)
|
||||
this.nostr.fetchPostsByAuthor(this.pubkey, 50)
|
||||
.then(() => this.loadingNotes = false)
|
||||
this.nostr.fetchReactionsByAuthor(this.pubkey, 50)
|
||||
.then(() => this.loadingReactions = false)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {useSettingsStore} from 'stores/Settings'
|
||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
@ -37,6 +38,12 @@ export const useAppStore = defineStore('app', {
|
||||
if (this.isSignedIn) return Promise.resolve(true)
|
||||
return this.signIn(fragment)
|
||||
},
|
||||
switchAccount(pubkey) {
|
||||
const settings = useSettingsStore()
|
||||
settings.switchAccount(pubkey)
|
||||
const nostr = useNostrStore()
|
||||
nostr.subscribeForUser(pubkey)
|
||||
},
|
||||
async createPost(options = {}) {
|
||||
if (!await this.signInIfNeeded()) return
|
||||
this.createPostDialog.params = options
|
||||
|
Loading…
Reference in New Issue
Block a user