diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 43cf046b4..82a87006a 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2459,4 +2459,126 @@ class ApiV1Controller extends Controller ->values(); return response()->json(compact('posts')); } + + /** + * GET /api/v2/statuses/{id}/replies + * + * + * @return array + */ + public function statusReplies(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'int|min:1|max:10', + 'sort' => 'in:all,newest,popular' + ]); + + $limit = $request->input('limit', 3); + $pid = $request->user()->profile_id; + $status = StatusService::get($id); + + abort_if(!in_array($status['visibility'], ['public', 'unlisted']), 404); + + $sortBy = $request->input('sort', 'all'); + + if($sortBy == 'all' && !$request->has('cursor')) { + $ids = Cache::remember('status:replies:all:' . $id, 900, function() use($id) { + return DB::table('statuses') + ->where('in_reply_to_id', $id) + ->orderBy('id') + ->cursorPaginate(3); + }); + } else { + $ids = DB::table('statuses') + ->where('in_reply_to_id', $id) + ->when($sortBy, function($q, $sortBy) { + if($sortBy === 'all') { + return $q->orderBy('id'); + } + + if($sortBy === 'newest') { + return $q->orderByDesc('created_at'); + } + + if($sortBy === 'popular') { + return $q->orderByDesc('likes_count'); + } + }) + ->cursorPaginate($limit); + } + + $data = $ids->map(function($post) use($pid) { + $status = StatusService::get($post->id); + + if(!$status || !isset($status['id'])) { + return false; + } + + $status['favourited'] = LikeService::liked($pid, $post->id); + return $status; + }) + ->filter(function($post) { + return $post && isset($post['id']); + }) + ->values(); + + $res = [ + 'data' => $data, + 'next' => $ids->nextPageUrl() + ]; + + return $res; + } + + /** + * GET /api/v2/statuses/{id}/state + * + * + * @return array + */ + public function statusState(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $status = Status::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); + + return StatusService::getState($status->id, $pid); + } + + /** + * GET /api/v1/discover/accounts/popular + * + * + * @return array + */ + public function discoverAccountsPopular(Request $request) + { + abort_if(!$request->user(), 403); + $pid = $request->user()->profile_id; + + $ids = DB::table('profiles') + ->where('is_private', false) + ->whereNull('status') + ->orderByDesc('profiles.followers_count') + ->limit(20) + ->get(); + + $ids = $ids->map(function($profile) { + return AccountService::get($profile->id); + }) + ->filter(function($profile) use($pid) { + return $profile && + isset($profile['id']) && + !FollowerService::follows($pid, $profile['id']) && + $profile['id'] != $pid; + }) + ->take(6) + ->values(); + + return response()->json($ids, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } } diff --git a/app/Http/Controllers/SpaController.php b/app/Http/Controllers/SpaController.php new file mode 100644 index 000000000..ca4eb93b3 --- /dev/null +++ b/app/Http/Controllers/SpaController.php @@ -0,0 +1,99 @@ +middleware('auth'); + } + + public function index() + { + abort_unless(config('exp.spa'), 404); + return view('layouts.spa'); + } + + public function webPost(Request $request, $id) + { + abort_unless(config('exp.spa'), 404); + if($request->user()) { + return view('layouts.spa'); + } + + if(SnowflakeService::byDate(now()->subDays(30)) > $id) { + abort(404); + } + + $post = StatusService::get($id); + + if( + $post && + isset($post['url']) && + isset($post['local']) && + $post['local'] === true + ) { + return redirect($post['url']); + } + + abort(404); + } + + public function webProfile(Request $request, $id) + { + abort_unless(config('exp.spa'), 404); + if($request->user()) { + if(substr($id, 0, 1) == '@') { + $id = AccountService::usernameToId(substr($id, 1)); + return redirect("/i/web/profile/{$id}"); + } + return view('layouts.spa'); + } + + $account = AccountService::get($id); + + if($account && isset($account['url'])) { + return redirect($account['url']); + } + + return redirect('404'); + } + + public function getPrivacy() + { + $body = $this->markdownToHtml('views/page/privacy.md'); + return [ + 'body' => $body + ]; + } + + public function getTerms() + { + $body = $this->markdownToHtml('views/page/terms.md'); + return [ + 'body' => $body + ]; + } + + protected function markdownToHtml($src, $ttl = 600) + { + return Cache::remember( + 'pf:doc_cache:markdown:' . $src, + $ttl, + function() use($src) { + $path = resource_path($src); + $file = file_get_contents($path); + $converter = new CommonMarkConverter(); + return (string) $converter->convertToHtml($file); + }); + } +} diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index c13ddd58b..9f00ab2b6 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -41,6 +41,25 @@ class StatusService }); } + public static function getState($id, $pid) + { + $status = self::get($id, false); + + if(!$status) { + return [ + 'liked' => false, + 'shared' => false, + 'bookmarked' => false + ]; + } + + return [ + 'liked' => LikeService::liked($pid, $id), + 'shared' => self::isShared($id, $pid), + 'bookmarked' => self::isBookmarked($id, $pid) + ]; + } + public static function getFull($id, $pid, $publicOnly = true) { $res = self::get($id, $publicOnly); @@ -89,4 +108,24 @@ class StatusService self::get($id, false); self::get($id, true); } + + public static function isShared($id, $pid = null) + { + return $pid ? + DB::table('statuses') + ->where('reblog_of_id', $id) + ->where('profile_id', $pid) + ->exists() : + false; + } + + public static function isBookmarked($id, $pid = null) + { + return $pid ? + DB::table('bookmarks') + ->where('status_id', $id) + ->where('profile_id', $pid) + ->exists() : + false; + } } diff --git a/config/exp.php b/config/exp.php index c90a2d33b..3db8817a6 100644 --- a/config/exp.php +++ b/config/exp.php @@ -8,4 +8,6 @@ return [ 'top' => env('EXP_TOP', false), 'polls' => env('EXP_POLLS', false), 'cached_public_timeline' => env('EXP_CPT', false), + 'gps' => env('EXP_GPS', false), + 'spa' => env('EXP_SPA', false), ]; diff --git a/public/css/spa.css b/public/css/spa.css new file mode 100644 index 000000000..f0280c5e3 Binary files /dev/null and b/public/css/spa.css differ diff --git a/public/js/app.js b/public/js/app.js index 6dd8ad84e..131f168cd 100644 Binary files a/public/js/app.js and b/public/js/app.js differ diff --git a/public/js/components.js b/public/js/components.js index 611acbe78..b63e0f857 100644 Binary files a/public/js/components.js and b/public/js/components.js differ diff --git a/public/js/compose.js b/public/js/compose.js index f8d715e6b..8ebfeb3d4 100644 Binary files a/public/js/compose.js and b/public/js/compose.js differ diff --git a/public/js/discover.js b/public/js/discover.js index c4dd3d50e..4030608a1 100644 Binary files a/public/js/discover.js and b/public/js/discover.js differ diff --git a/public/js/spa.js b/public/js/spa.js new file mode 100644 index 000000000..bff0e86e2 Binary files /dev/null and b/public/js/spa.js differ diff --git a/public/js/status.js b/public/js/status.js index d78162d91..9001d5854 100644 Binary files a/public/js/status.js and b/public/js/status.js differ diff --git a/public/js/stories.js b/public/js/stories.js index 6092422a2..9061a74e4 100644 Binary files a/public/js/stories.js and b/public/js/stories.js differ diff --git a/public/js/story-compose.js b/public/js/story-compose.js index 30e053e8c..1393e9083 100644 Binary files a/public/js/story-compose.js and b/public/js/story-compose.js differ diff --git a/public/js/timeline.js b/public/js/timeline.js index e14f4a2e8..6941a104e 100644 Binary files a/public/js/timeline.js and b/public/js/timeline.js differ diff --git a/public/js/vendor.js b/public/js/vendor.js index a6c023b21..cdb660f5e 100644 Binary files a/public/js/vendor.js and b/public/js/vendor.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 6fe0623fb..9c52095d5 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/views/layouts/spa.blade.php b/resources/views/layouts/spa.blade.php new file mode 100644 index 000000000..a63f89465 --- /dev/null +++ b/resources/views/layouts/spa.blade.php @@ -0,0 +1,51 @@ + + + + + + + + + {{ $title ?? config_cache('app.name') }} + + + + + + @stack('meta') + + + + + + + + + @auth + + @endauth + + +
+ + + +
+ + + + + diff --git a/routes/api.php b/routes/api.php index fde9963de..35e106bb1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -85,6 +85,8 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::group(['prefix' => 'v2'], function() use($middleware) { Route::get('search', 'Api\ApiV1Controller@searchV2')->middleware($middleware); + Route::get('statuses/{id}/replies', 'Api\ApiV1Controller@statusReplies')->middleware($middleware); + Route::get('statuses/{id}/state', 'Api\ApiV1Controller@statusState')->middleware($middleware); }); }); diff --git a/routes/web.php b/routes/web.php index 76f331545..951ffc993 100644 --- a/routes/web.php +++ b/routes/web.php @@ -333,6 +333,11 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('warning', 'AccountInterstitialController@get'); Route::post('warning', 'AccountInterstitialController@read'); Route::get('my2020', 'SeasonalController@yearInReview'); + + Route::get('web/post/{id}', 'SpaController@webPost'); + Route::get('web/profile/{id}', 'SpaController@webProfile'); + Route::get('web/{q}', 'SpaController@index')->where('q', '.*'); + Route::get('web', 'SpaController@index'); }); Route::group(['prefix' => 'account'], function () {