123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819 |
- <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>
|