mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-19 21:11:26 +00:00
commit
2c5281522a
50 changed files with 1018 additions and 64 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -94,6 +94,16 @@
|
||||||
- Update reply view, fix visibility filtering ([d419af4b](https://github.com/pixelfed/pixelfed/commit/d419af4b))
|
- Update reply view, fix visibility filtering ([d419af4b](https://github.com/pixelfed/pixelfed/commit/d419af4b))
|
||||||
- Update AP helpers, ingest attachments in replies ([c504e643](https://github.com/pixelfed/pixelfed/commit/c504e643))
|
- Update AP helpers, ingest attachments in replies ([c504e643](https://github.com/pixelfed/pixelfed/commit/c504e643))
|
||||||
- Update Media model, use cloud filesystem url if enabled instead of cdn_url to easily update S3 media urls ([e6bc57d7](https://github.com/pixelfed/pixelfed/commit/e6bc57d7))
|
- Update Media model, use cloud filesystem url if enabled instead of cdn_url to easily update S3 media urls ([e6bc57d7](https://github.com/pixelfed/pixelfed/commit/e6bc57d7))
|
||||||
|
- Update ap helpers, fix unset media name bug ([083f506b](https://github.com/pixelfed/pixelfed/commit/083f506b))
|
||||||
|
- Update MediaStorageService, fix improper path ([964c62da](https://github.com/pixelfed/pixelfed/commit/964c62da))
|
||||||
|
- Update ApiV1Controller, fix account statuses and bookmark pagination ([9f66d6b6](https://github.com/pixelfed/pixelfed/commit/9f66d6b6))
|
||||||
|
- Update SearchApiV2Service, improve account search results ([f6a588f9](https://github.com/pixelfed/pixelfed/commit/f6a588f9))
|
||||||
|
- Update profile model, improve avatarUrl fallback ([620ee826](https://github.com/pixelfed/pixelfed/commit/620ee826))
|
||||||
|
- Update ApiV1Controller, use cursor pagination for favourited_by and reblogged_by endpoints ([e1c7e701](https://github.com/pixelfed/pixelfed/commit/e1c7e701))
|
||||||
|
- Update ApiV1Controller, fix favourited_by and reblogged_by follows attribute ([1a130f3e](https://github.com/pixelfed/pixelfed/commit/1a130f3e))
|
||||||
|
- Update notifications component, improve UX with exponential retry and loading state ([937e6d07](https://github.com/pixelfed/pixelfed/commit/937e6d07))
|
||||||
|
- Update likeModal and shareModal components, use new pagination logic and re-add Follow/Unfollow buttons ([b565ead6](https://github.com/pixelfed/pixelfed/commit/b565ead6))
|
||||||
|
- Update profileFeed component, fix pagination ([7cf41628](https://github.com/pixelfed/pixelfed/commit/7cf41628))
|
||||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||||
|
|
||||||
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)
|
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)
|
||||||
|
|
|
@ -2471,15 +2471,21 @@ class ApiV1Controller extends Controller
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'page' => 'nullable|integer|min:1|max:40',
|
|
||||||
'limit' => 'nullable|integer|min:1|max:100'
|
'limit' => 'nullable|integer|min:1|max:100'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$limit = $request->input('limit') ?? 40;
|
$limit = $request->input('limit') ?? 10;
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$status = Status::findOrFail($id);
|
$status = Status::findOrFail($id);
|
||||||
|
$author = intval($status->profile_id) === intval($user->profile_id) || $user->is_admin;
|
||||||
|
|
||||||
if(intval($status->profile_id) !== intval($user->profile_id)) {
|
abort_if(
|
||||||
|
!$status->type ||
|
||||||
|
!in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!$author) {
|
||||||
if($status->scope == 'private') {
|
if($status->scope == 'private') {
|
||||||
abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403);
|
abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2487,35 +2493,46 @@ class ApiV1Controller extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$page = $request->input('page', 1);
|
$res = Status::where('reblog_of_id', $status->id)
|
||||||
$start = $page == 1 ? 0 : (($page * $limit) - $limit);
|
->orderByDesc('id')
|
||||||
$end = $start + $limit - 1;
|
->cursorPaginate($limit)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
$ids = ReblogService::getPostReblogs($id, $start, $end);
|
if(!$res) {
|
||||||
if(empty($ids)) {
|
return $this->json([]);
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = collect($ids)
|
$headers = [];
|
||||||
->map(function($id) {
|
if($author && $res->hasPages()) {
|
||||||
$status = StatusService::get($id);
|
$links = '';
|
||||||
if($status) {
|
if($res->previousPageUrl()) {
|
||||||
return AccountService::get($status['account']['id']);
|
$links = '<' . $res->previousPageUrl() .'>; rel="prev"';
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
|
if($res->nextPageUrl()) {
|
||||||
|
if(!empty($links)) {
|
||||||
|
$links .= ', ';
|
||||||
|
}
|
||||||
|
$links .= '<' . $res->nextPageUrl() .'>; rel="next"';
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = ['Link' => $links];
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = $res->map(function($status) use($user) {
|
||||||
|
$account = 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);
|
||||||
|
return $account;
|
||||||
})
|
})
|
||||||
->filter(function($account) {
|
->filter(function($account) {
|
||||||
return $account && isset($account['id']);
|
return $account && isset($account['id']);
|
||||||
})
|
})
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
$url = $request->url();
|
return $this->json($res, 200, $headers);
|
||||||
$page = $request->input('page', 1);
|
|
||||||
$next = $page < 40 ? $page + 1 : 40;
|
|
||||||
$prev = $page > 1 ? $page - 1 : 1;
|
|
||||||
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
|
|
||||||
|
|
||||||
return $this->json($res, 200, ['Link' => $links]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2530,44 +2547,64 @@ class ApiV1Controller extends Controller
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'page' => 'nullable|integer|min:1|max:40',
|
|
||||||
'limit' => 'nullable|integer|min:1|max:100'
|
'limit' => 'nullable|integer|min:1|max:100'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$page = $request->input('page', 1);
|
$limit = $request->input('limit') ?? 10;
|
||||||
$limit = $request->input('limit') ?? 40;
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$status = Status::findOrFail($id);
|
$status = Status::findOrFail($id);
|
||||||
$offset = $page == 1 ? 0 : ($page * $limit - $limit);
|
$author = intval($status->profile_id) === intval($user->profile_id) || $user->is_admin;
|
||||||
if($offset > 100) {
|
|
||||||
if($user->profile_id != $status->profile_id) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(intval($status->profile_id) !== intval($user->profile_id)) {
|
abort_if(
|
||||||
|
!$status->type ||
|
||||||
|
!in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!$author) {
|
||||||
if($status->scope == 'private') {
|
if($status->scope == 'private') {
|
||||||
abort_if(!$status->profile->followedBy($user->profile), 403);
|
abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403);
|
||||||
} else {
|
} else {
|
||||||
abort_if(!in_array($status->scope, ['public','unlisted']), 403);
|
abort_if(!in_array($status->scope, ['public','unlisted']), 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($request->has('cursor')) {
|
||||||
|
return $this->json([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = DB::table('likes')
|
$res = Like::where('status_id', $status->id)
|
||||||
->select('likes.id', 'likes.profile_id', 'likes.status_id', 'followers.created_at')
|
->orderByDesc('id')
|
||||||
->leftJoin('followers', function($join) use($user, $status) {
|
->cursorPaginate($limit)
|
||||||
return $join->on('likes.profile_id', '=', 'followers.following_id')
|
->withQueryString();
|
||||||
->where('followers.profile_id', $user->profile_id)
|
|
||||||
->where('likes.status_id', $status->id);
|
if(!$res) {
|
||||||
})
|
return $this->json([]);
|
||||||
->whereStatusId($status->id)
|
}
|
||||||
->orderByDesc('followers.created_at')
|
|
||||||
->offset($offset)
|
$headers = [];
|
||||||
->limit($limit)
|
if($author && $res->hasPages()) {
|
||||||
->get()
|
$links = '';
|
||||||
->map(function($like) {
|
if($res->previousPageUrl()) {
|
||||||
|
$links = '<' . $res->previousPageUrl() .'>; rel="prev"';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($res->nextPageUrl()) {
|
||||||
|
if(!empty($links)) {
|
||||||
|
$links .= ', ';
|
||||||
|
}
|
||||||
|
$links .= '<' . $res->nextPageUrl() .'>; rel="next"';
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = ['Link' => $links];
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = $res->map(function($like) use($user) {
|
||||||
$account = AccountService::getMastodon($like->profile_id, true);
|
$account = AccountService::getMastodon($like->profile_id, true);
|
||||||
$account['follows'] = isset($like->created_at);
|
if(!$account) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$account['follows'] = $like->profile_id == $user->profile_id ? null : FollowerService::follows($user->profile_id, $like->profile_id);
|
||||||
return $account;
|
return $account;
|
||||||
})
|
})
|
||||||
->filter(function($account) use($user) {
|
->filter(function($account) use($user) {
|
||||||
|
@ -2575,13 +2612,7 @@ class ApiV1Controller extends Controller
|
||||||
})
|
})
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
$url = $request->url();
|
return $this->json($res, 200, $headers);
|
||||||
$page = $request->input('page', 1);
|
|
||||||
$next = $page < 40 ? $page + 1 : 40;
|
|
||||||
$prev = $page > 1 ? $page - 1 : 1;
|
|
||||||
$links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
|
|
||||||
|
|
||||||
return $this->json($res, 200, ['Link' => $links]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -160,6 +160,10 @@ class Profile extends Model
|
||||||
$url = Cache::remember('avatar:'.$this->id, 1209600, function () {
|
$url = Cache::remember('avatar:'.$this->id, 1209600, function () {
|
||||||
$avatar = $this->avatar;
|
$avatar = $this->avatar;
|
||||||
|
|
||||||
|
if(!$avatar) {
|
||||||
|
return url('/storage/avatars/default.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
if($avatar->cdn_url) {
|
if($avatar->cdn_url) {
|
||||||
if(substr($avatar->cdn_url, 0, 8) === 'https://') {
|
if(substr($avatar->cdn_url, 0, 8) === 'https://') {
|
||||||
return $avatar->cdn_url;
|
return $avatar->cdn_url;
|
||||||
|
@ -170,6 +174,10 @@ class Profile extends Model
|
||||||
|
|
||||||
$path = $avatar->media_path;
|
$path = $avatar->media_path;
|
||||||
|
|
||||||
|
if(!$path) {
|
||||||
|
return url('/storage/avatars/default.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
if(substr($path, 0, 6) !== 'public') {
|
if(substr($path, 0, 6) !== 'public') {
|
||||||
return url('/storage/avatars/default.jpg');
|
return url('/storage/avatars/default.jpg');
|
||||||
}
|
}
|
||||||
|
|
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fancyapps/fancybox": "^3.5.7",
|
"@fancyapps/fancybox": "^3.5.7",
|
||||||
"@trevoreyre/autocomplete-vue": "^2.2.0",
|
"@trevoreyre/autocomplete-vue": "^2.2.0",
|
||||||
|
"@web3-storage/parse-link-header": "^3.1.0",
|
||||||
"animate.css": "^4.1.0",
|
"animate.css": "^4.1.0",
|
||||||
"bigpicture": "^2.6.2",
|
"bigpicture": "^2.6.2",
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^1.1.3",
|
||||||
|
@ -2171,6 +2172,11 @@
|
||||||
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
|
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@web3-storage/parse-link-header": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@web3-storage/parse-link-header/-/parse-link-header-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-K1undnK70vLLauqdE8bq/l98isTF2FDhcP0UPpXVSjkSWe3xhAn5eRXk5jfA1E5ycNm84Ws/rQFUD7ue11nciw=="
|
||||||
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||||
|
@ -10970,6 +10976,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@web3-storage/parse-link-header": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@web3-storage/parse-link-header/-/parse-link-header-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-K1undnK70vLLauqdE8bq/l98isTF2FDhcP0UPpXVSjkSWe3xhAn5eRXk5jfA1E5ycNm84Ws/rQFUD7ue11nciw=="
|
||||||
|
},
|
||||||
"@webassemblyjs/ast": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fancyapps/fancybox": "^3.5.7",
|
"@fancyapps/fancybox": "^3.5.7",
|
||||||
"@trevoreyre/autocomplete-vue": "^2.2.0",
|
"@trevoreyre/autocomplete-vue": "^2.2.0",
|
||||||
|
"@web3-storage/parse-link-header": "^3.1.0",
|
||||||
"animate.css": "^4.1.0",
|
"animate.css": "^4.1.0",
|
||||||
"bigpicture": "^2.6.2",
|
"bigpicture": "^2.6.2",
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^1.1.3",
|
||||||
|
|
BIN
public/js/admin.js
vendored
BIN
public/js/admin.js
vendored
Binary file not shown.
BIN
public/js/app.js
vendored
BIN
public/js/app.js
vendored
Binary file not shown.
BIN
public/js/components.js
vendored
BIN
public/js/components.js
vendored
Binary file not shown.
BIN
public/js/discover.chunk.3ef6d6fe45dbe91b.js
vendored
BIN
public/js/discover.chunk.3ef6d6fe45dbe91b.js
vendored
Binary file not shown.
BIN
public/js/discover.chunk.b33cd1cc42853828.js
vendored
Normal file
BIN
public/js/discover.chunk.b33cd1cc42853828.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~hashtag.bundle.7c5f7f5c21a1d88c.js
vendored
Normal file
BIN
public/js/discover~hashtag.bundle.7c5f7f5c21a1d88c.js
vendored
Normal file
Binary file not shown.
BIN
public/js/discover~myhashtags.chunk.075cc9fe49783f65.js
vendored
Normal file
BIN
public/js/discover~myhashtags.chunk.075cc9fe49783f65.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/dms.chunk.37131c41fc288259.js
vendored
Normal file
BIN
public/js/dms.chunk.37131c41fc288259.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dms.chunk.ffe36114c17441be.js
vendored
BIN
public/js/dms.chunk.ffe36114c17441be.js
vendored
Binary file not shown.
BIN
public/js/home.chunk.294faaa69171455b.js
vendored
Normal file
BIN
public/js/home.chunk.294faaa69171455b.js
vendored
Normal file
Binary file not shown.
BIN
public/js/home.chunk.c5608f771be873ca.js
vendored
BIN
public/js/home.chunk.c5608f771be873ca.js
vendored
Binary file not shown.
BIN
public/js/installer.js
vendored
BIN
public/js/installer.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/post.chunk.02abe63d47f8d51e.js
vendored
BIN
public/js/post.chunk.02abe63d47f8d51e.js
vendored
Binary file not shown.
BIN
public/js/post.chunk.dffb139831cf2ae9.js
vendored
Normal file
BIN
public/js/post.chunk.dffb139831cf2ae9.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.chunk.99838eb369862e91.js
vendored
Normal file
BIN
public/js/profile.chunk.99838eb369862e91.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.chunk.cdad3298b78ff083.js
vendored
BIN
public/js/profile.chunk.cdad3298b78ff083.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/spa.js
vendored
BIN
public/js/spa.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.
239
resources/assets/components/partials/post/LikeModal.vue
Normal file
239
resources/assets/components/partials/post/LikeModal.vue
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-modal
|
||||||
|
ref="likesModal"
|
||||||
|
centered
|
||||||
|
size="md"
|
||||||
|
:scrollable="true"
|
||||||
|
hide-footer
|
||||||
|
header-class="py-2"
|
||||||
|
body-class="p-0"
|
||||||
|
title-class="w-100 text-center pl-4 font-weight-bold"
|
||||||
|
title-tag="p"
|
||||||
|
:title="$t('common.likes')">
|
||||||
|
<div v-if="isLoading" class="likes-loader list-group border-top-0" style="max-height: 500px;">
|
||||||
|
<like-placeholder />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="!likes.length" class="d-flex justify-content-center align-items-center" style="height: 140px;">
|
||||||
|
<p class="font-weight-bold mb-0">{{ $t('post.noLikes') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="list-group" style="max-height: 500px;">
|
||||||
|
<div v-for="(account, index) in likes" class="list-group-item border-left-0 border-right-0 px-3" :class="[ index === 0 ? 'border-top-0' : '']">
|
||||||
|
<div class="media align-items-center">
|
||||||
|
<img :src="account.avatar" width="40" height="40" style="border-radius: 8px;" class="mr-3 shadow-sm" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
|
||||||
|
<div class="media-body">
|
||||||
|
<p class="mb-0 text-truncate"><a :href="account.url" class="text-dark font-weight-bold text-decoration-none" @click.prevent="goToProfile(account)">{{ getUsername(account) }}</a></p>
|
||||||
|
<p class="mb-0 mt-n1 text-dark font-weight-bold small text-break">@{{ account.acct }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="account.follows == null || account.id == user.id"
|
||||||
|
class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
|
||||||
|
@click="goToProfile(profile)"
|
||||||
|
style="width:110px;">
|
||||||
|
View Profile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="account.follows"
|
||||||
|
class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
|
||||||
|
:disabled="isUpdatingFollowState"
|
||||||
|
@click="handleUnfollow(index)"
|
||||||
|
style="width:110px;">
|
||||||
|
<span v-if="isUpdatingFollowState && followStateIndex === index">
|
||||||
|
<b-spinner small />
|
||||||
|
</span>
|
||||||
|
<span v-else>Following</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="!account.follows"
|
||||||
|
class="btn btn-primary rounded-pill btn-sm font-weight-bold"
|
||||||
|
:disabled="isUpdatingFollowState"
|
||||||
|
@click="handleFollow(index)"
|
||||||
|
style="width:110px;">
|
||||||
|
<span v-if="isUpdatingFollowState && followStateIndex === index">
|
||||||
|
<b-spinner small />
|
||||||
|
</span>
|
||||||
|
<span v-else>Follow</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canLoadMore">
|
||||||
|
<intersect @enter="enterIntersect">
|
||||||
|
<like-placeholder class="border-top-0" />
|
||||||
|
</intersect>
|
||||||
|
<like-placeholder />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
import Intersect from 'vue-intersect'
|
||||||
|
import LikePlaceholder from './LikeListPlaceholder.vue';
|
||||||
|
import { parseLinkHeader } from '@web3-storage/parse-link-header';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
|
||||||
|
profile: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
"intersect": Intersect,
|
||||||
|
"like-placeholder": LikePlaceholder
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isOpen: false,
|
||||||
|
isLoading: true,
|
||||||
|
canLoadMore: false,
|
||||||
|
isFetchingMore: false,
|
||||||
|
likes: [],
|
||||||
|
ids: [],
|
||||||
|
page: undefined,
|
||||||
|
isUpdatingFollowState: false,
|
||||||
|
followStateIndex: undefined,
|
||||||
|
user: window._sharedData.user
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
clear() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.isLoading = true;
|
||||||
|
this.canLoadMore = false;
|
||||||
|
this.isFetchingMore = false;
|
||||||
|
this.likes = [];
|
||||||
|
this.ids = [];
|
||||||
|
this.page = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchLikes() {
|
||||||
|
axios.get('/api/v1/statuses/'+this.status.id+'/favourited_by', {
|
||||||
|
params: {
|
||||||
|
limit: 40
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.ids = res.data.map(a => a.id);
|
||||||
|
this.likes = res.data;
|
||||||
|
if(res.headers && res.headers.link) {
|
||||||
|
const links = parseLinkHeader(res.headers.link);
|
||||||
|
if(links.next) {
|
||||||
|
this.page = links.next.cursor;
|
||||||
|
this.canLoadMore = true;
|
||||||
|
} else {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
open() {
|
||||||
|
if(this.page) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
this.isOpen = true;
|
||||||
|
this.fetchLikes();
|
||||||
|
this.$refs.likesModal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
enterIntersect() {
|
||||||
|
if(this.isFetchingMore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetchingMore = true;
|
||||||
|
|
||||||
|
axios.get('/api/v1/statuses/'+this.status.id+'/favourited_by', {
|
||||||
|
params: {
|
||||||
|
limit: 10,
|
||||||
|
cursor: this.page
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if(!res.data || !res.data.length) {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
this.isFetchingMore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.data.forEach(user => {
|
||||||
|
if(this.ids.indexOf(user.id) == -1) {
|
||||||
|
this.ids.push(user.id);
|
||||||
|
this.likes.push(user);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if(res.headers && res.headers.link) {
|
||||||
|
const links = parseLinkHeader(res.headers.link);
|
||||||
|
if(links.next) {
|
||||||
|
this.page = links.next.cursor;
|
||||||
|
} else {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isFetchingMore = false;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getUsername(account) {
|
||||||
|
return account.display_name ? account.display_name : account.username;
|
||||||
|
},
|
||||||
|
|
||||||
|
goToProfile(account) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'profile',
|
||||||
|
path: `/i/web/profile/${account.id}`,
|
||||||
|
params: {
|
||||||
|
id: account.id,
|
||||||
|
cachedProfile: account,
|
||||||
|
cachedUser: this.profile
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFollow(index) {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
|
||||||
|
this.followStateIndex = index;
|
||||||
|
this.isUpdatingFollowState = true;
|
||||||
|
|
||||||
|
let account = this.likes[index];
|
||||||
|
axios.post('/api/v1/accounts/' + account.id + '/follow')
|
||||||
|
.then(res => {
|
||||||
|
this.likes[index].follows = true;
|
||||||
|
this.followStateIndex = undefined;
|
||||||
|
this.isUpdatingFollowState = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUnfollow(index) {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
|
||||||
|
this.followStateIndex = index;
|
||||||
|
this.isUpdatingFollowState = true;
|
||||||
|
|
||||||
|
let account = this.likes[index];
|
||||||
|
axios.post('/api/v1/accounts/' + account.id + '/unfollow')
|
||||||
|
.then(res => {
|
||||||
|
this.likes[index].follows = false;
|
||||||
|
this.followStateIndex = undefined;
|
||||||
|
this.isUpdatingFollowState = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
239
resources/assets/components/partials/post/ShareModal.vue
Normal file
239
resources/assets/components/partials/post/ShareModal.vue
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-modal
|
||||||
|
ref="sharesModal"
|
||||||
|
centered
|
||||||
|
size="md"
|
||||||
|
:scrollable="true"
|
||||||
|
hide-footer
|
||||||
|
header-class="py-2"
|
||||||
|
body-class="p-0"
|
||||||
|
title-class="w-100 text-center pl-4 font-weight-bold"
|
||||||
|
title-tag="p"
|
||||||
|
title="Shared By">
|
||||||
|
<div v-if="isLoading" class="likes-loader list-group border-top-0" style="max-height: 500px;">
|
||||||
|
<like-placeholder />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="!likes.length" class="d-flex justify-content-center align-items-center" style="height: 140px;">
|
||||||
|
<p class="font-weight-bold mb-0">Nobody has shared this yet!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="list-group" style="max-height: 500px;">
|
||||||
|
<div v-for="(account, index) in likes" class="list-group-item border-left-0 border-right-0 px-3" :class="[ index === 0 ? 'border-top-0' : '']">
|
||||||
|
<div class="media align-items-center">
|
||||||
|
<img :src="account.avatar" width="40" height="40" style="border-radius: 8px;" class="mr-3 shadow-sm" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
|
||||||
|
<div class="media-body">
|
||||||
|
<p class="mb-0 text-truncate"><a :href="account.url" class="text-dark font-weight-bold text-decoration-none" @click.prevent="goToProfile(account)">{{ getUsername(account) }}</a></p>
|
||||||
|
<p class="mb-0 mt-n1 text-dark font-weight-bold small text-break">@{{ account.acct }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="account.id == user.id"
|
||||||
|
class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
|
||||||
|
@click="goToProfile(profile)"
|
||||||
|
style="width:110px;">
|
||||||
|
View Profile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="account.follows"
|
||||||
|
class="btn btn-outline-muted rounded-pill btn-sm font-weight-bold"
|
||||||
|
:disabled="isUpdatingFollowState"
|
||||||
|
@click="handleUnfollow(index)"
|
||||||
|
style="width:110px;">
|
||||||
|
<span v-if="isUpdatingFollowState && followStateIndex === index">
|
||||||
|
<b-spinner small />
|
||||||
|
</span>
|
||||||
|
<span v-else>Following</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="!account.follows"
|
||||||
|
class="btn btn-primary rounded-pill btn-sm font-weight-bold"
|
||||||
|
:disabled="isUpdatingFollowState"
|
||||||
|
@click="handleFollow(index)"
|
||||||
|
style="width:110px;">
|
||||||
|
<span v-if="isUpdatingFollowState && followStateIndex === index">
|
||||||
|
<b-spinner small />
|
||||||
|
</span>
|
||||||
|
<span v-else>Follow</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canLoadMore">
|
||||||
|
<intersect @enter="enterIntersect">
|
||||||
|
<like-placeholder class="border-top-0" />
|
||||||
|
</intersect>
|
||||||
|
<like-placeholder />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
import Intersect from 'vue-intersect'
|
||||||
|
import LikePlaceholder from './LikeListPlaceholder.vue';
|
||||||
|
import { parseLinkHeader } from '@web3-storage/parse-link-header';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
|
||||||
|
profile: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
"intersect": Intersect,
|
||||||
|
"like-placeholder": LikePlaceholder
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isOpen: false,
|
||||||
|
isLoading: true,
|
||||||
|
canLoadMore: false,
|
||||||
|
isFetchingMore: false,
|
||||||
|
likes: [],
|
||||||
|
ids: [],
|
||||||
|
page: undefined,
|
||||||
|
isUpdatingFollowState: false,
|
||||||
|
followStateIndex: undefined,
|
||||||
|
user: window._sharedData.user
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
clear() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.isLoading = true;
|
||||||
|
this.canLoadMore = false;
|
||||||
|
this.isFetchingMore = false;
|
||||||
|
this.likes = [];
|
||||||
|
this.ids = [];
|
||||||
|
this.page = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchShares() {
|
||||||
|
axios.get('/api/v1/statuses/'+this.status.id+'/reblogged_by', {
|
||||||
|
params: {
|
||||||
|
limit: 40
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.ids = res.data.map(a => a.id);
|
||||||
|
this.likes = res.data;
|
||||||
|
if(res.headers && res.headers.link) {
|
||||||
|
const links = parseLinkHeader(res.headers.link);
|
||||||
|
if(links.next) {
|
||||||
|
this.page = links.next.cursor;
|
||||||
|
this.canLoadMore = true;
|
||||||
|
} else {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
open() {
|
||||||
|
if(this.page) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
this.isOpen = true;
|
||||||
|
this.fetchShares();
|
||||||
|
this.$refs.sharesModal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
enterIntersect() {
|
||||||
|
if(this.isFetchingMore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetchingMore = true;
|
||||||
|
|
||||||
|
axios.get('/api/v1/statuses/'+this.status.id+'/reblogged_by', {
|
||||||
|
params: {
|
||||||
|
limit: 10,
|
||||||
|
cursor: this.page
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if(!res.data || !res.data.length) {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
this.isFetchingMore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.data.forEach(user => {
|
||||||
|
if(this.ids.indexOf(user.id) == -1) {
|
||||||
|
this.ids.push(user.id);
|
||||||
|
this.likes.push(user);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if(res.headers && res.headers.link) {
|
||||||
|
const links = parseLinkHeader(res.headers.link);
|
||||||
|
if(links.next) {
|
||||||
|
this.page = links.next.cursor;
|
||||||
|
} else {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isFetchingMore = false;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getUsername(account) {
|
||||||
|
return account.display_name ? account.display_name : account.username;
|
||||||
|
},
|
||||||
|
|
||||||
|
goToProfile(account) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'profile',
|
||||||
|
path: `/i/web/profile/${account.id}`,
|
||||||
|
params: {
|
||||||
|
id: account.id,
|
||||||
|
cachedProfile: account,
|
||||||
|
cachedUser: this.profile
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFollow(index) {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
|
||||||
|
this.followStateIndex = index;
|
||||||
|
this.isUpdatingFollowState = true;
|
||||||
|
|
||||||
|
let account = this.likes[index];
|
||||||
|
axios.post('/api/v1/accounts/' + account.id + '/follow')
|
||||||
|
.then(res => {
|
||||||
|
this.likes[index].follows = true;
|
||||||
|
this.followStateIndex = undefined;
|
||||||
|
this.isUpdatingFollowState = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUnfollow(index) {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
|
||||||
|
this.followStateIndex = index;
|
||||||
|
this.isUpdatingFollowState = true;
|
||||||
|
|
||||||
|
let account = this.likes[index];
|
||||||
|
axios.post('/api/v1/accounts/' + account.id + '/unfollow')
|
||||||
|
.then(res => {
|
||||||
|
this.likes[index].follows = false;
|
||||||
|
this.followStateIndex = undefined;
|
||||||
|
this.isUpdatingFollowState = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
415
resources/assets/components/sections/Notifications.vue
Normal file
415
resources/assets/components/sections/Notifications.vue
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
<template>
|
||||||
|
<div class="notifications-component">
|
||||||
|
<div class="card shadow-sm mb-3" style="overflow: hidden;border-radius: 15px !important;">
|
||||||
|
<div class="card-body pb-0">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-muted font-weight-bold">Notifications</span>
|
||||||
|
<div v-if="feed && feed.length">
|
||||||
|
<router-link to="/i/web/notifications" class="btn btn-outline-light btn-sm mr-2" style="color: #B8C2CC !important">
|
||||||
|
<i class="far fa-filter"></i>
|
||||||
|
</router-link>
|
||||||
|
<button
|
||||||
|
v-if="hasLoaded && feed.length"
|
||||||
|
class="btn btn-light btn-sm"
|
||||||
|
:class="{ 'text-lighter': isRefreshing }"
|
||||||
|
:disabled="isRefreshing"
|
||||||
|
@click="refreshNotifications">
|
||||||
|
<i class="fal fa-redo"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!hasLoaded" class="notifications-component-feed">
|
||||||
|
<div class="d-flex align-items-center justify-content-center flex-column bg-light rounded-lg p-3 mb-3" style="min-height: 100px;">
|
||||||
|
<b-spinner variant="grow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="notifications-component-feed">
|
||||||
|
<template v-if="isEmpty">
|
||||||
|
<div class="d-flex align-items-center justify-content-center flex-column bg-light rounded-lg p-3 mb-3" style="min-height: 100px;">
|
||||||
|
<i class="fal fa-bell fa-2x text-lighter"></i>
|
||||||
|
<p class="mt-2 small font-weight-bold text-center mb-0">{{ $t('notifications.noneFound') }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-for="(n, index) in feed" class="mb-2">
|
||||||
|
<div class="media align-items-center">
|
||||||
|
<img
|
||||||
|
class="mr-2 rounded-circle shadow-sm"
|
||||||
|
:src="n.account.avatar"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
|
||||||
|
|
||||||
|
<div class="media-body font-weight-light small">
|
||||||
|
<div v-if="n.type == 'favourite'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> liked your
|
||||||
|
<span v-if="n.status && n.status.hasOwnProperty('media_attachments')">
|
||||||
|
<a class="font-weight-bold" v-bind:href="getPostUrl(n.status)" :id="'fvn-' + n.id" @click.prevent="goToPost(n.status)">post</a>.
|
||||||
|
<b-popover :target="'fvn-' + n.id" title="" triggers="hover" placement="top" boundary="window">
|
||||||
|
<img :src="notificationPreview(n)" width="100px" height="100px" style="object-fit: cover;">
|
||||||
|
</b-popover>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">post</a>.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'comment'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">post</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'group:comment'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" :href="n.group_post_url">group post</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'story:react'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> reacted to your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'story:comment'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'mention'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)" @click.prevent="goToPost(n.status)">mentioned</a> you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'follow'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> followed you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'share'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" :href="getPostUrl(n.status)" @click.prevent="goToPost(n.status)">post</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'modlog'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{truncate(n.account.username)}}</a> updated a <a class="font-weight-bold" v-bind:href="n.modlog.url">modlog</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'tagged'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> tagged you in a <a class="font-weight-bold" v-bind:href="n.tagged.post_url">post</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="n.type == 'direct'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> sent a <router-link class="font-weight-bold" :to="'/i/web/direct/thread/'+n.account.id">dm</router-link>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="n.type == 'group.join.approved'">
|
||||||
|
<p class="my-0">
|
||||||
|
Your application to join the <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> group was approved!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="n.type == 'group.join.rejected'">
|
||||||
|
<p class="my-0">
|
||||||
|
Your application to join <a :href="n.group.url" class="font-weight-bold text-dark word-break" :title="n.group.name">{{truncate(n.group.name)}}</a> was rejected.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="n.type == 'group:invite'">
|
||||||
|
<p class="my-0">
|
||||||
|
<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.acct">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> invited you to join <a :href="n.group.url + '/invite/claim'" class="font-weight-bold text-dark word-break" :title="n.group.name">{{n.group.name}}</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<p class="my-0">
|
||||||
|
We cannot display this notification at this time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted font-weight-bold" :title="n.created_at">{{timeAgo(n.created_at)}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasLoaded && feed.length == 0">
|
||||||
|
<p class="small font-weight-bold text-center mb-0">{{ $t('notifications.noneFound') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<intersect v-if="hasLoaded && canLoadMore" @enter="enterIntersect">
|
||||||
|
<placeholder small style="margin-top: -6px" />
|
||||||
|
<placeholder small/>
|
||||||
|
<placeholder small/>
|
||||||
|
<placeholder small/>
|
||||||
|
</intersect>
|
||||||
|
|
||||||
|
<div v-else class="d-block" style="height: 10px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
import Placeholder from './../partials/placeholders/NotificationPlaceholder.vue';
|
||||||
|
import Intersect from 'vue-intersect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
profile: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
"intersect": Intersect,
|
||||||
|
"placeholder": Placeholder
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
feed: {},
|
||||||
|
maxId: undefined,
|
||||||
|
isIntersecting: false,
|
||||||
|
canLoadMore: false,
|
||||||
|
isRefreshing: false,
|
||||||
|
hasLoaded: false,
|
||||||
|
isEmpty: false,
|
||||||
|
retryTimeout: undefined,
|
||||||
|
retryAttempts: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
clearTimeout(this.retryTimeout);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
if(this.retryAttempts == 3) {
|
||||||
|
this.hasLoaded = true;
|
||||||
|
this.isEmpty = true;
|
||||||
|
clearTimeout(this.retryTimeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
axios.get('/api/pixelfed/v1/notifications', {
|
||||||
|
params: {
|
||||||
|
limit: 9,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if(!res || !res.data || !res.data.length) {
|
||||||
|
this.retryAttempts = this.retryAttempts + 1;
|
||||||
|
this.retryTimeout = setTimeout(() => this.init(), this.retryAttempts * 1500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = res.data.filter(n => {
|
||||||
|
if(n.type == 'share' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'comment' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'mention' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'favourite' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'follow' && !n.account) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'modlog' && !n.modlog) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!res.data.length) {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
} else {
|
||||||
|
this.canLoadMore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.retryTimeout || this.retryAttempts) {
|
||||||
|
this.retryAttempts = 0;
|
||||||
|
clearTimeout(this.retryTimeout);
|
||||||
|
}
|
||||||
|
this.maxId = res.data[res.data.length - 1].id;
|
||||||
|
this.feed = data;
|
||||||
|
|
||||||
|
this.hasLoaded = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isRefreshing = false;
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshNotifications() {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
this.isRefreshing = true;
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
|
||||||
|
enterIntersect() {
|
||||||
|
if(this.isIntersecting || !this.canLoadMore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isIntersecting = true;
|
||||||
|
|
||||||
|
axios.get('/api/pixelfed/v1/notifications', {
|
||||||
|
params: {
|
||||||
|
limit: 9,
|
||||||
|
max_id: this.maxId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if(!res.data || !res.data.length) {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
this.isIntersecting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = res.data.filter(n => {
|
||||||
|
if(n.type == 'share' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'comment' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'mention' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'favourite' && !n.status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'follow' && !n.account) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(n.type == 'modlog' && !n.modlog) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!res.data.length) {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maxId = res.data[res.data.length - 1].id;
|
||||||
|
this.feed.push(...data);
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isIntersecting = false;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
truncate(text) {
|
||||||
|
if(text.length <= 15) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.slice(0,15) + '...'
|
||||||
|
},
|
||||||
|
|
||||||
|
timeAgo(ts) {
|
||||||
|
return window.App.util.format.timeAgo(ts);
|
||||||
|
},
|
||||||
|
|
||||||
|
mentionUrl(status) {
|
||||||
|
let username = status.account.username;
|
||||||
|
let id = status.id;
|
||||||
|
return '/p/' + username + '/' + id;
|
||||||
|
},
|
||||||
|
|
||||||
|
redirect(url) {
|
||||||
|
window.location.href = url;
|
||||||
|
},
|
||||||
|
|
||||||
|
notificationPreview(n) {
|
||||||
|
if(!n.status || !n.status.hasOwnProperty('media_attachments') || !n.status.media_attachments.length) {
|
||||||
|
return '/storage/no-preview.png';
|
||||||
|
}
|
||||||
|
return n.status.media_attachments[0].preview_url;
|
||||||
|
},
|
||||||
|
|
||||||
|
getProfileUrl(account) {
|
||||||
|
return '/i/web/profile/' + account.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPostUrl(status) {
|
||||||
|
if(!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/i/web/post/' + status.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPost(status) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'post',
|
||||||
|
path: `/i/web/post/${status.id}`,
|
||||||
|
params: {
|
||||||
|
id: status.id,
|
||||||
|
cachedStatus: status,
|
||||||
|
cachedProfile: this.profile
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
goToProfile(account) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'profile',
|
||||||
|
path: `/i/web/profile/${account.id}`,
|
||||||
|
params: {
|
||||||
|
id: account.id,
|
||||||
|
cachedProfile: account,
|
||||||
|
cachedUser: this.profile
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.notifications-component {
|
||||||
|
&-feed {
|
||||||
|
min-height: 50px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue