mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-23 05:23:17 +00:00
Merge pull request #4465 from pixelfed/staging
Add missing vue components + spa.js
This commit is contained in:
commit
1dd9617da2
61 changed files with 17254 additions and 15 deletions
128
resources/assets/components/Changelog.vue
Normal file
128
resources/assets/components/Changelog.vue
Normal file
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div class="web-wrapper">
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="row">
|
||||
<div class="col-md-3 d-md-block">
|
||||
<sidebar :user="profile" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="jumbotron shadow-sm bg-white">
|
||||
<div class="text-center">
|
||||
<h1 class="font-weight-bold">What's New</h1>
|
||||
<p class="lead mb-0">A log of changes in our new UI (Metro 2.0)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pb-3">
|
||||
<p class="lead">Apr 3, 2022</p>
|
||||
<ul>
|
||||
<li class="font-weight-bold">
|
||||
Dark Mode <br />
|
||||
<p class="small">To enable dark or light mode, click on the top nav menu and then select UI Settings</p>
|
||||
</li>
|
||||
<li>Improved internationalization support with the addition of 7 new languages</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pb-3">
|
||||
<p class="lead">March 2022</p>
|
||||
<ul>
|
||||
<li class="font-weight-bold">Full screen previews on photo albums</li>
|
||||
<li class="font-weight-bold">Filter notifications by type on notifications tab</li>
|
||||
<li>Add "Shared by" link to posts that opens a list of accounts that reblogged the post</li>
|
||||
<li>Fix private profile feed not loading for owner</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pb-3">
|
||||
<p class="lead">Febuary 2022</p>
|
||||
<ul>
|
||||
<li class="font-weight-bold">New Discover layout with My Hashtags, My Memories, Account Insights, Find Friends and Server Timelines</li>
|
||||
<li class="font-weight-bold">Mobile app drawer menu</li>
|
||||
<li class="font-weight-bold">Add Preferred Profile Layout UI setting</li>
|
||||
<li>Add search bar to mobile breakpoints and adjust avatar size when necessary</li>
|
||||
<li>Improved profile layout on mobile breakpoints</li>
|
||||
<li class="font-weight-bold">Threaded Comments Beta</li>
|
||||
<li>Changed default media preview setting to non-fixed height media</li>
|
||||
<li>Improved Compose Location search, will display popular locations first</li>
|
||||
<li>Added "Comment" button to comment reply form and changed textarea/multiline icon toggle</li>
|
||||
<li>Fixed incorrect avatar in profile post comment drawers</li>
|
||||
<li>Hashtags will now include remote/federated posts on hashtag feeds</li>
|
||||
<li>Moved media license to post header</li>
|
||||
<li class="font-weight-bold">Improved Media Previews - disable to restore original preview aspect ratios</li>
|
||||
<li class="font-weight-bold">Comments + Hovercards - comment usernames now support hovercards</li>
|
||||
<li class="font-weight-bold">Quick Avatar Updates - click the <i class="far fa-cog"></i> button on your avatar. Supports drag-n-drop and updates without page refresh in most places.</li>
|
||||
<li class="font-weight-bold">Follows You badge on hovercards when applicable</li>
|
||||
<li>Add Hide Counts & Stats setting</li>
|
||||
<li>Fix nsfw videos not displaying sensitive warning</li>
|
||||
<li>Added mod tools button to posts for admin accounts</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pb-3">
|
||||
<p class="lead">January 2022</p>
|
||||
<ul>
|
||||
<li>Fixed comment deletion</li>
|
||||
<li class="font-weight-bold">New Reaction bar on posts - to disable toggle this option in UI Settings menu</li>
|
||||
<li class="font-weight-bold">Fresher Comments - fixed bug that prevented new comments from appearing and added a Refresh button to do it manually in the event it's needed</li>
|
||||
<li>Added Comment Autoloading setting, enabled by default, it loads the first 3 comments</li>
|
||||
<li>Added Likes feed to your own profile to view posts you liked</li>
|
||||
<li>Fixed custom emoji rendering on sidebar, profiles and hover cards</li>
|
||||
<li>Fixed duplicate notifications on feeds</li>
|
||||
<li>New onboarding home feed with 6 popular accounts to help newcomers find accounts to follow</li>
|
||||
<li>Added pronouns to hovercards</li>
|
||||
<li>Fixed custom emoji rendering on home timeline</li>
|
||||
<li class="font-weight-bold">Added Hovercards to usernames on timelines</li>
|
||||
<li class="font-weight-bold">Improved search bar, now resolves (and imports) remote accounts and posts, including webfinger addresses</li>
|
||||
<li>Added full account usernames to notifications on hover</li>
|
||||
<li>Discover page will default to the Yearly tab if Daily tab is empty</li>
|
||||
<li>Hashtag feed improvements (fix pagination placeholders, use 4x4 grid on larger screens)</li>
|
||||
<li class="font-weight-bold">New report modal for posts & comments</li>
|
||||
<li>Fixed profile <i class="far fa-bars px-1"></i> feed status interactions</li>
|
||||
<li>Improved profile mobile layout</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="pb-3">
|
||||
<p class="lead">Older</p>
|
||||
<p>To view the full list of changes, view the project changelog <a href="https://raw.githubusercontent.com/pixelfed/pixelfed/dev/CHANGELOG.md">here</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="alert alert-primary">
|
||||
<p class="mb-0 small">We know some of our UI changes are controversial. We value your feedback and are working hard to incorporate it. Your patience is greatly appreciated!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<drawer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import Sidebar from './partials/sidebar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"sidebar": Sidebar,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
profile: undefined,
|
||||
instance: undefined,
|
||||
nodeinfo: undefined,
|
||||
config: window.App.config
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.profile = window._sharedData.user;
|
||||
}
|
||||
}
|
||||
</script>
|
53
resources/assets/components/Compose.vue
Normal file
53
resources/assets/components/Compose.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div class="web-wrapper">
|
||||
<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-8">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 offset-md-1">
|
||||
<compose-modal v-on:close="closeModal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<drawer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import Sidebar from './partials/sidebar.vue';
|
||||
import ComposeModal from './../js/components/ComposeModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"sidebar": Sidebar,
|
||||
"compose-modal": ComposeModal
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
profile: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.profile = window._sharedData.user;
|
||||
this.isLoaded = true;
|
||||
},
|
||||
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.$router.push('/i/web');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
297
resources/assets/components/Direct.vue
Normal file
297
resources/assets/components/Direct.vue
Normal file
|
@ -0,0 +1,297 @@
|
|||
<template>
|
||||
<div class="dms-page-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-5 offset-md-1 mb-5 order-2 order-md-1">
|
||||
<h1 class="font-weight-bold mb-4">Direct Messages</h1>
|
||||
<div v-if="threadsLoaded">
|
||||
<div v-for="(thread, idx) in threads" class="card shadow-sm mb-1" style="border-radius:15px;">
|
||||
<div class="card-body p-3">
|
||||
<div class="media">
|
||||
<img :src="thread.accounts[0].avatar" width="45" height="45" class="shadow-sm mr-3" style="border-radius: 15px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
|
||||
<div class="media-body">
|
||||
<!-- <p class="lead mb-n2">{{ thread.accounts[0].display_name }}</p> -->
|
||||
<div class="d-flex justify-content-between align-items-start mb-1">
|
||||
<p class="dm-display-name font-weight-bold mb-0">@{{ thread.accounts[0].acct }}</p>
|
||||
<p class="font-weight-bold small text-muted mb-0">{{ timeago(thread.last_status.created_at) }} ago</p>
|
||||
</div>
|
||||
|
||||
<p class="dm-thread-summary text-muted mr-4" v-html="threadSummary(thread.last_status)"></p>
|
||||
</div>
|
||||
|
||||
<router-link class="btn btn-link stretched-link align-self-center mr-n3" :to="`/i/web/direct/thread/${thread.accounts[0].id}`">
|
||||
<i class="fal fa-chevron-right fa-lg text-lighter"></i>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!threads || !threads.length" class="row justify-content-center">
|
||||
<div class="col-12 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">Your inbox is empty</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canLoadMore">
|
||||
<intersect @enter="enterIntersect">
|
||||
<dm-placeholder />
|
||||
</intersect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<dm-placeholder />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-md-block order-1 order-md-2 mb-4">
|
||||
<button class="btn btn-dark shadow-sm font-weight-bold btn-block" @click="openCompose"><i class="far fa-envelope mr-1"></i> Compose</button>
|
||||
<hr>
|
||||
<div class="d-flex d-md-block">
|
||||
<button
|
||||
v-for="(tab, index) in tabs"
|
||||
class="btn shadow-sm font-weight-bold btn-block text-capitalize mt-0 mt-md-2 mx-1 mx-md-0"
|
||||
:class="[ index === tabIndex ? 'btn-primary' : 'btn-light' ]"
|
||||
@click="toggleTab(index)"
|
||||
>
|
||||
{{ $t('directMessages.' + tab) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<drawer />
|
||||
</div>
|
||||
<div v-else class="d-flex justify-content-center align-items-center" style="height:calc(100vh - 58px);">
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<b-modal
|
||||
ref="compose"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="md"
|
||||
>
|
||||
<div class="card shadow-none mt-4">
|
||||
<div class="card-body d-flex align-items-center justify-content-between flex-column" style="min-height: 50vh;">
|
||||
<h3 class="font-weight-bold">New Direct Message</h3>
|
||||
<div>
|
||||
<p class="mb-0 font-weight-bold">Select Recipient</p>
|
||||
<autocomplete
|
||||
:search="composeSearch"
|
||||
:disabled="composeLoading"
|
||||
placeholder="@dansup"
|
||||
aria-label="Search usernames"
|
||||
:get-result-value="getTagResultValue"
|
||||
@submit="onTagSubmitLocation"
|
||||
ref="autocomplete"
|
||||
>
|
||||
</autocomplete>
|
||||
<p class="small text-muted">Search by username, or webfinger (@dansup@pixelfed.social)</p>
|
||||
<div style="width:300px;"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-dark rounded-pill font-weight-bold px-5 py-1" @click="closeCompose">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import Sidebar from './partials/sidebar.vue';
|
||||
import Placeholder from './partials/placeholders/DirectMessagePlaceholder.vue';
|
||||
import Intersect from 'vue-intersect'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"sidebar": Sidebar,
|
||||
"intersect": Intersect,
|
||||
"dm-placeholder": Placeholder
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
profile: undefined,
|
||||
canLoadMore: true,
|
||||
threadsLoaded: false,
|
||||
composeLoading: false,
|
||||
threads: [],
|
||||
tabIndex: 0,
|
||||
tabs: [
|
||||
'inbox',
|
||||
'sent',
|
||||
'requests'
|
||||
],
|
||||
page: 1,
|
||||
ids: [],
|
||||
isIntersecting: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.profile = window._sharedData.user;
|
||||
this.isLoaded = true;
|
||||
this.fetchThreads();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchThreads() {
|
||||
axios.get('/api/v1/conversations', {
|
||||
params: {
|
||||
scope: this.tabs[this.tabIndex]
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
let data = res.data.filter(m => {
|
||||
return m && m.hasOwnProperty('last_status') && m.last_status;
|
||||
})
|
||||
let ids = data.map(dm => dm.accounts[0].id);
|
||||
this.ids = ids;
|
||||
this.threads = data;
|
||||
this.threadsLoaded = true;
|
||||
this.page++;
|
||||
});
|
||||
},
|
||||
|
||||
timeago(ts) {
|
||||
return App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
enterIntersect() {
|
||||
if(this.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isIntersecting = true;
|
||||
|
||||
axios.get('/api/v1/conversations', {
|
||||
params: {
|
||||
scope: this.tabs[this.tabIndex],
|
||||
page: this.page
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
let data = res.data.filter(m => {
|
||||
return m && m.hasOwnProperty('last_status') && m.last_status;
|
||||
})
|
||||
data.forEach(dm => {
|
||||
if(this.ids.indexOf(dm.accounts[0].id) == -1) {
|
||||
this.ids.push(dm.accounts[0].id);
|
||||
this.threads.push(dm);
|
||||
}
|
||||
})
|
||||
// this.threads.push(...res.data);
|
||||
if(!res.data.length || res.data.length < 5) {
|
||||
this.canLoadMore = false;
|
||||
this.isIntersecting = false;
|
||||
return;
|
||||
}
|
||||
this.page++;
|
||||
this.isIntersecting = false;
|
||||
});
|
||||
},
|
||||
|
||||
toggleTab(index) {
|
||||
event.currentTarget.blur();
|
||||
this.threadsLoaded = false;
|
||||
this.page = 1;
|
||||
this.tabIndex = index;
|
||||
this.fetchThreads();
|
||||
},
|
||||
|
||||
threadSummary(status, len = 50) {
|
||||
if(status.pf_type == 'photo') {
|
||||
let sender = this.profile.id == status.account.id;
|
||||
let icon = '<div class="' + (sender ? 'text-muted' : 'text-primary') + ' border px-2 py-1 mt-1 rounded" style="font-size:11px;width: fit-content"><i class="far fa-image mr-1"></i> <span>';
|
||||
icon += sender ? 'Sent a photo' : 'Received a photo';
|
||||
return icon + '</span></div>';
|
||||
}
|
||||
|
||||
if(status.pf_type == 'video') {
|
||||
let sender = this.profile.id == status.account.id;
|
||||
let icon = '<div class="' + (sender ? 'text-muted' : 'text-primary') + ' border px-2 py-1 mt-1 rounded" style="font-size:11px;width: fit-content"><i class="far fa-video mr-1"></i> <span>';
|
||||
icon += sender ? 'Sent a video' : 'Received a video';
|
||||
return icon + '</span></div>';
|
||||
}
|
||||
|
||||
let res = '';
|
||||
|
||||
if(this.profile.id == status.account.id) {
|
||||
res += '<i class="far fa-reply-all fa-flip-both"></i> ';
|
||||
}
|
||||
|
||||
let content = status.content;
|
||||
let text = content.replace(/(<([^>]+)>)/gi, "");
|
||||
|
||||
if(text.length > len) {
|
||||
return res + text.slice(0, len) + '...';
|
||||
}
|
||||
|
||||
return res + text;
|
||||
},
|
||||
|
||||
openCompose() {
|
||||
this.$refs.compose.show();
|
||||
},
|
||||
|
||||
composeSearch(input) {
|
||||
if (input.length < 1) { return []; };
|
||||
let self = this;
|
||||
let results = [];
|
||||
return axios.post('/api/direct/lookup', {
|
||||
q: input
|
||||
}).then(res => {
|
||||
return res.data;
|
||||
});
|
||||
},
|
||||
|
||||
getTagResultValue(result) {
|
||||
// return '@' + result.name;
|
||||
return result.local ? '@' + result.name : result.name;
|
||||
},
|
||||
|
||||
onTagSubmitLocation(result) {
|
||||
//this.$refs.autocomplete.value = '';
|
||||
this.composeLoading = true;
|
||||
window.location.href = '/i/web/direct/thread/' + result.id;
|
||||
return;
|
||||
},
|
||||
|
||||
closeCompose() {
|
||||
this.$refs.compose.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dms-page-component {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
|
||||
.dm {
|
||||
&-thread-summary {
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
&-display-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
882
resources/assets/components/DirectMessage.vue
Normal file
882
resources/assets/components/DirectMessage.vue
Normal file
|
@ -0,0 +1,882 @@
|
|||
<template>
|
||||
<div class="dm-page-component">
|
||||
<div v-if="isLoaded" class="container-fluid mt-lg-3 pb-lg-5">
|
||||
<div class="row dm-page-component-row">
|
||||
<div class="col-md-3 d-md-block">
|
||||
<sidebar :user="profile" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 p-0">
|
||||
<div v-if="loaded && page == 'read'" class="messages-page">
|
||||
<div class="card shadow-none">
|
||||
<div class="h4 card-header font-weight-bold text-dark d-flex justify-content-between align-items-center" style="letter-spacing: -0.3px;">
|
||||
<button class="btn btn-light rounded-pill text-dark" @click="goBack()">
|
||||
<i class="far fa-chevron-left fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<div>Direct Message</div>
|
||||
|
||||
<button class="btn btn-light rounded-pill text-dark" @click="showOptions()">
|
||||
<i class="far fa-cog fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush" style="position:relative;">
|
||||
<li class="list-group-item border-bottom sticky-top">
|
||||
<p class="text-center small text-muted mb-0">
|
||||
Conversation with <span class="font-weight-bold">{{thread.username}}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<transition name="fade">
|
||||
<ul v-if="showDMPrivacyWarning && showPrivacyWarning" class="list-group list-group-flush dm-privacy-warning" style="position:absolute;top:105px;width:100%;">
|
||||
<li class="list-group-item border-bottom sticky-top bg-warning">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="d-none d-lg-block">
|
||||
<i class="fas fa-exclamation-triangle text-danger fa-lg mr-3"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="small warning-text mb-0 font-weight-bold"><span class="d-inline d-lg-none">DMs</span><span class="d-none d-lg-inline">Direct messages on Pixelfed</span> are not end-to-end encrypted.
|
||||
</p>
|
||||
<p class="small warning-text mb-0 font-weight-bold">
|
||||
Use caution when sharing sensitive data.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-link text-decoration-none" @click="togglePrivacyWarning">
|
||||
<i class="far fa-times-circle fa-lg"></i>
|
||||
<span class="d-none d-lg-block">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
|
||||
<ul class="list-group list-group-flush dm-wrapper" style="overflow-y: scroll;position:relative;flex-direction: column-reverse;">
|
||||
<li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg mb-n2">
|
||||
<message
|
||||
:convo="convo"
|
||||
:thread="thread"
|
||||
:hideAvatars="hideAvatars"
|
||||
:hideTimestamps="hideTimestamps"
|
||||
:largerText="largerText"
|
||||
v-on:confirm-delete="deleteMessage(index)" />
|
||||
</li>
|
||||
|
||||
<li v-if="showLoadMore && thread.messages && thread.messages.length > 5" class="list-group-item border-0">
|
||||
<p class="text-center small text-muted">
|
||||
<button v-if="!loadingMessages" class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" @click="loadOlderMessages()">Load Older Messages</button>
|
||||
<button v-else class="btn btn-primary font-weight-bold rounded-pill btn-sm px-3" disabled>Loading...</button>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="card-footer bg-white p-0">
|
||||
<!-- <form class="border-0 rounded-0 align-middle" method="post" action="#">
|
||||
<textarea class="form-control border-0 rounded-0 no-focus" name="comment" placeholder="Reply ..." autocomplete="off" autocorrect="off" style="height:86px;line-height: 18px;max-height:80px;resize: none; padding-right:115.22px;" v-model="replyText" :disabled="blocked"></textarea>
|
||||
<input type="button" value="Send" :class="[replyText.length ? 'd-inline-block btn btn-sm btn-primary rounded-pill font-weight-bold reply-btn text-decoration-none text-uppercase' : 'd-inline-block btn btn-sm btn-primary rounded-pill font-weight-bold reply-btn text-decoration-none text-uppercase disabled']" :disabled="replyText.length == 0" @click.prevent="sendMessage"/>
|
||||
</form> -->
|
||||
<div class="dm-reply-form">
|
||||
<div class="dm-reply-form-input-group">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
placeholder="Type a message..."
|
||||
:disabled="uploading"
|
||||
v-model="replyText">
|
||||
|
||||
<button
|
||||
class="upload-media-btn btn btn-link"
|
||||
:disabled="uploading"
|
||||
@click="uploadMedia">
|
||||
<i class="far fa-image fa-2x"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="dm-reply-form-submit-btn btn btn-primary"
|
||||
:disabled="!replyText || !replyText.length || showReplyTooLong"
|
||||
@click="sendMessage">
|
||||
<i class="far fa-paper-plane fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploading" class="card-footer dm-status-bar">
|
||||
<p>Uploading ({{uploadProgress}}%) ...</p>
|
||||
</div>
|
||||
|
||||
<div v-if="showReplyLong" class="card-footer dm-status-bar">
|
||||
<p class="text-warning">{{ replyText.length }}/500</p>
|
||||
</div>
|
||||
|
||||
<div v-if="showReplyTooLong" class="card-footer dm-status-bar">
|
||||
<p class="text-danger">{{ replyText.length }}/500 - Your message exceeds the limit of 500 characters</p>
|
||||
</div>
|
||||
|
||||
<div class="d-none card-footer p-0">
|
||||
<p class="d-flex justify-content-between align-items-center mb-0 px-3 py-1 small">
|
||||
<!-- <span class="font-weight-bold" style="color: #D69E2E">
|
||||
<i class="fas fa-circle mr-1"></i>
|
||||
Typing ...
|
||||
</span> -->
|
||||
<span>
|
||||
<!-- <span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
|
||||
<i class="fas fa-share mr-1"></i>
|
||||
Share
|
||||
</span> -->
|
||||
<span class="btn btn-primary btn-sm font-weight-bold py-0 px-3 rounded-pill" @click="uploadMedia">
|
||||
<i class="fas fa-upload mr-1"></i>
|
||||
Add Photo/Video
|
||||
</span>
|
||||
</span>
|
||||
<input type="file" id="uploadMedia" class="d-none" name="uploadMedia" accept="image/jpeg,image/png,image/gif,video/mp4" >
|
||||
<span class="text-muted font-weight-bold">{{replyText.length}}/500</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loaded && page == 'options'" class="messages-page">
|
||||
<div class="card shadow-none">
|
||||
<div class="h4 card-header font-weight-bold text-dark d-flex justify-content-between align-items-center" style="letter-spacing: -0.3px;">
|
||||
<button class="btn btn-light rounded-pill text-dark" @click.prevent="goBack('read')">
|
||||
<i class="far fa-chevron-left fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<div>Direct Message Settings</div>
|
||||
|
||||
<div class="btn btn-light rounded-pill text-dark">
|
||||
<i class="far fa-smile fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-group list-group-flush" style="height: 698px;">
|
||||
<div class="list-group-item media border-bottom">
|
||||
<div class="d-inline-block custom-control custom-switch ml-3">
|
||||
<input type="checkbox" class="custom-control-input" id="customSwitch0" v-model="hideAvatars">
|
||||
<label class="custom-control-label" for="customSwitch0"></label>
|
||||
</div>
|
||||
<div class="d-inline-block ml-3 font-weight-bold">
|
||||
Hide Avatars
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item media border-bottom">
|
||||
<div class="d-inline-block custom-control custom-switch ml-3">
|
||||
<input type="checkbox" class="custom-control-input" id="customSwitch1" v-model="hideTimestamps">
|
||||
<label class="custom-control-label" for="customSwitch1"></label>
|
||||
</div>
|
||||
<div class="d-inline-block ml-3 font-weight-bold">
|
||||
Hide Timestamps
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item media border-bottom">
|
||||
<div class="d-inline-block custom-control custom-switch ml-3">
|
||||
<input type="checkbox" class="custom-control-input" id="customSwitch2" v-model="largerText">
|
||||
<label class="custom-control-label" for="customSwitch2"></label>
|
||||
</div>
|
||||
<div class="d-inline-block ml-3 font-weight-bold">
|
||||
Larger Text
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="list-group-item media border-bottom">
|
||||
<div class="d-inline-block custom-control custom-switch ml-3">
|
||||
<input type="checkbox" class="custom-control-input" id="customSwitch3" v-model="autoRefresh">
|
||||
<label class="custom-control-label" for="customSwitch3"></label>
|
||||
</div>
|
||||
<div class="d-inline-block ml-3 font-weight-bold">
|
||||
Auto Refresh
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="list-group-item media border-bottom d-flex align-items-center">
|
||||
<div class="d-inline-block custom-control custom-switch ml-3">
|
||||
<input type="checkbox" class="custom-control-input" id="customSwitch4" v-model="mutedNotifications">
|
||||
<label class="custom-control-label" for="customSwitch4"></label>
|
||||
</div>
|
||||
<div class="d-inline-block ml-3 font-weight-bold">
|
||||
Mute Notifications
|
||||
<p class="small mb-0">You will not receive any direct message notifications from <strong>{{thread.username}}</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item media border-bottom d-flex align-items-center">
|
||||
<div class="d-inline-block custom-control custom-switch ml-3">
|
||||
<input type="checkbox" class="custom-control-input" id="customSwitch5" v-model="showDMPrivacyWarning">
|
||||
<label class="custom-control-label" for="customSwitch5"></label>
|
||||
</div>
|
||||
<div class="d-inline-block ml-3 font-weight-bold">
|
||||
Show Privacy Warning
|
||||
<p class="small mb-0">Show privacy warning indicating that direct messages are not end-to-end encrypted and that caution is advised when sending sensitive/confidential information.</p>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="conversationProfile" class="col-md-3 d-none d-md-block">
|
||||
<div class="card shadow-sm mb-3" style="border-radius: 15px;">
|
||||
<div class="small card-header font-weight-bold text-uppercase text-lighter" style="letter-spacing: -0.3px;">
|
||||
Conversation
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="media user-card user-select-none">
|
||||
<div>
|
||||
<img :src="conversationProfile.avatar" class="avatar shadow cursor-pointer" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<p
|
||||
class="display-name"
|
||||
v-html="conversationProfile.display_name"
|
||||
@click="gotoProfile(conversationProfile)"
|
||||
>
|
||||
</p>
|
||||
|
||||
<p
|
||||
class="username primary"
|
||||
@click="gotoProfile(conversationProfile)">
|
||||
@{{ conversationProfile.acct }}
|
||||
</p>
|
||||
<p class="stats">
|
||||
<span class="stats-following">
|
||||
<span class="following-count">{{ formatCount(conversationProfile.following_count) }}</span> Following
|
||||
</span>
|
||||
<span class="stats-followers">
|
||||
<span class="followers-count">{{ formatCount(conversationProfile.followers_count) }}</span> Followers
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="card shadow-sm mb-3" style="border-radius: 15px;">
|
||||
<div class="small card-header font-weight-bold text-uppercase text-lighter" style="border-top-left-radius: 15px;letter-spacing: -0.3px;">
|
||||
History
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- <div class="list-group shadow-sm">
|
||||
<div class="list-group-item border-0 border-bottom" style="border-width: 1px;">
|
||||
<p class="mb-0"><i class="far fa-user-plus mr-2"></i> You both follow each other</p>
|
||||
</div>
|
||||
<div class="list-group-item border-0">
|
||||
<p class="mb-0"><i class="far fa-users mr-2"></i> You both follow <a class="font-weight-bold">@pixelfed</a>,<a class="font-weight-bold">@pixeldev</a> and <a class="font-weight-bold">@pixel</a></p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-else class="d-flex justify-content-center align-items-center" style="height:calc(100vh - 58px);">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import Sidebar from './partials/sidebar.vue';
|
||||
import Placeholder from './partials/placeholders/DirectMessagePlaceholder.vue';
|
||||
import Intersect from 'vue-intersect'
|
||||
import ProfileCard from './partials/profile/ProfileHoverCard.vue';
|
||||
import Message from './partials/direct/Message.vue';
|
||||
|
||||
export default {
|
||||
props: ['accountId'],
|
||||
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"sidebar": Sidebar,
|
||||
"intersect": Intersect,
|
||||
"dm-placeholder": Placeholder,
|
||||
"profile-card": ProfileCard,
|
||||
"message": Message
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
profile: undefined,
|
||||
conversationProfile: undefined,
|
||||
isIntersecting: false,
|
||||
|
||||
config: window.App.config,
|
||||
hideAvatars: true,
|
||||
hideTimestamps: false,
|
||||
largerText: false,
|
||||
autoRefresh: false,
|
||||
mutedNotifications: false,
|
||||
blocked: false,
|
||||
loaded: false,
|
||||
page: 'read',
|
||||
pages: ['browse', 'add', 'read'],
|
||||
threads: [],
|
||||
thread: false,
|
||||
threadIndex: false,
|
||||
|
||||
replyText: '',
|
||||
composeUsername: '',
|
||||
uploading: false,
|
||||
uploadProgress: null,
|
||||
|
||||
min_id: null,
|
||||
max_id: null,
|
||||
loadingMessages: false,
|
||||
showLoadMore: true,
|
||||
|
||||
showReplyLong: false,
|
||||
showReplyTooLong: false,
|
||||
|
||||
showPrivacyWarning: true,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.profile = window._sharedData.user;
|
||||
this.isLoaded = true;
|
||||
let self = this;
|
||||
axios.get('/api/v1/accounts/' + this.accountId)
|
||||
.then(res => {
|
||||
this.conversationProfile = res.data;
|
||||
});
|
||||
|
||||
axios.get('/api/direct/thread', {
|
||||
params: {
|
||||
pid: self.accountId
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
self.loaded = true;
|
||||
let d = res.data;
|
||||
this.thread = d;
|
||||
this.threads = [d];
|
||||
this.threadIndex = 0;
|
||||
let mids = d.messages.map(m => m.id);
|
||||
this.max_id = Math.max(...mids);
|
||||
this.min_id = Math.min(...mids);
|
||||
this.mutedNotifications = d.muted;
|
||||
this.markAsRead();
|
||||
//this.messagePoll();
|
||||
// setTimeout(function() {
|
||||
// let objDiv = document.querySelector('.dm-wrapper');
|
||||
// objDiv.scrollTop = objDiv.scrollHeight;
|
||||
// }, 300);
|
||||
});
|
||||
|
||||
let options = localStorage.getItem('px_dm_options');
|
||||
if(options) {
|
||||
options = JSON.parse(options);
|
||||
this.hideAvatars = options.hideAvatars;
|
||||
this.hideTimestamps = options.hideTimestamps;
|
||||
this.largerText = options.largerText;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showDMPrivacyWarning: {
|
||||
get() {
|
||||
return this.$store.state.showDMPrivacyWarning;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
window.localStorage.removeItem('pf_m2s.dmwarncounter');
|
||||
this.$store.commit('setShowDMPrivacyWarning', val);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
mutedNotifications: function(v) {
|
||||
if(v) {
|
||||
axios.post('/api/direct/mute', {
|
||||
id: this.accountId
|
||||
}).then(res => {
|
||||
|
||||
});
|
||||
} else {
|
||||
axios.post('/api/direct/unmute', {
|
||||
id: this.accountId
|
||||
}).then(res => {
|
||||
|
||||
});
|
||||
}
|
||||
this.mutedNotifications = v;
|
||||
},
|
||||
|
||||
hideAvatars: function(v) {
|
||||
this.hideAvatars = v;
|
||||
this.updateOptions();
|
||||
},
|
||||
|
||||
hideTimestamps: function(v) {
|
||||
this.hideTimestamps = v;
|
||||
this.updateOptions();
|
||||
},
|
||||
|
||||
largerText: function(v) {
|
||||
this.largerText = v;
|
||||
this.updateOptions();
|
||||
},
|
||||
|
||||
replyText: function(v) {
|
||||
let limit = 500;
|
||||
|
||||
if(v.length < limit) {
|
||||
this.showReplyLong = false;
|
||||
this.showReplyTooLong = false;
|
||||
}
|
||||
|
||||
if(v.length > limit) {
|
||||
this.showReplyLong = false;
|
||||
this.showReplyTooLong = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if(v.length > (limit - 50)) {
|
||||
this.showReplyTooLong = false;
|
||||
this.showReplyLong = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
sendMessage() {
|
||||
let self = this;
|
||||
let rt = this.replyText;
|
||||
axios.post('/api/direct/create', {
|
||||
'to_id': this.threads[this.threadIndex].id,
|
||||
'message': rt,
|
||||
'type': self.isEmoji(rt) && rt.length < 10 ? 'emoji' : 'text'
|
||||
}).then(res => {
|
||||
let msg = res.data;
|
||||
self.threads[self.threadIndex].messages.unshift(msg);
|
||||
let mids = self.threads[self.threadIndex].messages.map(m => m.id);
|
||||
this.max_id = Math.max(...mids)
|
||||
this.min_id = Math.min(...mids)
|
||||
// setTimeout(function() {
|
||||
// var objDiv = document.querySelector('.dm-wrapper');
|
||||
// objDiv.scrollTop = objDiv.scrollHeight;
|
||||
// }, 300);
|
||||
}).catch(err => {
|
||||
if(err.response.status == 403) {
|
||||
self.blocked = true;
|
||||
swal('Profile Unavailable', 'You cannot message this profile at this time.', 'error');
|
||||
}
|
||||
})
|
||||
this.replyText = '';
|
||||
},
|
||||
|
||||
truncate(t) {
|
||||
return _.truncate(t);
|
||||
},
|
||||
|
||||
deleteMessage(index) {
|
||||
let c = window.confirm('Are you sure you want to delete this message?');
|
||||
if(c) {
|
||||
axios.delete('/api/direct/message', {
|
||||
params: {
|
||||
id: this.thread.messages[index].reportId
|
||||
}
|
||||
}).then(res => {
|
||||
this.thread.messages.splice(index ,1);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reportMessage() {
|
||||
this.closeCtxMenu();
|
||||
let url = '/i/report?type=post&id=' + this.ctxContext.reportId;
|
||||
window.location.href = url;
|
||||
return;
|
||||
},
|
||||
|
||||
uploadMedia(event) {
|
||||
let self = this;
|
||||
$(document).on('change', '#uploadMedia', function(e) {
|
||||
self.handleUpload();
|
||||
});
|
||||
let el = $(event.target);
|
||||
el.attr('disabled', '');
|
||||
$('#uploadMedia').click();
|
||||
el.blur();
|
||||
el.removeAttr('disabled');
|
||||
},
|
||||
|
||||
handleUpload() {
|
||||
let self = this;
|
||||
if(self.uploading) {
|
||||
return;
|
||||
}
|
||||
self.uploading = true;
|
||||
let io = document.querySelector('#uploadMedia');
|
||||
if(!io.files.length) {
|
||||
this.uploading = false;
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(io.files, function(io, i) {
|
||||
let type = io.type;
|
||||
let acceptedMimes = self.config.uploader.media_types.split(',');
|
||||
let validated = $.inArray(type, acceptedMimes);
|
||||
if(validated == -1) {
|
||||
swal('Invalid File Type', 'The file you are trying to add is not a valid mime type. Please upload a '+self.config.uploader.media_types+' only.', 'error');
|
||||
self.uploading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let form = new FormData();
|
||||
form.append('file', io);
|
||||
form.append('to_id', self.threads[self.threadIndex].id);
|
||||
|
||||
let xhrConfig = {
|
||||
onUploadProgress: function(e) {
|
||||
let progress = Math.round( (e.loaded * 100) / e.total );
|
||||
self.uploadProgress = progress;
|
||||
}
|
||||
};
|
||||
|
||||
axios.post('/api/direct/media', form, xhrConfig)
|
||||
.then(function(e) {
|
||||
self.uploadProgress = 100;
|
||||
self.uploading = false;
|
||||
let msg = {
|
||||
id: e.data.id,
|
||||
type: e.data.type,
|
||||
reportId: e.data.reportId,
|
||||
isAuthor: true,
|
||||
text: null,
|
||||
media: e.data.url,
|
||||
timeAgo: '1s',
|
||||
seen: null
|
||||
};
|
||||
self.threads[self.threadIndex].messages.unshift(msg);
|
||||
// setTimeout(function() {
|
||||
// var objDiv = document.querySelector('.dm-wrapper');
|
||||
// objDiv.scrollTop = objDiv.scrollHeight;
|
||||
// }, 300);
|
||||
|
||||
}).catch(function(e) {
|
||||
if(e.hasOwnProperty('response') && e.response.hasOwnProperty('status') ) {
|
||||
switch(e.response.status) {
|
||||
case 451:
|
||||
self.uploading = false;
|
||||
io.value = null;
|
||||
swal('Banned Content', 'This content has been banned and cannot be uploaded.', 'error');
|
||||
break;
|
||||
|
||||
default:
|
||||
self.uploading = false;
|
||||
io.value = null;
|
||||
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
io.value = null;
|
||||
self.uploadProgress = 0;
|
||||
});
|
||||
},
|
||||
|
||||
viewOriginal() {
|
||||
let url = this.ctxContext.media;
|
||||
window.location.href = url;
|
||||
return;
|
||||
},
|
||||
|
||||
isEmoji(text) {
|
||||
const onlyEmojis = text.replace(new RegExp('[\u0000-\u1eeff]', 'g'), '')
|
||||
const visibleChars = text.replace(new RegExp('[\n\r\s]+|( )+', 'g'), '')
|
||||
return onlyEmojis.length === visibleChars.length
|
||||
},
|
||||
|
||||
copyText() {
|
||||
window.App.util.clipboard(this.ctxContext.text);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
clickLink() {
|
||||
let url = this.ctxContext.text;
|
||||
if(this.ctxContext.meta.local != true) {
|
||||
url = '/i/redirect?url=' + encodeURI(this.ctxContext.text);
|
||||
}
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
markAsRead() {
|
||||
return;
|
||||
axios.post('/api/direct/read', {
|
||||
pid: this.accountId,
|
||||
sid: this.max_id
|
||||
}).then(res => {
|
||||
}).catch(err => {
|
||||
});
|
||||
},
|
||||
|
||||
loadOlderMessages() {
|
||||
let self = this;
|
||||
this.loadingMessages = true;
|
||||
|
||||
axios.get('/api/direct/thread', {
|
||||
params: {
|
||||
pid: this.accountId,
|
||||
max_id: this.min_id,
|
||||
}
|
||||
}).then(res => {
|
||||
let d = res.data;
|
||||
if(!d.messages.length) {
|
||||
this.showLoadMore = false;
|
||||
this.loadingMessages = false;
|
||||
return;
|
||||
}
|
||||
let cids = this.thread.messages.map(m => m.id);
|
||||
let m = d.messages.filter(m => {
|
||||
return cids.indexOf(m.id) == -1;
|
||||
}).reverse();
|
||||
let mids = m.map(m => m.id);
|
||||
let min_id = Math.min(...mids);
|
||||
if(min_id == this.min_id) {
|
||||
this.showLoadMore = false;
|
||||
this.loadingMessages = false;
|
||||
return;
|
||||
}
|
||||
this.min_id = min_id;
|
||||
this.thread.messages.push(...m);
|
||||
setTimeout(function() {
|
||||
self.loadingMessages = false;
|
||||
}, 500);
|
||||
}).catch(err => {
|
||||
this.loadingMessages = false;
|
||||
})
|
||||
},
|
||||
|
||||
messagePoll() {
|
||||
let self = this;
|
||||
setInterval(function() {
|
||||
axios.get('/api/direct/thread', {
|
||||
params: {
|
||||
pid: self.accountId,
|
||||
min_id: self.thread.messages[self.thread.messages.length - 1].id
|
||||
}
|
||||
}).then(res => {
|
||||
});
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
showOptions() {
|
||||
this.page = 'options';
|
||||
},
|
||||
|
||||
updateOptions() {
|
||||
let options = {
|
||||
v: 1,
|
||||
hideAvatars: this.hideAvatars,
|
||||
hideTimestamps: this.hideTimestamps,
|
||||
largerText: this.largerText
|
||||
}
|
||||
window.localStorage.setItem('px_dm_options', JSON.stringify(options));
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
return window.App.util.format.count(val);
|
||||
},
|
||||
|
||||
goBack(page = false) {
|
||||
if(page) {
|
||||
this.page = page;
|
||||
} else {
|
||||
this.$router.push('/i/web/direct');
|
||||
}
|
||||
},
|
||||
|
||||
gotoProfile(profile) {
|
||||
this.$router.push(`/i/web/profile/${profile.id}`);
|
||||
},
|
||||
|
||||
togglePrivacyWarning() {
|
||||
console.log('clicked toggle privacy warning');
|
||||
let ls = window.localStorage;
|
||||
let key = 'pf_m2s.dmwarncounter';
|
||||
this.showPrivacyWarning = false;
|
||||
if(ls.getItem(key)) {
|
||||
let count = ls.getItem(key);
|
||||
count++;
|
||||
ls.setItem(key, count);
|
||||
if(count > 5) {
|
||||
this.showDMPrivacyWarning = false;
|
||||
}
|
||||
} else {
|
||||
ls.setItem(key, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dm-page-component {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
|
||||
.user-card {
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 15px;
|
||||
margin-right: 0.8rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.avatar-update-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid #dee2e6 !important;
|
||||
padding: 0;
|
||||
border-radius: 50rem;
|
||||
|
||||
&-icon {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
|
||||
&:before {
|
||||
content: "\F013";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
color: var(--body-color);
|
||||
line-height: 0.8;
|
||||
font-size: 14px;
|
||||
font-weight: 800 !important;
|
||||
user-select: all;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
|
||||
.stats-following {
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
|
||||
.following-count,
|
||||
.followers-count {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dm-reply-form {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--card-bg);
|
||||
padding: 1rem;
|
||||
|
||||
.btn:focus,
|
||||
.btn.focus,
|
||||
input:focus,
|
||||
input.focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
opacity: 20% !important;
|
||||
}
|
||||
|
||||
&-input-group {
|
||||
width: 100%;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
padding-right: 60px;
|
||||
background-color: var(--comment-bg);
|
||||
border-radius: 25px;
|
||||
border-color: var(--comment-bg) !important;
|
||||
font-size: 15px;
|
||||
color: var(--dark);
|
||||
|
||||
}
|
||||
|
||||
.upload-media-btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
&-submit-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.dm-status-bar {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-lighter);
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dm-privacy-warning {
|
||||
p,
|
||||
.btn {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
text-align: left;
|
||||
|
||||
@media (min-width: 992px) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
.dm-wrapper {
|
||||
padding-top: 100px;
|
||||
height: calc(100vh - 240px);
|
||||
|
||||
@media (min-width: 500px) {
|
||||
min-height: 40vh;
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
height: 60vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
405
resources/assets/components/Discover.vue
Normal file
405
resources/assets/components/Discover.vue
Normal 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>
|
332
resources/assets/components/Hashtag.vue
Normal file
332
resources/assets/components/Hashtag.vue
Normal file
|
@ -0,0 +1,332 @@
|
|||
<template>
|
||||
<div class="hashtag-component">
|
||||
<div 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">
|
||||
<div class="card border-0 shadow-sm mb-3" style="border-radius: 18px;">
|
||||
<div class="card-body">
|
||||
<div class="media align-items-center py-3">
|
||||
<div class="media-body">
|
||||
<p class="h3 text-break mb-0">
|
||||
<span class="text-lighter">#</span>{{ hashtag.name }}
|
||||
</p>
|
||||
<p v-if="hashtag.count && hashtag.count > 100" class="mb-0 text-muted font-weight-bold">
|
||||
{{ formatCount(hashtag.count) }} Posts
|
||||
</p>
|
||||
</div>
|
||||
<template v-if="hashtag && hashtag.hasOwnProperty('following') && feed && feed.length">
|
||||
<button
|
||||
v-if="hashtag.following"
|
||||
:disabled="followingLoading"
|
||||
class="btn btn-light hashtag-follow border rounded-pill font-weight-bold py-1 px-4"
|
||||
@click="unfollowHashtag()"
|
||||
>
|
||||
<b-spinner v-if="followingLoading" small />
|
||||
<span v-else>
|
||||
{{ $t('profile.unfollow') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else
|
||||
:disabled="followingLoading"
|
||||
class="btn btn-primary hashtag-follow font-weight-bold rounded-pill py-1 px-4"
|
||||
@click="followHashtag()"
|
||||
>
|
||||
<b-spinner v-if="followingLoading" small />
|
||||
<span v-else>
|
||||
{{ $t('profile.follow') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isLoaded && feedLoaded">
|
||||
<div class="row mx-0 hashtag-feed">
|
||||
<div class="col-6 col-md-4 col-lg-3 p-1" v-for="(status, index) in feed" :key="'tlob:'+index">
|
||||
<a
|
||||
class="card info-overlay card-md-border-0"
|
||||
:href="statusUrl(status)"
|
||||
@click.prevent="goToPost(status)">
|
||||
<div class="square">
|
||||
<div v-if="status.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="status.media_attachments[0].blurhash"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="square-content">
|
||||
<blur-hash-image
|
||||
width="32"
|
||||
height="32"
|
||||
:hash="status.media_attachments[0].blurhash"
|
||||
:src="status.media_attachments[0].url"
|
||||
/>
|
||||
</div>
|
||||
<span v-if="status.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
|
||||
<span v-if="status.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
|
||||
<span v-if="status.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
|
||||
<div class="info-overlay-text">
|
||||
<h5 class="text-white m-auto font-weight-bold">
|
||||
<span>
|
||||
<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
|
||||
<span class="d-flex-inline">{{formatCount(status.reply_count)}}</span>
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="canLoadMore" class="col-12">
|
||||
<intersect @enter="enterIntersect">
|
||||
<div class="d-flex justify-content-center py-5">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</intersect>
|
||||
|
||||
<!-- <div v-else class="ph-item">
|
||||
<div class="ph-picture big"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="feedLoaded && !feed.length" class="row mx-0 hashtag-feed justify-content-center">
|
||||
<div class="col-12 col-md-8 text-center">
|
||||
<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;max-width:400px">
|
||||
<p class="lead text-muted font-weight-bold">{{ $t('hashtags.emptyFeed') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="row justify-content-center align-items-center pt-5 mt-5">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<drawer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import Intersect from 'vue-intersect'
|
||||
import Sidebar from './partials/sidebar.vue';
|
||||
import Rightbar from './partials/rightbar.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"intersect": Intersect,
|
||||
"sidebar": Sidebar,
|
||||
"rightbar": Rightbar,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
profile: undefined,
|
||||
canLoadMore: false,
|
||||
isIntersecting: false,
|
||||
feedLoaded: false,
|
||||
feed: [],
|
||||
page: 1,
|
||||
hashtag: {
|
||||
name: this.id,
|
||||
count: 0
|
||||
},
|
||||
followingLoading: false,
|
||||
maxId: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route': 'init'
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.profile = window._sharedData.user;
|
||||
axios.get('/api/v1/tags/' + this.id, {
|
||||
params: {
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.hashtag = res.data;
|
||||
})
|
||||
.catch(err => {
|
||||
swal('Error', 'Something went wrong, please try again later!', 'error');
|
||||
this.isLoaded = true;
|
||||
this.feedLoaded = true;
|
||||
})
|
||||
.finally(() => {
|
||||
this.fetchFeed();
|
||||
})
|
||||
},
|
||||
|
||||
fetchFeed() {
|
||||
axios.get('/api/v1/timelines/tag/' + this.id, {
|
||||
params: {
|
||||
limit: 80,
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data && res.data.length) {
|
||||
this.feed = res.data;
|
||||
this.maxId = res.data[res.data.length - 1].id;
|
||||
return true;
|
||||
} else {
|
||||
this.feedLoaded = true;
|
||||
this.isLoaded = true;
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.canLoadMore = res;
|
||||
})
|
||||
.finally(() => {
|
||||
this.feedLoaded = true;
|
||||
this.isLoaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
statusUrl(status) {
|
||||
return '/i/web/post/' + status.id;
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
enterIntersect() {
|
||||
if(this.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isIntersecting = true;
|
||||
axios.get('/api/v1/timelines/tag/' + this.id, {
|
||||
params: {
|
||||
max_id: this.maxId,
|
||||
limit: 40,
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data && res.data.length) {
|
||||
this.feed.push(...res.data);
|
||||
this.maxId = res.data[res.data.length - 1].id;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.canLoadMore = res;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isIntersecting = false;
|
||||
})
|
||||
},
|
||||
|
||||
goToPost(status) {
|
||||
this.$router.push({
|
||||
name: 'post',
|
||||
path: `/i/web/post/${status.id}`,
|
||||
params: {
|
||||
id: status.id,
|
||||
cachedStatus: status,
|
||||
cachedProfile: this.profile
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
followHashtag() {
|
||||
this.followingLoading = true;
|
||||
axios.post('/api/v1/tags/' + this.id + '/follow')
|
||||
.then(res => {
|
||||
setTimeout(() => {
|
||||
this.hashtag.following = true;
|
||||
this.followingLoading = false;
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
unfollowHashtag() {
|
||||
this.followingLoading = true;
|
||||
axios.post('/api/v1/tags/' + this.id + '/unfollow')
|
||||
.then(res => {
|
||||
setTimeout(() => {
|
||||
this.hashtag.following = false;
|
||||
this.followingLoading = false;
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hashtag-component {
|
||||
.hashtag-feed {
|
||||
.card,
|
||||
.info-overlay-text,
|
||||
.info-overlay-text-label,
|
||||
img,
|
||||
canvas {
|
||||
border-radius: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hashtag-follow {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.ph-wrapper {
|
||||
padding: 0.25rem;
|
||||
|
||||
.ph-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
.ph-picture {
|
||||
height: auto;
|
||||
padding-bottom: 100%;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
& > * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
113
resources/assets/components/Home.vue
Normal file
113
resources/assets/components/Home.vue
Normal file
|
@ -0,0 +1,113 @@
|
|||
<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"
|
||||
@refresh="shouldRefresh = true" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 col-lg-6 px-0">
|
||||
<story-carousel
|
||||
v-if="storiesEnabled"
|
||||
:profile="profile" />
|
||||
|
||||
<timeline
|
||||
:profile="profile"
|
||||
:scope="scope"
|
||||
:key="scope"
|
||||
v-on:update-profile="updateProfile"
|
||||
:refresh="shouldRefresh"
|
||||
@refreshed="shouldRefresh = false" />
|
||||
</div>
|
||||
|
||||
<div class="d-none d-lg-block col-lg-3">
|
||||
<rightbar class="sticky-top sidebar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<drawer />
|
||||
</div>
|
||||
|
||||
<div v-else class="d-flex justify-content-center align-items-center" style="height:calc(100vh - 58px);">
|
||||
<b-spinner />
|
||||
</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 Timeline from './sections/Timeline.vue';
|
||||
import Notifications from './sections/Notifications.vue';
|
||||
import StoryCarousel from './partials/timeline/StoryCarousel.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
scope: {
|
||||
type: String,
|
||||
default: 'home'
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"sidebar": Sidebar,
|
||||
"timeline": Timeline,
|
||||
"rightbar": Rightbar,
|
||||
"story-carousel": StoryCarousel,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
profile: undefined,
|
||||
recommended: [],
|
||||
trending: [],
|
||||
storiesEnabled: false,
|
||||
shouldRefresh: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route': 'init'
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.profile = window._sharedData.user;
|
||||
this.isLoaded = true;
|
||||
this.storiesEnabled = window.App?.config?.features?.hasOwnProperty('stories') ? window.App.config.features.stories : false;
|
||||
},
|
||||
|
||||
updateProfile(delta) {
|
||||
this.profile = Object.assign(this.profile, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-bottom: -6px;
|
||||
}
|
||||
|
||||
.btn-white {
|
||||
background-color: #fff;
|
||||
border: 1px solid #F3F4F6;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
top: 90px;
|
||||
}
|
||||
</style>
|
111
resources/assets/components/Language.vue
Normal file
111
resources/assets/components/Language.vue
Normal file
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div class="web-wrapper">
|
||||
<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-6">
|
||||
<div class="jumbotron shadow-sm bg-white">
|
||||
<div class="text-center">
|
||||
<h1 class="font-weight-bold mb-0">Language</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="locale-changer form-group">
|
||||
<label>Language</label>
|
||||
<select class="form-control" v-model="locale">
|
||||
<option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">
|
||||
{{ fullName(lang) }}
|
||||
<template v-if="fullName(lang) != localeName(lang)"> · {{ localeName(lang) }}</template>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<drawer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import Sidebar from './partials/sidebar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"sidebar": Sidebar,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
profile: undefined,
|
||||
locale: 'en',
|
||||
// langs: ["af","ar","ca","cs","cy","da","de","el","en","eo","es","eu","fa","fi","fr","gl","he","hu","id","it","ja","ko","ms","nl","no","oc","pl","pt","ro","ru","sr","sv","th","tr","uk","vi","zh","zh-cn","zh-tw"]
|
||||
langs: [
|
||||
"en",
|
||||
"ar",
|
||||
"ca",
|
||||
"de",
|
||||
"el",
|
||||
"es",
|
||||
"eu",
|
||||
"fr",
|
||||
"he",
|
||||
"gd",
|
||||
"gl",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"uk",
|
||||
"vi"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.profile = window._sharedData.user;
|
||||
this.isLoaded = true;
|
||||
this.locale = this.$i18n.locale;
|
||||
},
|
||||
|
||||
watch: {
|
||||
locale: function(val) {
|
||||
this.loadLang(val);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fullName(val) {
|
||||
const factory = new Intl.DisplayNames([val], { type: 'language' });
|
||||
return factory.of(val);
|
||||
},
|
||||
|
||||
localeName(val) {
|
||||
const factory = new Intl.DisplayNames([this.$i18n.locale], { type: 'language' });
|
||||
return factory.of(val);
|
||||
},
|
||||
|
||||
loadLang(lang) {
|
||||
axios.post('/api/pixelfed/web/change-language.json', {
|
||||
v: 0.1,
|
||||
l: lang
|
||||
})
|
||||
.then(res => {
|
||||
this.$i18n.locale = lang;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
29
resources/assets/components/NotFound.vue
Normal file
29
resources/assets/components/NotFound.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="container d-flex justify-content-center">
|
||||
<div class="error-page py-5 my-5" style="max-width: 450px;">
|
||||
<h3 class="font-weight-bold">404 Page Not Found</h3>
|
||||
<p class="lead">The page you are trying to view is not available</p>
|
||||
|
||||
<div class="text-muted">
|
||||
<p class="mb-1">This can happen for a few reasons:</p>
|
||||
<ul>
|
||||
<li>The url is invalid or has a typo</li>
|
||||
<li>The page has been flagged for review by our automated abuse detection systems</li>
|
||||
<li>The content may have been deleted</li>
|
||||
<li>You do not have permission to view this content</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<drawer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"drawer": Drawer
|
||||
}
|
||||
}
|
||||
</script>
|
205
resources/assets/components/Profile.vue
Normal file
205
resources/assets/components/Profile.vue
Normal file
|
@ -0,0 +1,205 @@
|
|||
<template>
|
||||
<div class="profile-timeline-component">
|
||||
<div v-if="isLoaded" class="container-fluid mt-3">
|
||||
<div class="row">
|
||||
<div class="col-md-3 d-md-block px-md-3 px-xl-5">
|
||||
<profile-sidebar
|
||||
:profile="profile"
|
||||
:relationship="relationship"
|
||||
:user="curUser"
|
||||
v-on:back="goBack"
|
||||
v-on:toggletab="toggleTab"
|
||||
v-on:updateRelationship="updateRelationship"
|
||||
@follow="follow"
|
||||
@unfollow="unfollow" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 px-md-5">
|
||||
<component
|
||||
v-bind:is="getTabComponentName()"
|
||||
:key="getTabComponentName() + profile.id"
|
||||
:profile="profile"
|
||||
:relationship="relationship" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<drawer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import ProfileFeed from './partials/profile/ProfileFeed.vue';
|
||||
import ProfileSidebar from './partials/profile/ProfileSidebar.vue';
|
||||
import ProfileFollowers from './partials/profile/ProfileFollowers.vue';
|
||||
import ProfileFollowing from './partials/profile/ProfileFollowing.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String
|
||||
},
|
||||
|
||||
profileId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
username: {
|
||||
type: String
|
||||
},
|
||||
|
||||
cachedProfile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
cachedUser: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"profile-feed": ProfileFeed,
|
||||
"profile-sidebar": ProfileSidebar,
|
||||
"profile-followers": ProfileFollowers,
|
||||
"profile-following": ProfileFollowing
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
curUser: undefined,
|
||||
tab: "index",
|
||||
profile: undefined,
|
||||
relationship: undefined
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route': 'init'
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.tab = 'index';
|
||||
this.isLoaded = false;
|
||||
this.relationship = undefined;
|
||||
this.owner = false;
|
||||
|
||||
if(this.cachedProfile && this.cachedUser) {
|
||||
this.curUser = this.cachedUser;
|
||||
this.profile = this.cachedProfile;
|
||||
// this.fetchPosts();
|
||||
// this.isLoaded = true;
|
||||
this.fetchRelationship();
|
||||
} else {
|
||||
this.curUser = window._sharedData.user;
|
||||
this.fetchProfile();
|
||||
}
|
||||
},
|
||||
|
||||
getTabComponentName() {
|
||||
switch(this.tab) {
|
||||
case 'index':
|
||||
return "profile-feed";
|
||||
break;
|
||||
|
||||
default:
|
||||
return `profile-${this.tab}`;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
fetchProfile() {
|
||||
let id = this.profileId ? this.profileId : this.id;
|
||||
axios.get('/api/pixelfed/v1/accounts/' + id)
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
if(res.data.id == this.curUser.id) {
|
||||
this.owner = true;
|
||||
// this.isLoaded = true;
|
||||
// this.loaded();
|
||||
// this.fetchPosts();
|
||||
this.fetchRelationship();
|
||||
} else {
|
||||
this.owner = false;
|
||||
this.fetchRelationship();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.$router.push('/i/web/404');
|
||||
});
|
||||
},
|
||||
|
||||
fetchRelationship() {
|
||||
if(this.owner) {
|
||||
this.relationship = {};
|
||||
this.isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.profile.id
|
||||
}
|
||||
}).then(res => {
|
||||
this.relationship = res.data[0];
|
||||
this.isLoaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
toggleTab(tab) {
|
||||
this.tab = tab;
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
|
||||
unfollow() {
|
||||
axios.post('/api/v1/accounts/' + this.profile.id + '/unfollow')
|
||||
.then(res => {
|
||||
this.$store.commit('updateRelationship', [res.data])
|
||||
this.relationship = res.data;
|
||||
if(this.profile.locked) {
|
||||
location.reload();
|
||||
}
|
||||
this.profile.followers_count--;
|
||||
}).catch(err => {
|
||||
swal('Oops!', 'An error occured when attempting to unfollow this account.', 'error');
|
||||
this.relationship.following = true;
|
||||
});
|
||||
},
|
||||
|
||||
follow() {
|
||||
axios.post('/api/v1/accounts/' + this.profile.id + '/follow')
|
||||
.then(res => {
|
||||
this.$store.commit('updateRelationship', [res.data])
|
||||
this.relationship = res.data;
|
||||
if(this.profile.locked) {
|
||||
this.relationship.requested = true;
|
||||
}
|
||||
this.profile.followers_count++;
|
||||
}).catch(err => {
|
||||
swal('Oops!', 'An error occured when attempting to follow this account.', 'error');
|
||||
this.relationship.following = false;
|
||||
});
|
||||
},
|
||||
|
||||
updateRelationship(val) {
|
||||
this.relationship = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-timeline-component {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
</style>
|
127
resources/assets/components/ProfileFollowers.vue
Normal file
127
resources/assets/components/ProfileFollowers.vue
Normal file
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div class="profile-timeline-component">
|
||||
<div v-if="isLoaded" class="container-fluid mt-3">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 offset-md-2 px-md-5">
|
||||
<profile-followers
|
||||
:profile="profile"
|
||||
:relationship="relationship"
|
||||
@back="goBack()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<drawer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import ProfileFollowers from './partials/profile/ProfileFollowers.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String
|
||||
},
|
||||
|
||||
profileId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
username: {
|
||||
type: String
|
||||
},
|
||||
|
||||
cachedProfile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
cachedUser: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"profile-followers": ProfileFollowers
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
curUser: undefined,
|
||||
profile: undefined,
|
||||
relationship: undefined
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route': 'init'
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.isLoaded = false;
|
||||
this.relationship = undefined;
|
||||
this.owner = false;
|
||||
|
||||
if(this.cachedProfile && this.cachedUser) {
|
||||
this.curUser = this.cachedUser;
|
||||
this.profile = this.cachedProfile;
|
||||
this.fetchRelationship();
|
||||
} else {
|
||||
this.curUser = window._sharedData.user;
|
||||
this.fetchProfile();
|
||||
}
|
||||
},
|
||||
|
||||
fetchProfile() {
|
||||
let id = this.profileId ? this.profileId : this.id;
|
||||
axios.get('/api/pixelfed/v1/accounts/' + id)
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
if(res.data.id == this.curUser.id) {
|
||||
this.owner = true;
|
||||
// this.isLoaded = true;
|
||||
// this.loaded();
|
||||
// this.fetchPosts();
|
||||
this.fetchRelationship();
|
||||
} else {
|
||||
this.owner = false;
|
||||
this.fetchRelationship();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.$router.push('/i/web/404');
|
||||
});
|
||||
},
|
||||
|
||||
fetchRelationship() {
|
||||
if(this.owner) {
|
||||
this.relationship = {};
|
||||
this.isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.profile.id
|
||||
}
|
||||
}).then(res => {
|
||||
this.relationship = res.data[0];
|
||||
this.isLoaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$router.push('/i/web/profile/' + this.profile.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
124
resources/assets/components/ProfileFollowing.vue
Normal file
124
resources/assets/components/ProfileFollowing.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<div class="profile-timeline-component">
|
||||
<div v-if="isLoaded" class="container-fluid mt-3">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 offset-md-2 px-md-5">
|
||||
<profile-following
|
||||
:profile="profile"
|
||||
:relationship="relationship"
|
||||
@back="goBack()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<drawer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Drawer from './partials/drawer.vue';
|
||||
import ProfileFollowing from './partials/profile/ProfileFollowing.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String
|
||||
},
|
||||
|
||||
profileId: {
|
||||
type: String
|
||||
},
|
||||
|
||||
username: {
|
||||
type: String
|
||||
},
|
||||
|
||||
cachedProfile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
cachedUser: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"drawer": Drawer,
|
||||
"profile-following": ProfileFollowing
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
curUser: undefined,
|
||||
profile: undefined,
|
||||
relationship: undefined
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route': 'init'
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.isLoaded = false;
|
||||
this.relationship = undefined;
|
||||
this.owner = false;
|
||||
|
||||
if(this.cachedProfile && this.cachedUser) {
|
||||
this.curUser = this.cachedUser;
|
||||
this.profile = this.cachedProfile;
|
||||
this.fetchRelationship();
|
||||
} else {
|
||||
this.curUser = window._sharedData.user;
|
||||
this.fetchProfile();
|
||||
}
|
||||
},
|
||||
|
||||
fetchProfile() {
|
||||
let id = this.profileId ? this.profileId : this.id;
|
||||
axios.get('/api/pixelfed/v1/accounts/' + id)
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
if(res.data.id == this.curUser.id) {
|
||||
this.owner = true;
|
||||
this.fetchRelationship();
|
||||
} else {
|
||||
this.owner = false;
|
||||
this.fetchRelationship();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.$router.push('/i/web/404');
|
||||
});
|
||||
},
|
||||
|
||||
fetchRelationship() {
|
||||
if(this.owner) {
|
||||
this.relationship = {};
|
||||
this.isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/api/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.profile.id
|
||||
}
|
||||
}).then(res => {
|
||||
this.relationship = res.data[0];
|
||||
this.isLoaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$router.push('/i/web/profile/' + this.profile.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
1106
resources/assets/components/admin/AdminAutospam.vue
Normal file
1106
resources/assets/components/admin/AdminAutospam.vue
Normal file
File diff suppressed because it is too large
Load diff
1252
resources/assets/components/admin/AdminDirectory.vue
Normal file
1252
resources/assets/components/admin/AdminDirectory.vue
Normal file
File diff suppressed because it is too large
Load diff
182
resources/assets/components/discover/FindFriends.vue
Normal file
182
resources/assets/components/discover/FindFriends.vue
Normal 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>
|
384
resources/assets/components/discover/Hashtags.vue
Normal file
384
resources/assets/components/discover/Hashtags.vue
Normal 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>
|
190
resources/assets/components/discover/Insights.vue
Normal file
190
resources/assets/components/discover/Insights.vue
Normal 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>
|
172
resources/assets/components/discover/Memories.vue
Normal file
172
resources/assets/components/discover/Memories.vue
Normal 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>
|
149
resources/assets/components/discover/ServerFeed.vue
Normal file
149
resources/assets/components/discover/ServerFeed.vue
Normal 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>
|
280
resources/assets/components/discover/Settings.vue
Normal file
280
resources/assets/components/discover/Settings.vue
Normal 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>
|
61
resources/assets/components/partials/BlurhashCanvas.vue
Normal file
61
resources/assets/components/partials/BlurhashCanvas.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<canvas ref="canvas" :width="parseNumber(width)" :height="parseNumber(height)" />
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
hash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 32
|
||||
},
|
||||
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 32
|
||||
},
|
||||
|
||||
punch: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.draw();
|
||||
},
|
||||
|
||||
updated() {
|
||||
// this.draw();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// this.hash = null;
|
||||
// this.$refs.canvas = null;
|
||||
},
|
||||
|
||||
methods: {
|
||||
parseNumber(val) {
|
||||
return typeof val === 'number' ? val : parseInt(val, 10);
|
||||
},
|
||||
|
||||
draw() {
|
||||
const width = this.parseNumber(this.width);
|
||||
const height = this.parseNumber(this.height);
|
||||
const punch = this.parseNumber(this.punch);
|
||||
const pixels = decode(this.hash, width, height, punch);
|
||||
const ctx = this.$refs.canvas.getContext('2d');
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
imageData.data.set(pixels);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
19
resources/assets/components/partials/StatusPlaceholder.vue
Normal file
19
resources/assets/components/partials/StatusPlaceholder.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<div class="ph-item border-0 shadow-sm" style="border-radius:15px;margin-bottom: 1rem;">
|
||||
<div class="ph-col-12">
|
||||
<div class="ph-row align-items-center">
|
||||
<div class="ph-avatar mr-3 d-flex" style="width:50px;height:60px;border-radius: 15px;"></div>
|
||||
<div class="ph-col-6 big"></div>
|
||||
</div>
|
||||
<div class="empty"></div>
|
||||
<div class="empty"></div>
|
||||
<div class="ph-picture"></div>
|
||||
<div class="ph-row">
|
||||
<div class="ph-col-12 empty"></div>
|
||||
<div class="ph-col-12 big"></div>
|
||||
<div class="ph-col-12 empty"></div>
|
||||
<div class="ph-col-12"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
468
resources/assets/components/partials/TimelineStatus.vue
Normal file
468
resources/assets/components/partials/TimelineStatus.vue
Normal file
|
@ -0,0 +1,468 @@
|
|||
<template>
|
||||
<div class="timeline-status-component">
|
||||
<div class="card shadow-sm" style="border-radius: 15px;">
|
||||
<post-header
|
||||
:profile="profile"
|
||||
:status="status"
|
||||
@menu="openMenu"
|
||||
@follow="follow"
|
||||
@unfollow="unfollow" />
|
||||
|
||||
<post-content
|
||||
:profile="profile"
|
||||
:status="status" />
|
||||
|
||||
<post-reactions
|
||||
v-if="reactionBar"
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
:admin="admin"
|
||||
v-on:like="like"
|
||||
v-on:unlike="unlike"
|
||||
v-on:share="shareStatus"
|
||||
v-on:unshare="unshareStatus"
|
||||
v-on:likes-modal="showLikes"
|
||||
v-on:shares-modal="showShares"
|
||||
v-on:toggle-comments="showComments"
|
||||
v-on:bookmark="handleBookmark"
|
||||
v-on:mod-tools="openModTools" />
|
||||
|
||||
<div v-if="showCommentDrawer" class="card-footer rounded-bottom border-0" style="background: rgba(0,0,0,0.02);z-index: 3;">
|
||||
<comment-drawer
|
||||
:status="status"
|
||||
:profile="profile"
|
||||
v-on:handle-report="handleReport"
|
||||
v-on:counter-change="counterChange"
|
||||
v-on:show-likes="showCommentLikes"
|
||||
v-on:follow="follow"
|
||||
v-on:unfollow="unfollow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import CommentDrawer from './post/CommentDrawer.vue';
|
||||
import PostHeader from './post/PostHeader.vue';
|
||||
import PostContent from './post/PostContent.vue';
|
||||
import PostReactions from './post/PostReactions.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
reactionBar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
useDropdownMenu: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"comment-drawer": CommentDrawer,
|
||||
"post-content": PostContent,
|
||||
"post-header": PostHeader,
|
||||
"post-reactions": PostReactions
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
key: 1,
|
||||
menuLoading: true,
|
||||
sensitive: false,
|
||||
showCommentDrawer: false,
|
||||
isReblogging: false,
|
||||
isBookmarking: false,
|
||||
owner: false,
|
||||
admin: false,
|
||||
license: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.license = this.status.media_attachments && this.status.media_attachments.length ?
|
||||
this.status
|
||||
.media_attachments
|
||||
.filter(m => m.hasOwnProperty('license') && m.license && m.license.hasOwnProperty('id'))
|
||||
.map(m => m.license)[0] : false;
|
||||
this.admin = window._sharedData.user.is_admin;
|
||||
this.owner = this.status.account.id == window._sharedData.user.id;
|
||||
if(this.status.reply_count && this.autoloadComments && this.status.comments_disabled === false) {
|
||||
setTimeout(() => {
|
||||
this.showCommentDrawer = true;
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hideCounts: {
|
||||
get() {
|
||||
return this.$store.state.hideCounts == true;
|
||||
}
|
||||
},
|
||||
|
||||
fixedHeight: {
|
||||
get() {
|
||||
return this.$store.state.fixedHeight == true;
|
||||
}
|
||||
},
|
||||
|
||||
autoloadComments: {
|
||||
get() {
|
||||
return this.$store.state.autoloadComments == true;
|
||||
}
|
||||
},
|
||||
|
||||
newReactions: {
|
||||
get() {
|
||||
return this.$store.state.newReactions;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
status: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler: function(o, n) {
|
||||
this.isBookmarking = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openMenu() {
|
||||
this.$emit('menu');
|
||||
},
|
||||
|
||||
like() {
|
||||
this.$emit('like');
|
||||
},
|
||||
|
||||
unlike() {
|
||||
this.$emit('unlike');
|
||||
},
|
||||
|
||||
showLikes() {
|
||||
this.$emit('likes-modal');
|
||||
},
|
||||
|
||||
showShares() {
|
||||
this.$emit('shares-modal');
|
||||
},
|
||||
|
||||
showComments() {
|
||||
this.showCommentDrawer = !this.showCommentDrawer;
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
event.currentTarget.blur();
|
||||
App.util.clipboard(this.status.url);
|
||||
},
|
||||
|
||||
shareToOther() {
|
||||
if (navigator.canShare) {
|
||||
navigator.share({
|
||||
url: this.status.url
|
||||
})
|
||||
.then(() => console.log('Share was successful.'))
|
||||
.catch((error) => console.log('Sharing failed', error));
|
||||
} else {
|
||||
swal('Not supported', 'Your current device does not support native sharing.', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
counterChange(type) {
|
||||
this.$emit('counter-change', type);
|
||||
},
|
||||
|
||||
showCommentLikes(post) {
|
||||
this.$emit('comment-likes-modal', post);
|
||||
},
|
||||
|
||||
shareStatus() {
|
||||
this.$emit('share');
|
||||
},
|
||||
|
||||
unshareStatus() {
|
||||
this.$emit('unshare');
|
||||
},
|
||||
|
||||
handleReport(post) {
|
||||
this.$emit('handle-report', post);
|
||||
},
|
||||
|
||||
follow() {
|
||||
this.$emit('follow');
|
||||
},
|
||||
|
||||
unfollow() {
|
||||
this.$emit('unfollow');
|
||||
},
|
||||
|
||||
handleReblog() {
|
||||
this.isReblogging = true;
|
||||
if(this.status.reblogged) {
|
||||
this.$emit('unshare');
|
||||
} else {
|
||||
this.$emit('share');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.isReblogging = false;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
handleBookmark() {
|
||||
event.currentTarget.blur();
|
||||
this.isBookmarking = true;
|
||||
this.$emit('bookmark');
|
||||
|
||||
setTimeout(() => {
|
||||
this.isBookmarking = false;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
getStatusAvatar() {
|
||||
if(window._sharedData.user.id == this.status.account.id) {
|
||||
return window._sharedData.user.avatar;
|
||||
}
|
||||
|
||||
return this.status.account.avatar;
|
||||
},
|
||||
|
||||
openModTools() {
|
||||
this.$emit('mod-tools');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.timeline-status-component {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.btn:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.VueCarousel-wrapper {
|
||||
.VueCarousel-slide {
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
z-index: 3;
|
||||
&.py-0 {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.reaction-liked-by {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
.timestamp,
|
||||
.visibility,
|
||||
.location {
|
||||
color: #94a3b8;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blurhash-wrapper {
|
||||
img {
|
||||
border-radius:0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content-label-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-color: #000;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
|
||||
img, canvas {
|
||||
max-height: 400px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.content-label {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border-radius: 0;
|
||||
background: rgba(0, 0, 0, 0.2)
|
||||
}
|
||||
|
||||
.rounded-bottom {
|
||||
border-bottom-left-radius: 15px !important;
|
||||
border-bottom-right-radius: 15px !important;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
.media {
|
||||
position: relative;
|
||||
|
||||
.comment-border-link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 11px;
|
||||
width: 10px;
|
||||
height: calc(100% - 100px);
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
background-color: #E5E7EB;
|
||||
background-clip: padding-box;
|
||||
|
||||
&:hover {
|
||||
background-color: #BFDBFE;
|
||||
}
|
||||
}
|
||||
|
||||
.child-reply-form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment-border-arrow {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: -33px;
|
||||
width: 10px;
|
||||
height: 29px;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
background-color: #E5E7EB;
|
||||
background-clip: padding-box;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 2px;
|
||||
width: 15px;
|
||||
height: 2px;
|
||||
background-color: #E5E7EB;
|
||||
}
|
||||
}
|
||||
|
||||
&-status {
|
||||
margin-bottom: 1.3rem;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
margin-right: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&-body {
|
||||
&-comment {
|
||||
width: fit-content;
|
||||
padding: 0.4rem 0.7rem;
|
||||
background-color: var(--comment-bg);
|
||||
border-radius: 0.9rem;
|
||||
|
||||
&-username {
|
||||
margin-bottom: 0.25rem !important;
|
||||
font-size: 14px;
|
||||
font-weight: 700 !important;
|
||||
color: var(--body-color);
|
||||
|
||||
a {
|
||||
color: var(--body-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
margin-bottom: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-reactions {
|
||||
margin-top: 0.4rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
color: #B8C2CC !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fixedHeight {
|
||||
max-height: 400px;
|
||||
|
||||
.VueCarousel-wrapper {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.VueCarousel-slide {
|
||||
img {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.blurhash-wrapper {
|
||||
img {
|
||||
height: 400px;
|
||||
max-height: 400px;
|
||||
background-color: transparent;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
.content-label-wrapper {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.content-label {
|
||||
height: 400px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
295
resources/assets/components/partials/direct/Message.vue
Normal file
295
resources/assets/components/partials/direct/Message.vue
Normal file
|
@ -0,0 +1,295 @@
|
|||
<template>
|
||||
<div class="dm-chat-message chat-msg">
|
||||
<div
|
||||
class="media d-inline-flex mb-0"
|
||||
:class="{ isAuthor: convo.isAuthor }"
|
||||
>
|
||||
<img v-if="!convo.isAuthor && !hideAvatars" class="mr-3 shadow msg-avatar" :src="thread.avatar" alt="avatar" width="50" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
|
||||
<div class="media-body">
|
||||
<p v-if="convo.type == 'photo'" class="pill-to p-0 shadow">
|
||||
<img
|
||||
:src="convo.media"
|
||||
class="media-embed"
|
||||
style="cursor: pointer;"
|
||||
onerror="this.onerror=null;this.src='/storage/no-preview.png';"
|
||||
@click.prevent="expandMedia">
|
||||
</p>
|
||||
<div v-else-if="convo.type == 'link'" class="d-inline-flex mb-0 cursor-pointer">
|
||||
<div class="card shadow border" style="width:240px;border-radius: 18px;">
|
||||
<div class="card-body p-0" :title="convo.text">
|
||||
<div class="media align-items-center">
|
||||
<div v-if="convo.meta.local" class="bg-primary mr-3 p-3" style="border-radius: 18px;">
|
||||
<i class="fas fa-link text-white fa-2x"></i>
|
||||
</div>
|
||||
<div v-else class="bg-light mr-3 p-3" style="border-radius: 18px;">
|
||||
<i class="fas fa-link text-lighter fa-2x"></i>
|
||||
</div>
|
||||
<div class="media-body text-muted small text-truncate pr-2 font-weight-bold">
|
||||
{{convo.meta.local ? convo.text.substr(8) : convo.meta.domain}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="convo.type == 'video'" class="pill-to p-0 shadow mb-0" style="line-height: 0;">
|
||||
<video :src="convo.media" class="media-embed" style="border-radius:20px;" controls>
|
||||
</video>
|
||||
<!-- <span class="d-block bg-primary d-flex align-items-center justify-content-center" style="width:200px;height: 110px;border-radius: 20px;">
|
||||
<div class="text-center">
|
||||
<p class="mb-1">
|
||||
<i class="fas fa-play fa-2x text-white"></i>
|
||||
</p>
|
||||
<p class="mb-0 small font-weight-bold text-white">
|
||||
Play
|
||||
</p>
|
||||
</div>
|
||||
</span> -->
|
||||
</p>
|
||||
<p v-else-if="convo.type == 'emoji'" class="p-0 emoji-msg">
|
||||
{{convo.text}}
|
||||
</p>
|
||||
<p v-else-if="convo.type == 'story:react'" class="pill-to p-0 shadow" style="width: 140px;margin-bottom: 10px;position:relative;">
|
||||
<img :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
|
||||
<span class="badge badge-light rounded-pill border" style="font-size: 20px;position: absolute;bottom:-10px;left:-10px;">
|
||||
{{convo.meta.reaction}}
|
||||
</span>
|
||||
</p>
|
||||
<span v-else-if="convo.type == 'story:comment'" class="p-0" style="display: flex;justify-content: flex-start;margin-bottom: 10px;position:relative;">
|
||||
<span class="">
|
||||
<img class="d-block pill-to p-0 mr-0 pr-0 mb-n1" :src="convo.meta.story_media_url" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
|
||||
<span class="pill-to shadow text-break" style="width:fit-content;">{{convo.meta.caption}}</span>
|
||||
</span>
|
||||
</span>
|
||||
<p v-else :class="[largerText ? 'pill-to shadow larger-text text-break':'pill-to shadow text-break']">
|
||||
{{convo.text}}
|
||||
</p>
|
||||
<p v-if="convo.type == 'story:react'" class="small text-muted mb-0 ml-0">
|
||||
<span class="font-weight-bold">{{ convo.meta.story_actor_username }}</span> reacted your story
|
||||
</p>
|
||||
<p v-if="convo.type == 'story:comment'" class="small text-muted mb-0 ml-0">
|
||||
<span class="font-weight-bold">{{ convo.meta.story_actor_username }}</span> replied to your story
|
||||
</p>
|
||||
|
||||
<p
|
||||
class="msg-timestamp small text-muted font-weight-bold d-flex align-items-center justify-content-start"
|
||||
data-timestamp="timestamp">
|
||||
<span
|
||||
v-if="convo.hidden"
|
||||
class="small pr-2"
|
||||
title="Filtered Message"
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
|
||||
<span v-if="!hideTimestamps">
|
||||
{{convo.timeAgo}}
|
||||
</span>
|
||||
|
||||
<button
|
||||
v-if="convo.isAuthor"
|
||||
class="btn btn-link btn-sm text-lighter pl-2 font-weight-bold"
|
||||
@click="confirmDelete">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<img v-if="convo.isAuthor && !hideAvatars" class="ml-3 shadow msg-avatar" :src="profile.avatar" alt="avatar" width="50" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import BigPicture from 'bigpicture';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
thread: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
convo: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
hideAvatars: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
hideTimestamps: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
largerText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
profile: window._sharedData.user
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
truncate(t) {
|
||||
return _.truncate(t);
|
||||
},
|
||||
|
||||
viewOriginal() {
|
||||
let url = this.ctxContext.media;
|
||||
window.location.href = url;
|
||||
return;
|
||||
},
|
||||
|
||||
isEmoji(text) {
|
||||
const onlyEmojis = text.replace(new RegExp('[\u0000-\u1eeff]', 'g'), '')
|
||||
const visibleChars = text.replace(new RegExp('[\n\r\s]+|( )+', 'g'), '')
|
||||
return onlyEmojis.length === visibleChars.length
|
||||
},
|
||||
|
||||
copyText() {
|
||||
window.App.util.clipboard(this.ctxContext.text);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
clickLink() {
|
||||
let url = this.ctxContext.text;
|
||||
if(this.ctxContext.meta.local != true) {
|
||||
url = '/i/redirect?url=' + encodeURI(this.ctxContext.text);
|
||||
}
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
return window.App.util.format.count(val);
|
||||
},
|
||||
|
||||
confirmDelete() {
|
||||
this.$emit('confirm-delete');
|
||||
},
|
||||
|
||||
expandMedia(e) {
|
||||
BigPicture({
|
||||
el: e.target
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-msg {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.reply-btn {
|
||||
position: absolute;
|
||||
bottom: 54px;
|
||||
right: 20px;
|
||||
width: 90px;
|
||||
text-align: center;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.media-body .bg-primary {
|
||||
background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
|
||||
}
|
||||
|
||||
.pill-to {
|
||||
background: var(--bg-light);
|
||||
font-weight: 500;
|
||||
border-radius: 20px !important;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-right: 3rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.pill-from {
|
||||
color: white !important;
|
||||
text-align: right !important;
|
||||
background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
|
||||
font-weight: 500;
|
||||
border-radius: 20px !important;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-left: 3rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.chat-smsg:hover {
|
||||
background: var(--light-hover-bg);
|
||||
}
|
||||
.no-focus {
|
||||
border: none !important;
|
||||
}
|
||||
.no-focus:focus {
|
||||
outline: none !important;
|
||||
outline-width: 0 !important;
|
||||
box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
}
|
||||
.emoji-msg {
|
||||
font-size: 4rem !important;
|
||||
line-height: 30px !important;
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
.larger-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.dm-chat-message {
|
||||
|
||||
.isAuthor {
|
||||
float: right;
|
||||
margin-right: 0.5rem !important;
|
||||
|
||||
.pill-to {
|
||||
color: white !important;
|
||||
text-align: right !important;
|
||||
background: linear-gradient(135deg, #2EA2F4 0%, #0B93F6 100%) !important;
|
||||
font-weight: 500;
|
||||
border-radius: 20px !important;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-left: 3rem;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.msg-timestamp {
|
||||
display: block !important;
|
||||
text-align: right;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.media-embed {
|
||||
width: 140px;
|
||||
border-radius: 20px;
|
||||
|
||||
@media (min-width: 450px) {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="discover-daily-trending">
|
||||
<div class="card bg-stellar">
|
||||
<div class="card-body m-5">
|
||||
<div class="row d-flex align-items-center">
|
||||
<div class="col-12 col-md-5">
|
||||
<p class="font-default text-light mb-0">Popular and trending posts</p>
|
||||
<h1 class="display-4 font-default text-white" style="font-weight: 700;">Daily Trending</h1>
|
||||
<button class="btn btn-outline-light rounded-pill" @click="viewMore()">View more trending posts</button>
|
||||
</div>
|
||||
<div class="col-12 col-md-7">
|
||||
<div v-if="isLoaded" class="row">
|
||||
<div v-for="(post, index) in trending" class="col-4">
|
||||
<a :href="post.url" @click.prevent="gotoPost(post.id)">
|
||||
<img :src="post.media_attachments[0].url" class="shadow m-1" width="170" height="170" style="object-fit: cover;border-radius:8px">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<b-spinner type="grow" variant="light" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
initialFetch: false,
|
||||
trending: []
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if(!this.initialFetch) {
|
||||
this.fetchTrending();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchTrending() {
|
||||
axios.get('/api/pixelfed/v2/discover/posts/trending', {
|
||||
params: {
|
||||
range: 'daily'
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.trending = res.data.filter(p => p.pf_type === 'photo').slice(0, 9);
|
||||
this.isLoaded = true;
|
||||
this.initialFetch = true;
|
||||
});
|
||||
},
|
||||
|
||||
gotoPost(id) {
|
||||
this.$router.push('/i/web/post/' + id);
|
||||
},
|
||||
|
||||
viewMore() {
|
||||
this.$emit('btn-click', 'trending');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.discover-daily-trending {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<div class="discover-spotlight">
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body my-5 p-5 w-100 h-100 d-flex justify-content-center align-items-center">
|
||||
<transition enter-active-class="animate__animated animate__fadeInDownBig" leave-active-class="animate__animated animate__fadeOutDownBig" mode="out-in">
|
||||
<div v-if="isLoaded" class="row">
|
||||
<div class="col-5">
|
||||
<h1 class="display-3 font-default mb-3" style="line-height: 1;font-weight: 600;">
|
||||
Spotlight
|
||||
</h1>
|
||||
<h1 class="display-5 font-default" style="line-height: 1;">
|
||||
<span class="text-muted" style="line-height: 0.8;font-weight: 200;letter-spacing: -1.2px;">
|
||||
Community curated
|
||||
collection of creators
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p class="lead font-default mt-4">This weeks collection is curated by <span class="primary">@dansup</span></p>
|
||||
</div>
|
||||
<div class="col-7 d-flex justify-content-between">
|
||||
<div class="text-center mr-4">
|
||||
<h5 class="font-default mb-2">@dansup</h5>
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/skvft7.jpg?v=33" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';" class="avatar shadow cursor-pointer" width="160" height="160">
|
||||
<button class="btn btn-outline-light btn-sm rounded-pill py-1 mt-2 font-default">View Profile</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center mr-4">
|
||||
<h5 class="font-default mb-2">@dansup</h5>
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/skvft7.jpg?v=33" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';" class="avatar shadow cursor-pointer" width="160" height="160">
|
||||
<button class="btn btn-outline-light btn-sm rounded-pill py-1 mt-2 font-default">View Profile</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<h5 class="font-default mb-2">@dansup</h5>
|
||||
<img src="https://pixelfed.test/storage/avatars/321493203255693312/skvft7.jpg?v=33" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';" class="avatar shadow cursor-pointer" width="160" height="160">
|
||||
<button class="btn btn-outline-light btn-sm rounded-pill py-1 mt-2 font-default">View Profile</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-if="!isLoaded" class="">
|
||||
<b-spinner type="grow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.isLoaded = true;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.discover-spotlight {
|
||||
overflow: hidden;
|
||||
|
||||
.card-body {
|
||||
min-height: 322px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
</style>
|
162
resources/assets/components/partials/discover/grid-card.vue
Normal file
162
resources/assets/components/partials/discover/grid-card.vue
Normal file
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<div class="discover-grid-card">
|
||||
<div
|
||||
class="discover-grid-card-body"
|
||||
:class="{ 'dark': dark, 'small': small }"
|
||||
>
|
||||
|
||||
<div class="section-copy">
|
||||
<p class="subtitle">{{ subtitle }}</p>
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
<button v-if="buttonText" class="btn btn-outline-dark rounded-pill py-1" @click.prevent="handleLink()">{{ buttonText }}</button>
|
||||
</div>
|
||||
|
||||
<div class="section-icon">
|
||||
<i :class="iconClass"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
dark: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
subtitle: {
|
||||
type: String
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
|
||||
buttonText: {
|
||||
type: String
|
||||
},
|
||||
|
||||
buttonLink: {
|
||||
type: String
|
||||
},
|
||||
|
||||
buttonEvent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
iconClass: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleLink() {
|
||||
if(this.buttonEvent == true) {
|
||||
this.$emit('btn-click');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.buttonLink || this.buttonLink == undefined) {
|
||||
swal('Oops', 'This is embarassing, we cannot redirect you to the proper page at the moment', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push(this.buttonLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.discover-grid-card {
|
||||
width: 100%;
|
||||
|
||||
&-body {
|
||||
width: 100%;
|
||||
padding: 3rem 3rem 0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: #212529;
|
||||
background: #f8f9fa;
|
||||
overflow: hidden;
|
||||
|
||||
.section-copy {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
margin-bottom: 0;
|
||||
color: #6c757d;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
letter-spacing: -0.7px;
|
||||
}
|
||||
|
||||
.title,
|
||||
.btn {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
letter-spacing: -0.7px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 80%;
|
||||
height: 300px;
|
||||
border-radius: 21px 21px 0 0;
|
||||
background: #232526;
|
||||
background: linear-gradient(to right, #414345, #232526);
|
||||
box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important;
|
||||
|
||||
i {
|
||||
color: #fff;
|
||||
font-size: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
.section-icon {
|
||||
height: 120px;
|
||||
|
||||
i {
|
||||
font-size: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
color: #fff;
|
||||
background: #232526;
|
||||
background: linear-gradient(to right, #414345, #232526);
|
||||
|
||||
.section-icon {
|
||||
color: #fff;
|
||||
background: #f8f9fa;
|
||||
|
||||
i {
|
||||
color: #232526;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-dark {
|
||||
color: #f8f9fa;
|
||||
border-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div class="rounded-3 overflow-hidden discover-news-slider">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-xl-4 col-md-5 offset-lg-1">
|
||||
<div class="pt-5 pb-3 pb-md-5 px-4 px-lg-0">
|
||||
<p class="lead mb-3" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;letter-spacing: -0.7px;font-weight:300;font-size:20px;">Introducing</p>
|
||||
<h2 class="h1 pb-0 mb-3" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;letter-spacing: -1px;font-weight:700;">Emoji <span class="primary">Reactions</span></h2>
|
||||
<p class="lead" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;letter-spacing: -0.7px;font-weight:400;font-size:17px;line-height:15px;">
|
||||
A new way to interact with content,<br /> now available!
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary primary btn-sm">Learn more <i class="far fa-chevron-right fa-sm ml-2"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-7 offset-xl-1">
|
||||
<div class="position-relative d-flex flex-column align-items-center justify-content-center h-100">
|
||||
<svg class="d-none d-md-block position-absolute top-50 start-0 translate-middle-y" width="868" height="868" style="min-width: 868px;" viewBox="0 0 868 868" fill="none" xmlns="http://www.w3.org/2000/svg"><circle opacity="0.15" cx="434" cy="434" r="434" fill="#7dd3fc"></circle></svg>
|
||||
<div class="d-flex">
|
||||
<img src="/img/remoji/hushed_face.gif" class="position-relative zindex-3 mb-2 my-lg-4" width="100" alt="Illustration">
|
||||
<img src="/img/remoji/thumbs_up.gif" class="position-relative zindex-3 mb-2 my-lg-4" width="100" alt="Illustration">
|
||||
<img src="/img/remoji/sparkling_heart.gif" class="position-relative zindex-3 mb-2 my-lg-4" width="100" alt="Illustration">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute;left: 50%;transform: translateX(-50%);bottom:10px;">
|
||||
<div class="d-flex">
|
||||
<button class="btn btn-link p-0">
|
||||
<i class="far fa-dot-circle"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link p-0 mx-2">
|
||||
<i class="far fa-circle"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-link p-0">
|
||||
<i class="far fa-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.discover-news-slider {
|
||||
position: relative;
|
||||
background-color: #e0f2fe;
|
||||
}
|
||||
</style>
|
106
resources/assets/components/partials/drawer.vue
Normal file
106
resources/assets/components/partials/drawer.vue
Normal file
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="app-drawer-component">
|
||||
<div class="mobile-footer-spacer d-block d-sm-none mt-5"></div>
|
||||
|
||||
<div class="mobile-footer d-block d-sm-none fixed-bottom">
|
||||
<div class="card card-body rounded-0 px-0 pt-2 pb-3 box-shadow" style="border-top:1px solid var(--border-color)">
|
||||
<ul class="nav nav-pills nav-fill d-flex align-items-middle">
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link text-dark" to="/i/web">
|
||||
<p>
|
||||
<i class="far fa-home fa-lg"></i>
|
||||
</p>
|
||||
<p class="nav-link-label">
|
||||
<span>Home</span>
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link text-dark" to="/i/web/timeline/local">
|
||||
<p>
|
||||
<i class="far fa-stream fa-lg"></i>
|
||||
</p>
|
||||
<p class="nav-link-label">
|
||||
<span>Local</span>
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link text-dark" to="/i/web/compose">
|
||||
<p>
|
||||
<i class="far fa-plus-circle fa-lg"></i>
|
||||
</p>
|
||||
<p class="nav-link-label">
|
||||
<span>New</span>
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link text-dark" to="/i/web/notifications">
|
||||
<p>
|
||||
<i class="far fa-bell fa-lg"></i>
|
||||
</p>
|
||||
<p class="nav-link-label">
|
||||
<span>Alerts</span>
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link text-dark" :to="'/i/web/profile/' + user.id">
|
||||
<p>
|
||||
<i class="far fa-user fa-lg"></i>
|
||||
</p>
|
||||
<p class="nav-link-label">
|
||||
<span>Profile</span>
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: window._sharedData.user
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app-drawer-component {
|
||||
.nav-link {
|
||||
padding: 0.5rem 0.1rem;
|
||||
|
||||
&.active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
background-color: transparent;
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
margin-top: 0;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
187
resources/assets/components/partials/modal/ReportPost.vue
Normal file
187
resources/assets/components/partials/modal/ReportPost.vue
Normal file
|
@ -0,0 +1,187 @@
|
|||
<template>
|
||||
<b-modal
|
||||
ref="modal"
|
||||
centered
|
||||
hide-header
|
||||
hide-footer
|
||||
scrollable
|
||||
body-class="p-md-5 user-select-none"
|
||||
>
|
||||
<div v-if="tabIndex === 0">
|
||||
<h2 class="text-center font-weight-bold">{{ $t('report.report') }}</h2>
|
||||
|
||||
<p class="text-center">{{ $t('menu.confirmReportText') }}</p>
|
||||
|
||||
<div v-if="status && status.hasOwnProperty('account')" class="card shadow-none rounded-lg border my-4">
|
||||
<div class="card-body">
|
||||
<div class="media">
|
||||
<img
|
||||
:src="status.account.avatar"
|
||||
class="mr-3 rounded"
|
||||
width="40"
|
||||
height="40"
|
||||
style="border-radius: 8px;"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
|
||||
|
||||
<div class="media-body">
|
||||
<p class="h5 primary font-weight-bold mb-1">
|
||||
@{{ status.account.acct }}
|
||||
</p>
|
||||
|
||||
<div v-if="status.hasOwnProperty('pf_type') && status.pf_type == 'text'">
|
||||
<p v-if="status.content_text.length <= 140" class="mb-0">
|
||||
{{ status.content_text}}
|
||||
</p>
|
||||
|
||||
<p v-else class="mb-0">
|
||||
<span v-if="showFull">
|
||||
{{ status.content_text}}
|
||||
<a class="font-weight-bold primary ml-1" href="#" @click.prevent="showFull = false">Show less</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ status.content_text.substr(0, 140) + ' ...' }}
|
||||
<a class="font-weight-bold primary ml-1" href="#" @click.prevent="showFull = true">Show full post</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.hasOwnProperty('pf_type') && status.pf_type == 'photo'">
|
||||
<div class="w-100 rounded-lg d-flex justify-content-center mt-3" style="background: #000;max-height: 150px">
|
||||
<img :src="status.media_attachments[0].url" class="rounded-lg shadow" style="width: 100%;max-height: 150px;object-fit:contain;">
|
||||
</div>
|
||||
|
||||
<p v-if="status.content_text" class="mt-3 mb-0">
|
||||
<span v-if="showFull">
|
||||
{{ status.content_text}}
|
||||
<a class="font-weight-bold primary ml-1" href="#" @click.prevent="showFull = false">Show less</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ status.content_text.substr(0, 80) + ' ...' }}
|
||||
<a class="font-weight-bold primary ml-1" href="#" @click.prevent="showFull = true">Show full post</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-right mb-0 mb-md-n3">
|
||||
<button class="btn btn-light px-3 py-2 mr-3 font-weight-bold" @click="close">{{ $t('common.cancel')}}</button>
|
||||
<button class="btn btn-primary px-3 py-2 font-weight-bold" style="background-color: #3B82F6;" @click="tabIndex = 1">{{ $t('common.proceed') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tabIndex === 1">
|
||||
<h2 class="text-center font-weight-bold">{{ $t('report.report') }}</h2>
|
||||
|
||||
<p class="text-center">
|
||||
{{ $t('report.selectReason') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<!-- <button class="btn btn-light btn-block rounded-pill font-weight-bold" @click="handleReason('notinterested')">I'm not interested in it</button> -->
|
||||
<button class="btn btn-light btn-block rounded-pill font-weight-bold text-danger" @click="handleReason('spam')">{{ $t('menu.spam')}}</button>
|
||||
<button v-if="status.sensitive == false" class="btn btn-light btn-block rounded-pill font-weight-bold text-danger" @click="handleReason('sensitive')">Adult or {{ $t('menu.sensitive')}}</button>
|
||||
<button class="btn btn-light btn-block rounded-pill font-weight-bold text-danger" @click="handleReason('abusive')">{{ $t('menu.abusive')}}</button>
|
||||
<button class="btn btn-light btn-block rounded-pill font-weight-bold" @click="handleReason('underage')">{{ $t('menu.underageAccount')}}</button>
|
||||
<button class="btn btn-light btn-block rounded-pill font-weight-bold" @click="handleReason('copyright')">{{ $t('menu.copyrightInfringement')}}</button>
|
||||
<button class="btn btn-light btn-block rounded-pill font-weight-bold" @click="handleReason('impersonation')">{{ $t('menu.impersonation')}}</button>
|
||||
<!-- <button class="btn btn-light btn-block rounded-pill font-weight-bold" @click="handleReason('scam')">{{ $t('menu.scamOrFraud')}}</button> -->
|
||||
<button class="btn btn-light btn-block rounded-pill mt-md-5" @click="tabIndex = 0">Go back</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tabIndex === 2">
|
||||
<div class="my-4 text-center">
|
||||
<b-spinner />
|
||||
|
||||
<p class="small mb-0">{{ $t('report.sendingReport') }} ...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tabIndex === 3">
|
||||
<div class="my-4">
|
||||
<h2 class="text-center font-weight-bold mb-3">{{ $t('report.reported') }}</h2>
|
||||
<p class="text-center py-2">
|
||||
<span class="fa-stack fa-4x text-success">
|
||||
<i class="far fa-check fa-stack-1x"></i>
|
||||
<i class="fal fa-circle fa-stack-2x"></i>
|
||||
</span>
|
||||
</p>
|
||||
<p class="lead text-center">{{ $t('report.thanksMsg') }}</p>
|
||||
<hr>
|
||||
<p class="text-center">{{ $t('report.contactAdminMsg') }}, <a href="/site/contact" class="font-weight-bold primary">{{ $t('common.clickHere') }}</a>.</p>
|
||||
</div>
|
||||
|
||||
<p class="text-center mb-0 mb-md-n3">
|
||||
<button class="btn btn-light btn-block rounded-pill px-3 py-2 mr-3 font-weight-bold" @click="close">{{ $t('common.close') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tabIndex === 5">
|
||||
<div class="my-4">
|
||||
<h2 class="text-center font-weight-bold mb-3">{{ $t('common.oops') }}</h2>
|
||||
<p class="text-center py-2">
|
||||
<span class="fa-stack fa-3x text-danger">
|
||||
<i class="far fa-times fa-stack-1x"></i>
|
||||
<i class="fal fa-circle fa-stack-2x"></i>
|
||||
</span>
|
||||
</p>
|
||||
<p class="lead text-center">{{ $t('common.errorMsg') }}</p>
|
||||
<hr>
|
||||
<p class="text-center">{{ $t('report.contactAdminMsg') }}, <a href="/site/contact" class="font-weight-bold primary">{{ $t('common.clickHere') }}</a>.</p>
|
||||
</div>
|
||||
|
||||
<p class="text-center mb-0 mb-md-n3">
|
||||
<button class="btn btn-light btn-block rounded-pill px-3 py-2 mr-3 font-weight-bold" @click="close">{{ $t('common.close') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
statusId: undefined,
|
||||
tabIndex: 0,
|
||||
showFull: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
setTimeout(() => {
|
||||
this.tabIndex = 0;
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
handleReason(reason) {
|
||||
this.tabIndex = 2;
|
||||
|
||||
axios.post('/i/report', {
|
||||
id: this.status.id,
|
||||
report: reason,
|
||||
type: 'post'
|
||||
}).then(res => {
|
||||
this.tabIndex = 3;
|
||||
}).catch(err => {
|
||||
this.tabIndex = 5;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
148
resources/assets/components/partials/modal/UpdateAvatar.vue
Normal file
148
resources/assets/components/partials/modal/UpdateAvatar.vue
Normal file
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<b-modal
|
||||
ref="avatarUpdateModal"
|
||||
centered
|
||||
hide-footer
|
||||
header-class="py-2"
|
||||
body-class="p-0"
|
||||
title-class="w-100 text-center pl-4 font-weight-bold"
|
||||
title-tag="p"
|
||||
title="Upload Avatar"
|
||||
>
|
||||
<input type="file" class="d-none" ref="avatarUpdateRef" @change="handleAvatarUpdate()" accept="image/jpg,image/png">
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
v-if="avatarUpdateIndex === 0"
|
||||
class="py-5 user-select-none cursor-pointer"
|
||||
v-on:drop="handleDrop"
|
||||
v-on:dragover="handleDrop"
|
||||
@click="avatarUpdateStep(0)">
|
||||
<p class="text-center primary">
|
||||
<i class="fal fa-cloud-upload fa-3x"></i>
|
||||
</p>
|
||||
<p class="text-center lead">Drag photo here or click here</p>
|
||||
<p class="text-center small text-muted mb-0">Must be a <strong>png</strong> or <strong>jpg</strong> image up to 2MB</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="avatarUpdateIndex === 1" class="w-100 p-5">
|
||||
|
||||
<div class="d-md-flex justify-content-between align-items-center">
|
||||
<div class="text-center mb-4">
|
||||
<p class="small font-weight-bold" style="opacity:0.7;">Current</p>
|
||||
<img :src="user.avatar" class="shadow" style="width: 150px;height: 150px;object-fit: cover;border-radius: 18px;opacity: 0.7;">
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<p class="font-weight-bold">New</p>
|
||||
<img :src="avatarUpdatePreview" class="shadow" style="width: 220px;height: 220px;object-fit: cover;border-radius: 18px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<button class="btn btn-light font-weight-bold btn-block mr-3" @click="avatarUpdateClear()">Clear</button>
|
||||
<button class="btn btn-primary primary font-weight-bold btn-block mt-0" @click="confirmUpload()">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: ['user'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
avatarUpdateIndex: 0,
|
||||
avatarUpdateFile: undefined,
|
||||
avatarUpdatePreview: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.avatarUpdateModal.show();
|
||||
},
|
||||
|
||||
avatarUpdateClose() {
|
||||
this.$refs.avatarUpdateModal.hide();
|
||||
this.avatarUpdateIndex = 0;
|
||||
this.avatarUpdateFile = undefined;
|
||||
},
|
||||
|
||||
avatarUpdateClear() {
|
||||
this.avatarUpdateIndex = 0;
|
||||
this.avatarUpdateFile = undefined;
|
||||
},
|
||||
|
||||
avatarUpdateStep(index) {
|
||||
this.$refs.avatarUpdateRef.click();
|
||||
this.avatarUpdateIndex = index;
|
||||
},
|
||||
|
||||
handleAvatarUpdate() {
|
||||
let self = this;
|
||||
let files = event.target.files;
|
||||
Array.prototype.forEach.call(files, function(io, i) {
|
||||
self.avatarUpdateFile = io;
|
||||
self.avatarUpdatePreview = URL.createObjectURL(io);
|
||||
self.avatarUpdateIndex = 1;
|
||||
});
|
||||
},
|
||||
|
||||
handleDrop(ev) {
|
||||
ev.preventDefault();
|
||||
let self = this;
|
||||
|
||||
if (ev.dataTransfer.items) {
|
||||
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
var file = ev.dataTransfer.items[i].getAsFile();
|
||||
if(!file) {
|
||||
return;
|
||||
}
|
||||
self.avatarUpdateFile = file;
|
||||
self.avatarUpdatePreview = URL.createObjectURL(file);
|
||||
self.avatarUpdateIndex = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
|
||||
if(!ev.dataTransfer.files[i].hasOwnProperty('name')) {
|
||||
return;
|
||||
}
|
||||
self.avatarUpdateFile = ev.dataTransfer.files[i];
|
||||
self.avatarUpdatePreview = URL.createObjectURL(ev.dataTransfer.files[i]);
|
||||
self.avatarUpdateIndex = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
confirmUpload() {
|
||||
if(!window.confirm('Are you sure you want to change your avatar photo?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append('_method', 'PATCH');
|
||||
formData.append('avatar', this.avatarUpdateFile);
|
||||
|
||||
axios.post('/api/v1/accounts/update_credentials', formData)
|
||||
.then(res => {
|
||||
window._sharedData.user.avatar = res.data.avatar;
|
||||
this.avatarUpdateClose();
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response.data && err.response.data.errors) {
|
||||
if(err.response.data.errors.avatar && err.response.data.errors.avatar.length) {
|
||||
swal('Oops!', err.response.data.errors.avatar[0], 'error');
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
978
resources/assets/components/partials/navbar.vue
Normal file
978
resources/assets/components/partials/navbar.vue
Normal file
|
@ -0,0 +1,978 @@
|
|||
<template>
|
||||
<nav class="metro-nav navbar navbar-expand navbar-light navbar-laravel sticky-top shadow-none py-1">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/i/web" title="Logo" style="width:50px">
|
||||
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager" alt="Pixelfed logo">
|
||||
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">
|
||||
{{ brandName }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="collapse navbar-collapse">
|
||||
<div class="navbar-nav ml-auto">
|
||||
<!-- <form class="form-inline search-bar" method="get" action="/i/results">
|
||||
<input class="form-control" name="q" placeholder="Search ..." aria-label="search" autocomplete="off" required style="position: relative;line-height: 0.6;width:100%;min-width: 300px;max-width: 500px;border-radius: 8px;" role="search">
|
||||
</form> -->
|
||||
|
||||
<autocomplete
|
||||
class="searchbox"
|
||||
:search="autocompleteSearch"
|
||||
:placeholder="$t('navmenu.search')"
|
||||
aria-label="Search"
|
||||
:get-result-value="getSearchResultValue"
|
||||
:debounceTime="700"
|
||||
@submit="onSearchSubmit"
|
||||
ref="autocomplete">
|
||||
|
||||
<template #result="{ result, props }">
|
||||
<li
|
||||
v-bind="props"
|
||||
class="autocomplete-result sr"
|
||||
>
|
||||
<div v-if="result.s_type === 'account'" class="media align-items-center my-0">
|
||||
<img :src="result.avatar" width="40" height="40" class="sr-avatar" style="border-radius: 40px" onerror="this.src='/storage/avatars/default.png?v=0';this.onerror=null;">
|
||||
<div class="media-body sr-account">
|
||||
<div class="sr-account-acct" :class="{ compact: result.acct && result.acct.length > 24 }">
|
||||
@{{ result.acct }}
|
||||
<b-button
|
||||
v-if="result.locked"
|
||||
v-b-tooltip.html
|
||||
title="Private Account"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="p-0"
|
||||
>
|
||||
<i class="far fa-lock fa-sm text-lighter ml-1"></i>
|
||||
</b-button>
|
||||
</div>
|
||||
<template v-if="result.is_admin">
|
||||
<div class="sr-account-stats">
|
||||
<div class="sr-account-stats-followers text-danger font-weight-bold">
|
||||
Admin
|
||||
</div>
|
||||
<div>·</div>
|
||||
<div class="sr-account-stats-followers font-weight-bold">
|
||||
<span>{{ formatCount(result.followers_count) }}</span>
|
||||
<span>Followers</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="result.local">
|
||||
<div class="sr-account-stats">
|
||||
<div v-if="result.followers_count" class="sr-account-stats-followers font-weight-bold">
|
||||
<span>{{ formatCount(result.followers_count) }}</span>
|
||||
<span>Followers</span>
|
||||
</div>
|
||||
<div v-if="result.followers_count && result.statuses_count">·</div>
|
||||
<div v-if="result.statuses_count" class="sr-account-stats-statuses font-weight-bold">
|
||||
<span>{{ formatCount(result.statuses_count) }}</span>
|
||||
<span>Posts</span>
|
||||
</div>
|
||||
<div v-if="!result.followers_count && result.statuses_count">·</div>
|
||||
<div class="sr-account-stats-statuses font-weight-bold">
|
||||
<i class="far fa-clock fa-sm"></i>
|
||||
<span>{{ timeAgo(result.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sr-account-stats">
|
||||
<div v-if="result.followers_count" class="sr-account-stats-followers font-weight-bold">
|
||||
<span>{{ formatCount(result.followers_count) }}</span>
|
||||
<span>Followers</span>
|
||||
</div>
|
||||
<div v-if="result.followers_count && result.statuses_count">·</div>
|
||||
<div v-if="result.statuses_count" class="sr-account-stats-statuses font-weight-bold">
|
||||
<span>{{ formatCount(result.statuses_count) }}</span>
|
||||
<span>Posts</span>
|
||||
</div>
|
||||
<div v-if="!result.followers_count && result.statuses_count">·</div>
|
||||
|
||||
<div v-if="!result.followers_count && !result.statuses_count" class="sr-account-stats-statuses font-weight-bold">
|
||||
Remote Account
|
||||
</div>
|
||||
<div v-if="!result.followers_count && !result.statuses_count">
|
||||
·
|
||||
</div>
|
||||
<b-button
|
||||
v-b-tooltip.html
|
||||
:title="'Joined ' + timeAgo(result.created_at) + ' ago'"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="sr-account-stats-statuses p-0"
|
||||
>
|
||||
<i class="far fa-clock fa-sm"></i>
|
||||
<span class="font-weight-bold">{{ timeAgo(result.created_at) }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="result.s_type === 'hashtag'" class="media align-items-center my-0">
|
||||
<div class="media-icon">
|
||||
<i class="far fa-hashtag fa-large"></i>
|
||||
</div>
|
||||
<div class="media-body sr-tag">
|
||||
<div class="sr-tag-name" :class="{ compact: result.name && result.name.length > 26 }">
|
||||
#{{ result.name }}
|
||||
</div>
|
||||
<div v-if="result.count && result.count > 100" class="sr-tag-count">
|
||||
{{ formatCount(result.count) }} {{ result.count == 1 ? 'Post' : 'Posts' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="result.s_type === 'status'" class="media align-items-center my-0">
|
||||
<img :src="result.account.avatar" width="40" height="40" class="sr-avatar" style="border-radius: 40px" onerror="this.src='/storage/avatars/default.png?v=0';this.onerror=null;">
|
||||
|
||||
<div class="media-body sr-post">
|
||||
<div class="sr-post-acct" :class="{ compact: result.acct && result.acct.length > 26 }">
|
||||
@{{ truncate(result.account.acct, 20) }}
|
||||
<b-button
|
||||
v-if="result.locked"
|
||||
v-b-tooltip.html
|
||||
title="Private Account"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="p-0"
|
||||
>
|
||||
<i class="far fa-lock fa-sm text-lighter ml-1"></i>
|
||||
</b-button>
|
||||
</div>
|
||||
<div class="sr-post-action">
|
||||
<div class="sr-post-action-timestamp">
|
||||
<i class="far fa-clock fa-sm"></i>
|
||||
{{ timeAgo(result.created_at)}}
|
||||
</div>
|
||||
<div>·</div>
|
||||
<div class="sr-post-action-label">
|
||||
Tap to view post
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</autocomplete>
|
||||
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<ul class="navbar-nav align-items-center">
|
||||
<!-- <li class="nav-item px-md-2 d-none d-md-block">
|
||||
<router-link class="nav-link font-weight-bold text-dark" to="/i/web" title="Home" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-home fa-lg"></i>
|
||||
<span class="sr-only">Home</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item px-md-2 d-none d-md-block">
|
||||
<router-link class="nav-link font-weight-bold text-dark" title="Compose" data-toggle="tooltip" data-placement="bottom" to="/i/web/compose">
|
||||
<i class="far fa-plus-square fa-lg"></i>
|
||||
<span class="sr-only">Compose</span>
|
||||
</router-link>
|
||||
</li> -->
|
||||
<!-- <li class="nav-item px-md-2">
|
||||
<router-link class="nav-link font-weight-bold text-dark" to="/i/web/direct" title="Direct" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-comment-dots fa-lg"></i>
|
||||
<span class="sr-only">Direct</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item px-md-2 d-none d-md-block">
|
||||
<router-link class="nav-link font-weight-bold text-dark fa-layers fa-fw" to="/i/web/notifications" title="Notifications" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-bell fa-lg"></i>
|
||||
<span class="fa-layers-counter" style="background:Tomato"></span>
|
||||
<span class="sr-only">Notifications</span>
|
||||
</router-link>
|
||||
</li> -->
|
||||
<li class="nav-item dropdown ml-2">
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="User Menu">
|
||||
<i class="d-none far fa-user fa-lg text-dark"></i>
|
||||
<span class="sr-only">User Menu</span>
|
||||
<img :src="user.avatar" class="nav-avatar rounded-circle border shadow" width="30" height="30" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right shadow" aria-labelledby="navbarDropdown">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item nav-icons">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<router-link class="nav-link text-center" to="/i/web">
|
||||
<div class="icon text-lighter"><i class="far fa-home fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.homeFeed') }}</div>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="hasLocalTimeline" class="nav-link text-center" :to="{ name: 'timeline', params: { scope: 'local' } }">
|
||||
<div class="icon text-lighter"><i class="fas fa-stream fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.localFeed') }}</div>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="hasNetworkTimeline" class="nav-link text-center" :to="{ name: 'timeline', params: { scope: 'global' } }">
|
||||
<div class="icon text-lighter"><i class="far fa-globe fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.globalFeed') }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-icons">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<router-link class="nav-link text-center" to="/i/web/discover">
|
||||
<div class="icon text-lighter"><i class="far fa-compass"></i></div>
|
||||
<div class="small">{{ $t('navmenu.discover') }}</div>
|
||||
</router-link>
|
||||
|
||||
<router-link class="nav-link text-center" to="/i/web/notifications">
|
||||
<div class="icon text-lighter">
|
||||
<i class="far fa-bell"></i>
|
||||
</div>
|
||||
<div class="small">
|
||||
{{ $t('navmenu.notifications') }}
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<router-link class="nav-link text-center px-3" :to="'/i/web/profile/' + user.id">
|
||||
<div class="icon text-lighter">
|
||||
<i class="far fa-user"></i>
|
||||
</div>
|
||||
<div class="small">{{ $t('navmenu.profile') }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<hr class="mb-0" style="margin-top: -5px;opacity: 0.4;" />
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/compose">
|
||||
<span class="icon text-lighter"><i class="far fa-plus-square"></i></span>
|
||||
{{ $t('navmenu.compose') }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/discover">
|
||||
<span class="icon text-lighter"><i class="far fa-compass"></i></span>
|
||||
{{ $t('navmenu.discover') }}
|
||||
</router-link>
|
||||
</li> -->
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/direct">
|
||||
<span>
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-envelope"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.directMessages') }}
|
||||
</span>
|
||||
|
||||
<!-- <span class="badge badge-danger font-weight-light rounded-pill px-2" style="transform:scale(0.86)">99+</span> -->
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/i/web" @click.prevent="openUserInterfaceSettings">
|
||||
<span class="icon text-lighter"><i class="far fa-brush"></i></span>
|
||||
UI Settings
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/notifications">
|
||||
<span>
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-bell"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.notifications') }}
|
||||
</span>
|
||||
</router-link>
|
||||
</li> -->
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
|
||||
|
||||
<router-link class="nav-link" :to="'/i/web/profile/' + user.id">
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-user"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.profile') }}
|
||||
</router-link>
|
||||
</li> -->
|
||||
<li v-if="user.is_admin" class="nav-item">
|
||||
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
|
||||
<a class="nav-link" href="/i/admin/dashboard">
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-tools"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.admin') }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
|
||||
<a class="nav-link" href="/">
|
||||
<span class="icon text-lighter">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.backToPreviousDesign') }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
|
||||
<a class="nav-link" href="/" @click.prevent="logout()">
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-sign-out"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.logout') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-modal
|
||||
ref="uis"
|
||||
hide-footer
|
||||
centered
|
||||
body-class="p-0 ui-menu"
|
||||
title="UI Settings">
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="font-weight-bold mb-1">Theme</p>
|
||||
<p class="small text-muted mb-0"></p>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
class="btn"
|
||||
:class="[ uiColorScheme == 'system' ? 'btn-primary' : 'btn-outline-primary']"
|
||||
@click="toggleUi('system')">
|
||||
Auto
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="[ uiColorScheme == 'light' ? 'btn-primary' : 'btn-outline-primary']"
|
||||
@click="toggleUi('light')">
|
||||
Light mode
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="[ uiColorScheme == 'dark' ? 'btn-primary' : 'btn-outline-primary']"
|
||||
@click="toggleUi('dark')">
|
||||
Dark mode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="font-weight-bold mb-1">Profile Layout</p>
|
||||
<p class="small text-muted mb-0"></p>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
class="btn"
|
||||
:class="[ profileLayout == 'grid' ? 'btn-primary' : 'btn-outline-primary']"
|
||||
@click="toggleProfileLayout('grid')">
|
||||
Grid
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="[ profileLayout == 'masonry' ? 'btn-primary' : 'btn-outline-primary']"
|
||||
@click="toggleProfileLayout('masonry')">
|
||||
Masonry
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="[ profileLayout == 'feed' ? 'btn-primary' : 'btn-outline-primary']"
|
||||
@click="toggleProfileLayout('feed')">
|
||||
Feed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="font-weight-bold mb-0">Compact Media Previews</p>
|
||||
</div>
|
||||
<b-form-checkbox v-model="fixedHeight" switch size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="font-weight-bold mb-0">Load Comments</p>
|
||||
</div>
|
||||
<b-form-checkbox v-model="autoloadComments" switch size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group-item px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="font-weight-bold mb-0">Hide Counts & Stats</p>
|
||||
</div>
|
||||
<b-form-checkbox v-model="hideCounts" switch size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</b-modal>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Autocomplete from '@trevoreyre/autocomplete-vue'
|
||||
import '@trevoreyre/autocomplete-vue/dist/style.css'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Autocomplete
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
brandName: 'pixelfed',
|
||||
user: window._sharedData.user,
|
||||
profileLayoutModel: 'grid',
|
||||
hasLocalTimeline: true,
|
||||
hasNetworkTimeline: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
profileLayout: {
|
||||
get() {
|
||||
return this.$store.state.profileLayout;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
this.$store.commit('setProfileLayout', val);
|
||||
}
|
||||
},
|
||||
|
||||
hideCounts: {
|
||||
get() {
|
||||
return this.$store.state.hideCounts;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
this.$store.commit('setHideCounts', val);
|
||||
}
|
||||
},
|
||||
autoloadComments: {
|
||||
get() {
|
||||
return this.$store.state.autoloadComments;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
this.$store.commit('setAutoloadComments', val);
|
||||
}
|
||||
},
|
||||
newReactions: {
|
||||
get() {
|
||||
return this.$store.state.newReactions;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
this.$store.commit('setNewReactions', val);
|
||||
}
|
||||
},
|
||||
|
||||
fixedHeight: {
|
||||
get() {
|
||||
return this.$store.state.fixedHeight;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
this.$store.commit('setFixedHeight', val);
|
||||
}
|
||||
},
|
||||
|
||||
uiColorScheme: {
|
||||
get() {
|
||||
return this.$store.state.colorScheme;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
this.$store.commit('setColorScheme', val);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if(window.App.config.features.hasOwnProperty('timelines')) {
|
||||
this.hasLocalTimeline = App.config.features.timelines.local;
|
||||
this.hasNetworkTimeline = App.config.features.timelines.network;
|
||||
}
|
||||
|
||||
let u = new URLSearchParams(window.location.search);
|
||||
if(u.has('q') && u.get('q') && u.has('src') && u.get('src') === 'ac') {
|
||||
this.$refs.autocomplete.setValue(u.get('q'));
|
||||
setTimeout(() => {
|
||||
let ai = document.querySelector('.autocomplete-input')
|
||||
ai.focus();
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
this.brandName = window.App.config.site.name;
|
||||
},
|
||||
|
||||
methods: {
|
||||
autocompleteSearch(q) {
|
||||
if (!q || q.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let resolve = q.startsWith('https://') || q.startsWith('@');
|
||||
|
||||
return axios.get('/api/v2/search', {
|
||||
params: {
|
||||
q: q,
|
||||
resolve: resolve,
|
||||
'_pe': 1
|
||||
}
|
||||
}).then(res => {
|
||||
let results = [];
|
||||
let accounts = res.data.accounts.map(res => {
|
||||
let account = res;
|
||||
account.s_type = 'account';
|
||||
return account;
|
||||
});
|
||||
let hashtags = res.data.hashtags.map(res => {
|
||||
let tag = res;
|
||||
tag.s_type = 'hashtag';
|
||||
return tag;
|
||||
})
|
||||
// let statuses = res.data.statuses.map(res => {
|
||||
// let status = res;
|
||||
// status.s_type = 'status';
|
||||
// return status;
|
||||
// });
|
||||
|
||||
// results.push(...statuses.slice(0,5));
|
||||
results.push(...accounts.slice(0,5));
|
||||
results.push(...hashtags.slice(0,5));
|
||||
|
||||
if(res.data.statuses) {
|
||||
if(Array.isArray(res.data.statuses)) {
|
||||
let statuses = res.data.statuses.map(res => {
|
||||
let status = res;
|
||||
status.s_type = 'status';
|
||||
return status;
|
||||
});
|
||||
results.push(...statuses);
|
||||
} else {
|
||||
if(q === res.data.statuses.url) {
|
||||
this.$refs.autocomplete.value = '';
|
||||
|
||||
this.$router.push({
|
||||
name: 'post',
|
||||
path: `/i/web/post/${res.data.statuses.id}`,
|
||||
params: {
|
||||
id: res.data.statuses.id,
|
||||
cachedStatus: res.data.statuses,
|
||||
cachedProfile: this.user
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
getSearchResultValue(result) {
|
||||
return result;
|
||||
},
|
||||
|
||||
onSearchSubmit(result) {
|
||||
if (result.length < 1) {
|
||||
return;
|
||||
}
|
||||
this.$refs.autocomplete.value = '';
|
||||
switch(result.s_type) {
|
||||
case 'account':
|
||||
// this.$router.push({
|
||||
// name: 'profile',
|
||||
// path: `/i/web/profile/${result.id}`,
|
||||
// params: {
|
||||
// id: result.id,
|
||||
// cachedProfile: result,
|
||||
// cachedUser: this.user
|
||||
// }
|
||||
// });
|
||||
location.href = `/i/web/profile/${result.id}`;
|
||||
break;
|
||||
|
||||
case 'hashtag':
|
||||
// this.$router.push({
|
||||
// name: 'hashtag',
|
||||
// path: `/i/web/hashtag/${result.name}`,
|
||||
// params: {
|
||||
// id: result.name,
|
||||
// }
|
||||
// });
|
||||
location.href = `/i/web/hashtag/${result.name}`;
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
// this.$router.push({
|
||||
// name: 'post',
|
||||
// path: `/i/web/post/${result.id}`,
|
||||
// params: {
|
||||
// id: result.id,
|
||||
// }
|
||||
// });
|
||||
location.href = `/i/web/post/${result.id}`;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
truncate(text, limit = 30) {
|
||||
if(text.length <= limit) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0, limit) + '...'
|
||||
},
|
||||
|
||||
timeAgo(ts) {
|
||||
return window.App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
if(!val) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat('en-CA', { notation: 'compact' , compactDisplay: "short" }).format(val);
|
||||
},
|
||||
|
||||
logout() {
|
||||
axios.post('/logout')
|
||||
.then(res => {
|
||||
location.href = '/';
|
||||
}).catch(err => {
|
||||
location.href = '/';
|
||||
})
|
||||
},
|
||||
|
||||
openUserInterfaceSettings() {
|
||||
event.currentTarget.blur();
|
||||
this.$refs.uis.show();
|
||||
},
|
||||
|
||||
toggleUi(ui) {
|
||||
event.currentTarget.blur();
|
||||
this.uiColorScheme = ui;
|
||||
},
|
||||
|
||||
toggleProfileLayout(layout) {
|
||||
event.currentTarget.blur();
|
||||
this.profileLayout = layout;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.metro-nav {
|
||||
z-index: 4;
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 18rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
||||
.nav {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
.nav-link {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
font-weight: 500;
|
||||
color: rgba(156,163,175, 1);
|
||||
padding-left: 14px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.router-link-exact-active {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
padding-left: 14px;
|
||||
|
||||
&:not(.text-center) {
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid var(--primary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.nav-icons {
|
||||
.small {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:last-child) {
|
||||
.nav-link {
|
||||
margin-bottom: 0;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa-layers {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: -0.125em;
|
||||
width: 1em;
|
||||
|
||||
.fa-layers-counter {
|
||||
background-color: #ff253a;
|
||||
border-radius: 1em;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
height: 1.5em;
|
||||
line-height: 1;
|
||||
max-width: 5em;
|
||||
min-width: 1.5em;
|
||||
overflow: hidden;
|
||||
padding: 0.25em;
|
||||
right: 0;
|
||||
text-overflow: ellipsis;
|
||||
top: 0;
|
||||
transform: scale(.5);
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right;
|
||||
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
margin-right: -5px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.far {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.searchbox {
|
||||
@media (min-width: 768px) {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-avatar {
|
||||
@media (min-width: 768px) {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete[data-loading="true"]::after {
|
||||
content: "";
|
||||
border-right: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
&-input {
|
||||
padding: 0.375rem 0.75rem 0.375rem 2.6rem;
|
||||
background-color: var(--light-gray);
|
||||
font-size: 0.9rem;
|
||||
border-radius: 50rem;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjQjhDMkNDIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGNpcmNsZSBjeD0iMTEiIGN5PSIxMSIgcj0iNSIvPjxwYXRoIGQ9Ik0xOSAxOWwtNC00Ii8+PC9zdmc+");
|
||||
box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important;
|
||||
}
|
||||
|
||||
&-result {
|
||||
background-image: none;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&-list {
|
||||
box-shadow: 0 0.125rem 0.45rem var(--border-color);
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important
|
||||
}
|
||||
}
|
||||
|
||||
.media-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
background: var(--light-gray);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 40px;
|
||||
box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.sr {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
margin-right: 12px;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)
|
||||
}
|
||||
|
||||
&-account {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
|
||||
&-acct {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--dark);
|
||||
margin-right: 1rem;
|
||||
|
||||
&.compact {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
line-height: 14px;
|
||||
|
||||
&-followers,
|
||||
&-statuses {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-lighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-tag {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
|
||||
&-name {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--dark);
|
||||
margin-right: 1rem;
|
||||
|
||||
&.compact {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-count {
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
color: var(--text-lighter);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&-post {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
|
||||
&-acct {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
&-action {
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
color: var(--text-lighter);
|
||||
font-weight: 500;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
|
||||
&-timestamp {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.force-dark-mode {
|
||||
.autocomplete-result-list {
|
||||
border-color: var(--input-border);
|
||||
}
|
||||
|
||||
.autocomplete-result:hover, .autocomplete-result[aria-selected=true] {
|
||||
box-shadow: 0;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.autocomplete[data-loading="true"]::after {
|
||||
content: "";
|
||||
border: 3px solid rgba(255, 255, 255, 0.22);
|
||||
border-right: 3px solid var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div class="ph-item border-0 shadow-sm p-1" style="border-radius:15px;margin-bottom: 1rem;">
|
||||
<div class="ph-col-12">
|
||||
<div class="ph-row align-items-center mt-0">
|
||||
<div class="ph-avatar mr-3 d-flex" style="min-width: 40px;width:40px!important;height:40px!important;border-radius: 15px;"></div>
|
||||
<div class="ph-col-6 big"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<div class="card card-body shadow-sm mb-3" style="border-radius: 15px;">
|
||||
<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="max-height: 300px;opacity: 0.6;">
|
||||
<p class="lead mb-0 text-center">This feed is empty</p>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div v-if="small" class="ph-item border-0 mb-0 p-0" style="border-radius:15px;margin-left:-14px;">
|
||||
<div class="ph-col-12 mb-0">
|
||||
<div class="ph-row align-items-center mt-0">
|
||||
<div class="ph-avatar mr-2 d-flex" style="min-width: 32px;width:32px!important;height:32px!important;border-radius: 40px;"></div>
|
||||
<div class="ph-col-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="ph-item border-0 shadow-sm p-1" style="border-radius:15px;margin-bottom: 1rem;">
|
||||
<div class="ph-col-12">
|
||||
<div class="ph-row align-items-center mt-0">
|
||||
<div class="ph-avatar mr-3 d-flex" style="min-width: 40px;width:40px!important;height:40px!important;border-radius: 15px;"></div>
|
||||
<div class="ph-col-6 big"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div class="timeline-onboarding">
|
||||
<div class="card card-body shadow-sm mb-3 p-5" style="border-radius: 15px;">
|
||||
<h1 class="text-center mb-4">✨ {{ $t('timeline.onboarding.welcome') }}</h1>
|
||||
|
||||
<p class="text-center mb-3" style="font-size: 22px;">
|
||||
{{ $t('timeline.onboarding.thisIsYourHomeFeed') }}
|
||||
</p>
|
||||
|
||||
<p class="text-center lead">{{ $t('timeline.onboarding.letUsHelpYouFind') }}</p>
|
||||
|
||||
<p v-if="newlyFollowed" class="text-center mb-0">
|
||||
<a href="/i/web" class="btn btn-primary btn-lg primary font-weight-bold rounded-pill px-4" onclick="location.reload()">
|
||||
{{ $t('timeline.onboarding.refreshFeed') }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 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>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import ProfileCard from './../profile/ProfileHoverCard.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"profile-card": ProfileCard
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
popularAccounts: [],
|
||||
newlyFollowed: 0
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchPopularAccounts();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchPopularAccounts() {
|
||||
axios.get('/api/pixelfed/discover/accounts/popular')
|
||||
.then(res => {
|
||||
this.popularAccounts = res.data;
|
||||
})
|
||||
},
|
||||
|
||||
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">
|
||||
.timeline-onboarding {
|
||||
.profile-hover-card-inner {
|
||||
width: 100%;
|
||||
|
||||
.d-flex {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
1066
resources/assets/components/partials/post/CommentDrawer.vue
Normal file
1066
resources/assets/components/partials/post/CommentDrawer.vue
Normal file
File diff suppressed because it is too large
Load diff
470
resources/assets/components/partials/post/CommentReplies.vue
Normal file
470
resources/assets/components/partials/post/CommentReplies.vue
Normal file
|
@ -0,0 +1,470 @@
|
|||
<template>
|
||||
<div class="comment-replies-component">
|
||||
<div v-if="loading" class="mt-n2">
|
||||
<div class="ph-item border-0 mb-0 p-0 bg-transparent" style="border-radius:15px;margin-left:-14px;">
|
||||
<div class="ph-col-12 mb-0">
|
||||
<div class="ph-row align-items-center mt-0">
|
||||
<div class="ph-avatar mr-3 d-flex" style="min-width: 40px;width:40px!important;height:40px!important;border-radius: 8px;"></div>
|
||||
<div class="ph-col-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<transition-group tag="div" enter-active-class="animate__animated animate__fadeIn" leave-active-class="animate__animated animate__fadeOut" mode="out-in">
|
||||
<div
|
||||
v-for="(post, idx) in feed"
|
||||
:key="'cd:' + post.id + ':' + idx">
|
||||
<div class="media media-status align-items-top mb-3">
|
||||
<a href="#l">
|
||||
<img class="shadow-sm media-avatar border" :src="post.account.avatar" width="40" height="40" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<div class="media-body-wrapper">
|
||||
<div v-if="!post.media_attachments.length" class="media-body-comment">
|
||||
<p class="media-body-comment-username">
|
||||
<a :href="post.account.url" @click.prevent="goToProfile(post.account)">
|
||||
{{ post.account.acct }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<span v-if="post.sensitive">
|
||||
<p class="mb-0">
|
||||
{{ $t('common.sensitiveContentWarning') }}
|
||||
</p>
|
||||
<a href="#" class="small font-weight-bold primary" @click.prevent="post.sensitive = false">Show</a>
|
||||
</span>
|
||||
|
||||
<!-- <span v-else v-html="post.content"></span> -->
|
||||
<read-more v-else :status="post" />
|
||||
|
||||
<button
|
||||
v-if="post.favourites_count"
|
||||
class="btn btn-link media-body-likes-count shadow-sm"
|
||||
@click.prevent="showLikesModal(idx)">
|
||||
<i class="far fa-thumbs-up primary"></i>
|
||||
<span class="count">{{ prettyCount(post.favourites_count) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<p class="media-body-comment-username">
|
||||
<a :href="post.account.url" @click.prevent="goToProfile(post.account)">
|
||||
{{ post.account.acct }}
|
||||
</a>
|
||||
</p>
|
||||
<div v-if="post.sensitive" class="bh-comment" @click="post.sensitive = false">
|
||||
<blur-hash-image
|
||||
:width="blurhashWidth(post)"
|
||||
:height="blurhashHeight(post)"
|
||||
:punch="1"
|
||||
class="img-fluid border shadow"
|
||||
:hash="post.media_attachments[0].blurhash"
|
||||
/>
|
||||
|
||||
<div class="sensitive-warning">
|
||||
<p class="mb-0"><i class="far fa-eye-slash fa-lg"></i></p>
|
||||
<p class="mb-0 small">Click to view</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="bh-comment">
|
||||
<div @click="lightbox(post)">
|
||||
<blur-hash-image
|
||||
:width="blurhashWidth(post)"
|
||||
:height="blurhashHeight(post)"
|
||||
:punch="1"
|
||||
class="img-fluid border shadow"
|
||||
:hash="post.media_attachments[0].blurhash"
|
||||
:src="getMediaSource(post)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="post.favourites_count"
|
||||
class="btn btn-link media-body-likes-count shadow-sm"
|
||||
@click.prevent="showLikesModal(idx)">
|
||||
<i class="far fa-thumbs-up primary"></i>
|
||||
<span class="count">{{ prettyCount(post.favourites_count) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="media-body-reactions">
|
||||
<button
|
||||
class="btn btn-link font-weight-bold btn-sm p-0"
|
||||
:class="[ post.favourited ? 'primary' : 'text-muted' ]"
|
||||
@click="likeComment(idx)">
|
||||
{{ post.favourited ? 'Liked' : 'Like' }}
|
||||
</button>
|
||||
<!-- <span class="mx-1">·</span>
|
||||
<a class="font-weight-bold text-muted" :href="post.url" @click.prevent="toggleCommentReply(idx)">
|
||||
Reply
|
||||
</a> -->
|
||||
<span class="mx-1">·</span>
|
||||
<a class="font-weight-bold text-muted" :href="post.url" @click.prevent="goToPost(post)" v-once>
|
||||
{{ timeago(post.created_at) }}
|
||||
</a>
|
||||
<span v-if="profile && post.account.id === profile.id">
|
||||
<span class="mx-1">·</span>
|
||||
<a
|
||||
class="font-weight-bold text-muted"
|
||||
href="#"
|
||||
@click.prevent="deleteComment(idx)">
|
||||
Delete
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<span class="mx-1">·</span>
|
||||
<a
|
||||
class="font-weight-bold text-muted"
|
||||
href="#"
|
||||
@click.prevent="reportComment(idx)">
|
||||
Report
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- <div class="d-flex align-items-top reply-form child-reply-form my-3">
|
||||
<img class="shadow-sm media-avatar border" :src="profile.avatar" width="40" height="40">
|
||||
|
||||
<input
|
||||
class="form-control bg-light rounded-pill shadow-sm" style="border-color: #e2e8f0 !important;"
|
||||
placeholder="Write a comment...."
|
||||
v-model="replyContent"
|
||||
v-on:keyup.enter="storeComment"
|
||||
:disabled="isPostingReply" />
|
||||
|
||||
<div class="reply-form-input-actions">
|
||||
<button
|
||||
class="btn btn-link text-muted px-1 mr-2">
|
||||
<i class="far fa-image fa-lg"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link text-muted px-1 small font-weight-bold py-0 rounded-pill text-decoration-none"
|
||||
@click="toggleShowReplyOptions">
|
||||
<i class="far fa-ellipsis-h"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import ReadMore from './ReadMore.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
feed: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
ReadMore
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
profile: window._sharedData.user,
|
||||
ids: [],
|
||||
nextUrl: undefined,
|
||||
canLoadMore: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
feed: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(o, n) {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchContext() {
|
||||
axios.get('/api/v2/statuses/' + this.status.id + '/replies', {
|
||||
params: {
|
||||
limit: 3
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data.next) {
|
||||
this.nextUrl = res.data.next;
|
||||
this.canLoadMore = true;
|
||||
}
|
||||
res.data.data.forEach(post => {
|
||||
this.ids.push(post.id);
|
||||
this.feed.push(post);
|
||||
});
|
||||
|
||||
if(!res.data || !res.data.data || !res.data.data.length && this.status.reply_count) {
|
||||
this.showEmptyRepliesRefresh = true;
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
fetchMore(limit = 3) {
|
||||
axios.get(this.nextUrl, {
|
||||
params: {
|
||||
limit: limit,
|
||||
sort: this.sorts[this.sortIndex]
|
||||
}
|
||||
}).then(res => {
|
||||
this.feedLoading = false;
|
||||
if(!res.data.next) {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
|
||||
this.nextUrl = res.data.next;
|
||||
|
||||
res.data.data.forEach(post => {
|
||||
if(this.ids.indexOf(post.id) == -1) {
|
||||
this.ids.push(post.id);
|
||||
this.feed.push(post);
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
fetchSortedFeed() {
|
||||
axios.get('/api/v2/statuses/' + this.status.id + '/replies', {
|
||||
params: {
|
||||
limit: 3,
|
||||
sort: this.sorts[this.sortIndex]
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.feed = res.data.data;
|
||||
this.nextUrl = res.data.next;
|
||||
this.feedLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
forceRefresh() {
|
||||
axios.get('/api/v2/statuses/' + this.status.id + '/replies', {
|
||||
params: {
|
||||
limit: 3,
|
||||
refresh_cache: true
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data.next) {
|
||||
this.nextUrl = res.data.next;
|
||||
this.canLoadMore = true;
|
||||
}
|
||||
res.data.data.forEach(post => {
|
||||
this.ids.push(post.id);
|
||||
this.feed.push(post);
|
||||
});
|
||||
|
||||
this.showEmptyRepliesRefresh = false;
|
||||
})
|
||||
},
|
||||
|
||||
timeago(ts) {
|
||||
return App.util.format.timeAgo(ts);
|
||||
},
|
||||
|
||||
prettyCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
goToPost(post) {
|
||||
this.$router.push({
|
||||
name: 'post',
|
||||
path: `/i/web/post/${post.id}`,
|
||||
params: {
|
||||
id: post.id,
|
||||
cachedStatus: post,
|
||||
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
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
storeComment() {
|
||||
this.isPostingReply = true;
|
||||
|
||||
axios.post('/api/v1/statuses', {
|
||||
status: this.replyContent,
|
||||
in_reply_to_id: this.status.id,
|
||||
sensitive: this.settings.sensitive
|
||||
})
|
||||
.then(res => {
|
||||
this.replyContent = undefined;
|
||||
this.isPostingReply = false;
|
||||
this.ids.push(res.data.id);
|
||||
this.feed.push(res.data);
|
||||
this.$emit('new-comment', res.data);
|
||||
})
|
||||
},
|
||||
|
||||
toggleSort(index) {
|
||||
this.$refs.sortMenu.hide();
|
||||
this.feedLoading = true;
|
||||
this.sortIndex = index;
|
||||
this.fetchSortedFeed();
|
||||
},
|
||||
|
||||
deleteComment(index) {
|
||||
event.currentTarget.blur();
|
||||
|
||||
if(!window.confirm(this.$t('menu.deletePostConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/i/delete', {
|
||||
type: 'status',
|
||||
item: this.feed[index].id
|
||||
})
|
||||
.then(res => {
|
||||
this.feed.splice(index, 1);
|
||||
this.$emit('counter-change', 'comment-decrement');
|
||||
this.fetchMore(1);
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
})
|
||||
},
|
||||
|
||||
showLikesModal(index) {
|
||||
this.$emit('show-likes', this.feed[index]);
|
||||
},
|
||||
|
||||
reportComment(index) {
|
||||
// location.href = '/i/report?type=post&id=' + this.feed[index].id;
|
||||
this.$emit('handle-report', this.feed[index]);
|
||||
},
|
||||
|
||||
likeComment(index) {
|
||||
event.currentTarget.blur();
|
||||
let post = this.feed[index];
|
||||
let count = post.favourites_count;
|
||||
let state = post.favourited;
|
||||
this.feed[index].favourited = !this.feed[index].favourited;
|
||||
this.feed[index].favourites_count = state ? count - 1 : count + 1;
|
||||
|
||||
axios.post('/api/v1/statuses/' + post.id + '/' + (state ? 'unfavourite' : 'favourite'))
|
||||
.then(res => {
|
||||
|
||||
})
|
||||
},
|
||||
|
||||
toggleShowReplyOptions() {
|
||||
event.currentTarget.blur();
|
||||
this.showReplyOptions = !this.showReplyOptions;
|
||||
},
|
||||
|
||||
replyUpload() {
|
||||
event.currentTarget.blur();
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
|
||||
handleImageUpload() {
|
||||
if(!this.$refs.fileInput.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUploading = true;
|
||||
let self = this;
|
||||
let data = new FormData();
|
||||
data.append('file', this.$refs.fileInput.files[0]);
|
||||
|
||||
axios.post('/api/v1/media', data)
|
||||
.then(res => {
|
||||
axios.post('/api/v1/statuses', {
|
||||
media_ids: [ res.data.id ],
|
||||
in_reply_to_id: this.status.id,
|
||||
sensitive: this.settings.sensitive
|
||||
}).then(res => {
|
||||
this.feed.push(res.data)
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
lightbox(status) {
|
||||
this.lightboxStatus = status.media_attachments[0];
|
||||
this.$refs.lightboxModal.show();
|
||||
},
|
||||
|
||||
hideLightbox() {
|
||||
this.lightboxStatus = null;
|
||||
this.$refs.lightboxModal.hide();
|
||||
},
|
||||
|
||||
blurhashWidth(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 30;
|
||||
} else {
|
||||
return 20;
|
||||
}
|
||||
},
|
||||
|
||||
blurhashHeight(status) {
|
||||
if(!status.media_attachments[0].meta) {
|
||||
return 25;
|
||||
}
|
||||
let aspect = status.media_attachments[0].meta.original.aspect;
|
||||
if(aspect == 1) {
|
||||
return 25;
|
||||
} else if(aspect > 1) {
|
||||
return 20;
|
||||
} else {
|
||||
return 30;
|
||||
}
|
||||
},
|
||||
|
||||
getMediaSource(status) {
|
||||
let media = status.media_attachments[0];
|
||||
|
||||
if(media.preview_url.endsWith('storage/no-preview.png')) {
|
||||
return media.url;
|
||||
}
|
||||
|
||||
return media.preview_url;
|
||||
},
|
||||
|
||||
toggleReplyExpand() {
|
||||
event.currentTarget.blur();
|
||||
this.settings.expanded = !this.settings.expanded;
|
||||
},
|
||||
|
||||
toggleCommentReply(index) {
|
||||
this.commentReplyIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="my-3">
|
||||
<div class="d-flex align-items-top reply-form child-reply-form">
|
||||
<img class="shadow-sm media-avatar border" :src="profile.avatar" width="40" height="40" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
|
||||
|
||||
<div style="display: flex;flex-grow: 1;position: relative;">
|
||||
<textarea
|
||||
class="form-control bg-light rounded-lg shadow-sm" style="resize: none;padding-right: 60px;"
|
||||
placeholder="Write a comment...."
|
||||
v-model="replyContent"
|
||||
:disabled="isPostingReply" />
|
||||
|
||||
<button
|
||||
class="btn btn-sm py-1 font-weight-bold ml-1 rounded-pill"
|
||||
:class="[replyContent && replyContent.length ? 'btn-primary' : 'btn-outline-muted']"
|
||||
@click="storeComment"
|
||||
:disabled="!replyContent || !replyContent.length"
|
||||
style="position: absolute;right:10px;top:50%;transform:translateY(-50%)">
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-right small font-weight-bold text-lighter">{{ replyContent ? replyContent.length : 0 }}/{{ config.uploader.max_caption_length }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
parentId: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: App.config,
|
||||
isPostingReply: false,
|
||||
replyContent: '',
|
||||
profile: window._sharedData.user,
|
||||
sensitive: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
storeComment() {
|
||||
this.isPostingReply = true;
|
||||
|
||||
axios.post('/api/v1/statuses', {
|
||||
status: this.replyContent,
|
||||
in_reply_to_id: this.parentId,
|
||||
sensitive: this.sensitive
|
||||
})
|
||||
.then(res => {
|
||||
this.replyContent = undefined;
|
||||
this.isPostingReply = false;
|
||||
this.$emit('new-comment', res.data);
|
||||
// this.ids.push(res.data.id);
|
||||
// this.feed.push(res.data);
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
803
resources/assets/components/partials/post/ContextMenu.vue
Normal file
803
resources/assets/components/partials/post/ContextMenu.vue
Normal file
|
@ -0,0 +1,803 @@
|
|||
<template>
|
||||
<div class="modal-stack">
|
||||
<b-modal ref="ctxModal"
|
||||
id="ctx-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
|
||||
<div class="list-group text-center">
|
||||
<div
|
||||
v-if="status.visibility !== 'archived'"
|
||||
class="list-group-item rounded cursor-pointer font-weight-bold"
|
||||
@click="ctxMenuGoToPost()">
|
||||
{{ $t('menu.viewPost') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.visibility !== 'archived'"
|
||||
class="list-group-item rounded cursor-pointer font-weight-bold"
|
||||
@click="ctxMenuGoToProfile()">
|
||||
{{ $t('menu.viewProfile') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.visibility !== 'archived'"
|
||||
class="list-group-item rounded cursor-pointer font-weight-bold"
|
||||
@click="ctxMenuShare()">
|
||||
{{ $t('common.share') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status && profile && profile.is_admin == true && status.visibility !== 'archived'"
|
||||
class="list-group-item rounded cursor-pointer font-weight-bold"
|
||||
@click="ctxModMenuShow()">
|
||||
{{ $t('menu.moderationTools') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status && status.account.id != profile.id"
|
||||
class="list-group-item rounded cursor-pointer text-danger font-weight-bold"
|
||||
@click="ctxMenuReportPost()">
|
||||
{{ $t('menu.report') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status && profile.id == status.account.id && status.visibility !== 'archived'"
|
||||
class="list-group-item rounded cursor-pointer text-danger font-weight-bold"
|
||||
@click="archivePost(status)">
|
||||
{{ $t('menu.archive') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status && profile.id == status.account.id && status.visibility == 'archived'"
|
||||
class="list-group-item rounded cursor-pointer text-danger font-weight-bold"
|
||||
@click="unarchivePost(status)">
|
||||
{{ $t('menu.unarchive') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="config.ab.pue && status && profile.id == status.account.id && status.visibility !== 'archived'"
|
||||
class="list-group-item rounded cursor-pointer text-danger font-weight-bold"
|
||||
@click="editPost(status)">
|
||||
Edit
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status && (profile.is_admin || profile.id == status.account.id) && status.visibility !== 'archived'"
|
||||
class="list-group-item rounded cursor-pointer text-danger font-weight-bold"
|
||||
@click="deletePost(status)">
|
||||
<div v-if="isDeleting" class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ $t('common.delete') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="list-group-item rounded cursor-pointer text-lighter font-weight-bold"
|
||||
@click="closeCtxMenu()">
|
||||
{{ $t('common.cancel') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal ref="ctxModModal"
|
||||
id="ctx-mod-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
|
||||
<div class="list-group text-center">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div
|
||||
class="text-center font-weight-bold text-danger">
|
||||
{{ $t('menu.moderationTools') }}
|
||||
</div>
|
||||
|
||||
<div class="small text-center text-muted">
|
||||
{{ $t('menu.selectOneOption') }}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="list-group-item rounded cursor-pointer"
|
||||
@click="moderatePost(status, 'unlist')">
|
||||
{{ $t('menu.unlistFromTimelines') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.sensitive"
|
||||
class="list-group-item rounded cursor-pointer"
|
||||
@click="moderatePost(status, 'remcw')">
|
||||
{{ $t('menu.removeCW') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="list-group-item rounded cursor-pointer"
|
||||
@click="moderatePost(status, 'addcw')">
|
||||
{{ $t('menu.addCW') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="list-group-item rounded cursor-pointer"
|
||||
@click="moderatePost(status, 'spammer')">
|
||||
{{ $t('menu.markAsSpammer') }}<br />
|
||||
<span class="small">{{ $t('menu.markAsSpammerText') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="list-group-item rounded cursor-pointer text-lighter"
|
||||
@click="ctxModMenuClose()">
|
||||
{{ $t('common.cancel') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal ref="ctxModOtherModal"
|
||||
id="ctx-mod-other-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<div class="list-group text-center">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div class="text-center font-weight-bold text-danger">{{ $t('menu.moderationTools') }}</div>
|
||||
<div class="small text-center text-muted">{{ $t('menu.selectOneOption') }}</div>
|
||||
</p>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="confirmModal()">Unlist Posts</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="confirmModal()">Moderation Log</div>
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModOtherMenuClose()">{{ $t('common.cancel') }}</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal ref="ctxShareModal"
|
||||
id="ctx-share-modal"
|
||||
title="Share"
|
||||
hide-footer
|
||||
hide-header
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded text-center">
|
||||
<div class="list-group-item rounded cursor-pointer" @click="shareStatus(status, $event)">{{status.reblogged ? 'Unshare' : 'Share'}} {{ $t('menu.toFollowers') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">{{ $t('common.copyLink') }}</div>
|
||||
<div v-if="status && status.local == true && !status.in_reply_to_id" class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">{{ $t('menu.embed') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxShareMenu()">{{ $t('common.cancel') }}</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal ref="ctxEmbedModal"
|
||||
id="ctx-embed-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="md"
|
||||
body-class="p-2 rounded">
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control disabled text-monospace" rows="8" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea>
|
||||
</div>
|
||||
<div class="form-group pl-2 d-flex justify-content-center">
|
||||
<div class="form-check mr-3">
|
||||
<input class="form-check-input" type="checkbox" v-model="ctxEmbedShowCaption" :disabled="ctxEmbedCompactMode == true">
|
||||
<label class="form-check-label font-weight-light">
|
||||
{{ $t('menu.showCaption') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mr-3">
|
||||
<input class="form-check-input" type="checkbox" v-model="ctxEmbedShowLikes" :disabled="ctxEmbedCompactMode == true">
|
||||
<label class="form-check-label font-weight-light">
|
||||
{{ $t('menu.showLikes') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="ctxEmbedCompactMode">
|
||||
<label class="form-check-label font-weight-light">
|
||||
{{ $t('menu.compactMode') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
|
||||
<p class="mb-0 px-2 small text-muted">{{ $t('menu.embedConfirmText') }} <a href="/site/terms">{{ $t('site.terms') }}</a></p>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal ref="ctxReport"
|
||||
id="ctx-report"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div class="text-center font-weight-bold text-danger">{{ $t('menu.report') }}</div>
|
||||
<div class="small text-center text-muted">{{ $t('menu.selectOneOption') }}</div>
|
||||
</p>
|
||||
<div class="list-group text-center">
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('spam')">{{ $t('menu.spam') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('sensitive')">{{ $t('menu.sensitive') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('abusive')">{{ $t('menu.abusive') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="openCtxReportOtherMenu()">{{ $t('common.other') }}</div>
|
||||
<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxReportMenuGoBack()">Go Back</div> -->
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxReportMenuGoBack()">{{ $t('common.cancel') }}</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal ref="ctxReportOther"
|
||||
id="ctx-report-other"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<p class="py-2 px-3 mb-0">
|
||||
<div class="text-center font-weight-bold text-danger">{{ $t('menu.report') }}</div>
|
||||
<div class="small text-center text-muted">{{ $t('menu.selectOneOption') }}</div>
|
||||
</p>
|
||||
<div class="list-group text-center">
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('underage')">{{ $t('menu.underageAccount') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('copyright')">{{ $t('menu.copyrightInfringement') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('impersonation')">{{ $t('menu.impersonation') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer font-weight-bold" @click="sendReport('scam')">{{ $t('menu.scamOrFraud') }}</div>
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxReportOtherMenuGoBack()">{{ $t('common.cancel') }}</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal ref="ctxConfirm"
|
||||
id="ctx-confirm"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<div class="d-flex align-items-center justify-content-center py-3">
|
||||
<div>{{ this.confirmModalTitle }}</div>
|
||||
</div>
|
||||
<div class="d-flex border-top btn-group btn-group-block rounded-0" role="group">
|
||||
<button type="button" class="btn btn-outline-lighter border-left-0 border-top-0 border-bottom-0 border-right py-2" style="color: rgb(0,122,255) !important;" @click.prevent="confirmModalCancel()">{{ $t('common.cancel') }}</button>
|
||||
<button type="button" class="btn btn-outline-lighter border-0" style="color: rgb(0,122,255) !important;" @click.prevent="confirmModalConfirm()">Confirm</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: [
|
||||
'status',
|
||||
'profile'
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: window.App.config,
|
||||
ctxMenuStatus: false,
|
||||
ctxMenuRelationship: false,
|
||||
ctxEmbedPayload: false,
|
||||
copiedEmbed: false,
|
||||
replySending: false,
|
||||
ctxEmbedShowCaption: true,
|
||||
ctxEmbedShowLikes: false,
|
||||
ctxEmbedCompactMode: false,
|
||||
confirmModalTitle: 'Are you sure?',
|
||||
confirmModalIdentifer: null,
|
||||
confirmModalType: false,
|
||||
isDeleting: false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
ctxEmbedShowCaption: function (n,o) {
|
||||
if(n == true) {
|
||||
this.ctxEmbedCompactMode = false;
|
||||
}
|
||||
let mode = this.ctxEmbedCompactMode ? 'compact' : 'full';
|
||||
this.ctxEmbedPayload = window.App.util.embed.post(this.ctxMenuStatus.url, this.ctxEmbedShowCaption, this.ctxEmbedShowLikes, mode);
|
||||
},
|
||||
ctxEmbedShowLikes: function (n,o) {
|
||||
if(n == true) {
|
||||
this.ctxEmbedCompactMode = false;
|
||||
}
|
||||
let mode = this.ctxEmbedCompactMode ? 'compact' : 'full';
|
||||
this.ctxEmbedPayload = window.App.util.embed.post(this.ctxMenuStatus.url, this.ctxEmbedShowCaption, this.ctxEmbedShowLikes, mode);
|
||||
},
|
||||
ctxEmbedCompactMode: function (n,o) {
|
||||
if(n == true) {
|
||||
this.ctxEmbedShowCaption = false;
|
||||
this.ctxEmbedShowLikes = false;
|
||||
}
|
||||
let mode = this.ctxEmbedCompactMode ? 'compact' : 'full';
|
||||
this.ctxEmbedPayload = window.App.util.embed.post(this.ctxMenuStatus.url, this.ctxEmbedShowCaption, this.ctxEmbedShowLikes, mode);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.ctxMenu();
|
||||
},
|
||||
|
||||
openModMenu() {
|
||||
this.$refs.ctxModModal.show();
|
||||
},
|
||||
|
||||
ctxMenu() {
|
||||
this.ctxMenuStatus = this.status;
|
||||
this.ctxEmbedPayload = window.App.util.embed.post(this.status.url);
|
||||
// if(this.status.account.id == this.profile.id) {
|
||||
this.ctxMenuRelationship = false;
|
||||
this.$refs.ctxModal.show();
|
||||
// } else {
|
||||
// axios.get('/api/pixelfed/v1/accounts/relationships', {
|
||||
// params: {
|
||||
// 'id[]': this.status.account.id
|
||||
// }
|
||||
// }).then(res => {
|
||||
// this.ctxMenuRelationship = res.data[0];
|
||||
// this.$refs.ctxModal.show();
|
||||
// });
|
||||
// }
|
||||
},
|
||||
|
||||
closeCtxMenu() {
|
||||
this.copiedEmbed = false;
|
||||
this.ctxMenuStatus = false;
|
||||
this.ctxMenuRelationship = false;
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxReport.hide();
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.closeModals();
|
||||
},
|
||||
|
||||
ctxMenuCopyLink() {
|
||||
let status = this.ctxMenuStatus;
|
||||
navigator.clipboard.writeText(status.url);
|
||||
this.closeModals();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuGoToPost() {
|
||||
let status = this.ctxMenuStatus;
|
||||
this.statusUrl(status);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuGoToProfile() {
|
||||
let status = this.ctxMenuStatus;
|
||||
this.profileUrl(status);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuReportPost() {
|
||||
this.$refs.ctxModal.hide();
|
||||
// this.$refs.ctxReport.show();
|
||||
this.$emit('report-modal', this.ctxMenuStatus);
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuEmbed() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxEmbedModal.show();
|
||||
},
|
||||
|
||||
ctxMenuShare() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxShareModal.show();
|
||||
},
|
||||
|
||||
closeCtxShareMenu() {
|
||||
this.$refs.ctxShareModal.hide();
|
||||
this.$refs.ctxModal.show();
|
||||
},
|
||||
|
||||
ctxCopyEmbed() {
|
||||
navigator.clipboard.writeText(this.ctxEmbedPayload);
|
||||
this.ctxEmbedShowCaption = true;
|
||||
this.ctxEmbedShowLikes = false;
|
||||
this.ctxEmbedCompactMode = false;
|
||||
this.$refs.ctxEmbedModal.hide();
|
||||
},
|
||||
|
||||
ctxModMenuShow() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxModModal.show();
|
||||
},
|
||||
|
||||
ctxModOtherMenuShow() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxModModal.hide();
|
||||
this.$refs.ctxModOtherModal.show();
|
||||
},
|
||||
|
||||
ctxModMenu() {
|
||||
this.$refs.ctxModal.hide();
|
||||
},
|
||||
|
||||
ctxModMenuClose() {
|
||||
this.closeModals();
|
||||
},
|
||||
|
||||
ctxModOtherMenuClose() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxModModal.show();
|
||||
},
|
||||
|
||||
formatCount(count) {
|
||||
return App.util.format.count(count);
|
||||
},
|
||||
|
||||
openCtxReportOtherMenu() {
|
||||
let s = this.ctxMenuStatus;
|
||||
this.closeCtxMenu();
|
||||
this.ctxMenuStatus = s;
|
||||
this.$refs.ctxReportOther.show();
|
||||
},
|
||||
|
||||
ctxReportMenuGoBack() {
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.$refs.ctxReport.hide();
|
||||
this.$refs.ctxModal.show();
|
||||
},
|
||||
|
||||
ctxReportOtherMenuGoBack() {
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxReport.show();
|
||||
},
|
||||
|
||||
sendReport(type) {
|
||||
let id = this.ctxMenuStatus.id;
|
||||
|
||||
swal({
|
||||
'title': this.$t('menu.confirmReport'),
|
||||
'text': this.$t('menu.confirmReportText'),
|
||||
'icon': 'warning',
|
||||
'buttons': true,
|
||||
'dangerMode': true
|
||||
}).then((res) => {
|
||||
if(res) {
|
||||
axios.post('/i/report/', {
|
||||
'report': type,
|
||||
'type': 'post',
|
||||
'id': id,
|
||||
}).then(res => {
|
||||
this.closeCtxMenu();
|
||||
swal(this.$t('menu.reportSent'), this.$t('menu.reportSentText'), 'success');
|
||||
}).catch(err => {
|
||||
swal(this.$t('common.oops'), this.$t('menu.reportSentError'), 'error');
|
||||
})
|
||||
} else {
|
||||
this.closeCtxMenu();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
closeModals() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxModModal.hide();
|
||||
this.$refs.ctxModOtherModal.hide();
|
||||
this.$refs.ctxShareModal.hide();
|
||||
this.$refs.ctxEmbedModal.hide();
|
||||
this.$refs.ctxReport.hide();
|
||||
this.$refs.ctxReportOther.hide();
|
||||
this.$refs.ctxConfirm.hide();
|
||||
},
|
||||
|
||||
openCtxStatusModal() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxStatusModal.show();
|
||||
},
|
||||
|
||||
openConfirmModal() {
|
||||
this.closeModals();
|
||||
this.$refs.ctxConfirm.show();
|
||||
},
|
||||
|
||||
closeConfirmModal() {
|
||||
this.closeModals();
|
||||
this.confirmModalTitle = 'Are you sure?';
|
||||
this.confirmModalType = false;
|
||||
this.confirmModalIdentifer = null;
|
||||
},
|
||||
|
||||
confirmModalConfirm() {
|
||||
switch(this.confirmModalType) {
|
||||
case 'post.delete':
|
||||
axios.post('/i/delete', {
|
||||
type: 'status',
|
||||
item: this.confirmModalIdentifer
|
||||
}).then(res => {
|
||||
this.feed = this.feed.filter(s => {
|
||||
return s.id != this.confirmModalIdentifer;
|
||||
});
|
||||
this.closeConfirmModal();
|
||||
}).catch(err => {
|
||||
this.closeConfirmModal();
|
||||
swal(this.$t('common.error'), this.$t('common.errorMsg'), 'error');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
this.closeConfirmModal();
|
||||
},
|
||||
|
||||
confirmModalCancel() {
|
||||
this.closeConfirmModal();
|
||||
},
|
||||
|
||||
moderatePost(status, action, $event) {
|
||||
let username = status.account.username;
|
||||
let pid = status.id;
|
||||
let msg = '';
|
||||
let self = this;
|
||||
switch(action) {
|
||||
case 'addcw':
|
||||
msg = this.$t('menu.modAddCWConfirm');
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
swal(this.$t('common.success'), this.$t('menu.modCWSuccess'), 'success');
|
||||
// status.sensitive = true;
|
||||
this.$emit('moderate', 'addcw');
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
swal(this.$t('common.error'), this.$t('common.errorMsg'), 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'remcw':
|
||||
msg = this.$t('menu.modRemoveCWConfirm');
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
swal(this.$t('common.success'), this.$t('menu.modRemoveCWSuccess'), 'success');
|
||||
// status.sensitive = false;
|
||||
this.$emit('moderate', 'remcw');
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
swal(this.$t('common.error'), this.$t('common.errorMsg'), 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'unlist':
|
||||
msg = this.$t('menu.modUnlistConfirm');
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
// this.feed = this.feed.filter(f => {
|
||||
// return f.id != status.id;
|
||||
// });
|
||||
this.$emit('moderate', 'unlist');
|
||||
swal(this.$t('common.success'), this.$t('menu.modUnlistSuccess'), 'success');
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
swal(this.$t('common.error'), this.$t('common.errorMsg'), 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'spammer':
|
||||
msg = this.$t('menu.modMarkAsSpammerConfirm');
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: msg,
|
||||
icon: 'warning',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
axios.post('/api/v2/moderator/action', {
|
||||
action: action,
|
||||
item_id: status.id,
|
||||
item_type: 'status'
|
||||
}).then(res => {
|
||||
this.$emit('moderate', 'spammer');
|
||||
swal(this.$t('common.success'), this.$t('menu.modMarkAsSpammerSuccess'), 'success');
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
}).catch(err => {
|
||||
self.closeModals();
|
||||
self.ctxModMenuClose();
|
||||
swal(this.$t('common.error'), this.$t('common.errorMsg'), 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
shareStatus(status, $event) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeModals();
|
||||
|
||||
axios.post('/i/share', {
|
||||
item: status.id
|
||||
}).then(res => {
|
||||
status.reblogs_count = res.data.count;
|
||||
status.reblogged = !status.reblogged;
|
||||
// if(status.reblogged) {
|
||||
// swal('Success', 'You shared this post', 'success');
|
||||
// } else {
|
||||
// swal('Success', 'You unshared this post', 'success');
|
||||
// }
|
||||
}).catch(err => {
|
||||
swal(this.$t('common.error'), this.$t('common.errorMsg'), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
statusUrl(status) {
|
||||
if(status.account.local == true) {
|
||||
this.$router.push({
|
||||
name: 'post',
|
||||
path: `/i/web/post/${status.id}`,
|
||||
params: {
|
||||
id: status.id,
|
||||
cachedStatus: status,
|
||||
cachedProfile: this.profile
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let permalink = this.$route.params.hasOwnProperty('id');
|
||||
if(permalink) {
|
||||
location.href = status.url;
|
||||
return;
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'post',
|
||||
path: `/i/web/post/${status.id}`,
|
||||
params: {
|
||||
id: status.id,
|
||||
cachedStatus: status,
|
||||
cachedProfile: this.profile
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
profileUrl(status) {
|
||||
this.$router.push({
|
||||
name: 'profile',
|
||||
path: `/i/web/profile/${status.account.id}`,
|
||||
params: {
|
||||
id: status.account.id,
|
||||
cachedProfile: status.account,
|
||||
cachedUser: this.profile
|
||||
}
|
||||
});
|
||||
return;
|
||||
},
|
||||
|
||||
deletePost(status) {
|
||||
this.isDeleting = true;
|
||||
|
||||
if(this.ownerOrAdmin(status) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(window.confirm(this.$t('menu.deletePostConfirm')) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/i/delete', {
|
||||
type: 'status',
|
||||
item: status.id
|
||||
}).then(res => {
|
||||
this.$emit('delete');
|
||||
this.closeModals();
|
||||
this.isDeleting = false;
|
||||
}).catch(err => {
|
||||
swal(this.$t('common.error'), this.$t('common.errorMsg'), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
owner(status) {
|
||||
return this.profile.id === status.account.id;
|
||||
},
|
||||
|
||||
admin() {
|
||||
return this.profile.is_admin == true;
|
||||
},
|
||||
|
||||
ownerOrAdmin(status) {
|
||||
return this.owner(status) || this.admin();
|
||||
},
|
||||
|
||||
archivePost(status) {
|
||||
if(window.confirm(this.$t('menu.archivePostConfirm')) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/pixelfed/v2/status/' + status.id + '/archive')
|
||||
.then(res => {
|
||||
this.$emit('status-delete', status.id);
|
||||
this.$emit('archived', status.id);
|
||||
this.closeModals();
|
||||
});
|
||||
},
|
||||
|
||||
unarchivePost(status) {
|
||||
if(window.confirm(this.$t('menu.unarchivePostConfirm')) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/pixelfed/v2/status/' + status.id + '/unarchive')
|
||||
.then(res => {
|
||||
this.$emit('unarchived', status.id);
|
||||
this.closeModals();
|
||||
});
|
||||
},
|
||||
|
||||
editPost(status) {
|
||||
this.closeModals();
|
||||
this.$emit('edit', status);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -123,6 +123,7 @@
|
|||
</template>
|
||||
<div class="w-100 my-4 px-4 text-break justify-content-start">
|
||||
<p class="mb-0" v-html="allHistory[historyIndex].content"></p>
|
||||
<!-- <p class="mb-0" v-html="getDiff(historyIndex)"></p> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -170,6 +171,36 @@
|
|||
})
|
||||
},
|
||||
|
||||
getDiff(idx) {
|
||||
if(idx == this.allHistory.length - 1) {
|
||||
return this.allHistory[this.allHistory.length - 1].content;
|
||||
}
|
||||
|
||||
// let r = Diff.diffChars(this.allHistory[idx - 1].content.replace(/(<([^>]+)>)/gi, ""), this.allHistory[idx].content.replace(/(<([^>]+)>)/gi, ""));
|
||||
let fragment = document.createElement('div');
|
||||
r.forEach((part) => {
|
||||
// green for additions, red for deletions
|
||||
// grey for common parts
|
||||
const color = part.added ? 'green' :
|
||||
part.removed ? 'red' : 'grey';
|
||||
let span = document.createElement('span');
|
||||
span.style.color = color;
|
||||
console.log(part.value, part.value.length)
|
||||
if(part.added) {
|
||||
let trimmed = part.value.trim();
|
||||
if(!trimmed.length) {
|
||||
span.appendChild(document.createTextNode('·'));
|
||||
} else {
|
||||
span.appendChild(document.createTextNode(part.value));
|
||||
}
|
||||
} else {
|
||||
span.appendChild(document.createTextNode(part.value));
|
||||
}
|
||||
fragment.appendChild(span);
|
||||
});
|
||||
return fragment.innerHTML;
|
||||
},
|
||||
|
||||
formatTime(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div class="list-group-item border-left-0 border-right-0 px-3">
|
||||
<div class="ph-item border-0 p-0 m-0 align-items-center">
|
||||
<div class="p-0 mb-0" style="flex: unset">
|
||||
<div class="ph-avatar" style="min-width: 40px !important;width:40px !important;height:40px;"></div>
|
||||
</div>
|
||||
<div class="ph-col-9 mb-0">
|
||||
<div class="ph-row">
|
||||
<div class="ph-col-12"></div>
|
||||
<div class="ph-col-12"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
243
resources/assets/components/partials/post/MediaContainer.vue
Normal file
243
resources/assets/components/partials/post/MediaContainer.vue
Normal file
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<div class="feed-media-container bg-black">
|
||||
<div class="text-muted" style="max-height: 400px;">
|
||||
<div>
|
||||
<div v-if="post.pf_type === 'photo'">
|
||||
<div v-if="post.sensitive == true" class="content-label-wrapper">
|
||||
<div class="text-light content-label">
|
||||
<p class="text-center">
|
||||
<i class="far fa-eye-slash fa-2x"></i>
|
||||
</p>
|
||||
<p class="h4 font-weight-bold text-center">
|
||||
{{ $t('common.sensitiveContent') }}
|
||||
</p>
|
||||
<p class="text-center py-2 content-label-text">
|
||||
{{ post.spoiler_text ? post.spoiler_text : $t('common.sensitiveContentWarning') }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<button class="btn btn-outline-light btn-block btn-sm font-weight-bold" @click="toggleContentWarning()">See Post</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<blur-hash-image
|
||||
width="32"
|
||||
height="32"
|
||||
:punch="1"
|
||||
class="blurhash-wrapper"
|
||||
:hash="post.media_attachments[0].blurhash"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="content-label-wrapper">
|
||||
<blur-hash-image
|
||||
:key="key"
|
||||
width="32"
|
||||
height="32"
|
||||
:punch="1"
|
||||
:hash="post.media_attachments[0].blurhash"
|
||||
:src="post.media_attachments[0].url"
|
||||
class="blurhash-wrapper"
|
||||
/>
|
||||
|
||||
<p v-if="!post.sensitive && sensitive"
|
||||
@click="post.sensitive = true"
|
||||
style="
|
||||
margin-top: 0;
|
||||
padding: 10px;
|
||||
color: #000;
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255,.5);
|
||||
">
|
||||
<i class="fas fa-eye-slash fa-lg"></i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div v-else-if="post.pf_type === 'photo:album'">
|
||||
<img :src="media[mediaIndex].url" style="width: 100%;height: 500px;object-fit: contain;">
|
||||
|
||||
<div class="d-flex mt-3 justify-content-center">
|
||||
<div
|
||||
v-for="(thumb, index) in media"
|
||||
class="mr-2 border rounded p-1"
|
||||
:class="[ index === mediaIndex ? 'border-light' : 'border-dark' ]"
|
||||
@click="mediaIndex = index">
|
||||
<img :src="thumb.preview_url" width="60" height="40" style="object-fit:cover;">
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- <photo-album-presenter :status="post" v-on:togglecw="post.sensitive = false"/> -->
|
||||
|
||||
<!-- <video-presenter v-else-if="post.pf_type === 'video'" :status="post" v-on:togglecw="post.sensitive = false" /> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
post: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
user: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
media: {
|
||||
type: Array
|
||||
},
|
||||
|
||||
showArrows: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
shortcuts: undefined,
|
||||
sensitive: false,
|
||||
mediaIndex: 0
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initShortcuts();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keyup', this.shortcuts);
|
||||
},
|
||||
|
||||
methods: {
|
||||
navPrev() {
|
||||
// event.currentTarget.blur();
|
||||
if(this.mediaIndex == 0) {
|
||||
this.loading = true;
|
||||
axios.get('/api/v1/accounts/' + this.profile.id + '/statuses', {
|
||||
params: {
|
||||
limit: 1,
|
||||
max_id: this.post.id
|
||||
}
|
||||
}).then(res => {
|
||||
if(!res.data.length) {
|
||||
this.mediaIndex = this.media.length - 1;
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.$emit('navigate', res.data[0]);
|
||||
this.mediaIndex = 0;
|
||||
// this.post = res.data[0];
|
||||
// this.media = this.post.media_attachments;
|
||||
// this.fetchState(this.post.account.username, this.post.id);
|
||||
// this.loading = false;
|
||||
let url = window.location.origin + `/@${this.post.account.username}/post/${this.post.id}`;
|
||||
history.pushState(null, null, url);
|
||||
}).catch(err => {
|
||||
this.mediaIndex = this.media.length - 1;
|
||||
this.loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.mediaIndex--;
|
||||
},
|
||||
|
||||
navNext() {
|
||||
// event.currentTarget.blur();
|
||||
if(this.mediaIndex == this.media.length - 1) {
|
||||
this.loading = true;
|
||||
axios.get('/api/v1/accounts/' + this.profile.id + '/statuses', {
|
||||
params: {
|
||||
limit: 1,
|
||||
min_id: this.post.id
|
||||
}
|
||||
}).then(res => {
|
||||
if(!res.data.length) {
|
||||
this.mediaIndex = 0;
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.$emit('navigate', res.data[0]);
|
||||
this.mediaIndex = 0;
|
||||
// this.post = res.data[0];
|
||||
// this.media = this.post.media_attachments;
|
||||
// this.fetchState(this.post.account.username, this.post.id);
|
||||
// this.loading = false;
|
||||
let url = window.location.origin + `/@${this.post.account.username}/post/${this.post.id}`;
|
||||
history.pushState(null, null, url);
|
||||
}).catch(err => {
|
||||
this.mediaIndex = 0;
|
||||
this.loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.mediaIndex++;
|
||||
},
|
||||
|
||||
initShortcuts() {
|
||||
this.shortcuts = document.addEventListener('keyup', event => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
this.navPrev();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
this.navNext();
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.feed-media-container {
|
||||
|
||||
.blurhash-wrapper {
|
||||
img {
|
||||
border-radius:15px;
|
||||
max-height: 400px;
|
||||
object-fit: contain;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border-radius: 15px;
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-label-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-label {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
z-index: 2;
|
||||
border-radius: 15px;
|
||||
background: rgba(0, 0, 0, 0.2)
|
||||
}
|
||||
}
|
||||
</style>
|
222
resources/assets/components/partials/post/PostContent.vue
Normal file
222
resources/assets/components/partials/post/PostContent.vue
Normal file
|
@ -0,0 +1,222 @@
|
|||
<template>
|
||||
<div class="timeline-status-component-content">
|
||||
<div v-if="status.pf_type === 'poll'" class="postPresenterContainer" style="background: #000;">
|
||||
</div>
|
||||
|
||||
<div v-else-if="!fixedHeight" class="postPresenterContainer" style="background: #000;">
|
||||
<div v-if="status.pf_type === 'photo'" class="w-100">
|
||||
<photo-presenter
|
||||
:status="status"
|
||||
v-on:lightbox="toggleLightbox"
|
||||
v-on:togglecw="status.sensitive = false"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video'" class="w-100">
|
||||
<video-presenter :status="status" v-on:togglecw="status.sensitive = false"></video-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
<photo-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="status.sensitive = false"></photo-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
|
||||
<video-album-presenter :status="status" v-on:togglecw="status.sensitive = false"></video-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
|
||||
<mixed-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="status.sensitive = false"></mixed-album-presenter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="card-body p-0">
|
||||
<div v-if="status.pf_type === 'photo'" :class="{ fixedHeight: fixedHeight }">
|
||||
<div v-if="status.sensitive == true" class="content-label-wrapper">
|
||||
<div class="text-light content-label">
|
||||
<p class="text-center">
|
||||
<i class="far fa-eye-slash fa-2x"></i>
|
||||
</p>
|
||||
<p class="h4 font-weight-bold text-center">
|
||||
{{ $t('common.sensitiveContent') }}
|
||||
</p>
|
||||
<p class="text-center py-2 content-label-text">
|
||||
{{ status.spoiler_text ? status.spoiler_text : $t('common.sensitiveContentWarning') }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<button class="btn btn-outline-light btn-block btn-sm font-weight-bold" @click="toggleContentWarning()">See Post</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<blur-hash-image
|
||||
width="32"
|
||||
height="32"
|
||||
:punch="1"
|
||||
class="blurhash-wrapper"
|
||||
:hash="status.media_attachments[0].blurhash"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
@click.prevent="toggleLightbox"
|
||||
class="content-label-wrapper"
|
||||
style="position: relative;width:100%;height: 400px;overflow: hidden;z-index:1"
|
||||
>
|
||||
|
||||
<img
|
||||
:src="status.media_attachments[0].url"
|
||||
style="position: absolute;width: 105%;height: 410px;object-fit: cover;z-index: 1;top:0;left:0;filter: brightness(0.35) blur(6px);margin:-5px;">
|
||||
|
||||
<!-- <blur-hash-canvas
|
||||
v-if="status.media_attachments[0].blurhash && status.media_attachments[0].blurhash != 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay'"
|
||||
:key="key"
|
||||
width="32"
|
||||
height="32"
|
||||
:punch="1"
|
||||
:hash="status.media_attachments[0].blurhash"
|
||||
style="position: absolute;width: 105%;height: 410px;object-fit: cover;z-index: 1;top:0;left:0;filter: brightness(0.35);"
|
||||
/> -->
|
||||
|
||||
<blur-hash-image
|
||||
:key="key"
|
||||
width="32"
|
||||
height="32"
|
||||
:punch="1"
|
||||
:hash="status.media_attachments[0].blurhash"
|
||||
:src="status.media_attachments[0].url"
|
||||
class="blurhash-wrapper"
|
||||
:alt="status.media_attachments[0].description"
|
||||
:title="status.media_attachments[0].description"
|
||||
style="width: 100%;position: absolute;z-index:9;top:0:left:0"
|
||||
/>
|
||||
|
||||
<p v-if="!status.sensitive && sensitive"
|
||||
@click="status.sensitive = true"
|
||||
style="
|
||||
margin-top: 0;
|
||||
padding: 10px;
|
||||
color: #000;
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255,.5);
|
||||
">
|
||||
<i class="fas fa-eye-slash fa-lg"></i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="status.pf_type === 'video'">
|
||||
<div v-if="status.sensitive == true" class="content-label-wrapper">
|
||||
<div class="text-light content-label">
|
||||
<p class="text-center">
|
||||
<i class="far fa-eye-slash fa-2x"></i>
|
||||
</p>
|
||||
<p class="h4 font-weight-bold text-center">
|
||||
Sensitive Content
|
||||
</p>
|
||||
<p class="text-center py-2 content-label-text">
|
||||
{{ status.spoiler_text ? status.spoiler_text : 'This post may contain sensitive content.'}}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<button @click="status.sensitive = false" class="btn btn-outline-light btn-block btn-sm font-weight-bold">See Post</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<video v-else class="card-img-top shadow" :class="{ fixedHeight: fixedHeight }" style="border-radius:15px;object-fit: contain;background-color: #000;" controls :poster="getPoster(status)">
|
||||
<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="card-img-top shadow" style="border-radius: 15px;">
|
||||
<photo-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="toggleContentWarning()" style="border-radius:15px !important;object-fit: contain;background-color: #000;overflow: hidden;" :class="{ fixedHeight: fixedHeight }"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="card-img-top shadow" style="border-radius: 15px;">
|
||||
<mixed-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="status.sensitive = false" style="border-radius:15px !important;object-fit: contain;background-color: #000;overflow: hidden;align-items:center" :class="{ fixedHeight: fixedHeight }"></mixed-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'text'"></div>
|
||||
|
||||
<div v-else class="bg-light rounded-lg d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<div>
|
||||
<p class="text-center">
|
||||
<i class="fas fa-exclamation-triangle fa-4x"></i>
|
||||
</p>
|
||||
|
||||
<p class="lead text-center mb-0">
|
||||
Cannot display post
|
||||
</p>
|
||||
|
||||
<p class="small text-center mb-0">
|
||||
<!-- <a class="font-weight-bold primary" href="#">Report issue</a> -->
|
||||
{{status.pf_type}}:{{status.id}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.content && !status.sensitive"
|
||||
class="card-body status-text"
|
||||
:class="[ status.pf_type === 'text' ? 'py-0' : 'pb-0']">
|
||||
<p>
|
||||
<read-more :status="status" :cursor-limit="300"/>
|
||||
</p>
|
||||
<!-- <p v-html="status.content_text || status.content">
|
||||
</p> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import BigPicture from 'bigpicture';
|
||||
import ReadMore from './ReadMore.vue';
|
||||
|
||||
export default {
|
||||
props: ['status'],
|
||||
|
||||
components: {
|
||||
"read-more": ReadMore,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
key: 1,
|
||||
sensitive: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
fixedHeight: {
|
||||
get() {
|
||||
return this.$store.state.fixedHeight == true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleLightbox(e) {
|
||||
BigPicture({
|
||||
el: e.target
|
||||
})
|
||||
},
|
||||
|
||||
toggleContentWarning() {
|
||||
this.key++;
|
||||
this.sensitive = true;
|
||||
this.status.sensitive = !this.status.sensitive;
|
||||
},
|
||||
|
||||
getPoster(status) {
|
||||
let url = status.media_attachments[0].preview_url;
|
||||
if(url.endsWith('no-preview.jpg') || url.endsWith('no-preview.png')) {
|
||||
return;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
592
resources/assets/components/partials/post/PostEditModal.vue
Normal file
592
resources/assets/components/partials/post/PostEditModal.vue
Normal file
|
@ -0,0 +1,592 @@
|
|||
<template>
|
||||
<b-modal
|
||||
centered
|
||||
v-model="isOpen"
|
||||
body-class="p-0"
|
||||
footer-class="d-flex justify-content-between align-items-center">
|
||||
<template #modal-header="{ close }">
|
||||
<div class="d-flex flex-grow-1 justify-content-between align-items-center">
|
||||
<span style="width:40px;"></span>
|
||||
<h5 class="font-weight-bold mb-0">Edit Post</h5>
|
||||
<b-button size="sm" variant="link" @click="close()">
|
||||
<i class="far fa-times text-dark fa-lg"></i>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<b-card
|
||||
v-if="isLoading"
|
||||
no-body
|
||||
flush
|
||||
class="shadow-none p-0">
|
||||
<b-card-body style="min-height:300px" class="d-flex align-items-center justify-content-center">
|
||||
<div class="d-flex justify-content-center align-items-center flex-column" style="gap: 0.4rem;">
|
||||
<b-spinner variant="primary" />
|
||||
<p class="small mb-0 font-weight-lighter">Loading Post...</p>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
v-else-if="!isLoading && isOpen && status && status.id"
|
||||
no-body
|
||||
flush
|
||||
class="shadow-none p-0">
|
||||
<b-card-header header-tag="nav">
|
||||
<b-nav tabs fill card-header>
|
||||
<b-nav-item :active="tabIndex === 0" @click="toggleTab(0)">Caption</b-nav-item>
|
||||
<b-nav-item :active="tabIndex === 1" @click="toggleTab(1)">Media</b-nav-item>
|
||||
<!-- <b-nav-item :active="tabIndex === 2" @click="toggleTab(2)">Audience</b-nav-item> -->
|
||||
<b-nav-item :active="tabIndex === 4" @click="toggleTab(3)">Other</b-nav-item>
|
||||
</b-nav>
|
||||
</b-card-header>
|
||||
<b-card-body style="min-height:300px">
|
||||
<template v-if="tabIndex === 0">
|
||||
<p class="font-weight-bold small">Caption</p>
|
||||
<div class="media mb-0">
|
||||
<div class="media-body">
|
||||
<div class="form-group">
|
||||
<label class="font-weight-bold text-muted small d-none">Caption</label>
|
||||
<vue-tribute :options="tributeSettings">
|
||||
<textarea
|
||||
class="form-control border-0 rounded-0 no-focus"
|
||||
rows="4"
|
||||
placeholder="Write a caption..."
|
||||
v-model="fields.caption"
|
||||
:maxlength="config.uploader.max_caption_length"
|
||||
v-on:keyup="composeTextLength = fields.caption.length"></textarea>
|
||||
</vue-tribute>
|
||||
<p class="help-text small text-right text-muted mb-0">{{composeTextLength}}/{{config.uploader.max_caption_length}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="font-weight-bold small">Sensitive/NSFW</p>
|
||||
<div class="border py-2 px-3 bg-light rounded">
|
||||
<b-form-checkbox v-model="fields.sensitive" name="check-button" switch style="font-weight:300">
|
||||
<span class="ml-1 small">Contains spoilers, sensitive or nsfw content</span>
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<div v-if="fields.sensitive" class="form-group mt-3">
|
||||
<label class="font-weight-bold small">Content Warning</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
rows="2"
|
||||
placeholder="Add an optional spoiler/content warning..."
|
||||
:maxlength="140"
|
||||
v-model="fields.spoiler_text"></textarea>
|
||||
<p class="help-text small text-right text-muted mb-0">{{fields.spoiler_text ? fields.spoiler_text.length : 0}}/140</p>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tabIndex === 1">
|
||||
<div class="list-group">
|
||||
<div
|
||||
class="list-group-item"
|
||||
v-for="(media, idx) in fields.media"
|
||||
:key="'edm:' + media.id + ':' + idx">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<template v-if="media.type === 'image'">
|
||||
<img
|
||||
:src="media.url"
|
||||
width="40"
|
||||
height="40"
|
||||
style="object-fit: cover;"
|
||||
class="bg-light rounded cursor-pointer"
|
||||
@click="toggleLightbox"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<p class="d-none d-lg-block mb-0"><span class="small font-weight-light">{{ media.mime }}</span></p>
|
||||
|
||||
<button
|
||||
class="btn btn-sm font-weight-bold rounded-pill px-4"
|
||||
style="font-size: 13px"
|
||||
:class="[ media.description && media.description.length ? 'btn-success' : 'btn-outline-muted']"
|
||||
@click.prevent="handleAddAltText(idx)"
|
||||
>
|
||||
{{ media.description && media.description.length ? 'Edit Alt Text' : 'Add Alt Text' }}
|
||||
</button>
|
||||
|
||||
<div v-if="fields.media && fields.media.length > 1" class="btn-group">
|
||||
<a
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
href="#"
|
||||
:disabled="idx === 0"
|
||||
:class="{ disabled: idx === 0}"
|
||||
@click.prevent="toggleMediaOrder('prev', idx)">
|
||||
<i class="fas fa-arrow-alt-up"></i>
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
href="#"
|
||||
:disabled="idx === fields.media.length - 1"
|
||||
:class="{ disabled: idx === fields.media.length - 1}"
|
||||
@click.prevent="toggleMediaOrder('next', idx)">
|
||||
<i class="fas fa-arrow-alt-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
v-if="fields.media && fields.media.length && fields.media.length > 1"
|
||||
@click.prevent="removeMedia(idx)">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<template v-if="altTextEditIndex === idx">
|
||||
<div class="form-group mt-1">
|
||||
<label class="font-weight-bold small">Alt Text</label>
|
||||
<b-form-textarea
|
||||
v-model="media.description"
|
||||
placeholder="Describe your image for the visually impaired..."
|
||||
rows="3"
|
||||
max-rows="6"
|
||||
@input="handleAltTextUpdate(idx)"
|
||||
></b-form-textarea>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a class="font-weight-bold small text-muted" href="#" @click.prevent="altTextEditIndex = undefined">Close</a>
|
||||
<p class="help-text small mb-0">
|
||||
{{ fields.media[idx].description ? fields.media[idx].description.length : 0 }}/{{config.uploader.max_altext_length}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <template v-else-if="tabIndex === 2">
|
||||
<p class="font-weight-bold small">Audience</p>
|
||||
|
||||
<div class="list-group">
|
||||
<div
|
||||
v-if="!status.account.locked"
|
||||
class="list-group-item font-weight-bold cursor-pointer"
|
||||
:class="{ 'text-primary': fields.visibility == 'public' }"
|
||||
@click="toggleVisibility('public')">
|
||||
Public
|
||||
<i v-if="fields.visibility == 'public'" class="far fa-check-circle ml-1"></i>
|
||||
</div>
|
||||
<div
|
||||
v-if="!status.account.locked"
|
||||
class="list-group-item font-weight-bold cursor-pointer"
|
||||
:class="{ 'text-primary': fields.visibility == 'unlisted' }"
|
||||
@click="toggleVisibility('unlisted')">
|
||||
Unlisted
|
||||
<i v-if="fields.visibility == 'unlisted'" class="far fa-check-circle ml-1"></i>
|
||||
</div>
|
||||
<div
|
||||
class="list-group-item font-weight-bold cursor-pointer"
|
||||
:class="{ 'text-primary': fields.visibility == 'private' }"
|
||||
@click="toggleVisibility('private')">
|
||||
Followers Only
|
||||
<i v-if="fields.visibility == 'private'" class="far fa-check-circle ml-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template> -->
|
||||
|
||||
<template v-else-if="tabIndex === 3">
|
||||
<p class="font-weight-bold small">Location</p>
|
||||
<autocomplete
|
||||
:search="locationSearch"
|
||||
placeholder="Search locations ..."
|
||||
aria-label="Search locations ..."
|
||||
:get-result-value="getResultValue"
|
||||
@submit="onSubmitLocation"
|
||||
>
|
||||
</autocomplete>
|
||||
|
||||
<div v-if="fields.location && fields.location.hasOwnProperty('id')" class="mt-3 border rounded p-3 d-flex justify-content-between">
|
||||
<p class="font-weight-bold mb-0">
|
||||
{{ fields.location.name }}, {{ fields.location.country}}
|
||||
</p>
|
||||
<button class="btn btn-link text-danger m-0 p-0" @click.prevent="clearLocation">
|
||||
<i class="far fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<template
|
||||
#modal-footer="{ ok, cancel, hide }">
|
||||
<b-button class="rounded-pill px-3 font-weight-bold" variant="outline-muted" @click="cancel()">
|
||||
Cancel
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
class="rounded-pill font-weight-bold"
|
||||
variant="primary"
|
||||
style="min-width: 195px"
|
||||
@click="handleSave"
|
||||
:disabled="!canSave">
|
||||
<template v-if="isSubmitting">
|
||||
<b-spinner small />
|
||||
</template>
|
||||
<template v-else>
|
||||
Save Updates
|
||||
</template>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Autocomplete from '@trevoreyre/autocomplete-vue';
|
||||
import BigPicture from 'bigpicture';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Autocomplete,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: window.App.config,
|
||||
status: undefined,
|
||||
isLoading: true,
|
||||
isOpen: false,
|
||||
isSubmitting: false,
|
||||
tabIndex: 0,
|
||||
canEdit: false,
|
||||
composeTextLength: 0,
|
||||
canSave: false,
|
||||
originalFields: {
|
||||
caption: undefined,
|
||||
visibility: undefined,
|
||||
sensitive: undefined,
|
||||
location: undefined,
|
||||
spoiler_text: undefined,
|
||||
media: [],
|
||||
},
|
||||
fields: {
|
||||
caption: undefined,
|
||||
visibility: undefined,
|
||||
sensitive: undefined,
|
||||
location: undefined,
|
||||
spoiler_text: undefined,
|
||||
media: [],
|
||||
},
|
||||
medias: undefined,
|
||||
altTextEditIndex: undefined,
|
||||
tributeSettings: {
|
||||
noMatchTemplate: function () { return null; },
|
||||
collection: [
|
||||
{
|
||||
trigger: '@',
|
||||
menuShowMinLength: 2,
|
||||
values: (function (text, cb) {
|
||||
let url = '/api/compose/v0/search/mention';
|
||||
axios.get(url, { params: { q: text }})
|
||||
.then(res => {
|
||||
cb(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
})
|
||||
},
|
||||
{
|
||||
trigger: '#',
|
||||
menuShowMinLength: 2,
|
||||
values: (function (text, cb) {
|
||||
let url = '/api/compose/v0/search/hashtag';
|
||||
axios.get(url, { params: { q: text }})
|
||||
.then(res => {
|
||||
cb(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
fields: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler: function(n, o) {
|
||||
if(!this.canEdit) {
|
||||
return;
|
||||
}
|
||||
this.canSave = this.originalFields !== JSON.stringify(this.fields);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reset() {
|
||||
this.status = undefined;
|
||||
this.tabIndex = 0;
|
||||
this.isOpen = false;
|
||||
this.canEdit = false;
|
||||
this.composeTextLength = 0;
|
||||
this.canSave = false;
|
||||
this.originalFields = {
|
||||
caption: undefined,
|
||||
visibility: undefined,
|
||||
sensitive: undefined,
|
||||
location: undefined,
|
||||
spoiler_text: undefined,
|
||||
media: [],
|
||||
};
|
||||
this.fields = {
|
||||
caption: undefined,
|
||||
visibility: undefined,
|
||||
sensitive: undefined,
|
||||
location: undefined,
|
||||
spoiler_text: undefined,
|
||||
media: [],
|
||||
};
|
||||
this.medias = undefined;
|
||||
this.altTextEditIndex = undefined;
|
||||
this.isSubmitting = false;
|
||||
},
|
||||
|
||||
async show(status) {
|
||||
await axios.get('/api/v1/statuses/' + status.id, {
|
||||
params: {
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.reset();
|
||||
this.init(res.data);
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
}, 500);
|
||||
})
|
||||
},
|
||||
|
||||
init(status) {
|
||||
this.reset();
|
||||
this.originalFields = JSON.stringify({
|
||||
caption: status.content_text,
|
||||
visibility: status.visibility,
|
||||
sensitive: status.sensitive,
|
||||
location: status.place,
|
||||
spoiler_text: status.spoiler_text,
|
||||
media: status.media_attachments
|
||||
})
|
||||
this.fields = {
|
||||
caption: status.content_text,
|
||||
visibility: status.visibility,
|
||||
sensitive: status.sensitive,
|
||||
location: status.place,
|
||||
spoiler_text: status.spoiler_text,
|
||||
media: status.media_attachments
|
||||
}
|
||||
this.status = status;
|
||||
this.medias = status.media_attachments;
|
||||
this.composeTextLength = status.content_text ? status.content_text.length : 0;
|
||||
this.isOpen = true;
|
||||
setTimeout(() => {
|
||||
this.canEdit = true;
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
toggleTab(idx) {
|
||||
this.tabIndex = idx;
|
||||
this.altTextEditIndex = undefined;
|
||||
},
|
||||
|
||||
toggleVisibility(vis) {
|
||||
this.fields.visibility = vis;
|
||||
},
|
||||
|
||||
locationSearch(input) {
|
||||
if (input.length < 1) { return []; }
|
||||
let results = [];
|
||||
return axios.get('/api/compose/v0/search/location', {
|
||||
params: {
|
||||
q: input
|
||||
}
|
||||
}).then(res => {
|
||||
return res.data;
|
||||
});
|
||||
},
|
||||
|
||||
getResultValue(result) {
|
||||
return result.name + ', ' + result.country
|
||||
},
|
||||
|
||||
onSubmitLocation(result) {
|
||||
this.fields.location = result;
|
||||
this.tabIndex = 0;
|
||||
},
|
||||
|
||||
clearLocation() {
|
||||
event.currentTarget.blur();
|
||||
this.fields.location = null;
|
||||
this.tabIndex = 0;
|
||||
},
|
||||
|
||||
handleAltTextUpdate(idx) {
|
||||
if (this.fields.media[idx].description.length == 0) {
|
||||
this.fields.media[idx].description = null;
|
||||
}
|
||||
},
|
||||
|
||||
moveMedia(from, to, arr) {
|
||||
const newArr = [...arr];
|
||||
|
||||
const item = newArr.splice(from, 1)[0];
|
||||
newArr.splice(to, 0, item);
|
||||
|
||||
return newArr;
|
||||
},
|
||||
|
||||
toggleMediaOrder(dir, idx) {
|
||||
if(dir === 'prev') {
|
||||
this.fields.media = this.moveMedia(idx, idx - 1, this.fields.media);
|
||||
}
|
||||
|
||||
if(dir === 'next') {
|
||||
this.fields.media = this.moveMedia(idx, idx + 1, this.fields.media);
|
||||
}
|
||||
},
|
||||
|
||||
toggleLightbox(e) {
|
||||
BigPicture({
|
||||
el: e.target
|
||||
})
|
||||
},
|
||||
|
||||
handleAddAltText(idx) {
|
||||
event.currentTarget.blur();
|
||||
this.altTextEditIndex = idx
|
||||
},
|
||||
|
||||
removeMedia(idx) {
|
||||
swal({
|
||||
title: 'Confirm',
|
||||
text: 'Are you sure you want to remove this media from your post?',
|
||||
buttons: {
|
||||
cancel: "Cancel",
|
||||
confirm: {
|
||||
text: "Confirm Removal",
|
||||
value: "remove",
|
||||
className: "swal-button--danger"
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((val) => {
|
||||
if(val === 'remove') {
|
||||
this.fields.media.splice(idx, 1);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async handleSave() {
|
||||
event.currentTarget.blur();
|
||||
this.canSave = false;
|
||||
this.isSubmitting = true;
|
||||
|
||||
await this.checkMediaUpdates();
|
||||
|
||||
axios.put('/api/v1/statuses/' + this.status.id, {
|
||||
status: this.fields.caption,
|
||||
spoiler_text: this.fields.spoiler_text,
|
||||
sensitive: this.fields.sensitive,
|
||||
media_ids: this.fields.media.map(m => m.id),
|
||||
location: this.fields.location
|
||||
})
|
||||
.then(res => {
|
||||
this.isOpen = false;
|
||||
this.$emit('update', res.data);
|
||||
swal({
|
||||
title: 'Post Updated',
|
||||
text: 'You have successfully updated this post!',
|
||||
icon: 'success',
|
||||
buttons: {
|
||||
close: {
|
||||
text: "Close",
|
||||
value: "close",
|
||||
close: true,
|
||||
className: "swal-button--cancel"
|
||||
},
|
||||
view: {
|
||||
text: "View Post",
|
||||
value: "view",
|
||||
className: "btn-primary"
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((val) => {
|
||||
if(val === 'view') {
|
||||
if(this.$router.currentRoute.name === 'post') {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.$router.push('/i/web/post/' + this.status.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.isSubmitting = false;
|
||||
if(err.response.data.hasOwnProperty('error')) {
|
||||
swal('Error', err.response.data.error, 'error');
|
||||
} else {
|
||||
swal('Error', 'An error occured, please try again later', 'error');
|
||||
}
|
||||
console.log(err);
|
||||
})
|
||||
},
|
||||
|
||||
async checkMediaUpdates() {
|
||||
const cached = JSON.parse(this.originalFields);
|
||||
const medias = JSON.stringify(cached.media);
|
||||
if (medias !== JSON.stringify(this.fields.media)) {
|
||||
await axios.all(this.fields.media.map((media) => this.updateAltText(media)))
|
||||
}
|
||||
},
|
||||
|
||||
async updateAltText(media) {
|
||||
return await axios.put('/api/v1/media/' + media.id, {
|
||||
description: media.description
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
div, p {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-lighter);
|
||||
|
||||
&.active {
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
.slide-fade-leave-active {
|
||||
transition: all .2s cubic-bezier(0.5, 1.0, 0.6, 1.0);
|
||||
}
|
||||
.slide-fade-enter, .slide-fade-leave-to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
99
resources/assets/components/partials/post/ReadMore.vue
Normal file
99
resources/assets/components/partials/post/ReadMore.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<div class="read-more-component" style="word-break: break-word;">
|
||||
<div v-html="content"></div>
|
||||
<!-- <div v-if="status.content.length < 200" v-html="content"></div>
|
||||
<div v-else>
|
||||
<span v-html="content"></span>
|
||||
<a
|
||||
v-if="cursor == 200 || fullContent.length > cursor"
|
||||
class="font-weight-bold text-muted" href="#"
|
||||
style="display: block;white-space: nowrap;"
|
||||
@click.prevent="readMore">
|
||||
<i class="d-none fas fa-caret-down"></i> {{ $t('common.readMore') }}...
|
||||
</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
cursorLimit: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
preRender: undefined,
|
||||
fullContent: null,
|
||||
content: null,
|
||||
cursor: 200
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.rewriteLinks();
|
||||
},
|
||||
|
||||
methods: {
|
||||
readMore() {
|
||||
this.cursor = this.cursor + 200;
|
||||
this.content = this.fullContent.substr(0, this.cursor);
|
||||
},
|
||||
|
||||
rewriteLinks() {
|
||||
let content = this.status.content;
|
||||
let el = document.createElement('div');
|
||||
el.innerHTML = content;
|
||||
el.querySelectorAll('a[class*="hashtag"]')
|
||||
.forEach(elr => {
|
||||
let tag = elr.innerText;
|
||||
if(tag.substr(0, 1) == '#') {
|
||||
tag = tag.substr(1);
|
||||
}
|
||||
elr.removeAttribute('target');
|
||||
elr.setAttribute('href', '/i/web/hashtag/' + tag);
|
||||
})
|
||||
el.querySelectorAll('a:not(.hashtag)[class*="mention"], a:not(.hashtag)[class*="list-slug"]')
|
||||
.forEach(elr => {
|
||||
let name = elr.innerText;
|
||||
if(name.substr(0, 1) == '@') {
|
||||
name = name.substr(1);
|
||||
}
|
||||
if(this.status.account.local == false && !name.includes('@')) {
|
||||
let domain = document.createElement('a');
|
||||
domain.href = elr.getAttribute('href');
|
||||
name = name + '@' + domain.hostname;
|
||||
}
|
||||
elr.removeAttribute('target');
|
||||
elr.setAttribute('href', '/i/web/username/' + name);
|
||||
})
|
||||
this.content = el.outerHTML;
|
||||
|
||||
this.injectCustomEmoji();
|
||||
},
|
||||
|
||||
injectCustomEmoji() {
|
||||
// console.log('inecting custom emoji');
|
||||
// let re = /:\w{1,100}:/g;
|
||||
// let matches = this.status.content.match(re);
|
||||
// console.log(matches);
|
||||
// if(this.status.emojis.length == 0) {
|
||||
// return;
|
||||
// }
|
||||
let self = this;
|
||||
this.status.emojis.forEach(function(emoji) {
|
||||
let img = `<img draggable="false" class="emojione custom-emoji" alt="${emoji.shortcode}" title="${emoji.shortcode}" src="${emoji.url}" data-original="${emoji.url}" data-static="${emoji.static_url}" width="18" height="18" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`;
|
||||
self.content = self.content.replace(`:${emoji.shortcode}:`, img);
|
||||
});
|
||||
// this.content = this.content.replace(':fediverse:', '😅');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,348 @@
|
|||
<template>
|
||||
<div class="profile-hover-card">
|
||||
<div class="profile-hover-card-inner">
|
||||
<div class="d-flex justify-content-between align-items-start" style="max-width: 240px;">
|
||||
<a
|
||||
:href="profile.url"
|
||||
@click.prevent="goToProfile()">
|
||||
<img
|
||||
:src="profile.avatar"
|
||||
width="50"
|
||||
height="50"
|
||||
class="avatar"
|
||||
onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</a>
|
||||
|
||||
<div v-if="user.id == profile.id">
|
||||
<a class="btn btn-outline-primary px-3 py-1 font-weight-bold rounded-pill" href="/settings/home">Edit Profile</a>
|
||||
</div>
|
||||
|
||||
<div v-if="user.id != profile.id && relationship">
|
||||
<button
|
||||
v-if="relationship.following"
|
||||
class="btn btn-outline-primary px-3 py-1 font-weight-bold rounded-pill"
|
||||
:disabled="isLoading"
|
||||
@click="performUnfollow()">
|
||||
<span v-if="isLoading"><b-spinner small /></span>
|
||||
<span v-else>Following</span>
|
||||
</button>
|
||||
<div v-else>
|
||||
<button
|
||||
v-if="!relationship.requested"
|
||||
class="btn btn-primary primary px-3 py-1 font-weight-bold rounded-pill"
|
||||
:disabled="isLoading"
|
||||
@click="performFollow()">
|
||||
<span v-if="isLoading"><b-spinner small /></span>
|
||||
<span v-else>Follow</span>
|
||||
</button>
|
||||
<button v-else class="btn btn-primary primary px-3 py-1 font-weight-bold rounded-pill" disabled>Follow Requested</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="display-name">
|
||||
<a
|
||||
:href="profile.url"
|
||||
@click.prevent="goToProfile()"
|
||||
v-html="getDisplayName()">
|
||||
{{ profile.display_name ? profile.display_name : profile.username }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="username">
|
||||
<a
|
||||
:href="profile.url"
|
||||
class="username-link"
|
||||
@click.prevent="goToProfile()">
|
||||
@{{ getUsername() }}
|
||||
</a>
|
||||
|
||||
<p v-if="user.id != profile.id && relationship && relationship.followed_by" class="username-follows-you">
|
||||
<span>Follows You</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="profile.hasOwnProperty('pronouns') && profile.pronouns && profile.pronouns.length"
|
||||
class="pronouns">
|
||||
{{ profile.pronouns.join(', ') }}
|
||||
</p>
|
||||
|
||||
|
||||
<p class="bio" v-html="bio"></p>
|
||||
|
||||
<p class="stats">
|
||||
<span class="stats-following">
|
||||
<span class="following-count">{{ formatCount(profile.following_count) }}</span> Following
|
||||
</span>
|
||||
<span class="stats-followers">
|
||||
<span class="followers-count">{{ formatCount(profile.followers_count) }}</span> Followers
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import ReadMore from './../post/ReadMore.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
// relationship: {
|
||||
// type: Object
|
||||
// }
|
||||
},
|
||||
|
||||
components: {
|
||||
ReadMore
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
user: window._sharedData.user,
|
||||
bio: undefined,
|
||||
isLoading: false,
|
||||
relationship: undefined
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.rewriteLinks();
|
||||
this.relationship = this.$store.getters.getRelationship(this.profile.id);
|
||||
if(!this.relationship && this.profile.id != this.user.id) {
|
||||
axios.get('/api/pixelfed/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.profile.id
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.relationship = res.data[0];
|
||||
this.$store.commit('updateRelationship', res.data);
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getCustomEmoji'
|
||||
])
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDisplayName() {
|
||||
let self = this;
|
||||
let profile = this.profile;
|
||||
let dn = profile.display_name;
|
||||
if(!dn) {
|
||||
return profile.username;
|
||||
}
|
||||
|
||||
if(dn.includes(':')) {
|
||||
let re = /(<a?)?:\w+:(\d{18}>)?/g;
|
||||
let un = dn.replaceAll(re, function(em) {
|
||||
let shortcode = em.slice(1, em.length - 1);
|
||||
let emoji = self.getCustomEmoji.filter(e => {
|
||||
return e.shortcode == shortcode;
|
||||
});
|
||||
return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
|
||||
});
|
||||
return un;
|
||||
} else {
|
||||
return dn;
|
||||
}
|
||||
},
|
||||
|
||||
getUsername() {
|
||||
let profile = this.profile;
|
||||
// if(profile.hasOwnProperty('local') && profile.local) {
|
||||
// return profile.acct + '@' + window.location.hostname;
|
||||
// }
|
||||
return profile.acct;
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
goToProfile() {
|
||||
this.$router.push({
|
||||
name: 'profile',
|
||||
path: `/i/web/profile/${this.profile.id}`,
|
||||
params: {
|
||||
id: this.profile.id,
|
||||
cachedProfile: this.profile,
|
||||
cachedUser: this.user
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
rewriteLinks() {
|
||||
let content = this.profile.note;
|
||||
let el = document.createElement('div');
|
||||
el.innerHTML = content;
|
||||
el.querySelectorAll('a[class*="hashtag"]')
|
||||
.forEach(elr => {
|
||||
let tag = elr.innerText;
|
||||
if(tag.substr(0, 1) == '#') {
|
||||
tag = tag.substr(1);
|
||||
}
|
||||
elr.removeAttribute('target');
|
||||
elr.setAttribute('href', '/i/web/hashtag/' + tag);
|
||||
})
|
||||
el.querySelectorAll('a:not(.hashtag)[class*="mention"], a:not(.hashtag)[class*="list-slug"]')
|
||||
.forEach(elr => {
|
||||
let name = elr.innerText;
|
||||
if(name.substr(0, 1) == '@') {
|
||||
name = name.substr(1);
|
||||
}
|
||||
if(this.profile.local == false && !name.includes('@')) {
|
||||
let domain = document.createElement('a');
|
||||
domain.href = this.profile.url;
|
||||
name = name + '@' + domain.hostname;
|
||||
}
|
||||
elr.removeAttribute('target');
|
||||
elr.setAttribute('href', '/i/web/username/' + name);
|
||||
})
|
||||
this.bio = el.outerHTML;
|
||||
},
|
||||
|
||||
performFollow() {
|
||||
this.isLoading = true;
|
||||
this.$emit('follow');
|
||||
setTimeout(() => {
|
||||
this.relationship.following = true;
|
||||
this.isLoading = false;
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
performUnfollow() {
|
||||
this.isLoading = true;
|
||||
this.$emit('unfollow');
|
||||
setTimeout(() => {
|
||||
this.relationship.following = false;
|
||||
this.isLoading = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-hover-card {
|
||||
display: block;
|
||||
width: 300px;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
|
||||
.avatar {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%) !important;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
max-width: 240px;
|
||||
word-break: break-word;
|
||||
font-weight: 800;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 2px;
|
||||
line-height: 0.8;
|
||||
font-size: 16px;
|
||||
font-weight: 800 !important;
|
||||
user-select: all;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
|
||||
a {
|
||||
color: var(--body-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
max-width: 240px;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.6rem;
|
||||
user-select: all;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
|
||||
&-link {
|
||||
color: var(--text-lighter);
|
||||
text-decoration: none;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&-follows-you {
|
||||
margin: 4px 0;
|
||||
|
||||
span {
|
||||
color: var(--dropdown-item-color);
|
||||
background-color: var(--comment-bg);
|
||||
font-size: 12px;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
line-height: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.pronouns {
|
||||
font-size: 11px;
|
||||
color: #9CA3AF;
|
||||
margin-top: -0.8rem;
|
||||
margin-bottom: 0.6rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bio {
|
||||
max-width: 240px;
|
||||
max-height: 60px;
|
||||
word-break: break-word;
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
font-size: 12px;
|
||||
color: var(--body-color);
|
||||
|
||||
.invisible {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
color: var(--body-color);
|
||||
|
||||
.stats-following {
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
|
||||
.following-count,
|
||||
.followers-count {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
&.rounded-pill {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
821
resources/assets/components/partials/profile/ProfileSidebar.vue
Normal file
821
resources/assets/components/partials/profile/ProfileSidebar.vue
Normal file
|
@ -0,0 +1,821 @@
|
|||
<template>
|
||||
<div class="profile-sidebar-component">
|
||||
<div>
|
||||
<div class="d-block d-md-none">
|
||||
<div class="media user-card user-select-none">
|
||||
<div style="position: relative;">
|
||||
<img :src="profile.avatar" class="avatar shadow cursor-pointer" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<p class="display-name" v-html="getDisplayName()"></p>
|
||||
<p class="username" :class="{ remote: !profile.local }">
|
||||
<a v-if="!profile.local" :href="profile.url" class="primary">@{{ profile.acct }}</a>
|
||||
<span v-else>@{{ profile.acct }}</span>
|
||||
<span v-if="profile.locked">
|
||||
<i class="fal fa-lock ml-1 fa-sm text-lighter"></i>
|
||||
</span>
|
||||
</p>
|
||||
<div class="stats">
|
||||
<div class="stats-posts" @click="toggleTab('index')">
|
||||
<div class="posts-count">{{ formatCount(profile.statuses_count) }}</div>
|
||||
<div class="stats-label">
|
||||
{{ $t('profile.posts') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-followers" @click="toggleTab('followers')">
|
||||
<div class="followers-count">{{ formatCount(profile.followers_count) }}</div>
|
||||
<div class="stats-label">
|
||||
{{ $t('profile.followers') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-following" @click="toggleTab('following')">
|
||||
<div class="following-count">{{ formatCount(profile.following_count) }}</div>
|
||||
<div class="stats-label">
|
||||
{{ $t('profile.following') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-flex justify-content-between align-items-center">
|
||||
<button class="btn btn-link" @click="goBack()">
|
||||
<i class="far fa-chevron-left fa-lg text-lighter"></i>
|
||||
</button>
|
||||
<div>
|
||||
<img :src="getAvatar()" class="avatar img-fluid shadow border" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
<p v-if="profile.is_admin" class="text-right" style="margin-top: -30px;"><span class="admin-label">Admin</span></p>
|
||||
</div>
|
||||
<!-- <button class="btn btn-link">
|
||||
<i class="far fa-lg fa-cog text-lighter"></i>
|
||||
</button> -->
|
||||
|
||||
<b-dropdown
|
||||
variant="link"
|
||||
right
|
||||
no-caret>
|
||||
<template #button-content>
|
||||
<i class="far fa-lg fa-cog text-lighter"></i>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item v-if="profile.local" href="#" link-class="font-weight-bold" @click.prevent="goToOldProfile()">View in old UI</b-dropdown-item>
|
||||
<b-dropdown-item href="#" link-class="font-weight-bold" @click.prevent="copyTextToClipboard(profile.url)">Copy Link</b-dropdown-item>
|
||||
|
||||
|
||||
<b-dropdown-item v-if="profile.local" :href="'/users/' + profile.username + '.atom'" link-class="font-weight-bold">Atom feed</b-dropdown-item>
|
||||
|
||||
<div v-if="profile.id == user.id">
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item href="/settings/home" link-class="font-weight-bold">
|
||||
<i class="far fa-cog mr-1"></i> Settings
|
||||
</b-dropdown-item>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<b-dropdown-item v-if="!profile.local" :href="profile.url" link-class="font-weight-bold">View Remote Profile</b-dropdown-item>
|
||||
<b-dropdown-item :href="'/i/web/direct/thread/' + profile.id" link-class="font-weight-bold">Direct Message</b-dropdown-item>
|
||||
</div>
|
||||
|
||||
<div v-if="profile.id !== user.id">
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
|
||||
<b-dropdown-item link-class="font-weight-bold" @click="handleMute()">
|
||||
{{ relationship.muting ? 'Unmute' : 'Mute' }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item link-class="font-weight-bold" @click="handleBlock()">
|
||||
{{ relationship.blocking ? 'Unblock' : 'Block' }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item :href="'/i/report?type=user&id=' + profile.id" link-class="text-danger font-weight-bold">Report</b-dropdown-item>
|
||||
</div>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-block text-center">
|
||||
<p v-html="getDisplayName()" class="display-name"></p>
|
||||
|
||||
<p class="username" :class="{ remote: !profile.local }">
|
||||
<a v-if="!profile.local" :href="profile.url" class="primary">@{{ profile.acct }}</a>
|
||||
<span v-else>@{{ profile.acct }}</span>
|
||||
<span v-if="profile.locked">
|
||||
<i class="fal fa-lock ml-1 fa-sm text-lighter"></i>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p v-if="user.id != profile.id && (relationship.followed_by || relationship.muting || relationship.blocking)" class="mt-n3 text-center">
|
||||
<span v-if="relationship.followed_by" class="badge badge-primary p-1">Follows you</span>
|
||||
<span v-if="relationship.muting" class="badge badge-dark p-1 ml-1">Muted</span>
|
||||
<span v-if="relationship.blocking" class="badge badge-danger p-1 ml-1">Blocked</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-block stats py-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<button
|
||||
class="btn btn-link stat-item"
|
||||
@click="toggleTab('index')">
|
||||
<strong :title="profile.statuses_count">{{ formatCount(profile.statuses_count) }}</strong>
|
||||
<span>{{ $t('profile.posts') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-link stat-item"
|
||||
@click="toggleTab('followers')">
|
||||
<strong :title="profile.followers_count">{{ formatCount(profile.followers_count) }}</strong>
|
||||
<span>{{ $t('profile.followers') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-link stat-item"
|
||||
@click="toggleTab('following')">
|
||||
<strong :title="profile.following_count">{{ formatCount(profile.following_count) }}</strong>
|
||||
<span>{{ $t('profile.following') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center mb-3 mb-md-0">
|
||||
<div v-if="user.id === profile.id" style="flex-grow: 1;">
|
||||
<!-- <router-link
|
||||
class="btn btn-light font-weight-bold btn-block follow-btn"
|
||||
to="/i/web/settings">
|
||||
{{ $t('profile.editProfile') }}
|
||||
</router-link> -->
|
||||
<a class="btn btn-light font-weight-bold btn-block follow-btn" href="/settings/home">{{ $t('profile.editProfile') }}</a>
|
||||
<a v-if="!profile.locked" class="btn btn-light font-weight-bold btn-block follow-btn mt-md-n4" href="/i/web/my-portfolio">
|
||||
My Portfolio
|
||||
<span class="badge badge-success ml-1">NEW</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-else-if="profile.locked" style="flex-grow: 1;">
|
||||
<template v-if="!relationship.following && !relationship.requested">
|
||||
<button
|
||||
class="btn btn-primary font-weight-bold btn-block follow-btn"
|
||||
@click="follow"
|
||||
:disabled="relationship.blocking">
|
||||
Request Follow
|
||||
</button>
|
||||
<p v-if="relationship.blocking" class="mt-n4 text-lighter" style="font-size: 11px">You need to unblock this account before you can request to follow.</p>
|
||||
</template>
|
||||
|
||||
<div v-else-if="relationship.requested">
|
||||
<button class="btn btn-primary font-weight-bold btn-block follow-btn" disabled>
|
||||
{{ $t('profile.followRequested') }}
|
||||
</button>
|
||||
|
||||
<p class="small font-weight-bold text-center mt-n4">
|
||||
<a href="#" @click.prevent="cancelFollowRequest()">Cancel Follow Request</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-else-if="relationship.following"
|
||||
class="btn btn-primary font-weight-bold btn-block unfollow-btn"
|
||||
@click="unfollow">
|
||||
{{ $t('profile.unfollow') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else style="flex-grow: 1;">
|
||||
<template v-if="!relationship.following">
|
||||
<button
|
||||
class="btn btn-primary font-weight-bold btn-block follow-btn"
|
||||
@click="follow"
|
||||
:disabled="relationship.blocking">
|
||||
{{ $t('profile.follow') }}
|
||||
</button>
|
||||
<p v-if="relationship.blocking" class="mt-n4 text-lighter" style="font-size: 11px">You need to unblock this account before you can follow.</p>
|
||||
</template>
|
||||
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-primary font-weight-bold btn-block unfollow-btn"
|
||||
@click="unfollow">
|
||||
{{ $t('profile.unfollow') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="d-block d-md-none ml-3">
|
||||
<b-dropdown
|
||||
variant="link"
|
||||
right
|
||||
no-caret>
|
||||
<template #button-content>
|
||||
<i class="far fa-lg fa-cog text-lighter"></i>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item v-if="profile.local" href="#" link-class="font-weight-bold" @click.prevent="goToOldProfile()">View in old UI</b-dropdown-item>
|
||||
<b-dropdown-item href="#" link-class="font-weight-bold" @click.prevent="copyTextToClipboard(profile.url)">Copy Link</b-dropdown-item>
|
||||
|
||||
|
||||
<b-dropdown-item v-if="profile.local" :href="'/users/' + profile.username + '.atom'" link-class="font-weight-bold">Atom feed</b-dropdown-item>
|
||||
|
||||
<div v-if="profile.id == user.id">
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-item href="/settings/home" link-class="font-weight-bold">
|
||||
<i class="far fa-cog mr-1"></i> Settings
|
||||
</b-dropdown-item>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<b-dropdown-item v-if="!profile.local" :href="profile.url" link-class="font-weight-bold">View Remote Profile</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item :href="'/i/web/direct/thread/' + profile.id" link-class="font-weight-bold">Direct Message</b-dropdown-item>
|
||||
</div>
|
||||
|
||||
<div v-if="profile.id !== user.id">
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
|
||||
<b-dropdown-item link-class="font-weight-bold" @click="handleMute()">
|
||||
{{ relationship.muting ? 'Unmute' : 'Mute' }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item link-class="font-weight-bold" @click="handleBlock()">
|
||||
{{ relationship.blocking ? 'Unblock' : 'Block' }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item :href="'/i/report?type=user&id=' + profile.id" link-class="text-danger font-weight-bold">Report</b-dropdown-item>
|
||||
</div>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="profile.note && renderedBio && renderedBio.length" class="bio-wrapper card shadow-none">
|
||||
<div class="card-body">
|
||||
<div class="bio-body">
|
||||
<div v-html="renderedBio"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-block card card-body shadow-none py-2">
|
||||
<p v-if="profile.website" class="small">
|
||||
<span class="text-lighter mr-2">
|
||||
<i class="far fa-link"></i>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<a :href="profile.website" class="font-weight-bold">{{ profile.website }}</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="mb-0 small">
|
||||
<span class="text-lighter mr-2">
|
||||
<i class="far fa-clock"></i>
|
||||
</span>
|
||||
|
||||
<span v-if="profile.local">
|
||||
{{ $t('profile.joined') }} {{ getJoinedDate() }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('profile.joined') }} {{ getJoinedDate() }}
|
||||
|
||||
<span class="float-right primary">
|
||||
<i class="far fa-info-circle" v-b-tooltip.hover title="This user is from a remote server and may have created their account before this date"></i>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-flex sidebar-sitelinks">
|
||||
<a href="/site/about">{{ $t('navmenu.about') }}</a>
|
||||
<router-link to="/i/web/help">{{ $t('navmenu.help') }}</router-link>
|
||||
<router-link to="/i/web/language">{{ $t('navmenu.language') }}</router-link>
|
||||
<a href="/site/terms">{{ $t('navmenu.privacy') }}</a>
|
||||
<a href="/site/terms">{{ $t('navmenu.terms') }}</a>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-block sidebar-attribution">
|
||||
<a href="https://pixelfed.org" class="font-weight-bold">Powered by Pixelfed</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-modal
|
||||
ref="fullBio"
|
||||
centered
|
||||
hide-footer
|
||||
ok-only
|
||||
ok-title="Close"
|
||||
ok-variant="light"
|
||||
:scrollable="true"
|
||||
body-class="p-md-5"
|
||||
title="Bio"
|
||||
>
|
||||
<div v-html="profile.note"></div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
relationship: {
|
||||
type: Object,
|
||||
default: (function() {
|
||||
return {
|
||||
following: false,
|
||||
followed_by: false
|
||||
};
|
||||
})
|
||||
},
|
||||
|
||||
user: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getCustomEmoji'
|
||||
])
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
'renderedBio': ''
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.setBio();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDisplayName() {
|
||||
let self = this;
|
||||
let profile = this.profile;
|
||||
let dn = profile.display_name;
|
||||
if(!dn) {
|
||||
return profile.username;
|
||||
}
|
||||
if(dn.includes(':')) {
|
||||
// let re = /:(::|[^:\n])+:/g;
|
||||
let re = /(<a?)?:\w+:(\d{18}>)?/g;
|
||||
let un = dn.replaceAll(re, function(em) {
|
||||
let shortcode = em.slice(1, em.length - 1);
|
||||
let emoji = self.getCustomEmoji.filter(e => {
|
||||
return e.shortcode == shortcode;
|
||||
});
|
||||
return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
|
||||
});
|
||||
return un;
|
||||
} else {
|
||||
return dn;
|
||||
}
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$emit('back');
|
||||
},
|
||||
|
||||
showFullBio() {
|
||||
this.$refs.fullBio.show();
|
||||
},
|
||||
|
||||
toggleTab(tab) {
|
||||
event.currentTarget.blur();
|
||||
if(['followers', 'following'].includes(tab)) {
|
||||
this.$router.push('/i/web/profile/' + this.profile.id + '/' + tab);
|
||||
return;
|
||||
} else {
|
||||
this.$emit('toggletab', tab);
|
||||
}
|
||||
},
|
||||
|
||||
getJoinedDate() {
|
||||
let d = new Date(this.profile.created_at);
|
||||
let month = new Intl.DateTimeFormat("en-US", { month: "long" }).format(d);
|
||||
let year = d.getFullYear();
|
||||
return `${month} ${year}`;
|
||||
},
|
||||
|
||||
follow() {
|
||||
event.currentTarget.blur();
|
||||
this.$emit('follow');
|
||||
},
|
||||
|
||||
unfollow() {
|
||||
event.currentTarget.blur();
|
||||
this.$emit('unfollow');
|
||||
},
|
||||
|
||||
setBio() {
|
||||
if(!this.profile.note.length) {
|
||||
return;
|
||||
}
|
||||
if(this.profile.local) {
|
||||
let content = this.profile.hasOwnProperty('note_text') ?
|
||||
this.profile.note_text :
|
||||
this.profile.note.replace(/(<([^>]+)>)/gi, "");
|
||||
this.renderedBio = window.pftxt.autoLink(content, {
|
||||
usernameUrlBase: '/i/web/profile/@',
|
||||
hashtagUrlBase: '/i/web/hashtag/'
|
||||
})
|
||||
} else {
|
||||
if(this.profile.note === '<p></p>') {
|
||||
this.renderedBio = null;
|
||||
return;
|
||||
}
|
||||
let content = this.profile.note;
|
||||
let el = document.createElement('div');
|
||||
el.innerHTML = content;
|
||||
el.querySelectorAll('a[class*="hashtag"]')
|
||||
.forEach(elr => {
|
||||
let tag = elr.innerText;
|
||||
if(tag.substr(0, 1) == '#') {
|
||||
tag = tag.substr(1);
|
||||
}
|
||||
elr.removeAttribute('target');
|
||||
elr.setAttribute('href', '/i/web/hashtag/' + tag);
|
||||
})
|
||||
el.querySelectorAll('a:not(.hashtag)[class*="mention"], a:not(.hashtag)[class*="list-slug"]')
|
||||
.forEach(elr => {
|
||||
let name = elr.innerText;
|
||||
if(name.substr(0, 1) == '@') {
|
||||
name = name.substr(1);
|
||||
}
|
||||
if(this.profile.local == false && !name.includes('@')) {
|
||||
let domain = document.createElement('a');
|
||||
domain.href = this.profile.url;
|
||||
name = name + '@' + domain.hostname;
|
||||
}
|
||||
elr.removeAttribute('target');
|
||||
elr.setAttribute('href', '/i/web/username/' + name);
|
||||
})
|
||||
this.renderedBio = el.outerHTML;
|
||||
}
|
||||
},
|
||||
|
||||
getAvatar() {
|
||||
if(this.profile.id == this.user.id) {
|
||||
return window._sharedData.user.avatar;
|
||||
}
|
||||
|
||||
return this.profile.avatar;
|
||||
},
|
||||
|
||||
copyTextToClipboard(val) {
|
||||
App.util.clipboard(val);
|
||||
},
|
||||
|
||||
goToOldProfile() {
|
||||
if(this.profile.local) {
|
||||
location.href = this.profile.url + '?fs=1';
|
||||
} else {
|
||||
location.href = '/i/web/profile/_/' + this.profile.id;
|
||||
}
|
||||
},
|
||||
|
||||
handleMute() {
|
||||
let msg = this.relationship.muting ? 'unmuted' : 'muted';
|
||||
let url = this.relationship.muting == true ? '/i/unmute' : '/i/mute';
|
||||
axios.post(url, {
|
||||
type: 'user',
|
||||
item: this.profile.id
|
||||
}).then(res => {
|
||||
this.$emit('updateRelationship', res.data);
|
||||
swal('Success', 'You have successfully '+ msg +' ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
if(err.response.status === 422) {
|
||||
swal({
|
||||
title: 'Error',
|
||||
text: err.response?.data?.error,
|
||||
icon: "error",
|
||||
buttons: {
|
||||
review: {
|
||||
text: "Review muted accounts",
|
||||
value: "review",
|
||||
className: "btn-primary"
|
||||
},
|
||||
cancel: true,
|
||||
}
|
||||
})
|
||||
.then((val) => {
|
||||
if(val && val == 'review') {
|
||||
location.href = '/settings/privacy/muted-users';
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleBlock() {
|
||||
let msg = this.relationship.blocking ? 'unblock' : 'block';
|
||||
let url = this.relationship.blocking == true ? '/i/unblock' : '/i/block';
|
||||
axios.post(url, {
|
||||
type: 'user',
|
||||
item: this.profile.id
|
||||
}).then(res => {
|
||||
this.$emit('updateRelationship', res.data);
|
||||
swal('Success', 'You have successfully '+ msg +'ed ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
if(err.response.status === 422) {
|
||||
swal({
|
||||
title: 'Error',
|
||||
text: err.response?.data?.error,
|
||||
icon: "error",
|
||||
buttons: {
|
||||
review: {
|
||||
text: "Review blocked accounts",
|
||||
value: "review",
|
||||
className: "btn-primary"
|
||||
},
|
||||
cancel: true,
|
||||
}
|
||||
})
|
||||
.then((val) => {
|
||||
if(val && val == 'review') {
|
||||
location.href = '/settings/privacy/blocked-users';
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
cancelFollowRequest() {
|
||||
if(!window.confirm('Are you sure you want to cancel your follow request?')) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.blur();
|
||||
this.$emit('unfollow');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-sidebar-component {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
word-break: break-word;
|
||||
font-size: 15px;
|
||||
font-weight: 800 !important;
|
||||
user-select: all;
|
||||
line-height: 0.8;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
|
||||
&.remote {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.stat-item {
|
||||
max-width: 33%;
|
||||
flex: 0 0 33%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
color: var(--body-color);
|
||||
font-size: 18px;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #B8C2CC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follow-btn {
|
||||
@media (min-width: 768px) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
&.btn-light {
|
||||
border-color: var(--input-border);
|
||||
}
|
||||
}
|
||||
|
||||
.unfollow-btn {
|
||||
@media (min-width: 768px) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
background-color: rgba(59, 130, 246, 0.7);
|
||||
}
|
||||
|
||||
.bio-wrapper {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.bio-body {
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 12px !important;
|
||||
white-space: pre-wrap;
|
||||
|
||||
.username {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
&.long {
|
||||
max-height: 80px;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(180deg, transparent 0, rgba(255, 255, 255, .9) 60%, #fff 90%);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bio-more {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-label {
|
||||
padding: 1px 5px;
|
||||
font-size: 12px;
|
||||
color: #B91C1C;
|
||||
background: #FEE2E2;
|
||||
border: 1px solid #FCA5A5;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sidebar-sitelinks {
|
||||
margin-top: 1rem;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
color: #B8C2CC;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #212529;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-attribution {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 12px;
|
||||
color: #B8C2CC !important;
|
||||
|
||||
a {
|
||||
color: #B8C2CC !important;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card {
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 15px;
|
||||
margin-right: 0.8rem;
|
||||
border: 1px solid #E5E7EB;
|
||||
|
||||
@media (min-width: 390px) {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-update-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid #dee2e6 !important;
|
||||
padding: 0;
|
||||
border-radius: 50rem;
|
||||
|
||||
&-icon {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
|
||||
&:before {
|
||||
content: "\F013";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin: 4px 0;
|
||||
word-break: break-word;
|
||||
line-height: 12px;
|
||||
user-select: all;
|
||||
|
||||
@media (min-width: 390px) {
|
||||
margin: 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
color: var(--body-color);
|
||||
line-height: 0.8;
|
||||
font-size: 20px;
|
||||
font-weight: 800 !important;
|
||||
word-break: break-word;
|
||||
user-select: all;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (min-width: 390px) {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
|
||||
.posts-count,
|
||||
.following-count,
|
||||
.followers-count {
|
||||
display: flex;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
100
resources/assets/components/partials/rightbar.vue
Normal file
100
resources/assets/components/partials/rightbar.vue
Normal file
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- <p class="">
|
||||
<router-link class="btn btn-primary primary btn-sm rounded-pill font-weight-bold btn-block" to="/i/web/whats-new"><i class="fal fa-exclamation-circle mr-1"></i> New in Metro UI 2</router-link>
|
||||
</p> -->
|
||||
|
||||
<notifications :profile="profile" />
|
||||
|
||||
<!-- <div class="d-none card shadow-sm mb-3" style="border-radius: 15px;">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<p class="text-muted">{{ $t('timeline.peopleYouMayKnow') }}</p>
|
||||
<p class="text-lighter"><i class="far fa-cog"></i></p>
|
||||
</div>
|
||||
|
||||
<div class="media-list mb-n4">
|
||||
<div v-for="(account, index) in recommended" class="media align-items-center mb-3">
|
||||
<img :src="account.avatar" class="avatar shadow-sm mr-3" width="40" height="40">
|
||||
<div class="media-body">
|
||||
<p class="lead font-weight-bold username primary">@{{ account.username }}</p>
|
||||
<p class="text-muted mb-0 display-name">{{ account.display_name }}</p>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-sm follow">
|
||||
{{ $t('profile.follow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none card shadow-sm mb-3" style="border-radius: 15px;">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<p class="text-muted">Trending</p>
|
||||
<p class="text-lighter"><i class="far fa-cog"></i></p>
|
||||
</div>
|
||||
|
||||
<div class="media-list row mb-n3">
|
||||
<div v-for="(post, index) in trending" class="col-6 mb-1 p-1">
|
||||
<img :src="post.url" width="100%" height="100" class="bg-white p-1 shadow-sm" style="object-fit: cover;border-radius: 15px;">
|
||||
</div>
|
||||
|
||||
<div class="col-6 mb-1 p-1 d-flex justify-content-center align-items-center">
|
||||
<button class="btn btn-link text-dark">
|
||||
<i class="fal fa-plus-circle fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Notifications from './../sections/Notifications.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"notifications": Notifications
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
profile: {},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.profile = window._sharedData.user;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 15px;
|
||||
margin-bottom: -6px;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.follow {
|
||||
background-color: var(--primary);
|
||||
border-radius: 18px;
|
||||
font-weight: 600;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.btn-white {
|
||||
background-color: #fff;
|
||||
border: 1px solid #F3F4F6;
|
||||
}
|
||||
</style>
|
726
resources/assets/components/partials/sidebar.vue
Normal file
726
resources/assets/components/partials/sidebar.vue
Normal file
|
@ -0,0 +1,726 @@
|
|||
<template>
|
||||
<div class="sidebar-component sticky-top d-none d-md-block">
|
||||
<!-- <input type="file" class="d-none" ref="avatarUpdateRef" @change="handleAvatarUpdate()"> -->
|
||||
<!-- <div class="card shadow-sm mb-3 cursor-pointer" style="border-radius: 15px;" @click="gotoMyProfile()"> -->
|
||||
<div class="card shadow-sm mb-3" style="border-radius: 15px;">
|
||||
<div class="card-body p-2">
|
||||
<div class="media user-card user-select-none">
|
||||
<div style="position: relative;">
|
||||
<img :src="user.avatar" class="avatar shadow cursor-pointer" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';" @click="gotoMyProfile()">
|
||||
<button class="btn btn-light btn-sm avatar-update-btn" @click="updateAvatar()">
|
||||
<span class="avatar-update-btn-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<p class="display-name" v-html="getDisplayName()"></p>
|
||||
<p class="username primary">@{{ user.username }}</p>
|
||||
<p class="stats">
|
||||
<span class="stats-following">
|
||||
<span class="following-count">{{ formatCount(user.following_count) }}</span> Following
|
||||
</span>
|
||||
<span class="stats-followers">
|
||||
<span class="followers-count">{{ formatCount(user.followers_count) }}</span> Followers
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-lg btn-block mb-4">
|
||||
<!-- <button type="button" class="btn btn-outline-primary btn-block font-weight-bold" style="border-top-left-radius: 18px;border-bottom-left-radius:18px;font-size:18px;font-weight:300!important" @click="createNewPost()">
|
||||
<i class="fal fa-arrow-circle-up mr-1"></i> {{ $t('navmenu.compose') }} Post
|
||||
</button> -->
|
||||
<router-link to="/i/web/compose" class="btn btn-primary btn-block font-weight-bold">
|
||||
<i class="fal fa-arrow-circle-up mr-1"></i> {{ $t('navmenu.compose') }} Post
|
||||
</router-link>
|
||||
<button type="button" class="btn btn-outline-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item font-weight-bold" href="/i/collections/create">Create Collection</a>
|
||||
<a v-if="hasStories" class="dropdown-item font-weight-bold" href="/i/stories/new">Create Story</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="/settings/home">Account Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <router-link to="/i/web/compose" class="btn btn-primary btn-lg btn-block mb-4 shadow-sm font-weight-bold">
|
||||
<i class="far fa-plus-square mr-1"></i> {{ $t('navmenu.compose') }}
|
||||
</router-link> -->
|
||||
|
||||
<div class="sidebar-sticky shadow-sm">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<!-- <router-link class="nav-link text-center" to="/i/web">
|
||||
<div class="icon text-lighter"><i class="far fa-home fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.homeFeed') }}</div>
|
||||
</router-link> -->
|
||||
<a
|
||||
class="nav-link text-center"
|
||||
href="/i/web"
|
||||
:class="[ $route.path == '/i/web' ? 'router-link-exact-active active' : '' ]"
|
||||
@click.prevent="goToFeed('home')">
|
||||
<div class="icon text-lighter"><i class="far fa-home fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.homeFeed') }}</div>
|
||||
</a>
|
||||
|
||||
<!-- <router-link v-if="hasLocalTimeline" class="nav-link text-center" :to="{ name: 'timeline', params: { scope: 'local' } }">
|
||||
<div class="icon text-lighter"><i class="fas fa-stream fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.localFeed') }}</div>
|
||||
</router-link> -->
|
||||
<a
|
||||
v-if="hasLocalTimeline"
|
||||
class="nav-link text-center"
|
||||
href="/i/web/timeline/local"
|
||||
:class="[ $route.path == '/i/web/timeline/local' ? 'router-link-exact-active active' : '' ]"
|
||||
@click.prevent="goToFeed('local')">
|
||||
<div class="icon text-lighter"><i class="fas fa-stream fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.localFeed') }}</div>
|
||||
</a>
|
||||
|
||||
<!-- <router-link v-if="hasNetworkTimeline" class="nav-link text-center" :to="{ name: 'timeline', params: { scope: 'global' } }">
|
||||
<div class="icon text-lighter"><i class="far fa-globe fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.globalFeed') }}</div>
|
||||
</router-link> -->
|
||||
<a
|
||||
v-if="hasNetworkTimeline"
|
||||
class="nav-link text-center"
|
||||
href="/i/web/timeline/global"
|
||||
:class="[ $route.path == '/i/web/timeline/global' ? 'router-link-exact-active active' : '' ]"
|
||||
@click.prevent="goToFeed('global')">
|
||||
<div class="icon text-lighter"><i class="far fa-globe fa-lg"></i></div>
|
||||
<div class="small">{{ $t('navmenu.globalFeed') }}</div>
|
||||
</a>
|
||||
</div>
|
||||
<hr class="mb-0" style="margin-top: -5px;opacity: 0.4;" />
|
||||
</li>
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
|
||||
</li> -->
|
||||
|
||||
|
||||
<!-- <li v-for="(link, index) in links" class="nav-item">
|
||||
<router-link class="nav-link" :to="link.path">
|
||||
<span v-if="link.icon" class="icon text-lighter"><i :class="[ link.icon ]"></i></span>
|
||||
{{ link.name }}
|
||||
</router-link>
|
||||
</li> -->
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/discover">
|
||||
<span class="icon text-lighter"><i class="far fa-compass"></i></span>
|
||||
{{ $t('navmenu.discover') }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/direct">
|
||||
<span>
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-envelope"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.directMessages') }}
|
||||
</span>
|
||||
|
||||
<!-- <span class="badge badge-danger font-weight-light rounded-pill px-2" style="transform:scale(0.86)">99+</span> -->
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/groups">
|
||||
<span class="icon text-lighter"><i class="far fa-layer-group"></i></span>
|
||||
{{ $t('navmenu.groups') }}
|
||||
</router-link>
|
||||
</li> -->
|
||||
|
||||
<li v-if="hasLiveStreams" class="nav-item">
|
||||
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/livestreams">
|
||||
<span>
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-record-vinyl"></i>
|
||||
</span>
|
||||
Livestreams
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link d-flex justify-content-between align-items-center" to="/i/web/notifications">
|
||||
<span>
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-bell"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.notifications') }}
|
||||
</span>
|
||||
|
||||
<!-- <span class="badge badge-danger font-weight-light rounded-pill px-2" style="transform:scale(0.86)">99+</span> -->
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
|
||||
|
||||
<router-link class="nav-link" :to="'/i/web/profile/' + user.id">
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-user"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.profile') }}
|
||||
</router-link>
|
||||
|
||||
<!-- <router-link class="nav-link" to="/i/web/settings">
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-cog"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.settings') }}
|
||||
</router-link> -->
|
||||
</li>
|
||||
<!-- <li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/drive">
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-cloud-upload"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.drive') }}
|
||||
</router-link>
|
||||
</li> -->
|
||||
<!-- <li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/settings">
|
||||
<span class="icon text-lighter">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.settings') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/i/web/help">
|
||||
<span class="icon text-lighter">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</span>
|
||||
Help
|
||||
</a>
|
||||
</li> -->
|
||||
<li v-if="user.is_admin" class="nav-item">
|
||||
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
|
||||
<a class="nav-link" href="/i/admin/dashboard">
|
||||
<span class="icon text-lighter">
|
||||
<i class="far fa-tools"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.admin') }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<hr class="mt-n1" style="opacity: 0.4;margin-bottom: 0;" />
|
||||
<a class="nav-link" href="/?force_old_ui=1">
|
||||
<span class="icon text-lighter">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</span>
|
||||
{{ $t('navmenu.backToPreviousDesign') }}
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/?a=feed">
|
||||
<span class="fas fa-stream pr-2 text-lighter"></span>
|
||||
Feed
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/discover">
|
||||
<span class="fas fa-compass pr-2 text-lighter"></span>
|
||||
Discover
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link" to="/i/web/stories">
|
||||
<span class="fas fa-history pr-2 text-lighter"></span>
|
||||
Stories
|
||||
</router-link>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- <div class="sidebar-sitelinks">
|
||||
<a href="/site/about">{{ $t('navmenu.about') }}</a>
|
||||
<a href="/site/language">{{ $t('navmenu.language') }}</a>
|
||||
<a href="/site/terms">{{ $t('navmenu.privacy') }}</a>
|
||||
<a href="/site/terms">{{ $t('navmenu.terms') }}</a>
|
||||
</div> -->
|
||||
|
||||
<div class="sidebar-attribution pr-3 d-flex justify-content-between align-items-center">
|
||||
<router-link to="/i/web/language">
|
||||
<i class="fal fa-language fa-2x" alt="Select a language"></i>
|
||||
</router-link>
|
||||
<a href="/site/help" class="font-weight-bold">{{ $t('navmenu.help') }}</a>
|
||||
<a href="/site/privacy" class="font-weight-bold">{{ $t('navmenu.privacy') }}</a>
|
||||
<a href="/site/terms" class="font-weight-bold">{{ $t('navmenu.terms') }}</a>
|
||||
<a href="https://pixelfed.org" class="font-weight-bold powered-by">Powered by Pixelfed</a>
|
||||
</div>
|
||||
|
||||
<!-- <b-modal
|
||||
ref="avatarUpdateModal"
|
||||
centered
|
||||
hide-footer
|
||||
header-class="py-2"
|
||||
body-class="p-0"
|
||||
title-class="w-100 text-center pl-4 font-weight-bold"
|
||||
title-tag="p"
|
||||
title="Upload Avatar"
|
||||
>
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
v-if="avatarUpdateIndex === 0"
|
||||
class="py-5 user-select-none cursor-pointer"
|
||||
@click="avatarUpdateStep(0)">
|
||||
<p class="text-center primary">
|
||||
<i class="fal fa-cloud-upload fa-3x"></i>
|
||||
</p>
|
||||
<p class="text-center lead">Drag photo here or click here</p>
|
||||
<p class="text-center small text-muted mb-0">Must be a <strong>png</strong> or <strong>jpg</strong> image up to 2MB</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="avatarUpdateIndex === 1" class="w-100 p-5">
|
||||
|
||||
<div class="d-md-flex justify-content-between align-items-center">
|
||||
<div class="text-center mb-4">
|
||||
<p class="small font-weight-bold" style="opacity:0.7;">Current</p>
|
||||
<img :src="user.avatar" class="shadow" style="width: 150px;height: 150px;object-fit: cover;border-radius: 18px;opacity: 0.7;">
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<p class="font-weight-bold">New</p>
|
||||
<img :src="avatarUpdateFile" class="shadow" style="width: 220px;height: 220px;object-fit: cover;border-radius: 18px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<button class="btn btn-light font-weight-bold btn-block mr-3" @click="avatarUpdateClose()">Cancel</button>
|
||||
<button class="btn btn-primary primary font-weight-bold btn-block mt-0">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal> -->
|
||||
|
||||
<!-- <b-modal
|
||||
ref="createPostModal"
|
||||
centered
|
||||
hide-footer
|
||||
header-class="py-2"
|
||||
body-class="p-0 w-100 h-100"
|
||||
title-class="w-100 text-center pl-4 font-weight-bold"
|
||||
title-tag="p"
|
||||
title="Create New Post"
|
||||
>
|
||||
<compose-simple />
|
||||
</b-modal> -->
|
||||
|
||||
<update-avatar ref="avatarUpdate" :user="user" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import { mapGetters } from 'vuex'
|
||||
// import ComposeSimple from './../sections/ComposeSimple.vue';
|
||||
import UpdateAvatar from './modal/UpdateAvatar.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
default: (function() {
|
||||
return {
|
||||
avatar: '/storage/avatars/default.jpg',
|
||||
username: false,
|
||||
display_name: '',
|
||||
following_count: 0,
|
||||
followers_count: 0
|
||||
};
|
||||
})
|
||||
},
|
||||
|
||||
links: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return [
|
||||
// {
|
||||
// name: "Home",
|
||||
// path: "/i/web",
|
||||
// icon: "fas fa-home"
|
||||
// },
|
||||
// {
|
||||
// name: "Local",
|
||||
// path: "/i/web/timeline/local",
|
||||
// icon: "fas fa-stream"
|
||||
// },
|
||||
// {
|
||||
// name: "Global",
|
||||
// path: "/i/web/timeline/global",
|
||||
// icon: "far fa-globe"
|
||||
// },
|
||||
// {
|
||||
// name: "Audiences",
|
||||
// path: "/i/web/discover",
|
||||
// icon: "far fa-circle-notch"
|
||||
// },
|
||||
{
|
||||
name: "Discover",
|
||||
path: "/i/web/discover",
|
||||
icon: "fas fa-compass"
|
||||
},
|
||||
// {
|
||||
// name: "Events",
|
||||
// path: "/i/events",
|
||||
// icon: "far fa-calendar-alt"
|
||||
// },
|
||||
{
|
||||
name: "Groups",
|
||||
path: "/i/web/groups",
|
||||
icon: "far fa-user-friends"
|
||||
},
|
||||
// {
|
||||
// name: "Live",
|
||||
// path: "/i/web/?t=live",
|
||||
// icon: "far fa-play"
|
||||
// },
|
||||
// {
|
||||
// name: "Marketplace",
|
||||
// path: "/i/web/marketplace",
|
||||
// icon: "far fa-shopping-cart"
|
||||
// },
|
||||
// {
|
||||
// name: "Stories",
|
||||
// path: "/i/web/?t=stories",
|
||||
// icon: "fas fa-history"
|
||||
// },
|
||||
{
|
||||
name: "Videos",
|
||||
path: "/i/web/videos",
|
||||
icon: "far fa-video"
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
// ComposeSimple,
|
||||
UpdateAvatar
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getCustomEmoji'
|
||||
])
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
hasLocalTimeline: true,
|
||||
hasNetworkTimeline: false,
|
||||
hasLiveStreams: false,
|
||||
hasStories: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if(window.App.config.features.hasOwnProperty('timelines')) {
|
||||
this.hasLocalTimeline = App.config.features.timelines.local;
|
||||
this.hasNetworkTimeline = App.config.features.timelines.network;
|
||||
//this.hasLiveStreams = App.config.ab.hls == true;
|
||||
}
|
||||
if(window.App.config.features.hasOwnProperty('stories')) {
|
||||
this.hasStories = App.config.features.stories;
|
||||
}
|
||||
// if(!this.user.username) {
|
||||
// this.user = window._sharedData.user;
|
||||
// }
|
||||
// setTimeout(() => {
|
||||
// this.user = window._sharedData.curUser;
|
||||
// this.loaded = true;
|
||||
// }, 300);
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDisplayName() {
|
||||
let self = this;
|
||||
let profile = this.user;
|
||||
let dn = profile.display_name;
|
||||
if(!dn) {
|
||||
return profile.username;
|
||||
}
|
||||
if(dn.includes(':')) {
|
||||
let re = /(<a?)?:\w+:(\d{18}>)?/g;
|
||||
let un = dn.replaceAll(re, function(em) {
|
||||
let shortcode = em.slice(1, em.length - 1);
|
||||
let emoji = self.getCustomEmoji.filter(e => {
|
||||
return e.shortcode == shortcode;
|
||||
});
|
||||
return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
|
||||
});
|
||||
return un;
|
||||
} else {
|
||||
return dn;
|
||||
}
|
||||
},
|
||||
|
||||
gotoMyProfile() {
|
||||
let user = this.user;
|
||||
this.$router.push({
|
||||
name: 'profile',
|
||||
path: `/i/web/profile/${user.id}`,
|
||||
params: {
|
||||
id: user.id,
|
||||
cachedProfile: user,
|
||||
cachedUser: user
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
formatCount(count = 0, locale = 'en-GB', notation = 'compact') {
|
||||
return new Intl.NumberFormat(locale, { notation: notation , compactDisplay: "short" }).format(count);
|
||||
},
|
||||
|
||||
updateAvatar() {
|
||||
event.currentTarget.blur();
|
||||
// swal('update avatar', 'test', 'success');
|
||||
this.$refs.avatarUpdate.open();
|
||||
},
|
||||
|
||||
createNewPost() {
|
||||
this.$refs.createPostModal.show();
|
||||
},
|
||||
|
||||
goToFeed(feed) {
|
||||
const curPath = this.$route.path;
|
||||
switch(feed) {
|
||||
case 'home':
|
||||
if(curPath == '/i/web') {
|
||||
this.$emit('refresh');
|
||||
} else {
|
||||
this.$router.push('/i/web');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
if(curPath == '/i/web/timeline/local') {
|
||||
this.$emit('refresh');
|
||||
} else {
|
||||
this.$router.push({ name: 'timeline', params: { scope: 'local' }});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'global':
|
||||
if(curPath == '/i/web/timeline/global') {
|
||||
this.$emit('refresh');
|
||||
} else {
|
||||
this.$router.push({ name: 'timeline', params: { scope: 'global' }});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar-component {
|
||||
.sidebar-sticky {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
&.sticky-top {
|
||||
top: 90px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
.nav-link {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
font-weight: 500;
|
||||
color: rgba(156,163,175, 1);
|
||||
padding-left: 14px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-hover-bg);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.router-link-exact-active {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
padding-left: 14px;
|
||||
|
||||
&:not(.text-center) {
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid var(--primary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.nav-link {
|
||||
.small {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 15px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:last-child) {
|
||||
.nav-link {
|
||||
margin-bottom: 0;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
border-radius: 15px;
|
||||
margin-right: 0.8rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.avatar-update-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid #dee2e6 !important;
|
||||
padding: 0;
|
||||
border-radius: 50rem;
|
||||
|
||||
&-icon {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
|
||||
&:before {
|
||||
content: "\F013";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
color: var(--body-color);
|
||||
line-height: 0.8;
|
||||
font-size: 14px;
|
||||
font-weight: 800 !important;
|
||||
user-select: all;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
margin-bottom: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
|
||||
.stats-following {
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
|
||||
.following-count,
|
||||
.followers-count {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
|
||||
&.router-link-exact-active {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sitelinks {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 2rem;
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
color: #B8C2CC;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #212529;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-attribution {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 10px;
|
||||
color: #B8C2CC;
|
||||
padding-left: 2rem;
|
||||
|
||||
a {
|
||||
color: #B8C2CC !important;
|
||||
|
||||
&.powered-by {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
234
resources/assets/components/partials/timeline/Notification.vue
Normal file
234
resources/assets/components/partials/timeline/Notification.vue
Normal file
|
@ -0,0 +1,234 @@
|
|||
<template>
|
||||
<div class="media mb-2 align-items-center px-3 shadow-sm py-2 bg-white" style="border-radius: 15px;">
|
||||
<a href="#" @click.prevent="getProfileUrl(n.account)" v-b-tooltip.hover :title="n.account.acct">
|
||||
<img class="mr-3 shadow-sm" style="border-radius:8px" :src="n.account.avatar" alt="" width="40" height="40" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
</a>
|
||||
|
||||
<div class="media-body font-weight-light">
|
||||
<div v-if="n.type == 'favourite'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.liked') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'comment'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- <div v-else-if="n.type == 'group:comment'">
|
||||
<p class="my-0">
|
||||
<a href="#" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" v-bind:href="n.group_post_url">{{ $('notifications.groupPost') }}</a>.
|
||||
</p>
|
||||
</div> -->
|
||||
|
||||
<div v-else-if="n.type == 'story:react'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.reacted') }} <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'story:comment'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.commented') }} <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">{{ $t('notifications.story') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'mention'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">{{ $t('notifications.mentioned') }}</a> {{ $t('notifications.you') }}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'follow'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.followed') }} {{ $t('notifications.you') }}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'share'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.shared') }} <a class="font-weight-bold" :href="displayPostUrl(n.status)" @click.prevent="getPostUrl(n.status)">{{ $t('notifications.post') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'modlog'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">{{truncate(n.account.username)}}</a> {{ $t('notifications.updatedA') }} <a class="font-weight-bold" v-bind:href="n.modlog.url">{{ $t('notifications.modlog') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="n.type == 'tagged'">
|
||||
<p class="my-0">
|
||||
<a :href="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</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="displayProfileUrl(n.account)" @click.prevent="getProfileUrl(n.account)" class="font-weight-bold text-dark text-break" :title="n.account.acct">@{{n.account.acct}}</a> {{ $t('notifications.sentA') }} <router-link class="font-weight-bold" :to="'/i/web/direct/thread/'+n.account.id">{{ $t('notifications.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 text-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t('notifications.applicationApproved') }}
|
||||
</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 text-break" :title="n.group.name">{{truncate(n.group.name)}}</a> {{ $t('notifications.applicationRejected') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<p class="my-0 d-flex justify-content-between align-items-center">
|
||||
<span class="font-weight-bold">Notification</span>
|
||||
<span style="font-size:8px;">e_{{ n.type }}::{{ n.id }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="align-items-center">
|
||||
<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="n.status && n.status && n.status.media_attachments && n.status.media_attachments.length">
|
||||
<a href="#" @click.prevent="getPostUrl(n.status)">
|
||||
<img :src="n.status.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
|
||||
<a :href="n.status.parent.url">
|
||||
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
|
||||
<a :href="n.status.parent.url">
|
||||
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div> -->
|
||||
|
||||
<!-- <div v-else>
|
||||
<a v-if="viewContext(n) != '/'" class="btn btn-outline-primary py-0 font-weight-bold" href="#" @click.prevent="viewContext(n)">View</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
n: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
profile: window._sharedData.user
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
truncate(text, limit = 30) {
|
||||
if(text.length <= limit) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0, limit) + '...'
|
||||
},
|
||||
|
||||
timeAgo(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
let interval = Math.floor(seconds / 31536000);
|
||||
if (interval >= 1) {
|
||||
return interval + "y";
|
||||
}
|
||||
interval = Math.floor(seconds / 604800);
|
||||
if (interval >= 1) {
|
||||
return interval + "w";
|
||||
}
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval >= 1) {
|
||||
return interval + "d";
|
||||
}
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval >= 1) {
|
||||
return interval + "h";
|
||||
}
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval >= 1) {
|
||||
return interval + "m";
|
||||
}
|
||||
return Math.floor(seconds) + "s";
|
||||
},
|
||||
|
||||
mentionUrl(status) {
|
||||
let username = status.account.username;
|
||||
let id = status.id;
|
||||
return '/p/' + username + '/' + id;
|
||||
},
|
||||
|
||||
viewContext(n) {
|
||||
switch(n.type) {
|
||||
case 'follow':
|
||||
return this.getProfileUrl(n.account);
|
||||
return n.account.url;
|
||||
break;
|
||||
case 'mention':
|
||||
return n.status.url;
|
||||
break;
|
||||
case 'like':
|
||||
case 'favourite':
|
||||
case 'comment':
|
||||
return this.getPostUrl(n.status);
|
||||
// return n.status.url;
|
||||
break;
|
||||
case 'tagged':
|
||||
return n.tagged.post_url;
|
||||
break;
|
||||
case 'direct':
|
||||
return '/account/direct/t/'+n.account.id;
|
||||
break
|
||||
}
|
||||
return '/';
|
||||
},
|
||||
|
||||
displayProfileUrl(account) {
|
||||
return `/i/web/profile/${account.id}`;
|
||||
},
|
||||
|
||||
displayPostUrl(status) {
|
||||
return `/i/web/post/${status.id}`;
|
||||
},
|
||||
|
||||
getProfileUrl(account) {
|
||||
this.$router.push({
|
||||
name: 'profile',
|
||||
path: `/i/web/profile/${account.id}`,
|
||||
params: {
|
||||
id: account.id,
|
||||
cachedProfile: account,
|
||||
cachedUser: this.profile
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getPostUrl(status) {
|
||||
this.$router.push({
|
||||
name: 'post',
|
||||
path: `/i/web/post/${status.id}`,
|
||||
params: {
|
||||
id: status.id,
|
||||
cachedStatus: status,
|
||||
cachedProfile: this.profile
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
200
resources/assets/components/partials/timeline/StoryCarousel.vue
Normal file
200
resources/assets/components/partials/timeline/StoryCarousel.vue
Normal file
|
@ -0,0 +1,200 @@
|
|||
<template>
|
||||
<div class="story-carousel-component">
|
||||
<div v-if="canShow" class="d-flex story-carousel-component-wrapper" style="overflow-y: auto;z-index: 3;">
|
||||
<a class="col-4 col-lg-3 col-xl-2 px-1 text-dark text-decoration-none" href="/i/stories/new" style="max-width: 120px;">
|
||||
<template v-if="selfStory && selfStory.length">
|
||||
<div
|
||||
class="story-wrapper text-white shadow-sm mb-3"
|
||||
:style="{ background: `linear-gradient(rgba(0,0,0,0.2),rgba(0,0,0,0.4)), url(${selfStory[0].latest.preview_url})`, backgroundSize: 'cover', backgroundPosition: 'center'}"
|
||||
style="width: 100%;height:200px;border-radius:15px;">
|
||||
<div class="story-wrapper-blur d-flex flex-column align-items-center justify-content-between" style="display: block;width: 100%;height:100%;">
|
||||
<p class="mb-4"></p>
|
||||
<p class="mb-0"><i class="fal fa-plus-circle fa-2x"></i></p>
|
||||
<p class="font-weight-bold">My Story</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
class="story-wrapper text-white shadow-sm d-flex flex-column align-items-center justify-content-between"
|
||||
style="width: 100%;height:200px;border-radius:15px;">
|
||||
<p class="mb-4"></p>
|
||||
<p class="mb-0"><i class="fal fa-plus-circle fa-2x"></i></p>
|
||||
<p class="font-weight-bold">{{ $t('story.add') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</a>
|
||||
|
||||
<div v-for="(story, index) in stories" class="col-4 col-lg-3 col-xl-2 px-1" style="max-width: 120px;">
|
||||
<template v-if="story.hasOwnProperty('url')">
|
||||
<a class="story" :href="story.url">
|
||||
<div
|
||||
v-if="story.latest && story.latest.type == 'photo'"
|
||||
class="shadow-sm story-wrapper"
|
||||
:class="{ seen: story.seen }"
|
||||
:style="{ background: `linear-gradient(rgba(0,0,0,0.2),rgba(0,0,0,0.4)), url(${story.latest.preview_url})`, backgroundSize: 'cover', backgroundPosition: 'center'}">
|
||||
<div class="story-wrapper-blur" style="display: block;width: 100%;height:100%;position:relative;">
|
||||
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
|
||||
<p class="mt-3 mb-0">
|
||||
<img :src="story.avatar" width="30" height="30" class="avatar" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
|
||||
</p>
|
||||
<p class="mb-0"></p>
|
||||
<p class="username font-weight-bold small text-truncate">
|
||||
{{ story.username }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="shadow-sm story-wrapper">
|
||||
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
|
||||
<p class="mt-3 mb-0">
|
||||
<img :src="story.avatar" width="30" height="30" class="avatar">
|
||||
</p>
|
||||
<p class="mb-0"></p>
|
||||
<p class="username font-weight-bold small text-truncate">
|
||||
{{ story.username }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
class="story shadow-sm story-wrapper seen"
|
||||
:style="{ background: `linear-gradient(rgba(0,0,0,0.01),rgba(0,0,0,0.04))`}">
|
||||
<div class="story-wrapper-blur" style="display: block;width: 100%;height:100%;position:relative;">
|
||||
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
|
||||
<p class="mt-3 mb-0">
|
||||
</p>
|
||||
<p class="mb-0"></p>
|
||||
<p class="username font-weight-bold small text-truncate">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="selfStory && selfStory.length && stories.length < 2">
|
||||
<div v-for="i in 5" class="col-4 col-lg-3 col-xl-2 px-1 story" style="max-width: 120px;">
|
||||
<div
|
||||
class="shadow-sm story-wrapper seen"
|
||||
:style="{ background: `linear-gradient(rgba(0,0,0,0.01),rgba(0,0,0,0.04))`}">
|
||||
<div class="story-wrapper-blur" style="display: block;width: 100%;height:100%;position:relative;">
|
||||
<div class="px-2" style="display: block;width: 100%;bottom:0;position: absolute;">
|
||||
<p class="mt-3 mb-0">
|
||||
</p>
|
||||
<p class="mb-0"></p>
|
||||
<p class="username font-weight-bold small text-truncate">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
canShow: false,
|
||||
stories: [],
|
||||
selfStory: undefined
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchStories();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchStories() {
|
||||
axios.get('/api/web/stories/v1/recent')
|
||||
.then(res => {
|
||||
if(res.data && res.data.length) {
|
||||
this.selfStory = res.data.filter(s => s.pid == this.profile.id);
|
||||
let activeStories = res.data.filter(s => s.pid !== this.profile.id);
|
||||
this.stories = activeStories;
|
||||
this.canShow = true;
|
||||
|
||||
|
||||
if(!activeStories || !activeStories.length || activeStories.length < 5) {
|
||||
this.stories.push(...Array(5 - activeStories.length).keys())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.story-carousel-component {
|
||||
&-wrapper {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important
|
||||
}
|
||||
}
|
||||
|
||||
.story {
|
||||
&-wrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 1rem;
|
||||
background: #b24592;
|
||||
background: -webkit-linear-gradient(to right, #b24592, #f15f79);
|
||||
background: linear-gradient(to right, #b24592, #f15f79);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
.username {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&.seen {
|
||||
opacity: 30%;
|
||||
}
|
||||
|
||||
&-blur {
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.force-dark-mode {
|
||||
.story-wrapper {
|
||||
&.seen {
|
||||
opacity: 50%;
|
||||
background: linear-gradient(rgba(255,255,255,0.12),rgba(255,255,255,0.14)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
206
resources/assets/components/sections/DiscoverFeed.vue
Normal file
206
resources/assets/components/sections/DiscoverFeed.vue
Normal file
|
@ -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
resources/assets/components/sections/Timeline.vue
Normal file
582
resources/assets/components/sections/Timeline.vue
Normal file
|
@ -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>
|
10
resources/assets/js/admin.js
vendored
10
resources/assets/js/admin.js
vendored
|
@ -21,11 +21,21 @@ Array.from(document.querySelectorAll('.pagination .page-link'))
|
|||
.filter(el => el.textContent === '« Previous' || el.textContent === 'Next »')
|
||||
.forEach(el => el.textContent = (el.textContent === 'Next »' ? '›' :'‹'));
|
||||
|
||||
Vue.component(
|
||||
'admin-autospam',
|
||||
require('./../components/admin/AdminAutospam.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'admin-directory',
|
||||
require('./../components/admin/AdminDirectory.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'admin-reports',
|
||||
require('./../components/admin/AdminReports.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'instances-component',
|
||||
require('./../components/admin/AdminInstances.vue').default
|
||||
|
|
3
resources/assets/js/components.js
vendored
3
resources/assets/js/components.js
vendored
|
@ -1,4 +1,5 @@
|
|||
window.Vue = require('vue');
|
||||
import Vue from 'vue';
|
||||
window.Vue = Vue;
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
import Loading from 'vue-loading-overlay';
|
||||
|
|
189
resources/assets/js/components/NavMenu.vue
Normal file
189
resources/assets/js/components/NavMenu.vue
Normal file
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div>
|
||||
<nav class="navbar navbar-expand navbar-light navbar-laravel shadow-none border-bottom sticky-top py-1">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/" title="Logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager">
|
||||
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config.site.name }}</span>
|
||||
</a>
|
||||
<div v-if="loaded && loggedIn" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav d-none d-md-block mx-auto">
|
||||
<form class="form-inline search-bar" method="get" action="/i/results">
|
||||
<input class="form-control form-control-sm" name="q" placeholder="Search ..." aria-label="search" autocomplete="off" required style="line-height: 0.6;width:200px">
|
||||
</form>
|
||||
</ul>
|
||||
|
||||
<div class="ml-auto">
|
||||
<ul class="navbar-nav">
|
||||
<div class="d-none d-md-block">
|
||||
<li class="nav-item px-md-2">
|
||||
<a class="nav-link font-weight-bold text-muted" href="/discover" title="Discover" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-compass fa-lg"></i>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<div class="d-none d-md-block">
|
||||
<li class="nav-item px-md-2">
|
||||
<a class="nav-link font-weight-bold text-muted" href="/account/activity" title="Notifications" data-toggle="tooltip" data-placement="bottom">
|
||||
<span class="fa-layers fa-fw">
|
||||
<i class="far fa-bell fa-lg"></i>
|
||||
<span class="fa-layers-counter mr-n2 mt-n1" style="background:Tomato"></span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<li class="nav-item dropdown ml-2">
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="User Menu" data-placement="bottom">
|
||||
<i class="far fa-user fa-lg text-muted"></i>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
|
||||
<div class="dropdown-item font-weight-bold cursor-pointer" onclick="App.util.compose.post()">
|
||||
<span class="fas fa-plus-square pr-2 text-lighter"></span>
|
||||
New Post
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="/">
|
||||
<span class="fas fa-home pr-2 text-lighter"></span>
|
||||
Home
|
||||
</a>
|
||||
<a class="dropdown-item font-weight-bold" href="/timeline/public">
|
||||
<span class="fas fa-stream pr-2 text-lighter"></span>
|
||||
Local
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="/i/me">
|
||||
<span class="far fa-user pr-2 text-lighter"></span>
|
||||
My Profile
|
||||
</a>
|
||||
<a class="d-block d-md-none dropdown-item font-weight-bold" href="/discover">
|
||||
<span class="far fa-compass pr-2 text-lighter"></span>
|
||||
Discover
|
||||
</a>
|
||||
<a class="dropdown-item font-weight-bold" href="/notifications">
|
||||
<span class="far fa-bell pr-2 text-lighter"></span>
|
||||
Notifications
|
||||
</a>
|
||||
<a class="dropdown-item font-weight-bold" href="/settings/home">
|
||||
<span class="fas fa-cog pr-2 text-lighter"></span>
|
||||
Settings
|
||||
</a>
|
||||
<div v-if="curUser.is_admin">
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="/i/admin/dashboard">
|
||||
<span class="fas fa-shield-alt fa-sm pr-2 text-lighter"></span>
|
||||
Admin
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="/logout" @click="logout">
|
||||
<span class="fas fa-sign-out-alt pr-2"></span>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loaded && !loggedIn" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li>
|
||||
<a class="nav-link font-weight-bold text-primary" href="/login" title="Login">
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="config.open_registration">
|
||||
<a class="nav-link font-weight-bold" href="/register" title="Register">
|
||||
Register
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
.fa-layers-counter, .fa-layers-text {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
}
|
||||
.fa-layers .far {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.fa-fw {
|
||||
text-align: center;
|
||||
width: 1.25em;
|
||||
}
|
||||
.fa-layers {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: -.125em;
|
||||
width: 1em;
|
||||
}
|
||||
.fa-layers-counter {
|
||||
background-color: #ff253a;
|
||||
border-radius: 1em;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
height: 1.5em;
|
||||
line-height: 1;
|
||||
max-width: 5em;
|
||||
min-width: 1.5em;
|
||||
overflow: hidden;
|
||||
padding: .25em;
|
||||
right: 0;
|
||||
text-overflow: ellipsis;
|
||||
top: 0;
|
||||
-webkit-transform: scale(.25);
|
||||
transform: scale(.25);
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config: window.App.config,
|
||||
curUser: {},
|
||||
loggedIn: false,
|
||||
loaded: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.timeout();
|
||||
},
|
||||
|
||||
methods: {
|
||||
logout() {
|
||||
axios.post('/logout')
|
||||
.then(res => {
|
||||
window.location.href = '/';
|
||||
});
|
||||
},
|
||||
|
||||
timeout() {
|
||||
let self = this;
|
||||
setTimeout(function() {
|
||||
self.curUser = window._sharedData.curUser;
|
||||
self.loggedIn = self.curUser.hasOwnProperty('username');
|
||||
self.loaded = true;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
819
resources/assets/js/spa.js
vendored
Normal file
819
resources/assets/js/spa.js
vendored
Normal file
|
@ -0,0 +1,819 @@
|
|||
require('./polyfill');
|
||||
import Vue from 'vue';
|
||||
window.Vue = Vue;
|
||||
import VueRouter from "vue-router";
|
||||
import Vuex from "vuex";
|
||||
import { sync } from "vuex-router-sync";
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
import Loading from 'vue-loading-overlay';
|
||||
import VueTimeago from 'vue-timeago';
|
||||
import VueCarousel from 'vue-carousel';
|
||||
import VueBlurHash from 'vue-blurhash';
|
||||
import VueMasonry from 'vue-masonry-css';
|
||||
import VueI18n from 'vue-i18n';
|
||||
window.pftxt = require('twitter-text');
|
||||
import 'vue-blurhash/dist/vue-blurhash.css'
|
||||
window.filesize = require('filesize');
|
||||
import swal from 'sweetalert';
|
||||
window._ = require('lodash');
|
||||
window.Popper = require('popper.js').default;
|
||||
window.pixelfed = window.pixelfed || {};
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
require('bootstrap');
|
||||
window.axios = require('axios');
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
require('readmore-js');
|
||||
window.blurhash = require("blurhash");
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
let token = document.head.querySelector('meta[name="csrf-token"]');
|
||||
if (token) {
|
||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
|
||||
} else {
|
||||
console.error('CSRF token not found.');
|
||||
}
|
||||
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueBlurHash);
|
||||
Vue.use(VueCarousel);
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(InfiniteLoading);
|
||||
Vue.use(Loading);
|
||||
Vue.use(VueMasonry);
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(VueTimeago, {
|
||||
name: 'Timeago',
|
||||
locale: 'en'
|
||||
});
|
||||
|
||||
Vue.component(
|
||||
'navbar',
|
||||
require('./../components/partials/navbar.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'notification-card',
|
||||
require('./components/NotificationCard.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'photo-presenter',
|
||||
require('./components/presenter/PhotoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-presenter',
|
||||
require('./components/presenter/VideoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'photo-album-presenter',
|
||||
require('./components/presenter/PhotoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-album-presenter',
|
||||
require('./components/presenter/VideoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'mixed-album-presenter',
|
||||
require('./components/presenter/MixedAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'post-menu',
|
||||
require('./components/PostMenu.vue').default
|
||||
);
|
||||
|
||||
// Vue.component(
|
||||
// 'announcements-card',
|
||||
// require('./components/AnnouncementsCard.vue').default
|
||||
// );
|
||||
|
||||
Vue.component(
|
||||
'story-component',
|
||||
require('./components/StoryTimelineComponent.vue').default
|
||||
);
|
||||
|
||||
const HomeComponent = () => import(/* webpackChunkName: "home.chunk" */ "./../components/Home.vue");
|
||||
const ComposeComponent = () => import(/* webpackChunkName: "compose.chunk" */ "./../components/Compose.vue");
|
||||
const PostComponent = () => import(/* webpackChunkName: "post.chunk" */ "./../components/Post.vue");
|
||||
const ProfileComponent = () => import(/* webpackChunkName: "profile.chunk" */ "./../components/Profile.vue");
|
||||
const MemoriesComponent = () => import(/* webpackChunkName: "discover~memories.chunk" */ "./../components/discover/Memories.vue");
|
||||
const MyHashtagComponent = () => import(/* webpackChunkName: "discover~myhashtags.chunk" */ "./../components/discover/Hashtags.vue");
|
||||
const AccountInsightsComponent = () => import(/* webpackChunkName: "daci.chunk" */ "./../components/discover/Insights.vue");
|
||||
const DiscoverFindFriendsComponent = () => import(/* webpackChunkName: "discover~findfriends.chunk" */ "./../components/discover/FindFriends.vue");
|
||||
const DiscoverServerFeedComponent = () => import(/* webpackChunkName: "discover~serverfeed.chunk" */ "./../components/discover/ServerFeed.vue");
|
||||
const DiscoverSettingsComponent = () => import(/* webpackChunkName: "discover~settings.chunk" */ "./../components/discover/Settings.vue");
|
||||
const DiscoverComponent = () => import(/* webpackChunkName: "discover.chunk" */ "./../components/Discover.vue");
|
||||
const NotificationsComponent = () => import(/* webpackChunkName: "notifications.chunk" */ "./../components/Notifications.vue");
|
||||
const DirectComponent = () => import(/* webpackChunkName: "dms.chunk" */ "./../components/Direct.vue");
|
||||
const DirectMessageComponent = () => import(/* webpackChunkName: "dms~message.chunk" */ "./../components/DirectMessage.vue");
|
||||
const ProfileFollowersComponent = () => import(/* webpackChunkName: "profile~followers.bundle" */ "./../components/ProfileFollowers.vue");
|
||||
const ProfileFollowingComponent = () => import(/* webpackChunkName: "profile~following.bundle" */ "./../components/ProfileFollowing.vue");
|
||||
const HashtagComponent = () => import(/* webpackChunkName: "discover~hashtag.bundle" */ "./../components/Hashtag.vue");
|
||||
const NotFoundComponent = () => import(/* webpackChunkName: "error404.bundle" */ "./../components/NotFound.vue");
|
||||
// const HelpComponent = () => import(/* webpackChunkName: "help.bundle" */ "./../components/HelpComponent.vue");
|
||||
// const KnowledgebaseComponent = () => import(/* webpackChunkName: "kb.bundle" */ "./../components/Knowledgebase.vue");
|
||||
// const AboutComponent = () => import(/* webpackChunkName: "about.bundle" */ "./../components/About.vue");
|
||||
// const ContactComponent = () => import(/* webpackChunkName: "contact.bundle" */ "./../components/Contact.vue");
|
||||
const LanguageComponent = () => import(/* webpackChunkName: "i18n.bundle" */ "./../components/Language.vue");
|
||||
// const PrivacyComponent = () => import(/* webpackChunkName: "static~privacy.bundle" */ "./../components/Privacy.vue");
|
||||
// const TermsComponent = () => import(/* webpackChunkName: "static~tos.bundle" */ "./../components/Terms.vue");
|
||||
const ChangelogComponent = () => import(/* webpackChunkName: "changelog.bundle" */ "./../components/Changelog.vue");
|
||||
|
||||
// import LiveComponent from "./../components/Live.vue";
|
||||
// import LivestreamsComponent from "./../components/Livestreams.vue";
|
||||
// import LivePlayerComponent from "./../components/LivePlayer.vue";
|
||||
// import LiveHelpComponent from "./../components/LiveHelp.vue";
|
||||
|
||||
// import DriveComponent from "./../components/Drive.vue";
|
||||
// import SettingsComponent from "./../components/Settings.vue";
|
||||
// import ProfileComponent from "./components/ProfileNext.vue";
|
||||
// import VideosComponent from "./../components/Videos.vue";
|
||||
// import GroupsComponent from "./../components/Groups.vue";
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
linkActiveClass: "active",
|
||||
|
||||
routes: [
|
||||
{
|
||||
path: "/i/web/timeline/:scope",
|
||||
name: 'timeline',
|
||||
component: HomeComponent,
|
||||
props: true
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/timeline/local",
|
||||
// component: LocalTimeline
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/timeline/global",
|
||||
// component: GlobalTimeline
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/drive",
|
||||
// name: 'drive',
|
||||
// component: DriveComponent,
|
||||
// props: true
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/groups",
|
||||
// name: 'groups',
|
||||
// component: GroupsComponent,
|
||||
// props: true
|
||||
// },
|
||||
{
|
||||
path: "/i/web/post/:id",
|
||||
name: 'post',
|
||||
component: PostComponent,
|
||||
props: true
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/profile/:id/live",
|
||||
// component: LivePlayerComponent,
|
||||
// props: true
|
||||
// },
|
||||
{
|
||||
path: "/i/web/profile/:id/followers",
|
||||
name: 'profile-followers',
|
||||
component: ProfileFollowersComponent,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: "/i/web/profile/:id/following",
|
||||
name: 'profile-following',
|
||||
component: ProfileFollowingComponent,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: "/i/web/profile/:id",
|
||||
name: 'profile',
|
||||
component: ProfileComponent,
|
||||
props: true
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/videos",
|
||||
// component: VideosComponent
|
||||
// },
|
||||
{
|
||||
path: "/i/web/discover",
|
||||
component: DiscoverComponent
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/stories",
|
||||
// component: HomeComponent
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/settings/*",
|
||||
// component: SettingsComponent,
|
||||
// props: true
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/settings",
|
||||
// component: SettingsComponent
|
||||
// },
|
||||
{
|
||||
path: "/i/web/compose",
|
||||
component: ComposeComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/notifications",
|
||||
component: NotificationsComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/direct/thread/:accountId",
|
||||
component: DirectMessageComponent,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: "/i/web/direct",
|
||||
component: DirectComponent
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/kb/:id",
|
||||
// name: "kb",
|
||||
// component: KnowledgebaseComponent,
|
||||
// props: true
|
||||
// },
|
||||
{
|
||||
path: "/i/web/hashtag/:id",
|
||||
name: "hashtag",
|
||||
component: HashtagComponent,
|
||||
props: true
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/help",
|
||||
// component: HelpComponent
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/about",
|
||||
// component: AboutComponent
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/contact",
|
||||
// component: ContactComponent
|
||||
// },
|
||||
{
|
||||
path: "/i/web/language",
|
||||
component: LanguageComponent
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/privacy",
|
||||
// component: PrivacyComponent
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/terms",
|
||||
// component: TermsComponent
|
||||
// },
|
||||
{
|
||||
path: "/i/web/whats-new",
|
||||
component: ChangelogComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/discover/my-memories",
|
||||
component: MemoriesComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/discover/my-hashtags",
|
||||
component: MyHashtagComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/discover/account-insights",
|
||||
component: AccountInsightsComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/discover/find-friends",
|
||||
component: DiscoverFindFriendsComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/discover/server-timelines",
|
||||
component: DiscoverServerFeedComponent
|
||||
},
|
||||
{
|
||||
path: "/i/web/discover/settings",
|
||||
component: DiscoverSettingsComponent
|
||||
},
|
||||
// {
|
||||
// path: "/i/web/livestreams",
|
||||
// component: LivestreamsComponent
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/live/help",
|
||||
// component: LiveHelpComponent
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/live/player",
|
||||
// component: LivePlayerComponent
|
||||
// },
|
||||
// {
|
||||
// path: "/i/web/live",
|
||||
// component: LiveComponent
|
||||
// },
|
||||
|
||||
{
|
||||
path: "/i/web",
|
||||
component: HomeComponent,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: "/i/web/*",
|
||||
component: NotFoundComponent,
|
||||
props: true
|
||||
},
|
||||
],
|
||||
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (to.hash) {
|
||||
return {
|
||||
selector: `[id='${to.hash.slice(1)}']`
|
||||
};
|
||||
} else {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function lss(name, def) {
|
||||
let key = 'pf_m2s.' + name;
|
||||
let ls = window.localStorage;
|
||||
if(ls.getItem(key)) {
|
||||
let val = ls.getItem(key);
|
||||
if(['pl', 'color-scheme'].includes(name)) {
|
||||
return val;
|
||||
}
|
||||
return ['true', true].includes(val);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
version: 1,
|
||||
hideCounts: lss('hc', false),
|
||||
autoloadComments: lss('ac', true),
|
||||
newReactions: lss('nr', true),
|
||||
fixedHeight: lss('fh', false),
|
||||
profileLayout: lss('pl', 'grid'),
|
||||
showDMPrivacyWarning: lss('dmpwarn', true),
|
||||
relationships: {},
|
||||
emoji: [],
|
||||
colorScheme: lss('color-scheme', 'system'),
|
||||
},
|
||||
|
||||
getters: {
|
||||
getVersion: state => {
|
||||
return state.version;
|
||||
},
|
||||
|
||||
getHideCounts: state => {
|
||||
return state.hideCounts;
|
||||
},
|
||||
|
||||
getAutoloadComments: state => {
|
||||
return state.autoloadComments;
|
||||
},
|
||||
|
||||
getNewReactions: state => {
|
||||
return state.newReactions;
|
||||
},
|
||||
|
||||
getFixedHeight: state => {
|
||||
return state.fixedHeight;
|
||||
},
|
||||
|
||||
getProfileLayout: state => {
|
||||
return state.profileLayout;
|
||||
},
|
||||
|
||||
getRelationship: (state) => (id) => {
|
||||
// let rel = state.relationships[id];
|
||||
// if(!rel || !rel.hasOwnProperty('id')) {
|
||||
// return axios.get('/api/pixelfed/v1/accounts/relationships', {
|
||||
// params: {
|
||||
// 'id[]': id
|
||||
// }
|
||||
// })
|
||||
// .then(res => {
|
||||
// let relationship = res.data;
|
||||
// // Vue.set(state.relationships, relationship.id, relationship);
|
||||
// state.commit('updateRelationship', res.data[0]);
|
||||
// return res.data[0];
|
||||
// })
|
||||
// .catch(err => {
|
||||
// return {};
|
||||
// })
|
||||
// } else {
|
||||
// return state.relationships[id];
|
||||
// }
|
||||
return state.relationships[id];
|
||||
},
|
||||
|
||||
getCustomEmoji: state => {
|
||||
return state.emoji;
|
||||
},
|
||||
|
||||
getColorScheme: state => {
|
||||
return state.colorScheme;
|
||||
},
|
||||
|
||||
getShowDMPrivacyWarning: state => {
|
||||
return state.showDMPrivacyWarning;
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
setVersion(state, value) {
|
||||
state.version = value;
|
||||
},
|
||||
|
||||
setHideCounts(state, value) {
|
||||
localStorage.setItem('pf_m2s.hc', value);
|
||||
state.hideCounts = value;
|
||||
},
|
||||
|
||||
setAutoloadComments(state, value) {
|
||||
localStorage.setItem('pf_m2s.ac', value);
|
||||
state.autoloadComments = value;
|
||||
},
|
||||
|
||||
setNewReactions(state, value) {
|
||||
localStorage.setItem('pf_m2s.nr', value);
|
||||
state.newReactions = value;
|
||||
},
|
||||
|
||||
setFixedHeight(state, value) {
|
||||
localStorage.setItem('pf_m2s.fh', value);
|
||||
state.fixedHeight = value;
|
||||
},
|
||||
|
||||
setProfileLayout(state, value) {
|
||||
localStorage.setItem('pf_m2s.pl', value);
|
||||
state.profileLayout = value;
|
||||
},
|
||||
|
||||
updateRelationship(state, relationships) {
|
||||
relationships.forEach((relationship) => {
|
||||
Vue.set(state.relationships, relationship.id, relationship)
|
||||
})
|
||||
},
|
||||
|
||||
updateCustomEmoji(state, emojis) {
|
||||
state.emoji = emojis;
|
||||
},
|
||||
|
||||
setColorScheme(state, value) {
|
||||
if(state.colorScheme == value) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('pf_m2s.color-scheme', value);
|
||||
state.colorScheme = value;
|
||||
const name = value == 'system' ? '' : (value == 'light' ? 'force-light-mode' : 'force-dark-mode');
|
||||
document.querySelector("body").className = name;
|
||||
if(name != 'system') {
|
||||
const payload = name == 'force-dark-mode' ? { dark_mode: 'on' } : {};
|
||||
axios.post('/settings/labs', payload);
|
||||
}
|
||||
},
|
||||
|
||||
setShowDMPrivacyWarning(state, value) {
|
||||
localStorage.setItem('pf_m2s.dmpwarn', value);
|
||||
state.showDMPrivacyWarning = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let i18nMessages = {
|
||||
en: require('./i18n/en.json'),
|
||||
ar: require('./i18n/ar.json'),
|
||||
ca: require('./i18n/ca.json'),
|
||||
de: require('./i18n/de.json'),
|
||||
el: require('./i18n/el.json'),
|
||||
es: require('./i18n/es.json'),
|
||||
eu: require('./i18n/eu.json'),
|
||||
fr: require('./i18n/fr.json'),
|
||||
he: require('./i18n/he.json'),
|
||||
gd: require('./i18n/gd.json'),
|
||||
gl: require('./i18n/gl.json'),
|
||||
id: require('./i18n/id.json'),
|
||||
it: require('./i18n/it.json'),
|
||||
ja: require('./i18n/ja.json'),
|
||||
nl: require('./i18n/nl.json'),
|
||||
pl: require('./i18n/pl.json'),
|
||||
pt: require('./i18n/pt.json'),
|
||||
ru: require('./i18n/ru.json'),
|
||||
uk: require('./i18n/uk.json'),
|
||||
vi: require('./i18n/vi.json'),
|
||||
};
|
||||
|
||||
let locale = document.querySelector('html').getAttribute('lang');
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: locale, // set locale
|
||||
fallbackLocale: 'en',
|
||||
messages: i18nMessages
|
||||
});
|
||||
|
||||
sync(store, router);
|
||||
|
||||
const App = new Vue({
|
||||
el: '#content',
|
||||
i18n,
|
||||
router,
|
||||
store
|
||||
});
|
||||
|
||||
axios.get('/api/v1/custom_emojis')
|
||||
.then(res => {
|
||||
if(res && res.data && res.data.length) {
|
||||
store.commit('updateCustomEmoji', res.data);
|
||||
}
|
||||
});
|
||||
|
||||
if(store.state.colorScheme) {
|
||||
const name = store.state.colorScheme == 'system' ? '' : (store.state.colorScheme == 'light' ? 'force-light-mode' : 'force-dark-mode');
|
||||
if(name != 'system') {
|
||||
document.querySelector("body").className = name;
|
||||
}
|
||||
}
|
||||
|
||||
pixelfed.readmore = () => {
|
||||
$('.read-more').each(function(k,v) {
|
||||
let el = $(this);
|
||||
let attr = el.attr('data-readmore');
|
||||
if(typeof attr !== typeof undefined && attr !== false) {
|
||||
return;
|
||||
}
|
||||
el.readmore({
|
||||
collapsedHeight: 45,
|
||||
heightMargin: 48,
|
||||
moreLink: '<a href="#" class="d-block small font-weight-bold text-dark text-center">Show more</a>',
|
||||
lessLink: '<a href="#" class="d-block small font-weight-bold text-dark text-center">Show less</a>',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
document.createEvent("TouchEvent");
|
||||
$('body').addClass('touch');
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
window.App = window.App || {};
|
||||
|
||||
// window.App.redirect = function() {
|
||||
// document.querySelectorAll('a').forEach(function(i,k) {
|
||||
// let a = i.getAttribute('href');
|
||||
// if(a && a.length > 5 && a.startsWith('https://')) {
|
||||
// let url = new URL(a);
|
||||
// if(url.host !== window.location.host && url.pathname !== '/i/redirect') {
|
||||
// i.setAttribute('href', '/i/redirect?url=' + encodeURIComponent(a));
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// window.App.boot = function() {
|
||||
// new Vue({ el: '#content'});
|
||||
// }
|
||||
|
||||
// window.addEventListener("load", () => {
|
||||
// if ("serviceWorker" in navigator) {
|
||||
// navigator.serviceWorker.register("/sw.js");
|
||||
// }
|
||||
// });
|
||||
|
||||
window.App.util = {
|
||||
compose: {
|
||||
post: (function() {
|
||||
let path = window.location.pathname;
|
||||
let whitelist = [
|
||||
'/',
|
||||
'/timeline/public'
|
||||
];
|
||||
if(whitelist.includes(path)) {
|
||||
$('#composeModal').modal('show');
|
||||
} else {
|
||||
window.location.href = '/?a=co';
|
||||
}
|
||||
}),
|
||||
circle: (function() {
|
||||
console.log('Unsupported method.');
|
||||
}),
|
||||
collection: (function() {
|
||||
console.log('Unsupported method.');
|
||||
}),
|
||||
loop: (function() {
|
||||
console.log('Unsupported method.');
|
||||
}),
|
||||
story: (function() {
|
||||
console.log('Unsupported method.');
|
||||
}),
|
||||
},
|
||||
time: (function() {
|
||||
return new Date;
|
||||
}),
|
||||
version: 1,
|
||||
format: {
|
||||
count: (function(count = 0, locale = 'en-GB', notation = 'compact') {
|
||||
if(count < 1) {
|
||||
return 0;
|
||||
}
|
||||
return new Intl.NumberFormat(locale, { notation: notation , compactDisplay: "short" }).format(count);
|
||||
}),
|
||||
timeAgo: (function(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
let interval = Math.floor(seconds / 63072000);
|
||||
if (interval < 0) {
|
||||
return "0s";
|
||||
}
|
||||
if (interval >= 1) {
|
||||
return interval + "y";
|
||||
}
|
||||
interval = Math.floor(seconds / 604800);
|
||||
if (interval >= 1) {
|
||||
return interval + "w";
|
||||
}
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval >= 1) {
|
||||
return interval + "d";
|
||||
}
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval >= 1) {
|
||||
return interval + "h";
|
||||
}
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval >= 1) {
|
||||
return interval + "m";
|
||||
}
|
||||
return Math.floor(seconds) + "s";
|
||||
}),
|
||||
timeAhead: (function(ts, short = true) {
|
||||
let date = Date.parse(ts);
|
||||
let diff = date - Date.parse(new Date());
|
||||
let seconds = Math.floor((diff) / 1000);
|
||||
let interval = Math.floor(seconds / 63072000);
|
||||
if (interval >= 1) {
|
||||
return interval + (short ? "y" : " years");
|
||||
}
|
||||
interval = Math.floor(seconds / 604800);
|
||||
if (interval >= 1) {
|
||||
return interval + (short ? "w" : " weeks");
|
||||
}
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval >= 1) {
|
||||
return interval + (short ? "d" : " days");
|
||||
}
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval >= 1) {
|
||||
return interval + (short ? "h" : " hours");
|
||||
}
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval >= 1) {
|
||||
return interval + (short ? "m" : " minutes");
|
||||
}
|
||||
return Math.floor(seconds) + (short ? "s" : " seconds");
|
||||
}),
|
||||
rewriteLinks: (function(i) {
|
||||
|
||||
let tag = i.innerText;
|
||||
|
||||
if(i.href.startsWith(window.location.origin)) {
|
||||
return i.href;
|
||||
}
|
||||
|
||||
if(tag.startsWith('#') == true) {
|
||||
tag = '/discover/tags/' + tag.substr(1) +'?src=rph';
|
||||
} else if(tag.startsWith('@') == true) {
|
||||
tag = '/' + i.innerText + '?src=rpp';
|
||||
} else {
|
||||
tag = '/i/redirect?url=' + encodeURIComponent(tag);
|
||||
}
|
||||
|
||||
return tag;
|
||||
})
|
||||
},
|
||||
filters: [
|
||||
['1984','filter-1977'],
|
||||
['Azen','filter-aden'],
|
||||
['Astairo','filter-amaro'],
|
||||
['Grassbee','filter-ashby'],
|
||||
['Bookrun','filter-brannan'],
|
||||
['Borough','filter-brooklyn'],
|
||||
['Farms','filter-charmes'],
|
||||
['Hairsadone','filter-clarendon'],
|
||||
['Cleana ','filter-crema'],
|
||||
['Catpatch','filter-dogpatch'],
|
||||
['Earlyworm','filter-earlybird'],
|
||||
['Plaid','filter-gingham'],
|
||||
['Kyo','filter-ginza'],
|
||||
['Yefe','filter-hefe'],
|
||||
['Goddess','filter-helena'],
|
||||
['Yards','filter-hudson'],
|
||||
['Quill','filter-inkwell'],
|
||||
['Rankine','filter-kelvin'],
|
||||
['Juno','filter-juno'],
|
||||
['Mark','filter-lark'],
|
||||
['Chill','filter-lofi'],
|
||||
['Van','filter-ludwig'],
|
||||
['Apache','filter-maven'],
|
||||
['May','filter-mayfair'],
|
||||
['Ceres','filter-moon'],
|
||||
['Knoxville','filter-nashville'],
|
||||
['Felicity','filter-perpetua'],
|
||||
['Sandblast','filter-poprocket'],
|
||||
['Daisy','filter-reyes'],
|
||||
['Elevate','filter-rise'],
|
||||
['Nevada','filter-sierra'],
|
||||
['Futura','filter-skyline'],
|
||||
['Sleepy','filter-slumber'],
|
||||
['Steward','filter-stinson'],
|
||||
['Savoy','filter-sutro'],
|
||||
['Blaze','filter-toaster'],
|
||||
['Apricot','filter-valencia'],
|
||||
['Gloming','filter-vesper'],
|
||||
['Walter','filter-walden'],
|
||||
['Poplar','filter-willow'],
|
||||
['Xenon','filter-xpro-ii']
|
||||
],
|
||||
filterCss: {
|
||||
'filter-1977': 'sepia(.5) hue-rotate(-30deg) saturate(1.4)',
|
||||
'filter-aden': 'sepia(.2) brightness(1.15) saturate(1.4)',
|
||||
'filter-amaro': 'sepia(.35) contrast(1.1) brightness(1.2) saturate(1.3)',
|
||||
'filter-ashby': 'sepia(.5) contrast(1.2) saturate(1.8)',
|
||||
'filter-brannan': 'sepia(.4) contrast(1.25) brightness(1.1) saturate(.9) hue-rotate(-2deg)',
|
||||
'filter-brooklyn': 'sepia(.25) contrast(1.25) brightness(1.25) hue-rotate(5deg)',
|
||||
'filter-charmes': 'sepia(.25) contrast(1.25) brightness(1.25) saturate(1.35) hue-rotate(-5deg)',
|
||||
'filter-clarendon': 'sepia(.15) contrast(1.25) brightness(1.25) hue-rotate(5deg)',
|
||||
'filter-crema': 'sepia(.5) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-2deg)',
|
||||
'filter-dogpatch': 'sepia(.35) saturate(1.1) contrast(1.5)',
|
||||
'filter-earlybird': 'sepia(.25) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-5deg)',
|
||||
'filter-gingham': 'contrast(1.1) brightness(1.1)',
|
||||
'filter-ginza': 'sepia(.25) contrast(1.15) brightness(1.2) saturate(1.35) hue-rotate(-5deg)',
|
||||
'filter-hefe': 'sepia(.4) contrast(1.5) brightness(1.2) saturate(1.4) hue-rotate(-10deg)',
|
||||
'filter-helena': 'sepia(.5) contrast(1.05) brightness(1.05) saturate(1.35)',
|
||||
'filter-hudson': 'sepia(.25) contrast(1.2) brightness(1.2) saturate(1.05) hue-rotate(-15deg)',
|
||||
'filter-inkwell': 'brightness(1.25) contrast(.85) grayscale(1)',
|
||||
'filter-kelvin': 'sepia(.15) contrast(1.5) brightness(1.1) hue-rotate(-10deg)',
|
||||
'filter-juno': 'sepia(.35) contrast(1.15) brightness(1.15) saturate(1.8)',
|
||||
'filter-lark': 'sepia(.25) contrast(1.2) brightness(1.3) saturate(1.25)',
|
||||
'filter-lofi': 'saturate(1.1) contrast(1.5)',
|
||||
'filter-ludwig': 'sepia(.25) contrast(1.05) brightness(1.05) saturate(2)',
|
||||
'filter-maven': 'sepia(.35) contrast(1.05) brightness(1.05) saturate(1.75)',
|
||||
'filter-mayfair': 'contrast(1.1) brightness(1.15) saturate(1.1)',
|
||||
'filter-moon': 'brightness(1.4) contrast(.95) saturate(0) sepia(.35)',
|
||||
'filter-nashville': 'sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg)',
|
||||
'filter-perpetua': 'contrast(1.1) brightness(1.25) saturate(1.1)',
|
||||
'filter-poprocket': 'sepia(.15) brightness(1.2)',
|
||||
'filter-reyes': 'sepia(.75) contrast(.75) brightness(1.25) saturate(1.4)',
|
||||
'filter-rise': 'sepia(.25) contrast(1.25) brightness(1.2) saturate(.9)',
|
||||
'filter-sierra': 'sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg)',
|
||||
'filter-skyline': 'sepia(.15) contrast(1.25) brightness(1.25) saturate(1.2)',
|
||||
'filter-slumber': 'sepia(.35) contrast(1.25) saturate(1.25)',
|
||||
'filter-stinson': 'sepia(.35) contrast(1.25) brightness(1.1) saturate(1.25)',
|
||||
'filter-sutro': 'sepia(.4) contrast(1.2) brightness(.9) saturate(1.4) hue-rotate(-10deg)',
|
||||
'filter-toaster': 'sepia(.25) contrast(1.5) brightness(.95) hue-rotate(-15deg)',
|
||||
'filter-valencia': 'sepia(.25) contrast(1.1) brightness(1.1)',
|
||||
'filter-vesper': 'sepia(.35) contrast(1.15) brightness(1.2) saturate(1.3)',
|
||||
'filter-walden': 'sepia(.35) contrast(.8) brightness(1.25) saturate(1.4)',
|
||||
'filter-willow': 'brightness(1.2) contrast(.85) saturate(.05) sepia(.2)',
|
||||
'filter-xpro-ii': 'sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg)'
|
||||
},
|
||||
emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
|
||||
],
|
||||
embed: {
|
||||
post: (function(url, caption = true, likes = false, layout = 'full') {
|
||||
let u = url + '/embed?';
|
||||
u += caption ? 'caption=true&' : 'caption=false&';
|
||||
u += likes ? 'likes=true&' : 'likes=false&';
|
||||
u += layout == 'compact' ? 'layout=compact' : 'layout=full';
|
||||
return '<iframe title="Pixelfed Post Embed" src="'+u+'" class="pixelfed__embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"></iframe><script async defer src="'+window.location.origin +'/embed.js"><\/script>';
|
||||
}),
|
||||
profile: (function(url) {
|
||||
let u = url + '/embed';
|
||||
return '<iframe title="Pixelfed Profile Embed" src="'+u+'" class="pixelfed__embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"></iframe><script async defer src="'+window.location.origin +'/embed.js"><\/script>';
|
||||
})
|
||||
},
|
||||
|
||||
clipboard: (function(data) {
|
||||
return navigator.clipboard.writeText(data);
|
||||
}),
|
||||
|
||||
navatar: (function() {
|
||||
$('#navbarDropdown .far').addClass('d-none');
|
||||
$('#navbarDropdown img').attr('src',window._sharedData.curUser.avatar)
|
||||
.removeClass('d-none')
|
||||
.addClass('rounded-circle border shadow')
|
||||
.attr('width', 34).attr('height', 34);
|
||||
})
|
||||
};
|
||||
|
||||
const warningTitleCSS = 'color:red; font-size:60px; font-weight: bold; -webkit-text-stroke: 1px black;';
|
||||
const warningDescCSS = 'font-size: 18px;';
|
||||
console.log('%cStop!', warningTitleCSS);
|
||||
console.log("%cThis is a browser feature intended for developers. If someone told you to copy and paste something here to enable a Pixelfed feature or \"hack\" someone's account, it is a scam and will give them access to your Pixelfed account.", warningDescCSS);
|
4
resources/assets/js/stories.js
vendored
Normal file
4
resources/assets/js/stories.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'story-viewer',
|
||||
require('./components/StoryViewer.vue').default
|
||||
);
|
29
resources/assets/sass/lib/manrope.scss
vendored
Normal file
29
resources/assets/sass/lib/manrope.scss
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: url(/fonts/xn7gYHE41ni1AdIRggexSvfedN4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/fonts/xn7gYHE41ni1AdIRggexSvfedN4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(/fonts/xn7gYHE41ni1AdIRggexSvfedN4.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
63
webpack.mix.js
vendored
63
webpack.mix.js
vendored
|
@ -1,8 +1,16 @@
|
|||
let mix = require('laravel-mix');
|
||||
const fs = require("fs");
|
||||
|
||||
mix.before(() => {
|
||||
fs.rmSync('public/js', { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
mix.sass('resources/assets/sass/app.scss', 'public/css')
|
||||
.sass('resources/assets/sass/appdark.scss', 'public/css')
|
||||
.sass('resources/assets/sass/admin.scss', 'public/css')
|
||||
.sass('resources/assets/sass/portfolio.scss', 'public/css')
|
||||
.sass('resources/assets/sass/spa.scss', 'public/css')
|
||||
.sass('resources/assets/sass/landing.scss', 'public/css').version();
|
||||
|
||||
mix.js('resources/assets/js/app.js', 'public/js')
|
||||
|
@ -16,24 +24,51 @@ mix.js('resources/assets/js/app.js', 'public/js')
|
|||
.js('resources/assets/js/compose-classic.js', 'public/js')
|
||||
.js('resources/assets/js/search.js', 'public/js')
|
||||
.js('resources/assets/js/developers.js', 'public/js')
|
||||
.js('resources/assets/js/loops.js', 'public/js')
|
||||
.js('resources/assets/js/hashtag.js', 'public/js')
|
||||
.js('resources/assets/js/collectioncompose.js', 'public/js')
|
||||
.js('resources/assets/js/collections.js', 'public/js')
|
||||
.js('resources/assets/js/profile-directory.js', 'public/js')
|
||||
.js('resources/assets/js/story-compose.js', 'public/js')
|
||||
.js('resources/assets/js/direct.js', 'public/js')
|
||||
.js('resources/assets/js/direct.js', 'public/js')
|
||||
.js('resources/assets/js/admin.js', 'public/js')
|
||||
.js('resources/assets/js/rempro.js', 'public/js')
|
||||
.js('resources/assets/js/rempos.js', 'public/js')
|
||||
.js('resources/assets/js/spa.js', 'public/js')
|
||||
.js('resources/assets/js/stories.js', 'public/js')
|
||||
.js('resources/assets/js/portfolio.js', 'public/js')
|
||||
.js('resources/assets/js/admin_invite.js', 'public/js')
|
||||
.js('resources/assets/js/landing.js', 'public/js')
|
||||
.vue({ version: 2 });
|
||||
|
||||
.extract([
|
||||
'lodash',
|
||||
'popper.js',
|
||||
'jquery',
|
||||
'axios',
|
||||
'bootstrap',
|
||||
'vue',
|
||||
'readmore-js'
|
||||
])
|
||||
.version();
|
||||
mix.extract();
|
||||
mix.version();
|
||||
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
mix.options({
|
||||
processCssUrls: false,
|
||||
terser: {
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
compress: true,
|
||||
output: {
|
||||
comments: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
mix.webpackConfig({
|
||||
optimization: {
|
||||
providedExports: false,
|
||||
sideEffects: false,
|
||||
usedExports: false,
|
||||
minimize: true,
|
||||
minimizer: [ new TerserPlugin({
|
||||
extractComments: false,
|
||||
})]
|
||||
},
|
||||
output: {
|
||||
chunkFilename: 'js/[name].[chunkhash].js',
|
||||
}
|
||||
});
|
||||
mix.autoload({
|
||||
jquery: ['$', 'jQuery', 'window.jQuery']
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue