Selaa lähdekoodia

Add section components

Daniel Supernault 2 vuotta sitten
vanhempi
commit
bffd8f0771

+ 206 - 0
resources/assets/components/sections/DiscoverFeed.vue

@@ -0,0 +1,206 @@
+<template>
+    <div class="discover-feed-component">
+        <section class="mt-3 mb-5 section-explore">
+            <b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
+
+            <div class="profile-timeline">
+                <div class="row p-0 mt-5">
+                    <div class="col-12 mb-4 d-flex justify-content-between align-items-center">
+                        <p class="d-block d-md-none h1 font-weight-bold mb-0 font-default">Trending</p>
+                        <p class="d-none d-md-block display-4 font-weight-bold mb-0 font-default">Trending</p>
+
+                        <div>
+                            <div class="btn-group trending-range">
+                                <button @click="rangeToggle('daily')" :class="range == 'daily' ? 'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-danger':'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-outline-danger'">Today</button>
+                                <button @click="rangeToggle('monthly')" :class="range == 'monthly' ? 'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-danger':'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-outline-danger'">This month</button>
+                                <button @click="rangeToggle('yearly')" :class="range == 'yearly' ? 'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-danger':'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-outline-danger'">This year</button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div v-if="!loading" class="row p-0 px-lg-3">
+                    <div v-if="trending.length" v-for="(s, index) in trending" class="col-6 col-lg-4 col-xl-3 p-1">
+                        <a class="card info-overlay card-md-border-0" :href="s.url" @click.prevent="goToPost(s)">
+                            <div class="square square-next">
+                                <div v-if="s.sensitive" class="square-content">
+                                    <div class="info-overlay-text-label">
+                                        <h5 class="text-white m-auto font-weight-bold">
+                                            <span>
+                                                <span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
+                                            </span>
+                                        </h5>
+                                    </div>
+                                    <blur-hash-canvas
+                                        width="32"
+                                        height="32"
+                                        :hash="s.media_attachments[0].blurhash"
+                                        />
+                                </div>
+                                <div v-else class="square-content">
+                                    <blur-hash-image
+                                        width="32"
+                                        height="32"
+                                        :hash="s.media_attachments[0].blurhash"
+                                        :src="s.media_attachments[0].preview_url"
+                                        />
+                                </div>
+
+                                <div class="info-overlay-text">
+                                    <div class="text-white m-auto">
+                                        <p class="info-overlay-text-field font-weight-bold">
+                                            <span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
+                                            <span class="d-flex-inline">{{formatCount(s.favourites_count)}}</span>
+                                        </p>
+
+                                        <p class="info-overlay-text-field font-weight-bold">
+                                            <span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
+                                            <span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
+                                        </p>
+
+                                        <p class="mb-0 info-overlay-text-field font-weight-bold">
+                                            <span class="far fa-sync fa-lg p-2 d-flex-inline"></span>
+                                            <span class="d-flex-inline">{{formatCount(s.reblogs_count)}}</span>
+                                        </p>
+                                    </div>
+                                </div>
+                            </div>
+                        </a>
+                    </div>
+                    <div v-else class="col-12 d-flex align-items-center justify-content-center bg-light border" style="min-height: 40vh;">
+                        <div class="h2">No posts found :(</div>
+                    </div>
+                </div>
+
+                <div v-else class="row p-0 px-lg-3">
+                    <div class="col-12 d-flex align-items-center justify-content-center" style="min-height: 40vh;">
+                        <b-spinner size="lg" />
+                    </div>
+                </div>
+            </div>
+        </section>
+    </div>
+</template>
+
+<script type="text/javascript">
+    export default {
+        props: {
+            profile: {
+                type: Object
+            }
+        },
+
+        data() {
+            return {
+                loading: true,
+                trending: [],
+                range: 'daily',
+                breadcrumbItems: [
+                    {
+                        text: 'Discover',
+                        href: '/i/web/discover'
+                    },
+                    {
+                        text: 'Trending',
+                        active: true
+                    }
+                ]
+            }
+        },
+
+        mounted() {
+            this.loadTrending();
+        },
+
+        methods: {
+            fetchData() {
+                axios.get('/api/pixelfed/v2/discover/posts')
+                .then((res) => {
+                    this.posts = res.data.posts.filter(r => r != null);
+                    this.recommendedLoading = false;
+                });
+            },
+
+            loadTrending() {
+                this.loading = true;
+
+                axios.get('/api/pixelfed/v2/discover/posts/trending', {
+                    params: {
+                        range: this.range
+                    }
+                })
+                .then(res => {
+                    let data = res.data.filter(r => {
+                        return r !== null;
+                    });
+                    this.trending = data.filter(t => t.sensitive == false);
+
+                    if(this.range == 'daily' && data.length == 0) {
+                        this.range = 'yearly';
+                        this.loadTrending();
+                    }
+
+                    this.loading = false;
+                });
+            },
+
+            formatCount(s) {
+                return App.util.format.count(s);
+            },
+
+            goToPost(status) {
+                this.$router.push({
+                    name: 'post',
+                    params: {
+                        id: status.id,
+                        cachedStatus: status,
+                        cachedProfile: this.profile
+                    }
+                })
+            },
+
+            rangeToggle(range) {
+                event.currentTarget.blur();
+                this.range = range;
+                this.loadTrending();
+            }
+        }
+    }
+</script>
+
+<style lang="scss">
+    .discover-feed-component {
+        .font-default {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+            letter-spacing: -0.7px;
+        }
+
+        .info-overlay {
+            border-radius: 15px !important;
+        }
+
+        .square-next {
+            img,
+            .info-overlay-text {
+                border-radius: 15px !important;
+            }
+        }
+
+        .trending-range {
+            .btn {
+                &:hover:not(.btn-danger) {
+                    background-color: #fca5a5
+                }
+            }
+        }
+
+        .info-overlay-text-field {
+            font-size: 13.5px;
+            margin-bottom: 2px;
+
+            @media (min-width: 768px) {
+                font-size: 20px;
+                margin-bottom: 15px;
+            }
+        }
+    }
+</style>

+ 582 - 0
resources/assets/components/sections/Timeline.vue

@@ -0,0 +1,582 @@
+<template>
+    <div class="timeline-section-component">
+        <div v-if="!isLoaded">
+            <status-placeholder />
+            <status-placeholder />
+            <status-placeholder />
+            <status-placeholder />
+        </div>
+
+        <div v-else>
+            <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"
+        />
+
+        <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 {
+                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
+            }
+        },
+
+        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;
+                };
+            }
+            this.fetchTimeline();
+        },
+
+        methods: {
+            getScope() {
+                switch(this.scope) {
+                    case 'local':
+                        return 'public'
+                    break;
+
+                    case 'global':
+                        return 'network'
+                    break;
+
+                    default:
+                        return 'home';
+                    break;
+                }
+            },
+
+            fetchTimeline(scrollToTop = false) {
+                let url = `/api/pixelfed/v1/timelines/${this.getScope()}`;
+                axios.get(url, {
+                    params: {
+                        max_id: this.max_id,
+                        limit: 6
+                    }
+                }).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 !== 6) {
+                        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 = `/api/pixelfed/v1/timelines/${this.getScope()}`;
+                axios.get(url, {
+                    params: {
+                        max_id: this.max_id,
+                        limit: 6
+                    }
+                }).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];
+                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 => {
+                    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];
+                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 => {
+                    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;
+                this.likesModalPost = this.feed[this.postIndex];
+                this.showLikesModal = true;
+                this.$nextTick(() => {
+                    this.$refs.likesModal.open();
+                });
+            },
+
+            openSharesModal(idx) {
+                this.postIndex = idx;
+                this.sharesModalPost = this.feed[this.postIndex];
+                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);
+            },
+
+            counterChange(index, type) {
+                switch(type) {
+                    case 'comment-increment':
+                        this.feed[index].reply_count = this.feed[index].reply_count + 1;
+                    break;
+
+                    case 'comment-decrement':
+                        this.feed[index].reply_count = this.feed[index].reply_count - 1;
+                    break;
+                }
+            },
+
+            openCommentLikesModal(post) {
+                this.likesModalPost = post;
+                this.showLikesModal = true;
+                this.$nextTick(() => {
+                    this.$refs.likesModal.open();
+                });
+            },
+
+            shareStatus(index) {
+                let status = this.feed[index];
+                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 => {
+                    this.feed[index].reblogs_count = count;
+                    this.feed[index].reblogged = false;
+                })
+            },
+
+            unshareStatus(index) {
+                let status = this.feed[index];
+                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 => {
+                    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];
+
+                axios.post('/i/bookmark', {
+                    item: p.id
+                })
+                .then(res => {
+                    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) {
+                // this.feed[index].relationship.following = true;
+
+                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) {
+                // this.feed[index].relationship.following = false;
+
+                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++;
+                });
+            }
+        },
+
+        watch: {
+            'refresh': 'handleRefresh'
+        },
+
+        beforeDestroy() {
+            clearTimeout(this.loadMoreTimeout);
+        }
+    }
+</script>