<template> <div class="timeline-section-component"> <div v-if="!isLoaded"> <status-placeholder /> <status-placeholder /> <status-placeholder /> <status-placeholder /> </div> <div v-else> <transition name="fade"> <div v-if="showReblogBanner && getScope() === 'home'" class="card bg-g-amin card-body shadow-sm mb-3" style="border-radius: 15px;"> <div class="d-flex justify-content-around align-items-center"> <div class="flex-grow-1 ft-std"> <h2 class="font-weight-bold text-white mb-0">Introducing Reblogs in feeds</h2> <hr /> <p class="lead text-white mb-0"> See reblogs from accounts you follow in your home feed! </p> <p class="text-white small mb-1" style="opacity:0.6"> You can disable reblogs in feeds on the Timeline Settings page. </p> <hr /> <div class="d-flex"> <button class="btn btn-light rounded-pill font-weight-bold btn-block mr-2" @click.prevent="enableReblogs()"> <template v-if="!enablingReblogs">Show reblogs in home feed</template> <b-spinner small v-else /> </button> <button class="btn btn-outline-light rounded-pill font-weight-bold px-5" @click.prevent="hideReblogs()">Hide</button> </div> </div> </div> </div> </transition> <status v-for="(status, index) in feed" :key="'pf_feed:' + status.id + ':idx:' + index + ':fui:' + forceUpdateIdx" :status="status" :profile="profile" v-on:like="likeStatus(index)" v-on:unlike="unlikeStatus(index)" v-on:share="shareStatus(index)" v-on:unshare="unshareStatus(index)" v-on:menu="openContextMenu(index)" v-on:counter-change="counterChange(index, $event)" v-on:likes-modal="openLikesModal(index)" v-on:shares-modal="openSharesModal(index)" v-on:follow="follow(index)" v-on:unfollow="unfollow(index)" v-on:comment-likes-modal="openCommentLikesModal" v-on:handle-report="handleReport" v-on:bookmark="handleBookmark(index)" v-on:mod-tools="handleModTools(index)" /> <div v-if="showLoadMore" class="text-center"> <button class="btn btn-primary rounded-pill font-weight-bold" @click="tryToLoadMore"> Load more </button> </div> <div v-if="canLoadMore"> <intersect @enter="enterIntersect"> <status-placeholder style="margin-bottom: 10rem;"/> </intersect> </div> <div v-if="!isLoaded && feed.length && endFeedReached" style="margin-bottom: 50vh"> <div class="card card-body shadow-sm mb-3" style="border-radius: 15px;"> <p class="display-4 text-center">✨</p> <p class="lead mb-0 text-center">You have reached the end of this feed</p> </div> </div> <timeline-onboarding v-if="scope == 'home' && !feed.length" :profile="profile" v-on:update-profile="updateProfile" /> <empty-timeline v-if="isLoaded && scope !== 'home' && !feed.length" /> </div> <context-menu v-if="showMenu" ref="contextMenu" :status="feed[postIndex]" :profile="profile" v-on:moderate="commitModeration" v-on:delete="deletePost" v-on:report-modal="handleReport" v-on:edit="handleEdit" v-on:muted="handleMuted" v-on:unfollow="handleUnfollow" /> <likes-modal v-if="showLikesModal" ref="likesModal" :status="likesModalPost" :profile="profile" /> <shares-modal v-if="showSharesModal" ref="sharesModal" :status="sharesModalPost" :profile="profile" /> <report-modal ref="reportModal" :key="reportedStatusId" :status="reportedStatus" /> <post-edit-modal ref="editModal" v-on:update="mergeUpdatedPost" /> </div> </template> <script type="text/javascript"> import StatusPlaceholder from './../partials/StatusPlaceholder.vue'; import Status from './../partials/TimelineStatus.vue'; import Intersect from 'vue-intersect'; import ContextMenu from './../partials/post/ContextMenu.vue'; import LikesModal from './../partials/post/LikeModal.vue'; import SharesModal from './../partials/post/ShareModal.vue'; import ReportModal from './../partials/modal/ReportPost.vue'; import EmptyTimeline from './../partials/placeholders/EmptyTimeline.vue' import TimelineOnboarding from './../partials/placeholders/TimelineOnboarding.vue' import PostEditModal from './../partials/post/PostEditModal.vue'; export default { props: { scope: { type: String, default: "home" }, profile: { type: Object }, refresh: { type: Boolean, default: false } }, components: { "intersect": Intersect, "status-placeholder": StatusPlaceholder, "status": Status, "context-menu": ContextMenu, "likes-modal": LikesModal, "shares-modal": SharesModal, "report-modal": ReportModal, "empty-timeline": EmptyTimeline, "timeline-onboarding": TimelineOnboarding, "post-edit-modal": PostEditModal }, data() { return { settings: [], isLoaded: false, feed: [], ids: [], max_id: 0, canLoadMore: true, showLoadMore: false, loadMoreTimeout: undefined, loadMoreAttempts: 0, isFetchingMore: false, endFeedReached: false, postIndex: 0, showMenu: false, showLikesModal: false, likesModalPost: {}, showReportModal: false, reportedStatus: {}, reportedStatusId: 0, showSharesModal: false, sharesModalPost: {}, forceUpdateIdx: 0, showReblogBanner: false, enablingReblogs: false, baseApi: '/api/v1/timelines/', } }, mounted() { if(window.App.config.features.hasOwnProperty('timelines')) { if(this.scope == 'local' && !window.App.config.features.timelines.local) { swal('Error', 'Cannot load this timeline', 'error'); return; }; if(this.scope == 'network' && !window.App.config.features.timelines.network) { swal('Error', 'Cannot load this timeline', 'error'); return; }; } if(window.App.config.ab.hasOwnProperty('cached_home_timeline')) { const cht = window.App.config.ab.cached_home_timeline == true; this.baseApi = cht ? '/api/v1/timelines/' : '/api/v1/timelines/'; } this.fetchSettings(); }, methods: { getScope() { switch(this.scope) { case 'local': return 'public' break; case 'global': return 'network' break; default: return 'home'; break; } }, fetchSettings() { axios.get('/api/pixelfed/v1/web/settings') .then(res => { this.settings = res.data; if(!res.data) { this.showReblogBanner = true; } else { if(res.data.hasOwnProperty('hide_reblog_banner')) { } else if(res.data.hasOwnProperty('enable_reblogs')) { if(!res.data.enable_reblogs) { this.showReblogBanner = true; } } else { this.showReblogBanner = true; } } }) .finally(() => { this.fetchTimeline(); }) }, fetchTimeline(scrollToTop = false) { let url, params; if(this.getScope() === 'home' && this.settings && this.settings.hasOwnProperty('enable_reblogs') && this.settings.enable_reblogs) { url = this.baseApi + `home`; params = { '_pe': 1, max_id: this.max_id, limit: 6, include_reblogs: true, } } else { url = this.baseApi + this.getScope(); if(this.max_id === 0) { params = { min_id: 1, limit: 6, '_pe': 1, } } else { params = { max_id: this.max_id, limit: 6, '_pe': 1, } } } if(this.getScope() === 'network') { params.remote = true; url = this.baseApi + `public`; } axios.get(url, { params: params }).then(res => { let ids = res.data.map(p => { if(p && p.hasOwnProperty('relationship')) { this.$store.commit('updateRelationship', [p.relationship]); } return p.id }); this.isLoaded = true; if(res.data.length == 0) { return; } this.ids = ids; this.max_id = Math.min(...ids); this.feed = res.data; if(res.data.length < 4) { this.canLoadMore = false; this.showLoadMore = true; } }) .then(() => { if(scrollToTop) { this.$nextTick(() => { window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); this.$emit('refreshed'); }); } }) }, enterIntersect() { if(this.isFetchingMore) { return; } this.isFetchingMore = true; let url, params; if(this.getScope() === 'home' && this.settings && this.settings.hasOwnProperty('enable_reblogs') && this.settings.enable_reblogs) { url = this.baseApi + `home`; params = { '_pe': 1, max_id: this.max_id, limit: 6, include_reblogs: true, } } else { url = this.baseApi + this.getScope(); params = { max_id: this.max_id, limit: 6, '_pe': 1, } } if(this.getScope() === 'network') { params.remote = true; url = this.baseApi + `public`; } axios.get(url, { params: params }).then(res => { if(!res.data.length) { this.endFeedReached = true; this.canLoadMore = false; this.isFetchingMore = false; } setTimeout(() => { res.data.forEach(p => { if(this.ids.indexOf(p.id) == -1) { if(this.max_id > p.id) { this.max_id = p.id; } this.ids.push(p.id); this.feed.push(p); if(p && p.hasOwnProperty('relationship')) { this.$store.commit('updateRelationship', [p.relationship]); } } }); this.isFetchingMore = false; }, 100); }); }, tryToLoadMore() { this.loadMoreAttempts++; if(this.loadMoreAttempts >= 3) { this.showLoadMore = false; } this.showLoadMore = false; this.canLoadMore = true; this.loadMoreTimeout = setTimeout(() => { this.canLoadMore = false; this.showLoadMore = true; }, 5000); }, likeStatus(index) { let status = this.feed[index]; if(status.reblog) { status = status.reblog; let state = status.favourited; let count = status.favourites_count; this.feed[index].reblog.favourites_count = count + 1; this.feed[index].reblog.favourited = !status.favourited; } else { let state = status.favourited; let count = status.favourites_count; this.feed[index].favourites_count = count + 1; this.feed[index].favourited = !status.favourited; } axios.post('/api/v1/statuses/' + status.id + '/favourite') .then(res => { // }).catch(err => { if(status.reblog) { this.feed[index].reblog.favourites_count = count; this.feed[index].reblog.favourited = false; } else { this.feed[index].favourites_count = count; this.feed[index].favourited = false; } let el = document.createElement('p'); el.classList.add('text-left'); el.classList.add('mb-0'); el.innerHTML = '<span class="lead">We limit certain interactions to keep our community healthy and it appears that you have reached that limit. <span class="font-weight-bold">Please try again later.</span></span>'; let wrapper = document.createElement('div'); wrapper.appendChild(el); if(err.response.status === 429) { swal({ title: 'Too many requests', content: wrapper, icon: 'warning', buttons: { // moreInfo: { // text: "Contact a human", // visible: true, // value: "more", // className: "text-lighter bg-transparent border" // }, confirm: { text: "OK", value: false, visible: true, className: "bg-transparent primary", closeModal: true } } }) .then((val) => { if(val == 'more') { location.href = '/site/contact' } return; }); } }) }, unlikeStatus(index) { let status = this.feed[index]; if(status.reblog) { status = status.reblog; let state = status.favourited; let count = status.favourites_count; this.feed[index].reblog.favourites_count = count - 1; this.feed[index].reblog.favourited = !status.favourited; } else { let state = status.favourited; let count = status.favourites_count; this.feed[index].favourites_count = count - 1; this.feed[index].favourited = !status.favourited; } axios.post('/api/v1/statuses/' + status.id + '/unfavourite') .then(res => { // }).catch(err => { if(status.reblog && status.pf_type == 'share') { this.feed[index].reblog.favourites_count = count; this.feed[index].reblog.favourited = false; } else { this.feed[index].favourites_count = count; this.feed[index].favourited = false; } }) }, openContextMenu(idx) { this.postIndex = idx; this.showMenu = true; this.$nextTick(() => { this.$refs.contextMenu.open(); }); }, handleModTools(idx) { this.postIndex = idx; this.showMenu = true; this.$nextTick(() => { this.$refs.contextMenu.openModMenu(); }); }, openLikesModal(idx) { this.postIndex = idx; let post = this.feed[this.postIndex]; this.likesModalPost = post.reblog ? post.reblog : post; this.showLikesModal = true; this.$nextTick(() => { this.$refs.likesModal.open(); }); }, openSharesModal(idx) { this.postIndex = idx; let post = this.feed[this.postIndex]; this.sharesModalPost = post.reblog ? post.reblog : post; this.showSharesModal = true; this.$nextTick(() => { this.$refs.sharesModal.open(); }); }, commitModeration(type) { let idx = this.postIndex; switch(type) { case 'addcw': this.feed[idx].sensitive = true; break; case 'remcw': this.feed[idx].sensitive = false; break; case 'unlist': this.feed.splice(idx, 1); break; case 'spammer': let id = this.feed[idx].account.id; this.feed = this.feed.filter(post => { return post.account.id != id; }); break; } }, deletePost() { this.feed.splice(this.postIndex, 1); this.forceUpdateIdx++; }, counterChange(index, type) { let post = this.feed[index]; switch(type) { case 'comment-increment': if(post.reblog != null) { this.feed[index].reblog.reply_count = this.feed[index].reblog.reply_count + 1; } else { this.feed[index].reply_count = this.feed[index].reply_count + 1; } break; case 'comment-decrement': if(post.reblog != null) { this.feed[index].reblog.reply_count = this.feed[index].reblog.reply_count - 1; } else { this.feed[index].reply_count = this.feed[index].reply_count - 1; } break; } }, openCommentLikesModal(post) { if(post.reblog != null) { this.likesModalPost = post.reblog; } else { this.likesModalPost = post; } this.showLikesModal = true; this.$nextTick(() => { this.$refs.likesModal.open(); }); }, shareStatus(index) { let status = this.feed[index]; if(status.reblog) { status = status.reblog; let state = status.reblogged; let count = status.reblogs_count; this.feed[index].reblog.reblogs_count = count + 1; this.feed[index].reblog.reblogged = !status.reblogged; } else { let state = status.reblogged; let count = status.reblogs_count; this.feed[index].reblogs_count = count + 1; this.feed[index].reblogged = !status.reblogged; } axios.post('/api/v1/statuses/' + status.id + '/reblog') .then(res => { // }).catch(err => { if(status.reblog) { this.feed[index].reblog.reblogs_count = count; this.feed[index].reblog.reblogged = false; } else { this.feed[index].reblogs_count = count; this.feed[index].reblogged = false; } }) }, unshareStatus(index) { let status = this.feed[index]; if(status.reblog) { status = status.reblog; let state = status.reblogged; let count = status.reblogs_count; this.feed[index].reblog.reblogs_count = count - 1; this.feed[index].reblog.reblogged = !status.reblogged; } else { let state = status.reblogged; let count = status.reblogs_count; this.feed[index].reblogs_count = count - 1; this.feed[index].reblogged = !status.reblogged; } axios.post('/api/v1/statuses/' + status.id + '/unreblog') .then(res => { // }).catch(err => { if(status.reblog) { this.feed[index].reblog.reblogs_count = count; this.feed[index].reblog.reblogged = false; } else { this.feed[index].reblogs_count = count; this.feed[index].reblogged = false; } }) }, handleReport(post) { this.reportedStatusId = post.id; this.$nextTick(() => { this.reportedStatus = post; this.$refs.reportModal.open(); }); }, handleBookmark(index) { let p = this.feed[index]; if(p.reblog) { p = p.reblog; } axios.post('/i/bookmark', { item: p.id }) .then(res => { if(this.feed[index].reblog) { this.feed[index].reblog.bookmarked = !p.bookmarked; } else { this.feed[index].bookmarked = !p.bookmarked; } }) .catch(err => { // this.feed[index].bookmarked = false; this.$bvToast.toast('Cannot bookmark post at this time.', { title: 'Bookmark Error', variant: 'danger', autoHideDelay: 5000 }); }); }, follow(index) { if(this.feed[index].reblog) { axios.post('/api/v1/accounts/' + this.feed[index].reblog.account.id + '/follow') .then(res => { this.$store.commit('updateRelationship', [res.data]); this.updateProfile({ following_count: this.profile.following_count + 1 }); this.feed[index].reblog.account.followers_count = this.feed[index].reblog.account.followers_count + 1; }).catch(err => { swal('Oops!', 'An error occured when attempting to follow this account.', 'error'); this.feed[index].reblog.relationship.following = false; }); } else { axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/follow') .then(res => { this.$store.commit('updateRelationship', [res.data]); this.updateProfile({ following_count: this.profile.following_count + 1 }); this.feed[index].account.followers_count = this.feed[index].account.followers_count + 1; }).catch(err => { swal('Oops!', 'An error occured when attempting to follow this account.', 'error'); this.feed[index].relationship.following = false; }); } }, unfollow(index) { if(this.feed[index].reblog) { axios.post('/api/v1/accounts/' + this.feed[index].reblog.account.id + '/unfollow') .then(res => { this.$store.commit('updateRelationship', [res.data]); this.updateProfile({ following_count: this.profile.following_count - 1 }); this.feed[index].reblog.account.followers_count = this.feed[index].reblog.account.followers_count - 1; }).catch(err => { swal('Oops!', 'An error occured when attempting to unfollow this account.', 'error'); this.feed[index].reblog.relationship.following = true; }); } else { axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/unfollow') .then(res => { this.$store.commit('updateRelationship', [res.data]); this.updateProfile({ following_count: this.profile.following_count - 1 }); this.feed[index].account.followers_count = this.feed[index].account.followers_count - 1; }).catch(err => { swal('Oops!', 'An error occured when attempting to unfollow this account.', 'error'); this.feed[index].relationship.following = true; }); } }, updateProfile(delta) { this.$emit('update-profile', delta); }, handleRefresh() { this.isLoaded = false; this.feed = []; this.ids = []; this.max_id = 0; this.canLoadMore = true; this.showLoadMore = false; this.loadMoreTimeout = undefined; this.loadMoreAttempts = 0; this.isFetchingMore = false; this.endFeedReached = false; this.postIndex = 0; this.showMenu = false; this.showLikesModal = false; this.likesModalPost = {}; this.showReportModal = false; this.reportedStatus = {}; this.reportedStatusId = 0; this.showSharesModal = false; this.sharesModalPost = {}; this.$nextTick(() => { this.fetchTimeline(true); }); }, handleEdit(status) { this.$refs.editModal.show(status); }, mergeUpdatedPost(post) { this.feed = this.feed.map(p => { if(p.id == post.id) { p = post; } return p; }); this.$nextTick(() => { this.forceUpdateIdx++; }); }, enableReblogs() { this.enablingReblogs = true; axios.post('/api/pixelfed/v1/web/settings', { field: 'enable_reblogs', value: true }) .then(res => { setTimeout(() => { window.location.reload(); }, 1000); }) }, hideReblogs() { this.showReblogBanner = false; axios.post('/api/pixelfed/v1/web/settings', { field: 'hide_reblog_banner', value: true }) .then(res => { }) }, handleMuted(post) { this.feed = this.feed.filter(p => { return p.account.id !== post.account.id; }); }, handleUnfollow(post) { if(this.scope === 'home') { this.feed = this.feed.filter(p => { return p.account.id !== post.account.id; }); } this.updateProfile({ following_count: this.profile.following_count - 1 }); }, }, watch: { 'refresh': 'handleRefresh' }, beforeDestroy() { clearTimeout(this.loadMoreTimeout); } } </script>