Add Discover components

This commit is contained in:
Daniel Supernault 2023-06-11 15:01:02 -06:00
parent bb97b55c66
commit b447db082f
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
7 changed files with 1762 additions and 0 deletions

View file

@ -0,0 +1,405 @@
<template>
<div class="web-wrapper">
<div v-if="isLoaded" class="container-fluid mt-3">
<div class="row">
<div class="col-md-4 col-lg-3">
<sidebar :user="profile" />
</div>
<div v-if="tab == 'index'" class="col-md-8 col-lg-9 mt-n4">
<div v-if="profile.is_admin" class="d-md-flex my-md-3">
<grid-card
:dark="true"
:title="'Hello ' + profile.username"
subtitle="Welcome to the new Discover experience! Only admins can see this"
button-text="Manage Discover Settings"
button-link="/i/web/discover/settings"
icon-class="fal fa-cog"
:small="true" />
</div>
<!-- <section class="mb-1 mb-md-3 mb-lg-4">
<news-slider />
</section> -->
<!-- <discover-spotlight /> -->
<!-- <div class="d-md-flex my-md-3">
<grid-card
:dark="true"
title="The Not So Trending"
subtitle="Explore the posts that deserve more attention"
button-text="Explore posts"
icon-class="fal fa-analytics"
button-link="/i/web/discover/future-trending"
:button-event="true"
v-on:btn-click="toggleTab('trending')"
:small="true" />
<grid-card
title="Behind The Posts"
subtitle="Discover the people"
button-text="Discover People"
button-link="/i/web/discover/people"
icon-class="fal fa-user-friends"
:small="true" />
</div> -->
<daily-trending v-on:btn-click="toggleTab('trending')"/>
<!-- <div class="d-md-flex my-md-3">
<grid-card
title="Explore Loops"
subtitle="Loops are short, looping videos"
button-text="Explore Loops"
icon-class="fal fa-camcorder"
button-link="/i/web/discover/loops"
:small="false" />
<grid-card
:dark="true"
title="Popular Places"
subtitle="Explore posts by popular locations"
button-text="Explore Popular Places"
icon-class="fal fa-map"
:button-event="true"
v-on:btn-click="toggleTab('popular-places')"
button-link="/i/web/discover/popular-places"
:small="false" />
</div> -->
<div class="d-md-flex my-md-3">
<grid-card
v-if="config.hashtags.enabled"
:dark="true"
title="My Hashtags"
subtitle="Explore posts tagged with hashtags you follow"
button-text="Explore Posts"
button-link="/i/web/discover/my-hashtags"
icon-class="fal fa-hashtag"
:small="false" />
<grid-card
v-if="config.memories.enabled"
title="My Memories"
subtitle="A distant look back"
button-text="View Memories"
button-link="/i/web/discover/my-memories"
icon-class="fal fa-history"
:small="false" />
</div>
<div class="d-md-flex my-md-3">
<grid-card
v-if="config.insights.enabled"
title="Account Insights"
subtitle="Get a rich overview of your account activity and interactions"
button-text="View Account Insights"
icon-class="fal fa-user-circle"
button-link="/i/web/discover/account-insights"
:small="false" />
<grid-card
v-if="config.friends.enabled"
:dark="true"
title="Find Friends"
subtitle="Find accounts to follow based on common interests"
button-text="Find Friends & Followers"
button-link="/i/web/discover/find-friends"
icon-class="fal fa-user-plus"
:small="false" />
</div>
<div class="d-md-flex my-md-3">
<grid-card
v-if="config.server.enabled && config.server.domains && config.server.domains.length"
:dark="true"
title="Server Timelines"
subtitle="Browse timelines of a specific remote instance"
button-text="Browse Server Feeds"
icon-class="fal fa-list"
button-link="/i/web/discover/server-timelines"
:small="false" />
<!-- <grid-card
title="Curate the Spotlight"
subtitle="Apply to curate the spotlight for one week"
button-text="Apply to Curate Spotlight"
button-link="/i/web/discover/spotlight/curate/apply"
icon-class="fal fa-thumbs-up"
:small="false" /> -->
</div>
</div>
<div v-else-if="tab == 'trending'" class="col-md-8 col-lg-9 mt-n4">
<discover :profile="profile" />
</div>
<div v-else-if="tab == 'popular-places'" class="col-md-8 col-lg-9 mt-n4">
<section class="mt-3 mb-5 section-explore">
<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">Popular Places</p>
<p class="d-none d-md-block display-4 font-weight-bold mb-0 font-default">Popular Places</p>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-12 col-md-12 mb-3">
<div class="card-img big">
<img src="/img/places/nyc.jpg">
<div class="title font-default">New York City</div>
</div>
</div>
<div class="col-12 col-md-6 mb-3">
<div class="card-img">
<img src="/img/places/edmonton.jpg">
<div class="title font-default">Edmonton</div>
</div>
</div>
<div class="col-12 col-md-6 mb-3">
<div class="card-img">
<img src="/img/places/paris.jpg">
<div class="title font-default">Paris</div>
</div>
</div>
<div class="col-12 col-md-4 mb-3">
<div class="card-img">
<img src="/img/places/london.jpg">
<div class="title font-default">London</div>
</div>
</div>
<div class="col-12 col-md-4 mb-3">
<div class="card-img">
<img src="/img/places/vancouver.jpg">
<div class="title font-default">Vancouver</div>
</div>
</div>
<div class="col-12 col-md-4 mb-3">
<div class="card-img">
<img src="/img/places/toronto.jpg">
<div class="title font-default">Toronto</div>
</div>
</div>
</div>
</section>
</div>
</div>
<drawer />
</div>
</div>
</template>
<script type="text/javascript">
import Drawer from './partials/drawer.vue';
import Sidebar from './partials/sidebar.vue';
import Rightbar from './partials/rightbar.vue';
import Discover from './sections/DiscoverFeed.vue';
import DiscoverNewsSlider from './partials/discover/news-slider.vue';
import DiscoverSpotlight from './partials/discover/discover-spotlight.vue';
import DailyTrending from './partials/discover/daily-trending.vue';
import DiscoverGridCard from './partials/discover/grid-card.vue';
export default {
components: {
"drawer": Drawer,
"sidebar": Sidebar,
"rightbar": Rightbar,
"discover": Discover,
"news-slider": DiscoverNewsSlider,
"discover-spotlight": DiscoverSpotlight,
"daily-trending": DailyTrending,
"grid-card": DiscoverGridCard
},
data() {
return {
isLoaded: false,
profile: undefined,
config: {},
tab: 'index',
popularAccounts: [],
followingIndex: undefined
}
},
updated() {
// let u = new URLSearchParams(window.location.search);
// if(u.has('ft') && u.get('ft') == '1') {
// this.tab = 'index';
// }
},
mounted() {
this.profile = window._sharedData.user;
this.fetchConfig();
},
methods: {
fetchConfig() {
axios.get('/api/pixelfed/v2/discover/meta')
.then(res => {
this.config = res.data;
this.isLoaded = true;
window._sharedData.discoverMeta = res.data;
// this.fetchPopularAccounts();
})
},
fetchPopularAccounts() {
// axios.get('/api/pixelfed/discover/accounts/popular')
// .then(res => {
// this.popularAccounts = res.data;
// })
},
followProfile(index) {
event.currentTarget.blur();
this.followingIndex = index;
let id = this.popularAccounts[index].id;
axios.post('/api/v1/accounts/' + id + '/follow')
.then(res => {
this.followingIndex = undefined;
this.popularAccounts.splice(index, 1);
}).catch(err => {
this.followingIndex = undefined;
swal('Oops!', 'An error occured when attempting to follow this account.', 'error');
});
},
goToProfile(account) {
this.$router.push({
path: `/i/web/profile/${account.id}`,
params: {
id: account.id,
cachedProfile: account,
cachedUser: this.profile
}
})
},
toggleTab(index) {
this.tab = index;
setTimeout(() => {
window.scrollTo({top: 0, behavior: 'smooth'});
}, 300);
},
openManageModal() {
event.currentTarget.blur();
swal('Settings', 'Discover settings here', 'info');
}
}
}
</script>
<style lang="scss" scoped>
.card-img {
position: relative;
img {
object-fit: cover;
width: 100%;
height: 200px;
border-radius: 10px;
}
&:before,
&:after {
content: "";
background: rgba(0,0,0,0.2);
z-index: 2;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
border-radius: 10px;
}
.title {
position: absolute;
bottom: 5px;
left: 10px;
font-size: 40px;
color: #fff;
z-index: 3;
font-weight: 700;
}
&.big {
img {
height: 300px;
}
}
}
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.bg-stellar {
background: #7474BF;
background: -webkit-linear-gradient(to right, #348AC7, #7474BF);
background: linear-gradient(to right, #348AC7, #7474BF);
}
.bg-berry {
background: #5433FF;
background: -webkit-linear-gradient(to right, #acb6e5, #86fde8);
background: linear-gradient(to right, #acb6e5, #86fde8);
}
.bg-midnight {
background: #232526;
background: -webkit-linear-gradient(to right, #414345, #232526);
background: linear-gradient(to right, #414345, #232526);
}
.media-body {
margin-right: 0.5rem;
}
.avatar {
border-radius: 15px;
}
.username {
font-size: 14px;
line-height: 14px;
margin-bottom: 2px;
word-break: break-word !important;
word-wrap: break-word !important;
}
.display-name {
margin-bottom: 0;
font-size: 12px;
word-break: break-word !important;
word-wrap: break-word !important;
}
.follower-count {
margin-bottom: 0;
font-size: 10px;
word-break: break-word !important;
word-wrap: break-word !important;
}
.follow {
background-color: var(--primary);
border-radius: 18px;
font-weight: 600;
padding: 5px 15px;
}
</style>

View file

@ -0,0 +1,182 @@
<template>
<div class="discover-find-friends-component">
<div v-if="isLoaded" class="container-fluid mt-3">
<div class="row">
<div class="col-md-4 col-lg-3">
<sidebar :user="profile" />
</div>
<div class="col-md-6 col-lg-6">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<h1 class="font-default">Find Friends</h1>
<!-- <p class="font-default lead">Posts from hashtags you follow</p> -->
<hr>
<b-spinner v-if="isLoading" />
<div v-if="!isLoading" class="row justify-content-center">
<div class="col-12 col-lg-10 mb-3" v-for="(profile, index) in popularAccounts">
<div class="card shadow-sm border-0 rounded-px">
<div class="card-body p-2">
<profile-card
:key="'pfc' + index"
:profile="profile"
class="w-100"
v-on:follow="follow(index)"
v-on:unfollow="unfollow(index)"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
import Drawer from './../partials/drawer.vue';
import Sidebar from './../partials/sidebar.vue';
import StatusCard from './../partials/TimelineStatus.vue';
import ProfileCard from './../partials/profile/ProfileHoverCard.vue';
export default {
components: {
"drawer": Drawer,
"sidebar": Sidebar,
"status-card": StatusCard,
"profile-card": ProfileCard
},
data() {
return {
isLoaded: true,
isLoading: true,
profile: window._sharedData.user,
feed: [],
popular: [],
popularAccounts: [],
popularLoaded: false,
breadcrumbItems: [
{
text: 'Discover',
href: '/i/web/discover'
},
{
text: 'Find Friends',
active: true
}
]
}
},
mounted() {
this.fetchConfig();
},
methods: {
fetchConfig() {
axios.get('/api/pixelfed/v2/discover/meta')
.then(res => {
if(res.data.friends.enabled == false) {
this.$router.push('/i/web/discover');
} else {
this.fetchPopularAccounts();
}
})
.catch(e => {
this.isLoading = false;
})
},
fetchPopular() {
axios.get('/api/pixelfed/v2/discover/account-insights')
.then(res => {
this.popular = res.data;
this.popularLoaded = true;
this.isLoading = false;
})
.catch(e => {
this.isLoading = false;
})
},
formatCount(val) {
return App.util.format.count(val);
},
timeago(ts) {
return App.util.format.timeAgo(ts);
},
fetchPopularAccounts() {
axios.get('/api/pixelfed/discover/accounts/popular')
.then(res => {
this.popularAccounts = res.data;
this.isLoading = false;
})
.catch(e => {
this.isLoading = false;
})
},
follow(index) {
axios.post('/api/v1/accounts/' + this.popularAccounts[index].id + '/follow')
.then(res => {
this.newlyFollowed++;
this.$store.commit('updateRelationship', [res.data]);
this.$emit('update-profile', {
'following_count': this.profile.following_count + 1
})
});
},
unfollow(index) {
axios.post('/api/v1/accounts/' + this.popularAccounts[index].id + '/unfollow')
.then(res => {
this.newlyFollowed--;
this.$store.commit('updateRelationship', [res.data]);
this.$emit('update-profile', {
'following_count': this.profile.following_count - 1
})
});
}
}
}
</script>
<style lang="scss">
.discover-find-friends-component {
.bg-stellar {
background: #7474BF;
background: -webkit-linear-gradient(to right, #348AC7, #7474BF);
background: linear-gradient(to right, #348AC7, #7474BF);
}
.bg-midnight {
background: #232526;
background: -webkit-linear-gradient(to right, #414345, #232526);
background: linear-gradient(to right, #414345, #232526);
}
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.active {
font-weight: 700;
}
.profile-hover-card-inner {
width: 100%;
.d-flex {
max-width: 100% !important;
}
}
}
</style>

View file

@ -0,0 +1,384 @@
<template>
<div class="discover-my-hashtags-component">
<div v-if="isLoaded" class="container-fluid mt-3">
<div class="row">
<div class="col-md-4 col-lg-3">
<sidebar :user="profile" />
</div>
<div class="col-md-6 col-lg-6">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<h1 class="font-default">My Hashtags</h1>
<p class="font-default lead">Posts from hashtags you follow</p>
<hr>
<b-spinner v-if="isLoading" />
<status-card
v-if="!isLoading"
v-for="(post, index) in feed"
:key="'ti1:'+index+':'+post.id"
:profile="profile"
:status="post"
@like="likeStatus(index)"
@unlike="unlikeStatus(index)"
@share="shareStatus(index)"
@unshare="unshareStatus(index)"
@menu="openContextMenu(index)"
@mod-tools="handleModTools(index)"
@likes-modal="openLikesModal(index)"
@shares-modal="openSharesModal(index)"
@bookmark="handleBookmark(index)"
/>
<p v-if="!isLoading && tagsLoaded && feed.length == 0" class="lead">No hashtags found :(</p>
</div>
<div class="col-md-2 col-lg-3">
<div class="nav flex-column nav-pills font-default">
<a
v-for="(tag, idx) in tags"
class="nav-link"
:class="{ active: tagIndex == idx }"
href="#"
@click.prevent="toggleTag(idx)">
{{ tag }}
</a>
</div>
</div>
</div>
</div>
<context-menu
v-if="showMenu"
ref="contextMenu"
:status="feed[postIndex]"
:profile="profile"
@moderate="commitModeration"
@delete="deletePost"
@report-modal="handleReport"
/>
<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"
/>
</div>
</template>
<script type="text/javascript">
import Drawer from './../partials/drawer.vue';
import Sidebar from './../partials/sidebar.vue';
import StatusCard from './../partials/TimelineStatus.vue';
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';
export default {
components: {
"drawer": Drawer,
"sidebar": Sidebar,
"context-menu": ContextMenu,
"likes-modal": LikesModal,
"shares-modal": SharesModal,
"report-modal": ReportModal,
"status-card": StatusCard
},
data() {
return {
isLoaded: true,
isLoading: true,
profile: window._sharedData.user,
tagIndex: 0,
tags: [],
feed: [],
tagsLoaded: false,
breadcrumbItems: [
{
text: 'Discover',
href: '/i/web/discover'
},
{
text: 'My Hashtags',
active: true
}
],
canLoadMore: true,
isFetchingMore: false,
endFeedReached: false,
postIndex: 0,
showMenu: false,
showLikesModal: false,
likesModalPost: {},
showReportModal: false,
reportedStatus: {},
reportedStatusId: 0,
showSharesModal: false,
sharesModalPost: {},
}
},
mounted() {
this.fetchHashtags();
},
methods: {
fetchHashtags() {
axios.get('/api/local/discover/tag/list')
.then(res => {
this.tags = res.data;
this.tagsLoaded = true;
if(this.tags.length) {
this.fetchTagFeed(this.tags[0]);
} else {
this.isLoading = false;
}
})
.catch(e => {
this.isLoading = false;
})
},
fetchTagFeed(hashtag) {
this.isLoading = true;
axios.get('/api/v2/discover/tag', {
params: {
hashtag: hashtag
}
})
.then(res => {
this.feed = res.data.tags.map(p => p.status);
this.isLoading = false;
})
.catch(e => {
this.isLoading = false;
})
},
toggleTag(tag) {
this.tagIndex = tag;
this.fetchTagFeed(this.tags[tag]);
},
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;
})
},
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;
})
},
openContextMenu(idx) {
this.postIndex = idx;
this.showMenu = true;
this.$nextTick(() => {
this.$refs.contextMenu.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);
},
handleReport(post) {
this.reportedStatusId = post.id;
this.$nextTick(() => {
this.reportedStatus = post;
this.$refs.reportModal.open();
});
},
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();
});
},
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.$bvToast.toast('Cannot bookmark post at this time.', {
title: 'Bookmark Error',
variant: 'danger',
autoHideDelay: 5000
});
});
},
}
}
</script>
<style lang="scss" scoped>
.discover-my-hashtags-component {
.bg-stellar {
background: #7474BF;
background: -webkit-linear-gradient(to right, #348AC7, #7474BF);
background: linear-gradient(to right, #348AC7, #7474BF);
}
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.active {
font-weight: 700;
}
}
</style>

View file

@ -0,0 +1,190 @@
<template>
<div class="discover-insights-component">
<div v-if="isLoaded" class="container-fluid mt-3">
<div class="row">
<div class="col-md-4 col-lg-3">
<sidebar :user="profile" />
</div>
<div class="col-md-6 col-lg-6">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<h1 class="font-default">Account Insights</h1>
<p class="font-default lead">A brief overview of your account</p>
<hr>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<div class="card bg-midnight">
<div class="card-body font-default text-white">
<h1 class="display-4 mb-n2">{{ formatCount(profile.statuses_count) }}</h1>
<p class="primary lead mb-0 font-weight-bold">Posts</p>
</div>
</div>
</div>
<div class="col-12 col-md-6 mb-3">
<div class="card bg-midnight">
<div class="card-body font-default text-white">
<h1 class="display-4 mb-n2">{{ formatCount(profile.followers_count) }}</h1>
<p class="primary lead mb-0 font-weight-bold">Followers</p>
</div>
</div>
</div>
</div>
<div v-if="profile.statuses_count" class="card my-3 bg-midnight">
<div class="card-header bg-dark border-bottom border-primary text-white font-default lead">Popular Posts</div>
<div v-if="!popularLoaded" class="card-body text-white">
<b-spinner/>
</div>
<ul v-else class="list-group list-group-flush font-default text-white">
<li v-for="post in popular" class="list-group-item bg-midnight">
<div class="media align-items-center">
<img
v-if="post.media_attachments.length"
:src="post.media_attachments[0].url"
onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0'"
class="media-photo shadow">
<div class="media-body">
<p class="media-caption mb-0">{{ post.content_text.slice(0, 40) }}</p>
<p class="mb-0">
<span class="font-weight-bold">{{ post.favourites_count }} Likes</span>
<span class="mx-2">·</span>
<span class="text-muted">Posted {{ timeago(post.created_at) }} ago</span>
</p>
</div>
<button class="btn btn-primary primary font-weight-bold rounded-pill" @click="gotoPost(post)">View</button>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
import Drawer from './../partials/drawer.vue';
import Sidebar from './../partials/sidebar.vue';
import StatusCard from './../partials/TimelineStatus.vue';
export default {
components: {
"drawer": Drawer,
"sidebar": Sidebar,
"status-card": StatusCard
},
data() {
return {
isLoaded: true,
isLoading: true,
profile: window._sharedData.user,
feed: [],
popular: [],
popularLoaded: false,
breadcrumbItems: [
{
text: 'Discover',
href: '/i/web/discover'
},
{
text: 'Account Insights',
active: true
}
]
}
},
mounted() {
this.fetchConfig();
},
methods: {
fetchConfig() {
axios.get('/api/pixelfed/v2/discover/meta')
.then(res => {
if(res.data.insights.enabled == false) {
this.$router.push('/i/web/discover');
}
this.fetchPopular();
})
},
fetchPopular() {
axios.get('/api/pixelfed/v2/discover/account-insights')
.then(res => {
this.popular = res.data.filter(p => {
return p.favourites_count;
});
this.popularLoaded = true;
})
},
formatCount(val) {
return App.util.format.count(val);
},
timeago(ts) {
return App.util.format.timeAgo(ts);
},
gotoPost(status) {
this.$router.push({
name: 'post',
path: `/i/web/post/${status.id}`,
params: {
id: status.id,
cachedStatus: status,
cachedProfile: this.profile
}
})
}
}
}
</script>
<style lang="scss" scoped>
.discover-insights-component {
.bg-stellar {
background: #7474BF;
background: -webkit-linear-gradient(to right, #348AC7, #7474BF);
background: linear-gradient(to right, #348AC7, #7474BF);
}
.bg-midnight {
background: #232526;
background: -webkit-linear-gradient(to right, #414345, #232526);
background: linear-gradient(to right, #414345, #232526);
}
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.active {
font-weight: 700;
}
.media-photo {
width: 70px;
height: 70px;
border-radius: 8px;
margin-right: 2rem;
object-fit: cover;
}
.media-caption {
letter-spacing: -0.3px;
font-size: 17px;
opacity: 0.7;
}
}
</style>

View file

@ -0,0 +1,172 @@
<template>
<div class="discover-my-memories web-wrapper">
<div v-if="isLoaded" class="container-fluid mt-3">
<div class="row">
<div class="col-md-4 col-lg-3">
<sidebar :user="profile" />
</div>
<div v-if="tabIndex === 0" class="col-md-6 col-lg-6">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<h1 class="font-default">My Memories</h1>
<p class="font-default lead">Posts from this day in previous years</p>
<hr>
<b-spinner v-if="!feedLoaded" />
<status-card
v-for="(post, idx) in feed"
:key="'ti0:'+idx+':'+post.id"
:profile="profile"
:status="post"
/>
<p v-if="feedLoaded && feed.length == 0" class="lead">No memories found :(</p>
</div>
<div v-else-if="tabIndex === 1" class="col-md-6 col-lg-6">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<h1 class="font-default">My Memories</h1>
<p class="font-default lead">Posts I've liked from this day in previous years</p>
<hr>
<b-spinner v-if="!likedLoaded" />
<status-card
v-for="(post, idx) in liked"
:key="'ti1:'+idx+':'+post.id"
:profile="profile"
:status="post"
/>
<p v-if="likedLoaded && liked.length == 0" class="lead">No memories found :(</p>
</div>
<div class="col-md-2 col-lg-3">
<div class="nav flex-column nav-pills font-default">
<a class="nav-link" :class="{ active: tabIndex == 0 }" href="#" @click.prevent="toggleTab(0)">My Posts</a>
<a class="nav-link" :class="{ active: tabIndex == 1 }" href="#" @click.prevent="toggleTab(1)">Posts I've Liked</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
import Drawer from './../partials/drawer.vue';
import Sidebar from './../partials/sidebar.vue';
import StatusCard from './../partials/TimelineStatus.vue';
export default {
components: {
"drawer": Drawer,
"sidebar": Sidebar,
"status-card": StatusCard
},
data() {
return {
isLoaded: true,
profile: window._sharedData.user,
curDate: undefined,
tabIndex: 0,
feedLoaded: false,
likedLoaded: false,
feed: [],
liked: [],
breadcrumbItems: [
{
text: 'Discover',
href: '/i/web/discover'
},
{
text: 'My Memories',
active: true
}
]
}
},
mounted() {
this.curDate = new Date();
this.fetchConfig();
},
methods: {
fetchConfig() {
if(
window._sharedData.hasOwnProperty('discoverMeta') &&
window._sharedData.discoverMeta
) {
this.config = window._sharedData.discoverMeta;
this.isLoaded = true;
if(this.config.memories.enabled == false) {
this.$router.push('/i/web/discover');
} else {
this.fetchMemories();
}
return;
}
axios.get('/api/pixelfed/v2/discover/meta')
.then(res => {
this.config = res.data;
this.isLoaded = true;
window._sharedData.discoverMeta = res.data;
if(res.data.memories.enabled == false) {
this.$router.push('/i/web/discover');
} else {
this.fetchMemories();
}
})
},
fetchMemories() {
axios.get('/api/pixelfed/v2/discover/memories')
.then(res => {
this.feed = res.data;
this.feedLoaded = true;
});
},
fetchLiked() {
axios.get('/api/pixelfed/v2/discover/memories?type=liked')
.then(res => {
this.liked = res.data;
this.likedLoaded = true;
});
},
toggleTab(idx) {
if(idx == 1) {
if(!this.likedLoaded) {
this.fetchLiked();
}
}
this.tabIndex = idx;
}
}
}
</script>
<style lang="scss" scoped>
.discover-my-memories {
.bg-stellar {
background: #7474BF;
background: -webkit-linear-gradient(to right, #348AC7, #7474BF);
background: linear-gradient(to right, #348AC7, #7474BF);
}
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.active {
font-weight: 700;
}
}
</style>

View file

@ -0,0 +1,149 @@
<template>
<div class="discover-serverfeeds-component">
<div class="container-fluid mt-3">
<div class="row">
<div class="col-md-4 col-lg-3">
<sidebar :user="profile" />
</div>
<div class="col-md-6 col-lg-6">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<h1 class="font-default">Server Timelines</h1>
<p class="font-default lead">Browse timelines of a specific instance</p>
<hr>
<b-spinner v-if="isLoading && !initialTab" />
<status-card
v-if="!isLoading"
v-for="(post, idx) in feed"
:key="'ti1:'+idx+':'+post.id"
:profile="profile"
:status="post"
/>
<p v-if="!initialTab && !isLoading && feed.length == 0" class="lead">No posts found :(</p>
<div v-if="initialTab === true">
<p v-if="config.server.mode == 'allowlist'" class="lead">Select an instance from the menu</p>
</div>
</div>
<div class="col-md-2 col-lg-3">
<div v-if="config.server.mode === 'allowlist'" class="nav flex-column nav-pills font-default">
<a
v-for="(tag, idx) in domains"
class="nav-link"
:class="{ active: tagIndex == idx }"
href="#"
@click.prevent="toggleTag(idx)">
{{ tag }}
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
import Drawer from './../partials/drawer.vue';
import Sidebar from './../partials/sidebar.vue';
import StatusCard from './../partials/TimelineStatus.vue';
export default {
components: {
"drawer": Drawer,
"sidebar": Sidebar,
"status-card": StatusCard
},
data() {
return {
isLoaded: false,
isLoading: true,
initialTab: true,
config: {},
profile: window._sharedData.user,
tagIndex: undefined,
domains: [],
feed: [],
breadcrumbItems: [
{
text: 'Discover',
href: '/i/web/discover'
},
{
text: 'Server Timelines',
active: true
}
]
}
},
mounted() {
this.fetchConfig();
},
methods: {
fetchConfig() {
axios.get('/api/pixelfed/v2/discover/meta')
.then(res => {
this.config = res.data;
if(this.config.server.enabled == false) {
this.$router.push('/i/web/discover');
}
if(this.config.server.mode === 'allowlist') {
this.domains = this.config.server.domains.split(',');
}
})
},
fetchFeed(domain) {
this.isLoading = true;
axios.get('/api/pixelfed/v2/discover/server-timeline', {
params: {
domain: domain
}
}).then(res => {
this.feed = res.data;
this.isLoading = false;
this.isLoaded = true;
})
.catch(err => {
this.feed = [];
this.tagIndex = null;
this.isLoaded = true;
this.isLoading = false;
})
},
toggleTag(tag) {
this.initialTab = false;
this.tagIndex = tag;
this.fetchFeed(this.domains[tag]);
}
}
}
</script>
<style lang="scss" scoped>
.discover-serverfeeds-component {
.bg-stellar {
background: #7474BF;
background: -webkit-linear-gradient(to right, #348AC7, #7474BF);
background: linear-gradient(to right, #348AC7, #7474BF);
}
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.active {
font-weight: 700;
}
}
</style>

View file

@ -0,0 +1,280 @@
<template>
<div class="discover-admin-settings-component">
<div v-if="isLoaded" class="container-fluid mt-3">
<div class="row">
<div class="col-md-4 col-lg-3">
<sidebar :user="profile" />
</div>
<div class="col-md-6 col-lg-6">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<h1 class="font-default">Discover Settings</h1>
<!-- <p class="font-default lead">Browse timelines of a specific instance</p> -->
<hr>
<div class="card font-default shadow-none border">
<div class="card-header">
<p class="text-center font-weight-bold mb-0">Manage Features</p>
</div>
<div class="card-body">
<div class="mb-2">
<b-form-checkbox size="lg" v-model="hashtags.enabled" name="check-button" switch class="font-weight-bold">
My Hashtags
</b-form-checkbox>
<p class="text-muted">Allow users to browse timelines of hashtags they follow</p>
</div>
<div class="mb-2">
<b-form-checkbox size="lg" v-model="memories.enabled" name="check-button" switch class="font-weight-bold">
My Memories
</b-form-checkbox>
<p class="text-muted">Allow users to access Memories, a timeline of posts they made or liked on this day in past years</p>
</div>
<div class="mb-2">
<b-form-checkbox size="lg" v-model="insights.enabled" name="check-button" switch class="font-weight-bold">
Account Insights
</b-form-checkbox>
<p class="text-muted">Allow users to access Account Insights, an overview of their account activity</p>
</div>
<div class="mb-2">
<b-form-checkbox size="lg" v-model="friends.enabled" name="check-button" switch class="font-weight-bold">
Find Friends
</b-form-checkbox>
<p class="text-muted">Allow users to access Find Friends, a directory of popular accounts</p>
</div>
<div>
<b-form-checkbox size="lg" v-model="server.enabled" name="check-button" switch class="font-weight-bold">
Server Timelines
</b-form-checkbox>
<p class="text-muted">Allow users to access Server Timelines, a timeline of public posts from a specific instance</p>
</div>
</div>
</div>
<div v-if="server.enabled" class="card font-default shadow-none border my-3">
<div class="card-header">
<p class="text-center font-weight-bold mb-0">Manage Server Timelines</p>
</div>
<div class="card-body">
<div class="mb-2">
<b-form-group label="Server Mode">
<b-form-radio v-model="server.mode" value="all" disabled>Allow any instance (Not Recommended)</b-form-radio>
<b-form-radio v-model="server.mode" value="allowlist">Limit by approved domains</b-form-radio>
</b-form-group>
<p class="text-muted">Set the allowed instances to browse</p>
</div>
<div v-if="server.mode == 'allowlist'">
<b-form-group label="Allowed Domains">
<b-form-textarea
v-model="server.domains"
placeholder="Add domains to allow here, separated by commas"
rows="3"
max-rows="6"
></b-form-textarea>
</b-form-group>
</div>
</div>
</div>
</div>
<div class="col-md-2 col-lg-3">
<button v-if="hasChanged" class="btn btn-primary btn-block primary font-weight-bold" @click="saveFeatures">Save changes</button>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
import Drawer from './../partials/drawer.vue';
import Sidebar from './../partials/sidebar.vue';
import StatusCard from './../partials/TimelineStatus.vue';
export default {
components: {
"drawer": Drawer,
"sidebar": Sidebar,
"status-card": StatusCard
},
data() {
return {
isLoaded: false,
isLoading: true,
profile: window._sharedData.user,
breadcrumbItems: [
{
text: 'Discover',
href: '/i/web/discover'
},
{
text: 'Settings',
active: true
}
],
hasChanged: false,
features: {},
original: undefined,
hashtags: { enabled: undefined },
memories: { enabled: undefined },
insights: { enabled: undefined },
friends: { enabled: undefined },
server: { enabled: undefined, mode: 'allowlist', domains: '' },
}
},
watch: {
hashtags: {
deep: true,
handler: function(val, old) {
this.updateFeatures('hashtags');
},
},
memories: {
deep: true,
handler: function(val, old) {
this.updateFeatures('memories');
},
},
insights: {
deep: true,
handler: function(val, old) {
this.updateFeatures('insights');
},
},
friends: {
deep: true,
handler: function(val, old) {
this.updateFeatures('friends');
},
},
server: {
deep: true,
handler: function(val, old) {
this.updateFeatures('server');
},
}
},
beforeMount() {
if(!this.profile.is_admin) {
this.$router.push('/i/web/discover');
}
this.fetchConfig();
},
methods: {
fetchConfig() {
axios.get('/api/pixelfed/v2/discover/meta')
.then(res => {
this.original = res.data;
this.storeOriginal(res.data);
})
},
storeOriginal(data) {
this.friends.enabled = data.friends.enabled;
this.hashtags.enabled = data.hashtags.enabled;
this.insights.enabled = data.insights.enabled;
this.memories.enabled = data.memories.enabled;
this.server = {
domains: data.server.domains,
enabled: data.server.enabled,
mode: data.server.mode
};
this.isLoaded = true;
},
updateFeatures(id) {
if(!this.isLoaded) {
return;
}
let changed = false;
if(this.friends.enabled !== this.original.friends.enabled) {
changed = true;
}
if(this.hashtags.enabled !== this.original.hashtags.enabled) {
changed = true;
}
if(this.insights.enabled !== this.original.insights.enabled) {
changed = true;
}
if(this.memories.enabled !== this.original.memories.enabled) {
changed = true;
}
if(this.server.enabled !== this.original.server.enabled) {
changed = true;
}
if(this.server.domains !== this.original.server.domains) {
changed = true;
}
if(this.server.mode !== this.original.server.mode) {
changed = true;
}
// if(JSON.stringify(this.server) !== JSON.stringify(this.original.server)) {
// changed = true;
// }
this.hasChanged = changed;
},
saveFeatures() {
axios.post('/api/pixelfed/v2/discover/admin/features', {
features: {
friends: this.friends,
hashtags: this.hashtags,
insights: this.insights,
memories: this.memories,
server: this.server
}
})
.then(res => {
// let data = {
// friends: res.data.friends,
// hashtags: res.data.hashtags,
// insights: res.data.insights,
// memories: res.data.memories,
// server: res.data.server
// }
// this.original = data;
this.server = res.data.server;
this.$bvToast.toast('Successfully updated settings!', {
title: 'Discover Settings',
autoHideDelay: 5000,
appendToast: true,
variant: 'success'
})
})
}
}
}
</script>
<style lang="scss" scoped>
.discover-admin-settings-component {
.bg-stellar {
background: #7474BF;
background: -webkit-linear-gradient(to right, #348AC7, #7474BF);
background: linear-gradient(to right, #348AC7, #7474BF);
}
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.active {
font-weight: 700;
}
}
</style>