mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-27 08:43:17 +00:00
commit
1518d632d9
52 changed files with 995 additions and 3284 deletions
|
@ -118,6 +118,13 @@
|
|||
- Update ApiV1Controller, fix media update. Fixes #4196 ([f3164650](https://github.com/pixelfed/pixelfed/commit/f3164650))
|
||||
- Update SearchApiV2Service, fix hashtag search. ([1992b5bc](https://github.com/pixelfed/pixelfed/commit/1992b5bc))
|
||||
- Update ApiV1Controller, allow optional mastodonMode on v2/search endpoint. ([f4a69631](https://github.com/pixelfed/pixelfed/commit/f4a69631))
|
||||
- Update ApiV1Controller, add cursor pagination and pagination link headers to account/{id}/followers and account/{id}/following endpoints with legacy support for `page=` simple pagination ([713aa5fd](https://github.com/pixelfed/pixelfed/commit/713aa5fd))
|
||||
- Update legacy Profile component to use new cursor pagination for following/follower modals ([7a1495e6](https://github.com/pixelfed/pixelfed/commit/7a1495e6))
|
||||
- Update ApiV1Controller, fix link header pagination in /api/v1/statuses/{id}/favourited_by ([adc82eca](https://github.com/pixelfed/pixelfed/commit/adc82eca))
|
||||
- Update ApiV1Controller, fix link header pagination in /api/v1/statuses/{id}/reblogged_by ([e346b675](https://github.com/pixelfed/pixelfed/commit/e346b675))
|
||||
- Update ApiV1Controller, fix following/follower entities, use masto schema by default and update components accordingly ([4716c280](https://github.com/pixelfed/pixelfed/commit/4716c280))
|
||||
- Update FollowerController, remove deprecated /i/follow endpoint ([4739d614](https://github.com/pixelfed/pixelfed/commit/4739d614))
|
||||
- Update queue config, set "after_commit" to true ([304ea956](https://github.com/pixelfed/pixelfed/commit/304ea956))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)
|
||||
|
|
|
@ -467,6 +467,11 @@ class ApiV1Controller extends Controller
|
|||
$account = AccountService::get($id);
|
||||
abort_if(!$account, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|min:1|max:80'
|
||||
]);
|
||||
$limit = $request->input('limit', 10);
|
||||
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
||||
|
||||
if(intval($pid) !== intval($account['id'])) {
|
||||
if($account['locked']) {
|
||||
|
@ -479,18 +484,21 @@ class ApiV1Controller extends Controller
|
|||
return [];
|
||||
}
|
||||
|
||||
if($request->has('page') && $request->page >= 5) {
|
||||
if($request->has('page') && $request->user()->is_admin == false) {
|
||||
$page = (int) $request->input('page');
|
||||
if(($page * $limit) >= 100) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if($request->has('page')) {
|
||||
$res = DB::table('followers')
|
||||
->select('id', 'profile_id', 'following_id')
|
||||
->whereFollowingId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->simplePaginate(10)
|
||||
->map(function($follower) {
|
||||
return AccountService::getMastodon($follower->profile_id);
|
||||
->simplePaginate($limit)
|
||||
->map(function($follower) use($napi) {
|
||||
return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true);
|
||||
})
|
||||
->filter(function($account) {
|
||||
return $account && isset($account['id']);
|
||||
|
@ -501,6 +509,42 @@ class ApiV1Controller extends Controller
|
|||
return $this->json($res);
|
||||
}
|
||||
|
||||
$paginator = DB::table('followers')
|
||||
->select('id', 'profile_id', 'following_id')
|
||||
->whereFollowingId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->cursorPaginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
$link = null;
|
||||
|
||||
if($paginator->onFirstPage()) {
|
||||
if($paginator->hasMorePages()) {
|
||||
$link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
} else {
|
||||
if($paginator->previousPageUrl()) {
|
||||
$link = '<'.$paginator->previousPageUrl().'>; rel="next"';
|
||||
}
|
||||
|
||||
if($paginator->hasMorePages()) {
|
||||
$link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
}
|
||||
|
||||
$res = $paginator->map(function($follower) use($napi) {
|
||||
return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true);
|
||||
})
|
||||
->filter(function($account) {
|
||||
return $account && isset($account['id']);
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
$headers = isset($link) ? ['Link' => $link] : [];
|
||||
return $this->json($res, 200, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/accounts/{id}/following
|
||||
*
|
||||
|
@ -514,6 +558,11 @@ class ApiV1Controller extends Controller
|
|||
$account = AccountService::get($id);
|
||||
abort_if(!$account, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|min:1|max:80'
|
||||
]);
|
||||
$limit = $request->input('limit', 10);
|
||||
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
||||
|
||||
if(intval($pid) !== intval($account['id'])) {
|
||||
if($account['locked']) {
|
||||
|
@ -526,18 +575,56 @@ class ApiV1Controller extends Controller
|
|||
return [];
|
||||
}
|
||||
|
||||
if($request->has('page') && $request->page >= 5) {
|
||||
if($request->has('page') && $request->user()->is_admin == false) {
|
||||
$page = (int) $request->input('page');
|
||||
if(($page * $limit) >= 100) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($request->has('page')) {
|
||||
$res = DB::table('followers')
|
||||
->select('id', 'profile_id', 'following_id')
|
||||
->whereProfileId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->simplePaginate(10)
|
||||
->map(function($follower) {
|
||||
return AccountService::get($follower->following_id);
|
||||
->simplePaginate($limit)
|
||||
->map(function($follower) use($napi) {
|
||||
return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true);
|
||||
})
|
||||
->filter(function($account) {
|
||||
return $account && isset($account['id']);
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
$paginator = DB::table('followers')
|
||||
->select('id', 'profile_id', 'following_id')
|
||||
->whereProfileId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->cursorPaginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
$link = null;
|
||||
|
||||
if($paginator->onFirstPage()) {
|
||||
if($paginator->hasMorePages()) {
|
||||
$link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
} else {
|
||||
if($paginator->previousPageUrl()) {
|
||||
$link = '<'.$paginator->previousPageUrl().'>; rel="next"';
|
||||
}
|
||||
|
||||
if($paginator->hasMorePages()) {
|
||||
$link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
}
|
||||
|
||||
$res = $paginator->map(function($follower) use($napi) {
|
||||
return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true);
|
||||
})
|
||||
->filter(function($account) {
|
||||
return $account && isset($account['id']);
|
||||
|
@ -545,7 +632,8 @@ class ApiV1Controller extends Controller
|
|||
->values()
|
||||
->toArray();
|
||||
|
||||
return $this->json($res);
|
||||
$headers = isset($link) ? ['Link' => $link] : [];
|
||||
return $this->json($res, 200, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2496,13 +2584,17 @@ class ApiV1Controller extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|integer|min:1|max:100'
|
||||
'limit' => 'sometimes|integer|min:1|max:80'
|
||||
]);
|
||||
|
||||
$limit = $request->input('limit') ?? 10;
|
||||
$limit = $request->input('limit', 10);
|
||||
$user = $request->user();
|
||||
$pid = $user->profile_id;
|
||||
$status = Status::findOrFail($id);
|
||||
$author = intval($status->profile_id) === intval($user->profile_id) || $user->is_admin;
|
||||
$account = AccountService::get($status->profile_id, true);
|
||||
abort_if(!$account, 404);
|
||||
$author = intval($status->profile_id) === intval($pid) || $user->is_admin;
|
||||
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
||||
|
||||
abort_if(
|
||||
!$status->type ||
|
||||
|
@ -2512,10 +2604,14 @@ class ApiV1Controller extends Controller
|
|||
|
||||
if(!$author) {
|
||||
if($status->scope == 'private') {
|
||||
abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403);
|
||||
abort_if(!FollowerService::follows($pid, $status->profile_id), 403);
|
||||
} else {
|
||||
abort_if(!in_array($status->scope, ['public','unlisted']), 403);
|
||||
}
|
||||
|
||||
if($request->has('cursor')) {
|
||||
return $this->json([]);
|
||||
}
|
||||
}
|
||||
|
||||
$res = Status::where('reblog_of_id', $status->id)
|
||||
|
@ -2530,26 +2626,34 @@ class ApiV1Controller extends Controller
|
|||
$headers = [];
|
||||
if($author && $res->hasPages()) {
|
||||
$links = '';
|
||||
if($res->onFirstPage()) {
|
||||
if($res->nextPageUrl()) {
|
||||
$links = '<' . $res->nextPageUrl() .'>; rel="prev"';
|
||||
}
|
||||
} else {
|
||||
if($res->previousPageUrl()) {
|
||||
$links = '<' . $res->previousPageUrl() .'>; rel="prev"';
|
||||
$links = '<' . $res->previousPageUrl() .'>; rel="next"';
|
||||
}
|
||||
|
||||
if($res->nextPageUrl()) {
|
||||
if(!empty($links)) {
|
||||
$links .= ', ';
|
||||
}
|
||||
$links .= '<' . $res->nextPageUrl() .'>; rel="next"';
|
||||
$links .= '<' . $res->nextPageUrl() .'>; rel="prev"';
|
||||
}
|
||||
}
|
||||
|
||||
$headers = ['Link' => $links];
|
||||
}
|
||||
|
||||
$res = $res->map(function($status) use($user) {
|
||||
$account = AccountService::getMastodon($status->profile_id, true);
|
||||
$res = $res->map(function($status) use($pid, $napi) {
|
||||
$account = $napi ? AccountService::get($status->profile_id, true) : AccountService::getMastodon($status->profile_id, true);
|
||||
if(!$account) {
|
||||
return false;
|
||||
}
|
||||
$account['follows'] = $status->profile_id == $user->profile_id ? null : FollowerService::follows($user->profile_id, $status->profile_id);
|
||||
if($napi) {
|
||||
$account['follows'] = $status->profile_id == $pid ? null : FollowerService::follows($pid, $status->profile_id);
|
||||
}
|
||||
return $account;
|
||||
})
|
||||
->filter(function($account) {
|
||||
|
@ -2572,13 +2676,17 @@ class ApiV1Controller extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'limit' => 'nullable|integer|min:1|max:100'
|
||||
'limit' => 'nullable|integer|min:1|max:80'
|
||||
]);
|
||||
|
||||
$limit = $request->input('limit') ?? 10;
|
||||
$limit = $request->input('limit', 10);
|
||||
$user = $request->user();
|
||||
$pid = $user->profile_id;
|
||||
$status = Status::findOrFail($id);
|
||||
$author = intval($status->profile_id) === intval($user->profile_id) || $user->is_admin;
|
||||
$account = AccountService::get($status->profile_id, true);
|
||||
abort_if(!$account, 404);
|
||||
$author = intval($status->profile_id) === intval($pid) || $user->is_admin;
|
||||
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
||||
|
||||
abort_if(
|
||||
!$status->type ||
|
||||
|
@ -2588,7 +2696,7 @@ class ApiV1Controller extends Controller
|
|||
|
||||
if(!$author) {
|
||||
if($status->scope == 'private') {
|
||||
abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403);
|
||||
abort_if(!FollowerService::follows($pid, $status->profile_id), 403);
|
||||
} else {
|
||||
abort_if(!in_array($status->scope, ['public','unlisted']), 403);
|
||||
}
|
||||
|
@ -2610,29 +2718,39 @@ class ApiV1Controller extends Controller
|
|||
$headers = [];
|
||||
if($author && $res->hasPages()) {
|
||||
$links = '';
|
||||
|
||||
if($res->onFirstPage()) {
|
||||
if($res->nextPageUrl()) {
|
||||
$links = '<' . $res->nextPageUrl() .'>; rel="prev"';
|
||||
}
|
||||
} else {
|
||||
if($res->previousPageUrl()) {
|
||||
$links = '<' . $res->previousPageUrl() .'>; rel="prev"';
|
||||
$links = '<' . $res->previousPageUrl() .'>; rel="next"';
|
||||
}
|
||||
|
||||
if($res->nextPageUrl()) {
|
||||
if(!empty($links)) {
|
||||
$links .= ', ';
|
||||
}
|
||||
$links .= '<' . $res->nextPageUrl() .'>; rel="next"';
|
||||
$links .= '<' . $res->nextPageUrl() .'>; rel="prev"';
|
||||
}
|
||||
}
|
||||
|
||||
$headers = ['Link' => $links];
|
||||
}
|
||||
|
||||
$res = $res->map(function($like) use($user) {
|
||||
$account = AccountService::getMastodon($like->profile_id, true);
|
||||
$res = $res->map(function($like) use($pid, $napi) {
|
||||
$account = $napi ? AccountService::get($like->profile_id, true) : AccountService::getMastodon($like->profile_id, true);
|
||||
if(!$account) {
|
||||
return false;
|
||||
}
|
||||
$account['follows'] = $like->profile_id == $user->profile_id ? null : FollowerService::follows($user->profile_id, $like->profile_id);
|
||||
|
||||
if($napi) {
|
||||
$account['follows'] = $like->profile_id == $pid ? null : FollowerService::follows($pid, $like->profile_id);
|
||||
}
|
||||
return $account;
|
||||
})
|
||||
->filter(function($account) use($user) {
|
||||
->filter(function($account) {
|
||||
return $account && isset($account['id']);
|
||||
})
|
||||
->values();
|
||||
|
|
|
@ -23,109 +23,7 @@ class FollowerController extends Controller
|
|||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|string',
|
||||
'force' => 'nullable|boolean',
|
||||
]);
|
||||
$force = (bool) $request->input('force', true);
|
||||
$item = (int) $request->input('item');
|
||||
$url = $this->handleFollowRequest($item, $force);
|
||||
if($request->wantsJson() == true) {
|
||||
return response()->json(200);
|
||||
} else {
|
||||
return redirect($url);
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleFollowRequest($item, $force)
|
||||
{
|
||||
$user = Auth::user()->profile;
|
||||
|
||||
$target = Profile::where('id', '!=', $user->id)->whereNull('status')->findOrFail($item);
|
||||
$private = (bool) $target->is_private;
|
||||
$remote = (bool) $target->domain;
|
||||
$blocked = UserFilter::whereUserId($target->id)
|
||||
->whereFilterType('block')
|
||||
->whereFilterableId($user->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->exists();
|
||||
|
||||
if($blocked == true) {
|
||||
abort(400, 'You cannot follow this user.');
|
||||
}
|
||||
|
||||
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->exists();
|
||||
|
||||
if($private == true && $isFollowing == 0) {
|
||||
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
|
||||
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
|
||||
}
|
||||
|
||||
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
||||
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
||||
}
|
||||
|
||||
$follow = FollowRequest::firstOrCreate([
|
||||
'follower_id' => $user->id,
|
||||
'following_id' => $target->id
|
||||
]);
|
||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
|
||||
FollowerService::add($user->id, $target->id);
|
||||
} elseif ($private == false && $isFollowing == 0) {
|
||||
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
|
||||
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
|
||||
}
|
||||
|
||||
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
||||
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
||||
}
|
||||
$follower = new Follower();
|
||||
$follower->profile_id = $user->id;
|
||||
$follower->following_id = $target->id;
|
||||
$follower->save();
|
||||
|
||||
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
FollowerService::add($user->id, $target->id);
|
||||
FollowPipeline::dispatch($follower);
|
||||
} else {
|
||||
if($force == true) {
|
||||
$request = FollowRequest::whereFollowerId($user->id)->whereFollowingId($target->id)->exists();
|
||||
$follower = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->exists();
|
||||
if($remote == true && $request && !$follower) {
|
||||
$this->sendFollow($user, $target);
|
||||
}
|
||||
if($remote == true && $follower) {
|
||||
$this->sendUndoFollow($user, $target);
|
||||
}
|
||||
Follower::whereProfileId($user->id)
|
||||
->whereFollowingId($target->id)
|
||||
->delete();
|
||||
FollowerService::remove($user->id, $target->id);
|
||||
}
|
||||
}
|
||||
|
||||
Cache::forget('profile:following:'.$target->id);
|
||||
Cache::forget('profile:followers:'.$target->id);
|
||||
Cache::forget('profile:following:'.$user->id);
|
||||
Cache::forget('profile:followers:'.$user->id);
|
||||
Cache::forget('api:local:exp:rec:'.$user->id);
|
||||
Cache::forget('user:account:id:'.$target->user_id);
|
||||
Cache::forget('user:account:id:'.$user->user_id);
|
||||
Cache::forget('px:profile:followers-v1.3:'.$user->id);
|
||||
Cache::forget('px:profile:followers-v1.3:'.$target->id);
|
||||
Cache::forget('px:profile:following-v1.3:'.$user->id);
|
||||
Cache::forget('px:profile:following-v1.3:'.$target->id);
|
||||
Cache::forget('profile:follower_count:'.$target->id);
|
||||
Cache::forget('profile:follower_count:'.$user->id);
|
||||
Cache::forget('profile:following_count:'.$target->id);
|
||||
Cache::forget('profile:following_count:'.$user->id);
|
||||
|
||||
return $target->url();
|
||||
abort(422, 'Deprecated API Endpoint, use /api/v1/accounts/{id}/follow or /api/v1/accounts/{id}/unfollow instead.');
|
||||
}
|
||||
|
||||
public function sendFollow($user, $target)
|
||||
|
|
|
@ -62,36 +62,6 @@ class PublicApiController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
protected function getLikes($status)
|
||||
{
|
||||
if(false == Auth::check()) {
|
||||
return [];
|
||||
} else {
|
||||
$profile = Auth::user()->profile;
|
||||
if($profile->status) {
|
||||
return [];
|
||||
}
|
||||
$likes = $status->likedBy()->orderBy('created_at','desc')->paginate(10);
|
||||
$collection = new Fractal\Resource\Collection($likes, new AccountTransformer());
|
||||
return $this->fractal->createData($collection)->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getShares($status)
|
||||
{
|
||||
if(false == Auth::check()) {
|
||||
return [];
|
||||
} else {
|
||||
$profile = Auth::user()->profile;
|
||||
if($profile->status) {
|
||||
return [];
|
||||
}
|
||||
$shares = $status->sharedBy()->orderBy('created_at','desc')->paginate(10);
|
||||
$collection = new Fractal\Resource\Collection($shares, new AccountTransformer());
|
||||
return $this->fractal->createData($collection)->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatus(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
@ -216,41 +186,6 @@ class PublicApiController extends Controller
|
|||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function statusLikes(Request $request, $username, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$status = Status::findOrFail($id);
|
||||
$this->scopeCheck($status->profile, $status);
|
||||
$page = $request->input('page');
|
||||
if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
|
||||
return response()->json([
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
$likes = $this->getLikes($status);
|
||||
return response()->json([
|
||||
'data' => $likes
|
||||
]);
|
||||
}
|
||||
|
||||
public function statusShares(Request $request, $username, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->findOrFail($id);
|
||||
$this->scopeCheck($profile, $status);
|
||||
$page = $request->input('page');
|
||||
if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
|
||||
return response()->json([
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
$shares = $this->getShares($status);
|
||||
return response()->json([
|
||||
'data' => $shares
|
||||
]);
|
||||
}
|
||||
|
||||
protected function scopeCheck(Profile $profile, Status $status)
|
||||
{
|
||||
if($profile->is_private == true && Auth::check() == false) {
|
||||
|
@ -811,68 +746,6 @@ class PublicApiController extends Controller
|
|||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowers(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$account = AccountService::get($id, true);
|
||||
abort_if(!$account, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if(AccountService::hiddenFollowers($id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if($request->has('page') && $request->page >= 10) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$res = collect(FollowerService::followersPaginate($account['id'], $request->input('page', 1)))
|
||||
->map(fn($id) => AccountService::get($id, true))
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountFollowing(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$account = AccountService::get($id, true);
|
||||
abort_if(!$account, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
if($pid != $account['id']) {
|
||||
if($account['locked']) {
|
||||
if(!FollowerService::follows($pid, $account['id'])) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if(AccountService::hiddenFollowing($id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if($request->has('page') && $request->page >= 10) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$res = collect(FollowerService::followingPaginate($account['id'], $request->input('page', 1)))
|
||||
->map(fn($id) => AccountService::get($id, true))
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function accountStatuses(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
|
|
|
@ -63,6 +63,7 @@ return [
|
|||
'queue' => 'default',
|
||||
'retry_after' => 1800,
|
||||
'block_for' => null,
|
||||
'after_commit' => true,
|
||||
],
|
||||
|
||||
],
|
||||
|
|
BIN
public/js/activity.js
vendored
BIN
public/js/activity.js
vendored
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~myhashtags.chunk.e466a09b04298c01.js
vendored
Normal file
BIN
public/js/discover~myhashtags.chunk.e466a09b04298c01.js
vendored
Normal file
Binary file not shown.
BIN
public/js/home.chunk.02577ae670229a49.js
vendored
Normal file
BIN
public/js/home.chunk.02577ae670229a49.js
vendored
Normal file
Binary file not shown.
BIN
public/js/home.chunk.e9544807955a793b.js
vendored
BIN
public/js/home.chunk.e9544807955a793b.js
vendored
Binary file not shown.
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
BIN
public/js/notifications.chunk.d0d53f16d9998dd2.js
vendored
BIN
public/js/notifications.chunk.d0d53f16d9998dd2.js
vendored
Binary file not shown.
BIN
public/js/notifications.chunk.ee310da155305164.js
vendored
Normal file
BIN
public/js/notifications.chunk.ee310da155305164.js
vendored
Normal file
Binary file not shown.
BIN
public/js/post.chunk.3ea384fc9ebf43c7.js
vendored
BIN
public/js/post.chunk.3ea384fc9ebf43c7.js
vendored
Binary file not shown.
BIN
public/js/post.chunk.bad88be5fc04f10b.js
vendored
Normal file
BIN
public/js/post.chunk.bad88be5fc04f10b.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.chunk.4e79ac3514929747.js
vendored
BIN
public/js/profile.chunk.4e79ac3514929747.js
vendored
Binary file not shown.
BIN
public/js/profile.chunk.d6cf0345664c2864.js
vendored
Normal file
BIN
public/js/profile.chunk.d6cf0345664c2864.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/profile~followers.bundle.54e01681585449b4.js
vendored
Normal file
BIN
public/js/profile~followers.bundle.54e01681585449b4.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/js/profile~following.bundle.f9d69ec593f3d011.js
vendored
Normal file
BIN
public/js/profile~following.bundle.f9d69ec593f3d011.js
vendored
Normal file
Binary file not shown.
BIN
public/js/rempos.js
vendored
BIN
public/js/rempos.js
vendored
Binary file not shown.
BIN
public/js/rempro.js
vendored
BIN
public/js/rempro.js
vendored
Binary file not shown.
BIN
public/js/search.js
vendored
BIN
public/js/search.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -104,7 +104,7 @@
|
|||
isFetchingMore: false,
|
||||
likes: [],
|
||||
ids: [],
|
||||
page: undefined,
|
||||
cursor: undefined,
|
||||
isUpdatingFollowState: false,
|
||||
followStateIndex: undefined,
|
||||
user: window._sharedData.user
|
||||
|
@ -119,13 +119,14 @@
|
|||
this.isFetchingMore = false;
|
||||
this.likes = [];
|
||||
this.ids = [];
|
||||
this.page = undefined;
|
||||
this.cursor = undefined;
|
||||
},
|
||||
|
||||
fetchLikes() {
|
||||
axios.get('/api/v1/statuses/'+this.status.id+'/favourited_by', {
|
||||
params: {
|
||||
limit: 40
|
||||
limit: 40,
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
|
@ -133,19 +134,21 @@
|
|||
this.likes = res.data;
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.next) {
|
||||
this.page = links.next.cursor;
|
||||
if(links.prev) {
|
||||
this.cursor = links.prev.cursor;
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
open() {
|
||||
if(this.page) {
|
||||
if(this.cursor) {
|
||||
this.clear();
|
||||
}
|
||||
this.isOpen = true;
|
||||
|
@ -163,7 +166,8 @@
|
|||
axios.get('/api/v1/statuses/'+this.status.id+'/favourited_by', {
|
||||
params: {
|
||||
limit: 10,
|
||||
cursor: this.page
|
||||
cursor: this.cursor,
|
||||
'_pe': 1
|
||||
}
|
||||
}).then(res => {
|
||||
if(!res.data || !res.data.length) {
|
||||
|
@ -179,11 +183,13 @@
|
|||
})
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.next) {
|
||||
this.page = links.next.cursor;
|
||||
if(links.prev) {
|
||||
this.cursor = links.prev.cursor;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
this.isFetchingMore = false;
|
||||
})
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
isFetchingMore: false,
|
||||
likes: [],
|
||||
ids: [],
|
||||
page: undefined,
|
||||
cursor: undefined,
|
||||
isUpdatingFollowState: false,
|
||||
followStateIndex: undefined,
|
||||
user: window._sharedData.user
|
||||
|
@ -119,13 +119,14 @@
|
|||
this.isFetchingMore = false;
|
||||
this.likes = [];
|
||||
this.ids = [];
|
||||
this.page = undefined;
|
||||
this.cursor = undefined;
|
||||
},
|
||||
|
||||
fetchShares() {
|
||||
axios.get('/api/v1/statuses/'+this.status.id+'/reblogged_by', {
|
||||
params: {
|
||||
limit: 40
|
||||
limit: 40,
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
|
@ -133,19 +134,21 @@
|
|||
this.likes = res.data;
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.next) {
|
||||
this.page = links.next.cursor;
|
||||
if(links.prev) {
|
||||
this.cursor = links.prev.cursor;
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
open() {
|
||||
if(this.page) {
|
||||
if(this.cursor) {
|
||||
this.clear();
|
||||
}
|
||||
this.isOpen = true;
|
||||
|
@ -163,7 +166,8 @@
|
|||
axios.get('/api/v1/statuses/'+this.status.id+'/reblogged_by', {
|
||||
params: {
|
||||
limit: 10,
|
||||
cursor: this.page
|
||||
cursor: this.cursor,
|
||||
'_pe': 1
|
||||
}
|
||||
}).then(res => {
|
||||
if(!res.data || !res.data.length) {
|
||||
|
@ -179,11 +183,13 @@
|
|||
})
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.next) {
|
||||
this.page = links.next.cursor;
|
||||
if(links.prev) {
|
||||
this.cursor = links.prev.cursor;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
this.isFetchingMore = false;
|
||||
})
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
<template>
|
||||
<div class="profile-followers-component">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-8">
|
||||
<div v-if="isLoaded" class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-outline-dark rounded-pill font-weight-bold"
|
||||
@click="goBack()">
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-center flex-column w-100 overflow-hidden">
|
||||
<p class="small text-muted mb-0 text-uppercase font-weight-light cursor-pointer text-truncate text-center" style="width: 70%;" @click="goBack()">@{{ profile.acct }}</p>
|
||||
<p class="lead font-weight-bold mt-n1 mb-0">{{ $t('profile.followers') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-dark rounded-pill font-weight-bold spacer-btn" href="#">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoaded" class="list-group scroll-card">
|
||||
<div v-for="(account, idx) in feed" class="list-group-item">
|
||||
<a
|
||||
:id="'apop_'+account.id"
|
||||
:href="account.url"
|
||||
@click.prevent="goToProfile(account)"
|
||||
class="text-decoration-none">
|
||||
<div class="media">
|
||||
<img
|
||||
:src="account.avatar"
|
||||
width="40"
|
||||
height="40"
|
||||
style="border-radius: 8px;"
|
||||
class="mr-3 shadow-sm"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
|
||||
|
||||
<div class="media-body">
|
||||
<p class="mb-0 text-truncate">
|
||||
<span class="text-dark font-weight-bold text-decoration-none" v-html="getUsername(account)"></span>
|
||||
</p>
|
||||
<p class="mb-0 mt-n1 text-muted small text-break">@{{ account.acct }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<b-popover :target="'apop_'+account.id" triggers="hover" placement="left" delay="1000" custom-class="shadow border-0 rounded-px">
|
||||
<profile-hover-card :profile="account" />
|
||||
</b-popover>
|
||||
</div>
|
||||
|
||||
<div v-if="canLoadMore">
|
||||
<intersect @enter="enterIntersect">
|
||||
<placeholder />
|
||||
</intersect>
|
||||
</div>
|
||||
|
||||
<div v-if="!canLoadMore && !feed.length">
|
||||
<div class="list-group-item text-center">
|
||||
<div v-if="isWarmingCache" class="px-4">
|
||||
<p class="mb-0 lead font-weight-bold">Loading Followers...</p>
|
||||
<div class="py-3">
|
||||
<b-spinner variant="primary" style="width: 1.5rem; height: 1.5rem;" />
|
||||
</div>
|
||||
<p class="small text-muted mb-0">Please wait while we collect followers of this account, this shouldn't take long!</p>
|
||||
</div>
|
||||
<p v-else class="mb-0 font-weight-bold">No followers yet!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="list-group">
|
||||
<placeholder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Intersect from 'vue-intersect'
|
||||
import Placeholder from './../post/LikeListPlaceholder.vue';
|
||||
import ProfileHoverCard from './ProfileHoverCard.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { parseLinkHeader } from '@web3-storage/parse-link-header';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
ProfileHoverCard,
|
||||
Intersect,
|
||||
Placeholder
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getCustomEmoji'
|
||||
])
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
feed: [],
|
||||
page: 1,
|
||||
cursor: null,
|
||||
canLoadMore: true,
|
||||
isFetchingMore: false,
|
||||
isWarmingCache: false,
|
||||
cacheWarmTimeout: undefined,
|
||||
cacheWarmInterations: 0,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchFollowers();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.cacheWarmTimeout);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchFollowers() {
|
||||
axios.get('/api/v1/accounts/'+this.profile.id+'/followers', {
|
||||
params: {
|
||||
cursor: this.cursor,
|
||||
'_pe': 1
|
||||
}
|
||||
}).then(res => {
|
||||
if(!res.data.length) {
|
||||
this.canLoadMore = false;
|
||||
this.isLoaded = true;
|
||||
if(this.cursor == null && this.profile.followers_count) {
|
||||
this.isWarmingCache = true;
|
||||
this.setCacheWarmTimeout();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.cursor = links.prev.cursor;
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
this.feed.push(...res.data);
|
||||
this.isLoaded = true;
|
||||
this.isFetchingMore = false;
|
||||
if(this.isWarmingCache || this.cacheWarmTimeout) {
|
||||
this.isWarmingCache = false;
|
||||
clearTimeout(this.cacheWarmTimeout);
|
||||
this.cacheWarmTimeout = undefined;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.canLoadMore = false;
|
||||
this.isLoaded = true;
|
||||
this.isFetchingMore = false;
|
||||
})
|
||||
},
|
||||
|
||||
enterIntersect() {
|
||||
if(this.isFetchingMore) {
|
||||
return;
|
||||
}
|
||||
this.isFetchingMore = true;
|
||||
this.fetchFollowers();
|
||||
},
|
||||
|
||||
getUsername(profile) {
|
||||
let self = this;
|
||||
let dn = profile.display_name;
|
||||
if(!dn || !dn.trim().length) {
|
||||
return profile.username;
|
||||
}
|
||||
|
||||
if(dn.includes(':')) {
|
||||
let re = /(<a?)?:\w+:(\d{18}>)?/g;
|
||||
let un = dn.replaceAll(re, function(em) {
|
||||
let shortcode = em.slice(1, em.length - 1);
|
||||
let emoji = self.getCustomEmoji.filter(e => {
|
||||
return e.shortcode == shortcode;
|
||||
});
|
||||
return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
|
||||
});
|
||||
return un;
|
||||
} else {
|
||||
return dn;
|
||||
}
|
||||
},
|
||||
|
||||
goToProfile(account) {
|
||||
this.$router.push({
|
||||
path: `/i/web/profile/${account.id}`,
|
||||
params: {
|
||||
id: account.id,
|
||||
cachedProfile: account,
|
||||
cachedUser: this.profile
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$emit('back');
|
||||
},
|
||||
|
||||
setCacheWarmTimeout() {
|
||||
if(this.cacheWarmInterations >= 5) {
|
||||
this.isWarmingCache = false;
|
||||
swal('Oops', 'Its taking longer than expected to collect this account followers. Please try again later', 'error');
|
||||
return;
|
||||
}
|
||||
this.cacheWarmTimeout = setTimeout(() => {
|
||||
this.cacheWarmInterations++;
|
||||
this.fetchFollowers();
|
||||
}, 45000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-followers-component {
|
||||
.list-group-item {
|
||||
border: none;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-card {
|
||||
max-height: calc(100vh - 250px);
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer-btn {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,259 @@
|
|||
<template>
|
||||
<div class="profile-following-component">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div v-if="isLoaded" class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-outline-dark rounded-pill font-weight-bold"
|
||||
@click="goBack()">
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-center flex-column w-100 overflow-hidden">
|
||||
<p class="small text-muted mb-0 text-uppercase font-weight-light cursor-pointer text-truncate text-center" style="width: 70%;" @click="goBack()">@{{ profile.acct }}</p>
|
||||
<p class="lead font-weight-bold mt-n1 mb-0">{{ $t('profile.following') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-dark rounded-pill font-weight-bold spacer-btn" href="#">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoaded" class="list-group scroll-card">
|
||||
<div v-for="(account, idx) in feed" class="list-group-item">
|
||||
<a
|
||||
:id="'apop_'+account.id"
|
||||
:href="account.url"
|
||||
@click.prevent="goToProfile(account)"
|
||||
class="text-decoration-none">
|
||||
<div class="media">
|
||||
<img
|
||||
:src="account.avatar"
|
||||
width="40"
|
||||
height="40"
|
||||
style="border-radius: 8px;"
|
||||
class="mr-3 shadow-sm"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
|
||||
|
||||
<div class="media-body">
|
||||
<p class="mb-0 text-truncate">
|
||||
<span class="text-dark font-weight-bold text-decoration-none" v-html="getUsername(account)"></span>
|
||||
</p>
|
||||
<p class="mb-0 mt-n1 text-muted small text-break">@{{ account.acct }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<b-popover :target="'apop_'+account.id" triggers="hover" placement="left" delay="1000" custom-class="shadow border-0 rounded-px">
|
||||
<profile-hover-card :profile="account" />
|
||||
</b-popover>
|
||||
</div>
|
||||
|
||||
<div v-if="canLoadMore">
|
||||
<intersect @enter="enterIntersect">
|
||||
<placeholder />
|
||||
</intersect>
|
||||
</div>
|
||||
|
||||
<div v-if="!canLoadMore && !feed.length">
|
||||
<div class="list-group-item text-center">
|
||||
<div v-if="isWarmingCache" class="px-4">
|
||||
<p class="mb-0 lead font-weight-bold">Loading Following...</p>
|
||||
<div class="py-3">
|
||||
<b-spinner variant="primary" style="width: 1.5rem; height: 1.5rem;" />
|
||||
</div>
|
||||
<p class="small text-muted mb-0">Please wait while we collect following accounts, this shouldn't take long!</p>
|
||||
</div>
|
||||
<p v-else class="mb-0 font-weight-bold">No following anyone yet!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="list-group">
|
||||
<placeholder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import Intersect from 'vue-intersect'
|
||||
import Placeholder from './../post/LikeListPlaceholder.vue';
|
||||
import ProfileHoverCard from './ProfileHoverCard.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { parseLinkHeader } from '@web3-storage/parse-link-header';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
profile: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
ProfileHoverCard,
|
||||
Intersect,
|
||||
Placeholder
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getCustomEmoji'
|
||||
])
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoaded: false,
|
||||
feed: [],
|
||||
cursor: null,
|
||||
canLoadMore: true,
|
||||
isFetchingMore: false,
|
||||
cacheWarmTimeout: undefined,
|
||||
cacheWarmInterations: 0,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchFollowers();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.cacheWarmTimeout);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchFollowers() {
|
||||
axios.get('/api/v1/accounts/'+this.profile.id+'/following', {
|
||||
params: {
|
||||
cursor: this.cursor,
|
||||
'_pe': 1
|
||||
}
|
||||
}).then(res => {
|
||||
if(!res.data.length) {
|
||||
this.canLoadMore = false;
|
||||
this.isLoaded = true;
|
||||
if(this.cursor == null && this.profile.following_count) {
|
||||
this.isWarmingCache = true;
|
||||
this.setCacheWarmTimeout();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.cursor = links.prev.cursor;
|
||||
this.canLoadMore = true;
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} else {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
this.feed.push(...res.data);
|
||||
this.isLoaded = true;
|
||||
this.isFetchingMore = false;
|
||||
if(this.isWarmingCache || this.cacheWarmTimeout) {
|
||||
this.isWarmingCache = false;
|
||||
clearTimeout(this.cacheWarmTimeout);
|
||||
this.cacheWarmTimeout = undefined;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.canLoadMore = false;
|
||||
this.isLoaded = true;
|
||||
this.isFetchingMore = false;
|
||||
})
|
||||
},
|
||||
|
||||
enterIntersect() {
|
||||
if(this.isFetchingMore) {
|
||||
return;
|
||||
}
|
||||
this.isFetchingMore = true;
|
||||
this.fetchFollowers();
|
||||
},
|
||||
|
||||
getUsername(profile) {
|
||||
let self = this;
|
||||
let dn = profile.display_name;
|
||||
if(!dn || !dn.trim().length) {
|
||||
return profile.username;
|
||||
}
|
||||
|
||||
if(dn.includes(':')) {
|
||||
let re = /(<a?)?:\w+:(\d{18}>)?/g;
|
||||
let un = dn.replaceAll(re, function(em) {
|
||||
let shortcode = em.slice(1, em.length - 1);
|
||||
let emoji = self.getCustomEmoji.filter(e => {
|
||||
return e.shortcode == shortcode;
|
||||
});
|
||||
return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
|
||||
});
|
||||
return un;
|
||||
} else {
|
||||
return dn;
|
||||
}
|
||||
},
|
||||
|
||||
goToProfile(account) {
|
||||
this.$router.push({
|
||||
path: `/i/web/profile/${account.id}`,
|
||||
params: {
|
||||
id: account.id,
|
||||
cachedProfile: account,
|
||||
cachedUser: this.profile
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.$emit('back');
|
||||
},
|
||||
|
||||
setCacheWarmTimeout() {
|
||||
if(this.cacheWarmInterations >= 5) {
|
||||
this.isWarmingCache = false;
|
||||
swal('Oops', 'Its taking longer than expected to collect following accounts. Please try again later', 'error');
|
||||
return;
|
||||
}
|
||||
this.cacheWarmTimeout = setTimeout(() => {
|
||||
this.cacheWarmInterations++;
|
||||
this.fetchFollowers();
|
||||
}, 45000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-following-component {
|
||||
.list-group-item {
|
||||
border: none;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-card {
|
||||
max-height: calc(100vh - 250px);
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer-btn {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -122,12 +122,6 @@
|
|||
</a>
|
||||
</div> -->
|
||||
|
||||
<!-- <div v-else-if="n.type == 'follow' && n.relationship.following == false">
|
||||
<a href="#" class="btn btn-primary py-0 font-weight-bold" @click.prevent="followProfile(n);">
|
||||
Follow
|
||||
</a>
|
||||
</div> -->
|
||||
|
||||
<!-- <div v-else-if="n.status && n.status.parent && !n.status.parent.media_attachments && n.type == 'like' && n.relationship.following == false">
|
||||
<a href="#" class="btn btn-primary py-0 font-weight-bold">
|
||||
Follow
|
||||
|
@ -290,24 +284,6 @@ export default {
|
|||
return '/p/' + username + '/' + id;
|
||||
},
|
||||
|
||||
followProfile(n) {
|
||||
let self = this;
|
||||
let id = n.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
self.notifications.map(notification => {
|
||||
if(notification.account.id === id) {
|
||||
notification.relationship.following = true;
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
viewContext(n) {
|
||||
switch(n.type) {
|
||||
case 'follow':
|
||||
|
|
|
@ -371,7 +371,7 @@
|
|||
centered
|
||||
title="Likes"
|
||||
body-class="list-group-flush py-3 px-0">
|
||||
<div class="list-group">
|
||||
<div v-if="likedLoaded" class="list-group">
|
||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
|
||||
<div class="media">
|
||||
<a :href="user.url">
|
||||
|
@ -392,44 +392,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<infinite-loading @infinite="infiniteLikesHandler" spinner="spiral">
|
||||
<infinite-loading v-if="likesCanLoadMore" @infinite="infiniteLikesHandler" spinner="spiral">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="sharesModal"
|
||||
id="s-modal"
|
||||
hide-footer
|
||||
centered
|
||||
title="Shares"
|
||||
body-class="list-group-flush py-3 px-0">
|
||||
<div class="list-group">
|
||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in shares" :key="'modal_shares_'+index">
|
||||
<div class="media">
|
||||
<a :href="user.url">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<div class="d-inline-block">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="user.url" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-muted mb-0" style="font-size: 14px">
|
||||
{{user.display_name}}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="float-right"><!-- <a class="btn btn-primary font-weight-bold py-1" href="#">Follow</a> --></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<infinite-loading @infinite="infiniteSharesHandler" spinner="spiral">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
<div v-else class="d-flex justify-content-center align-items-center h-100">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="lightboxModal"
|
||||
|
@ -515,8 +484,6 @@
|
|||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<div class="list-group text-center">
|
||||
<!-- <div v-if="user && user.id != status.account.id && relationship && relationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
|
||||
<div v-if="user && user.id != status.account.id && relationship && !relationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div> -->
|
||||
<div v-if="status && status.local == true" class="list-group-item rounded cursor-pointer" @click="showEmbedPostModal()">Embed</div>
|
||||
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
|
||||
<div v-if="status && user.id == status.account.id" class="list-group-item rounded cursor-pointer" @click="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</div>
|
||||
|
@ -667,6 +634,7 @@ import VueTribute from 'vue-tribute';
|
|||
import PollCard from './partials/PollCard.vue';
|
||||
import CommentFeed from './partials/CommentFeed.vue';
|
||||
import StatusCard from './partials/StatusCard.vue';
|
||||
import { parseLinkHeader } from '@web3-storage/parse-link-header';
|
||||
|
||||
pixelfed.postComponent = {};
|
||||
|
||||
|
@ -702,9 +670,10 @@ export default {
|
|||
shared: false
|
||||
},
|
||||
likes: [],
|
||||
likesPage: 1,
|
||||
likesCursor: null,
|
||||
likesCanLoadMore: true,
|
||||
likedLoaded: false,
|
||||
shares: [],
|
||||
sharesPage: 1,
|
||||
lightboxMedia: false,
|
||||
replyText: '',
|
||||
replyStatus: {},
|
||||
|
@ -847,8 +816,6 @@ export default {
|
|||
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);
|
||||
});
|
||||
self.likesPage = 2;
|
||||
self.sharesPage = 2;
|
||||
self.showCaption = !response.data.status.sensitive;
|
||||
if(self.status.comments_disabled == false) {
|
||||
self.showComments = true;
|
||||
|
@ -886,59 +853,68 @@ export default {
|
|||
window.location.href = '/login?next=' + encodeURIComponent('/p/' + this.status.shortcode);
|
||||
return;
|
||||
}
|
||||
if(this.likes.length) {
|
||||
if(this.likes && this.likes.length) {
|
||||
this.$refs.likesModal.show();
|
||||
return;
|
||||
}
|
||||
axios.get('/api/v2/likes/profile/'+this.statusUsername+'/status/'+this.statusId)
|
||||
axios.get('/api/v1/statuses/'+ this.statusId + '/favourited_by', {
|
||||
params: {
|
||||
limit: 40,
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.likes = res.data.data;
|
||||
this.$refs.likesModal.show();
|
||||
});
|
||||
},
|
||||
this.likes = res.data;
|
||||
|
||||
sharesModal() {
|
||||
if(this.status.reblogs_count == 0 || $('body').hasClass('loggedIn') == false) {
|
||||
window.location.href = '/login?next=' + encodeURIComponent('/p/' + this.status.shortcode);
|
||||
return;
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.likesCursor = links.prev.cursor;
|
||||
this.likesCanLoadMore = true;
|
||||
} else {
|
||||
this.likesCanLoadMore = false;
|
||||
}
|
||||
if(this.shares.length) {
|
||||
this.$refs.sharesModal.show();
|
||||
return;
|
||||
} else {
|
||||
this.likesCanLoadMore = false;
|
||||
}
|
||||
axios.get('/api/v2/shares/profile/'+this.statusUsername+'/status/'+this.statusId)
|
||||
.then(res => {
|
||||
this.shares = res.data.data;
|
||||
this.$refs.sharesModal.show();
|
||||
});
|
||||
this.$refs.likesModal.show();
|
||||
})
|
||||
.then(() => {
|
||||
setTimeout(() => { this.likedLoaded = true }, 1000);
|
||||
})
|
||||
},
|
||||
|
||||
infiniteLikesHandler($state) {
|
||||
let api = '/api/v2/likes/profile/'+this.statusUsername+'/status/'+this.statusId;
|
||||
axios.get(api, {
|
||||
params: {
|
||||
page: this.likesPage,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
if (data.data.length > 0) {
|
||||
this.likes.push(...data.data);
|
||||
this.likesPage++;
|
||||
$state.loaded();
|
||||
} else {
|
||||
if(!this.likesCanLoadMore) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
infiniteSharesHandler($state) {
|
||||
axios.get('/api/v2/shares/profile/'+this.statusUsername+'/status/'+this.statusId, {
|
||||
axios.get('/api/v1/statuses/'+ this.statusId + '/favourited_by', {
|
||||
params: {
|
||||
page: this.sharesPage,
|
||||
cursor: this.likesCursor,
|
||||
limit: 20,
|
||||
'_pe': 1
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
if (data.data.length > 0) {
|
||||
this.shares.push(...data.data);
|
||||
this.sharesPage++;
|
||||
}).then(res => {
|
||||
if (res && res.data.length) {
|
||||
this.likes.push(...res.data);
|
||||
}
|
||||
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.likesCursor = links.prev.cursor;
|
||||
this.likesCanLoadMore = true;
|
||||
} else {
|
||||
this.likesCanLoadMore = false;
|
||||
}
|
||||
} else {
|
||||
this.likesCanLoadMore = false;
|
||||
}
|
||||
return this.likesCanLoadMore;
|
||||
}).then(res => {
|
||||
if(res) {
|
||||
$state.loaded();
|
||||
} else {
|
||||
$state.complete();
|
||||
|
@ -1627,34 +1603,6 @@ export default {
|
|||
return;
|
||||
},
|
||||
|
||||
ctxMenuFollow() {
|
||||
let id = this.status.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
let username = this.status.account.acct;
|
||||
this.relationship.following = true;
|
||||
this.$refs.ctxModal.hide();
|
||||
setTimeout(function() {
|
||||
swal('Follow successful!', 'You are now following ' + username, 'success');
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
ctxMenuUnfollow() {
|
||||
let id = this.status.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
let username = this.status.account.acct;
|
||||
this.relationship.following = false;
|
||||
this.$refs.ctxModal.hide();
|
||||
setTimeout(function() {
|
||||
swal('Unfollow successful!', 'You are no longer following ' + username, 'success');
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
archivePost(status) {
|
||||
if(window.confirm('Are you sure you want to archive this post?') == false) {
|
||||
return;
|
||||
|
|
|
@ -133,10 +133,10 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="d-flex align-items-center mb-1">
|
||||
<span class="font-weight-bold mr-1">{{profile.display_name}}</span>
|
||||
<span v-if="profile.pronouns" class="text-muted small">{{profile.pronouns.join('/')}}</span>
|
||||
</p>
|
||||
<div class="d-md-flex align-items-center mb-1 text-break">
|
||||
<div class="font-weight-bold mr-1">{{profile.display_name}}</div>
|
||||
<div v-if="profile.pronouns" class="text-muted small">{{profile.pronouns.join('/')}}</div>
|
||||
</div>
|
||||
<p v-if="profile.note" class="mb-0" v-html="profile.note"></p>
|
||||
<p v-if="profile.website"><a :href="profile.website" class="profile-website small" rel="me external nofollow noopener" target="_blank">{{formatWebsite(profile.website)}}</a></p>
|
||||
<p class="d-flex small text-muted align-items-center">
|
||||
|
@ -154,8 +154,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-block d-md-none my-0 pt-3 border-bottom">
|
||||
<p v-if="user && user.hasOwnProperty('id')" class="pt-3">
|
||||
<div v-if="user && user.hasOwnProperty('id')" class="d-block d-md-none my-0 pt-3 border-bottom">
|
||||
<p class="pt-3">
|
||||
<button v-if="owner" class="btn btn-outline-secondary bg-white btn-sm py-1 btn-block text-center font-weight-bold text-dark border border-lighter" @click.prevent="redirect('/settings/home')">Edit Profile</button>
|
||||
<button v-if="!owner && relationship.following" class="btn btn-outline-secondary bg-white btn-sm py-1 px-5 font-weight-bold text-dark border border-lighter" @click="followProfile"> Unfollow </button>
|
||||
<button v-if="!owner && !relationship.following" class="btn btn-primary btn-sm py-1 px-5 font-weight-bold" @click="followProfile">{{relationship.followed_by ? 'Follow Back' : ' Follow '}}</button>
|
||||
|
@ -339,16 +339,10 @@
|
|||
<span class="text-dark">{{profileUsername}}</span> is not following yet</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
|
||||
<span class="d-flex px-4 pb-0 align-items-center">
|
||||
<i class="fas fa-search text-lighter"></i>
|
||||
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
|
||||
</span>
|
||||
</div>
|
||||
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
|
||||
<div class="media">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;" v-once>
|
||||
</a>
|
||||
<div class="media-body text-truncate">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
|
@ -368,7 +362,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
|
||||
<div v-if="!followingLoading && following.length == 0" class="list-group-item border-0">
|
||||
<div class="list-group-item border-0 pt-5">
|
||||
<p class="p-3 text-center mb-0 lead">No Results Found</p>
|
||||
</div>
|
||||
|
@ -394,7 +388,7 @@
|
|||
dialog-class="follow-modal"
|
||||
>
|
||||
<div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
|
||||
<div v-if="!followers.length" class="list-group-item border-0">
|
||||
<div v-if="!followerLoading && !followers.length" class="list-group-item border-0">
|
||||
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
||||
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
|
||||
</div>
|
||||
|
@ -403,7 +397,7 @@
|
|||
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in followers" :key="'follower_'+index">
|
||||
<div class="media mb-0">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;" v-once>
|
||||
</a>
|
||||
<div class="media-body mb-0">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
|
@ -593,6 +587,7 @@
|
|||
<script type="text/javascript">
|
||||
import VueMasonry from 'vue-masonry-css'
|
||||
import StatusCard from './partials/StatusCard.vue';
|
||||
import { parseLinkHeader } from '@web3-storage/parse-link-header';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
|
@ -623,11 +618,11 @@
|
|||
modalStatus: false,
|
||||
relationship: {},
|
||||
followers: [],
|
||||
followerCursor: 1,
|
||||
followerCursor: null,
|
||||
followerMore: true,
|
||||
followerLoading: true,
|
||||
following: [],
|
||||
followingCursor: 1,
|
||||
followingCursor: null,
|
||||
followingMore: true,
|
||||
followingLoading: true,
|
||||
warning: false,
|
||||
|
@ -641,8 +636,6 @@
|
|||
ctxEmbedPayload: null,
|
||||
copiedEmbed: false,
|
||||
hasStory: null,
|
||||
followingModalSearch: null,
|
||||
followingModalSearchCache: null,
|
||||
followingModalTab: 'following',
|
||||
bookmarksLoading: true,
|
||||
archives: [],
|
||||
|
@ -1056,19 +1049,22 @@
|
|||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
axios.post('/i/follow', {
|
||||
item: this.profileId
|
||||
}).then(res => {
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
if(this.relationship.following) {
|
||||
const curState = this.relationship.following;
|
||||
const apiUrl = curState ?
|
||||
'/api/v1/accounts/' + this.profileId + '/unfollow' :
|
||||
'/api/v1/accounts/' + this.profileId + '/follow';
|
||||
axios.post(apiUrl)
|
||||
.then(res => {
|
||||
if(curState) {
|
||||
this.profile.followers_count--;
|
||||
if(this.profile.locked == true) {
|
||||
window.location.href = '/';
|
||||
if(this.profile.locked) {
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
this.profile.followers_count++;
|
||||
}
|
||||
this.relationship.following = !this.relationship.following;
|
||||
this.relationship = res.data;
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
|
@ -1084,23 +1080,34 @@
|
|||
if(this.profileSettings.following.list == false) {
|
||||
return;
|
||||
}
|
||||
if(this.followingCursor > 1) {
|
||||
if(this.followingCursor) {
|
||||
this.$refs.followingModal.show();
|
||||
return;
|
||||
} else {
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profileId+'/following', {
|
||||
axios.get('/api/v1/accounts/'+this.profileId+'/following', {
|
||||
params: {
|
||||
page: this.followingCursor
|
||||
cursor: this.followingCursor,
|
||||
limit: 40,
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.following = res.data;
|
||||
this.followingModalSearchCache = res.data;
|
||||
this.followingCursor++;
|
||||
if(res.data.length < 10) {
|
||||
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.followingCursor = links.prev.cursor;
|
||||
this.followingMore = true;
|
||||
} else {
|
||||
this.followingMore = false;
|
||||
}
|
||||
this.followingLoading = false;
|
||||
} else {
|
||||
this.followingMore = false;
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
setTimeout(() => { this.followingLoading = false }, 1000);
|
||||
});
|
||||
this.$refs.followingModal.show();
|
||||
return;
|
||||
|
@ -1119,19 +1126,30 @@
|
|||
this.$refs.followerModal.show();
|
||||
return;
|
||||
} else {
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profileId+'/followers', {
|
||||
axios.get('/api/v1/accounts/'+this.profileId+'/followers', {
|
||||
params: {
|
||||
page: this.followerCursor
|
||||
cursor: this.followerCursor,
|
||||
limit: 40,
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.followers.push(...res.data);
|
||||
this.followerCursor++;
|
||||
if(res.data.length < 10) {
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.followerCursor = links.prev.cursor;
|
||||
this.followerMore = true;
|
||||
} else {
|
||||
this.followerMore = false;
|
||||
}
|
||||
} else {
|
||||
this.followerMore = false;
|
||||
}
|
||||
this.followerLoading = false;
|
||||
})
|
||||
.then(() => {
|
||||
setTimeout(() => { this.followerLoading = false }, 1000);
|
||||
});
|
||||
this.$refs.followerModal.show();
|
||||
return;
|
||||
}
|
||||
|
@ -1142,20 +1160,27 @@
|
|||
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
|
||||
return;
|
||||
}
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profile.id+'/following', {
|
||||
axios.get('/api/v1/accounts/'+this.profile.id+'/following', {
|
||||
params: {
|
||||
page: this.followingCursor,
|
||||
fbu: this.followingModalSearch
|
||||
cursor: this.followingCursor,
|
||||
limit: 40,
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data.length > 0) {
|
||||
this.following.push(...res.data);
|
||||
this.followingCursor++;
|
||||
this.followingModalSearchCache = this.following;
|
||||
}
|
||||
if(res.data.length < 10) {
|
||||
this.followingModalSearchCache = this.following;
|
||||
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.followingCursor = links.prev.cursor;
|
||||
this.followingMore = true;
|
||||
} else {
|
||||
this.followingMore = false;
|
||||
}
|
||||
} else {
|
||||
this.followingMore = false;
|
||||
}
|
||||
});
|
||||
|
@ -1165,17 +1190,27 @@
|
|||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profile.id+'/followers', {
|
||||
axios.get('/api/v1/accounts/'+this.profile.id+'/followers', {
|
||||
params: {
|
||||
page: this.followerCursor
|
||||
cursor: this.followerCursor,
|
||||
limit: 40,
|
||||
'_pe': 1
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data.length > 0) {
|
||||
this.followers.push(...res.data);
|
||||
this.followerCursor++;
|
||||
}
|
||||
if(res.data.length < 10) {
|
||||
|
||||
if(res.headers && res.headers.link) {
|
||||
const links = parseLinkHeader(res.headers.link);
|
||||
if(links.prev) {
|
||||
this.followerCursor = links.prev.cursor;
|
||||
this.followerMore = true;
|
||||
} else {
|
||||
this.followerMore = false;
|
||||
}
|
||||
} else {
|
||||
this.followerMore = false;
|
||||
}
|
||||
});
|
||||
|
@ -1186,9 +1221,11 @@
|
|||
},
|
||||
|
||||
followModalAction(id, index, type = 'following') {
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
const apiUrl = type === 'following' ?
|
||||
'/api/v1/accounts/' + id + '/unfollow' :
|
||||
'/api/v1/accounts/' + id + '/follow';
|
||||
axios.post(apiUrl)
|
||||
.then(res => {
|
||||
if(type == 'following') {
|
||||
this.following.splice(index, 1);
|
||||
this.profile.following_count--;
|
||||
|
@ -1284,28 +1321,6 @@
|
|||
window.location.href = '/stories/' + this.profileUsername + '?t=4';
|
||||
},
|
||||
|
||||
followingModalSearchHandler() {
|
||||
let self = this;
|
||||
let q = this.followingModalSearch;
|
||||
|
||||
if(q.length == 0) {
|
||||
this.following = this.followingModalSearchCache;
|
||||
this.followingModalSearch = null;
|
||||
}
|
||||
if(q.length > 0) {
|
||||
let url = '/api/pixelfed/v1/accounts/' +
|
||||
self.profileId + '/following?page=1&fbu=' +
|
||||
q;
|
||||
|
||||
axios.get(url).then(res => {
|
||||
this.following = res.data;
|
||||
}).catch(err => {
|
||||
self.following = self.followingModalSearchCache;
|
||||
self.followingModalSearch = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
truncate(str, len) {
|
||||
return _.truncate(str, {
|
||||
length: len
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,746 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom">
|
||||
<div class="container">
|
||||
<p class="text-center font-weight-bold">You are blocking this account</p>
|
||||
<p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false;">here</a> to view profile</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" style="height: 80vh;" class="d-flex justify-content-center align-items-center">
|
||||
<img src="/img/pixelfed-icon-grey.svg" class="">
|
||||
</div>
|
||||
<div v-if="!loading && !warning" class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 pt-5">
|
||||
<div class="card shadow-none border">
|
||||
<div class="card-header p-0 m-0">
|
||||
<img v-if="profile.header_bg" :src="profile.header_bg" style="width: 100%; height: 140px; object-fit: cover;">
|
||||
<div v-else class="bg-primary" style="width: 100%;height: 140px;"></div>
|
||||
</div>
|
||||
<div class="card-body pb-0">
|
||||
<div class="mt-n5 mb-3">
|
||||
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.avatar" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
|
||||
<span class="float-right mt-n1">
|
||||
<span>
|
||||
<button v-if="relationship && relationship.following == false" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="followProfile();">Follow</button>
|
||||
<button v-if="relationship && relationship.following == true" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="unfollowProfile();">Unfollow</button>
|
||||
</span>
|
||||
<span class="mx-2">
|
||||
<a :href="'/account/direct/t/' + profile.id" class="btn btn-outline-light btn-sm mt-n1" style="padding-top:2px;padding-bottom:1px;">
|
||||
<i class="far fa-comment-dots cursor-pointer" style="font-size:13px;"></i>
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<button class="btn btn-outline-light btn-sm mt-n1" @click="showCtxMenu()" style="padding-top:2px;padding-bottom:1px;">
|
||||
<i class="fas fa-cog cursor-pointer" style="font-size:13px;"></i>
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="pl-2 h4 font-weight-bold mb-1">{{profile.display_name}}</p>
|
||||
<p class="pl-2 font-weight-bold mb-2"><a class="text-muted" :href="profile.url" @click.prevent="urlRedirectHandler(profile.url)">{{profile.acct}}</a></p>
|
||||
<p class="pl-2 text-muted small d-flex justify-content-between">
|
||||
<span>
|
||||
<span class="font-weight-bold text-dark">{{profile.statuses_count}}</span>
|
||||
<span>Posts</span>
|
||||
</span>
|
||||
<span class="cursor-pointer" @click="followingModal()">
|
||||
<span class="font-weight-bold text-dark">{{profile.following_count}}</span>
|
||||
<span>Following</span>
|
||||
</span>
|
||||
<span class="cursor-pointer" @click="followersModal()">
|
||||
<span class="font-weight-bold text-dark">{{profile.followers_count}}</span>
|
||||
<span>Followers</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="pl-2 text-muted small pt-2" v-html="profile.note"></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="small text-lighter p-2">Last updated: <time :datetime="profile.last_fetched_at">{{timeAgo(profile.last_fetched_at, 'ago')}}</time></p>
|
||||
<p class="card border-left-primary card-body small py-2 text-muted font-weight-bold shadow-none border-top border-bottom border-right">You are viewing a profile from a remote server, it may not contain up-to-date information.</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 pt-5">
|
||||
<div class="row">
|
||||
<div class="col-12" v-for="(status, index) in feed" :key="'remprop' + index">
|
||||
<status-card
|
||||
:class="{'border-top': index === 0}"
|
||||
:status="status" />
|
||||
</div>
|
||||
|
||||
<div v-if="feed.length == 0" class="col-12 mb-2">
|
||||
<div class="d-flex justify-content-center align-items-center bg-white border rounded" style="height:60vh;">
|
||||
<div class="text-center">
|
||||
<p class="lead">We haven't seen any posts from this account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="col-12 mt-4">
|
||||
<p v-if="showLoadMore" class="text-center mb-0 px-0">
|
||||
<button @click="loadMorePosts()" class="btn btn-outline-primary btn-block font-weight-bold">
|
||||
<span v-if="!loadingMore">Load More</span>
|
||||
<span v-else>
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-modal
|
||||
v-if="profile && following"
|
||||
ref="followingModal"
|
||||
id="following-modal"
|
||||
hide-footer
|
||||
centered
|
||||
scrollable
|
||||
title="Following"
|
||||
body-class="list-group-flush py-3 px-0"
|
||||
dialog-class="follow-modal">
|
||||
<div v-if="!followingLoading" class="list-group" style="max-height: 60vh;">
|
||||
<div v-if="!following.length" class="list-group-item border-0">
|
||||
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
||||
<span class="text-dark">{{profileUsername}}</span> is not following yet</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
|
||||
<span class="d-flex px-4 pb-0 align-items-center">
|
||||
<i class="fas fa-search text-lighter"></i>
|
||||
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
|
||||
</span>
|
||||
</div>
|
||||
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
|
||||
<div class="media">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
</a>
|
||||
<div class="media-body text-truncate">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name ? user.display_name : user.username}}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="owner">
|
||||
<a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
|
||||
<div class="list-group-item border-0 pt-5">
|
||||
<p class="p-3 text-center mb-0 lead">No Results Found</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
|
||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="followerModal"
|
||||
id="follower-modal"
|
||||
hide-footer
|
||||
centered
|
||||
scrollable
|
||||
title="Followers"
|
||||
body-class="list-group-flush py-3 px-0"
|
||||
dialog-class="follow-modal"
|
||||
>
|
||||
<div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
|
||||
<div v-if="!followers.length" class="list-group-item border-0">
|
||||
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
||||
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in followers" :key="'follower_'+index">
|
||||
<div class="media mb-0">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
</a>
|
||||
<div class="media-body mb-0">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name ? user.display_name : user.username}}
|
||||
</p>
|
||||
</div>
|
||||
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()">
|
||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="visitorContextMenu"
|
||||
id="visitor-context-menu"
|
||||
hide-footer
|
||||
hide-header
|
||||
centered
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0">
|
||||
<div class="list-group" v-if="relationship">
|
||||
<div class="list-group-item cursor-pointer text-center rounded text-dark" @click="copyProfileLink">
|
||||
Copy Link
|
||||
</div>
|
||||
<div v-if="user && !owner && !relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="muteProfile">
|
||||
Mute
|
||||
</div>
|
||||
<div v-if="user && !owner && relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="unmuteProfile">
|
||||
Unmute
|
||||
</div>
|
||||
<div v-if="user && !owner" class="list-group-item cursor-pointer text-center rounded text-dark" @click="reportProfile">
|
||||
Report User
|
||||
</div>
|
||||
<div v-if="user && !owner && !relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="blockProfile">
|
||||
Block
|
||||
</div>
|
||||
<div v-if="user && !owner && relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="unblockProfile">
|
||||
Unblock
|
||||
</div>
|
||||
|
||||
<div class="list-group-item cursor-pointer text-center rounded text-muted" @click="$refs.visitorContextMenu.hide()">
|
||||
Close
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<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="ctxMenuStatus && profile.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div>
|
||||
<div v-if="ctxMenuStatus && profile.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="ctxMenuStatus && profile.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()">Go to post</div>
|
||||
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
|
||||
<div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
|
||||
<div v-if="ctxMenuStatus && (profile.is_admin || profile.id == profile.id)" class="list-group-item rounded cursor-pointer" @click="deletePost(ctxMenuStatus)">Delete</div>
|
||||
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import StatusCard from './partials/StatusCard.vue';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'profile-id',
|
||||
],
|
||||
|
||||
components: {
|
||||
StatusCard
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
id: [],
|
||||
ids: [],
|
||||
user: false,
|
||||
profile: {},
|
||||
feed: [],
|
||||
min_id: null,
|
||||
max_id: null,
|
||||
loading: true,
|
||||
owner: false,
|
||||
layoutType: true,
|
||||
relationship: null,
|
||||
warning: false,
|
||||
ctxMenuStatus: false,
|
||||
ctxMenuRelationship: false,
|
||||
fetchingRemotePosts: false,
|
||||
showMutualFollowers: false,
|
||||
loadingMore: false,
|
||||
showLoadMore: true,
|
||||
followers: [],
|
||||
followerCursor: 1,
|
||||
followerMore: true,
|
||||
followerLoading: true,
|
||||
following: [],
|
||||
followingCursor: 1,
|
||||
followingMore: true,
|
||||
followingLoading: true,
|
||||
followingModalSearch: null,
|
||||
followingModalSearchCache: null,
|
||||
followingModalTab: 'following',
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
this.fetchRelationships();
|
||||
this.fetchProfile();
|
||||
},
|
||||
|
||||
updated() {
|
||||
document.querySelectorAll('.hashtag').forEach(function(i, e) {
|
||||
i.href = App.util.format.rewriteLinks(i);
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchProfile() {
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
|
||||
this.user = res.data
|
||||
window._sharedData.curUser = res.data;
|
||||
window.App.util.navatar();
|
||||
});
|
||||
axios.get('/api/pixelfed/v1/accounts/' + this.profileId)
|
||||
.then(res => {
|
||||
this.profile = res.data;
|
||||
this.fetchPosts();
|
||||
});
|
||||
},
|
||||
|
||||
fetchPosts() {
|
||||
let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
|
||||
axios.get(apiUrl, {
|
||||
params: {
|
||||
only_media: true,
|
||||
min_id: 1,
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
let data = res.data
|
||||
.filter(status => status.media_attachments.length > 0);
|
||||
let ids = data.map(status => status.id);
|
||||
this.ids = ids;
|
||||
this.min_id = Math.max(...ids);
|
||||
this.max_id = Math.min(...ids);
|
||||
this.feed = data;
|
||||
this.loading = false;
|
||||
//this.loadSponsor();
|
||||
}).catch(err => {
|
||||
swal('Oops, something went wrong',
|
||||
'Please release the page.',
|
||||
'error');
|
||||
});
|
||||
},
|
||||
|
||||
loadMorePosts() {
|
||||
this.loadingMore = true;
|
||||
let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
|
||||
axios.get(apiUrl, {
|
||||
params: {
|
||||
only_media: true,
|
||||
max_id: this.max_id,
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
let data = res.data
|
||||
.filter(status => this.ids.indexOf(status.id) === -1)
|
||||
.filter(status => status.media_attachments.length > 0)
|
||||
.map(status => {
|
||||
return {
|
||||
id: status.id,
|
||||
caption: {
|
||||
text: status.content_text,
|
||||
html: status.content
|
||||
},
|
||||
count: {
|
||||
likes: status.favourites_count,
|
||||
shares: status.reblogs_count,
|
||||
comments: status.reply_count
|
||||
},
|
||||
thumb: status.media_attachments[0].url,
|
||||
media: status.media_attachments,
|
||||
timestamp: status.created_at,
|
||||
type: status.pf_type,
|
||||
url: status.url,
|
||||
sensitive: status.sensitive,
|
||||
cw: status.sensitive,
|
||||
spoiler_text: status.spoiler_text
|
||||
}
|
||||
});
|
||||
let ids = data.map(status => status.id);
|
||||
this.ids.push(...ids);
|
||||
this.max_id = Math.min(...ids);
|
||||
this.feed.push(...data);
|
||||
this.loadingMore = false;
|
||||
}).catch(err => {
|
||||
this.loadingMore = false;
|
||||
this.showLoadMore = false;
|
||||
});
|
||||
},
|
||||
|
||||
fetchRelationships() {
|
||||
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
axios.get('/api/pixelfed/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.profileId
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data.length) {
|
||||
this.relationship = res.data[0];
|
||||
if(res.data[0].blocking == true) {
|
||||
this.loading = false;
|
||||
this.warning = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
postPreviewUrl(post) {
|
||||
return 'background: url("'+post.thumb+'");background-size:cover';
|
||||
},
|
||||
|
||||
timestampFormat(timestamp) {
|
||||
let ts = new Date(timestamp);
|
||||
return ts.toDateString() + ' ' + ts.toLocaleTimeString();
|
||||
},
|
||||
|
||||
remoteProfileUrl(profile) {
|
||||
return '/i/web/profile/_/' + profile.id;
|
||||
},
|
||||
|
||||
remotePostUrl(status) {
|
||||
return '/i/web/post/_/' + this.profile.id + '/' + status.id;
|
||||
},
|
||||
|
||||
followProfile() {
|
||||
axios.post('/i/follow', {
|
||||
item: this.profileId
|
||||
}).then(res => {
|
||||
swal('Followed', 'You are now following ' + this.profile.username +'!', 'success');
|
||||
this.relationship.following = true;
|
||||
}).catch(err => {
|
||||
swal('Oops!', 'Something went wrong, please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
unfollowProfile() {
|
||||
axios.post('/i/follow', {
|
||||
item: this.profileId
|
||||
}).then(res => {
|
||||
swal('Unfollowed', 'You are no longer following ' + this.profile.username +'.', 'warning');
|
||||
this.relationship.following = false;
|
||||
}).catch(err => {
|
||||
swal('Oops!', 'Something went wrong, please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
showCtxMenu() {
|
||||
this.$refs.visitorContextMenu.show();
|
||||
},
|
||||
|
||||
copyProfileLink() {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
},
|
||||
|
||||
muteProfile() {
|
||||
let id = this.profileId;
|
||||
axios.post('/i/mute', {
|
||||
type: 'user',
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
},
|
||||
|
||||
unmuteProfile() {
|
||||
let id = this.profileId;
|
||||
axios.post('/i/unmute', {
|
||||
type: 'user',
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully unmuted ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
},
|
||||
|
||||
blockProfile() {
|
||||
let id = this.profileId;
|
||||
axios.post('/i/block', {
|
||||
type: 'user',
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.warning = true;
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
},
|
||||
|
||||
unblockProfile() {
|
||||
let id = this.profileId;
|
||||
axios.post('/i/unblock', {
|
||||
type: 'user',
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.warning = false;
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully unblocked ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
},
|
||||
|
||||
reportProfile() {
|
||||
window.location.href = '/l/i/report?type=profile&id=' + this.profileId;
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
},
|
||||
|
||||
ctxMenu(status) {
|
||||
this.ctxMenuStatus = status;
|
||||
let self = this;
|
||||
axios.get('/api/pixelfed/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': self.profileId
|
||||
}
|
||||
}).then(res => {
|
||||
self.ctxMenuRelationship = res.data[0];
|
||||
self.$refs.ctxModal.show();
|
||||
});
|
||||
},
|
||||
|
||||
closeCtxMenu() {
|
||||
this.ctxMenuStatus = false;
|
||||
this.ctxMenuRelationship = false;
|
||||
this.$refs.ctxModal.hide();
|
||||
},
|
||||
|
||||
ctxMenuCopyLink() {
|
||||
let status = this.ctxMenuStatus;
|
||||
navigator.clipboard.writeText(status.url);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
ctxMenuGoToPost() {
|
||||
let status = this.ctxMenuStatus;
|
||||
window.location.href = this.statusUrl(status);
|
||||
this.closeCtxMenu();
|
||||
return;
|
||||
},
|
||||
|
||||
statusUrl(status) {
|
||||
return '/i/web/post/_/' + this.profile.id + '/' + status.id;
|
||||
},
|
||||
|
||||
deletePost(status) {
|
||||
if(this.user.is_admin == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(window.confirm('Are you sure you want to delete this post?') == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/i/delete', {
|
||||
type: 'status',
|
||||
item: status.id
|
||||
}).then(res => {
|
||||
this.feed = this.feed.filter(s => {
|
||||
return s.id != status.id;
|
||||
});
|
||||
this.$refs.ctxModal.hide();
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
manuallyFetchRemotePosts($event) {
|
||||
this.fetchingRemotePosts = true;
|
||||
event.target.blur();
|
||||
swal(
|
||||
'Fetching Remote Posts',
|
||||
'Check back in a few minutes!',
|
||||
'info'
|
||||
);
|
||||
},
|
||||
|
||||
timeAgo(ts, suffix = false) {
|
||||
if(ts == null) {
|
||||
return 'never';
|
||||
}
|
||||
suffix = suffix ? ' ' + suffix : '';
|
||||
return App.util.format.timeAgo(ts) + suffix;
|
||||
},
|
||||
|
||||
urlRedirectHandler(url) {
|
||||
let p = new URL(url);
|
||||
let path = '';
|
||||
if(p.hostname == window.location.hostname) {
|
||||
path = url;
|
||||
} else {
|
||||
path = '/i/redirect?url=';
|
||||
path += encodeURI(url);
|
||||
}
|
||||
window.location.href = path;
|
||||
},
|
||||
|
||||
followingModal() {
|
||||
if(this.followingCursor > 1) {
|
||||
this.$refs.followingModal.show();
|
||||
return;
|
||||
} else {
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profileId+'/following', {
|
||||
params: {
|
||||
page: this.followingCursor
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.following = res.data;
|
||||
this.followingModalSearchCache = res.data;
|
||||
this.followingCursor++;
|
||||
if(res.data.length < 10) {
|
||||
this.followingMore = false;
|
||||
}
|
||||
this.followingLoading = false;
|
||||
});
|
||||
this.$refs.followingModal.show();
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
followersModal() {
|
||||
if(this.followerCursor > 1) {
|
||||
this.$refs.followerModal.show();
|
||||
return;
|
||||
} else {
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profileId+'/followers', {
|
||||
params: {
|
||||
page: this.followerCursor
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.followers.push(...res.data);
|
||||
this.followerCursor++;
|
||||
if(res.data.length < 10) {
|
||||
this.followerMore = false;
|
||||
}
|
||||
this.followerLoading = false;
|
||||
})
|
||||
this.$refs.followerModal.show();
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
followingLoadMore() {
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profile.id+'/following', {
|
||||
params: {
|
||||
page: this.followingCursor,
|
||||
fbu: this.followingModalSearch
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data.length > 0) {
|
||||
this.following.push(...res.data);
|
||||
this.followingCursor++;
|
||||
this.followingModalSearchCache = this.following;
|
||||
}
|
||||
if(res.data.length < 10) {
|
||||
this.followingModalSearchCache = this.following;
|
||||
this.followingMore = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
followersLoadMore() {
|
||||
axios.get('/api/pixelfed/v1/accounts/'+this.profile.id+'/followers', {
|
||||
params: {
|
||||
page: this.followerCursor
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if(res.data.length > 0) {
|
||||
this.followers.push(...res.data);
|
||||
this.followerCursor++;
|
||||
}
|
||||
if(res.data.length < 10) {
|
||||
this.followerMore = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
profileUrlRedirect(profile) {
|
||||
if(profile.local == true) {
|
||||
return profile.url;
|
||||
}
|
||||
|
||||
return '/i/web/profile/_/' + profile.id;
|
||||
},
|
||||
|
||||
followingModalSearchHandler() {
|
||||
let self = this;
|
||||
let q = this.followingModalSearch;
|
||||
|
||||
if(q.length == 0) {
|
||||
this.following = this.followingModalSearchCache;
|
||||
this.followingModalSearch = null;
|
||||
}
|
||||
if(q.length > 0) {
|
||||
let url = '/api/pixelfed/v1/accounts/' +
|
||||
self.profileId + '/following?page=1&fbu=' +
|
||||
q;
|
||||
|
||||
axios.get(url).then(res => {
|
||||
this.following = res.data;
|
||||
}).catch(err => {
|
||||
self.following = self.followingModalSearchCache;
|
||||
self.followingModalSearch = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1050px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -379,26 +379,6 @@ export default {
|
|||
this.searchContext(this.analysis);
|
||||
},
|
||||
|
||||
followProfile(profile, index) {
|
||||
this.loading = true;
|
||||
axios.post('/i/follow', {
|
||||
item: profile.entity.id
|
||||
}).then(res => {
|
||||
if(profile.entity.local == true) {
|
||||
this.fetchSearchResults();
|
||||
return;
|
||||
} else {
|
||||
this.loading = false;
|
||||
this.results.profiles[index].entity.follow_request = true;
|
||||
return;
|
||||
}
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
searchLexer() {
|
||||
let q = this.query;
|
||||
|
||||
|
|
|
@ -324,7 +324,6 @@
|
|||
</p>
|
||||
<p class="mb-0 small text-muted">{{rec.message}}</p>
|
||||
</div>
|
||||
<a class="font-weight-bold small" href="#" @click.prevent="expRecFollow(rec.id, index)">Follow</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -761,24 +760,6 @@
|
|||
})
|
||||
},
|
||||
|
||||
expRecFollow(id, index) {
|
||||
return;
|
||||
|
||||
if(this.config.ab.rec == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.suggestions.splice(index, 1);
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
owner(status) {
|
||||
return this.profile.id === status.account.id;
|
||||
},
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
size="sm"
|
||||
body-class="list-group-flush p-0 rounded">
|
||||
<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 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>
|
||||
|
@ -289,37 +287,6 @@
|
|||
return;
|
||||
},
|
||||
|
||||
ctxMenuFollow() {
|
||||
let id = this.ctxMenuStatus.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
let username = this.ctxMenuStatus.account.acct;
|
||||
this.closeCtxMenu();
|
||||
setTimeout(function() {
|
||||
swal('Follow successful!', 'You are now following ' + username, 'success');
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
ctxMenuUnfollow() {
|
||||
let id = this.ctxMenuStatus.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
let username = this.ctxMenuStatus.account.acct;
|
||||
if(this.scope == 'home') {
|
||||
this.feed = this.feed.filter(s => {
|
||||
return s.account.id != this.ctxMenuStatus.account.id;
|
||||
});
|
||||
}
|
||||
this.closeCtxMenu();
|
||||
setTimeout(function() {
|
||||
swal('Unfollow successful!', 'You are no longer following ' + username, 'success');
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
ctxMenuReportPost() {
|
||||
this.$refs.ctxModal.hide();
|
||||
this.$refs.ctxReport.show();
|
||||
|
|
|
@ -71,14 +71,7 @@
|
|||
<a v-if="status.place" class="small text-decoration-none text-muted" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" title="Location" data-toggle="tooltip"><i class="fas fa-map-marked-alt"></i> {{status.place.name}}, {{status.place.country}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="canFollow(status)">
|
||||
<span class="px-2"></span>
|
||||
<button class="btn btn-primary btn-sm font-weight-bold py-1 px-3 rounded-lg" @click="follow(status.account.id)"><i class="far fa-user-plus mr-1"></i> Follow</button>
|
||||
</div>
|
||||
<div v-if="status.hasOwnProperty('relationship') && status.relationship.hasOwnProperty('following') && status.relationship.following">
|
||||
<span class="px-2"></span>
|
||||
<button class="btn btn-outline-primary btn-sm font-weight-bold py-1 px-3 rounded-lg" @click="unfollow(status.account.id)"><i class="far fa-user-check mr-1"></i> Following</button>
|
||||
</div>
|
||||
|
||||
<div class="text-right" style="flex-grow:1;">
|
||||
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
|
||||
<span class="fas fa-ellipsis-h text-lighter"></span>
|
||||
|
@ -397,52 +390,6 @@
|
|||
|
||||
statusDeleted(status) {
|
||||
this.$emit('status-delete', status);
|
||||
},
|
||||
|
||||
canFollow(status) {
|
||||
if(!status.hasOwnProperty('relationship')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!status.hasOwnProperty('account') || !status.account.hasOwnProperty('id')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(status.account.id == this.profile.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !status.relationship.following;
|
||||
},
|
||||
|
||||
follow(id) {
|
||||
event.currentTarget.blur();
|
||||
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.status.relationship.following = true;
|
||||
this.$emit('followed', id);
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unfollow(id) {
|
||||
event.currentTarget.blur();
|
||||
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.status.relationship.following = false;
|
||||
this.$emit('unfollowed', id);
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
34
resources/assets/js/rempos.js
vendored
34
resources/assets/js/rempos.js
vendored
|
@ -1,34 +0,0 @@
|
|||
Vue.component(
|
||||
'photo-presenter',
|
||||
require('./components/presenter/PhotoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-presenter',
|
||||
require('./components/presenter/VideoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'photo-album-presenter',
|
||||
require('./components/presenter/PhotoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-album-presenter',
|
||||
require('./components/presenter/VideoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'mixed-album-presenter',
|
||||
require('./components/presenter/MixedAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'post-menu',
|
||||
require('./components/PostMenu.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'remote-post',
|
||||
require('./components/RemotePost.vue').default
|
||||
);
|
29
resources/assets/js/rempro.js
vendored
29
resources/assets/js/rempro.js
vendored
|
@ -1,29 +0,0 @@
|
|||
Vue.component(
|
||||
'photo-presenter',
|
||||
require('./components/presenter/PhotoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-presenter',
|
||||
require('./components/presenter/VideoPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'photo-album-presenter',
|
||||
require('./components/presenter/PhotoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'video-album-presenter',
|
||||
require('./components/presenter/VideoAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'mixed-album-presenter',
|
||||
require('./components/presenter/MixedAlbumPresenter.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'remote-profile',
|
||||
require('./components/RemoteProfile.vue').default
|
||||
);
|
|
@ -1,121 +0,0 @@
|
|||
@extends('layouts.app',['title' => $user->username . " on " . config('app.name')])
|
||||
|
||||
@section('content')
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger text-center font-weight-bold mb-0">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
@include('profile.partial.user-info')
|
||||
|
||||
@if(true === $owner)
|
||||
<div>
|
||||
<ul class="nav nav-topbar d-flex justify-content-center border-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->is($user->username) ? 'active': ''}} font-weight-bold text-uppercase" href="{{$user->url()}}">Posts</a>
|
||||
</li>
|
||||
{{-- <li class="nav-item">
|
||||
<a class="nav-link {{request()->is('*/collections') ? 'active': ''}} font-weight-bold text-uppercase" href="{{$user->url()}}/collections">Collections</a>
|
||||
</li> --}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->is('*/saved') ? 'active':''}} font-weight-bold text-uppercase" href="{{$user->url('/saved')}}">Saved</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
<div class="container">
|
||||
@if($owner && request()->is('*/saved'))
|
||||
<div class="col-12">
|
||||
<p class="text-muted font-weight-bold small">{{__('profile.savedWarning')}}</p>
|
||||
</div>
|
||||
@endif
|
||||
<div class="profile-timeline mt-2 mt-md-4">
|
||||
<div class="row">
|
||||
@if($timeline->count() > 0)
|
||||
@foreach($timeline as $status)
|
||||
<div class="col-4 p-0 p-sm-2 p-md-3">
|
||||
<a class="card info-overlay card-md-border-0" href="{{$status->url()}}">
|
||||
<div class="square {{$status->firstMedia()->filter_class}}">
|
||||
@switch($status->viewType())
|
||||
@case('album')
|
||||
@case('photo:album')
|
||||
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6;text-shadow: 3px 3px 16px #272634;"><i class="fas fa-images fa-2x"></i></span>
|
||||
@break
|
||||
@case('video')
|
||||
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6;text-shadow: 3px 3px 16px #272634;"><i class="fas fa-video fa-2x"></i></span>
|
||||
@break
|
||||
@case('video-album')
|
||||
<span class="float-right mr-3" style="color:#fff;position:relative;margin-top:10px;z-index: 999999;opacity:0.6;text-shadow: 3px 3px 16px #272634;"><i class="fas fa-film fa-2x"></i></span>
|
||||
@break
|
||||
@endswitch
|
||||
<div class="square-content" style="background-image: url('{{$status->thumb()}}')">
|
||||
</div>
|
||||
<div class="info-overlay-text">
|
||||
<h5 class="text-white m-auto font-weight-bold">
|
||||
<span>
|
||||
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
|
||||
<span class="d-flex-inline">{{App\Util\Lexer\PrettyNumber::convert($status->likes_count)}}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
|
||||
<span class="d-flex-inline">{{App\Util\Lexer\PrettyNumber::convert($status->comments_count)}}</span>
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination-container">
|
||||
<div class="d-flex justify-content-center">
|
||||
{{$timeline->links()}}
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body py-5 my-5">
|
||||
<div class="d-flex my-5 py-5 justify-content-center align-items-center">
|
||||
@if($owner && request()->is('*/saved'))
|
||||
<p class="lead font-weight-bold">{{ __('profile.emptySaved') }}</p>
|
||||
@else
|
||||
<p class="lead font-weight-bold">{{ __('profile.emptyTimeline') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('meta')<meta property="og:description" content="{{$user->bio}}">
|
||||
<meta property="og:image" content="{{$user->avatarUrl()}}">
|
||||
<link href="{{$user->permalink('.atom')}}" rel="alternate" title="{{$user->username}} on Pixelfed" type="application/atom+xml">
|
||||
@if(false == $settings->crawlable || $user->remote_url)
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$('.pagination-container').hide();
|
||||
$('.pagination').hide();
|
||||
let elem = document.querySelector('.profile-timeline');
|
||||
let infScroll = new InfiniteScroll( elem, {
|
||||
path: '.pagination__next',
|
||||
append: '.profile-timeline .row',
|
||||
status: '.page-load-status',
|
||||
history: false,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@endpush
|
||||
|
|
@ -10,28 +10,20 @@
|
|||
<div class="profile-details">
|
||||
<div class="username-bar pb-2 d-flex align-items-center">
|
||||
<span class="font-weight-ultralight h3">{{$user->username}}</span>
|
||||
@if(Auth::check() && $is_following == true)
|
||||
@auth
|
||||
@if($is_following == true)
|
||||
<span class="pl-4">
|
||||
<form class="follow-form" method="post" action="/i/follow" style="display: inline;" data-id="{{$user->id}}" data-action="unfollow">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$user->id}}">
|
||||
<button class="btn btn-outline-secondary font-weight-bold px-4 py-0" type="submit">Unfollow</button>
|
||||
</form>
|
||||
<button class="btn btn-outline-secondary font-weight-bold px-4 py-0" type="button" onclick="unfollowProfile()">Unfollow</button>
|
||||
</span>
|
||||
@elseif(Auth::check() && $requested == true)
|
||||
@elseif($requested == true)
|
||||
<span class="pl-4">
|
||||
<button class="btn btn-outline-secondary font-weight-bold px-4 py-0 disabled" disabled type="button">Follow Requested</button>
|
||||
<button class="btn btn-outline-secondary font-weight-bold px-4 py-0" type="button" onclick="unfollowProfile()">Follow Requested</button>
|
||||
</span>
|
||||
@elseif(Auth::check() && $is_following == false)
|
||||
@elseif($is_following == false)
|
||||
<span class="pl-4">
|
||||
<form class="follow-form" method="post" action="/i/follow" style="display: inline;" data-id="{{$user->id}}" data-action="follow">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$user->id}}">
|
||||
<button class="btn btn-primary font-weight-bold px-4 py-0" type="submit">Follow</button>
|
||||
</form>
|
||||
<button class="btn btn-primary font-weight-bold px-4 py-0" type="button" onclick="followProfile()">Follow</button>
|
||||
</span>
|
||||
@endif
|
||||
@auth
|
||||
<span class="pl-4">
|
||||
<i class="fas fa-cog fa-lg text-muted cursor-pointer" data-toggle="modal" data-target="#ctxProfileMenu"></i>
|
||||
<div class="modal" tabindex="-1" role="dialog" id="ctxProfileMenu">
|
||||
|
@ -81,6 +73,7 @@
|
|||
swal('Muted Profile', 'You have successfully muted this profile.', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
function blockProfile() {
|
||||
axios.post('/i/block', {
|
||||
type: 'user',
|
||||
|
@ -92,6 +85,20 @@
|
|||
});
|
||||
}
|
||||
|
||||
function followProfile() {
|
||||
axios.post('/api/v1/accounts/{{$user->id}}/follow')
|
||||
.then(res => {
|
||||
location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
function unfollowProfile() {
|
||||
axios.post('/api/v1/accounts/{{$user->id}}/unfollow')
|
||||
.then(res => {
|
||||
location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
@endauth
|
||||
@endpush
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
<div class="bg-white py-5 border-bottom">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 d-flex">
|
||||
<div class="profile-avatar mx-auto">
|
||||
<img class="rounded-circle box-shadow" src="{{$user->avatarUrl()}}" width="172px" height="172px">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 d-flex align-items-center">
|
||||
<div class="profile-details">
|
||||
<div class="username-bar pb-2 d-flex align-items-center">
|
||||
<span class="font-weight-ultralight h1">{{$user->username}}</span>
|
||||
@if($is_admin == true)
|
||||
<span class="pl-4">
|
||||
<span class="btn btn-outline-danger font-weight-bold py-0">ADMIN</span>
|
||||
</span>
|
||||
@endif
|
||||
@if($owner == true)
|
||||
<span class="pl-4">
|
||||
<a class="fas fa-cog fa-lg text-muted" href="{{route('settings')}}"></a>
|
||||
</span>
|
||||
@elseif (Auth::check() && $is_following == true)
|
||||
<span class="pl-4">
|
||||
<form class="follow-form" method="post" action="/i/follow" style="display: inline;" data-id="{{$user->id}}" data-action="unfollow">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$user->id}}">
|
||||
<button class="btn btn-outline-secondary font-weight-bold px-4 py-0" type="submit">Unfollow</button>
|
||||
</form>
|
||||
</span>
|
||||
@elseif (Auth::check() && $is_following == false)
|
||||
<span class="pl-4">
|
||||
<form class="follow-form" method="post" action="/i/follow" style="display: inline;" data-id="{{$user->id}}" data-action="follow">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$user->id}}">
|
||||
<button class="btn btn-primary font-weight-bold px-4 py-0" type="submit">Follow</button>
|
||||
</form>
|
||||
</span>
|
||||
@endif
|
||||
{{-- <span class="pl-4">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-muted dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="text-decoration: none;">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item font-weight-bold" href="#">Report User</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item font-weight-bold" href="#">Mute User</a>
|
||||
<a class="dropdown-item font-weight-bold" href="#">Block User</a>
|
||||
<a class="dropdown-item font-weight-bold mute-users" href="#">Mute User & User Followers</a>
|
||||
<a class="dropdown-item font-weight-bold" href="#">Block User & User Followers</a>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
--}}
|
||||
</div>
|
||||
<div class="profile-stats pb-3 d-inline-flex lead">
|
||||
<div class="font-weight-light pr-5">
|
||||
<a class="text-dark" href="{{$user->url()}}">
|
||||
<span class="font-weight-bold">{{$user->statusCount()}}</span>
|
||||
Posts
|
||||
</a>
|
||||
</div>
|
||||
@if($settings->show_profile_follower_count)
|
||||
<div class="font-weight-light pr-5">
|
||||
<a class="text-dark" href="{{$user->url('/followers')}}">
|
||||
<span class="font-weight-bold">{{$user->followerCount(true)}}</span>
|
||||
Followers
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@if($settings->show_profile_following_count)
|
||||
<div class="font-weight-light pr-5">
|
||||
<a class="text-dark" href="{{$user->url('/following')}}">
|
||||
<span class="font-weight-bold">{{$user->followingCount(true)}}</span>
|
||||
Following
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<p class="lead mb-0 d-flex align-items-center">
|
||||
<span class="font-weight-bold pr-3">{{$user->name}}</span>
|
||||
@if($user->remote_url)
|
||||
<span class="btn btn-outline-secondary btn-sm py-0">REMOTE PROFILE</span>
|
||||
@endif
|
||||
</p>
|
||||
<div class="mb-0 lead" v-pre>{!!str_limit($user->bio, 127)!!}</div>
|
||||
<p class="mb-0"><a href="{{$user->website}}" class="font-weight-bold" rel="me external nofollow noopener" target="_blank">{{str_limit($user->website, 30)}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,16 +0,0 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<remote-profile profile-id="{{$profile->id}}"></remote-profile>
|
||||
@endsection
|
||||
|
||||
@push('meta')
|
||||
<meta name="robots" content="noindex, noimageindex, nofollow, nosnippet, noarchive">
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{mix('js/rempro.js')}}"></script>
|
||||
<script type="text/javascript">
|
||||
App.boot();
|
||||
</script>
|
||||
@endpush
|
|
@ -131,14 +131,20 @@
|
|||
break;
|
||||
|
||||
case 'unfollow':
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
axios.post('/api/v1/accounts/' + id + '/unfollow')
|
||||
.then(res => {
|
||||
swal(
|
||||
'Unfollow Successful',
|
||||
'You have successfully unfollowed that user',
|
||||
'success'
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
swal(
|
||||
'Error',
|
||||
'An error occured when attempting to unfollow this user',
|
||||
'error'
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
|
|
|
@ -11,25 +11,35 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mt-n5 mb-4">
|
||||
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" src="{{$profile->avatarUrl()}}" width="90px" height="90px;">
|
||||
<img
|
||||
class="rounded-circle p-1 border mt-n4 bg-white shadow"
|
||||
src="{{$profile->avatarUrl()}}"
|
||||
width="90"
|
||||
height="90"
|
||||
loading="lazy"
|
||||
onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
|
||||
</div>
|
||||
<p class="text-center lead font-weight-bold mb-1">{{$profile->username}}</p>
|
||||
<p class="text-center text-muted small text-uppercase mb-4">{{$profile->followerCount()}} followers</p>
|
||||
<div class="d-flex justify-content-center">
|
||||
@if($following == true)
|
||||
<form class="d-inline-block" action="/i/follow" method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{(string)$profile->id}}">
|
||||
<input type="hidden" name="force" value="0">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Unfollow</button>
|
||||
</form>
|
||||
<button
|
||||
id="unfollow"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3"
|
||||
style="font-weight: 500"
|
||||
onclick="unfollowProfile()">
|
||||
Unfollow
|
||||
</button>
|
||||
@else
|
||||
<form class="d-inline-block" action="/i/follow" method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{(string)$profile->id}}">
|
||||
<input type="hidden" name="force" value="0">
|
||||
<button type="submit" class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Follow</button>
|
||||
</form>
|
||||
<button
|
||||
id="follow"
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3"
|
||||
style="font-weight: 500"
|
||||
onclick="followProfile()">
|
||||
Follow
|
||||
</button>
|
||||
@endif
|
||||
<a class="btn btn-outline-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" href="{{$profile->url()}}" style="font-weight: 500">View Profile</a>
|
||||
</div>
|
||||
|
@ -51,3 +61,32 @@
|
|||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
function followProfile() {
|
||||
let btn = document.querySelector('#follow');
|
||||
btn.setAttribute('disabled', 'disabled');
|
||||
axios.post('/api/v1/accounts/{{$profile->id}}/follow')
|
||||
.then(res => {
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
})
|
||||
.catch(err => {
|
||||
location.href = '/login?next=' + encodeURI(location.href);
|
||||
})
|
||||
}
|
||||
|
||||
function unfollowProfile() {
|
||||
let btn = document.querySelector('#unfollow');
|
||||
btn.setAttribute('disabled', 'disabled');
|
||||
axios.post('/api/v1/accounts/{{$profile->id}}/unfollow')
|
||||
.then(res => {
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
})
|
||||
.catch(err => {
|
||||
location.href = '/login?next=' + encodeURI(location.href);
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="mt-md-4"></div>
|
||||
<remote-post status-template="{{$status->viewType()}}" status-id="{{$status->id}}" status-username="{{$status->profile->username}}" status-url="{{$status->url()}}" status-profile-url="{{$status->profile->url()}}" status-avatar="{{$status->profile->avatarUrl()}}" status-profile-id="{{$status->profile_id}}" profile-layout="metro"></remote-post>
|
||||
|
||||
|
||||
@endsection
|
||||
|
||||
@push('meta')
|
||||
<meta name="robots" content="noindex, noimageindex, nofollow, nosnippet, noarchive">
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/rempos.js') }}"></script>
|
||||
<script type="text/javascript">App.boot()</script>
|
||||
@endpush
|
|
@ -189,8 +189,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
|
||||
Route::get('profile/{username}/status/{postid}/state', 'PublicApiController@statusState');
|
||||
Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
|
||||
Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
|
||||
Route::get('shares/profile/{username}/status/{id}', 'PublicApiController@statusShares');
|
||||
Route::get('status/{id}/replies', 'InternalApiController@statusReplies');
|
||||
Route::post('moderator/action', 'InternalApiController@modAction');
|
||||
Route::get('discover/categories', 'InternalApiController@discoverCategories');
|
||||
|
@ -207,8 +205,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('accounts/relationships', 'Api\ApiV1Controller@accountRelationshipsById');
|
||||
Route::get('accounts/search', 'Api\ApiV1Controller@accountSearch');
|
||||
Route::get('accounts/{id}/statuses', 'PublicApiController@accountStatuses');
|
||||
Route::get('accounts/{id}/following', 'PublicApiController@accountFollowing');
|
||||
Route::get('accounts/{id}/followers', 'PublicApiController@accountFollowers');
|
||||
Route::post('accounts/{id}/block', 'Api\ApiV1Controller@accountBlockById');
|
||||
Route::post('accounts/{id}/unblock', 'Api\ApiV1Controller@accountUnblockById');
|
||||
Route::get('statuses/{id}', 'PublicApiController@getStatus');
|
||||
|
@ -236,8 +232,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('discover/profiles', 'DiscoverController@profilesDirectoryApi');
|
||||
Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
|
||||
Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
|
||||
Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
|
||||
Route::get('shares/profile/{username}/status/{id}', 'PublicApiController@statusShares');
|
||||
Route::post('moderator/action', 'InternalApiController@modAction');
|
||||
Route::get('discover/categories', 'InternalApiController@discoverCategories');
|
||||
Route::get('loops', 'DiscoverController@loopsApi');
|
||||
|
|
Loading…
Reference in a new issue