mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-22 13:03:16 +00:00
Add partial components
This commit is contained in:
parent
fff692a25c
commit
5361082026
21 changed files with 5715 additions and 0 deletions
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>
|
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>
|
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>
|
Loading…
Reference in a new issue