123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- <template>
- <div class="notifications-component">
- <div class="card shadow-sm mb-3" style="overflow: hidden;border-radius: 15px !important;">
- <div class="card-body pb-0">
- <div class="d-flex justify-content-between align-items-center mb-3">
- <span class="text-muted font-weight-bold">{{ $t("notifications.title")}}</span>
- <div v-if="feed && feed.length">
- <router-link to="/i/web/notifications" class="btn btn-outline-light btn-sm mr-2" style="color: #B8C2CC !important">
- <i class="far fa-filter"></i>
- </router-link>
- <button
- v-if="hasLoaded && feed.length"
- class="btn btn-light btn-sm"
- :class="{ 'text-lighter': isRefreshing }"
- :disabled="isRefreshing"
- @click="refreshNotifications">
- <i class="fal fa-redo"></i>
- </button>
- </div>
- </div>
- <div v-if="!hasLoaded" class="notifications-component-feed">
- <div class="d-flex align-items-center justify-content-center flex-column bg-light rounded-lg p-3 mb-3" style="min-height: 100px;">
- <b-spinner variant="grow" />
- </div>
- </div>
- <div v-else class="notifications-component-feed">
- <template v-if="isEmpty">
- <div class="d-flex align-items-center justify-content-center flex-column bg-light rounded-lg p-3 mb-3" style="min-height: 100px;">
- <i class="fal fa-bell fa-2x text-lighter"></i>
- <p class="mt-2 small font-weight-bold text-center mb-0">{{ $t('notifications.noneFound') }}</p>
- </div>
- </template>
- <template v-else>
- <div v-for="(n, index) in feed" class="mb-2">
- <div class="media align-items-center">
- <img
- v-if="n.type === 'autospam.warning'"
- class="mr-2 rounded-circle shadow-sm p-1"
- style="border: 2px solid var(--danger)"
- src="/img/pixelfed-icon-color.svg"
- width="32"
- height="32"
- />
- <img
- v-else
- class="mr-2 rounded-circle shadow-sm"
- :src="n.account.avatar"
- width="32"
- height="32"
- onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
- <div class="media-body font-weight-light small">
- <div v-if="n.type == 'favourite'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.liked")}}
- <span v-if="n.status && n.status.hasOwnProperty('media_attachments')">
- <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)" :id="'fvn-' + n.id" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
- <b-popover :target="'fvn-' + n.id" title="" triggers="hover" placement="top" boundary="window">
- <img :src="notificationPreview(n)" width="100px" height="100px" style="object-fit: cover;">
- </b-popover>
- </span>
- <span v-else>
- <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
- </span>
- </p>
- </div>
- <div v-else-if="n.type == 'autospam.warning'">
- <p class="my-0">
- {{ $t("notifications.youRecent")}} <a :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)" class="font-weight-bold">{{ $t("notifications.post")}}</a> {{ $t("notifications.hasUnlisted")}}.
- </p>
- <p class="mt-n1 mb-0">
- <span class="small text-muted"><a href="#" class="font-weight-bold" @click.prevent="showAutospamInfo(n.status)">Click here</a> for more info.</span>
- </p>
- </div>
- <div v-else-if="n.type == 'comment'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.commented")}} <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
- </p>
- </div>
- <div v-else-if="n.type == 'group:comment'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.commented")}} <a class="font-weight-bold" :href="n.group_post_url">{{ $t("notifications.groupPost") }}</a>.
- </p>
- </div>
- <div v-else-if="n.type == 'story:react'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.reacted")}} <a class="font-weight-bold" v-bind:href="'/i/web/direct/thread/'+n.account.id">{{ $t("notifications.story")}}</a>.
- </p>
- </div>
- <div v-else-if="n.type == 'story:comment'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.commented")}} <a class="font-weight-bold" v-bind:href="'/i/web/direct/thread/'+n.account.id">{{ $t("notifications.story")}}</a>.
- </p>
- </div>
- <div v-else-if="n.type == 'mention'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)" @click.prevent="goToPost(n.status)">{{ $t("notifications.mentioned")}}</a> {{ $t("notifications.you")}}.
- </p>
- </div>
- <div v-else-if="n.type == 'follow'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.followed")}} {{ $t("notifications.you")}}.
- </p>
- </div>
- <div v-else-if="n.type == 'share'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.shared")}}
- <span v-if="n.status && n.status.hasOwnProperty('media_attachments')">
- <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)" :id="'fvn-' + n.id" @click.prevent="goToPost(n.status)">{{ $t("notifications.post")}}</a>.
- <b-popover :target="'fvn-' + n.id" title="" triggers="hover" placement="top" boundary="window">
- <img :src="notificationPreview(n)" width="100px" height="100px" style="object-fit: cover;">
- </b-popover>
- </span>
- </p>
- </div>
- <div v-else-if="n.type == 'modlog'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{truncate(n.account.username)}}</a> {{ $t("notifications.updatedA")}} <a class="font-weight-bold" v-bind:href="n.modlog.url">modlog</a>.
- </p>
- </div>
- <div v-else-if="n.type == 'tagged'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.tagged")}} <a class="font-weight-bold" v-bind:href="n.tagged.post_url">{{ $t("notifications.post")}}</a>.
- </p>
- </div>
- <div v-else-if="n.type == 'direct'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> {{ $t("notifications.sentA")}} <router-link class="font-weight-bold" :to="'/i/web/direct/thread/'+n.account.id">dm</router-link>.
- </p>
- </div>
- <div v-else-if="n.type == 'group.join.approved'">
- <p class="my-0">
- {{ $t("notifications.yourApplication")}} <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t("notifications.wasApproved")}}
- </p>
- </div>
- <div v-else-if="n.type == 'group.join.rejected'">
- <p class="my-0">
- {{ $t("notifications.yourApplication")}} <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t("notifications.wasRejected")}}
- </p>
- </div>
- <div v-else-if="n.type == 'group:invite'">
- <p class="my-0">
- <a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> invited you to join <a :href="n.group.url + '/invite/claim'" class="font-weight-bold text-dark word-break" :title="n.group.name">{{n.group.name}}</a>.
- </p>
- </div>
- <div v-else>
- <p class="my-0">
- {{ $t("notifications.cannotDisplay")}}
- </p>
- </div>
- </div>
- <div class="small text-muted font-weight-bold" style="font-size: 12px;" :title="n.created_at">{{timeAgo(n.created_at)}}</div>
- </div>
- </div>
- <div v-if="hasLoaded && feed.length == 0">
- <p class="small font-weight-bold text-center mb-0">{{ $t('notifications.noneFound') }}</p>
- </div>
- <div v-else>
- <intersect v-if="hasLoaded && canLoadMore" @enter="enterIntersect">
- <placeholder small style="margin-top: -6px" />
- <placeholder small/>
- <placeholder small/>
- <placeholder small/>
- </intersect>
- <div v-else class="d-block" style="height: 10px;">
- </div>
- </div>
- </template>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script type="text/javascript">
- import Placeholder from './../partials/placeholders/NotificationPlaceholder.vue';
- import Intersect from 'vue-intersect';
- export default {
- props: {
- profile: {
- type: Object
- }
- },
- components: {
- "intersect": Intersect,
- "placeholder": Placeholder
- },
- data() {
- return {
- feed: {},
- maxId: undefined,
- isIntersecting: false,
- canLoadMore: false,
- isRefreshing: false,
- hasLoaded: false,
- isEmpty: false,
- retryTimeout: undefined,
- retryAttempts: 0
- }
- },
- mounted() {
- this.init();
- },
- destroyed() {
- clearTimeout(this.retryTimeout);
- },
- methods: {
- init() {
- if(this.retryAttempts == 1) {
- this.hasLoaded = true;
- this.isEmpty = true;
- clearTimeout(this.retryTimeout);
- return;
- }
- axios.get('/api/pixelfed/v1/notifications', {
- params: {
- limit: 9,
- }
- })
- .then(res => {
- if(!res || !res.data || !res.data.length) {
- this.retryAttempts = this.retryAttempts + 1;
- this.retryTimeout = setTimeout(() => this.init(), this.retryAttempts * 1500);
- return;
- }
- let data = res.data.filter(n => {
- if(n.type == 'share' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'comment' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'mention' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'favourite' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'follow' && !n.account) {
- return false;
- }
- if(n.type == 'modlog' && !n.modlog) {
- return false;
- }
- return true;
- });
- if(!res.data.length) {
- this.canLoadMore = false;
- } else {
- this.canLoadMore = true;
- }
- if(this.retryTimeout || this.retryAttempts) {
- this.retryAttempts = 0;
- clearTimeout(this.retryTimeout);
- }
- this.maxId = res.data[res.data.length - 1].id;
- this.feed = data;
- this.hasLoaded = true;
- setTimeout(() => {
- this.isRefreshing = false;
- }, 15000);
- });
- },
- refreshNotifications() {
- event.currentTarget.blur();
- this.isRefreshing = true;
- this.init();
- },
- enterIntersect() {
- if(this.isIntersecting || !this.canLoadMore) {
- return;
- }
- this.isIntersecting = true;
- axios.get('/api/pixelfed/v1/notifications', {
- params: {
- limit: 9,
- max_id: this.maxId
- }
- })
- .then(res => {
- if(!res.data || !res.data.length) {
- this.canLoadMore = false;
- this.isIntersecting = false;
- return;
- }
- let data = res.data.filter(n => {
- if(n.type == 'share' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'comment' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'mention' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'favourite' && (!n.status || !n.account)) {
- return false;
- }
- if(n.type == 'follow' && !n.account) {
- return false;
- }
- if(n.type == 'modlog' && !n.modlog) {
- return false;
- }
- return true;
- });
- if(!res.data.length) {
- this.canLoadMore = false;
- return;
- }
- this.maxId = res.data[res.data.length - 1].id;
- this.feed.push(...data);
- this.$nextTick(() => {
- this.isIntersecting = false;
- })
- });
- },
- truncate(text) {
- if(text.length <= 15) {
- return text;
- }
- return text.slice(0,15) + '...'
- },
- timeAgo(ts) {
- return window.App.util.format.timeAgo(ts);
- },
- mentionUrl(status) {
- let username = status.account.username;
- let id = status.id;
- return '/p/' + username + '/' + id;
- },
- redirect(url) {
- window.location.href = url;
- },
- notificationPreview(n) {
- if(!n.status || !n.status.hasOwnProperty('media_attachments') || !n.status.media_attachments.length) {
- return '/storage/no-preview.png';
- }
- return n.status.media_attachments[0].preview_url;
- },
- getProfileUrl(account) {
- return '/i/web/profile/' + account.id;
- },
- getPostUrl(status) {
- if(!status) {
- return;
- }
- return '/i/web/post/' + status.id;
- },
- goToPost(status) {
- this.$router.push({
- name: 'post',
- path: `/i/web/post/${status.id}`,
- params: {
- id: status.id,
- cachedStatus: status,
- cachedProfile: this.profile
- }
- })
- },
- goToProfile(account) {
- this.$router.push({
- name: 'profile',
- path: `/i/web/profile/${account.id}`,
- params: {
- id: account.id,
- cachedProfile: account,
- cachedUser: this.profile
- }
- })
- },
- showAutospamInfo(status) {
- let el = document.createElement('p');
- el.classList.add('text-left');
- el.classList.add('mb-0');
- el.innerHTML = '<p class="">We use automated systems to help detect potential abuse and spam. Your recent <a href="/i/web/post/' + status.id + '" class="font-weight-bold">post</a> was flagged for review. <br /> <p class=""><span class="font-weight-bold">Don\'t worry! Your post will be reviewed by a human</span>, and they will restore your post if they determine it appropriate.</p><p style="font-size:12px">Once a human approves your post, any posts you create after will not be marked as unlisted. If you delete this post and share more posts before a human can approve any of them, you will need to wait for at least one unlisted post to be reviewed by a human.';
- let wrapper = document.createElement('div');
- wrapper.appendChild(el);
- swal({
- title: 'Why was my post unlisted?',
- content: wrapper,
- icon: 'warning'
- })
- }
- }
- }
- </script>
- <style lang="scss">
- .notifications-component {
- &-feed {
- min-height: 50px;
- max-height: 300px;
- overflow-y: auto;
- -ms-overflow-style: none;
- scrollbar-width: none;
- overflow-y: scroll;
- &::-webkit-scrollbar {
- display: none;
- }
- }
- .card {
- width: 100%;
- position: relative;
- }
- .card-body {
- width: 100%;
- }
- }
- </style>
|