cubash-archive/frontend/src/components/routes/Thread.vue

534 lines
13 KiB
Vue

<template>
<div class='route_container' :style='posts.length ? "padding-bottom: 8.5rem;" : null'>
<confirm-modal v-model='showConfirmModal' @confirm='deleteThread' text='Delete' color='red'>
Are you sure you want to delete this thread?
<br>This <b>cannot</b> be undone
</confirm-modal>
<thread-post-notification
v-if='$store.state.thread.postNotification'
:post='$store.state.thread.postNotification'
@close='$store.commit("thread/setPostNotification", null)'
@goToPost='goToPostNotification'
></thread-post-notification>
<header class='thread_header'>
<div class='thread_header__thread_title' ref='title'>
{{thread}}
</div>
</header>
<div class='thread_side_bar'>
<loading-button
class='button--thin_text'
:class='{ "button--disabled" : !$store.state.thread.selectedPosts.length }'
:loading='false || $store.state.thread.removePostsButtonLoading'
:dark='true'
@click='removePosts'
v-if='$store.state.thread.showRemovePostsButton'
>
Remove posts ({{$store.state.thread.selectedPosts.length}})
</loading-button>
<menu-button
v-if='$store.state.admin'
:options='[
{ event: "lock_thread", value: $store.state.thread.locked ? "Unlock thread" : "Lock thread" },
{ event: "delete_thread", value: "Delete thread" },
{ event: "remove_posts", value: "Post selection remover" }
]'
@lock_thread='setThreadLockedState'
@remove_posts='setThreadSelectState'
@delete_thread='showConfirmModal = true'
>
<button class='button button--thin_text'>
<font-awesome-icon :icon='["fa", "cog"]' style='margin-right: 0.25rem;' />
Manage thread
</button>
</menu-button>
<button
class='button button--thin_text'
@click='replyThread'
v-if='!$store.state.thread.locked'
>
{{replyThreadButton}}
</button>
<post-scrubber
:posts='$store.state.thread.totalPostsCount'
:value='$route.params.post_number || 0'
@input='goToPost'
></post-scrubber>
</div>
<input-editor
v-model='editor'
:show='editorState'
:replyUsername='replyUsername'
:loading='$store.state.thread.editor.loading'
v-on:mentions='setMentions'
v-on:close='hideEditor'
v-on:submit='addPost'
>
</input-editor>
<div class='locked_thread' v-if='$store.state.thread.locked'>
<h2>Thread locked</h2>
You can't post in this thread because it has been locked by an administrator
</div>
<thread-poll
v-if='$store.state.thread.PollQuestionId'
:id='$store.state.thread.PollQuestionId'
></thread-poll>
<div class='posts'>
<scroll-load
@loadNext='loadNextPosts'
@loadPrevious='loadPreviousPosts'
>
<template v-if='!posts.length'>
<thread-post-placeholder
v-for='n in 3'
:key='"thread-post-placeholder-loading-" + n'
:class='{"post--last": n === 2}'
></thread-post-placeholder>
</template>
<template v-if='$store.state.thread.loadingPosts === "previous"'>
<thread-post-placeholder
v-for='n in $store.state.thread.previousPostsCount'
:key='"thread-post-placeholder-upper-" + n'
>
</thread-post-placeholder>
</template>
<thread-post
v-for='(post, index) in posts'
:key='"thread-post-" + post.id'
@reply='replyUser'
@goToPost='goToPost'
@selected='setSelectedPosts'
:post='post'
:show-reply='!$store.state.thread.locked'
:showSelect='$store.state.thread.showRemovePostsButton'
:highlight='highlightedPostIndex === index'
:allowQuote='true'
:class='{"post--last": index === posts.length-1}'
ref='posts'
></thread-post>
<template v-if='$store.state.thread.loadingPosts === "next"'>
<thread-post-placeholder
v-for='n in $store.state.thread.nextPostsCount'
:key='"thread-post-placeholder-lower-" + n'
>
</thread-post-placeholder>
</template>
</scroll-load>
</div>
<more-threads
:category='$store.state.thread.category'
:threadId='$store.state.thread.threadId'
v-if='$store.state.thread.category'
></more-threads>
</div>
</template>
<script>
import InputEditor from '../InputEditor'
import ScrollLoad from '../ScrollLoad'
import ThreadPost from '../ThreadPost'
import ThreadPostNotification from '../ThreadPostNotification'
import ThreadPostPlaceholder from '../ThreadPostPlaceholder'
import PostScrubber from '../PostScrubber'
import MenuButton from '../MenuButton'
import LoadingButton from '../LoadingButton'
import ThreadPoll from '../ThreadPoll'
import ConfirmModal from '../ConfirmModal'
import MoreThreads from '../MoreThreads'
import logger from '../../assets/js/logger'
import throttle from 'lodash.throttle'
export default {
name: 'Thread',
components: {
InputEditor,
ScrollLoad,
ThreadPost,
ThreadPostNotification,
ThreadPostPlaceholder,
PostScrubber,
MenuButton,
LoadingButton,
ThreadPoll,
ConfirmModal,
MoreThreads
},
data () {
return {
highlightedPostIndex: null,
postNotification: null,
showConfirmModal: false
}
},
computed: {
thread () {
return this.$store.state.thread.thread;
},
posts () {
return this.$store.getters.sortedPosts;
},
replyUsername () {
return this.$store.state.thread.reply.username
},
editor: {
get () { return this.$store.state.thread.editor.value },
set (val) {
this.$store.commit('setThreadEditorValue', val)
}
},
editorState () { return this.$store.state.thread.editor.show },
replyThreadButton () {
if(this.$store.state.username) {
return 'Reply to thread';
} else {
return 'Login to reply'
}
}
},
methods: {
deleteThread () {
this.$store.dispatch("deleteThread", this)
},
removePosts () {
this.$store.dispatch("removePostsAsync", this)
},
setThreadLockedState () {
this.$store.dispatch('setThreadLockedState', this)
},
setThreadSelectState () {
this.$store.commit('setShowRemovePostsButton', !this.$store.state.thread.showRemovePostsButton)
},
setSelectedPosts (postId) {
this.$store.commit('setSelectedPosts', postId)
},
showEditor () {
this.$store.commit('setThreadEditorState', true);
},
hideEditor () {
this.$store.commit('setThreadEditorState', false);
this.clearReply()
},
setMentions (mentions) {
this.$store.commit('setMentions', mentions)
},
clearReply () {
this.$store.commit({
type: 'setReply',
username: '',
id: ''
});
},
replyThread () {
if(this.$store.state.username) {
this.clearReply();
this.showEditor();
} else {
this.$store.commit('setAccountTabs', 1);
this.$store.commit('setAccountModalState', true);
}
},
replyUser (id, username, quote) {
this.$store.commit({
type: 'setReply',
username,
id,
quote
});
this.showEditor();
},
addPost () {
this.$store.dispatch('addPostAsync', this);
},
loadNextPosts () {
let vue = this
this.$store.dispatch('loadPostsAsync', { vue, previous: false });
},
loadPreviousPosts () {
let vue = this
this.$store.dispatch('loadPostsAsync', { vue, previous: true });
},
loadInitialPosts () {
this.$store.dispatch('loadInitialPostsAsync', this)
},
goToPost (number, getPostNumber) {
let pushRoute = postNumber => {
//If postNumber is a post in `this.posts`
if(this.posts.find(post => post.postNumber === postNumber)) {
this.highlightPost(postNumber)
} else {
this.$router.replace({ name: 'thread-post', params: { post_number: postNumber } })
this.loadInitialPosts()
}
}
//If `number` is actually the postId
//Get the postNumber via api request
if(getPostNumber) {
this.axios
.get('/api/v1/forums/post/' + number)
.then( res => pushRoute(res.data.postNumber) )
} else {
pushRoute(number)
}
},
scrollTo (postNumber, cb) {
this.$nextTick(() => {
let getScrollTopPosition = i => {
let postTop = this.$refs.posts[i].$el.getBoundingClientRect().top
let header = this.$refs.title.getBoundingClientRect().height
return window.pageYOffset + postTop - header - 32
}
let scroll = (i) => {
let post = this.posts[i]
window.scrollTo(0, getScrollTopPosition(i))
if(cb) cb(i, post)
}
for(var i = 0; i < this.posts.length; i++) {
if(this.posts[i].postNumber === postNumber) {
if(this.$refs.posts) {
scroll(i)
} else {
this.$nextTick(() => scroll(i))
}
break;
}
}
})
},
highlightPost (postNumber) {
this.scrollTo(postNumber, (i) => {
this.highlightedPostIndex = i
this.$router.replace({ name: 'thread-post', params: { post_number: postNumber } })
if(this.highlightedPostIndex === i) {
setTimeout(() => this.highlightedPostIndex = null, 3000)
}
})
},
showPostNotification (post) {
if(post.username === this.$store.state.username) return;
this.$store.commit('thread/setPostNotification', null)
this.$store.commit('thread/setPostNotification', post)
setTimeout(() => {
this.$store.commit('thread/setPostNotification', null)
}, 5000)
},
goToPostNotification () {
let post = this.$store.state.thread.postNotification
this.goToPost(post.postNumber)
this.$store.commit('thread/setPostNotification', null)
}
},
watch: {
'$route.params.id': 'loadInitialPosts'
},
mounted () {
let self = this;
let postInView = function() {
if(!self.$refs.posts) return;
let posts = self.$refs.posts.sort((a, b) => {
return a.post.postNumber - b.post.postNumber;
});
let topPostInView = posts.find(post => {
let rect = post.$el.getBoundingClientRect()
return (rect.top >= 74) && (rect.bottom <= window.innerHeight)
})
let postIndex = posts.indexOf(topPostInView)
if(postIndex > -1) {
let postNumber = self.posts[postIndex].postNumber
self.$router.replace({ name: 'thread-post', params: { post_number: postNumber } })
}
};
document.addEventListener('scroll', throttle(postInView, 20));
this.loadInitialPosts()
this.$socket.emit('join', 'thread/' + this.$route.params.id)
this.$socket.on('new post', post => {
this.showPostNotification(post)
this.$store.dispatch('loadNewPostsSinceLoad', post)
})
logger('thread', this.$route.params.id)
},
destroyed () {
this.$socket.emit('leave', 'thread/' + this.$route.params.id)
this.$socket.off('new post')
}
}
</script>
<style lang='scss' scoped>
@import '../../assets/scss/variables.scss';
.thread_side_bar {
align-items: flex-start;
display: flex;
flex-direction: column;
min-width: 10rem;
position: fixed;
right: 10%;
.button {
margin-bottom: 0.75rem;
height: 3rem;
}
}
.thread_header {
display: flex;
justify-content: space-between;
@at-root #{&}__thread_title {
@include text($font--role-default, 2rem, 400);
width: 80%;
margin-bottom: 1rem;
word-break: break-all;
}
}
.locked_thread {
h2 {
margin-top: 0;
margin-bottom: 0.5rem;
}
padding: 1.5rem;
margin-bottom: 1rem;
width: 80%;
border-radius: 0.25rem;
}
.posts {
width: 80%;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
@media (min-width: 1500px) {
.thread_side_bar {
right: calc((100% - 1200px) / 2);
}
}
@media (max-width: $breakpoint--phone-thread) {
.route_container {
width: 100%;
margin: 0;
}
.thread_side_bar {
margin-left: 1rem;
}
.posts {
padding: 0.25rem 0.5rem;
border-radius: 0;
border-left: 0;
border-right: 0;
}
div.thread_header__thread_title {
padding-left: 1rem;
}
}
@media (max-width: $breakpoint--large_screen-thread) {
.posts {
width: calc(80% - 5rem);
}
}
@media (min-width: $breakpoint--phone-thread) and (max-width: $breakpoint--tablet-thread) {
div.posts {
margin-left: 2rem;
margin-right: 2rem;
width: calc(100% - 4rem);
}
div.thread_header__thread_title {
font-size: 2rem;
padding-left: 2.25rem;
width: 100%;
}
div.thread_side_bar {
padding-left: 2rem;
&> :first-child {
margin-left: 0;
}
}
}
@media (max-width: $breakpoint--tablet-thread) {
.route_container {
padding-bottom: 2rem ;
}
.thread_side_bar {
display: flex;
position: initial;
flex-direction: row;
align-items: flex-end;
margin-left: 1rem;
& > :first-child {
margin-left: 0;
}
> * {
margin: 0 0.5rem;
}
.post_scrubber {
display: none;
}
}
.posts {
width: 100%;
overflow: hidden;
}
.locked_thread {
width: 100%;
}
.thread_header__thread_title {
font-size: 2rem;
padding-left: 0.25rem;
width: 100%;
}
}
</style>