From 29ddfe164efd69fb9ab1742cf97b483d04fa95b7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 20:52:32 -0700 Subject: [PATCH 01/17] Add new vue packages --- package-lock.json | 10 ++++++++++ package.json | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index a00e65ad7..a77f1ee82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11274,6 +11274,11 @@ "integrity": "sha512-2j/t+wIbyVMP5NvctQoSUvLkYKoWAAk2QlQiilrM2a6/ulzFgdcLUJfTvs4XQ/3eZhHiBmmEojbjmM4AzZj8JA==", "dev": true }, + "vue-infinite-loading": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/vue-infinite-loading/-/vue-infinite-loading-2.4.3.tgz", + "integrity": "sha512-CKITl7I1cb3X4zIHbVSyrupPTs9XxZGVV/N+P5lSxSrGW+D92gq6zuTy/XnvJOwMRkjJuiotJAQrgv+gOwSx3g==" + }, "vue-loader": { "version": "13.7.3", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-13.7.3.tgz", @@ -11336,6 +11341,11 @@ } } }, + "vue-loading-overlay": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-3.1.0.tgz", + "integrity": "sha512-EJOaqxfkSwt6LRoKYnWWPch6fLRRzHWFxLBnRHjXHIK/fP0MSmbBLh9ZRpxarXJeDBiyykQevDXa7h7809JaAA==" + }, "vue-style-loader": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-3.1.2.tgz", diff --git a/package.json b/package.json index 63da29fe1..cee9ecfa8 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "readmore-js": "^2.2.1", "socket.io-client": "^2.1.1", "sweetalert": "^2.1.0", - "twitter-text": "^2.0.5" + "twitter-text": "^2.0.5", + "vue-infinite-loading": "^2.4.3", + "vue-loading-overlay": "^3.1.0" } } From d99d7e7c19758032bd42e616f18e800092abc5c9 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 21:01:15 -0700 Subject: [PATCH 02/17] Update PublicApiController --- app/Http/Controllers/PublicApiController.php | 50 +++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index 88dcfab71..440effd86 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -31,7 +31,7 @@ class PublicApiController extends Controller public function __construct() { - $this->middleware('throttle:200, 30'); + $this->middleware('throttle:3000, 30'); $this->fractal = new Fractal\Manager(); $this->fractal->setSerializer(new ArraySerializer()); } @@ -47,6 +47,30 @@ class PublicApiController extends Controller } } + protected function getLikes($status) + { + if(false == Auth::check()) { + return []; + } else { + $profile = Auth::user()->profile; + $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; + $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 status(Request $request, $username, int $postid) { $profile = Profile::whereUsername($username)->first(); @@ -56,6 +80,8 @@ class PublicApiController extends Controller $res = [ 'status' => $this->fractal->createData($item)->toArray(), 'user' => $this->getUserData(), + 'likes' => $this->getLikes($status), + 'shares' => $this->getShares($status), 'reactions' => [ 'liked' => $status->liked(), 'shared' => $status->shared(), @@ -104,6 +130,28 @@ class PublicApiController extends Controller return response()->json($res, 200, [], JSON_PRETTY_PRINT); } + public function statusLikes(Request $request, $username, $id) + { + $profile = Profile::whereUsername($username)->first(); + $status = Status::whereProfileId($profile->id)->find($id); + $this->scopeCheck($profile, $status); + $likes = $this->getLikes($status); + return response()->json([ + 'data' => $likes + ]); + } + + public function statusShares(Request $request, $username, $id) + { + $profile = Profile::whereUsername($username)->first(); + $status = Status::whereProfileId($profile->id)->find($id); + $this->scopeCheck($profile, $status); + $shares = $this->getShares($status); + return response()->json([ + 'data' => $shares + ]); + } + protected function scopeCheck(Profile $profile, Status $status) { if($profile->is_private == true && Auth::check() == false) { From bd503e5a577f89e9aceb9743007dec31e5d1dcc7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 21:02:26 -0700 Subject: [PATCH 03/17] Update Status model --- app/Status.php | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/Status.php b/app/Status.php index 12800213a..a58a2b0b8 100644 --- a/app/Status.php +++ b/app/Status.php @@ -20,6 +20,19 @@ class Status extends Model protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id']; + const STATUS_TYPES = [ + 'photo', + 'photo:album', + 'video', + 'video:album', + 'share', + 'reply', + 'story', + 'story:reply', + 'story:reaction', + 'story:live' + ]; + public function profile() { return $this->belongsTo(Profile::class); @@ -108,6 +121,18 @@ class Status extends Model return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count(); } + public function likedBy() + { + return $this->hasManyThrough( + Profile::class, + Like::class, + 'status_id', + 'id', + 'id', + 'profile_id' + ); + } + public function comments() { return $this->hasMany(self::class, 'in_reply_to_id'); @@ -138,6 +163,18 @@ class Status extends Model return self::whereProfileId($profile->id)->whereReblogOfId($this->id)->count(); } + public function sharedBy() + { + return $this->hasManyThrough( + Profile::class, + Status::class, + 'reblog_of_id', + 'id', + 'id', + 'profile_id' + ); + } + public function parent() { $parent = $this->in_reply_to_id ?? $this->reblog_of_id; From 716586a4a6e407239d6a5750430805d611455688 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 21:02:59 -0700 Subject: [PATCH 04/17] Update app.scss --- resources/assets/sass/app.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 58e427707..179a956ce 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -22,3 +22,5 @@ @import '~bootstrap-vue/dist/bootstrap-vue.css'; @import '~plyr/dist/plyr.css'; + +@import '~vue-loading-overlay/dist/vue-loading.css'; From 73230b7b2902a3d24c2f17f2b92154fa3baa3400 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 21:03:27 -0700 Subject: [PATCH 05/17] Update components.js --- resources/assets/js/components.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/assets/js/components.js b/resources/assets/js/components.js index b7af4034e..edb50ceab 100644 --- a/resources/assets/js/components.js +++ b/resources/assets/js/components.js @@ -1,6 +1,11 @@ window.Vue = require('vue'); import BootstrapVue from 'bootstrap-vue' +import InfiniteLoading from 'vue-infinite-loading'; +import Loading from 'vue-loading-overlay'; + Vue.use(BootstrapVue); +Vue.use(InfiniteLoading); +Vue.use(Loading); pixelfed.readmore = () => { $('.read-more').each(function(k,v) { From 25a3d142ff9eea8db2b65f0dc55c1b855172bd98 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 21:57:43 -0700 Subject: [PATCH 06/17] Update PostComponent.vue --- .../assets/js/components/PostComponent.vue | 164 ++++++++++++++++-- 1 file changed, 153 insertions(+), 11 deletions(-) diff --git a/resources/assets/js/components/PostComponent.vue b/resources/assets/js/components/PostComponent.vue index 92dff4c87..0816fc09b 100644 --- a/resources/assets/js/components/PostComponent.vue +++ b/resources/assets/js/components/PostComponent.vue @@ -1,8 +1,12 @@ @@ -272,9 +344,14 @@ export default { status: {}, media: {}, user: {}, - reactions: {} + reactions: {}, + likes: {}, + likesPage: 1, + shares: {}, + sharesPage: 1, } }, + mounted() { let token = $('meta[name="csrf-token"]').attr('content'); $('input[name="_token"]').each(function(k, v) { @@ -282,11 +359,12 @@ export default { el.val(token); }); this.fetchData(); - //pixelfed.hydrateLikes(); this.authCheck(); }, + updated() { $('.carousel').carousel(); + if(this.reactions) { if(this.reactions.bookmarked == true) { $('.far.fa-bookmark').removeClass('far').addClass('fas text-warning'); @@ -298,6 +376,11 @@ export default { $('.far.fa-heart ').removeClass('far text-dark').addClass('fas text-danger'); } } + + if(this.status) { + let title = this.status.account.username + ' posted a photo: ' + this.status.favourites_count + ' likes'; + $('head title').text(title); + } }, methods: { authCheck() { @@ -314,24 +397,36 @@ export default { $('.post-actions').removeClass('d-none'); } }, + reportUrl() { return '/i/report?type=post&id=' + this.status.id; }, + timestampFormat() { let ts = new Date(this.status.created_at); return ts.toDateString() + ' ' + ts.toLocaleTimeString(); }, + fetchData() { - let url = '/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId; - axios.get(url) + let loader = this.$loading.show({ + 'opacity': 0, + 'background-color': '#f5f8fa' + }); + axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId) .then(response => { let self = this; self.status = response.data.status; self.user = response.data.user; self.media = self.status.media_attachments; self.reactions = response.data.reactions; + self.likes = response.data.likes; + self.shares = response.data.shares; + self.likesPage = 2; + self.sharesPage = 2; this.buildPresenter(); this.showMuteBlock(); + loader.hide(); + $('.postComponent').removeClass('d-none'); }).catch(error => { if(!error.response) { $('.postPresenterLoader .lds-ring').attr('style','width:100%').addClass('pt-4 font-weight-bold text-muted').text('An error occured, cannot fetch media. Please try again later.'); @@ -351,9 +446,58 @@ export default { } }); }, + commentFocus() { $('.comment-form input[name="comment"]').focus(); }, + + likesModal() { + if(this.status.favourites_count == 0 || $('body').hasClass('loggedIn') == false) { + return; + } + this.$refs.likesModal.show(); + }, + + sharesModal() { + if(this.status.reblogs_count == 0 || $('body').hasClass('loggedIn') == false) { + return; + } + this.$refs.sharesModal.show(); + }, + + 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) { + this.likesPage += 1; + this.likes.push(...data.data); + $state.loaded(); + } else { + $state.complete(); + } + }); + }, + + infiniteSharesHandler($state) { + axios.get('/api/v2/shares/profile/'+this.statusUsername+'/status/'+this.statusId, { + params: { + page: this.sharesPage, + }, + }).then(({ data }) => { + if (data.data.length) { + this.sharesPage += 1; + this.shares.push(...data.data); + $state.loaded(); + } else { + $state.complete(); + } + }); + }, + buildPresenter() { let container = $('.postPresenterContainer'); let status = this.status; @@ -364,8 +508,6 @@ export default { el.val(status.account.id); }); - $('.status-comment .comment-text').html(status.content); - if(container.children().length != 0) { return; } From bb7e2ce27ae6f134f22cb286199d174d1e3bb6c5 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 21:58:51 -0700 Subject: [PATCH 07/17] Update compiled assets --- public/css/app.css | Bin 253645 -> 254165 bytes public/js/components.js | Bin 521034 -> 557668 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/css/app.css b/public/css/app.css index 1ddf76154014efbc877bcee374eab67efc24450c..c67c44c2b74d615bd5172dfb7f845659f2833208 100644 GIT binary patch delta 301 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 delta 4890 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 Date: Sat, 1 Dec 2018 22:06:03 -0700 Subject: [PATCH 08/17] Update web routes --- routes/web.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routes/web.php b/routes/web.php index aa3e97935..9e2b5a0c8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -50,6 +50,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('discover/posts', 'InternalApiController@discoverPosts'); 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::group(['prefix' => 'local'], function () { Route::get('i/follow-suggestions', 'ApiController@followSuggestions'); From 6b7509a7348e4c7dafef524ee7c27038d307de74 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 22:06:46 -0700 Subject: [PATCH 09/17] Update asset manifest --- public/mix-manifest.json | Bin 321 -> 321 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 8400066b4215d4c9d4c6ee777ddc926660a22295..4fd418d4d5098574a3c3a09cb3aaaaca080f877d 100644 GIT binary patch delta 54 zcmX@ebdYI+p@^YHYMP;;si9$Vnz@-}im_R;dD2AJ1QAOMv*grNv*fhoL<>`+q{O5& Ji`0obI{>^25jy|? delta 54 zcmX@ebdYI+p@@a4iII_^v4yc|a+ Date: Sat, 1 Dec 2018 22:07:52 -0700 Subject: [PATCH 10/17] Bump version --- config/pixelfed.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pixelfed.php b/config/pixelfed.php index dbb99cac8..b722e8f47 100644 --- a/config/pixelfed.php +++ b/config/pixelfed.php @@ -23,7 +23,7 @@ return [ | This value is the version of your PixelFed instance. | */ - 'version' => '0.3.0', + 'version' => '0.4.0', /* |-------------------------------------------------------------------------- From 3d7c97b0ed46cc32f2b5ecb232444446bcd37e92 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 22:54:30 -0700 Subject: [PATCH 11/17] Update Status model --- app/Status.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Status.php b/app/Status.php index a58a2b0b8..c7b5f0bf8 100644 --- a/app/Status.php +++ b/app/Status.php @@ -18,13 +18,14 @@ class Status extends Model */ protected $dates = ['deleted_at']; - protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id']; + protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id', 'reblog_of_id']; const STATUS_TYPES = [ 'photo', 'photo:album', 'video', 'video:album', + 'photo:video:album' 'share', 'reply', 'story', From 71393514b25de14125ed344bc99ecf1179e1737d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 23:04:42 -0700 Subject: [PATCH 12/17] Update StatusController --- app/Http/Controllers/StatusController.php | 40 +++++++++++++++++++++++ app/Status.php | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index db0b0f453..aa3295448 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -92,6 +92,8 @@ class StatusController extends Controller $photos = $request->file('photo'); $order = 1; + $mimes = []; + foreach ($photos as $k => $v) { $storagePath = "public/m/{$monthHash}/{$userHash}"; $path = $v->store($storagePath); @@ -108,10 +110,14 @@ class StatusController extends Controller $media->filter_name = $request->input('filter_name'); $media->order = $order; $media->save(); + array_push($mimes, $media->mime); ImageOptimize::dispatch($media); $order++; } + $status->type = $this->mimeTypeCheck($mimes); + $status->save(); + NewStatusPipeline::dispatch($status); // TODO: Send to subscribers @@ -254,4 +260,38 @@ class StatusController extends Controller $allowed = ['public', 'unlisted', 'private']; return in_array($visibility, $allowed) ? $visibility : 'public'; } + + public static function mimeTypeCheck($mimes) + { + $allowed = explode(',', config('pixelfed.media_types')); + $count = count($mimes); + $photos = 0; + $videos = 0; + foreach($mimes as $mime) { + if(in_array($mime, $allowed) == false) { + continue; + } + if(str_contains($mime, 'image/')) { + $photos++; + } + if(str_contains($mime, 'video/')) { + $videos++; + } + } + if($photos == 1 && $videos == 0) { + return 'photo'; + } + if($videos == 1 && $photos == 0) { + return 'video'; + } + if($photos > 1 && $videos == 0) { + return 'photo:album'; + } + if($videos > 1 && $photos == 0) { + return 'video:album'; + } + if($photos >= 1 && $videos >= 1) { + return 'photo:video:album'; + } + } } diff --git a/app/Status.php b/app/Status.php index c7b5f0bf8..589e622cf 100644 --- a/app/Status.php +++ b/app/Status.php @@ -25,7 +25,7 @@ class Status extends Model 'photo:album', 'video', 'video:album', - 'photo:video:album' + 'photo:video:album', 'share', 'reply', 'story', From a3ec4a7f4c3a68d2591ac52b1761fdabb7e5a7a0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 23:39:29 -0700 Subject: [PATCH 13/17] Update StatusController --- app/Http/Controllers/StatusController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index aa3295448..80120c1be 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -115,7 +115,7 @@ class StatusController extends Controller $order++; } - $status->type = $this->mimeTypeCheck($mimes); + $status->type = (new self)::mimeTypeCheck($mimes); $status->save(); NewStatusPipeline::dispatch($status); From ed6ebc84f4f7aa1052dc7a481aee176051694851 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 1 Dec 2018 23:41:19 -0700 Subject: [PATCH 14/17] Update InternalApiController --- app/Http/Controllers/InternalApiController.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/InternalApiController.php b/app/Http/Controllers/InternalApiController.php index ae8a45ef3..d21b8eaba 100644 --- a/app/Http/Controllers/InternalApiController.php +++ b/app/Http/Controllers/InternalApiController.php @@ -53,6 +53,7 @@ class InternalApiController extends Controller $medias = $request->input('media'); $attachments = []; $status = new Status; + $mimes = []; foreach($medias as $k => $media) { $m = Media::findOrFail($media['id']); @@ -69,6 +70,7 @@ class InternalApiController extends Controller } $m->save(); $attachments[] = $m; + array_push($mimes, $m->mime); } $status->caption = strip_tags($request->caption); @@ -84,6 +86,7 @@ class InternalApiController extends Controller $status->visibility = $visibility; $status->scope = $visibility; + $status->type = StatusController::mimeTypeCheck($mimes); $status->save(); NewStatusPipeline::dispatch($status); @@ -96,6 +99,7 @@ class InternalApiController extends Controller $this->validate($request, [ 'page' => 'nullable|min:1|max:3', ]); + $profile = Auth::user()->profile; $timeago = Carbon::now()->subMonths(6); $notifications = Notification::with('actor') @@ -148,8 +152,7 @@ class InternalApiController extends Controller ->get(); $posts = Status::select('id', 'caption', 'profile_id') - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') + ->whereHas('media') ->whereIsNsfw(false) ->whereVisibility('public') ->whereNotIn('profile_id', $following) @@ -233,8 +236,7 @@ class InternalApiController extends Controller $following = array_merge($following, $filters); $posts = Status::select('id', 'caption', 'profile_id') - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') + ->whereHas('media') ->whereIsNsfw(false) ->whereVisibility('public') ->whereNotIn('profile_id', $following) From 072a5749afd3a79761b293c94a6ee9f7a3191379 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 2 Dec 2018 00:11:07 -0700 Subject: [PATCH 15/17] Update Status model --- app/Status.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/Status.php b/app/Status.php index 589e622cf..a8a62376f 100644 --- a/app/Status.php +++ b/app/Status.php @@ -3,6 +3,7 @@ namespace App; use Auth, Cache; +use App\Http\Controllers\StatusController; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Storage; @@ -49,9 +50,11 @@ class Status extends Model return $this->hasMany(Media::class)->orderBy('order', 'asc')->first(); } + // todo: deprecate after 0.6.0 public function viewType() { - return Cache::remember('status:view-type:'.$this->id, 40320, function() { + return Cache::remember('status:view-type:'.$this->id, 10080, function() { + $this->setType(); $media = $this->firstMedia(); $mime = explode('/', $media->mime)[0]; $count = $this->media()->count(); @@ -63,6 +66,20 @@ class Status extends Model }); } + // todo: deprecate after 0.6.0 + public function setType() + { + if(in_array($this->type, self::STATUS_TYPES)) { + return; + } + $mimes = $this->media->pluck('mime')->toArray(); + $type = StatusController::mimeTypeCheck($mimes); + if($type) { + $this->type = $type; + $this->save(); + } + } + public function thumb($showNsfw = false) { return Cache::remember('status:thumb:'.$this->id, 40320, function() use ($showNsfw) { From 111a6270489e42c217ac0f2e9d300b06fffee54d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 2 Dec 2018 00:42:34 -0700 Subject: [PATCH 16/17] Update StatusTransformer --- app/Transformer/Api/StatusTransformer.php | 46 ++++++++++++----------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php index 63d8fc29d..bc2b354c0 100644 --- a/app/Transformer/Api/StatusTransformer.php +++ b/app/Transformer/Api/StatusTransformer.php @@ -17,29 +17,31 @@ class StatusTransformer extends Fractal\TransformerAbstract public function transform(Status $status) { return [ - 'id' => $status->id, - 'uri' => $status->url(), - 'url' => $status->url(), - 'in_reply_to_id' => $status->in_reply_to_id, - 'in_reply_to_account_id' => $status->in_reply_to_profile_id, + 'id' => $status->id, + 'uri' => $status->url(), + 'url' => $status->url(), + 'in_reply_to_id' => $status->in_reply_to_id, + 'in_reply_to_account_id' => $status->in_reply_to_profile_id, + 'reblog' => $status->reblog_of_id || $status->in_reply_to_id ? $this->transform($status->parent()) : null, + 'content' => "$status->rendered", + 'created_at' => $status->created_at->format('c'), + 'emojis' => [], + 'reblogs_count' => $status->shares()->count(), + 'favourites_count' => $status->likes()->count(), + 'reblogged' => $status->shared(), + 'favourited' => $status->liked(), + 'muted' => null, + 'sensitive' => (bool) $status->is_nsfw, + 'spoiler_text' => $status->cw_summary, + 'visibility' => $status->visibility, + 'application' => [ + 'name' => 'web', + 'website' => null + ], + 'language' => null, + 'pinned' => null, - // TODO: fixme - 'reblog' => null, - - 'content' => "$status->rendered", - 'created_at' => $status->created_at->format('c'), - 'emojis' => [], - 'reblogs_count' => $status->shares()->count(), - 'favourites_count' => $status->likes()->count(), - 'reblogged' => $status->shared(), - 'favourited' => $status->liked(), - 'muted' => null, - 'sensitive' => (bool) $status->is_nsfw, - 'spoiler_text' => '', - 'visibility' => $status->visibility, - 'application' => null, - 'language' => null, - 'pinned' => null, + 'pf_type' => $status->type, ]; } From 0376a497ba442a7c3192104d8a704bde5e03d5c5 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 2 Dec 2018 18:42:55 -0700 Subject: [PATCH 17/17] Update compose APIs --- .../Controllers/Api/BaseApiController.php | 26 ++++++++++++++++--- app/Http/Controllers/StatusController.php | 16 ++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index 5345062c8..61c8b9e8b 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -8,9 +8,15 @@ use App\Http\Controllers\{ AvatarController }; use Auth, Cache, URL; -use App\{Avatar,Media,Profile}; +use App\{ + Avatar, + Notification, + Media, + Profile +}; use App\Transformer\Api\{ AccountTransformer, + NotificationTransformer, MediaTransformer, StatusTransformer }; @@ -35,6 +41,15 @@ class BaseApiController extends Controller $this->fractal->setSerializer(new ArraySerializer()); } + public function notification(Request $request, $id) + { + $notification = Notification::findOrFail($id); + $resource = new Fractal\Resource\Item($notification, new NotificationTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT); + } + public function accounts(Request $request, $id) { $profile = Profile::findOrFail($id); @@ -173,6 +188,11 @@ class BaseApiController extends Controller $photo = $request->file('file'); + $mimes = explode(',', config('pixelfed.media_types')); + if(in_array($photo->getMimeType(), $mimes) == false) { + return; + } + $storagePath = "public/m/{$monthHash}/{$userHash}"; $path = $photo->store($storagePath); $hash = \hash_file('sha256', $photo); @@ -183,8 +203,8 @@ class BaseApiController extends Controller $media->user_id = $user->id; $media->media_path = $path; $media->original_sha256 = $hash; - $media->size = $photo->getClientSize(); - $media->mime = $photo->getClientMimeType(); + $media->size = $photo->getSize(); + $media->mime = $photo->getMimeType(); $media->filter_class = null; $media->filter_name = null; $media->save(); diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index 80120c1be..605470f78 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -93,8 +93,15 @@ class StatusController extends Controller $photos = $request->file('photo'); $order = 1; $mimes = []; + $medias = 0; foreach ($photos as $k => $v) { + + $allowedMimes = explode(',', config('pixelfed.media_types')); + if(in_array($v->getMimeType(), $allowedMimes) == false) { + continue; + } + $storagePath = "public/m/{$monthHash}/{$userHash}"; $path = $v->store($storagePath); $hash = \hash_file('sha256', $v); @@ -104,8 +111,8 @@ class StatusController extends Controller $media->user_id = $user->id; $media->media_path = $path; $media->original_sha256 = $hash; - $media->size = $v->getClientSize(); - $media->mime = $v->getClientMimeType(); + $media->size = $v->getSize(); + $media->mime = $v->getMimeType(); $media->filter_class = $request->input('filter_class'); $media->filter_name = $request->input('filter_name'); $media->order = $order; @@ -113,8 +120,13 @@ class StatusController extends Controller array_push($mimes, $media->mime); ImageOptimize::dispatch($media); $order++; + $medias++; } + if($medias == 0) { + $status->delete(); + return; + } $status->type = (new self)::mimeTypeCheck($mimes); $status->save();