mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
* Profile page refinements
* Add basic reaction support
This commit is contained in:
parent
3c66a50046
commit
371c068254
16
src/components/EmptyPlaceholder.vue
Normal file
16
src/components/EmptyPlaceholder.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<p>Nothing here 🤷</p>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EmptyPlaceholder'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
p {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -15,7 +15,7 @@
|
||||
<div class="post-content-header">
|
||||
<p v-if="note.isReply()" class="in-reply-to">
|
||||
Replying to
|
||||
<a @click.stop="linkToProfile(ancestor?.author)">
|
||||
<a @click.stop="goToProfile(ancestor?.author)">
|
||||
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
||||
</a>
|
||||
</p>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="post"
|
||||
:class="{clickable}"
|
||||
@click.stop="clickable && linkToThread(note.id)"
|
||||
@click.stop="clickable && goToThread(note.id)"
|
||||
>
|
||||
<div class="post-author">
|
||||
<div class="connector-top">
|
||||
@ -22,7 +22,7 @@
|
||||
</p>
|
||||
<p v-if="note.isReply()" class="in-reply-to">
|
||||
Replying to
|
||||
<a @click.stop="linkToProfile(ancestor?.author)">
|
||||
<a @click.stop="goToProfile(ancestor?.author)">
|
||||
<UserName v-if="ancestor?.author" :pubkey="ancestor?.author" />
|
||||
</a>
|
||||
</p>
|
||||
@ -31,7 +31,7 @@
|
||||
<BaseMarkdown :content="note.content" />
|
||||
<!-- {{ note.content }}-->
|
||||
</div>
|
||||
<div v-if="actions" class="post-content-actions">
|
||||
<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>
|
||||
@ -109,6 +109,9 @@ export default {
|
||||
shares: 0,
|
||||
}
|
||||
},
|
||||
showActions() {
|
||||
return this.actions && this.note.canReply()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatPostDate(timestamp) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="thread">
|
||||
<ListPost
|
||||
v-for="(note, index) in thread"
|
||||
v-for="(note, index) in filteredThread"
|
||||
:key="note.id"
|
||||
:note="note"
|
||||
:connector-top="thread.length > 1 && index > 0"
|
||||
:connector-bottom="(thread.length > 1 && index < thread.length - 1) || forceBottomConnector"
|
||||
:connector-top="filteredThread.length > 1 && index > 0"
|
||||
:connector-bottom="(filteredThread.length > 1 && index < filteredThread.length - 1) || forceBottomConnector"
|
||||
actions
|
||||
clickable
|
||||
/>
|
||||
@ -30,6 +30,11 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredThread() {
|
||||
return this.thread.filter(note => !!note)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<q-avatar
|
||||
:class="{'cursor-pointer': clickable}"
|
||||
:size="size"
|
||||
@click="clickable && linkToProfile(pubkey)"
|
||||
@click="clickable && goToProfile(pubkey)"
|
||||
>
|
||||
<img
|
||||
v-if="hasAvatar && !avatarFetchFailed"
|
||||
|
@ -3,7 +3,7 @@
|
||||
class="username"
|
||||
:class="{'two-line': twoLine, clickable}"
|
||||
>
|
||||
<a @click="clickable && linkToProfile(pubkey)">
|
||||
<a @click="clickable && goToProfile(pubkey)">
|
||||
<span v-if="profile?.name" class="name">{{ profile.name }}</span>
|
||||
<!-- <q-icon v-if="showFollowing && isFollow" name="visibility" color="secondary">-->
|
||||
<!-- <q-tooltip>-->
|
||||
|
@ -12,7 +12,7 @@
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary : #1976D2;
|
||||
$primary : #ee517d;
|
||||
$secondary : #26A69A;
|
||||
$accent : #9C27B0;
|
||||
|
||||
|
@ -41,7 +41,7 @@ export default class FetchQueue extends Observable {
|
||||
const ids = Object.keys(this.queue).slice(0, this.batchSize)
|
||||
if (!ids.length) return
|
||||
|
||||
console.log(`Fetching ${ids.length}/${Object.keys(this.queue).length} ${this.subId}s`, ids)
|
||||
//console.log(`Fetching ${ids.length}/${Object.keys(this.queue).length} ${this.subId}s`, ids)
|
||||
|
||||
// Remove ids that we have tried too many times.
|
||||
const filteredIds = []
|
||||
|
@ -31,7 +31,7 @@ export default class NostrClient {
|
||||
return this.pool.connectedRelays()
|
||||
}
|
||||
|
||||
subscribe(filters, callback, opts) {
|
||||
subscribe(filters, callback, opts = {}) {
|
||||
let subId
|
||||
if (opts?.subId) {
|
||||
//if (this.subs[opts.subId]) throw new Error(`SubId '${opts.subId}' already exists`)
|
||||
@ -69,7 +69,7 @@ export default class NostrClient {
|
||||
|
||||
const sub = this.subs[subId]
|
||||
if (!sub) {
|
||||
console.warn(`Event for invalid subId ${subId} from ${relay}`)
|
||||
//console.warn(`Event for invalid subId ${subId} from ${relay}`)
|
||||
return
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export default class NostrClient {
|
||||
}
|
||||
|
||||
onEose(relay, subId) {
|
||||
console.log(`[EOSE] from ${relay} for ${subId}`)
|
||||
//console.log(`[EOSE] from ${relay} for ${subId}`)
|
||||
|
||||
const sub = this.subs[subId]
|
||||
if (!sub) return
|
||||
|
@ -1,12 +1,14 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {markRaw} from 'vue'
|
||||
import {EventKind} from 'src/nostr/model/Event'
|
||||
import Note from 'src/nostr/model/Note'
|
||||
import NostrClient from 'src/nostr/NostrClient'
|
||||
import FetchQueue from 'src/nostr/FetchQueue'
|
||||
import {NoteOrder, useNoteStore} from 'src/nostr/store/NoteStore'
|
||||
import {useProfileStore} from 'src/nostr/store/ProfileStore'
|
||||
import {useContactStore} from 'src/nostr/store/ContactStore'
|
||||
import {useSettingsStore} from 'stores/Settings'
|
||||
import {ReactionOrder, useReactionStore} from 'src/nostr/store/ReactionStore'
|
||||
|
||||
export const Feeds = {
|
||||
GLOBAL: {
|
||||
@ -87,8 +89,13 @@ export const useNostrStore = defineStore('nostr', {
|
||||
return profiles.addEvent(event)
|
||||
}
|
||||
case EventKind.NOTE: {
|
||||
const notes = useNoteStore()
|
||||
return notes.addEvent(event)
|
||||
if (Note.isReaction(event)) {
|
||||
const reactions = useReactionStore()
|
||||
return reactions.addEvent(event)
|
||||
} else {
|
||||
const notes = useNoteStore()
|
||||
return notes.addEvent(event)
|
||||
}
|
||||
}
|
||||
case EventKind.RELAY:
|
||||
break
|
||||
@ -102,8 +109,10 @@ export const useNostrStore = defineStore('nostr', {
|
||||
break
|
||||
case EventKind.SHARE:
|
||||
break
|
||||
case EventKind.REACTION:
|
||||
break
|
||||
case EventKind.REACTION: {
|
||||
const reactions = useReactionStore()
|
||||
return reactions.addEvent(event)
|
||||
}
|
||||
case EventKind.CHATROOM:
|
||||
break
|
||||
}
|
||||
@ -181,6 +190,40 @@ export const useNostrStore = defineStore('nostr', {
|
||||
)
|
||||
},
|
||||
|
||||
getReactionsTo(id, order = ReactionOrder.CREATION_DATE_DESC) {
|
||||
const store = useReactionStore()
|
||||
const reactions = store.allByEvent(id, order)
|
||||
// TODO fetch?
|
||||
return reactions
|
||||
},
|
||||
|
||||
fetchReactionsTo(id, limit = 500) {
|
||||
return this.fetchMultiple(
|
||||
{
|
||||
kinds: [EventKind.REACTION],
|
||||
'#e': [id],
|
||||
},
|
||||
limit
|
||||
)
|
||||
},
|
||||
|
||||
getReactionsByAuthor(pubkey, order = ReactionOrder.CREATION_DATE_DESC) {
|
||||
const store = useReactionStore()
|
||||
const reactions = store.allByAuthor(pubkey, order)
|
||||
// TODO fetch?
|
||||
return reactions
|
||||
},
|
||||
|
||||
fetchReactionsByAuthor(pubkey, limit = 500) {
|
||||
return this.fetchMultiple(
|
||||
{
|
||||
kinds: [EventKind.REACTION],
|
||||
authors: [pubkey],
|
||||
},
|
||||
limit
|
||||
)
|
||||
},
|
||||
|
||||
streamThread(rootId, eventCallback, initialFetchCompleteCallback) {
|
||||
return this.streamEvents(
|
||||
{
|
||||
@ -208,8 +251,40 @@ export const useNostrStore = defineStore('nostr', {
|
||||
)
|
||||
},
|
||||
|
||||
cancelStream(subId) {
|
||||
this.client.unsubscribe(subId)
|
||||
streamFullProfile(pubkey) {
|
||||
const handles = []
|
||||
// Everything authored by pubkey
|
||||
handles.push(this.client.subscribe({
|
||||
kinds: [EventKind.NOTE],
|
||||
authors: [pubkey],
|
||||
limit: 200,
|
||||
}, () => {}, { subId: 'foo' }))
|
||||
handles.push(this.client.subscribe({
|
||||
kinds: [EventKind.REACTION],
|
||||
authors: [pubkey],
|
||||
limit: 100,
|
||||
}))
|
||||
handles.push(this.client.subscribe({
|
||||
kinds: [EventKind.METADATA, EventKind.CONTACT],
|
||||
authors: [pubkey],
|
||||
}))
|
||||
// handles.push(this.client.subscribe({
|
||||
// kinds: [EventKind.METADATA, EventKind.NOTE, EventKind.RELAY, EventKind.CONTACT, EventKind.REACTION],
|
||||
// authors: [pubkey],
|
||||
// }))
|
||||
// Followers
|
||||
handles.push(this.client.subscribe({
|
||||
kinds: [EventKind.CONTACT],
|
||||
'#p': [pubkey]
|
||||
}))
|
||||
return handles
|
||||
},
|
||||
|
||||
cancelStream(subIds) {
|
||||
if (!Array.isArray(subIds)) subIds = [subIds]
|
||||
for (const subId of subIds) {
|
||||
this.client.unsubscribe(subId)
|
||||
}
|
||||
},
|
||||
|
||||
fetchEvent(id) {
|
||||
|
@ -29,6 +29,7 @@ export class Relay extends Observable {
|
||||
}
|
||||
|
||||
subscribe(subId, filters) {
|
||||
console.log(`${this} subscribing to ${subId}`, filters)
|
||||
this.socket.send(['REQ', subId, filters])
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {EventKind} from 'src/nostr/model/Event'
|
||||
import {isEmoji} from 'src/utils/utils'
|
||||
|
||||
export default class Note {
|
||||
constructor(id, args) {
|
||||
this.id = id
|
||||
this.kind = args.kind || EventKind.NOTE
|
||||
this.author = args.author || args.pubkey
|
||||
this.createdAt = args.createdAt
|
||||
this.content = args.content
|
||||
@ -13,11 +15,15 @@ export default class Note {
|
||||
}
|
||||
|
||||
static from(event) {
|
||||
console.assert(event.kind === EventKind.NOTE)
|
||||
const content = Note.isReaction(event)
|
||||
? Note.normalizeReactionContent(event.content)
|
||||
: event.content
|
||||
|
||||
return new Note(event.id, {
|
||||
kind: event.kind,
|
||||
author: event.pubkey,
|
||||
createdAt: event.createdAt,
|
||||
content: event.content,
|
||||
content,
|
||||
refs: {
|
||||
events: event.eventRefs(),
|
||||
pubkeys: event.pubkeyRefs(),
|
||||
@ -29,6 +35,10 @@ export default class Note {
|
||||
return !this.refs.events.isEmpty()
|
||||
}
|
||||
|
||||
canReply() {
|
||||
return this.kind === EventKind.NOTE
|
||||
}
|
||||
|
||||
root() {
|
||||
return this.refs.events.root()
|
||||
}
|
||||
@ -36,4 +46,26 @@ export default class Note {
|
||||
ancestor() {
|
||||
return this.refs.events.ancestor()
|
||||
}
|
||||
|
||||
isReaction() {
|
||||
return this.kind === EventKind.REACTION
|
||||
|| (this.isReply() && Note.isReactionContent(this.content))
|
||||
}
|
||||
|
||||
static isReaction(event) {
|
||||
return event.kind === EventKind.REACTION
|
||||
|| (!event.eventRefs().isEmpty() && Note.isReactionContent(event.content))
|
||||
}
|
||||
|
||||
static isReactionContent(content) {
|
||||
return content === '+'
|
||||
|| content === ''
|
||||
|| isEmoji(content)
|
||||
}
|
||||
|
||||
static normalizeReactionContent(content) {
|
||||
return isEmoji(content)
|
||||
? content
|
||||
: '❤️'
|
||||
}
|
||||
}
|
||||
|
75
src/nostr/store/ReactionStore.js
Normal file
75
src/nostr/store/ReactionStore.js
Normal file
@ -0,0 +1,75 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {NoteOrder} from 'src/nostr/store/NoteStore'
|
||||
import Note from 'src/nostr/model/Note'
|
||||
|
||||
// class Reaction {
|
||||
// constructor(id, ancestor, author, createdAt, emoji) {
|
||||
// this.id = id
|
||||
// this.ancestor = ancestor
|
||||
// this.author = author
|
||||
// this.createdAt = createdAt
|
||||
// this.emoji = emoji
|
||||
// }
|
||||
//
|
||||
// static from(event) {
|
||||
// console.assert([EventKind.REACTION, EventKind.NOTE].includes(event.kind))
|
||||
// if (event.eventRefs().isEmpty()) return
|
||||
//
|
||||
// // TODO Normalize content better
|
||||
// const emoji = isEmoji(event.content)
|
||||
// ? event.content
|
||||
// : '❤️'
|
||||
//
|
||||
// return new Reaction(
|
||||
// event.id,
|
||||
// event.eventRefs().ancestor(),
|
||||
// event.pubkey,
|
||||
// event.createdAt,
|
||||
// emoji
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
export const ReactionOrder = NoteOrder
|
||||
|
||||
export const useReactionStore = defineStore('reaction', {
|
||||
state: () => ({
|
||||
reactions: {},
|
||||
byEvent: {},
|
||||
byAuthor: {},
|
||||
}),
|
||||
getters: {
|
||||
get(state) {
|
||||
return id => state.reactions[id]
|
||||
},
|
||||
allByEvent(state) {
|
||||
return (id, order) => (state.byEvent[id] || []).sort(order || ReactionOrder.CREATION_DATE_DESC)
|
||||
},
|
||||
allByAuthor(state) {
|
||||
return (pubkey, order) => (state.byAuthor[pubkey] || []).sort(order || ReactionOrder.CREATION_DATE_DESC)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
addEvent(event) {
|
||||
const note = Note.from(event)
|
||||
if (!note || !note.isReply()) return false
|
||||
|
||||
// Skip if reaction already exists
|
||||
if (this.reactions[note.id]) return this.reactions[note.id]
|
||||
|
||||
this.reactions[note.id] = note
|
||||
|
||||
if (!this.byEvent[note.ancestor()]) {
|
||||
this.byEvent[note.ancestor()] = []
|
||||
}
|
||||
this.byEvent[note.ancestor()].push(note)
|
||||
|
||||
if (!this.byAuthor[note.author]) {
|
||||
this.byAuthor[note.author] = []
|
||||
}
|
||||
this.byAuthor[note.author].push(note)
|
||||
|
||||
return this.reactions[note.id]
|
||||
}
|
||||
}
|
||||
})
|
134
src/pages/profile/Followers.vue
Normal file
134
src/pages/profile/Followers.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<q-page class="followers">
|
||||
<PageHeader :title="profile?.name || hexToBech32(pubkey) || 'Followers'" back-button />
|
||||
|
||||
<div class="profile-tabs">
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
outline
|
||||
align="justify"
|
||||
indicator-color="primary"
|
||||
:breakpoint="0"
|
||||
>
|
||||
<q-tab name="following" label="Following" />
|
||||
<q-tab name="followers" label="Followers" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<q-tab-panels v-model="activeTab" class="profile-tab-panels" animated>
|
||||
<q-tab-panel name="following" class="no-padding">
|
||||
<UserCard
|
||||
v-for="contact in contacts"
|
||||
:key="contact.pubkey"
|
||||
:pubkey="contact.pubkey"
|
||||
clickable
|
||||
/>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="followers" class="no-padding">
|
||||
<UserCard
|
||||
v-for="follower in followers"
|
||||
:key="follower"
|
||||
:pubkey="follower"
|
||||
clickable
|
||||
/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from 'vue'
|
||||
import PageHeader from 'components/PageHeader.vue'
|
||||
import UserCard from 'components/User/UserCard.vue'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||
import {bech32ToHex, hexToBech32} from 'src/utils/utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Profile',
|
||||
components: {
|
||||
UserCard,
|
||||
PageHeader,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
app: useAppStore(),
|
||||
nostr: useNostrStore(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: this.$route.params.tab || 'following',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pubkey() {
|
||||
return bech32ToHex(this.$route.params.pubkey)
|
||||
},
|
||||
profile() {
|
||||
return this.nostr.getProfile(this.pubkey)
|
||||
},
|
||||
contacts() {
|
||||
return this.nostr.getContacts(this.pubkey)
|
||||
},
|
||||
followers() {
|
||||
return this.nostr.getFollowers(this.pubkey)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hexToBech32,
|
||||
},
|
||||
mounted() {
|
||||
this.nostr.fetchFollowers(this.pubkey, 1000)
|
||||
},
|
||||
watch: {
|
||||
activeTab() {
|
||||
this.$router.replace({
|
||||
params: {
|
||||
tab: this.activeTab
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/theme/colors.scss";
|
||||
|
||||
.profile {
|
||||
&-header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
&-avatar {
|
||||
height: 128px;
|
||||
width: 128px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&-content {
|
||||
.followers {
|
||||
a + a {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&-tabs {
|
||||
border-bottom: $border-dark;
|
||||
}
|
||||
&-tab-panels {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.profile-tabs {
|
||||
.q-tab {
|
||||
}
|
||||
}
|
||||
.profile-header-content .username {
|
||||
.name, .pubkey:first-child {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -8,12 +8,12 @@
|
||||
<p class="username"><UserName :pubkey="pubkey" two-line show-verified /></p>
|
||||
<p class="about">{{ profile?.about }}</p>
|
||||
<p class="followers">
|
||||
<span>
|
||||
<a @click="goToFollowers('following')">
|
||||
<strong>{{ contacts?.length || 0 }}</strong> Following
|
||||
</span>
|
||||
<span>
|
||||
</a>
|
||||
<a @click="goToFollowers('followers')">
|
||||
<strong>{{ followers?.length || 0 }}</strong> Followers
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -21,15 +21,14 @@
|
||||
<div class="profile-tabs">
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
dense
|
||||
outline
|
||||
align="left"
|
||||
align="justify"
|
||||
indicator-color="primary"
|
||||
:breakpoint="0"
|
||||
>
|
||||
<q-tab name="posts" label="Posts" />
|
||||
<q-tab name="replies" label="Replies" />
|
||||
<q-tab name="following" label="Following" />
|
||||
<q-tab name="followers" label="Followers" />
|
||||
<q-tab name="reactions" label="Reactions" />
|
||||
<q-tab name="relays" label="Relays" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
@ -43,31 +42,23 @@
|
||||
clickable
|
||||
actions
|
||||
/>
|
||||
<EmptyPlaceholder v-if="!posts?.length" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="replies" class="no-padding">
|
||||
<ListPost
|
||||
v-for="note in replies"
|
||||
:key="note.id"
|
||||
:note="note"
|
||||
clickable
|
||||
actions
|
||||
<Thread
|
||||
v-for="thread in replies"
|
||||
:key="thread[1].id"
|
||||
:thread="thread"
|
||||
/>
|
||||
<EmptyPlaceholder v-if="!replies?.length" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="following" class="no-padding">
|
||||
<UserCard
|
||||
v-for="contact in contacts"
|
||||
:key="contact.pubkey"
|
||||
:pubkey="contact.pubkey"
|
||||
clickable
|
||||
/>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="followers" class="no-padding">
|
||||
<UserCard
|
||||
v-for="follower in followers"
|
||||
:key="follower"
|
||||
:pubkey="follower"
|
||||
clickable
|
||||
<q-tab-panel name="reactions" class="no-padding">
|
||||
<Thread
|
||||
v-for="thread in reactions"
|
||||
:key="thread[1].id"
|
||||
:thread="thread"
|
||||
/>
|
||||
<EmptyPlaceholder v-if="!reactions?.length" />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
@ -79,15 +70,17 @@ import PageHeader from 'components/PageHeader.vue'
|
||||
import UserAvatar from 'components/User/UserAvatar.vue'
|
||||
import UserName from 'components/User/UserName.vue'
|
||||
import ListPost from 'components/Post/ListPost.vue'
|
||||
import UserCard from 'components/User/UserCard.vue'
|
||||
import Thread from 'components/Post/Thread.vue'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||
import {bech32ToHex} from 'src/utils/utils'
|
||||
import {bech32ToHex, hexToBech32} from 'src/utils/utils'
|
||||
import EmptyPlaceholder from 'components/EmptyPlaceholder.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Profile',
|
||||
components: {
|
||||
UserCard,
|
||||
EmptyPlaceholder,
|
||||
Thread,
|
||||
PageHeader,
|
||||
UserAvatar,
|
||||
UserName,
|
||||
@ -101,6 +94,7 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeScreen: 'posts',
|
||||
activeTab: 'posts',
|
||||
}
|
||||
},
|
||||
@ -119,6 +113,11 @@ export default defineComponent({
|
||||
},
|
||||
replies() {
|
||||
return this.notes.filter(note => note.isReply())
|
||||
.map(note => [this.nostr.getNote(note.ancestor()), note])
|
||||
},
|
||||
reactions() {
|
||||
return this.nostr.getReactionsByAuthor(this.pubkey)
|
||||
.map(note => [this.nostr.getNote(note.ancestor()), note])
|
||||
},
|
||||
contacts() {
|
||||
return this.nostr.getContacts(this.pubkey)
|
||||
@ -127,14 +126,33 @@ export default defineComponent({
|
||||
return this.nostr.getFollowers(this.pubkey)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goToFollowers(tab = 'following') {
|
||||
this.$router.push({
|
||||
name: 'followers',
|
||||
params: {
|
||||
pubkey: hexToBech32(this.pubkey),
|
||||
tab,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// FIXME
|
||||
// this.stream = this.nostr.streamFullProfile(this.pubkey)
|
||||
this.nostr.fetchNotesByAuthor(this.pubkey)
|
||||
this.nostr.fetchFollowers(this.pubkey)
|
||||
this.nostr.fetchReactionsByAuthor(this.pubkey, 100)
|
||||
this.nostr.fetchFollowers(this.pubkey, 1000)
|
||||
},
|
||||
unmounted() {
|
||||
// if (this.stream) this.nostr.cancelStream(this.stream)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/theme/colors.scss";
|
||||
|
||||
.profile {
|
||||
&-header {
|
||||
display: flex;
|
||||
@ -146,18 +164,35 @@ export default defineComponent({
|
||||
}
|
||||
&-content {
|
||||
.followers {
|
||||
span + span {
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: $color-light-gray;
|
||||
&:hover, &:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
strong {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
a + a {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&-tabs {
|
||||
border-bottom: $border-dark;
|
||||
}
|
||||
&-tab-panels {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.profile-tabs {
|
||||
.q-tab {
|
||||
}
|
||||
}
|
||||
.profile-header-content .username {
|
||||
.name, .pubkey:first-child {
|
||||
font-size: 1.4rem;
|
@ -2,7 +2,7 @@ import {hexToBech32} from 'src/utils/utils'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
linkToProfile(pubkey) {
|
||||
goToProfile(pubkey) {
|
||||
this.$router.push({
|
||||
name: 'profile',
|
||||
params: {
|
||||
@ -10,7 +10,7 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
linkToThread(id) {
|
||||
goToThread(id) {
|
||||
this.$router.push({
|
||||
name: 'thread',
|
||||
params: {
|
||||
|
@ -15,9 +15,14 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/profile/:pubkey(npub[a-z0-9A-Z]{59})',
|
||||
component: () => import('pages/Profile.vue'),
|
||||
component: () => import('pages/profile/Profile.vue'),
|
||||
name: 'profile',
|
||||
},
|
||||
{
|
||||
path: '/profile/:pubkey(npub[a-z0-9A-Z]{59})/:tab(following|followers)',
|
||||
component: () => import('pages/profile/Followers.vue'),
|
||||
name: 'followers',
|
||||
},
|
||||
{
|
||||
path: '/thread/:id(note[a-z0-9A-Z]{59})',
|
||||
component: () => import('pages/Thread.vue'),
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user