123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- <template>
- <div class="web-wrapper notification-metro-component">
- <div v-if="isLoaded" class="container-fluid mt-3">
- <div class="row">
- <div class="col-md-3 d-md-block">
- <sidebar :user="profile" />
- </div>
- <div class="col-md-9 col-lg-9 col-xl-5 offset-xl-1">
- <template v-if="tabIndex === 0">
- <h1 class="font-weight-bold">
- Notifications
- </h1>
- <p class="small mt-n2"> </p>
- </template>
- <template v-else-if="tabIndex === 10">
- <div class="d-flex align-items-center mb-3">
- <a class="text-muted" href="#" @click.prevent="tabIndex = 0" style="opacity:0.3">
- <i class="far fa-chevron-circle-left fa-2x mr-3" title="Go back to notifications"></i>
- </a>
- <h1 class="font-weight-bold">
- Follow Requests
- </h1>
- </div>
- </template>
- <template v-else>
- <h1 class="font-weight-bold">
- {{ tabs[tabIndex].name }}
- </h1>
- <p class="small text-lighter mt-n2">{{ tabs[tabIndex].description }}</p>
- </template>
- <div v-if="!notificationsLoaded">
- <placeholder />
- </div>
- <template v-else>
- <ul v-if="tabIndex != 10 && notificationsLoaded && notifications && notifications.length" class="notification-filters nav nav-tabs nav-fill mb-3">
- <li v-for="(item, idx) in tabs" class="nav-item">
- <a
- class="nav-link"
- :class="{ active: tabIndex === idx }"
- href="#"
- @click.prevent="toggleTab(idx)">
- <i
- class="mr-1 nav-link-icon"
- :class="[ item.icon ]"
- >
- </i>
- <span class="d-none d-xl-inline-block">
- {{ item.name }}
- </span>
- </a>
- </li>
- </ul>
- <div v-if="notificationsEmpty && followRequestsChecked && !followRequests.accounts.length && notificationRetries < 2">
- <div class="row justify-content-center">
- <div class="col-12 col-md-10 text-center">
- <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
- <p class="lead text-muted font-weight-bold">{{ $t('notifications.noneFound') }}</p>
- </div>
- </div>
- </div>
- <div v-else-if="!notificationsLoaded || tabSwitching || ((notificationsEmpty && notificationRetries < 2 ) || !notifications && !followRequests && !followRequests.accounts && !followRequests.accounts.length)">
- <placeholder />
- </div>
- <div v-else>
- <div v-if="tabIndex === 0">
- <div
- v-if="followRequests && followRequests.hasOwnProperty('accounts') && followRequests.accounts.length"
- class="card card-body shadow-none border border-warning rounded-pill mb-3 py-2">
- <div class="media align-items-center">
- <i class="far fa-exclamation-circle mr-3 text-warning"></i>
- <div class="media-body">
- <p class="mb-0">
- <strong>{{ followRequests.count }} follow {{ followRequests.count > 1 ? 'requests' : 'request' }}</strong>
- </p>
- </div>
- <a
- class="ml-2 small d-flex font-weight-bold primary text-uppercase mb-0"
- href="#"
- @click.prevent="showFollowRequests()">
- View<span class="d-none d-md-block"> Follow Requests</span>
- </a>
- </div>
- </div>
- <div v-if="notificationsLoaded">
- <notification
- v-for="(n, index) in notifications"
- :key="`notification:${index}:${n.id}`"
- :n="n" />
- <div v-if="notifications && notificationsLoaded && !notifications.length && notificationRetries <= 2">
- <div class="row justify-content-center">
- <div class="col-12 col-md-10 text-center">
- <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
- <p class="lead text-muted font-weight-bold">{{ $t('notifications.noneFound') }}</p>
- </div>
- </div>
- </div>
- <div v-if="canLoadMore">
- <intersect @enter="enterIntersect">
- <placeholder />
- </intersect>
- </div>
- </div>
- </div>
- <div v-else-if="tabIndex === 10">
- <div v-if="followRequests && followRequests.accounts && followRequests.accounts.length" class="list-group">
- <div v-for="(acct, index) in followRequests.accounts" class="list-group-item">
- <div class="media align-items-center">
- <router-link :to="`/i/web/profile/${acct.account.id}`" class="primary">
- <img :src="acct.avatar" width="80" height="80" class="rounded-lg shadow mr-3" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
- </router-link>
- <div class="media-body mr-3">
- <p class="font-weight-bold mb-0 text-break" style="font-size:17px">
- <router-link :to="`/i/web/profile/${acct.account.id}`" class="primary">
- {{ acct.username }}
- </router-link>
- </p>
- <p class="mb-1 text-muted text-break" style="font-size:11px">{{ truncate(acct.account.note_text, 100) }}</p>
- <div class="d-flex text-lighter" style="font-size:11px">
- <span class="mr-3">
- <span class="font-weight-bold">{{ acct.account.statuses_count }}</span>
- <span>Posts</span>
- </span>
- <span>
- <span class="font-weight-bold">{{ acct.account.followers_count }}</span>
- <span>Followers</span>
- </span>
- </div>
- </div>
- <div class="d-flex flex-column d-md-block">
- <button
- class="btn btn-outline-success py-1 btn-sm font-weight-bold rounded-pill mr-2 mb-1"
- @click.prevent="handleFollowRequest('accept', index)"
- >
- Accept
- </button>
- <button class="btn btn-outline-lighter py-1 btn-sm font-weight-bold rounded-pill mb-1"
- @click.prevent="handleFollowRequest('reject', index)"
- >
- Reject
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-else>
- <div v-if="filteredLoaded">
- <div class="card card-body bg-transparent shadow-none border p-2 mb-3 rounded-pill text-lighter">
- <div class="media align-items-center small">
- <i class="far fa-exclamation-triangle mx-2"></i>
- <div class="media-body">
- <p class="mb-0 font-weight-bold">Filtering results may not include older notifications</p>
- </div>
- </div>
- </div>
- <div v-if="filteredFeed.length">
- <notification
- v-for="(n, index) in filteredFeed"
- :key="`notification:filtered:${index}:${n.id}`"
- :n="n" />
- </div>
- <div v-else>
- <div v-if="filteredEmpty && notificationRetries <= 2">
- <div class="card card-body shadow-sm border-0 d-flex flex-row align-items-center" style="border-radius: 20px;gap:1rem;">
- <i class="far fa-inbox fa-2x text-muted"></i>
- <div class="font-weight-bold">No recent {{ tabs[tabIndex].name }}!</div>
- </div>
- </div>
- <placeholder v-else />
- </div>
- <div v-if="canLoadMoreFiltered">
- <intersect @enter="enterFilteredIntersect">
- <placeholder />
- </intersect>
- </div>
- </div>
- <div v-else>
- <placeholder />
- </div>
- </div>
- </div>
- </template>
- </div>
- </div>
- <drawer />
- </div>
- </div>
- </template>
- <script type="text/javascript">
- import Drawer from './partials/drawer.vue';
- import Sidebar from './partials/sidebar.vue';
- import Notification from './partials/timeline/Notification.vue';
- import Placeholder from './partials/placeholders/NotificationPlaceholder.vue';
- import Intersect from 'vue-intersect';
- export default {
- components: {
- "drawer": Drawer,
- "sidebar": Sidebar,
- "intersect": Intersect,
- "notification": Notification,
- "placeholder": Placeholder,
- },
- data() {
- return {
- isLoaded: false,
- profile: undefined,
- ids: [],
- notifications: undefined,
- notificationsLoaded: false,
- notificationRetries: 0,
- notificationsEmpty: true,
- notificationRetryTimeout: undefined,
- max_id: undefined,
- canLoadMore: false,
- isIntersecting: false,
- tabIndex: 0,
- tabs: [
- {
- id: 'all',
- name: 'All',
- icon: 'far fa-bell',
- types: []
- },
- {
- id: 'mentions',
- name: 'Mentions',
- description: 'Replies to your posts and posts you were mentioned in',
- icon: 'far fa-at',
- types: ['comment', 'mention']
- },
- {
- id: 'likes',
- name: 'Likes',
- description: 'Accounts that liked your posts',
- icon: 'far fa-heart',
- types: ['favourite']
- },
- {
- id: 'followers',
- name: 'Followers',
- description: 'Accounts that followed you',
- icon: 'far fa-user-plus',
- types: ['follow']
- },
- {
- id: 'reblogs',
- name: 'Reblogs',
- description: 'Accounts that shared or reblogged your posts',
- icon: 'far fa-retweet',
- types: ['share']
- },
- {
- id: 'direct',
- name: 'DMs',
- description: 'Direct messages you have with other accounts',
- icon: 'far fa-envelope',
- types: ['direct']
- },
- ],
- tabSwitching: false,
- filteredFeed: [],
- filteredLoaded: false,
- filteredIsIntersecting: false,
- filteredMaxId: undefined,
- canLoadMoreFiltered: true,
- filterPaginationTimeout: undefined,
- filteredIterations: 0,
- filteredEmpty: false,
- followRequests: [],
- followRequestsChecked: false,
- followRequestsPage: 1
- }
- },
- updated() {
- },
- mounted() {
- this.profile = window._sharedData.user;
- this.isLoaded = true;
- if(this.profile.locked) {
- this.fetchFollowRequests();
- }
- this.fetchNotifications();
- },
- beforeDestroy() {
- clearTimeout(this.notificationRetryTimeout);
- },
- methods: {
- fetchNotifications() {
- this.notificationRetries++;
- axios.get('/api/pixelfed/v1/notifications?pg=true')
- .then(res => {
- if(!res || !res.data || !res.data.length) {
- if(this.notificationRetries == 2) {
- clearTimeout(this.notificationRetryTimeout);
- this.canLoadMore = false;
- this.notificationsLoaded = true;
- this.notificationsEmpty = true;
- return;
- }
- this.notificationRetryTimeout = setTimeout(() => {
- this.fetchNotifications();
- }, 1000);
- return;
- }
- let data = res.data.filter(n => {
- if(n.type == 'share' && !n.status) {
- return false;
- }
- if(n.type == 'comment' && !n.status) {
- return false;
- }
- if(n.type == 'mention' && !n.status) {
- return false;
- }
- if(n.type == 'favourite' && !n.status) {
- return false;
- }
- if(n.type == 'follow' && !n.account) {
- return false;
- }
- return true;
- });
- let ids = res.data.map(n => n.id);
- this.max_id = Math.min(...ids);
- this.ids.push(...ids);
- this.notifications = data;
- this.notificationsLoaded = true;
- this.notificationsEmpty = false;
- this.canLoadMore = true;
- });
- },
- enterIntersect() {
- if(this.isIntersecting) {
- return;
- }
- if(!isFinite(this.max_id)) {
- return;
- }
- this.isIntersecting = true;
- axios.get('/api/pixelfed/v1/notifications', {
- params: {
- max_id: this.max_id
- }
- }).then(res => {
- if(!res.data.length) {
- this.canLoadMore = false;
- }
- let ids = res.data.map(n => n.id);
- this.max_id = Math.min(...ids);
- this.notifications.push(...res.data);
- this.isIntersecting = false;
- })
- },
- toggleTab(idx) {
- this.tabSwitching = true;
- this.canLoadMoreFiltered = true;
- this.filteredEmpty = false;
- this.filteredIterations = 0;
- this.filterFeed(this.tabs[idx].id);
- },
- filterFeed(type) {
- switch(type) {
- case 'all':
- this.tabIndex = 0;
- this.filteredFeed = [];
- this.filteredLoaded = false;
- this.filteredIsIntersecting = false;
- this.filteredMaxId = undefined;
- this.canLoadMoreFiltered = false;
- this.tabSwitching = false;
- break;
- case 'mentions':
- this.tabIndex = 1;
- this.filteredMaxId = this.max_id;
- this.filteredFeed = this.notifications.filter(n => this.tabs[this.tabIndex].types.includes(n.type));
- this.filteredIsIntersecting = false;
- this.tabSwitching = false;
- this.filteredLoaded = true;
- break;
- case 'likes':
- this.tabIndex = 2;
- this.filteredMaxId = this.max_id;
- this.filteredFeed = this.notifications.filter(n => n.type === 'favourite');
- this.filteredIsIntersecting = false;
- this.tabSwitching = false;
- this.filteredLoaded = true;
- break;
- case 'followers':
- this.tabIndex = 3;
- this.filteredMaxId = this.max_id;
- this.filteredFeed = this.notifications.filter(n => n.type === 'follow');
- this.filteredIsIntersecting = false;
- this.tabSwitching = false;
- this.filteredLoaded = true;
- break;
- case 'reblogs':
- this.tabIndex = 4;
- this.filteredMaxId = this.max_id;
- this.filteredFeed = this.notifications.filter(n => n.type === 'share');
- this.filteredIsIntersecting = false;
- this.tabSwitching = false;
- this.filteredLoaded = true;
- break;
- case 'direct':
- this.tabIndex = 5;
- this.filteredMaxId = this.max_id;
- this.filteredFeed = this.notifications.filter(n => n.type === 'direct');
- this.filteredIsIntersecting = false;
- this.tabSwitching = false;
- this.filteredLoaded = true;
- break;
- }
- },
- enterFilteredIntersect() {
- if( !this.canLoadMoreFiltered ||
- this.filteredIsIntersecting ||
- this.filteredIterations > 10
- ) {
- if(this.filteredFeed.length == 0) {
- this.filteredEmpty = true;
- this.canLoadMoreFiltered = false;
- }
- return;
- }
- if(!isFinite(this.max_id) || !isFinite(this.filteredMaxId)) {
- this.canLoadMoreFiltered = false;
- return;
- }
- this.filteredIsIntersecting = true;
- axios.get('/api/pixelfed/v1/notifications', {
- params: {
- max_id: this.filteredMaxId,
- limit: 40
- }
- })
- .then(res => {
- let mids = res.data.map(n => n.id);
- let max_id = Math.min(...mids);
- if(max_id < this.max_id) {
- this.max_id = max_id;
- res.data.forEach(n => {
- if(this.ids.indexOf(n.id) == -1) {
- this.ids.push(n.id);
- this.notifications.push(n);
- } else {
- }
- });
- }
- this.filteredIterations++;
- if(this.filterPaginationTimeout && this.filterPaginationTimeout < 500) {
- clearTimeout(this.filterPaginationTimeout);
- }
- if(!res.data || !res.data.length) {
- this.canLoadMoreFiltered = false;
- }
- if(!res.data.length) {
- this.canLoadMoreFiltered = false;
- }
- let ids = res.data.map(n => n.id);
- this.filteredMaxId = Math.min(...ids);
- let types = this.tabs[this.tabIndex].types;
- let data = res.data.filter(n => types.includes(n.type));
- this.filteredFeed.push(...data);
- this.filteredIsIntersecting = false;
- if(this.filteredFeed.length < 10) {
- setTimeout(() => this.enterFilteredIntersect(), 500);
- }
- this.filterPaginationTimeout = setTimeout(() => {
- this.canLoadMoreFiltered = false;
- }, 2000);
- })
- .catch(err => {
- this.canLoadMoreFiltered = false;
- })
- },
- fetchFollowRequests() {
- axios.get('/account/follow-requests.json')
- .then(res => {
- if(this.followRequestsPage == 1) {
- this.followRequests = res.data;
- this.followRequestsChecked = true;
- } else {
- this.followRequests.accounts.push(...res.data.accounts);
- }
- this.followRequestsPage++;
- });
- },
- showFollowRequests() {
- this.tabSwitching = false;
- this.filteredEmpty = false;
- this.filteredIterations = 0;
- this.tabIndex = 10;
- },
- handleFollowRequest(action, index) {
- if(!window.confirm('Are you sure you want to ' + action + ' this follow request?')) {
- return;
- }
- axios.post('/account/follow-requests', {
- action: action,
- id: this.followRequests.accounts[index].rid
- })
- .then(res => {
- this.followRequests.count--;
- this.followRequests.accounts.splice(index, 1);
- this.toggleTab(0);
- })
- },
- truncate(str, len = 40) {
- return _.truncate(str, { length: len });
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .notification-metro-component {
- .notification-filters {
- .nav-link {
- font-size: 12px;
- &.active {
- font-weight: bold;
- }
- &-icon:not(.active) {
- opacity: 0.5;
- }
- &:not(.active) {
- color: #9ca3af;
- }
- }
- }
- }
- </style>
|