123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- <template>
- <div>
- <div class="container p-0 overflow-hidden">
- <div class="row">
- <div class="col-12 col-md-6 offset-md-3">
- <div class="card shadow-none border" style="height:100vh;">
- <div class="card-header d-flex justify-content-between align-items-center">
- <div
- @click="commentNavigateBack(status.id)"
- class="cursor-pointer"
- >
- <i class="fas fa-chevron-left fa-lg px-2"></i>
- </div>
- <div>
- <p class="font-weight-bold mb-0 h5">Comments</p>
- </div>
- <div>
- <i class="fas fa-cog fa-lg text-white"></i>
- </div>
- </div>
- <div class="card-body" style="overflow-y: auto !important">
- <div class="media">
- <img :src="status.account.avatar" class="rounded-circle border mr-3" width="32px" height="32px">
- <div class="media-body">
- <p class="d-flex justify-content-between align-items-top mb-0" style="overflow-y: hidden;">
- <span class="mr-2" style="font-size: 13px;">
- <a class="text-dark font-weight-bold mr-1 text-break" :href="status.account.url" v-bind:title="status.account.username">{{trimCaption(status.account.username,15)}}</a>
- <span class="text-break comment-body" style="word-break: break-all;" v-html="status.content"></span>
- </span>
- </p>
- </div>
- </div>
- <hr>
- <div class="postCommentsLoader text-center py-2">
- <div class="spinner-border" role="status">
- <span class="sr-only">Loading...</span>
- </div>
- </div>
- <div class="postCommentsContainer d-none">
- <p v-if="replies.length" class="mb-1 text-center load-more-link my-4">
- <a
- href="#"
- class="text-dark"
- title="Load more comments"
- @click.prevent="loadMoreComments"
- >
- <svg class="bi bi-plus-circle" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" style="font-size:2em;"> <path fill-rule="evenodd" d="M8 3.5a.5.5 0 01.5.5v4a.5.5 0 01-.5.5H4a.5.5 0 010-1h3.5V4a.5.5 0 01.5-.5z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M7.5 8a.5.5 0 01.5-.5h4a.5.5 0 010 1H8.5V12a.5.5 0 01-1 0V8z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M8 15A7 7 0 108 1a7 7 0 000 14zm0 1A8 8 0 108 0a8 8 0 000 16z" clip-rule="evenodd"/></svg>
- </a>
- </p>
- <div v-if="replies.length" v-for="(reply, index) in replies" class="pb-3 media" :key="'tl' + reply.id + '_' + index">
- <img :src="reply.account.avatar" class="rounded-circle border mr-3" width="32px" height="32px">
- <div class="media-body">
- <div v-if="reply.sensitive == true">
- <span class="py-3">
- <a class="text-dark font-weight-bold mr-3" style="font-size: 13px;" :href="reply.account.url" v-bind:title="reply.account.username">{{trimCaption(reply.account.username,15)}}</a>
- <span class="text-break" style="font-size: 13px;">
- <span class="font-italic text-muted">This comment may contain sensitive material</span>
- <span class="text-primary cursor-pointer pl-1" @click="reply.sensitive = false;">Show</span>
- </span>
- </span>
- </div>
- <div v-else>
- <p class="d-flex justify-content-between align-items-top read-more mb-0" style="overflow-y: hidden;">
- <span class="mr-3" style="font-size: 13px;">
- <a class="text-dark font-weight-bold mr-1 text-break" :href="reply.account.url" v-bind:title="reply.account.username">{{trimCaption(reply.account.username,15)}}</a>
- <span class="text-break comment-body" style="word-break: break-all;" v-html="reply.content"></span>
- </span>
- <span class="text-right" style="min-width: 30px;">
- <span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
- <span class="pl-2 text-lighter cursor-pointer" @click="ctxMenu(reply)">
- <span class="fas fa-ellipsis-v text-lighter"></span>
- </span>
- </span>
- </p>
- <p class="mb-0">
- <a v-once class="text-muted mr-3 text-decoration-none small" style="width: 20px;" v-text="timeAgo(reply.created_at)" :href="reply.url"></a>
- <span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3 small">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
- <span class="small text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply, index, true)">Reply</span>
- </p>
- <div v-if="reply.reply_count > 0" class="cursor-pointer pb-2" v-on:click="toggleReplies(reply)">
- <span class="show-reply-bar"></span>
- <span class="comment-reaction small font-weight-bold">{{reply.thread ? 'Hide' : 'View'}} Replies ({{reply.reply_count}})</span>
- </div>
- <div v-if="reply.thread == true" class="comment-thread">
- <div v-for="(s, sindex) in reply.replies" class="py-1 media" :key="'cr' + s.id + '_' + index">
- <img :src="s.account.avatar" class="rounded-circle border mr-3" width="25px" height="25px">
- <div class="media-body">
- <p class="d-flex justify-content-between align-items-top read-more mb-0" style="overflow-y: hidden;">
- <span class="mr-2" style="font-size: 13px;">
- <a class="text-dark font-weight-bold mr-1" :href="s.account.url" :title="s.account.username">{{s.account.username}}</a>
- <span class="text-break comment-body" style="word-break: break-all;" v-html="s.content"></span>
- </span>
- <span>
- <span v-on:click="likeReply(s, $event)"><i v-bind:class="[s.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
- </span>
- </p>
- <p class="mb-0">
- <a v-once class="text-muted mr-3 text-decoration-none small" style="width: 20px;" v-text="timeAgo(s.created_at)" :href="s.url"></a>
- <span v-if="s.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{s.favourites_count == 1 ? '1 like' : s.favourites_count + ' likes'}}</span>
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-if="!replies.length">
- <p class="text-center text-muted font-weight-bold small">No comments yet</p>
- </div>
- </div>
- </div>
- <div class="card-footer mb-3">
- <div class="align-middle d-flex">
- <img
- :src="profile.avatar"
- width="36"
- height="36"
- class="rounded-circle border mr-3">
- <textarea
- class="form-control rounded-pill"
- name="comment"
- placeholder="Add a comment…"
- autocomplete="off"
- autocorrect="off"
- rows="1"
- maxlength="0"
- style="resize: none;overflow-y: hidden"
- @click="replyFocus(status)">
- </textarea>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <context-menu
- ref="cMenu"
- :status="ctxMenuStatus"
- :profile="profile"
- />
- <b-modal ref="replyModal"
- id="ctx-reply-modal"
- hide-footer
- centered
- rounded
- :title-html="status.account ? 'Reply to <span class=text-dark>' + status.account.username + '</span>' : ''"
- title-tag="p"
- title-class="font-weight-bold text-muted"
- size="md"
- body-class="p-2 rounded">
- <div>
- <vue-tribute :options="tributeSettings">
- <textarea
- class="form-control replyModalTextarea"
- rows="4"
- v-model="replyText">
- </textarea>
- </vue-tribute>
- <div class="border-top border-bottom my-2">
- <ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
- <li class="nav-item" v-on:click="emojiReaction(status)" v-for="e in emoji">{{e}}</li>
- </ul>
- </div>
- <div class="d-flex justify-content-between align-items-center">
- <div>
- <span class="pl-2 small text-muted font-weight-bold text-monospace">
- <span :class="[replyText.length > config.uploader.max_caption_length ? 'text-danger':'text-dark']">{{replyText.length > config.uploader.max_caption_length ? config.uploader.max_caption_length - replyText.length : replyText.length}}</span>/{{config.uploader.max_caption_length}}
- </span>
- </div>
- <div class="d-flex align-items-center">
- <div class="custom-control custom-switch mr-3">
- <input type="checkbox" class="custom-control-input" id="replyModalCWSwitch" v-model="replyNsfw">
- <label :class="[replyNsfw ? 'custom-control-label font-weight-bold text-dark':'custom-control-label text-lighter']" for="replyModalCWSwitch">Mark as NSFW</label>
- </div>
- <button class="btn btn-primary btn-sm py-2 px-4 lead text-uppercase font-weight-bold" v-on:click.prevent="commentSubmit(status, $event)" :disabled="replyText.length == 0">
- {{replySending == true ? 'POSTING' : 'POST'}}
- </button>
- </div>
- </div>
- </div>
- </b-modal>
- </div>
- </template>
- <script type="text/javascript">
- import ContextMenu from './ContextMenu.vue';
- export default {
- props: ['status', 'profile'],
- components: {
- "context-menu": ContextMenu
- },
- data() {
- return {
- ids: [],
- config: window.App.config,
- tributeSettings: {
- collection: [
- {
- trigger: '@',
- menuShowMinLength: 2,
- values: (function (text, cb) {
- let url = '/api/compose/v0/search/mention';
- axios.get(url, { params: { q: text }})
- .then(res => {
- cb(res.data);
- })
- .catch(err => {
- console.log(err);
- })
- })
- },
- {
- trigger: '#',
- menuShowMinLength: 2,
- values: (function (text, cb) {
- let url = '/api/compose/v0/search/hashtag';
- axios.get(url, { params: { q: text }})
- .then(res => {
- cb(res.data);
- })
- .catch(err => {
- console.log(err);
- })
- })
- }
- ]
- },
- replies: [],
- replyId: null,
- replyText: '',
- replyNsfw: false,
- replySending: false,
- pagination: {},
- ctxMenuStatus: false,
- emoji: window.App.util.emoji
- }
- },
- beforeMount() {
- this.fetchComments();
- },
- methods: {
- commentNavigateBack(id) {
- $('nav').show();
- $('footer').show();
- $('.mobile-footer-spacer').attr('style', 'display:block');
- $('.mobile-footer').attr('style', 'display:block');
- this.$emit('current-layout', 'feed');
- let path = '/';
- window.history.pushState({}, '', path);
- },
- trimCaption(caption, len = 60) {
- return _.truncate(caption, {
- length: len
- });
- },
- replyFocus(e, index, prependUsername = false) {
- if($('body').hasClass('loggedIn') == false) {
- this.redirect('/login?next=' + encodeURIComponent(window.location.pathname));
- return;
- }
- if(this.status.comments_disabled) {
- return;
- }
- this.replyToIndex = index;
- this.replyingToId = e.id;
- this.replyingToUsername = e.account.username;
- this.reply_to_profile_id = e.account.id;
- let username = e.account.local ? '@' + e.account.username + ' '
- : '@' + e.account.acct + ' ';
- if(prependUsername == true) {
- this.replyText = username;
- }
- this.$refs.replyModal.show();
- setTimeout(function() {
- $('.replyModalTextarea').focus();
- }, 500);
- },
- commentSubmit(status, $event) {
- this.replySending = true;
- let id = status.id;
- let comment = this.replyText;
- let limit = this.config.uploader.max_caption_length;
- if(comment.length > limit) {
- this.replySending = false;
- swal('Comment Too Long', 'Please make sure your comment is '+limit+' characters or less.', 'error');
- return;
- }
- axios.post('/i/comment', {
- item: id,
- comment: comment,
- sensitive: this.replyNsfw
- }).then(res => {
- this.replyText = '';
- this.replies.push(res.data.entity);
- this.$refs.replyModal.hide();
- });
- this.replySending = false;
- },
- timeAgo(ts) {
- return App.util.format.timeAgo(ts);
- },
- fetchComments() {
- console.log('Fetching comments...');
- let url = '/api/v2/comments/'+this.status.account.id+'/status/'+this.status.id;
- axios.get(url)
- .then(res => {
- this.replies = res.data.data;
- this.pagination = res.data.meta.pagination;
- }).catch(error => {
- if(!error.response) {
- $('.postCommentsLoader .lds-ring')
- .attr('style','width:100%')
- .addClass('pt-4 font-weight-bold text-muted')
- .text('An error occurred, cannot fetch comments. Please try again later.');
- } else {
- switch(error.response.status) {
- case 401:
- $('.postCommentsLoader .lds-ring')
- .attr('style','width:100%')
- .addClass('pt-4 font-weight-bold text-muted')
- .text('Please login to view.');
- break;
- default:
- $('.postCommentsLoader .lds-ring')
- .attr('style','width:100%')
- .addClass('pt-4 font-weight-bold text-muted')
- .text('An error occurred, cannot fetch comments. Please try again later.');
- break;
- }
- }
- });
- },
- loadMoreComments() {
- if(this.pagination.total_pages == 1 || this.pagination.current_page == this.pagination.total_pages) {
- $('.load-more-link').addClass('d-none');
- return;
- }
- $('.load-more-link').addClass('d-none');
- $('.postCommentsLoader').removeClass('d-none');
- let next = this.pagination.links.next;
- axios.get(next)
- .then(response => {
- let self = this;
- let res = response.data.data;
- $('.postCommentsLoader').addClass('d-none');
- for(let i=0; i < res.length; i++) {
- this.replies.unshift(res[i]);
- }
- this.pagination = response.data.meta.pagination;
- $('.load-more-link').removeClass('d-none');
- });
- },
- likeReply(status, $event) {
- if($('body').hasClass('loggedIn') == false) {
- swal('Login', 'Please login to perform this action.', 'info');
- return;
- }
- axios.post('/i/like', {
- item: status.id
- }).then(res => {
- status.favourites_count = res.data.count;
- if(status.favourited == true) {
- status.favourited = false;
- } else {
- status.favourited = true;
- }
- }).catch(err => {
- swal('Error', 'Something went wrong, please try again later.', 'error');
- });
- },
- ctxMenu(status) {
- this.ctxMenuStatus = status;
- this.$refs.cMenu.open();
- }
- }
- }
- </script>
- <style type="text/css" scoped>
- .emoji-reactions .nav-item {
- font-size: 1.2rem;
- padding: 9px;
- cursor: pointer;
- }
- .emoji-reactions::-webkit-scrollbar {
- width: 0px;
- height: 0px;
- background: transparent;
- }
- </style>
|