Add Archive Posts

This commit is contained in:
Daniel Supernault 2021-07-26 22:49:46 -06:00
parent 6e45021fc2
commit e9ef0c887a
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
7 changed files with 346 additions and 80 deletions

View file

@ -15,7 +15,8 @@ use App\{
Media,
Notification,
Profile,
Status
Status,
StatusArchived
};
use App\Transformer\Api\{
AccountTransformer,
@ -39,6 +40,7 @@ use App\Jobs\VideoPipeline\{
use App\Services\NotificationService;
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Services\StatusService;
class BaseApiController extends Controller
{
@ -286,4 +288,75 @@ class BaseApiController extends Controller
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function archive(Request $request, $id)
{
abort_if(!$request->user(), 403);
$status = Status::whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId($request->user()->profile_id)
->findOrFail($id);
if($status->scope === 'archived') {
return [200];
}
$archive = new StatusArchived;
$archive->status_id = $status->id;
$archive->profile_id = $status->profile_id;
$archive->original_scope = $status->scope;
$archive->save();
$status->scope = 'archived';
$status->visibility = 'draft';
$status->save();
StatusService::del($status->id);
// invalidate caches
return [200];
}
public function unarchive(Request $request, $id)
{
abort_if(!$request->user(), 403);
$status = Status::whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId($request->user()->profile_id)
->findOrFail($id);
if($status->scope !== 'archived') {
return [200];
}
$archive = StatusArchived::whereStatusId($status->id)
->whereProfileId($status->profile_id)
->firstOrFail();
$status->scope = $archive->original_scope;
$status->visibility = $archive->original_scope;
$status->save();
$archive->delete();
return [200];
}
public function archivedPosts(Request $request)
{
abort_if(!$request->user(), 403);
$statuses = Status::whereProfileId($request->user()->profile_id)
->whereScope('archived')
->orderByDesc('id')
->simplePaginate(10);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
return $fractal->createData($resource)->toArray();
}
}

View file

@ -616,6 +616,8 @@
<div v-if="status && user.id != status.account.id && !relationship.blocking && !user.is_admin" class="list-group-item rounded cursor-pointer text-danger" @click="blockProfile()">Block</div>
<div v-if="status && user.id != status.account.id && relationship.blocking && !user.is_admin" class="list-group-item rounded cursor-pointer text-danger" @click="unblockProfile()">Unblock</div>
<a v-if="user && user.id != status.account.id && !user.is_admin" class="list-group-item rounded cursor-pointer text-danger text-decoration-none" :href="reportUrl()">Report</a>
<div v-if="status && user.id == status.account.id && status.visibility != 'archived'" class="list-group-item rounded cursor-pointer text-danger" @click="archivePost(status)">Archive</div>
<div v-if="status && user.id == status.account.id && status.visibility == 'archived'" class="list-group-item rounded cursor-pointer text-danger" @click="unarchivePost(status)">Unarchive</div>
<div v-if="status && (user.is_admin || user.id == status.account.id)" class="list-group-item rounded cursor-pointer text-danger" @click="deletePost(ctxMenuStatus)">Delete</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
</div>
@ -1757,6 +1759,29 @@ export default {
});
},
archivePost(status) {
if(window.confirm('Are you sure you want to archive this post?') == false) {
return;
}
axios.post('/api/pixelfed/v2/status/' + status.id + '/archive')
.then(res => {
this.$refs.ctxModal.hide();
window.location.href = '/';
});
},
unarchivePost(status) {
if(window.confirm('Are you sure you want to unarchive this post?') == false) {
return;
}
axios.post('/api/pixelfed/v2/status/' + status.id + '/unarchive')
.then(res => {
this.$refs.ctxModal.hide();
});
}
},
}
</script>

View file

@ -181,64 +181,68 @@
<li v-if="owner" class="nav-item border-top">
<a :class="this.mode == 'bookmarks' ? 'nav-link text-dark' : 'nav-link'" href="#" v-on:click.prevent="switchMode('bookmarks')"><i class="fas fa-bookmark"></i> <span class="d-none d-md-inline-block small pl-1">SAVED</span></a>
</li>
<li v-if="owner" class="nav-item border-top">
<a :class="this.mode == 'archives' ? 'nav-link text-dark' : 'nav-link'" href="#" v-on:click.prevent="switchMode('archives')"><i class="far fa-folder-open"></i> <span class="d-none d-md-inline-block small pl-1">ARCHIVES</span></a>
</li>
</ul>
</div>
<div class="container px-0">
<div class="profile-timeline mt-md-4">
<div class="row" v-if="mode == 'grid'">
<div class="col-4 p-1 p-md-3" v-for="(s, index) in timeline" :key="'tlob:'+index">
<a class="card info-overlay card-md-border-0" :href="statusUrl(s)" v-once>
<div class="square">
<div v-if="s.sensitive" class="square-content">
<div class="info-overlay-text-label">
<div v-if="mode == 'grid'">
<div class="row">
<div class="col-4 p-1 p-md-3" v-for="(s, index) in timeline" :key="'tlob:'+index">
<a class="card info-overlay card-md-border-0" :href="statusUrl(s)" v-once>
<div class="square">
<div v-if="s.sensitive" class="square-content">
<div class="info-overlay-text-label">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
</span>
</h5>
</div>
<blur-hash-canvas
width="32"
height="32"
:hash="s.media_attachments[0].blurhash"
/>
</div>
<div v-else class="square-content">
<blur-hash-image
width="32"
height="32"
:hash="s.media_attachments[0].blurhash"
:src="s.media_attachments[0].preview_url"
/>
</div>
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
</span>
</h5>
</div>
<blur-hash-canvas
width="32"
height="32"
:hash="s.media_attachments[0].blurhash"
/>
</div>
<div v-else class="square-content">
<blur-hash-image
width="32"
height="32"
:hash="s.media_attachments[0].blurhash"
:src="s.media_attachments[0].preview_url"
/>
</div>
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
</span>
</h5>
</div>
</a>
</div>
<div v-if="timeline.length == 0" class="col-12">
<div class="py-5 text-center text-muted">
<p><i class="fas fa-camera-retro fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">No posts yet</p>
</div>
</a>
</div>
<div v-if="timeline.length == 0" class="col-12">
<div class="py-5 text-center text-muted">
<p><i class="fas fa-camera-retro fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">No posts yet</p>
</div>
</div>
</div>
<div v-if="timeline.length && mode == 'grid'">
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
<div v-if="timeline.length">
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</div>
<div v-if="mode == 'bookmarks'">
<div v-if="bookmarksLoading">
@ -280,8 +284,9 @@
</div>
</div>
</div>
<div v-if="mode == 'collections'">
<div v-if="collections.length" class="row">
<div v-if="collections.length && collectionsLoaded" class="row">
<div class="col-4 p-1 p-sm-2 p-md-3" v-for="(c, index) in collections">
<a class="card info-overlay card-md-border-0" :href="c.url">
<div class="square">
@ -298,6 +303,28 @@
</div>
</div>
</div>
<div v-if="mode == 'archives'">
<div v-if="archives.length" class="col-12 col-md-8 offset-md-2 px-0 mb-sm-3 timeline mt-5">
<div class="alert alert-info">
<p class="mb-0">Posts you archive can only be seen by you.</p>
<p class="mb-0">For more information see the <a href="/site/kb/sharing-media">Sharing Media</a> help center page.</p>
</div>
<div v-for="(status, index) in archives">
<status-card
:class="{ 'border-top': index === 0 }"
:status="status"
:reaction-bar="false"
/>
</div>
<infinite-loading @infinite="archivesInfiniteLoader">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</div>
</div>
</div>
</div>
@ -663,6 +690,7 @@
</style>
<script type="text/javascript">
import VueMasonry from 'vue-masonry-css'
import StatusCard from './partials/StatusCard.vue';
export default {
props: [
@ -671,6 +699,11 @@
'profile-settings',
'profile-username'
],
components: {
StatusCard,
},
data() {
return {
ids: [],
@ -684,7 +717,7 @@
owner: false,
layout: this.profileLayout,
mode: 'grid',
modes: ['grid', 'collections', 'bookmarks'],
modes: ['grid', 'collections', 'bookmarks', 'archives'],
modalStatus: false,
relationship: {},
followers: [],
@ -700,6 +733,7 @@
bookmarks: [],
bookmarksPage: 2,
collections: [],
collectionsLoaded: false,
collectionsPage: 2,
isMobile: false,
ctxEmbedPayload: null,
@ -709,6 +743,8 @@
followingModalSearchCache: null,
followingModalTab: 'following',
bookmarksLoading: true,
archives: [],
archivesPage: 2
}
},
beforeMount() {
@ -734,6 +770,39 @@
}
}
if(u.has('m') && this.modes.includes(u.get('m'))) {
this.mode = u.get('m');
if(this.mode == 'bookmarks') {
axios.get('/api/local/bookmarks')
.then(res => {
this.bookmarks = res.data;
this.bookmarksLoading = false;
}).catch(err => {
this.mode = 'grid';
});
}
if(this.mode == 'collections') {
axios.get('/api/local/profile/collections/' + this.profileId)
.then(res => {
this.collections = res.data
this.collectionsLoaded = true;
}).catch(err => {
this.mode = 'grid';
});
}
if(this.mode == 'archives') {
axios.get('/api/pixelfed/v2/statuses/archives')
.then(res => {
this.archives = res.data;
}).catch(err => {
this.mode = 'grid';
});
}
}
},
mounted() {
@ -858,19 +927,15 @@
},
switchMode(mode) {
this.mode = _.indexOf(this.modes, mode) ? mode : 'grid';
if(this.mode == 'bookmarks' && this.bookmarks.length == 0) {
axios.get('/api/local/bookmarks')
.then(res => {
this.bookmarks = res.data;
this.bookmarksLoading = false;
});
}
if(this.mode == 'collections' && this.collections.length == 0) {
axios.get('/api/local/profile/collections/' + this.profileId)
.then(res => {
this.collections = res.data
});
if(mode == 'grid') {
this.mode = mode;
} else if(mode == 'bookmarks' && this.bookmarks.length) {
this.mode = 'bookmarks';
} else if(mode == 'collections' && this.collections.length) {
this.mode = 'collections';
} else {
window.location.href = '/' + this.profileUsername + '?m=' + mode;
return;
}
},
@ -1362,6 +1427,23 @@
joinedAtFormat(created) {
let d = new Date(created);
return d.toDateString();
},
archivesInfiniteLoader($state) {
axios.get('/api/pixelfed/v2/statuses/archives', {
params: {
page: this.archivesPage
}
}).then(res => {
if(res.data.length) {
this.archives.push(...res.data);
this.archivesPage++;
$state.loaded();
} else {
$state.complete();
}
});
}
}
}

View file

@ -11,14 +11,16 @@
<div class="list-group text-center">
<!-- <div v-if="status && status.account.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
<div v-if="status && status.account.id != profile.id && ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div> -->
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">View Post</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToProfile()">View Profile</div>
<div v-if="status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">View Post</div>
<div v-if="status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToProfile()">View Profile</div>
<!-- <div v-if="status && status.local == true && !status.in_reply_to_id" class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">Embed</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div> -->
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</div>
<div v-if="status && profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
<div v-if="status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</div>
<div v-if="status && profile && profile.is_admin == true && status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
<div v-if="status && status.account.id != profile.id" class="list-group-item rounded cursor-pointer text-danger" @click="ctxMenuReportPost()">Report</div>
<div v-if="status && (profile.is_admin || profile.id == status.account.id)" class="list-group-item rounded cursor-pointer text-danger" @click="deletePost(status)">Delete</div>
<div v-if="status && profile.id == status.account.id && status.visibility !== 'archived'" class="list-group-item rounded cursor-pointer text-danger" @click="archivePost(status)">Archive</div>
<div v-if="status && profile.id == status.account.id && status.visibility == 'archived'" class="list-group-item rounded cursor-pointer text-danger" @click="unarchivePost(status)">Unarchive</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" @click="deletePost(status)">Delete</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
</div>
</b-modal>
@ -680,6 +682,29 @@
ownerOrAdmin(status) {
return this.owner(status) || this.admin();
},
archivePost(status) {
if(window.confirm('Are you sure you want to archive this post?') == false) {
return;
}
axios.post('/api/pixelfed/v2/status/' + status.id + '/archive')
.then(res => {
this.$emit('status-delete', status.id);
this.closeModals();
});
},
unarchivePost(status) {
if(window.confirm('Are you sure you want to unarchive this post?') == false) {
return;
}
axios.post('/api/pixelfed/v2/status/' + status.id + '/unarchive')
.then(res => {
this.closeModals();
});
}
}
}
</script>

View file

@ -77,7 +77,10 @@
<div class="postPresenterContainer" style="background: #000;">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status" v-on:lightbox="lightbox" v-on:togglecw="status.sensitive = false"></photo-presenter>
<photo-presenter
:status="status"
v-on:lightbox="lightbox"
v-on:togglecw="status.sensitive = false"/>
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
@ -149,9 +152,13 @@
</div>
<div class="timestamp mt-2">
<p class="small mb-0">
<a :href="statusUrl(status)" class="text-muted text-uppercase">
<a v-if="status.visibility != 'archived'" :href="statusUrl(status)" class="text-muted text-uppercase">
<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
</a>
<span v-else class="text-muted text-uppercase">
Posted <timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
</span>
<span v-if="recommended">
<span class="px-1">&middot;</span>
<span class="text-muted">Based on popular and trending content</span>

View file

@ -104,7 +104,7 @@
<div>
You can upload the following media types:
<ul>
@foreach(explode(',', config('pixelfed.media_types')) as $type)
@foreach(explode(',', config_cache('pixelfed.media_types')) as $type)
<li class="font-weight-bold">{{$type}}</li>
@endforeach
</ul>
@ -171,4 +171,55 @@
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse12" role="button" aria-expanded="false" aria-controls="collapse11">
<i class="fas fa-chevron-down mr-2"></i>
What does archive mean?
</a>
<div class="collapse" id="collapse12">
<div>
You can archive your posts which prevents anyone from interacting or viewing it.
<br />
<strong class="text-danger">Archived posts cannot be deleted or otherwise interacted with. You may not recieve interactions (comments, likes, shares) from other servers while a post is archived.</strong>
<br />
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse13" role="button" aria-expanded="false" aria-controls="collapse11">
<i class="fas fa-chevron-down mr-2"></i>
How can I archive my posts?
</a>
<div class="collapse" id="collapse13">
<div>
To archive your posts:
<ul>
<li>Navigate to the post</li>
<li>Open the menu, click the <i class="fas fa-ellipsis-v text-muted mx-2 cursor-pointer"></i> or <i class="fas fa-ellipsis-h text-muted mx-2 cursor-pointer"></i> button</li>
<li>Click on <span class="small font-weight-bold cursor-pointer">Archive</span></li>
</ul>
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse14" role="button" aria-expanded="false" aria-controls="collapse11">
<i class="fas fa-chevron-down mr-2"></i>
How do I unarchive my posts?
</a>
<div class="collapse" id="collapse14">
<div>
To unarchive your posts:
<ul>
<li>Navigate to your profile</li>
<li>Click on the <strong>ARCHIVES</strong> tab</li>
<li>Scroll to the post you want to unarchive</li>
<li>Open the menu, click the <i class="fas fa-ellipsis-v text-muted mx-2 cursor-pointer"></i> or <i class="fas fa-ellipsis-h text-muted mx-2 cursor-pointer"></i> button</li>
<li>Click on <span class="small font-weight-bold cursor-pointer">Unarchive</span></li>
</ul>
</div>
</div>
</p>
@endsection

View file

@ -200,6 +200,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/posts/places', 'DiscoverController@trendingPlaces');
Route::get('seasonal/yir', 'SeasonalController@getData');
Route::post('seasonal/yir', 'SeasonalController@store');
Route::post('status/{id}/archive', 'ApiController@archive');
Route::post('status/{id}/unarchive', 'ApiController@unarchive');
Route::get('statuses/archives', 'ApiController@archivedPosts');
});
});