mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 13:33:22 +00:00
Basic thread component, thread styling
This commit is contained in:
parent
7c27ec8963
commit
d4460d2769
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
class="cursor-pointer username"
|
:class="'username' + (wrap ? ' two-line' : '')"
|
||||||
@click.stop="toProfile(pubkey)"
|
@click.stop="toProfile(pubkey)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -15,6 +15,7 @@
|
|||||||
</q-icon>
|
</q-icon>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-italic">anonymous</span>
|
<span v-else class="text-italic">anonymous</span>
|
||||||
|
|
||||||
<span v-if="$store.getters.NIP05Id(pubkey)">
|
<span v-if="$store.getters.NIP05Id(pubkey)">
|
||||||
<BaseButtonNIP05 :pubkey="pubkey" />
|
<BaseButtonNIP05 :pubkey="pubkey" />
|
||||||
<span style="opacity: .9; font-size: 90%; font-weight: 300; line-height: 90%">
|
<span style="opacity: .9; font-size: 90%; font-weight: 300; line-height: 90%">
|
||||||
@ -34,10 +35,22 @@ export default {
|
|||||||
BaseButtonNIP05,
|
BaseButtonNIP05,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
pubkey: {type: String, required: true},
|
pubkey: {
|
||||||
wrap: {type: Boolean, default: false},
|
type: String,
|
||||||
showFollowing: {type: Boolean, default: false},
|
required: true
|
||||||
showVerified: {type: Boolean, default: false},
|
},
|
||||||
|
wrap: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showFollowing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showVerified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
niceNIP05() {
|
niceNIP05() {
|
||||||
@ -47,27 +60,22 @@ export default {
|
|||||||
},
|
},
|
||||||
isFollow() {
|
isFollow() {
|
||||||
return this.$store.state.follows.includes(this.pubkey)
|
return this.$store.state.follows.includes(this.pubkey)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
// methods: {
|
|
||||||
// openNIP05() {
|
|
||||||
// let [name, domain] = this.$store.getters
|
|
||||||
// .displayName(this.pubkey)
|
|
||||||
// .split('@')
|
|
||||||
// if (!domain) {
|
|
||||||
// domain = name
|
|
||||||
// name = '_'
|
|
||||||
// }
|
|
||||||
// window.open(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.username {
|
.username {
|
||||||
|
cursor: pointer;
|
||||||
> span + span {
|
> span + span {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
&.two-line {
|
||||||
|
display: block;
|
||||||
|
> span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<textarea v-model="text" :placeholder="placeholder" @input="resize()" ref="textarea"></textarea>
|
<textarea v-model="text" :placeholder="placeholder" @input="resize" @focus="resize" ref="textarea"></textarea>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -12,15 +12,15 @@ export default {
|
|||||||
},
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
type: Number,
|
type: Number,
|
||||||
'default': null,
|
default: null,
|
||||||
},
|
},
|
||||||
maxHeight: {
|
maxHeight: {
|
||||||
type: Number,
|
type: Number,
|
||||||
'default': null,
|
default: null,
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'What\'s happening',
|
default: 'What\'s happening?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
@ -41,8 +41,6 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
resize() {
|
resize() {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
textarea.style.height = 'auto'
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
let height = textarea.scrollHeight
|
let height = textarea.scrollHeight
|
||||||
if (this.minHeight) {
|
if (this.minHeight) {
|
||||||
@ -66,7 +64,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.resize()
|
if (this.text) {
|
||||||
|
this.resize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="post-editor">
|
<div :class="rootCssClass">
|
||||||
<div class="post-editor-avatar">
|
<div class="post-editor-avatar">
|
||||||
<BaseUserAvatar :pubkey="$store.state.keys.pub" />
|
<BaseUserAvatar :pubkey="$store.state.keys.pub" />
|
||||||
</div>
|
</div>
|
||||||
@ -8,7 +8,8 @@
|
|||||||
<AutoSizeTextarea
|
<AutoSizeTextarea
|
||||||
v-model="post.text"
|
v-model="post.text"
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
placeholder="What's happening?"
|
:placeholder="placeholder"
|
||||||
|
@focus.once="collapsed = false"
|
||||||
/>
|
/>
|
||||||
<!-- <div-->
|
<!-- <div-->
|
||||||
<!-- v-if="tweetContent.imageList"-->
|
<!-- v-if="tweetContent.imageList"-->
|
||||||
@ -59,15 +60,13 @@
|
|||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-submit">
|
<div class="controls-submit">
|
||||||
<button
|
<button :disabled="!hasPostText()" @click="handleSubmit" class="btn">
|
||||||
:disabled="!hasPostText()"
|
|
||||||
@click="handleSubmit"
|
|
||||||
>
|
|
||||||
Post
|
Post
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button v-if="collapsed" class="btn" disabled>Post</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -85,11 +84,30 @@ export default {
|
|||||||
BaseUserAvatar,
|
BaseUserAvatar,
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'What\'s happening?',
|
||||||
|
},
|
||||||
|
compact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
post: {
|
post: {
|
||||||
text: '',
|
text: '',
|
||||||
},
|
},
|
||||||
|
collapsed: this.compact,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rootCssClass() {
|
||||||
|
let classes = ['post-editor']
|
||||||
|
if (this.compact) classes.push('post-editor-compact')
|
||||||
|
if (this.collapsed) classes.push('post-editor-compact-collapsed')
|
||||||
|
return classes
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -109,11 +127,27 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "assets/theme/colors.scss";
|
@import "assets/theme/colors.scss";
|
||||||
|
|
||||||
|
button.btn {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: $color-primary;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8px 16px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
height: fit-content;
|
||||||
|
&:disabled {
|
||||||
|
cursor: no-drop;
|
||||||
|
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
||||||
|
color: rgba($color: #fff, $alpha: 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-editor {
|
.post-editor {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: $border-dark;
|
|
||||||
&-avatar {
|
&-avatar {
|
||||||
}
|
}
|
||||||
&-content {
|
&-content {
|
||||||
@ -127,6 +161,7 @@ export default {
|
|||||||
display: block;
|
display: block;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.3em;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@ -140,41 +175,6 @@ export default {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-images {
|
|
||||||
display: flex;
|
|
||||||
padding: 1rem;
|
|
||||||
.image-container {
|
|
||||||
& + .image-container {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.close-button {
|
|
||||||
position: absolute;
|
|
||||||
background-color: rgba($color: $color-dark-gray, $alpha: 0.3);
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 7px;
|
|
||||||
svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
border-top: $border-dark;
|
border-top: $border-dark;
|
||||||
@ -200,22 +200,30 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-submit {
|
}
|
||||||
button {
|
}
|
||||||
cursor: pointer;
|
&-compact {
|
||||||
background-color: $color-primary;
|
.input-section {
|
||||||
color: #fff;
|
textarea {
|
||||||
font-weight: bold;
|
padding: 10px 0;
|
||||||
padding: 10px 16px;
|
resize: none;
|
||||||
outline: none;
|
min-height: 48px;
|
||||||
border: none;
|
height: 48px;
|
||||||
border-radius: 9999px;
|
overflow: hidden;
|
||||||
&:disabled{
|
}
|
||||||
cursor: no-drop;
|
}
|
||||||
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
.controls {
|
||||||
color: rgba($color: #fff, $alpha: 0.3);
|
border-top: 0;
|
||||||
}
|
padding: 0;
|
||||||
}
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-collapsed {
|
||||||
|
.controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<Logo />
|
<Logo />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<menu-item
|
<MenuItem
|
||||||
v-for="(route, i) in items"
|
v-for="(route, i) in items"
|
||||||
:key="i"
|
:key="i"
|
||||||
:icon="route.name.toLowerCase()"
|
:icon="route.name.toLowerCase()"
|
||||||
@ -14,21 +14,21 @@
|
|||||||
:required="route.req"
|
:required="route.req"
|
||||||
>
|
>
|
||||||
{{ route.name }}
|
{{ route.name }}
|
||||||
</menu-item>
|
</MenuItem>
|
||||||
<menu-item
|
<MenuItem
|
||||||
icon="profile"
|
icon="profile"
|
||||||
:to="`/profile/${me.id}`"
|
:to="`/profile/${me.id}`"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
Profile
|
Profile
|
||||||
</menu-item>
|
</MenuItem>
|
||||||
<menu-item
|
<MenuItem
|
||||||
icon="settings"
|
icon="settings"
|
||||||
to="/settings"
|
to="/settings"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</menu-item>
|
</MenuItem>
|
||||||
|
|
||||||
<!-- <menu-item-->
|
<!-- <menu-item-->
|
||||||
<!-- icon="more"-->
|
<!-- icon="more"-->
|
||||||
@ -45,13 +45,13 @@
|
|||||||
Post
|
Post
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<profile-popup />
|
<ProfilePopup />
|
||||||
<div
|
<div
|
||||||
class="mobile-close-menu-button"
|
class="mobile-close-menu-button"
|
||||||
@click="$store.commit('setMobileMenuState', false)"
|
@click="$store.commit('setMobileMenuState', false)"
|
||||||
>
|
>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<base-icon icon="left" />
|
<BaseIcon icon="left" />
|
||||||
</div>
|
</div>
|
||||||
<span>Close</span>
|
<span>Close</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,9 +7,7 @@
|
|||||||
@click="toggleMenu"
|
@click="toggleMenu"
|
||||||
>
|
>
|
||||||
<div class="menu-profile-pic">
|
<div class="menu-profile-pic">
|
||||||
<img
|
<BaseUserAvatar :pubkey="$store.state.keys.pub" />
|
||||||
:src="me.profile.pic"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-profile-items">
|
<div class="menu-profile-items">
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
@ -65,10 +63,12 @@
|
|||||||
<script>
|
<script>
|
||||||
// import { mapGetters } from 'vuex'
|
// import { mapGetters } from 'vuex'
|
||||||
import BaseIcon from '../BaseIcon/index'
|
import BaseIcon from '../BaseIcon/index'
|
||||||
|
import BaseUserAvatar from 'components/BaseUserAvatar.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProfilePopup',
|
name: 'ProfilePopup',
|
||||||
components: {
|
components: {
|
||||||
|
BaseUserAvatar,
|
||||||
BaseIcon
|
BaseIcon
|
||||||
},
|
},
|
||||||
data: function() {
|
data: function() {
|
||||||
@ -110,11 +110,12 @@ export default {
|
|||||||
.menu-profile {
|
.menu-profile {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
|
||||||
padding: 4px 1rem;
|
padding: 4px 1rem;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
transition: 120ms ease-in-out;
|
||||||
&-wrapper {
|
&-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -122,8 +123,6 @@ export default {
|
|||||||
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
||||||
}
|
}
|
||||||
&-pic {
|
&-pic {
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
img {
|
img {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@ -131,12 +130,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-items {
|
&-items {
|
||||||
margin-left: 10px;
|
margin-left: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
.profile-info{
|
.profile-info {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
293
src/components/Post/HeroPost.vue
Normal file
293
src/components/Post/HeroPost.vue
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<div class="post">
|
||||||
|
<div class="post-author">
|
||||||
|
<div class="post-author-avatar">
|
||||||
|
<div class="connector-top">
|
||||||
|
<div v-if="connector" class="connector-line"></div>
|
||||||
|
</div>
|
||||||
|
<BaseUserAvatar :pubkey="post.author" />
|
||||||
|
</div>
|
||||||
|
<div class="post-author-name">
|
||||||
|
<BaseUserName :pubkey="post.author" wrap />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-content">
|
||||||
|
<div class="post-content-header">
|
||||||
|
<p v-if="post.inReplyTo" class="in-reply-to">
|
||||||
|
Replying to <a @click.stop="toEvent(post.inReplyTo)">{{ shorten(post.inReplyTo) }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="post-content-body">
|
||||||
|
<p>
|
||||||
|
<BaseMarkdown :content="post.content" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="post-content-footer">
|
||||||
|
<p class="created-at">
|
||||||
|
<span>{{ formatPostTime(post.createdAt) }}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{{ formatPostDate(post.createdAt) }}</span>
|
||||||
|
</p>
|
||||||
|
<div class="post-content-actions">
|
||||||
|
<div class="action-item comment">
|
||||||
|
<BaseIcon icon="comment" />
|
||||||
|
<span>{{ numComments }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-item repost">
|
||||||
|
<BaseIcon icon="repost" />
|
||||||
|
<span>{{ post.stats.reposts }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-item like">
|
||||||
|
<BaseIcon icon="like" />
|
||||||
|
<span>{{ post.stats.likes }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-reply">
|
||||||
|
<PostEditor
|
||||||
|
compact
|
||||||
|
placeholder="Post your reply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseIcon from 'components/BaseIcon'
|
||||||
|
import BaseUserAvatar from 'components/BaseUserAvatar.vue'
|
||||||
|
import BaseUserName from 'components/BaseUserName.vue'
|
||||||
|
import BaseMarkdown from 'components/BaseMarkdown.vue'
|
||||||
|
import helpersMixin from 'src/utils/mixin'
|
||||||
|
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||||
|
|
||||||
|
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
||||||
|
|
||||||
|
function countRepliesRecursive(event) {
|
||||||
|
if (!event.replies) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
let count = 0
|
||||||
|
for (const thread of event.replies) {
|
||||||
|
if (!thread || !thread.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count += thread.length
|
||||||
|
for (const reply of thread) {
|
||||||
|
count += countRepliesRecursive(reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
function postFromEvent(event) {
|
||||||
|
return {
|
||||||
|
id: event.id,
|
||||||
|
author: event.pubkey,
|
||||||
|
createdAt: event.created_at * 1000,
|
||||||
|
content: event.interpolated.text,
|
||||||
|
inReplyTo: event.interpolated.replyEvents[event.interpolated.replyEvents.length - 1],
|
||||||
|
images: [],
|
||||||
|
stats: {
|
||||||
|
comments: '',
|
||||||
|
reposts: '',
|
||||||
|
likes: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HeroPost',
|
||||||
|
mixins: [helpersMixin],
|
||||||
|
components: {
|
||||||
|
BaseMarkdown,
|
||||||
|
BaseUserName,
|
||||||
|
BaseUserAvatar,
|
||||||
|
BaseIcon,
|
||||||
|
PostEditor,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
event: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
post: postFromEvent(this.event),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
numComments() {
|
||||||
|
return countRepliesRecursive(this.event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatPostDate(ts) {
|
||||||
|
const date = new Date(ts)
|
||||||
|
const month = this.$t(MONTHS[date.getMonth()])
|
||||||
|
|
||||||
|
const sameYear = date.getFullYear() === (new Date().getFullYear())
|
||||||
|
const year = !sameYear ? ' ' + date.getFullYear() : ''
|
||||||
|
|
||||||
|
return `${date.getDate()} ${month}${year}`
|
||||||
|
},
|
||||||
|
formatPostTime(ts) {
|
||||||
|
const date = new Date(ts)
|
||||||
|
return `${date.getHours()}:${date.getMinutes()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import 'assets/theme/colors.scss';
|
||||||
|
@import 'assets/variables.scss';
|
||||||
|
|
||||||
|
.post {
|
||||||
|
border-bottom: $border-dark;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
&-author {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0 1rem;
|
||||||
|
&-name {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.connector-top {
|
||||||
|
height: 1rem;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.connector-line {
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
background: rgb(56, 68, 77);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-content {
|
||||||
|
padding: 1rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 644px;
|
||||||
|
&-header {
|
||||||
|
p.in-reply-to {
|
||||||
|
color: $color-dark-gray;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
a {
|
||||||
|
color: $color-primary;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-body {
|
||||||
|
p {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.6em;
|
||||||
|
line-height: 1.3em;
|
||||||
|
}
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-footer {
|
||||||
|
color: $color-dark-gray;
|
||||||
|
border-bottom: $border-dark;
|
||||||
|
p.created-at {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem 0;
|
||||||
|
border-bottom: $border-dark;
|
||||||
|
span + span {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $phone) {
|
||||||
|
.post{
|
||||||
|
&-content {
|
||||||
|
&-header {
|
||||||
|
span{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.created-at {
|
||||||
|
display: block;
|
||||||
|
color: rgba($color: $color-dark-gray, $alpha: 0.5);
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.nip05 {
|
||||||
|
display: unset;
|
||||||
|
color: $color-dark-gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-actions {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -4,12 +4,18 @@
|
|||||||
@click.stop="toEvent(post.id)"
|
@click.stop="toEvent(post.id)"
|
||||||
>
|
>
|
||||||
<div class="post-author">
|
<div class="post-author">
|
||||||
<base-user-avatar :pubkey="post.author" />
|
<div class="connector-top">
|
||||||
|
<div v-if="connectorTop" class="connector-line"></div>
|
||||||
|
</div>
|
||||||
|
<BaseUserAvatar :pubkey="post.author" />
|
||||||
|
<div class="connector-bottom">
|
||||||
|
<div v-if="connectorBottom" class="connector-line"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<div class="post-content-header">
|
<div class="post-content-header">
|
||||||
<p>
|
<p>
|
||||||
<base-user-name :pubkey="post.author" />
|
<BaseUserName :pubkey="post.author" />
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span class="created-at">{{ moment(post.createdAt).fromNow() }}</span>
|
<span class="created-at">{{ moment(post.createdAt).fromNow() }}</span>
|
||||||
</p>
|
</p>
|
||||||
@ -19,42 +25,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="post-content-body">
|
<div class="post-content-body">
|
||||||
<p>
|
<p>
|
||||||
<base-markdown :content="post.content" />
|
<BaseMarkdown :content="post.content" />
|
||||||
</p>
|
</p>
|
||||||
<div
|
|
||||||
v-if="post.images.length > 0"
|
|
||||||
class="post-content-body-images"
|
|
||||||
>
|
|
||||||
<div class="post-content-body-images-wrapper">
|
|
||||||
<div
|
|
||||||
v-for="(image, i) in post.images"
|
|
||||||
:key="i"
|
|
||||||
class="post-content-image-item"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="image.url"
|
|
||||||
@click="$store.dispatch('setLightbox', imageUrls)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content-actions">
|
<div class="post-content-actions">
|
||||||
<div class="action-item comment">
|
<div class="action-item comment">
|
||||||
<base-icon icon="comment" />
|
<BaseIcon icon="comment" />
|
||||||
<span>{{ post.stats.comments }}</span>
|
<span>{{ numComments }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-item repost">
|
<div class="action-item repost">
|
||||||
<base-icon icon="repost" />
|
<BaseIcon icon="repost" />
|
||||||
<span>{{ post.stats.reposts }}</span>
|
<span>{{ post.stats.reposts }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-item like">
|
<div class="action-item like">
|
||||||
<base-icon icon="like" />
|
<BaseIcon icon="like" />
|
||||||
<span>{{ post.stats.likes }}</span>
|
<span>{{ post.stats.likes }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-item comment">
|
|
||||||
<base-icon icon="share" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +60,7 @@ function countRepliesRecursive(event) {
|
|||||||
}
|
}
|
||||||
let count = 0
|
let count = 0
|
||||||
for (const thread of event.replies) {
|
for (const thread of event.replies) {
|
||||||
if (!thread || !Array.isArray(thread)) {
|
if (!thread || !thread.length) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
count += thread.length
|
count += thread.length
|
||||||
@ -85,11 +71,7 @@ function countRepliesRecursive(event) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
function postFromEvents(events) {
|
function postFromEvent(event) {
|
||||||
const event = events[0]
|
|
||||||
if (event.interpolated.replyEvents.length) {
|
|
||||||
console.log(event.content, event.interpolated.replyEvents)
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
author: event.pubkey,
|
author: event.pubkey,
|
||||||
@ -98,15 +80,15 @@ function postFromEvents(events) {
|
|||||||
inReplyTo: event.interpolated.replyEvents[event.interpolated.replyEvents.length - 1],
|
inReplyTo: event.interpolated.replyEvents[event.interpolated.replyEvents.length - 1],
|
||||||
images: [],
|
images: [],
|
||||||
stats: {
|
stats: {
|
||||||
comments: countRepliesRecursive(event),
|
comments: '',
|
||||||
reposts: 11,
|
reposts: '',
|
||||||
likes: 52,
|
likes: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Post',
|
name: 'ListPost',
|
||||||
mixins: [helpersMixin],
|
mixins: [helpersMixin],
|
||||||
components: {
|
components: {
|
||||||
BaseMarkdown,
|
BaseMarkdown,
|
||||||
@ -115,33 +97,41 @@ export default {
|
|||||||
BaseIcon,
|
BaseIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
events: {
|
event: {
|
||||||
type: Array,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
connectorTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
connectorBottom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
post: postFromEvents(this.events),
|
post: postFromEvent(this.event),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
imageUrls() {
|
numComments() {
|
||||||
return this.post.images.map(image => image.url)
|
return countRepliesRecursive(this.event)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
moment,
|
moment,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import 'assets/theme/colors.scss';
|
@import 'assets/theme/colors.scss';
|
||||||
@import 'assets/variables.scss';
|
@import 'assets/variables.scss';
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
padding: 1rem;
|
padding: 0 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: 100ms ease background-color;
|
transition: 100ms ease background-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -151,6 +141,22 @@ export default {
|
|||||||
background-color: rgba($color: $color-dark-gray, $alpha: 0.2);
|
background-color: rgba($color: $color-dark-gray, $alpha: 0.2);
|
||||||
}
|
}
|
||||||
&-author {
|
&-author {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.connector-top {
|
||||||
|
height: 1rem;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.connector-bottom {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
.connector-line {
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
background: rgb(56, 68, 77);
|
||||||
|
}
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@ -158,6 +164,7 @@ export default {
|
|||||||
}
|
}
|
||||||
&-content {
|
&-content {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
|
padding: 1rem 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-width: 570px;
|
max-width: 570px;
|
||||||
&-header {
|
&-header {
|
46
src/components/Post/Thread.vue
Normal file
46
src/components/Post/Thread.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="thread">
|
||||||
|
<ListPost
|
||||||
|
v-for="(event, index) in events"
|
||||||
|
:key="event.id"
|
||||||
|
:event="event"
|
||||||
|
:connector-top="events.length > 1 && index > 0"
|
||||||
|
:connector-bottom="(events.length > 1 && index < events.length - 1) || forceBottomConnector"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ListPost from 'components/Post/ListPost.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Thread',
|
||||||
|
components: {
|
||||||
|
ListPost
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
events: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
forceBottomConnector: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
console.log(`Thread (${this.events.length}) ${this.events[0].content.substr(0, 100)}`, this.events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "assets/theme/colors.scss";
|
||||||
|
|
||||||
|
.thread {
|
||||||
|
border-bottom: $border-dark;
|
||||||
|
.post {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -30,7 +30,7 @@
|
|||||||
:label="'load ' + unreadFeed[tab].length + ' unread'"
|
:label="'load ' + unreadFeed[tab].length + ' unread'"
|
||||||
@click="loadUnread"
|
@click="loadUnread"
|
||||||
/>
|
/>
|
||||||
<Post v-for="item in items" :key="item[0].id" :events="item" class="full-width" @add-event="processEvent"/>
|
<Thread v-for="item in items" :key="item[0].id" :events="item" class="full-width" @add-event="processEvent" />
|
||||||
<BaseButtonLoadMore
|
<BaseButtonLoadMore
|
||||||
:loading="loadingMore"
|
:loading="loadingMore"
|
||||||
:label="items.length === feed[tab].length ? 'load another day' : 'load 100 more'"
|
:label="items.length === feed[tab].length ? 'load another day' : 'load 100 more'"
|
||||||
@ -49,8 +49,9 @@ import {dbFeed, dbUserFollows} from '../query'
|
|||||||
import BaseButtonLoadMore from 'components/BaseButtonLoadMore.vue'
|
import BaseButtonLoadMore from 'components/BaseButtonLoadMore.vue'
|
||||||
import { createMetaMixin } from 'quasar'
|
import { createMetaMixin } from 'quasar'
|
||||||
import PageHeader from 'components/PageHeader.vue'
|
import PageHeader from 'components/PageHeader.vue'
|
||||||
import Post from 'components/Post/Post.vue'
|
//import Post from 'components/Post/ListPost.vue'
|
||||||
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||||
|
import Thread from 'components/Post/Thread.vue'
|
||||||
|
|
||||||
|
|
||||||
// const debouncedAddToThread = mergebounce(
|
// const debouncedAddToThread = mergebounce(
|
||||||
@ -80,8 +81,9 @@ export default defineComponent({
|
|||||||
mixins: [helpersMixin, createMetaMixin(metaData)],
|
mixins: [helpersMixin, createMetaMixin(metaData)],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
Thread,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
Post,
|
//Post,
|
||||||
PostEditor,
|
PostEditor,
|
||||||
BaseButtonLoadMore,
|
BaseButtonLoadMore,
|
||||||
},
|
},
|
||||||
|
216
src/pages/Thread.vue
Normal file
216
src/pages/Thread.vue
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<template>
|
||||||
|
<q-page ref="page">
|
||||||
|
<PageHeader :title="$t('thread')" back-button />
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="ancestors"
|
||||||
|
v-if="ancestorsCompiled.length || rootAncestor"
|
||||||
|
>
|
||||||
|
<Thread
|
||||||
|
:events="ancestorsCompiled"
|
||||||
|
force-bottom-connector
|
||||||
|
@add-event="addEventAncestors"
|
||||||
|
class="ancestors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-item ref="main" class='no-padding column'>
|
||||||
|
<HeroPost
|
||||||
|
v-if="event"
|
||||||
|
:event="event"
|
||||||
|
:highlighted="true"
|
||||||
|
:position="ancestors.length ? 'last' : 'standalone'"
|
||||||
|
connector
|
||||||
|
@add-event="processChildEvent"
|
||||||
|
/>
|
||||||
|
<div v-else>
|
||||||
|
{{ $t('event') }} {{ $route.params.eventId }}
|
||||||
|
</div>
|
||||||
|
<!-- <BaseRelayList v-if="event?.seen_on?.length" :event='event' class='q-px-sm'/>-->
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<div v-if="childrenThreadsFiltered.length">
|
||||||
|
<div v-for="(thread) in childrenThreadsFiltered" :key="thread[0].id">
|
||||||
|
<Thread :events="thread" @add-event='processChildEvent'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style='min-height: 30vh;'/>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent, nextTick } from 'vue'
|
||||||
|
import {dbStreamEvent, dbStreamTagKind} from '../query'
|
||||||
|
import helpersMixin from '../utils/mixin'
|
||||||
|
import {addToThread} from '../utils/threads'
|
||||||
|
//import BaseRelayList from 'components/BaseRelayList.vue'
|
||||||
|
import PageHeader from 'components/PageHeader.vue'
|
||||||
|
import { createMetaMixin } from 'quasar'
|
||||||
|
import Thread from 'components/Post/Thread.vue'
|
||||||
|
import HeroPost from 'components/Post/HeroPost.vue'
|
||||||
|
|
||||||
|
const metaData = {
|
||||||
|
// sets document title
|
||||||
|
title: 'hamstr - thread',
|
||||||
|
|
||||||
|
// meta tags
|
||||||
|
meta: {
|
||||||
|
description: { name: 'description', content: 'Nostr event thread' },
|
||||||
|
keywords: { name: 'keywords', content: 'nostr decentralized social media' },
|
||||||
|
equiv: { 'http-equiv': 'Content-Type', content: 'text/html; charset=UTF-8' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Event',
|
||||||
|
emits: ['scroll-to-rect'],
|
||||||
|
mixins: [helpersMixin, createMetaMixin(metaData)],
|
||||||
|
components: {
|
||||||
|
HeroPost,
|
||||||
|
Thread,
|
||||||
|
PageHeader,
|
||||||
|
//BaseRelayList
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
replying: false,
|
||||||
|
ancestors: [],
|
||||||
|
ancestorsSeen: new Map(),
|
||||||
|
ancestorIds: [],
|
||||||
|
rootAncestor: null,
|
||||||
|
event: null,
|
||||||
|
childrenThreads: [],
|
||||||
|
childrenSet: new Set(),
|
||||||
|
sub: {},
|
||||||
|
profilesUsed: new Set(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
childrenThreadsFiltered() {
|
||||||
|
return this.childrenThreads.filter(thread => thread[0].interpolated.replyEvents.includes(this.$route.params.eventId))
|
||||||
|
},
|
||||||
|
ancestorsCompiled() {
|
||||||
|
if (!this.rootAncestor) return this.ancestors
|
||||||
|
if (this.ancestors.length && this.rootAncestor && this.ancestors[0].id === this.rootAncestor.id) return this.ancestors
|
||||||
|
return [this.rootAncestor].concat(this.ancestors)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.start()
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
this.stop()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async start() {
|
||||||
|
this.sub.event = await dbStreamEvent(this.$route.params.eventId, event => {
|
||||||
|
let getAncestorsChildren = false
|
||||||
|
if (!this.event) getAncestorsChildren = true
|
||||||
|
this.interpolateEventMentions(event)
|
||||||
|
this.event = null
|
||||||
|
this.event = event
|
||||||
|
if (getAncestorsChildren) {
|
||||||
|
if (this.event.interpolated.replyEvents.length) this.subRootAncestor()
|
||||||
|
this.subAncestorsChildren()
|
||||||
|
}
|
||||||
|
this.useProfile(event.pubkey)
|
||||||
|
}, true)
|
||||||
|
this.subAncestorsChildren()
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.replying = false
|
||||||
|
if (this.sub.event) this.sub.event.cancel()
|
||||||
|
if (this.sub.ancestorsChildren) this.sub.ancestorsChildren.cancel()
|
||||||
|
if (this.sub.rootAncestor) this.sub.rootAncestor.cancel()
|
||||||
|
this.profilesUsed.forEach(pubkey => this.$store.dispatch('cancelUseProfile', {pubkey}))
|
||||||
|
},
|
||||||
|
|
||||||
|
async subRootAncestor() {
|
||||||
|
this.sub.rootAncestor = await dbStreamEvent(this.event.interpolated.replyEvents[0], event => {
|
||||||
|
this.processAncestorEvent(event)
|
||||||
|
this.sub.rootAncestor.cancel()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async subAncestorsChildren() {
|
||||||
|
let tags = this.event?.interpolated?.replyEvents?.length ? [this.$route.params.eventId, this.event.interpolated.replyEvents[0]] : [this.$route.params.eventId]
|
||||||
|
|
||||||
|
if (this.sub.ancestorsChildren) this.sub.ancestorsChildren.update('e', tags, 1)
|
||||||
|
else this.sub.ancestorsChildren = await dbStreamTagKind('e', tags, 1, event => {
|
||||||
|
if (this.event && event.created_at < this.event.created_at) {
|
||||||
|
this.processAncestorEvent(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.processChildEvent(event)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
processAncestorEvent(event) {
|
||||||
|
let currAncestor = this.ancestors.length ? this.ancestors[this.ancestors.length - 1] : this.event
|
||||||
|
if (currAncestor.interpolated.replyEvents.length === 0) return
|
||||||
|
|
||||||
|
let existing = this.ancestorsSeen.get(event.id)
|
||||||
|
if (existing) return
|
||||||
|
|
||||||
|
this.interpolateEventMentions(event)
|
||||||
|
this.ancestorsSeen.set(event.id, event)
|
||||||
|
if (this.event?.interpolated?.replyEvents?.[0] === event.id) this.rootAncestor = event
|
||||||
|
|
||||||
|
let prevAncestorId = currAncestor.interpolated.replyEvents[currAncestor.interpolated.replyEvents.length - 1]
|
||||||
|
if (prevAncestorId === event.id) {
|
||||||
|
let prevAncestor = event
|
||||||
|
while (prevAncestor) {
|
||||||
|
this.ancestors = [prevAncestor].concat(this.ancestors)
|
||||||
|
this.scrollToMainEvent()
|
||||||
|
this.useProfile(prevAncestor.pubkey)
|
||||||
|
currAncestor = prevAncestor
|
||||||
|
prevAncestorId = currAncestor.interpolated.replyEvents[currAncestor.interpolated.replyEvents.length - 1]
|
||||||
|
prevAncestor = this.ancestorsSeen.get(prevAncestorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
processChildEvent(event) {
|
||||||
|
if (event.id === this.$route.params.eventId) return
|
||||||
|
if (this.childrenSet.has(event.id)) return
|
||||||
|
|
||||||
|
this.childrenSet.add(event.id)
|
||||||
|
this.useProfile(event.pubkey)
|
||||||
|
this.interpolateEventMentions(event)
|
||||||
|
addToThread(this.childrenThreads, event, '', event.pubkey !== this.$store.state.keys.pub)
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollToMainEvent() {
|
||||||
|
nextTick(() => {
|
||||||
|
let mainRect = this.$refs.main?.$el.getBoundingClientRect()
|
||||||
|
this.$emit('scroll-to-rect', mainRect)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
addEventAncestors(event) {
|
||||||
|
this.interpolateEventMentions(event)
|
||||||
|
this.toEvent(event.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
useProfile(pubkey) {
|
||||||
|
if (this.profilesUsed.has(pubkey)) return
|
||||||
|
|
||||||
|
this.profilesUsed.add(pubkey)
|
||||||
|
this.$store.dispatch('useProfile', {pubkey})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ancestors {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
@ -33,6 +33,11 @@ const routes = [
|
|||||||
component: () => import('pages/Event.vue'),
|
component: () => import('pages/Event.vue'),
|
||||||
name: 'event',
|
name: 'event',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/thread/:eventId([a-f0-9A-F]{64})',
|
||||||
|
component: () => import('pages/Thread.vue'),
|
||||||
|
name: 'thread',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/notifications',
|
path: '/notifications',
|
||||||
component: () => import('pages/Notifications.vue'),
|
component: () => import('pages/Notifications.vue'),
|
||||||
|
@ -53,7 +53,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
toEvent(id) {
|
toEvent(id) {
|
||||||
if (!window.getSelection().toString().length) this.$router.push('/event/' + id)
|
if (!window.getSelection().toString().length) this.$router.push('/thread/' + id)
|
||||||
},
|
},
|
||||||
|
|
||||||
shorten,
|
shorten,
|
||||||
|
Loading…
Reference in New Issue
Block a user