mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 13:33:22 +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'
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {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)
|
||||||
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user