mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
Create posts & replies
This commit is contained in:
parent
ca6cfa6e82
commit
6273251130
@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<textarea v-model="text" :placeholder="placeholder" @input="resize" @focus="resize" ref="textarea"></textarea>
|
||||
<textarea
|
||||
v-model="text"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
@input="resize"
|
||||
@focus="resize"
|
||||
ref="textarea"
|
||||
></textarea>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -22,6 +29,10 @@ export default {
|
||||
type: String,
|
||||
default: 'What\'s happening?',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
@ -42,7 +53,9 @@ export default {
|
||||
resize() {
|
||||
const textarea = this.$refs.textarea
|
||||
this.$nextTick(() => {
|
||||
textarea.style.height = 'inherit'
|
||||
let height = textarea.scrollHeight
|
||||
|
||||
if (this.minHeight) {
|
||||
height = Math.max(height, this.minHeight)
|
||||
}
|
||||
@ -61,7 +74,11 @@ export default {
|
||||
const caretPos = textarea.selectionStart + text.length
|
||||
textarea.setSelectionRange(caretPos, caretPos)
|
||||
}
|
||||
}
|
||||
this.$emit('update:modelValue', textarea.value)
|
||||
},
|
||||
focus() {
|
||||
this.$refs.textarea.focus()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.text) {
|
||||
|
@ -1,20 +1,124 @@
|
||||
<template>
|
||||
<q-dialog v-model="$store.state.postDialogOpen">
|
||||
<PostEditor />
|
||||
<q-dialog
|
||||
v-model="$store.state.postDialogOpen"
|
||||
@before-show="updateReplyToEvent"
|
||||
@show="$refs.postEditor.focus()"
|
||||
@hide="onClose"
|
||||
>
|
||||
<div class="create-post-dialog">
|
||||
<q-btn v-close-popup icon="close" size="md" flat round class="icon" />
|
||||
|
||||
<ListPost
|
||||
v-if="replyToEvent"
|
||||
:event="replyToEvent"
|
||||
connector-bottom
|
||||
/>
|
||||
<PostEditor
|
||||
ref="postEditor"
|
||||
class="post-editor"
|
||||
:class="{standalone: !replyToEvent}"
|
||||
:placeholder="placeholderText"
|
||||
:connector="!!replyToEvent"
|
||||
:reply-to="replyTo"
|
||||
@publish="onClose"
|
||||
compact
|
||||
expanded
|
||||
/>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListPost from 'components/Post/ListPost.vue'
|
||||
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||
import {dbStreamEvent} from 'src/query'
|
||||
import helpers from 'src/utils/mixin'
|
||||
|
||||
export default {
|
||||
name: 'CreatePostDialog',
|
||||
mixins: [helpers],
|
||||
components: {
|
||||
ListPost,
|
||||
PostEditor
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
replyToEvent: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
paramsReplyTo() {
|
||||
return this.$store.state.postDialogParams?.replyTo || []
|
||||
},
|
||||
replyTo() {
|
||||
const replyTo = this.paramsReplyTo.map(id => ({id}))
|
||||
if (replyTo.length && this.replyToEvent && this.replyToEvent.id === replyTo[replyTo.length - 1].id) {
|
||||
replyTo[replyTo.length - 1].pubkey = this.replyToEvent.pubkey
|
||||
}
|
||||
return replyTo
|
||||
},
|
||||
placeholderText() {
|
||||
return this.replyToEvent
|
||||
? 'Post your reply'
|
||||
: 'What\'s happening?'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchEvent(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return dbStreamEvent(id, event => {
|
||||
this.interpolateEventMentions(event)
|
||||
this.$store.dispatch('useProfile', {pubkey: event.pubkey})
|
||||
resolve(event)
|
||||
}).catch(reject)
|
||||
})
|
||||
},
|
||||
async updateReplyToEvent() {
|
||||
if (this.paramsReplyTo.length > 0) {
|
||||
const ancestor = this.paramsReplyTo[this.paramsReplyTo.length - 1]
|
||||
if (!this.replyToEvent || ancestor.id !== this.replyToEvent.id) {
|
||||
this.replyToEvent = await this.fetchEvent(ancestor)
|
||||
}
|
||||
} else {
|
||||
this.replyToEvent = null
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
this.$refs.postEditor.reset()
|
||||
this.$store.commit('dismissPostDialog')
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.updateReplyToEvent()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/theme/colors.scss";
|
||||
|
||||
.create-post-dialog {
|
||||
position: relative;
|
||||
background-color: $color-bg;
|
||||
padding: 3rem 1rem 1rem;
|
||||
min-width: 440px;
|
||||
.icon {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: .5rem;
|
||||
left: .5rem;
|
||||
fill: #fff;
|
||||
}
|
||||
.post {
|
||||
padding: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.post-editor {
|
||||
padding: 0;
|
||||
&.standalone {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,40 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
class="post-editor"
|
||||
:class="{
|
||||
'post-editor-compact': compact,
|
||||
'post-editor-compact-collapsed': collapsed
|
||||
}"
|
||||
:class="{compact, collapsed, connector}"
|
||||
>
|
||||
<div class="post-editor-avatar">
|
||||
<BaseUserAvatar :pubkey="$store.state.keys.pub" />
|
||||
<div class="post-editor-author">
|
||||
<div v-if="connector" class="connector-top">
|
||||
<div class="connector-line"></div>
|
||||
</div>
|
||||
<BaseUserAvatar :pubkey="$store.getters.myPubkey" />
|
||||
</div>
|
||||
<div class="post-editor-content">
|
||||
<div class="input-section">
|
||||
<AutoSizeTextarea
|
||||
v-model="post.text"
|
||||
v-model="content"
|
||||
ref="textarea"
|
||||
:placeholder="placeholder"
|
||||
:disabled="publishing"
|
||||
@focus.once="collapsed = false"
|
||||
/>
|
||||
<!-- <div-->
|
||||
<!-- v-if="tweetContent.imageList"-->
|
||||
<!-- class="tweet-section-images"-->
|
||||
<!-- >-->
|
||||
<!-- <div-->
|
||||
<!-- v-for="(image, i) in tweetContent.imageList"-->
|
||||
<!-- :key="i"-->
|
||||
<!-- class="image-container"-->
|
||||
<!-- >-->
|
||||
<!-- <img :src="image.url">-->
|
||||
<!-- <div-->
|
||||
<!-- class="close-button"-->
|
||||
<!-- @click="deleteImage(i)"-->
|
||||
<!-- >-->
|
||||
<!-- <base-icon icon="close" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="controls-media">
|
||||
@ -44,35 +27,21 @@
|
||||
<EmojiPicker @select="onEmojiSelected"/>
|
||||
</q-menu>
|
||||
</div>
|
||||
|
||||
<!-- <div-->
|
||||
<!-- class="controls-media-item"-->
|
||||
<!-- @click="$refs.uploadImageInput.click()"-->
|
||||
<!-- >-->
|
||||
<!-- <BaseIcon icon="image" />-->
|
||||
<!-- <input-->
|
||||
<!-- ref="uploadImageInput"-->
|
||||
<!-- type="file"-->
|
||||
<!-- accept="image/*"-->
|
||||
<!-- hidden-->
|
||||
<!-- @change="showFiles"-->
|
||||
<!-- >-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="controls-media-item">-->
|
||||
<!-- <BaseIcon icon="gif" />-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="controls-media-item">-->
|
||||
<!-- <BaseIcon icon="graph" />-->
|
||||
<!-- </div>-->
|
||||
<div class="controls-media-item disabled">
|
||||
<BaseIcon icon="image" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-submit">
|
||||
<button :disabled="!hasPostText()" @click="handleSubmit" class="btn">
|
||||
Post
|
||||
<button :disabled="!hasContent() || publishing" @click="publishPost" class="btn">
|
||||
<q-spinner v-if="publishing" />
|
||||
<span v-else>Post</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="collapsed" class="btn" disabled>Post</button>
|
||||
<div class="post-editor-fake-submit" v-if="collapsed">
|
||||
<button class="btn" disabled>Post</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -91,6 +60,10 @@ export default {
|
||||
EmojiPicker,
|
||||
},
|
||||
props: {
|
||||
replyTo: {
|
||||
type: Array, // [{id: <eventId>, pubkey: <authorPubkey>},...]
|
||||
default: () => [],
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'What\'s happening?',
|
||||
@ -99,25 +72,74 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
connector: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['publish'],
|
||||
data() {
|
||||
return {
|
||||
post: {
|
||||
text: '',
|
||||
},
|
||||
collapsed: this.compact,
|
||||
content: '',
|
||||
collapsed: this.compact && !this.expanded,
|
||||
publishing: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasPostText() {
|
||||
return this.post.text.trim().length > 0
|
||||
},
|
||||
handleSubmit() {
|
||||
hasContent() {
|
||||
return this.content.trim().length > 0
|
||||
},
|
||||
onEmojiSelected(emoji) {
|
||||
this.$refs.menuEmojiPicker.hide()
|
||||
this.$refs.textarea.insertText(emoji.native)
|
||||
},
|
||||
focus() {
|
||||
this.$refs.textarea.focus()
|
||||
},
|
||||
reset() {
|
||||
this.content = ''
|
||||
},
|
||||
async publishPost() {
|
||||
this.publishing = true
|
||||
const post = {
|
||||
message: this.content,
|
||||
tags: this.buildTags(),
|
||||
}
|
||||
try {
|
||||
const event = await this.$store.dispatch('sendPost', post)
|
||||
|
||||
this.reset()
|
||||
this.$emit('publish', event)
|
||||
|
||||
// TODO i18n
|
||||
const postType = this.replyTo.length ? 'Reply' : 'Post'
|
||||
this.$q.notify({
|
||||
message: `${postType} published`,
|
||||
color: 'positive',
|
||||
})
|
||||
} catch (e) {
|
||||
this.$q.notify({
|
||||
message: `Failed to publish post`,
|
||||
color: 'negative'
|
||||
})
|
||||
}
|
||||
this.publishing = false
|
||||
},
|
||||
buildTags() {
|
||||
const e = []
|
||||
const p = []
|
||||
for (const {id, pubkey} of this.replyTo) {
|
||||
e.push(['e', id])
|
||||
if (pubkey) {
|
||||
p.push(['p', pubkey])
|
||||
}
|
||||
}
|
||||
return e.concat(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -146,7 +168,19 @@ button.btn {
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
&-avatar {
|
||||
&-author {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.connector-top {
|
||||
height: 1rem;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.connector-line {
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
background: rgb(56, 68, 77);
|
||||
}
|
||||
}
|
||||
&-content {
|
||||
margin-left: 12px;
|
||||
@ -196,16 +230,27 @@ button.btn {
|
||||
&:hover {
|
||||
background-color: rgba($color: $color-primary, $alpha: 0.3);
|
||||
}
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
svg {
|
||||
fill: rgba($color: $color-primary, $alpha: 0.5);
|
||||
}
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&-compact {
|
||||
&-fake-submit {
|
||||
height: fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
&.compact {
|
||||
.input-section {
|
||||
textarea {
|
||||
padding: 10px 0;
|
||||
min-height: 48px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@ -215,13 +260,22 @@ button.btn {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&-collapsed {
|
||||
&.collapsed {
|
||||
.input-section textarea {
|
||||
min-height: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.controls {
|
||||
display: none;
|
||||
}
|
||||
> button {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.connector {
|
||||
.post-editor-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.post-editor-fake-submit {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="post"
|
||||
@click.stop="toEvent(post.id)"
|
||||
:class="{clickable}"
|
||||
@click.stop="clickable && toEvent(post.id)"
|
||||
>
|
||||
<div class="post-author">
|
||||
<div class="connector-top">
|
||||
@ -19,8 +20,8 @@
|
||||
<span>·</span>
|
||||
<span class="created-at">{{ moment(post.createdAt).fromNow() }}</span>
|
||||
</p>
|
||||
<p v-if="post.inReplyTo" class="in-reply-to">
|
||||
Replying to <a @click.stop="toEvent(post.inReplyTo)">{{ shorten(post.inReplyTo) }}</a>
|
||||
<p v-if="ancestor" class="in-reply-to">
|
||||
Replying to <a @click.stop="toEvent(ancestor)">{{ shorten(ancestor) }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="post-content-body">
|
||||
@ -28,16 +29,16 @@
|
||||
<BaseMarkdown :content="post.content" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="post-content-actions">
|
||||
<div class="action-item comment">
|
||||
<div v-if="actions" class="post-content-actions">
|
||||
<div class="action-item comment" @click.stop="$store.dispatch('createPost', {replyTo})">
|
||||
<BaseIcon icon="comment" />
|
||||
<span>{{ numComments }}</span>
|
||||
</div>
|
||||
<div class="action-item repost">
|
||||
<div class="action-item repost" @click.stop>
|
||||
<BaseIcon icon="repost" />
|
||||
<span>{{ post.stats.reposts }}</span>
|
||||
</div>
|
||||
<div class="action-item like">
|
||||
<div class="action-item like" @click.stop>
|
||||
<BaseIcon icon="like" />
|
||||
<span>{{ post.stats.likes }}</span>
|
||||
</div>
|
||||
@ -77,7 +78,7 @@ function postFromEvent(event) {
|
||||
author: event.pubkey,
|
||||
createdAt: event.created_at * 1000,
|
||||
content: event.interpolated.text,
|
||||
inReplyTo: event.interpolated.replyEvents[event.interpolated.replyEvents.length - 1],
|
||||
replyTo: event.interpolated.replyEvents,
|
||||
images: [],
|
||||
stats: {
|
||||
comments: '',
|
||||
@ -109,13 +110,26 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
post: postFromEvent(this.event),
|
||||
}
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
post() {
|
||||
return postFromEvent(this.event)
|
||||
},
|
||||
ancestor() {
|
||||
if (this.post.replyTo.length === 0) return
|
||||
return this.post.replyTo[this.post.replyTo.length - 1]
|
||||
},
|
||||
replyTo() {
|
||||
return this.post.replyTo.concat([this.post.id])
|
||||
},
|
||||
numComments() {
|
||||
return countRepliesRecursive(this.event)
|
||||
},
|
||||
@ -134,11 +148,12 @@ export default {
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
transition: 100ms ease background-color;
|
||||
cursor: pointer;
|
||||
border-bottom: $border-dark;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color: $color-dark-gray, $alpha: 0.2);
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgba($color: $color-dark-gray, $alpha: 0.2);
|
||||
}
|
||||
}
|
||||
&-author {
|
||||
display: flex;
|
||||
@ -157,10 +172,6 @@ export default {
|
||||
margin: auto;
|
||||
background: rgb(56, 68, 77);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
&-content {
|
||||
margin-left: 12px;
|
||||
|
@ -6,6 +6,8 @@
|
||||
:event="event"
|
||||
:connector-top="events.length > 1 && index > 0"
|
||||
:connector-bottom="(events.length > 1 && index < events.length - 1) || forceBottomConnector"
|
||||
actions
|
||||
clickable
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -29,7 +31,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log(`Thread (${this.events.length}) ${this.events[0].content.substr(0, 100)}`, this.events)
|
||||
//console.log(`Thread (${this.events.length}) ${this.events[0].content.substr(0, 100)}`, this.events)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -462,7 +462,5 @@ export async function createPost(store, options) {
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO options
|
||||
store.state.postDialogOpen = true
|
||||
store.commit('openPostDialog', options)
|
||||
}
|
||||
|
@ -184,3 +184,13 @@ export function dismissSignInDialog(state) {
|
||||
state.signInFailure = null
|
||||
state.signInDialogOpen = false
|
||||
}
|
||||
|
||||
export function openPostDialog(state, params) {
|
||||
state.postDialogParams = params
|
||||
state.postDialogOpen = true
|
||||
}
|
||||
|
||||
export function dismissPostDialog(state) {
|
||||
state.postDialogParams = null
|
||||
state.postDialogOpen = false
|
||||
}
|
||||
|
@ -106,5 +106,6 @@ export default function () {
|
||||
signInFailure: null,
|
||||
|
||||
postDialogOpen: false,
|
||||
postDialogParams: {},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user