Browse Source

Merge pull request #1830 from pixelfed/staging

Staging
daniel 5 years ago
parent
commit
e3f16c8b16
47 changed files with 863 additions and 217 deletions
  1. 6 0
      CHANGELOG.md
  2. 3 1
      app/Http/Controllers/Api/ApiV1Controller.php
  3. 2 1
      app/Http/Controllers/Auth/RegisterController.php
  4. 28 2
      app/Http/Controllers/DiscoverController.php
  5. 29 114
      app/Http/Controllers/PublicApiController.php
  6. 20 0
      app/Http/Controllers/SeasonalController.php
  7. 8 4
      app/Http/Controllers/SiteController.php
  8. 28 6
      app/Http/Controllers/StatusController.php
  9. 1 0
      app/Http/Kernel.php
  10. 26 0
      app/Http/Middleware/FrameGuard.php
  11. 29 0
      app/Services/AccountService.php
  12. 1 1
      app/Services/PublicTimelineService.php
  13. 1 0
      app/Transformer/Api/AccountTransformer.php
  14. 56 0
      app/Transformer/Api/AccountWithStatusesTransformer.php
  15. 1 0
      app/Transformer/Api/StatusStatelessTransformer.php
  16. BIN
      public/css/app.css
  17. BIN
      public/css/appdark.css
  18. BIN
      public/css/landing.css
  19. BIN
      public/embed.js
  20. BIN
      public/js/app.js
  21. BIN
      public/js/compose.js
  22. BIN
      public/js/discover.js
  23. BIN
      public/js/profile-directory.js
  24. BIN
      public/js/quill.js
  25. BIN
      public/js/search.js
  26. BIN
      public/js/status.js
  27. BIN
      public/js/theme-monokai.js
  28. BIN
      public/js/timeline.js
  29. BIN
      public/js/vendor.js
  30. BIN
      public/mix-manifest.json
  31. 17 3
      resources/assets/js/app.js
  32. 20 8
      resources/assets/js/components/ComposeModal.vue
  33. 34 6
      resources/assets/js/components/DiscoverComponent.vue
  34. 6 2
      resources/assets/js/components/PostComponent.vue
  35. 127 0
      resources/assets/js/components/ProfileDirectory.vue
  36. 7 7
      resources/assets/js/components/SearchResults.vue
  37. 116 49
      resources/assets/js/components/Timeline.vue
  38. 4 0
      resources/assets/js/profile-directory.js
  39. 0 4
      resources/assets/sass/custom.scss
  40. 17 0
      resources/views/discover/profiles/home.blade.php
  41. 14 4
      resources/views/site/index.blade.php
  42. 46 0
      resources/views/status/embed-removed.blade.php
  43. 178 0
      resources/views/status/embed.blade.php
  44. 15 1
      resources/views/timeline/home.blade.php
  45. 14 1
      resources/views/timeline/local.blade.php
  46. 4 0
      routes/web.php
  47. 5 3
      webpack.mix.js

+ 6 - 0
CHANGELOG.md

@@ -4,6 +4,8 @@
 
 
 ### Added
 ### Added
 - Added drafts API endpoint for Camera Roll ([bad2ecde](https://github.com/pixelfed/pixelfed/commit/bad2ecde))
 - Added drafts API endpoint for Camera Roll ([bad2ecde](https://github.com/pixelfed/pixelfed/commit/bad2ecde))
+- Added AccountService ([885a1258](https://github.com/pixelfed/pixelfed/commit/885a1258))
+- Added post embeds ([1fecf717](https://github.com/pixelfed/pixelfed/commit/1fecf717))
 
 
 ### Fixed
 ### Fixed
 - Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09))
 - Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09))
@@ -45,6 +47,10 @@
 - Updated StatusTransformer, added ```local``` attribute ([484bb509](https://github.com/pixelfed/pixelfed/commit/484bb509))
 - Updated StatusTransformer, added ```local``` attribute ([484bb509](https://github.com/pixelfed/pixelfed/commit/484bb509))
 - Updated PostComponent, fix bug affecting MomentUI and non authenticated users ([7b3fe215](https://github.com/pixelfed/pixelfed/commit/7b3fe215))
 - Updated PostComponent, fix bug affecting MomentUI and non authenticated users ([7b3fe215](https://github.com/pixelfed/pixelfed/commit/7b3fe215))
 - Updated FixUsernames command to allow usernames containing ```.``` ([e5d77c6d](https://github.com/pixelfed/pixelfed/commit/e5d77c6d))
 - Updated FixUsernames command to allow usernames containing ```.``` ([e5d77c6d](https://github.com/pixelfed/pixelfed/commit/e5d77c6d))
+- Updated landing page, add age check ([d11e82c3](https://github.com/pixelfed/pixelfed/commit/d11e82c3))
+- Updated ApiV1Controller, add ```mobile_apis``` to /api/v1/instance endpoint ([57407463](https://github.com/pixelfed/pixelfed/commit/57407463))
+- Updated PublicTimelineService, add video media scopes ([7b00eba3](https://github.com/pixelfed/pixelfed/commit/7b00eba3))
+- Updated PublicApiController, add AccountService ([5ebd2c8a](https://github.com/pixelfed/pixelfed/commit/5ebd2c8a))
 
 
 ## Deprecated
 ## Deprecated
     
     

+ 3 - 1
app/Http/Controllers/Api/ApiV1Controller.php

@@ -906,7 +906,9 @@ class ApiV1Controller extends Controller
                 'max_avatar_size' => config('pixelfed.max_avatar_size'),
                 'max_avatar_size' => config('pixelfed.max_avatar_size'),
                 'max_caption_length' => config('pixelfed.max_caption_length'),
                 'max_caption_length' => config('pixelfed.max_caption_length'),
                 'max_bio_length' => config('pixelfed.max_bio_length'),
                 'max_bio_length' => config('pixelfed.max_bio_length'),
-                'max_album_length' => config('pixelfed.max_album_length')
+                'max_album_length' => config('pixelfed.max_album_length'),
+                'mobile_apis' => config('pixelfed.oauth_enabled')
+
             ]
             ]
         ];
         ];
         return response()->json($res, 200, [], JSON_PRETTY_PRINT);
         return response()->json($res, 200, [], JSON_PRETTY_PRINT);

+ 2 - 1
app/Http/Controllers/Auth/RegisterController.php

@@ -63,7 +63,7 @@ class RegisterController extends Controller
             'unique:users',
             'unique:users',
             function ($attribute, $value, $fail) {
             function ($attribute, $value, $fail) {
                 if (!ctype_alpha($value[0])) {
                 if (!ctype_alpha($value[0])) {
-                    return $fail('Username is invalid. Username must be alpha-numeric and start with a letter.');
+                    return $fail('Username is invalid. Must start with a letter or number.');
                 }
                 }
                 $val = str_replace(['_', '-', '.'], '', $value);
                 $val = str_replace(['_', '-', '.'], '', $value);
                 if(!ctype_alnum($val)) {
                 if(!ctype_alnum($val)) {
@@ -73,6 +73,7 @@ class RegisterController extends Controller
         ];
         ];
 
 
         $rules = [
         $rules = [
+            'agecheck' => 'required|accepted',
             'name'     => 'nullable|string|max:'.config('pixelfed.max_name_length'),
             'name'     => 'nullable|string|max:'.config('pixelfed.max_name_length'),
             'username' => $usernameRules,
             'username' => $usernameRules,
             'email'    => 'required|string|email|max:255|unique:users',
             'email'    => 'required|string|email|max:255|unique:users',

+ 28 - 2
app/Http/Controllers/DiscoverController.php

@@ -14,6 +14,8 @@ use App\{
 };
 };
 use Auth, DB, Cache;
 use Auth, DB, Cache;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
+use App\Transformer\Api\AccountTransformer;
+use App\Transformer\Api\AccountWithStatusesTransformer;
 use App\Transformer\Api\StatusStatelessTransformer;
 use App\Transformer\Api\StatusStatelessTransformer;
 use League\Fractal;
 use League\Fractal;
 use League\Fractal\Serializer\ArraySerializer;
 use League\Fractal\Serializer\ArraySerializer;
@@ -131,7 +133,31 @@ class DiscoverController extends Controller
 
 
     public function profilesDirectory(Request $request)
     public function profilesDirectory(Request $request)
     {
     {
-      $profiles = Profile::whereNull('domain')->simplePaginate(48);
-      return view('discover.profiles.home', compact('profiles'));
+      return view('discover.profiles.home');
+    }
+
+    public function profilesDirectoryApi(Request $request)
+    {
+      $this->validate($request, [
+        'page' => 'integer|max:10'
+      ]);
+
+      $page = $request->input('page') ?? 1;
+      $key = 'discover:profiles:page:' . $page;
+      $ttl = now()->addHours(12);
+
+      $res = Cache::remember($key, $ttl, function() {
+          $profiles = Profile::whereNull('domain')
+                ->whereNull('status')
+                ->whereIsPrivate(false)
+                ->has('statuses')
+                ->whereIsSuggestable(true)
+                // ->inRandomOrder()
+                ->simplePaginate(8);
+          $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
+          return $this->fractal->createData($resource)->toArray();
+      });
+
+      return $res;
     }
     }
 }
 }

+ 29 - 114
app/Http/Controllers/PublicApiController.php

@@ -22,7 +22,11 @@ use App\Transformer\Api\{
     RelationshipTransformer,
     RelationshipTransformer,
     StatusTransformer,
     StatusTransformer,
 };
 };
-use App\Services\UserFilterService;
+use App\Services\{
+    AccountService,
+    PublicTimelineService,
+    UserFilterService
+};
 use App\Jobs\StatusPipeline\NewStatusPipeline;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
 use League\Fractal\Serializer\ArraySerializer;
 use League\Fractal\Serializer\ArraySerializer;
 use League\Fractal\Pagination\IlluminatePaginatorAdapter;
 use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -38,17 +42,12 @@ class PublicApiController extends Controller
         $this->fractal->setSerializer(new ArraySerializer());
         $this->fractal->setSerializer(new ArraySerializer());
     }
     }
 
 
-    protected function getUserData()
+    protected function getUserData($user)
     {
     {
-    	if(false == Auth::check()) {
+    	if(!$user) {
     		return [];
     		return [];
     	} else {
     	} else {
-	        $profile = Auth::user()->profile;
-            if($profile->status) {
-                return [];
-            }
-	        $user = new Fractal\Resource\Item($profile, new AccountTransformer());
-        	return $this->fractal->createData($user)->toArray();
+            return AccountService::get($user->profile_id);
     	}
     	}
     }
     }
 
 
@@ -90,7 +89,7 @@ class PublicApiController extends Controller
         $item = new Fractal\Resource\Item($status, new StatusTransformer());
         $item = new Fractal\Resource\Item($status, new StatusTransformer());
         $res = [
         $res = [
         	'status' => $this->fractal->createData($item)->toArray(),
         	'status' => $this->fractal->createData($item)->toArray(),
-        	'user' => $this->getUserData(),
+        	'user' => $this->getUserData($request->user()),
             'likes' => $this->getLikes($status),
             'likes' => $this->getLikes($status),
             'shares' => $this->getShares($status),
             'shares' => $this->getShares($status),
             'reactions' => [
             'reactions' => [
@@ -235,12 +234,13 @@ class PublicApiController extends Controller
         $max = $request->input('max_id');
         $max = $request->input('max_id');
         $limit = $request->input('limit') ?? 3;
         $limit = $request->input('limit') ?? 3;
 
 
-        // $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
-        //     return Profile::whereIsPrivate(true)
-        //         ->orWhere('unlisted', true)
-        //         ->orWhere('status', '!=', null)
-        //         ->pluck('id');
-        // });
+        $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
+            return Profile::whereIsPrivate(true)
+                ->orWhere('unlisted', true)
+                ->orWhere('status', '!=', null)
+                ->pluck('id')
+                ->toArray();
+        });
 
 
         // if(Auth::check()) {
         // if(Auth::check()) {
         //     // $pid = Auth::user()->profile->id;
         //     // $pid = Auth::user()->profile->id;
@@ -255,7 +255,17 @@ class PublicApiController extends Controller
         //     $filtered = [];
         //     $filtered = [];
         // }
         // }
 
 
-        $filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
+        $filtered = Auth::check() ? array_merge($private, UserFilterService::filters(Auth::user()->profile_id)) : [];
+        // if($max == 0) {
+        //     $res = PublicTimelineService::count();
+        //     if($res == 0) {
+        //         PublicTimelineService::warmCache();
+        //         $res = PublicTimelineService::get(0,4);
+        //     } else {
+        //         $res = PublicTimelineService::get(0,4);
+        //     }
+        //     return response()->json($res);
+        // } 
 
 
         if($min || $max) {
         if($min || $max) {
             $dir = $min ? '>' : '<';
             $dir = $min ? '>' : '<';
@@ -321,7 +331,6 @@ class PublicApiController extends Controller
 
 
         $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
         $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
         $res = $this->fractal->createData($fractal)->toArray();
         $res = $this->fractal->createData($fractal)->toArray();
-        // $res = $timeline;
         return response()->json($res);
         return response()->json($res);
 
 
     }
     }
@@ -439,98 +448,7 @@ class PublicApiController extends Controller
 
 
     public function networkTimelineApi(Request $request)
     public function networkTimelineApi(Request $request)
     {
     {
-        if(!Auth::check()) {
-            return abort(403);
-        }
-
-        $this->validate($request,[
-          'page'        => 'nullable|integer|max:40',
-          'min_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
-          'max_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
-          'limit'       => 'nullable|integer|max:20'
-        ]);
-
-        $page = $request->input('page');
-        $min = $request->input('min_id');
-        $max = $request->input('max_id');
-        $limit = $request->input('limit') ?? 3;
-
-        // TODO: Use redis for timelines
-        // $timeline = Timeline::build()->local();
-        $pid = Auth::user()->profile->id;
-
-        $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
-            return Profile::whereIsPrivate(true)
-                ->orWhere('unlisted', true)
-                ->orWhere('status', '!=', null)
-                ->pluck('id');
-        });
-        $filters = UserFilter::whereUserId($pid)
-                  ->whereFilterableType('App\Profile')
-                  ->whereIn('filter_type', ['mute', 'block'])
-                  ->pluck('filterable_id')->toArray();
-        $filtered = array_merge($private->toArray(), $filters);
-
-        if($min || $max) {
-            $dir = $min ? '>' : '<';
-            $id = $min ?? $max;
-            $timeline = Status::select(
-                        'id', 
-                        'uri',
-                        'caption',
-                        'rendered',
-                        'profile_id', 
-                        'type',
-                        'in_reply_to_id',
-                        'reblog_of_id',
-                        'is_nsfw',
-                        'scope',
-                        'local',
-                        'reply_count',
-                        'comments_disabled',
-                        'created_at',
-                        'updated_at'
-                      )->where('id', $dir, $id)
-                      ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
-                      ->whereNotIn('profile_id', $filtered)
-                      ->whereNotNull('uri')
-                      ->whereNull('in_reply_to_id')
-                      ->whereNull('reblog_of_id')
-                      ->whereVisibility('public')
-                      ->latest()
-                      ->limit($limit)
-                      ->get();
-        } else {
-            $timeline = Status::select(
-                        'id', 
-                        'uri',
-                        'caption',
-                        'rendered',
-                        'profile_id', 
-                        'type',
-                        'in_reply_to_id',
-                        'reblog_of_id',
-                        'is_nsfw',
-                        'scope',
-                        'local',
-                        'reply_count',
-                        'comments_disabled',
-                        'created_at',
-                        'updated_at'
-                      )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
-                      ->whereNotIn('profile_id', $filtered)
-                      ->whereNull('in_reply_to_id')
-                      ->whereNull('reblog_of_id')
-                      ->whereNotNull('uri')
-                      ->whereVisibility('public')
-                      ->latest()
-                      ->simplePaginate($limit);
-        }
-
-        $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
-        $res = $this->fractal->createData($fractal)->toArray();
-        return response()->json($res);
-
+        return response()->json([]);
     }
     }
 
 
     public function relationships(Request $request)
     public function relationships(Request $request)
@@ -555,10 +473,7 @@ class PublicApiController extends Controller
 
 
     public function account(Request $request, $id)
     public function account(Request $request, $id)
     {
     {
-        $profile = Profile::whereNull('status')->findOrFail($id);
-        $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
-        $res = $this->fractal->createData($resource)->toArray();
-
+        $res = AccountService::get($id);
         return response()->json($res);
         return response()->json($res);
     }
     }
 
 

+ 20 - 0
app/Http/Controllers/SeasonalController.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Auth;
+
+class SeasonalController extends Controller
+{
+    public function __construct()
+    {
+    	$this->middleware('auth');
+    }
+
+    public function yearInReview()
+    {
+    	$profile = Auth::user()->profile;
+    	return view('account.yir', compact('profile'));
+    }
+}

+ 8 - 4
app/Http/Controllers/SiteController.php

@@ -10,10 +10,10 @@ use App\Util\Localization\Localization;
 
 
 class SiteController extends Controller
 class SiteController extends Controller
 {
 {
-    public function home()
+    public function home(Request $request)
     {
     {
         if (Auth::check()) {
         if (Auth::check()) {
-            return $this->homeTimeline();
+            return $this->homeTimeline($request);
         } else {
         } else {
             return $this->homeGuest();
             return $this->homeGuest();
         }
         }
@@ -24,9 +24,13 @@ class SiteController extends Controller
         return view('site.index');
         return view('site.index');
     }
     }
 
 
-    public function homeTimeline()
+    public function homeTimeline(Request $request)
     {
     {
-        return view('timeline.home');
+        $this->validate($request, [
+            'layout' => 'nullable|string|in:grid,feed'
+        ]);
+        $layout = $request->input('layout', 'feed');
+        return view('timeline.home', compact('layout'));
     }
     }
 
 
     public function changeLocale(Request $request, $locale)
     public function changeLocale(Request $request, $locale)

+ 28 - 6
app/Http/Controllers/StatusController.php

@@ -51,6 +51,12 @@ class StatusController extends Controller
             }
             }
         }
         }
 
 
+        if($status->type == 'archived') {
+            if(Auth::user()->profile_id !== $status->profile_id) {
+                abort(404);
+            }
+        }
+
         if ($request->wantsJson() && config('federation.activitypub.enabled')) {
         if ($request->wantsJson() && config('federation.activitypub.enabled')) {
             return $this->showActivityPub($request, $status);
             return $this->showActivityPub($request, $status);
         }
         }
@@ -70,13 +76,29 @@ class StatusController extends Controller
 
 
     public function showEmbed(Request $request, $username, int $id)
     public function showEmbed(Request $request, $username, int $id)
     {
     {
-        abort(404);
-        $profile = Profile::whereNull('status')->whereUsername($username)->first();
-        $status = Status::whereScope('private')->find($id);
-        if(!$profile || !$status) {
-            return view('status.embed-removed');
+        $profile = Profile::whereNull(['domain','status'])
+            ->whereIsPrivate(false)
+            ->whereUsername($username)
+            ->first();
+        if(!$profile) {
+            $content = view('status.embed-removed');
+            return response($content)->header('X-Frame-Options', 'ALLOWALL');
+        }
+        $status = Status::whereProfileId($profile->id)
+            ->whereNull('uri')
+            ->whereScope('public')
+            ->whereIsNsfw(false)
+            ->whereIn('type', ['photo', 'video'])
+            ->find($id);
+        if(!$status) {
+            $content = view('status.embed-removed');
+            return response($content)->header('X-Frame-Options', 'ALLOWALL');
         }
         }
-        return view('status.embed', compact('status'));
+        $showLikes = $request->filled('likes') && $request->likes == true;
+        $showCaption = $request->filled('caption') && $request->caption !== false;
+        $layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
+        $content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
+        return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
     }
     }
 
 
     public function showObject(Request $request, $username, int $id)
     public function showObject(Request $request, $username, int $id)

+ 1 - 0
app/Http/Kernel.php

@@ -29,6 +29,7 @@ class Kernel extends HttpKernel
     protected $middlewareGroups = [
     protected $middlewareGroups = [
         'web' => [
         'web' => [
             \App\Http\Middleware\EncryptCookies::class,
             \App\Http\Middleware\EncryptCookies::class,
+            \App\Http\Middleware\FrameGuard::class,
             \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
             \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
             \Illuminate\Session\Middleware\StartSession::class,
             \Illuminate\Session\Middleware\StartSession::class,
             // \Illuminate\Session\Middleware\AuthenticateSession::class,
             // \Illuminate\Session\Middleware\AuthenticateSession::class,

+ 26 - 0
app/Http/Middleware/FrameGuard.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+
+class FrameGuard
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $response = $next($request);
+
+        if (!$response->headers->has('X-Frame-Options')) {
+            $response->headers->set('X-Frame-Options', 'SAMEORIGIN', false);
+        }
+
+        return $response;
+    }
+}

+ 29 - 0
app/Services/AccountService.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Services;
+
+use Cache;
+use App\Profile;
+use App\Transformer\Api\AccountTransformer;
+use League\Fractal;
+use League\Fractal\Serializer\ArraySerializer;
+
+class AccountService {
+
+	const CACHE_KEY = 'pf:services:account:';
+
+	public static function get($id)
+	{
+		$key = self::CACHE_KEY . ':' . $id;
+		$ttl = now()->addHours(12);
+
+		return Cache::remember($key, $ttl, function() use($id) {
+			$fractal = new Fractal\Manager();
+			$fractal->setSerializer(new ArraySerializer());
+			$profile = Profile::whereNull('status')->findOrFail($id);
+			$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
+			return $fractal->createData($resource)->toArray();
+		});
+	}
+
+}

+ 1 - 1
app/Services/PublicTimelineService.php

@@ -52,7 +52,7 @@ class PublicTimelineService {
 			$ids = Status::whereNull('uri')
 			$ids = Status::whereNull('uri')
 				->whereNull('in_reply_to_id')
 				->whereNull('in_reply_to_id')
 				->whereNull('reblog_of_id')
 				->whereNull('reblog_of_id')
-				->whereIn('type', ['photo', 'photo:album'])
+				->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
 				->whereScope('public')
 				->whereScope('public')
 				->latest()
 				->latest()
 				->limit($limit)
 				->limit($limit)

+ 1 - 0
app/Transformer/Api/AccountTransformer.php

@@ -33,6 +33,7 @@ class AccountTransformer extends Fractal\TransformerAbstract
 			'website' => $profile->website,
 			'website' => $profile->website,
 			'local' => (bool) $local,
 			'local' => (bool) $local,
 			'is_admin' => (bool) $is_admin,
 			'is_admin' => (bool) $is_admin,
+			'created_at' => $profile->created_at->timestamp
 		];
 		];
 	}
 	}
 
 

+ 56 - 0
app/Transformer/Api/AccountWithStatusesTransformer.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Transformer\Api;
+
+use Auth;
+use App\Profile;
+use League\Fractal;
+
+class AccountWithStatusesTransformer extends Fractal\TransformerAbstract
+{
+    protected $defaultIncludes = [
+        // 'relationship',
+        'posts',
+    ];
+
+	public function transform(Profile $profile)
+	{
+		$local = $profile->domain == null;
+		$is_admin = !$local ? false : $profile->user->is_admin;
+		$acct = $local ? $profile->username : substr($profile->username, 1);
+		$username = $local ? $profile->username : explode('@', $acct)[0];
+		return [
+			'id' => (string) $profile->id,
+			'username' => $username,
+			'acct' => $acct,
+			'display_name' => $profile->name,
+			'locked' => (bool) $profile->is_private,
+			'followers_count' => $profile->followerCount(),
+			'following_count' => $profile->followingCount(),
+			'statuses_count' => (int) $profile->statusCount(),
+			'note' => $profile->bio ?? '',
+			'url' => $profile->url(),
+			'avatar' => $profile->avatarUrl(),
+			'website' => $profile->website,
+			'local' => (bool) $local,
+			'is_admin' => (bool) $is_admin,
+			'created_at' => $profile->created_at->timestamp
+		];
+	}
+
+	protected function includePosts(Profile $profile)
+	{
+		$posts = $profile
+				->statuses()
+				->whereIsNsfw(false)
+				->whereType('photo')
+				->whereScope('public')
+				->whereNull('in_reply_to_id')
+				->whereNull('reblog_of_id')
+				->latest()
+				->take(5)
+				->get();
+
+		return $this->collection($posts, new StatusStatelessTransformer());
+	}
+}

+ 1 - 0
app/Transformer/Api/StatusStatelessTransformer.php

@@ -48,6 +48,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
             'thread'                    => false,
             'thread'                    => false,
             'replies'                   => [],
             'replies'                   => [],
             'parent'                    => $status->parent() ? $this->transform($status->parent()) : [],
             'parent'                    => $status->parent() ? $this->transform($status->parent()) : [],
+            'local'                     => (bool) $status->local,
         ];
         ];
     }
     }
 
 

BIN
public/css/app.css


BIN
public/css/appdark.css


BIN
public/css/landing.css


BIN
public/embed.js


BIN
public/js/app.js


BIN
public/js/compose.js


BIN
public/js/discover.js


BIN
public/js/profile-directory.js


BIN
public/js/quill.js


BIN
public/js/search.js


BIN
public/js/status.js


BIN
public/js/theme-monokai.js


BIN
public/js/timeline.js


BIN
public/js/vendor.js


BIN
public/mix-manifest.json


+ 17 - 3
resources/assets/js/app.js

@@ -76,7 +76,21 @@ window.App.util = {
 			['Walden','filter-walden'], 
 			['Walden','filter-walden'], 
 			['Willow','filter-willow'], 
 			['Willow','filter-willow'], 
 			['X-Pro II','filter-xpro-ii']
 			['X-Pro II','filter-xpro-ii']
-		],
-		emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
-		],
+	],
+	emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
+	],
+	embed: {
+		post: (function(url, caption = true, likes = false, layout = 'full') {
+			let u = url + '/embed?';
+			u += caption ? 'caption=true&' : 'caption=false&';
+			u += likes ? 'likes=true&' : 'likes=false&';
+			u += layout == 'compact' ? 'layout=compact' : 'layout=full';
+			return '<iframe src="'+u+'" class="pixelfed__embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"></iframe><script async defer src="'+window.location.origin +'/embed.js"><\/script>';
+		}),
+		profile: (function(url) {
+			// placeholder
+			console.error('This method is not supported yet');
+		})
+	}
+
 };
 };

+ 20 - 8
resources/assets/js/components/ComposeModal.vue

@@ -98,6 +98,22 @@
 								</div>
 								</div>
 							</a>
 							</a>
 
 
+							<a class="d-none card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" :click="showAddToStoryCard">
+								<div class="card-body">
+									<div class="media">
+										<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;background-color: #008DF5">
+											<i class="fas fa-history text-white fa-lg"></i>
+										</div>	
+										<div class="media-body text-left">
+											<p class="mb-0">
+												<span class="h5 mt-0 font-weight-bold text-primary">Add to Story</span> 
+											</p>
+											<p class="mb-0 text-muted">Add a photo or video to your story.</p>
+										</div>
+									</div>
+								</div>
+							</a>
+
 							<a class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/collections/create">
 							<a class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/collections/create">
 								<div class="card-body">
 								<div class="card-body">
 									<div class="media">
 									<div class="media">
@@ -132,9 +148,8 @@
 									</div>
 									</div>
 								</div>
 								</div>
 							</div>
 							</div>
-							<hr>
-							<p>
-								<a class="font-weight-bold" href="/site/help">Need Help?</a>
+							<p class="pt-3">
+								<a class="font-weight-bold" href="/site/help">Help</a>
 							</p>
 							</p>
 						</div>
 						</div>
 					</div>
 					</div>
@@ -755,10 +770,6 @@ export default {
 			this.pageTitle = '';
 			this.pageTitle = '';
 			
 			
 			switch(this.page) {
 			switch(this.page) {
-				case 'addToStory':
-					this.page = 1;
-				break;
-
 				case 'cropPhoto':
 				case 'cropPhoto':
 				case 'editMedia':
 				case 'editMedia':
 					this.page = 2;
 					this.page = 2;
@@ -906,7 +917,8 @@ export default {
 			.then(res => {
 			.then(res => {
 				this.cameraRollMedia = res.data;
 				this.cameraRollMedia = res.data;
 			});
 			});
-		}
+		},
+
 	}
 	}
 }
 }
 </script>
 </script>

+ 34 - 6
resources/assets/js/components/DiscoverComponent.vue

@@ -4,10 +4,10 @@
 			<img src="/img/pixelfed-icon-grey.svg">
 			<img src="/img/pixelfed-icon-grey.svg">
 		</div>
 		</div>
 		<div v-else>
 		<div v-else>
-			<div class="d-block d-md-none px-0 border-top-0 mx-n3">
+			<!-- <div class="d-block d-md-none px-0 border-top-0 mx-n3">
 				<input class="form-control rounded-0" placeholder="Search" v-model="searchTerm" v-on:keyup.enter="searchSubmit">
 				<input class="form-control rounded-0" placeholder="Search" v-model="searchTerm" v-on:keyup.enter="searchSubmit">
-			</div>
-			<section class="d-none d-md-flex mb-md-2 pt-5 discover-bar" style="width:auto; overflow: auto hidden;" v-if="categories.length > 0">
+			</div> -->
+			<!-- <section class="d-none d-md-flex mb-md-2 pt-5 discover-bar" style="width:auto; overflow: auto hidden;" v-if="categories.length > 0">
 				<a v-if="config.ab.loops == true" class="text-decoration-none bg-transparent border border-success rounded d-inline-flex align-items-center justify-content-center mr-3 card-disc" href="/discover/loops">
 				<a v-if="config.ab.loops == true" class="text-decoration-none bg-transparent border border-success rounded d-inline-flex align-items-center justify-content-center mr-3 card-disc" href="/discover/loops">
 					<p class="text-success lead font-weight-bold mb-0">Loops</p>
 					<p class="text-success lead font-weight-bold mb-0">Loops</p>
 				</a>
 				</a>
@@ -15,11 +15,39 @@
 					<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p>
 					<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p>
 				</a>
 				</a>
 
 
-			</section>
+			</section> -->
 			<section class="mb-5 section-explore">
 			<section class="mb-5 section-explore">
 				<div class="profile-timeline">
 				<div class="profile-timeline">
-					<div class="row p-0">
-						<div class="col-4 p-1 p-sm-2 p-md-3" v-for="post in posts">
+					<div class="row p-0 mt-5">
+						<div class="col-12 col-md-6">
+							<div class="">
+								<a class="card info-overlay card-md-border-0" :href="posts[0].url">
+									<div class="square">
+										<span v-if="posts[0].type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
+										<span v-if="posts[0].type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
+										<span v-if="posts[0].type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
+										<div class="square-content" v-bind:style="{ 'background-image': 'url(' + posts[0].thumb + ')' }">
+										</div>
+									</div>
+								</a>
+							</div>				
+						</div>
+						<div class="col-12 col-md-6 row p-0 m-0">
+							<div v-for="(post, index) in posts.slice(1,5)" class="col-6" style="margin-bottom:1.8rem;">
+								<a class="card info-overlay card-md-border-0" :href="post.url">
+									<div class="square">
+										<span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
+										<span v-if="post.type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
+										<span v-if="post.type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
+										<div class="square-content" v-bind:style="{ 'background-image': 'url(' + post.thumb + ')' }">
+										</div>
+									</div>
+								</a>
+							</div>
+						</div>
+					</div>
+					<div class="row p-0" style="display: flex;">
+						<div v-for="(post, index) in posts.slice(5)" class="col-3 p-1 p-sm-2 p-md-3">
 							<a class="card info-overlay card-md-border-0" :href="post.url">
 							<a class="card info-overlay card-md-border-0" :href="post.url">
 								<div class="square">
 								<div class="square">
 									<span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
 									<span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>

+ 6 - 2
resources/assets/js/components/PostComponent.vue

@@ -234,7 +234,7 @@
               </div>
               </div>
               <form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
               <form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
                 <textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
                 <textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
-                <input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/>
+                <input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply" :disabled="replyText.length == 0" />
               </form>
               </form>
             </div>
             </div>
           </div>
           </div>
@@ -351,7 +351,7 @@
                       </span>
                       </span>
                       <button 
                       <button 
                       :class="[replyText.length > 1 ? 'btn btn-sm font-weight-bold float-right btn-outline-dark ':'btn btn-sm font-weight-bold float-right btn-outline-lighter']" 
                       :class="[replyText.length > 1 ? 'btn btn-sm font-weight-bold float-right btn-outline-dark ':'btn btn-sm font-weight-bold float-right btn-outline-lighter']" 
-                      :disabled="replyText.length < 2" 
+                      :disabled="replyText.length == 0 ? 'disabled':''" 
                       @click="postReply"
                       @click="postReply"
                       >Post</button>
                       >Post</button>
                     </p>
                     </p>
@@ -547,6 +547,10 @@
   .momentui .carousel-item {
   .momentui .carousel-item {
     background: #000 !important;
     background: #000 !important;
   }
   }
+  .reply-btn[disabled] {
+    opacity: .3;
+    color: #3897f0;
+  }
 </style>
 </style>
 
 
 <script>
 <script>

+ 127 - 0
resources/assets/js/components/ProfileDirectory.vue

@@ -0,0 +1,127 @@
+<template>
+<div>
+	<div class="col-12">
+		<p class="font-weight-bold text-lighter text-uppercase">Profiles Directory</p>
+		<div v-if="loaded" class="">
+			<div class="row">
+				<div class="col-12 col-md-6 p-1" v-for="(profile, index) in profiles">
+					<div class="card card-body border shadow-none py-2">
+						<div class="media">
+							<a :href="profile.url"><img :src="profile.avatar" class="rounded-circle border mr-3" alt="..." width="40px" height="40px"></a>
+							<div class="media-body">
+								<p class="mt-0 mb-0 font-weight-bold">
+									<a :href="profile.url" class="text-dark">{{profile.username}}</a>
+								</p>
+								<p class="mb-1 small text-lighter d-flex justify-content-between font-weight-bold">
+									<span>
+										<span>{{prettyCount(profile.statuses_count)}}</span> POSTS
+									</span>
+									<span>
+										<span>{{postsPerDay(profile)}}</span> POSTS/DAY
+									</span>
+									<span>
+										<span>{{prettyCount(profile.followers_count)}}</span> FOLLOWERS
+									</span>
+								</p>
+								<p class="mb-1">
+									<span v-for="(post, i) in profile.posts" class="shadow-sm" :key="'profile_posts_'+i">
+										<a :href="post.url" class="text-decoration-none mr-1">
+											<img :src="thumbUrl(post)" width="62.3px" height="62.3px" class="border rounded">
+										</a>
+									</span>
+								</p>
+								
+							</div>
+						</div>
+					</div>
+				</div>
+
+				<div v-if="showLoadMore" class="col-12">
+					<p class="text-center mb-0 pt-3">
+						<button class="btn btn-outline-secondary btn-sm px-4 py-1 font-weight-bold" @click="loadMore()">Load More</button>
+					</p>
+				</div>
+			</div>
+			
+		</div>
+		<div v-else>
+			<div class="row">
+				<div class="col-12 d-flex justify-content-center align-items-center">
+					<div class="spinner-border" role="status">
+						<span class="sr-only">Loading...</span>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<style type="text/css" scoped></style>
+
+<script type="text/javascript">
+	export default {
+		props: ['profileId'],
+
+		data() {
+			return {
+				loaded: false,
+				showLoadMore: true,
+				profiles: [],
+				page: 1
+			}
+		},
+
+		beforeMount() {
+			this.fetchData();
+		},
+
+		methods: {
+			fetchData() {
+				axios.get('/api/pixelfed/v2/discover/profiles', {
+					params: {
+						page: this.page
+					}
+				})
+				.then(res => {
+					if(res.data.length == 0) {
+						this.showLoadMore = false;
+						this.loaded = true;
+						return;
+					}
+					this.profiles = res.data;
+					this.showLoadMore = this.profiles.length == 8;
+					this.loaded = true;
+				});
+			},
+
+			prettyCount(val) {
+				return App.util.format.count(val);
+			},
+
+			loadMore() {
+				this.loaded = false;
+				this.page++;
+				this.fetchData();
+			},
+
+			thumbUrl(p) {
+				return p.media_attachments[0].url;
+			},
+
+			postsPerDay(profile) {
+				let created = profile.created_at;
+				let now = Date.now();
+				let diff = Math.abs(created, now)
+				let day = 1000 * 60 * 60 * 24;
+				let days = Math.round(diff / day);
+
+				let statuses = profile.statuses_count;
+
+				let perDay = this.prettyCount(Math.floor(statuses / days));
+				console.log(perDay);
+				return perDay;
+			}
+		}
+	}
+</script>

+ 7 - 7
resources/assets/js/components/SearchResults.vue

@@ -11,31 +11,31 @@
 
 
 	<div v-if="!loading && !networkError" class="mt-5 row">
 	<div v-if="!loading && !networkError" class="mt-5 row">
 
 
-		<div class="col-12 col-md-3 mb-4">
+		<div class="col-12 col-md-2 mb-4">
 			<div v-if="results.hashtags || results.profiles || results.statuses">
 			<div v-if="results.hashtags || results.profiles || results.statuses">
 				<p class="font-weight-bold">Filters</p>
 				<p class="font-weight-bold">Filters</p>
 				<div class="custom-control custom-checkbox">
 				<div class="custom-control custom-checkbox">
 					<input type="checkbox" class="custom-control-input" id="filter1" v-model="filters.hashtags">
 					<input type="checkbox" class="custom-control-input" id="filter1" v-model="filters.hashtags">
-					<label class="custom-control-label text-muted font-weight-light" for="filter1">Show Hashtags</label>
+					<label class="custom-control-label text-muted font-weight-light" for="filter1">Hashtags</label>
 				</div>
 				</div>
 				<div class="custom-control custom-checkbox">
 				<div class="custom-control custom-checkbox">
 					<input type="checkbox" class="custom-control-input" id="filter2" v-model="filters.profiles">
 					<input type="checkbox" class="custom-control-input" id="filter2" v-model="filters.profiles">
-					<label class="custom-control-label text-muted font-weight-light" for="filter2">Show Profiles</label>
+					<label class="custom-control-label text-muted font-weight-light" for="filter2">Profiles</label>
 				</div>
 				</div>
 				<div class="custom-control custom-checkbox">
 				<div class="custom-control custom-checkbox">
 					<input type="checkbox" class="custom-control-input" id="filter3" v-model="filters.statuses">
 					<input type="checkbox" class="custom-control-input" id="filter3" v-model="filters.statuses">
-					<label class="custom-control-label text-muted font-weight-light" for="filter3">Show Statuses</label>
+					<label class="custom-control-label text-muted font-weight-light" for="filter3">Statuses</label>
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>
-		<div class="col-12 col-md-9">
+		<div class="col-12 col-md-10">
 			<p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p>
 			<p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p>
 			<hr>
 			<hr>
 
 
 			<div v-if="filters.hashtags && results.hashtags" class="row mb-4">
 			<div v-if="filters.hashtags && results.hashtags" class="row mb-4">
 				<p class="col-12 font-weight-bold text-muted">Hashtags</p>
 				<p class="col-12 font-weight-bold text-muted">Hashtags</p>
 				<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-3 mb-3" style="text-decoration: none;" :href="hashtag.url">
 				<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-3 mb-3" style="text-decoration: none;" :href="hashtag.url">
-					<div class="card card-body text-center">
+					<div class="card card-body text-center shadow-none border">
 						<p class="lead mb-0 text-truncate text-dark" data-toggle="tooltip" :title="hashtag.value">
 						<p class="lead mb-0 text-truncate text-dark" data-toggle="tooltip" :title="hashtag.value">
 							#{{hashtag.value}}
 							#{{hashtag.value}}
 						</p>
 						</p>
@@ -49,7 +49,7 @@
 			<div v-if="filters.profiles && results.profiles" class="row mb-4">
 			<div v-if="filters.profiles && results.profiles" class="row mb-4">
 				<p class="col-12 font-weight-bold text-muted">Profiles</p>
 				<p class="col-12 font-weight-bold text-muted">Profiles</p>
 				<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="profile.url">
 				<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="profile.url">
-					<div class="card card-body text-center">
+					<div class="card card-body text-center shadow-none border">
 						<p class="text-center">
 						<p class="text-center">
 							<img :src="profile.entity.thumb" width="32px" height="32px" class="rounded-circle box-shadow">
 							<img :src="profile.entity.thumb" width="32px" height="32px" class="rounded-circle box-shadow">
 						</p>
 						</p>

+ 116 - 49
resources/assets/js/components/Timeline.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
 <div class="container" style="">
 <div class="container" style="">
-	<div class="row">
+	<div v-if="layout === 'feed'" class="row">
 		<div :class="[modes.distractionFree ? 'col-md-8 col-lg-8 offset-md-2 px-0 my-sm-3 timeline order-2 order-md-1':'col-md-8 col-lg-8 px-0 my-sm-3 timeline order-2 order-md-1']">
 		<div :class="[modes.distractionFree ? 'col-md-8 col-lg-8 offset-md-2 px-0 my-sm-3 timeline order-2 order-md-1':'col-md-8 col-lg-8 px-0 my-sm-3 timeline order-2 order-md-1']">
 			<div class="d-none" data-id="StoryTimelineComponent"></div>
 			<div class="d-none" data-id="StoryTimelineComponent"></div>
 			<div style="padding-top:10px;">
 			<div style="padding-top:10px;">
@@ -211,13 +211,13 @@
 						<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white sticky-md-bottom p-0">
 						<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white sticky-md-bottom p-0">
 							<form class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="status.id" data-truncate="false">
 							<form class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="status.id" data-truncate="false">
 								<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
 								<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
-								<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="commentSubmit(status, $event)"/>
+								<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="commentSubmit(status, $event)" :disabled="replyText.length == 0" />
 							</form>
 							</form>
 						</div>
 						</div>
 					</div>
 					</div>
 				</div>
 				</div>
 				<div v-if="!loading && feed.length">
 				<div v-if="!loading && feed.length">
-					<div class="card shadow-none border">
+					<div class="card shadow-none">
 						<div class="card-body">
 						<div class="card-body">
 							<infinite-loading @infinite="infiniteTimeline" :distance="800">
 							<infinite-loading @infinite="infiniteTimeline" :distance="800">
 							<div slot="no-more" class="font-weight-bold">No more posts to load</div>
 							<div slot="no-more" class="font-weight-bold">No more posts to load</div>
@@ -227,7 +227,7 @@
 					</div>
 					</div>
 				</div>
 				</div>
 				<div v-if="!loading && scope == 'home' && feed.length == 0">
 				<div v-if="!loading && scope == 'home' && feed.length == 0">
-					<div class="card">
+					<div class="card shadow-none border">
 						<div class="card-body text-center">
 						<div class="card-body text-center">
 							<p class="h2 font-weight-lighter p-5">Hello, {{profile.acct}}</p>
 							<p class="h2 font-weight-lighter p-5">Hello, {{profile.acct}}</p>
 							<p class="text-lighter"><i class="fas fa-camera-retro fa-5x"></i></p>
 							<p class="text-lighter"><i class="fas fa-camera-retro fa-5x"></i></p>
@@ -240,7 +240,7 @@
 		</div>
 		</div>
 
 
 		<div v-if="!modes.distractionFree" class="col-md-4 col-lg-4 my-3 order-1 order-md-2 d-none d-md-block">
 		<div v-if="!modes.distractionFree" class="col-md-4 col-lg-4 my-3 order-1 order-md-2 d-none d-md-block">
-			<div class="position-sticky" style="top:68px;">
+			<div class="position-sticky" style="top:78px;">
 				<div class="mb-4">
 				<div class="mb-4">
 					<div class="">
 					<div class="">
 						<div class="">
 						<div class="">
@@ -327,11 +327,11 @@
 						<p class="mb-0 text-uppercase font-weight-bold text-muted small">
 						<p class="mb-0 text-uppercase font-weight-bold text-muted small">
 							<a href="/site/about" class="text-dark pr-2">About Us</a>
 							<a href="/site/about" class="text-dark pr-2">About Us</a>
 							<a href="/site/help" class="text-dark pr-2">Help</a>
 							<a href="/site/help" class="text-dark pr-2">Help</a>
-							<a href="/site/open-source" class="text-dark pr-2">Open Source</a>
 							<a href="/site/language" class="text-dark pr-2">Language</a>
 							<a href="/site/language" class="text-dark pr-2">Language</a>
-							<a href="/site/terms" class="text-dark pr-2">Terms</a>
-							<a href="/site/privacy" class="text-dark pr-2">Privacy</a>
+							<a href="/discover/profiles" class="text-dark pr-2">Profiles</a>
 							<a href="/discover/places" class="text-dark pr-2">Places</a>
 							<a href="/discover/places" class="text-dark pr-2">Places</a>
+							<a href="/site/privacy" class="text-dark pr-2">Privacy</a>
+							<a href="/site/terms" class="text-dark pr-2">Terms</a>
 						</p>
 						</p>
 						<p class="mb-0 text-uppercase font-weight-bold text-muted small">
 						<p class="mb-0 text-uppercase font-weight-bold text-muted small">
 							<a href="http://pixelfed.org" class="text-muted" rel="noopener" title="" data-toggle="tooltip">Powered by Pixelfed</a>
 							<a href="http://pixelfed.org" class="text-muted" rel="noopener" title="" data-toggle="tooltip">Powered by Pixelfed</a>
@@ -341,40 +341,91 @@
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>
- <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 && ctxMenuStatus.account.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div>
-      <div v-if="ctxMenuStatus && ctxMenuStatus.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="ctxMenuStatus && ctxMenuStatus.account.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="ctxMenuEmbed()">Embed</div>
-      <div class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</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 == ctxMenuStatus.account.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>
- <b-modal ref="ctxModModal"
-    id="ctx-mod-modal"
-    hide-header
-    hide-footer
-    centered
-    rounded
-    size="sm"
-    body-class="list-group-flush p-0 rounded">
-    <div class="list-group text-center">
-      <div class="list-group-item rounded cursor-pointer" @click="moderatePost(ctxMenuStatus, 'unlist')">Unlist from Timelines</div>
-      <div class="list-group-item rounded cursor-pointer" @click="">Add Content Warning</div>
-      <div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModMenuClose()">Cancel</div>
-    </div>
+	<div v-else class="row pt-2">
+		<div class="col-12">
+			<div v-if="loading" class="text-center">
+				<div class="spinner-border" role="status">
+					<span class="sr-only">Loading...</span>
+				</div>
+			</div>
+			<div v-else class="row">
+				<div class="col-12 col-md-4 p-1 p-md-3 mb-3" v-for="(s, index) in feed" :key="`${index}-${s.id}`">
+					<div class="card info-overlay card-md-border-0 shadow-sm border border-light" :href="statusUrl(s)">
+						<div :class="[s.sensitive ? 'square' : 'square ' + s.media_attachments[0].filter_class]">
+							<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
+							<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
+							<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
+							<div class="square-content" v-bind:style="previewBackground(s)">
+							</div>
+							<div class="info-overlay-text px-4">
+								<p class="text-white m-auto text-center">
+									{{trimCaption(s.content_text)}}
+								</p>
+							</div>
+						</div>
+					</div>
+					<div class="py-3 media align-items-center">
+						<img :src="s.account.avatar" class="mr-3 rounded-circle shadow-sm" :alt="s.account.username + ' \'s avatar'" width="30px" height="30px">
+						<div class="media-body">
+							<p class="mb-0 font-weight-bold small">{{s.account.username}}</p>
+							<p class="mb-0" style="line-height: 0.7;">
+								<a :href="statusUrl(s)" class="small text-lighter">
+									<timeago :datetime="s.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(s.created_at)" v-b-tooltip.hover.bottom></timeago>
+								</a>
+							</p>
+						</div>
+						<div class="ml-3">
+							<p class="mb-0">
+								<span class="font-weight-bold small">{{s.favourites_count == 1 ? '1 like' : s.favourites_count+' likes'}}</span>
+								<span class="px-2"><i v-bind:class="[s.favourited ? 'fas fa-heart text-danger cursor-pointer' : 'far fa-heart like-btn text-lighter cursor-pointer']" v-on:click="likeStatus(s, $event)"></i></span>
+								<span class="mr-2 cursor-pointer"><i class="fas fa-ellipsis-v" @click="ctxMenu(s)"></i></span>
+							</p>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div v-if="!loading && feed.length">
+					<infinite-loading @infinite="infiniteTimeline" :distance="800">
+					<div slot="no-more" class="font-weight-bold">No more posts to load</div>
+					<div slot="no-results" class="font-weight-bold">No more posts to load</div>
+					</infinite-loading>
+			</div>
+		</div>
+	</div>
+<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 && ctxMenuStatus.account.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div>
+		<div v-if="ctxMenuStatus && ctxMenuStatus.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="ctxMenuStatus && ctxMenuStatus.account.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 v-if="ctxMenuStatus && ctxMenuStatus.local == true" class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">Embed</div>
+		<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</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 == ctxMenuStatus.account.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>
+<b-modal ref="ctxModModal"
+	id="ctx-mod-modal"
+	hide-header
+	hide-footer
+	centered
+	rounded
+	size="sm"
+	body-class="list-group-flush p-0 rounded">
+	<div class="list-group text-center">
+		<div class="list-group-item rounded cursor-pointer" @click="moderatePost(ctxMenuStatus, 'unlist')">Unlist from Timelines</div>
+		<div class="list-group-item rounded cursor-pointer" @click="">Add Content Warning</div>
+		<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModMenuClose()">Cancel</div>
+	</div>
  </b-modal>
  </b-modal>
  <b-modal ref="ctxShareModal"
  <b-modal ref="ctxShareModal"
     id="ctx-share-modal"
     id="ctx-share-modal"
@@ -402,10 +453,10 @@
     size="md"
     size="md"
     body-class="p-2 rounded">
     body-class="p-2 rounded">
 	<div>
 	<div>
-		<textarea class="form-control disabled" rows="1" style="border: 1px solid #efefef; font-size: 14px; line-height: 17px; min-height: 37px; margin: 0 0 7px; resize: none; white-space: nowrap;" v-model="ctxEmbedPayload"></textarea>
+		<textarea class="form-control disabled" rows="1" style="border: 1px solid #efefef; font-size: 14px; line-height: 12px; height: 37px; margin: 0 0 7px; resize: none; white-space: nowrap;" v-model="ctxEmbedPayload"></textarea>
 		<hr>
 		<hr>
 		<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
 		<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
-		<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="#">API Terms of Use</a>.</p>
+		<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
 	</div>
 	</div>
   </b-modal>
   </b-modal>
   <b-modal
   <b-modal
@@ -454,11 +505,15 @@
 		height: 0px;
 		height: 0px;
 		background: transparent;
 		background: transparent;
 	}
 	}
+	.reply-btn[disabled] {
+		opacity: .3;
+		color: #3897f0;
+	}
 </style>
 </style>
 
 
 <script type="text/javascript">
 <script type="text/javascript">
 	export default {
 	export default {
-		props: ['scope'],
+		props: ['scope', 'layout'],
 		data() {
 		data() {
 			return {
 			return {
 				ids: [],
 				ids: [],
@@ -1177,10 +1232,7 @@
 
 
 			ctxMenu(status) {
 			ctxMenu(status) {
 				this.ctxMenuStatus = status;
 				this.ctxMenuStatus = status;
-				// let payload = '<div class="pixlfed-media" data-id="'+ this.ctxMenuStatus.id + '"></div><script ';
-				// payload += 'src="https://pixelfed.dev/js/embed.js" async><';
-				// payload += '/script>';
-				// this.ctxEmbedPayload = payload;
+				this.ctxEmbedPayload = window.App.util.embed.post(status.url);
 				if(status.account.id == this.profile.id) {
 				if(status.account.id == this.profile.id) {
 					this.$refs.ctxModal.show();
 					this.$refs.ctxModal.show();
 				} else {
 				} else {
@@ -1354,6 +1406,21 @@
 					break;
 					break;
 				}
 				}
 			},
 			},
+
+			previewUrl(status) {
+				return status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].preview_url;
+			},
+
+			previewBackground(status) {
+				let preview = this.previewUrl(status);
+				return 'background-image: url(' + preview + ');';
+			},
+
+			trimCaption(caption, len = 60) {
+				return _.truncate(caption, {
+					length: len
+				});
+			}
 		}
 		}
 	}
 	}
 </script>
 </script>

+ 4 - 0
resources/assets/js/profile-directory.js

@@ -0,0 +1,4 @@
+Vue.component(
+    'profile-directory',
+    require('./components/ProfileDirectory.vue').default
+);

+ 0 - 4
resources/assets/sass/custom.scss

@@ -37,10 +37,6 @@ body, button, input, textarea {
   color: #212529 !important;
   color: #212529 !important;
 }
 }
 
 
-.settings-nav .active {
-  border-left: 2px solid #6c757d !important
-}
-
 .settings-nav .active .nav-link{
 .settings-nav .active .nav-link{
   font-weight: bold;
   font-weight: bold;
 }
 }

+ 17 - 0
resources/views/discover/profiles/home.blade.php

@@ -0,0 +1,17 @@
+@extends('layouts.app')
+
+@section('content')
+
+<div class="container mt-5">	
+	<div class="col-12">
+		<profile-directory profile-id="{{Auth::user()->profile_id}}"></profile-directory>
+	</div>
+</div>
+
+@endsection
+
+@push('scripts')
+<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
+<script type="text/javascript" src="{{mix('js/profile-directory.js')}}"></script>
+<script type="text/javascript">App.boot();</script>
+@endpush

+ 14 - 4
resources/views/site/index.blade.php

@@ -75,7 +75,7 @@
                 </div>
                 </div>
                 <div class="col-12 col-md-5 offset-md-1">
                 <div class="col-12 col-md-5 offset-md-1">
                     <div>
                     <div>
-                        <div class="card my-4">
+                        <div class="card my-4 shadow-none border">
                             <div class="card-body px-lg-5">
                             <div class="card-body px-lg-5">
                                 <div class="text-center pt-3">
                                 <div class="text-center pt-3">
                                     <img src="/img/pixelfed-icon-color.svg">
                                     <img src="/img/pixelfed-icon-color.svg">
@@ -86,7 +86,7 @@
                                 </div>
                                 </div>
                                 <div>
                                 <div>
                                     @if(true === config('pixelfed.open_registration'))
                                     @if(true === config('pixelfed.open_registration'))
-                                    <form class="px-1" method="POST" action="{{ route('register') }}">
+                                    <form class="px-1" method="POST" action="{{ route('register') }}" id="register_form">
                                         @csrf
                                         @csrf
                                         <div class="form-group row">
                                         <div class="form-group row">
                                             <div class="col-md-12">
                                             <div class="col-md-12">
@@ -102,7 +102,7 @@
 
 
                                         <div class="form-group row">
                                         <div class="form-group row">
                                             <div class="col-md-12">
                                             <div class="col-md-12">
-                                                <input id="username" type="text" class="form-control{{ $errors->has('username') ? ' is-invalid' : '' }}" name="username" value="{{ old('username') }}" placeholder="{{ __('Username') }}" required>
+                                                <input id="username" type="text" class="form-control{{ $errors->has('username') ? ' is-invalid' : '' }}" name="username" value="{{ old('username') }}" placeholder="{{ __('Username') }}" required maxlength="15" minlength="2">
 
 
                                                 @if ($errors->has('username'))
                                                 @if ($errors->has('username'))
                                                 <span class="invalid-feedback">
                                                 <span class="invalid-feedback">
@@ -141,6 +141,16 @@
                                                 <input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required>
                                                 <input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required>
                                             </div>
                                             </div>
                                         </div>
                                         </div>
+                                        <div class="form-group row">
+                                            <div class="col-md-12">
+                                                <div class="form-check">
+                                                  <input class="form-check-input" name="agecheck" type="checkbox" value="true" id="ageCheck" required>
+                                                  <label class="form-check-label" for="ageCheck">
+                                                    I am at least 16 years old
+                                                  </label>
+                                                </div>
+                                            </div>
+                                        </div>
                                         <div class="form-group row">
                                         <div class="form-group row">
                                             <div class="col-md-12">
                                             <div class="col-md-12">
                                                 <button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold">
                                                 <button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold">
@@ -161,7 +171,7 @@
                                 </div>
                                 </div>
                             </div>
                             </div>
                         </div>
                         </div>
-                        <div class="card card-body">
+                        <div class="card shadow-none border card-body">
                             <p class="text-center mb-0 font-weight-bold">Have an account? <a href="/login">Log in</a></p>
                             <p class="text-center mb-0 font-weight-bold">Have an account? <a href="/login">Log in</a></p>
                         </div>
                         </div>
                     </div>
                     </div>

+ 46 - 0
resources/views/status/embed-removed.blade.php

@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+
+	<meta charset="utf-8">
+	<meta http-equiv="X-UA-Compatible" content="IE=edge">
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	<meta name="mobile-web-app-capable" content="yes">
+
+	<title>Pixelfed | 404 Embed Not Found</title>
+
+	<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
+	<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
+	<meta name="medium" content="image">
+	<meta name="theme-color" content="#10c5f8">
+	<meta name="apple-mobile-web-app-capable" content="yes">
+	<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
+	<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
+	<link href="{{ mix('css/app.css') }}" rel="stylesheet">
+	<style type="text/css">
+		body.embed-card {
+			background: #fff !important;
+			margin: 0;
+			padding-bottom: 0;
+		}
+		.status-card-embed {
+			box-shadow: none;
+			border-radius: 4px;
+			overflow: hidden;
+		}
+	</style>
+</head>
+<body class="bg-white">
+	<div class="embed-card">
+		<div class="card  status-card-embed card-md-rounded-0 border card-body border shadow-none rounded-0 d-flex justify-content-center align-items-center">
+			<div class="text-center p-5">
+				<img src="/img/pixelfed-icon-color.svg" width="40px" height="40px">
+				<p class="h2 py-3 font-weight-bold">Pixelfed</p>
+				<p style="font-size:14px;font-weight: 500;" class="p-2">The link to this photo or video may be broken, or the post may have been removed.</p>
+				<p><a href="{{config('app.url')}}" class="font-weight-bold" target="_blank">Visit Pixelfed</a></p>
+			</div>
+		</div>
+	</div>
+	<script type="text/javascript">window.addEventListener("message",e=>{const t=e.data||{};window.parent&&"setHeight"===t.type&&window.parent.postMessage({type:"setHeight",id:t.id,height:document.getElementsByTagName("html")[0].scrollHeight},"*")});</script>
+</body>
+</html>

+ 178 - 0
resources/views/status/embed.blade.php

@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<html lang="{{ app()->getLocale() }}">
+<head>
+    
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="mobile-web-app-capable" content="yes">
+
+    <title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
+
+    <meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
+    <meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
+    <meta property="og:type" content="article">
+    <meta property="og:url" content="{{$status->url()}}">
+    <meta name="medium" content="image">
+    <meta name="theme-color" content="#10c5f8">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
+    <link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
+    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
+    <style type="text/css">
+      body.embed-card {
+          background: #fff !important;
+          margin: 0;
+          padding-bottom: 0;
+      }
+      .status-card-embed {
+        box-shadow: none;
+        border-radius: 4px;
+        overflow: hidden;
+      }
+    </style>
+</head>
+<body class="bg-white">
+  <div class="embed-card">
+  @php($item = $status)
+  <div class="card status-card-embed card-md-rounded-0 border">
+    <div class="card-header d-inline-flex align-items-center bg-white">
+      <img src="{{$item->profile->avatarUrl()}}" width="32px" height="32px" target="_blank" style="border-radius: 32px;">
+      <a class="username font-weight-bold pl-2 text-dark" target="_blank" href="{{$item->profile->url()}}">
+        {{$item->profile->username}}
+      </a>
+    </div>
+    <a href="{{$status->url()}}" target="_blank">
+    @php($status = $item)
+    @switch($status->viewType())
+      @case('photo')
+      @case('image')
+        @if($status->is_nsfw)
+        <details class="details-animated">
+          <summary>
+            <p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
+            <p class="font-weight-light">(click to show)</p>
+          </summary>
+          <a class="max-hide-overflow {{$status->firstMedia()->filter_class}}" href="{{$status->url()}}" target="_blank">
+            <img class="card-img-top" src="{{$status->mediaUrl()}}">
+          </a>
+        </details>
+        @else
+        <div class="{{$status->firstMedia()->filter_class}}">
+          <img src="{{$status->mediaUrl()}}" width="100%">
+        </div>
+        @endif
+      @break
+      @case('album')
+        @if($status->is_nsfw)
+
+        @else
+          <div id="photo-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="carousel">
+            <ol class="carousel-indicators">
+              @for($i = 0; $i < $status->media_count; $i++)
+              <li data-target="#photo-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
+              @endfor
+            </ol>
+            <div class="carousel-inner">
+              @foreach($status->media()->orderBy('order')->get() as $media)
+              <div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
+                <figure class="{{$media->filter_class}}">
+                  <span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;">{{$loop->iteration}}/{{$loop->count}}</span>
+                  <img class="d-block w-100" src="{{$media->url()}}" alt="{{$status->caption}}">
+                </figure>
+              </div>
+              @endforeach
+            </div>
+            <a class="carousel-control-prev" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
+              <span class="carousel-control-prev-icon" aria-hidden="true"></span>
+              <span class="sr-only">Previous</span>
+            </a>
+            <a class="carousel-control-next" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
+              <span class="carousel-control-next-icon" aria-hidden="true"></span>
+              <span class="sr-only">Next</span>
+            </a>
+          </div>
+        @endif
+      @break
+      @case('video')
+        @if($status->is_nsfw)
+        <details class="details-animated">
+          <summary>
+            <p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
+            <p class="font-weight-light">(click to show)</p>
+          </summary>
+          <div class="embed-responsive embed-responsive-16by9">
+            <video class="video" preload="none" controls loop>
+              <source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
+            </video>
+          </div>
+         </details>
+        @else
+        <div class="embed-responsive embed-responsive-16by9">
+          <video class="video" preload="none" controls loop>
+            <source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
+          </video>
+        </div>
+        @endif
+      @break
+      @case('video-album')
+        @if($status->is_nsfw)
+        <details class="details-animated">
+          <summary>
+            <p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
+            <p class="font-weight-light">(click to show)</p>
+          </summary>
+          <div class="embed-responsive embed-responsive-16by9">
+            <video class="video" preload="none" controls loop>
+              <source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
+            </video>
+          </div>
+         </details>
+        @else
+        <div class="embed-responsive embed-responsive-16by9">
+          <video class="video" preload="none" controls loop>
+            <source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
+          </video>
+        </div>
+        @endif
+      @break
+    @endswitch
+  </a>
+  @if($layout != 'compact')
+    <div class="card-body">
+      <div class="view-more mb-2">
+        <a class="font-weight-bold" href="{{$status->url()}}" target="_blank">View More on Pixelfed</a>
+      </div>
+      <hr>
+      @if($showLikes)
+      <div class="likes font-weight-bold pb-2">
+        <span class="like-count">{{$item->likes_count}}</span> likes
+      </div>
+      @endif
+      <div class="caption">
+        <p class="my-0">
+          <span class="username font-weight-bold">
+            <bdi><a class="text-dark" href="{{$item->profile->url()}}" target="_blank">{{$item->profile->username}}</a></bdi>
+          </span>
+         @if($showCaption)
+          <span class="caption-container">{!! $item->rendered ?? e($item->caption) !!}</span>
+          @endif
+        </p>
+      </div>
+    </div>
+    @endif
+    <div class="card-footer bg-white d-inline-flex justify-content-between align-items-center">
+      <div class="timestamp">
+        <p class="small text-uppercase mb-0"><a href="{{$item->url()}}" class="text-muted" target="_blank">{{$item->created_at->diffForHumans()}}</a></p>
+      </div>
+      <div>
+        <a class="small font-weight-bold text-muted pr-1" href="{{config('app.url')}}" target="_blank">{{config('pixelfed.domain.app')}}</a>
+        <a href="https://pixelfed.org" target="_blank"><img src="/img/pixelfed-icon-color.svg" width="26px"></a>
+      </div>
+    </div>
+  </div>
+  </div>
+  <script type="text/javascript">window.addEventListener("message",e=>{const t=e.data||{};window.parent&&"setHeight"===t.type&&window.parent.postMessage({type:"setHeight",id:t.id,height:document.getElementsByTagName("html")[0].scrollHeight},"*")});</script>
+  <script type="text/javascript">document.querySelectorAll('.caption-container a').forEach(function(i) {i.setAttribute('target', '_blank');});</script>
+</body>
+</html>

+ 15 - 1
resources/views/timeline/home.blade.php

@@ -2,10 +2,24 @@
 
 
 @section('content')
 @section('content')
 
 
-<timeline scope="home"></timeline>
+<timeline scope="home" layout="feed"></timeline>
 
 
 @endsection
 @endsection
 
 
+
+@if($layout == 'grid')
+@push('styles')
+<style type="text/css">
+	body {
+		background: #fff !important;
+	}
+	.navbar.border-bottom {
+		border-bottom: none !important;
+	}
+</style>
+@endpush
+@endif
+
 @push('scripts')
 @push('scripts')
 <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
 <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
 <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
 <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>

+ 14 - 1
resources/views/timeline/local.blade.php

@@ -2,10 +2,23 @@
 
 
 @section('content')
 @section('content')
 
 
-<timeline scope="local"></timeline>
+<timeline scope="local" layout="feed"></timeline>
 
 
 @endsection
 @endsection
 
 
+@if($layout == 'grid')
+@push('styles')
+<style type="text/css">
+	body {
+		background: #fff !important;
+	}
+	.navbar.border-bottom {
+		border-bottom: none !important;
+	}
+</style>
+@endpush
+@endif
+
 @push('scripts')
 @push('scripts')
 <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
 <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
 <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
 <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>

+ 4 - 0
routes/web.php

@@ -71,6 +71,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
     Route::redirect('discover/personal', '/discover');
     Route::redirect('discover/personal', '/discover');
     Route::get('discover', 'DiscoverController@home')->name('discover');
     Route::get('discover', 'DiscoverController@home')->name('discover');
     Route::get('discover/loops', 'DiscoverController@showLoops');
     Route::get('discover/loops', 'DiscoverController@showLoops');
+    Route::get('discover/profiles', 'DiscoverController@profilesDirectory')->name('discover.profiles');
+    
     
     
     Route::group(['prefix' => 'api'], function () {
     Route::group(['prefix' => 'api'], function () {
         Route::get('search', 'SearchController@searchAPI');
         Route::get('search', 'SearchController@searchAPI');
@@ -117,6 +119,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
                 Route::get('config', 'ApiController@siteConfiguration');
                 Route::get('config', 'ApiController@siteConfiguration');
                 Route::get('discover', 'InternalApiController@discover');
                 Route::get('discover', 'InternalApiController@discover');
                 Route::get('discover/posts', 'InternalApiController@discoverPosts');
                 Route::get('discover/posts', 'InternalApiController@discoverPosts');
+                Route::get('discover/profiles', 'DiscoverController@profilesDirectoryApi');
                 Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
                 Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
                 Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
                 Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
                 Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
                 Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
@@ -373,6 +376,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 
 
     Route::get('c/{collection}', 'CollectionController@show');
     Route::get('c/{collection}', 'CollectionController@show');
     Route::get('p/{username}/{id}/c', 'CommentController@showAll');
     Route::get('p/{username}/{id}/c', 'CommentController@showAll');
+    Route::get('p/{username}/{id}/embed', 'StatusController@showEmbed');
     Route::get('p/{username}/{id}/edit', 'StatusController@edit');
     Route::get('p/{username}/{id}/edit', 'StatusController@edit');
     Route::post('p/{username}/{id}/edit', 'StatusController@editStore');
     Route::post('p/{username}/{id}/edit', 'StatusController@editStore');
     Route::get('p/{username}/{id}.json', 'StatusController@showObject');
     Route::get('p/{username}/{id}.json', 'StatusController@showObject');

+ 5 - 3
webpack.mix.js

@@ -29,12 +29,14 @@ mix.js('resources/assets/js/app.js', 'public/js')
 .js('resources/assets/js/lib/ace/ace.js', 'public/js')
 .js('resources/assets/js/lib/ace/ace.js', 'public/js')
 .js('resources/assets/js/lib/ace/mode-dot.js', 'public/js')
 .js('resources/assets/js/lib/ace/mode-dot.js', 'public/js')
 .js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js')
 .js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js')
-// .js('resources/assets/js/embed.js', 'public')
-// .js('resources/assets/js/direct.js', 'public/js')
 .js('resources/assets/js/hashtag.js', 'public/js')
 .js('resources/assets/js/hashtag.js', 'public/js')
 .js('resources/assets/js/collectioncompose.js', 'public/js')
 .js('resources/assets/js/collectioncompose.js', 'public/js')
 .js('resources/assets/js/collections.js', 'public/js')
 .js('resources/assets/js/collections.js', 'public/js')
-//.js('resources/assets/js/admin.js', 'public/js')
+.js('resources/assets/js/profile-directory.js', 'public/js')
+// .js('resources/assets/js/embed.js', 'public')
+// .js('resources/assets/js/direct.js', 'public/js')
+// .js('resources/assets/js/admin.js', 'public/js')
+// .js('resources/assets/js/micro.js', 'public/js')
 
 
 .extract([
 .extract([
 	'lodash',
 	'lodash',