1
0
Эх сурвалжийг харах

Merge pull request #4166 from pixelfed/staging

Staging
daniel 2 жил өмнө
parent
commit
a3fb4d24c3

+ 1 - 0
.editorconfig

@@ -2,6 +2,7 @@ root = true
 
 [*]
 indent_size = 4
+indent_style = tab
 end_of_line = lf
 charset = utf-8
 trim_trailing_whitespace = true

+ 5 - 0
CHANGELOG.md

@@ -104,6 +104,11 @@
 - Update notifications component, improve UX with exponential retry and loading state ([937e6d07](https://github.com/pixelfed/pixelfed/commit/937e6d07))
 - Update likeModal and shareModal components, use new pagination logic and re-add Follow/Unfollow buttons ([b565ead6](https://github.com/pixelfed/pixelfed/commit/b565ead6))
 - Update profileFeed component, fix pagination ([7cf41628](https://github.com/pixelfed/pixelfed/commit/7cf41628))
+- Update ApiV1Controller, add BookmarkService logic to bookmark endpoints ([29b1af10](https://github.com/pixelfed/pixelfed/commit/29b1af10))
+- Update ApiV1Controller, filter conversations without last_status ([e8a6a8c7](https://github.com/pixelfed/pixelfed/commit/e8a6a8c7))
+- Update ApiV1Controller and BookmarkController, fix api differences and allow unbookmarking regardless of relationship ([e343061a](https://github.com/pixelfed/pixelfed/commit/e343061a))
+- Update ApiV1Controller, add pixelfed entity support to bookmarks endpoint ([94069db9](https://github.com/pixelfed/pixelfed/commit/94069db9))
+- Update PostReactions, reduce bookmark timeout to 2s from 5s ([a8094e6c](https://github.com/pixelfed/pixelfed/commit/a8094e6c))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)

+ 47 - 15
app/Http/Controllers/Api/ApiV1Controller.php

@@ -2335,7 +2335,7 @@ class ApiV1Controller extends Controller
 				return $res;
 			})
 			->filter(function($dm) {
-				return isset($dm['accounts']) && count($dm['accounts']);
+				return isset($dm['accounts']) && count($dm['accounts']) && !empty($dm['last_status']);
 			})
 			->unique(function($item, $key) {
 				return $item['accounts'][0]['id'];
@@ -2376,6 +2376,7 @@ class ApiV1Controller extends Controller
 
 		$res['favourited'] = LikeService::liked($user->profile_id, $res['id']);
 		$res['reblogged'] = ReblogService::get($user->profile_id, $res['id']);
+		$res['bookmarked'] = BookmarkService::get($user->profile_id, $res['id']);
 
 		return $this->json($res);
 	}
@@ -3004,6 +3005,7 @@ class ApiV1Controller extends Controller
 			'min_id' => 'nullable|integer|min:0'
 		]);
 
+		$pe = $request->has('_pe');
 		$pid = $request->user()->profile_id;
 		$limit = $request->input('limit') ?? 20;
 		$max_id = $request->input('max_id');
@@ -3017,8 +3019,15 @@ class ApiV1Controller extends Controller
             ->orderByDesc('id')
             ->cursorPaginate($limit);
 
-        $bookmarks = $bookmarkQuery->map(function($bookmark) {
-				return \App\Services\StatusService::getMastodon($bookmark->status_id);
+        $bookmarks = $bookmarkQuery->map(function($bookmark) use($pid, $pe) {
+				$status = $pe ? StatusService::get($bookmark->status_id, false) : StatusService::getMastodon($bookmark->status_id, false);
+
+				if($status) {
+					$status['bookmarked'] = true;
+					$status['favourited'] = LikeService::liked($pid, $status['id']);
+					$status['reblogged'] = ReblogService::get($pid, $status['id']);
+				}
+				return $status;
 			})
 			->filter()
 			->values()
@@ -3056,15 +3065,30 @@ class ApiV1Controller extends Controller
 	{
 		abort_if(!$request->user(), 403);
 
-		$status = Status::whereNull('uri')
-			->whereScope('public')
-			->findOrFail($id);
+		$status = Status::findOrFail($id);
+		$pid = $request->user()->profile_id;
+
+		abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
+		abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
+		abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
+
+		if($status->scope == 'private') {
+			abort_if(
+				$pid !== $status->profile_id && !FollowerService::follows($pid, $status->profile_id),
+				404,
+				'Error: You cannot bookmark private posts from accounts you do not follow.'
+			);
+		}
 
 		Bookmark::firstOrCreate([
 			'status_id' => $status->id,
-			'profile_id' => $request->user()->profile_id
+			'profile_id' => $pid
 		]);
-		$res = StatusService::getMastodon($status->id);
+
+		BookmarkService::add($pid, $status->id);
+
+		$res = StatusService::getMastodon($status->id, false);
+		$res['bookmarked'] = true;
 
 		return $this->json($res);
 	}
@@ -3080,15 +3104,23 @@ class ApiV1Controller extends Controller
 	{
 		abort_if(!$request->user(), 403);
 
-		$status = Status::whereNull('uri')
-			->whereScope('public')
-			->findOrFail($id);
+		$status = Status::findOrFail($id);
+		$pid = $request->user()->profile_id;
+
+		abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
+		abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
+		abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
 
 		$bookmark = Bookmark::whereStatusId($status->id)
-			->whereProfileId($request->user()->profile_id)
-			->firstOrFail();
-		$bookmark->delete();
-		$res = StatusService::getMastodon($status->id);
+			->whereProfileId($pid)
+			->first();
+
+		if($bookmark) {
+			BookmarkService::del($pid, $status->id);
+			$bookmark->delete();
+		}
+		$res = StatusService::getMastodon($status->id, false);
+		$res['bookmarked'] = false;
 
 		return $this->json($res);
 	}

+ 53 - 43
app/Http/Controllers/BookmarkController.php

@@ -11,47 +11,57 @@ use App\Services\FollowerService;
 
 class BookmarkController extends Controller
 {
-    public function __construct()
-    {
-        $this->middleware('auth');
-    }
-
-    public function store(Request $request)
-    {
-        $this->validate($request, [
-            'item' => 'required|integer|min:1',
-        ]);
-
-        $profile = Auth::user()->profile;
-        $status = Status::findOrFail($request->input('item'));
-
-        abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
-
-        if($status->scope == 'private') {
-            abort_if(
-                $profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id),
-                404,
-                'Error: You cannot bookmark private posts from accounts you do not follow.'
-            );
-        }
-
-        $bookmark = Bookmark::firstOrCreate(
-            ['status_id' => $status->id], ['profile_id' => $profile->id]
-        );
-
-        if (!$bookmark->wasRecentlyCreated) {
-        	BookmarkService::del($profile->id, $status->id);
-            $bookmark->delete();
-        } else {
-        	BookmarkService::add($profile->id, $status->id);
-        }
-
-        if ($request->ajax()) {
-            $response = ['code' => 200, 'msg' => 'Bookmark saved!'];
-        } else {
-            $response = redirect()->back();
-        }
-
-        return $response;
-    }
+	public function __construct()
+	{
+		$this->middleware('auth');
+	}
+
+	public function store(Request $request)
+	{
+		$this->validate($request, [
+			'item' => 'required|integer|min:1',
+		]);
+
+		$profile = Auth::user()->profile;
+		$status = Status::findOrFail($request->input('item'));
+
+		abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
+		abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
+		abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
+
+		if($status->scope == 'private') {
+			if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) {
+				if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) {
+					BookmarkService::del($profile->id, $status->id);
+					$exists->delete();
+
+					if ($request->ajax()) {
+						return ['code' => 200, 'msg' => 'Bookmark removed!'];
+					} else {
+						return redirect()->back();
+					}
+				}
+				abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
+			}
+		}
+
+		$bookmark = Bookmark::firstOrCreate(
+			['status_id' => $status->id], ['profile_id' => $profile->id]
+		);
+
+		if (!$bookmark->wasRecentlyCreated) {
+			BookmarkService::del($profile->id, $status->id);
+			$bookmark->delete();
+		} else {
+			BookmarkService::add($profile->id, $status->id);
+		}
+
+		if ($request->ajax()) {
+			$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
+		} else {
+			$response = redirect()->back();
+		}
+
+		return $response;
+	}
 }

BIN
public/js/daci.chunk.232f6f724c527858.js → public/js/daci.chunk.289add6be0f9f34f.js


BIN
public/js/discover~findfriends.chunk.e3a7e0813bc9e3ec.js → public/js/discover~findfriends.chunk.f9f303e4742d4d0e.js


BIN
public/js/discover~memories.chunk.487c14a0180fbf85.js → public/js/discover~memories.chunk.b6fd5951cd01560a.js


BIN
public/js/discover~myhashtags.chunk.075cc9fe49783f65.js → public/js/discover~myhashtags.chunk.ec2c96b72899819b.js


BIN
public/js/discover~serverfeed.chunk.c37e8a7a49d49297.js → public/js/discover~serverfeed.chunk.556f2541edd05a9c.js


BIN
public/js/discover~settings.chunk.ddc15c2d10514bf9.js → public/js/discover~settings.chunk.9bac38bba3619276.js


BIN
public/js/home.chunk.294faaa69171455b.js → public/js/home.chunk.64a8f34f10a4fa8b.js


BIN
public/js/manifest.js


BIN
public/js/post.chunk.dffb139831cf2ae9.js → public/js/post.chunk.d7408f11b67053fd.js


BIN
public/js/profile.chunk.99838eb369862e91.js


BIN
public/js/profile.chunk.e6ac60336120dcd5.js


BIN
public/mix-manifest.json


+ 251 - 0
resources/assets/components/partials/post/PostReactions.vue

@@ -0,0 +1,251 @@
+<template>
+	<div class="px-3 my-3" style="z-index:3;">
+		<div v-if="(status.favourites_count || status.reblogs_count) && ((status.hasOwnProperty('liked_by') && status.liked_by.url) || (status.hasOwnProperty('reblogs_count') && status.reblogs_count))" class="mb-0 d-flex justify-content-between">
+			<p v-if="!hideCounts && status.favourites_count" class="mb-2 reaction-liked-by">
+				Liked by
+				<span v-if="status.favourites_count == 1 && status.favourited == true" class="font-weight-bold">me</span>
+				<span v-else>
+					<router-link :to="'/i/web/profile/' + status.liked_by.id" class="primary font-weight-bold"">&commat;{{ status.liked_by.username}}</router-link>
+					<span v-if="status.liked_by.others || status.favourites_count > 1">
+						and <a href="#" class="primary font-weight-bold" @click.prevent="showLikes()">{{ count(status.favourites_count - 1) }} others</a>
+					</span>
+				</span>
+			</p>
+
+			<p v-if="!hideCounts && status.reblogs_count" class="mb-2 reaction-liked-by">
+				Shared by
+				<span v-if="status.reblogs_count == 1 && status.reblogged == true" class="font-weight-bold">me</span>
+				<a v-else class="primary font-weight-bold" href="#" @click.prevent="showShares()">
+					{{ count(status.reblogs_count) }} {{ status.reblogs_count > 1 ? 'others' : 'other' }}
+				</a>
+			</p>
+		</div>
+
+		<div class="d-flex justify-content-between" style="font-size: 14px !important;">
+			<div>
+				<button type="button" class="btn btn-light font-weight-bold rounded-pill mr-2" @click.prevent="like()">
+					<span v-if="status.favourited" class="primary">
+						<i class="fas fa-heart mr-md-1 text-danger fa-lg"></i>
+					</span>
+					<span v-else>
+						<i class="far fa-heart mr-md-2"></i>
+					</span>
+					<span v-if="likesCount && !hideCounts">
+						{{ count(likesCount)}}
+						<span class="d-none d-md-inline">{{ likesCount == 1 ? $t('common.like') : $t('common.likes') }}</span>
+					</span>
+					<span v-else>
+						<span class="d-none d-md-inline">{{ $t('common.like') }}</span>
+					</span>
+				</button>
+
+				<button v-if="!status.comments_disabled" type="button" class="btn btn-light font-weight-bold rounded-pill mr-2 px-3" @click="showComments()">
+					<i class="far fa-comment mr-md-2"></i>
+					<span v-if="replyCount && !hideCounts">
+						{{ count(replyCount) }}
+						<span class="d-none d-md-inline">{{ replyCount == 1 ? $t('common.comment') : $t('common.comments') }}</span>
+					</span>
+					<span v-else>
+						<span class="d-none d-md-inline">{{ $t('common.comment') }}</span>
+					</span>
+				</button>
+
+			</div>
+			<div>
+				<button
+					type="button"
+					class="btn btn-light font-weight-bold rounded-pill"
+					:disabled="isReblogging"
+					@click="handleReblog()">
+					<span v-if="isReblogging">
+						<b-spinner variant="warning" small />
+					</span>
+					<span v-else>
+						<i v-if="status.reblogged == true" class="fas fa-retweet fa-lg text-warning"></i>
+						<i v-else class="far fa-retweet"></i>
+
+						<span v-if="status.reblogs_count && !hideCounts" class="ml-md-2">
+							{{ count(status.reblogs_count) }}
+						</span>
+					</span>
+				</button>
+
+				<button
+					v-if="!status.in_reply_to_id && !status.reblog_of_id"
+					type="button"
+					class="btn btn-light font-weight-bold rounded-pill ml-3"
+					:disabled="isBookmarking"
+					@click="handleBookmark()">
+					<span v-if="isBookmarking">
+						<b-spinner variant="warning" small />
+					</span>
+					<span v-else>
+						<i v-if="status.hasOwnProperty('bookmarked_at') || (status.hasOwnProperty('bookmarked') && status.bookmarked == true)" class="fas fa-bookmark fa-lg text-warning"></i>
+						<i v-else class="far fa-bookmark"></i>
+					</span>
+				</button>
+
+				<button v-if="admin" type="button" class="ml-3 btn btn-light font-weight-bold rounded-pill" v-b-tooltip.hover title="Moderation Tools" @click="openModTools()">
+					<i class="far fa-user-crown"></i>
+				</button>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script type="text/javascript">
+	import CommentDrawer from './CommentDrawer.vue';
+	import ProfileHoverCard from './../profile/ProfileHoverCard.vue';
+
+	export default {
+		props: {
+			status: {
+				type: Object
+			},
+
+			profile: {
+				type: Object
+			},
+
+			admin: {
+				type: Boolean,
+				default: false
+			}
+		},
+
+		components: {
+			"comment-drawer": CommentDrawer,
+			"profile-hover-card": ProfileHoverCard
+		},
+
+		data() {
+			return {
+				key: 1,
+				menuLoading: true,
+				sensitive: false,
+				isReblogging: false,
+				isBookmarking: false,
+				owner: false,
+				license: false
+			}
+		},
+
+		computed: {
+			hideCounts: {
+				get() {
+					return this.$store.state.hideCounts == true;
+				}
+			},
+
+			autoloadComments: {
+				get() {
+					return this.$store.state.autoloadComments == true;
+				}
+			},
+
+			newReactions: {
+				get() {
+					return this.$store.state.newReactions;
+				},
+			},
+
+			likesCount: function() {
+				return this.status.favourites_count;
+			},
+
+			replyCount: function() {
+				return this.status.reply_count;
+			}
+		},
+
+		methods: {
+			count(val) {
+				return App.util.format.count(val);
+			},
+
+			like() {
+				event.currentTarget.blur();
+				if(this.status.favourited) {
+					this.$emit('unlike');
+				} else {
+					this.$emit('like');
+				}
+			},
+
+			showLikes() {
+				event.currentTarget.blur();
+				this.$emit('likes-modal');
+			},
+
+			showShares() {
+				event.currentTarget.blur();
+				this.$emit('shares-modal');
+			},
+
+			showComments() {
+				event.currentTarget.blur();
+				this.$emit('toggle-comments');
+			},
+
+			copyLink() {
+				event.currentTarget.blur();
+				App.util.clipboard(this.status.url);
+			},
+
+			shareToOther() {
+				if (navigator.canShare) {
+					navigator.share({
+						url: this.status.url
+					})
+					.then(() => console.log('Share was successful.'))
+					.catch((error) => console.log('Sharing failed', error));
+				} else {
+					swal('Not supported', 'Your current device does not support native sharing.', 'error');
+				}
+			},
+
+			counterChange(type) {
+				this.$emit('counter-change', type);
+			},
+
+			showCommentLikes(post) {
+				this.$emit('comment-likes-modal', post);
+			},
+
+			handleReblog() {
+				this.isReblogging = true;
+				if(this.status.reblogged) {
+					this.$emit('unshare');
+				} else {
+					this.$emit('share');
+				}
+
+				setTimeout(() => {
+					this.isReblogging = false;
+				}, 5000);
+			},
+
+			handleBookmark() {
+				event.currentTarget.blur();
+				this.isBookmarking = true;
+				this.$emit('bookmark');
+
+				setTimeout(() => {
+					this.isBookmarking = false;
+				}, 2000);
+			},
+
+			getStatusAvatar() {
+				if(window._sharedData.user.id == this.status.account.id) {
+					return window._sharedData.user.avatar;
+				}
+
+				return this.status.account.avatar;
+			},
+
+			openModTools() {
+				this.$emit('mod-tools');
+			}
+		}
+	}
+</script>

+ 1165 - 0
resources/assets/components/partials/profile/ProfileFeed.vue

@@ -0,0 +1,1165 @@
+<template>
+	<div class="profile-feed-component">
+		<div class="profile-feed-component-nav d-flex justify-content-center justify-content-md-between align-items-center mb-4">
+			<div class="d-none d-md-block border-bottom flex-grow-1 profile-nav-btns">
+				<div class="btn-group">
+					<button
+						class="btn btn-link"
+						:class="[ tabIndex === 1 ? 'active' : '' ]"
+						@click="toggleTab(1)"
+						>
+						Posts
+					</button>
+					<!-- <button
+						class="btn btn-link"
+						:class="[ tabIndex === 3 ? 'text-dark font-weight-bold' : 'text-lighter' ]"
+						@click="toggleTab(3)">
+						Albums
+					</button> -->
+
+					<button
+						v-if="isOwner"
+						class="btn btn-link"
+						:class="[ tabIndex === 'archives' ? 'active' : '' ]"
+						@click="toggleTab('archives')">
+						Archives
+					</button>
+
+					<button
+						v-if="isOwner"
+						class="btn btn-link"
+						:class="[ tabIndex === 'bookmarks' ? 'active' : '' ]"
+						@click="toggleTab('bookmarks')">
+						Bookmarks
+					</button>
+
+					<button
+						v-if="canViewCollections"
+						class="btn btn-link"
+						:class="[ tabIndex === 2 ? 'active' : '' ]"
+						@click="toggleTab(2)">
+						Collections
+					</button>
+
+					<button
+						v-if="isOwner"
+						class="btn btn-link"
+						:class="[ tabIndex === 3 ? 'active' : '' ]"
+						@click="toggleTab(3)">
+						Likes
+					</button>
+				</div>
+			</div>
+
+			<div v-if="tabIndex === 1" class="btn-group layout-sort-toggle">
+				<button
+					class="btn btn-sm"
+					:class="[ layoutIndex === 0 ? 'btn-dark' : 'btn-light' ]"
+					@click="toggleLayout(0, true)">
+					<i class="far fa-th fa-lg"></i>
+				</button>
+
+				<button
+					class="btn btn-sm"
+					:class="[ layoutIndex === 1 ? 'btn-dark' : 'btn-light' ]"
+					@click="toggleLayout(1, true)">
+					<i class="fas fa-th-large fa-lg"></i>
+				</button>
+
+				<button
+					class="btn btn-sm"
+					:class="[ layoutIndex === 2 ? 'btn-dark' : 'btn-light' ]"
+					@click="toggleLayout(2, true)">
+					<i class="far fa-bars fa-lg"></i>
+				</button>
+			</div>
+		</div>
+
+		<div v-if="tabIndex == 0" class="d-flex justify-content-center mt-5">
+			<b-spinner />
+		</div>
+
+		<div v-else-if="tabIndex == 1" class="px-0 mx-0">
+			<div v-if="layoutIndex === 0" class="row">
+				<div class="col-4 p-1" v-for="(s, index) in feed" :key="'tlob:'+index+s.id">
+					<a v-if="s.hasOwnProperty('pf_type') && s.pf_type == 'video'" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
+						<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">
+								</blur-hash-canvas>
+							</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">
+								</blur-hash-image>
+							</div>
+							<div class="info-overlay-text">
+								<div class="text-white m-auto">
+									<p class="info-overlay-text-field font-weight-bold">
+										<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
+										<span class="d-flex-inline">{{formatCount(s.favourites_count)}}</span>
+									</p>
+
+									<p class="info-overlay-text-field font-weight-bold">
+										<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
+										<span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
+									</p>
+
+									<p class="mb-0 info-overlay-text-field font-weight-bold">
+										<span class="far fa-sync fa-lg p-2 d-flex-inline"></span>
+										<span class="d-flex-inline">{{formatCount(s.reblogs_count)}}</span>
+									</p>
+								</div>
+							</div>
+						</div>
+
+						<span class="badge badge-light video-overlay-badge">
+							<i class="far fa-video fa-2x"></i>
+						</span>
+
+						<span class="badge badge-light timestamp-overlay-badge">
+							{{ timeago(s.created_at) }}
+						</span>
+					</a>
+					<a v-else class="card info-overlay card-md-border-0" :href="statusUrl(s)">
+						<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">
+								</blur-hash-canvas>
+							</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].url">
+								</blur-hash-image>
+							</div>
+							<div class="info-overlay-text">
+								<div class="text-white m-auto">
+									<p class="info-overlay-text-field font-weight-bold">
+										<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
+										<span class="d-flex-inline">{{formatCount(s.favourites_count)}}</span>
+									</p>
+
+									<p class="info-overlay-text-field font-weight-bold">
+										<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
+										<span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
+									</p>
+
+									<p class="mb-0 info-overlay-text-field font-weight-bold">
+										<span class="far fa-sync fa-lg p-2 d-flex-inline"></span>
+										<span class="d-flex-inline">{{formatCount(s.reblogs_count)}}</span>
+									</p>
+								</div>
+							</div>
+						</div>
+
+						<span class="badge badge-light timestamp-overlay-badge">
+							{{ timeago(s.created_at) }}
+						</span>
+					</a>
+				</div>
+
+				<intersect v-if="canLoadMore" @enter="enterIntersect">
+					<div class="col-4 ph-wrapper">
+						<div class="ph-item">
+						   <div class="ph-picture big"></div>
+					   </div>
+				   </div>
+			   </intersect>
+			</div>
+
+			<div v-else-if="layoutIndex === 1" class="row">
+				<masonry
+					:cols="{default: 3, 800: 2}"
+					:gutter="{default: '5px'}">
+
+					<div class="p-1" v-for="(s, index) in feed" :key="'tlog:'+index+s.id">
+						<a v-if="s.hasOwnProperty('pf_type') && s.pf_type == 'video'" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
+							<div class="square">
+								<div class="square-content">
+									<blur-hash-image
+										width="32"
+										height="32"
+										class="rounded"
+										:hash="s.media_attachments[0].blurhash"
+										:src="s.media_attachments[0].preview_url">
+									</blur-hash-image>
+								</div>
+							</div>
+
+							<span class="badge badge-light video-overlay-badge">
+								<i class="far fa-video fa-2x"></i>
+							</span>
+
+							<span class="badge badge-light timestamp-overlay-badge">
+								{{ timeago(s.created_at) }}
+							</span>
+						</a>
+
+						<a v-else-if="s.sensitive" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
+							<div class="square">
+								<div class="square-content">
+									<div class="info-overlay-text-label rounded">
+										<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"
+										class="rounded"
+										:hash="s.media_attachments[0].blurhash">
+									</blur-hash-canvas>
+								</div>
+							</div>
+						</a>
+
+						<a v-else class="card info-overlay card-md-border-0" :href="statusUrl(s)">
+							<img :src="previewUrl(s)" class="img-fluid w-100 rounded-lg" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0'">
+							<span class="badge badge-light timestamp-overlay-badge">
+								{{ timeago(s.created_at) }}
+							</span>
+						</a>
+					</div>
+
+					<intersect v-if="canLoadMore" @enter="enterIntersect">
+						<div class="p-1 ph-wrapper">
+							<div class="ph-item">
+								<div class="ph-picture big"></div>
+							</div>
+						</div>
+					</intersect>
+				</masonry>
+			</div>
+
+			<div v-else-if="layoutIndex === 2" class="row justify-content-center">
+				<div class="col-12 col-md-10">
+					<status-card
+						v-for="(s, index) in feed"
+						:key="'prs'+s.id+':'+index"
+						:profile="user"
+						:status="s"
+						v-on:like="likeStatus(index)"
+						v-on:unlike="unlikeStatus(index)"
+						v-on:share="shareStatus(index)"
+						v-on:unshare="unshareStatus(index)"
+						v-on:menu="openContextMenu(index)"
+						v-on:counter-change="counterChange(index, $event)"
+						v-on:likes-modal="openLikesModal(index)"
+						v-on:shares-modal="openSharesModal(index)"
+						v-on:comment-likes-modal="openCommentLikesModal"
+						v-on:handle-report="handleReport" />
+				</div>
+
+				<intersect v-if="canLoadMore" @enter="enterIntersect">
+					<div class="col-12 col-md-10">
+						<status-placeholder style="margin-bottom: 10rem;" />
+					 </div>
+				</intersect>
+			</div>
+
+			<div v-if="feedLoaded && !feed.length">
+				<div class="row justify-content-center">
+					<div class="col-12 col-md-8 text-center">
+						<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
+						<p class="lead text-muted font-weight-bold">{{ $t('profile.emptyPosts') }}</p>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<div v-else-if="tabIndex === 'private'" class="row justify-content-center">
+			<div class="col-12 col-md-8 text-center">
+				<img src="/img/illustrations/dk-secure-feed.svg" class="img-fluid" style="opacity: 0.6;">
+				<p class="h3 text-dark font-weight-bold mt-3 py-3">This profile is private</p>
+				<div class="lead text-muted px-3">
+					Only approved followers can see <span class="font-weight-bold text-dark text-break">&commat;{{ profile.acct }}</span>'s <br />
+					posts. To request access, click <span class="font-weight-bold">Follow</span>.
+				</div>
+			</div>
+		</div>
+
+		<div v-else-if="tabIndex == 2" class="row justify-content-center">
+			<div class="col-12 col-md-8">
+				<div class="list-group">
+					<a
+						v-for="(collection, index) in collections"
+						class="list-group-item text-decoration-none text-dark"
+						:href="collection.url">
+						<div class="media">
+							<img :src="collection.thumb" width="65" height="65" style="object-fit: cover;" class="rounded-lg border mr-3" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
+							<div class="media-body text-left">
+								<p class="lead mb-0">{{ collection.title ? collection.title : 'Untitled' }}</p>
+								<p class="small text-muted mb-1">{{ collection.description || 'No description available' }}</p>
+								<p class="small text-lighter mb-0 font-weight-bold">
+									<span>{{ collection.post_count }} posts</span>
+									<span>&middot;</span>
+									<span v-if="collection.visibility === 'public'" class="text-dark">Public</span>
+									<span v-else-if="collection.visibility === 'private'" class="text-dark"><i class="far fa-lock fa-sm"></i> Followers Only</span>
+									<span v-else-if="collection.visibility === 'draft'" class="primary"><i class="far fa-lock fa-sm"></i> Draft</span>
+									<span>&middot;</span>
+									<span v-if="collection.published_at">Created {{ timeago(collection.published_at) }} ago</span>
+									<span v-else class="text-warning">UNPUBLISHED</span>
+								</p>
+							</div>
+						</div>
+					</a>
+				</div>
+			</div>
+
+			<div v-if="collectionsLoaded && !collections.length" class="col-12 col-md-8 text-center">
+				<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
+				<p class="lead text-muted font-weight-bold">{{ $t('profile.emptyCollections') }}</p>
+			</div>
+
+			<div v-if="canLoadMoreCollections" class="col-12 col-md-8">
+				<intersect @enter="enterCollectionsIntersect">
+					<div class="d-flex justify-content-center mt-5">
+						<b-spinner small />
+					</div>
+				</intersect>
+			</div>
+		</div>
+
+		<div v-else-if="tabIndex == 3" class="px-0 mx-0">
+			<div class="row justify-content-center">
+				<div class="col-12 col-md-10">
+					<status-card
+						v-for="(s, index) in favourites"
+						:key="'prs'+s.id+':'+index"
+						:profile="user"
+						:status="s"
+						v-on:like="likeStatus(index)"
+						v-on:unlike="unlikeStatus(index)"
+						v-on:share="shareStatus(index)"
+						v-on:unshare="unshareStatus(index)"
+						v-on:counter-change="counterChange(index, $event)"
+						v-on:likes-modal="openLikesModal(index)"
+						v-on:comment-likes-modal="openCommentLikesModal"
+						v-on:handle-report="handleReport" />
+				</div>
+
+				<div v-if="canLoadMoreFavourites" class="col-12 col-md-10">
+					<intersect @enter="enterFavouritesIntersect">
+						<status-placeholder style="margin-bottom: 10rem;" />
+					</intersect>
+				</div>
+			</div>
+
+			<div v-if="!favourites || !favourites.length" class="row justify-content-center">
+				<div class="col-12 col-md-8 text-center">
+					<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
+					<p class="lead text-muted font-weight-bold">We can't seem to find any posts you have liked</p>
+				</div>
+			</div>
+		</div>
+
+		<div v-else-if="tabIndex == 'bookmarks'" class="px-0 mx-0">
+			<div class="row justify-content-center">
+				<div class="col-12 col-md-10">
+					<status-card
+						v-for="(s, index) in bookmarks"
+						:key="'prs'+s.id+':'+index"
+						:profile="user"
+						:new-reactions="true"
+						:status="s"
+						v-on:menu="openContextMenu(index)"
+						v-on:counter-change="counterChange(index, $event)"
+						v-on:likes-modal="openLikesModal(index)"
+						v-on:bookmark="handleBookmark(index)"
+						v-on:comment-likes-modal="openCommentLikesModal"
+						v-on:handle-report="handleReport" />
+				</div>
+
+				<div class="col-12 col-md-10">
+					<intersect v-if="canLoadMoreBookmarks" @enter="enterBookmarksIntersect">
+						<status-placeholder style="margin-bottom: 10rem;" />
+					</intersect>
+				</div>
+			</div>
+
+			<div v-if="!bookmarks || !bookmarks.length" class="row justify-content-center">
+				<div class="col-12 col-md-8 text-center">
+					<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
+					<p class="lead text-muted font-weight-bold">We can't seem to find any posts you have bookmarked</p>
+				</div>
+			</div>
+		</div>
+
+		<div v-else-if="tabIndex == 'archives'" class="px-0 mx-0">
+			<div class="row justify-content-center">
+				<div class="col-12 col-md-10">
+					<status-card
+						v-for="(s, index) in archives"
+						:key="'prarc'+s.id+':'+index"
+						:profile="user"
+						:new-reactions="true"
+						:reaction-bar="false"
+						:status="s"
+						v-on:menu="openContextMenu(index, 'archive')"
+						/>
+				</div>
+
+				<div v-if="canLoadMoreArchives" class="col-12 col-md-10">
+					<intersect @enter="enterArchivesIntersect">
+						<status-placeholder style="margin-bottom: 10rem;" />
+					</intersect>
+				</div>
+			</div>
+
+			<div v-if="!archives || !archives.length" class="row justify-content-center">
+				<div class="col-12 col-md-8 text-center">
+					<img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
+					<p class="lead text-muted font-weight-bold">We can't seem to find any posts you have bookmarked</p>
+				</div>
+			</div>
+		</div>
+
+		<context-menu
+			v-if="showMenu"
+			ref="contextMenu"
+			:status="contextMenuPost"
+			:profile="user"
+			v-on:moderate="commitModeration"
+			v-on:delete="deletePost"
+			v-on:archived="handleArchived"
+			v-on:unarchived="handleUnarchived"
+			v-on:report-modal="handleReport"
+		/>
+
+		<likes-modal
+			v-if="showLikesModal"
+			ref="likesModal"
+			:status="likesModalPost"
+			:profile="user"
+		/>
+
+		<shares-modal
+			v-if="showSharesModal"
+			ref="sharesModal"
+			:status="sharesModalPost"
+			:profile="profile"
+		/>
+
+		<report-modal
+			ref="reportModal"
+			:key="reportedStatusId"
+			:status="reportedStatus"
+		/>
+	</div>
+</template>
+
+<script type="text/javascript">
+	import Intersect from 'vue-intersect'
+	import StatusCard from './../TimelineStatus.vue';
+	import StatusPlaceholder from './../StatusPlaceholder.vue';
+	import BlurHashCanvas from './../BlurhashCanvas.vue';
+	import ContextMenu from './../../partials/post/ContextMenu.vue';
+	import LikesModal from './../../partials/post/LikeModal.vue';
+	import SharesModal from './../../partials/post/ShareModal.vue';
+	import ReportModal from './../../partials/modal/ReportPost.vue';
+	import { parseLinkHeader } from '@web3-storage/parse-link-header';
+
+	export default {
+		props: {
+			profile: {
+				type: Object
+			},
+
+			relationship: {
+				type: Object
+			}
+		},
+
+		components: {
+			"intersect": Intersect,
+			"status-card": StatusCard,
+			"bh-canvas": BlurHashCanvas,
+			"status-placeholder": StatusPlaceholder,
+			"context-menu": ContextMenu,
+			"likes-modal": LikesModal,
+			"shares-modal": SharesModal,
+			"report-modal": ReportModal
+		},
+
+		data() {
+			return {
+				isLoaded: false,
+				user: {},
+				isOwner: false,
+				layoutIndex: 0,
+				tabIndex: 0,
+				ids: [],
+				feed: [],
+				feedLoaded: false,
+				collections: [],
+				collectionsLoaded: false,
+				canLoadMore: false,
+				max_id: 1,
+				isIntersecting: false,
+				postIndex: 0,
+				showMenu: false,
+				showLikesModal: false,
+				likesModalPost: {},
+				showReportModal: false,
+				reportedStatus: {},
+				reportedStatusId: 0,
+				favourites: [],
+				favouritesLoaded: false,
+				favouritesPage: 1,
+				canLoadMoreFavourites: false,
+				bookmarks: [],
+				bookmarksLoaded: false,
+				bookmarksPage: 1,
+				bookmarksCursor: undefined,
+				canLoadMoreBookmarks: false,
+				canLoadMoreCollections: false,
+				collectionsPage: 1,
+				isCollectionsIntersecting: false,
+				canViewCollections: false,
+				showSharesModal: false,
+				sharesModalPost: {},
+				archives: [],
+				archivesLoaded: false,
+				archivesPage: 1,
+				canLoadMoreArchives: false,
+				contextMenuPost: {}
+			}
+		},
+
+		mounted() {
+			this.init();
+		},
+
+		methods: {
+			init() {
+				this.user = window._sharedData.user;
+
+				if(this.$store.state.profileLayout != 'grid') {
+					let index = this.$store.state.profileLayout === 'masonry' ? 1 : 2;
+					this.toggleLayout(index);
+				}
+
+				if(this.user) {
+					this.isOwner = this.user.id == this.profile.id;
+					if(this.isOwner) {
+						this.canViewCollections = true;
+					}
+				}
+
+				if(this.profile.locked) {
+					this.privateProfileCheck();
+				} else {
+					if(this.profile.local) {
+						this.canViewCollections = true;
+					}
+					this.fetchFeed();
+				}
+			},
+
+			privateProfileCheck() {
+				if(this.relationship.following || this.isOwner) {
+					this.canViewCollections = true;
+					this.fetchFeed();
+				} else {
+					this.tabIndex = 'private';
+					this.isLoaded = true;
+				}
+			},
+
+			fetchFeed() {
+				axios.get('/api/pixelfed/v1/accounts/' + this.profile.id + '/statuses', {
+					params: {
+						limit: 9,
+						only_media: true,
+						min_id: 1
+					}
+				})
+				.then(res => {
+					this.tabIndex = 1;
+					let data = res.data.filter(status => status.media_attachments.length > 0);
+					let ids = data.map(status => status.id);
+					this.ids = ids;
+					this.max_id = Math.min(...ids);
+					data.forEach(s => {
+						this.feed.push(s);
+					});
+					setTimeout(() => {
+					   this.canLoadMore = res.data.length > 1;
+					   this.feedLoaded = true;
+					}, 500);
+				});
+			},
+
+			enterIntersect() {
+				if(this.isIntersecting) {
+					return;
+				}
+				this.isIntersecting = true;
+
+				axios.get('/api/pixelfed/v1/accounts/' + this.profile.id + '/statuses', {
+					params: {
+						limit: 9,
+						only_media: true,
+						max_id: this.max_id,
+					}
+				})
+				.then(res => {
+					if(!res.data || !res.data.length) {
+						this.canLoadMore = false;
+					}
+					let data = res.data
+						.filter(status => status.media_attachments.length > 0)
+						.filter(status => this.ids.indexOf(status.id) == -1)
+
+					if(!data || !data.length) {
+						this.canLoadMore = false;
+						this.isIntersecting = false;
+						return;
+					}
+
+					let filtered = data.forEach(status => {
+							if(status.id < this.max_id) {
+								this.max_id = status.id;
+							} else {
+								this.max_id--;
+							}
+							this.ids.push(status.id);
+							this.feed.push(status);
+						});
+					this.isIntersecting = false;
+					this.canLoadMore = res.data.length >= 1;
+				}).catch(err => {
+					this.canLoadMore = false;
+				});
+			},
+
+			toggleLayout(idx, blur = false) {
+				if(blur) {
+					event.currentTarget.blur();
+				}
+				this.layoutIndex = idx;
+				this.isIntersecting = false;
+			},
+
+			toggleTab(idx) {
+				event.currentTarget.blur();
+
+				switch(idx) {
+					case 1:
+						this.isIntersecting = false;
+						this.tabIndex = 1;
+					break;
+
+					case 2:
+						this.fetchCollections();
+					break;
+
+					case 3:
+						this.fetchFavourites();
+					break;
+
+					case 'bookmarks':
+						this.fetchBookmarks();
+					break;
+
+					case 'archives':
+						this.fetchArchives();
+					break;
+				}
+			},
+
+			fetchCollections() {
+				if(this.collectionsLoaded) {
+					this.tabIndex = 2;
+				}
+
+				axios.get('/api/local/profile/collections/' + this.profile.id)
+				.then(res => {
+					this.collections = res.data;
+					this.collectionsLoaded = true;
+					this.tabIndex = 2;
+					this.collectionsPage++;
+					this.canLoadMoreCollections = res.data.length === 9;
+				})
+			},
+
+			enterCollectionsIntersect() {
+				if(this.isCollectionsIntersecting) {
+					return;
+				}
+				this.isCollectionsIntersecting = true;
+
+				axios.get('/api/local/profile/collections/' + this.profile.id, {
+					params: {
+						limit: 9,
+						page: this.collectionsPage
+					}
+				})
+				.then(res => {
+					if(!res.data || !res.data.length) {
+						this.canLoadMoreCollections = false;
+					}
+					this.collectionsLoaded = true;
+					this.collections.push(...res.data);
+					this.collectionsPage++;
+					this.canLoadMoreCollections = res.data.length > 0;
+					this.isCollectionsIntersecting = false;
+				}).catch(err => {
+					this.canLoadMoreCollections = false;
+					this.isCollectionsIntersecting = false;
+				});
+			},
+
+			fetchFavourites() {
+				this.tabIndex = 0;
+				axios.get('/api/pixelfed/v1/favourites')
+				.then(res => {
+					this.tabIndex = 3;
+					this.favourites = res.data;
+					this.favouritesPage++;
+					this.favouritesLoaded = true;
+
+					if(res.data.length != 0) {
+						this.canLoadMoreFavourites = true;
+					}
+				})
+			},
+
+			enterFavouritesIntersect() {
+				if(this.isIntersecting) {
+					return;
+				}
+				this.isIntersecting = true;
+
+				axios.get('/api/pixelfed/v1/favourites', {
+					params: {
+						page: this.favouritesPage,
+					}
+				})
+				.then(res => {
+					this.favourites.push(...res.data);
+					this.favouritesPage++;
+					this.canLoadMoreFavourites = res.data.length != 0;
+					this.isIntersecting = false;
+				})
+				.catch(err => {
+					this.canLoadMoreFavourites = false;
+				})
+			},
+
+			fetchBookmarks() {
+				this.tabIndex = 0;
+				axios.get('/api/v1/bookmarks', {
+					params: {
+						'_pe': 1
+					}
+				})
+				.then(res => {
+					this.tabIndex = 'bookmarks';
+					this.bookmarks = res.data;
+
+					if(res.headers && res.headers.link) {
+						const links = parseLinkHeader(res.headers.link);
+						if(links.next) {
+							this.bookmarksPage = links.next.cursor;
+							this.canLoadMoreBookmarks = true;
+						} else {
+							this.canLoadMoreBookmarks = false;
+						}
+					}
+
+					this.bookmarksLoaded = true;
+				})
+			},
+
+			enterBookmarksIntersect() {
+				if(this.isIntersecting) {
+					return;
+				}
+				this.isIntersecting = true;
+
+				axios.get('/api/v1/bookmarks', {
+					params: {
+						'_pe': 1,
+						cursor: this.bookmarksPage,
+					}
+				})
+				.then(res => {
+					this.bookmarks.push(...res.data);
+					if(res.headers && res.headers.link) {
+						const links = parseLinkHeader(res.headers.link);
+						if(links.next) {
+							this.bookmarksPage = links.next.cursor;
+							this.canLoadMoreBookmarks = true;
+						} else {
+							this.canLoadMoreBookmarks = false;
+						}
+					}
+					this.isIntersecting = false;
+				})
+				.catch(err => {
+					this.canLoadMoreBookmarks = false;
+				})
+			},
+
+			fetchArchives() {
+				this.tabIndex = 0;
+				axios.get('/api/pixelfed/v2/statuses/archives')
+				.then(res => {
+					this.tabIndex = 'archives';
+					this.archives = res.data;
+					this.archivesPage++;
+					this.archivesLoaded = true;
+
+					if(res.data.length != 0) {
+						this.canLoadMoreArchives = true;
+					}
+				})
+			},
+
+			formatCount(val) {
+				return App.util.format.count(val);
+			},
+
+			statusUrl(s) {
+				return '/i/web/post/' + s.id;
+			},
+
+			previewUrl(status) {
+				return status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].url;
+			},
+
+			timeago(ts) {
+				return App.util.format.timeAgo(ts);
+			},
+
+			likeStatus(index) {
+				let status = this.feed[index];
+				let state = status.favourited;
+				let count = status.favourites_count;
+				this.feed[index].favourites_count = count + 1;
+				this.feed[index].favourited = !status.favourited;
+
+				axios.post('/api/v1/statuses/' + status.id + '/favourite')
+				.catch(err => {
+					this.feed[index].favourites_count = count;
+					this.feed[index].favourited = false;
+				})
+			},
+
+			unlikeStatus(index) {
+				let status = this.feed[index];
+				let state = status.favourited;
+				let count = status.favourites_count;
+				this.feed[index].favourites_count = count - 1;
+				this.feed[index].favourited = !status.favourited;
+
+				axios.post('/api/v1/statuses/' + status.id + '/unfavourite')
+				.catch(err => {
+					this.feed[index].favourites_count = count;
+					this.feed[index].favourited = false;
+				})
+			},
+
+			openContextMenu(idx, type = 'feed') {
+				switch(type) {
+					case 'feed':
+						this.postIndex = idx;
+						this.contextMenuPost = this.feed[idx];
+					break;
+
+					case 'archive':
+						this.postIndex = idx;
+						this.contextMenuPost = this.archives[idx];
+					break;
+				}
+				this.showMenu = true;
+				this.$nextTick(() => {
+					this.$refs.contextMenu.open();
+				});
+			},
+
+			openLikesModal(idx) {
+				this.postIndex = idx;
+				this.likesModalPost = this.feed[this.postIndex];
+				this.showLikesModal = true;
+				this.$nextTick(() => {
+					this.$refs.likesModal.open();
+				});
+			},
+
+			openSharesModal(idx) {
+				this.postIndex = idx;
+				this.sharesModalPost = this.feed[this.postIndex];
+				this.showSharesModal = true;
+				this.$nextTick(() => {
+					this.$refs.sharesModal.open();
+				});
+			},
+
+			commitModeration(type) {
+				let idx = this.postIndex;
+
+				switch(type) {
+					case 'addcw':
+						this.feed[idx].sensitive = true;
+					break;
+
+					case 'remcw':
+						this.feed[idx].sensitive = false;
+					break;
+
+					case 'unlist':
+						this.feed.splice(idx, 1);
+					break;
+
+					case 'spammer':
+						let id = this.feed[idx].account.id;
+
+						this.feed = this.feed.filter(post => {
+							return post.account.id != id;
+						});
+					break;
+				}
+			},
+
+			counterChange(index, type) {
+				switch(type) {
+					case 'comment-increment':
+						this.feed[index].reply_count = this.feed[index].reply_count + 1;
+					break;
+
+					case 'comment-decrement':
+						this.feed[index].reply_count = this.feed[index].reply_count - 1;
+					break;
+				}
+			},
+
+			openCommentLikesModal(post) {
+				this.likesModalPost = post;
+				this.showLikesModal = true;
+				this.$nextTick(() => {
+					this.$refs.likesModal.open();
+				});
+			},
+
+			shareStatus(index) {
+				let status = this.feed[index];
+				let state = status.reblogged;
+				let count = status.reblogs_count;
+				this.feed[index].reblogs_count = count + 1;
+				this.feed[index].reblogged = !status.reblogged;
+
+				axios.post('/api/v1/statuses/' + status.id + '/reblog')
+				.catch(err => {
+					this.feed[index].reblogs_count = count;
+					this.feed[index].reblogged = false;
+				})
+			},
+
+			unshareStatus(index) {
+				let status = this.feed[index];
+				let state = status.reblogged;
+				let count = status.reblogs_count;
+				this.feed[index].reblogs_count = count - 1;
+				this.feed[index].reblogged = !status.reblogged;
+
+				axios.post('/api/v1/statuses/' + status.id + '/unreblog')
+				.catch(err => {
+					this.feed[index].reblogs_count = count;
+					this.feed[index].reblogged = false;
+				})
+			},
+
+			handleReport(post) {
+				this.reportedStatusId = post.id;
+				this.$nextTick(() => {
+					this.reportedStatus = post;
+					this.$refs.reportModal.open();
+				});
+			},
+
+			deletePost() {
+				this.feed.splice(this.postIndex, 1);
+			},
+
+			handleArchived(id) {
+				this.feed.splice(this.postIndex, 1);
+			},
+
+			handleUnarchived(id) {
+				this.feed = [];
+				this.fetchFeed();
+			},
+
+			enterArchivesIntersect() {
+				if(this.isIntersecting) {
+					return;
+				}
+				this.isIntersecting = true;
+
+				axios.get('/api/pixelfed/v2/statuses/archives', {
+					params: {
+						page: this.archivesPage
+					}
+				})
+				.then(res => {
+					this.archives.push(...res.data);
+					this.archivesPage++;
+					this.canLoadMoreArchives = res.data.length != 0;
+					this.isIntersecting = false;
+				})
+				.catch(err => {
+					this.canLoadMoreArchives = false;
+				})
+			},
+
+			handleBookmark(index) {
+				if(!window.confirm('Are you sure you want to unbookmark this post?')) {
+					return;
+				}
+
+				let p = this.bookmarks[index];
+
+				axios.post('/i/bookmark', {
+					item: p.id
+				})
+				.then(res => {
+					this.bookmarks = this.bookmarks.map(post => {
+						if(post.id == p.id) {
+							post.bookmarked = false;
+							delete post.bookmarked_at;
+						}
+						return post;
+					});
+					this.bookmarks.splice(index, 1);
+				})
+				.catch(err => {
+					this.$bvToast.toast('Cannot bookmark post at this time.', {
+						title: 'Bookmark Error',
+						variant: 'danger',
+						autoHideDelay: 5000
+					});
+				});
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	.profile-feed-component {
+		margin-top: 0;
+
+		.ph-wrapper {
+			padding: 0.25rem;
+
+			.ph-item {
+				margin: 0;
+				padding: 0;
+				border: none;
+				background-color: transparent;
+
+				.ph-picture {
+					height: auto;
+					padding-bottom: 100%;
+					border-radius: 5px;
+				}
+
+				& > * {
+					margin-bottom: 0;
+				}
+			}
+		}
+
+		.info-overlay-text-field {
+			font-size: 13.5px;
+			margin-bottom: 2px;
+
+			@media (min-width: 768px) {
+				font-size: 20px;
+				margin-bottom: 15px;
+			}
+		}
+
+		.video-overlay-badge {
+			position: absolute;
+			top: 10px;
+			right: 10px;
+			opacity: 0.6;
+			color: var(--dark);
+			padding-bottom: 1px;
+		}
+
+		.timestamp-overlay-badge {
+			position: absolute;
+			bottom: 10px;
+			right: 10px;
+			opacity: 0.6;
+		}
+
+		.profile-nav-btns {
+			margin-right: 1rem;
+
+			.btn-group {
+				min-height: 45px;
+			}
+
+			.btn-link {
+				color: var(--text-lighter);
+				font-size: 14px;
+				border-radius: 0;
+				margin-right: 1rem;
+				font-weight: bold;
+
+				&:hover {
+					color: var(--text-muted);
+					text-decoration: none;
+				}
+
+				&.active {
+					color: var(--dark);
+					border-bottom: 1px solid var(--dark);
+					transition: border-bottom 250ms ease-in-out;
+				}
+			}
+		}
+
+		.layout-sort-toggle {
+			.btn {
+				border: none;
+
+				&.btn-light {
+					opacity: 0.4;
+				}
+			}
+		}
+	}
+</style>