Browse Source

Merge pull request #2753 from pixelfed/staging

Staging
daniel 4 years ago
parent
commit
70e3fed0f2
58 changed files with 2581 additions and 1988 deletions
  1. 4 1
      CHANGELOG.md
  2. 106 11
      app/Http/Controllers/Admin/AdminSettingsController.php
  3. 14 14
      app/Http/Controllers/Api/ApiV1Controller.php
  4. 1 1
      app/Http/Controllers/Api/InstanceApiController.php
  5. 179 179
      app/Http/Controllers/Auth/RegisterController.php
  6. 11 11
      app/Http/Controllers/ComposeController.php
  7. 8 8
      app/Http/Controllers/DirectMessageController.php
  8. 8 8
      app/Http/Controllers/FederationController.php
  9. 1 1
      app/Http/Controllers/ImportController.php
  10. 225 225
      app/Http/Controllers/ProfileController.php
  11. 2 2
      app/Http/Controllers/PublicApiController.php
  12. 340 340
      app/Http/Controllers/SearchController.php
  13. 24 11
      app/Http/Controllers/Settings/HomeSettings.php
  14. 2 2
      app/Http/Controllers/SettingsController.php
  15. 7 2
      app/Http/Controllers/StatusController.php
  16. 17 17
      app/Http/Controllers/StoryController.php
  17. 26 26
      app/Http/Middleware/EmailVerificationCheck.php
  18. 56 56
      app/Jobs/AvatarPipeline/AvatarOptimize.php
  19. 1 1
      app/Jobs/ImportPipeline/ImportInstagram.php
  20. 128 128
      app/Jobs/SharePipeline/SharePipeline.php
  21. 152 152
      app/Jobs/StatusPipeline/StatusDelete.php
  22. 142 142
      app/Jobs/StatusPipeline/StatusEntityLexer.php
  23. 14 0
      app/Models/ConfigCache.php
  24. 11 0
      app/Models/UserPronoun.php
  25. 1 1
      app/Providers/AuthServiceProvider.php
  26. 95 0
      app/Services/ConfigCacheService.php
  27. 8 8
      app/Services/MediaStorageService.php
  28. 102 0
      app/Services/PronounService.php
  29. 3 1
      app/Transformer/Api/AccountTransformer.php
  30. 2 2
      app/Util/ActivityPub/Helpers.php
  31. 13 13
      app/Util/ActivityPub/Inbox.php
  32. 2 2
      app/Util/ActivityPub/Outbox.php
  33. 2 2
      app/Util/Media/Image.php
  34. 13 13
      app/Util/Site/Config.php
  35. 63 63
      app/Util/Site/Nodeinfo.php
  36. 9 0
      app/helpers.php
  37. 94 91
      composer.json
  38. 5 0
      config/app.php
  39. 2 0
      config/instance.php
  40. 34 0
      database/migrations/2021_04_28_060450_create_config_caches_table.php
  41. 34 0
      database/migrations/2021_05_12_042153_create_user_pronouns_table.php
  42. BIN
      public/js/profile.js
  43. BIN
      public/mix-manifest.json
  44. 3 1
      resources/assets/js/components/Profile.vue
  45. 86 61
      resources/views/admin/partial/topnav.blade.php
  46. 178 58
      resources/views/admin/settings/home.blade.php
  47. 14 11
      resources/views/admin/settings/sidebar.blade.php
  48. 2 2
      resources/views/admin/users/show.blade.php
  49. 4 4
      resources/views/layouts/anon.blade.php
  50. 104 100
      resources/views/layouts/app.blade.php
  51. 41 41
      resources/views/layouts/blank.blade.php
  52. 2 2
      resources/views/layouts/partial/nav.blade.php
  53. 2 2
      resources/views/settings/applications.blade.php
  54. 2 2
      resources/views/settings/developers.blade.php
  55. 171 159
      resources/views/settings/home.blade.php
  56. 2 2
      resources/views/settings/partial/sidebar.blade.php
  57. 8 8
      resources/views/site/help/getting-started.blade.php
  58. 1 1
      resources/views/site/partial/template.blade.php

+ 4 - 1
CHANGELOG.md

@@ -4,7 +4,9 @@
 ### Added
 - Autocomplete Support (hashtags + mentions) ([de514f7d](https://github.com/pixelfed/pixelfed/commit/de514f7d))
 - Creative Commons Licenses ([552e950](https://github.com/pixelfed/pixelfed/commit/552e950))
-- Add Network Timeline ([af7face4](https://github.com/pixelfed/pixelfed/commit/af7face4))
+- Network Timeline ([af7face4](https://github.com/pixelfed/pixelfed/commit/af7face4))
+- Admin config settings ([f2066b74](https://github.com/pixelfed/pixelfed/commit/f2066b74))
+- Profile pronouns ([fabb57a9](https://github.com/pixelfed/pixelfed/commit/fabb57a9))
 
 ### Updated
 - Updated AdminController, fix variable name in updateSpam method. ([6edaf940](https://github.com/pixelfed/pixelfed/commit/6edaf940))
@@ -83,6 +85,7 @@
 - Updated PostComponent, change like logic. ([0a35f5d6](https://github.com/pixelfed/pixelfed/commit/0a35f5d6))
 - Updated Timeline component, change like logic. ([7bcbf96b](https://github.com/pixelfed/pixelfed/commit/7bcbf96b))
 - Updated LikeService, fix likedBy method. ([a5e64da6](https://github.com/pixelfed/pixelfed/commit/a5e64da6))
+- Updated PublicApiController, increase public timeline to 6 months from 3. ([8a736432](https://github.com/pixelfed/pixelfed/commit/8a736432))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10)

+ 106 - 11
app/Http/Controllers/Admin/AdminSettingsController.php

@@ -8,12 +8,116 @@ use Carbon\Carbon;
 use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
 use App\Http\Controllers\Controller;
 use App\Util\Lexer\PrettyNumber;
+use App\Models\ConfigCache;
+use App\Services\ConfigCacheService;
 
 trait AdminSettingsController
 {
 	public function settings(Request $request)
 	{
-		return view('admin.settings.home');
+		$name = ConfigCacheService::get('app.name');
+		$short_description = ConfigCacheService::get('app.short_description');
+		$description = ConfigCacheService::get('app.description');
+		$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
+		$jpeg = in_array('image/jpg', $types) ? true : in_array('image/jpeg', $types);
+		$png = in_array('image/png', $types);
+		$gif = in_array('image/gif', $types);
+		$mp4 = in_array('video/mp4', $types);
+
+		return view('admin.settings.home', compact(
+			'name',
+			'short_description',
+			'description',
+			'jpeg',
+			'png',
+			'gif',
+			'mp4'
+		));
+	}
+
+	public function settingsHomeStore(Request $request)
+	{
+		$this->validate($request, [
+			'name' => 'nullable|string',
+			'short_description' => 'nullable',
+			'long_description' => 'nullable',
+			'max_photo_size' => 'nullable|integer|min:1',
+			'max_album_length' => 'nullable|integer|min:1|max:100',
+			'image_quality' => 'nullable|integer|min:1|max:100',
+			'type_jpeg' => 'nullable',
+			'type_png' => 'nullable',
+			'type_gif' => 'nullable',
+			'type_mp4' => 'nullable',
+		]);
+
+		$media_types = explode(',', config_cache('pixelfed.media_types'));
+		$media_types_original = $media_types;
+
+		$mimes = [
+			'type_jpeg' => 'image/jpeg',
+			'type_png' => 'image/png',
+			'type_gif' => 'image/gif',
+			'type_mp4' => 'video/mp4',
+		];
+
+		foreach ($mimes as $key => $value) {
+			if($request->input($key) == 'on') {
+				if(!in_array($value, $media_types)) {
+					array_push($media_types, $value);
+				}
+			} else {
+				$media_types = array_diff($media_types, [$value]);
+			}
+		}
+
+		if($media_types !== $media_types_original) {
+			ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
+		}
+
+		$keys = [
+			'name' => 'app.name',
+			'short_description' => 'app.short_description',
+			'long_description' => 'app.description',
+			'max_photo_size' => 'pixelfed.max_photo_size',
+			'max_album_length' => 'pixelfed.max_album_length',
+			'image_quality' => 'pixelfed.image_quality',
+			'account_limit' => 'pixelfed.max_account_size',
+			'custom_css' => 'uikit.custom.css',
+			'custom_js' => 'uikit.custom.js'
+		];
+
+		foreach ($keys as $key => $value) {
+			$cc = ConfigCache::whereK($value)->first();
+			$val = $request->input($key);
+			if($cc && $cc->v != $val) {
+				ConfigCacheService::put($value, $val);
+			}
+		}
+
+		$bools = [
+			'activitypub' => 'federation.activitypub.enabled',
+			'open_registration' => 'pixelfed.open_registration',
+			'mobile_apis' => 'pixelfed.oauth_enabled',
+			'stories' => 'instance.stories.enabled',
+			'ig_import' => 'pixelfed.import.instagram.enabled',
+			'spam_detection' => 'pixelfed.bouncer.enabled',
+			'require_email_verification' => 'pixelfed.enforce_email_verification',
+			'enforce_account_limit' => 'pixelfed.enforce_account_limit',
+			'show_custom_css' => 'uikit.show_custom.css',
+			'show_custom_js' => 'uikit.show_custom.js',
+		];
+
+		foreach ($bools as $key => $value) {
+			$active = $request->input($key) == 'on';
+
+			if(config_cache($value) !== $active) {
+				ConfigCacheService::put($value, (bool) $active);
+			}
+		}
+
+		Cache::forget('api:site:configuration:_v0.2');
+
+		return redirect('/i/admin/settings');
 	}
 
 	public function settingsBackups(Request $request)
@@ -84,15 +188,6 @@ trait AdminSettingsController
 		return view('admin.settings.features');
 	}
 
-	public function settingsHomeStore(Request $request)
-	{
-		$this->validate($request, [
-			'APP_NAME' => 'required|string',
-		]);
-		// Artisan::call('config:clear');
-		return redirect()->back();
-	}
-
 	public function settingsPages(Request $request)
 	{
 		$pages = Page::orderByDesc('updated_at')->paginate(10);
@@ -135,4 +230,4 @@ trait AdminSettingsController
 		}
 		return view('admin.settings.system', compact('sys'));
 	}
-}
+}

+ 14 - 14
app/Http/Controllers/Api/ApiV1Controller.php

@@ -66,7 +66,7 @@ class ApiV1Controller extends Controller
 
 	public function apps(Request $request)
 	{
-		abort_if(!config('pixelfed.oauth_enabled'), 404);
+		abort_if(!config_cache('pixelfed.oauth_enabled'), 404);
 
 		$this->validate($request, [
 			'client_name' 		=> 'required',
@@ -960,31 +960,31 @@ class ApiV1Controller extends Controller
         $res = [
         	'approval_required' => false,
         	'contact_account' => null,
-            'description' => config('instance.description'),
+            'description' => config_cache('app.description'),
             'email' => config('instance.email'),
             'invites_enabled' => false,
             'rules' => [],
             'short_description' => 'Pixelfed - Photo sharing for everyone',
             'languages' => ['en'],
             'max_toot_chars' => (int) config('pixelfed.max_caption_length'),
-            'registrations' => config('pixelfed.open_registration'),
+            'registrations' => config_cache('pixelfed.open_registration'),
             'stats' => [
                 'user_count' => 0,
                 'status_count' => 0,
                 'domain_count' => 0
             ],
             'thumbnail' => config('app.url') . '/img/pixelfed-icon-color.png',
-            'title' => config('app.name'),
+            'title' => config_cache('app.name'),
             'uri' => config('pixelfed.domain.app'),
             'urls' => [],
             'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') . ')',
             'environment' => [
-                'max_photo_size' => (int) config('pixelfed.max_photo_size'),
+                'max_photo_size' => (int) config_cache('pixelfed.max_photo_size'),
                 'max_avatar_size' => (int) config('pixelfed.max_avatar_size'),
                 'max_caption_length' => (int) config('pixelfed.max_caption_length'),
                 'max_bio_length' => (int) config('pixelfed.max_bio_length'),
-                'max_album_length' => (int) config('pixelfed.max_album_length'),
-                'mobile_apis' => config('pixelfed.oauth_enabled')
+                'max_album_length' => (int) config_cache('pixelfed.max_album_length'),
+                'mobile_apis' => config_cache('pixelfed.oauth_enabled')
 
             ]
         ];
@@ -1033,8 +1033,8 @@ class ApiV1Controller extends Controller
           'file.*'      => function() {
             return [
                 'required',
-                'mimes:' . config('pixelfed.media_types'),
-                'max:' . config('pixelfed.max_photo_size'),
+                'mimes:' . config_cache('pixelfed.media_types'),
+                'max:' . config_cache('pixelfed.max_photo_size'),
             ];
           },
           'filter_name' => 'nullable|string|max:24',
@@ -1059,11 +1059,11 @@ class ApiV1Controller extends Controller
 
         $profile = $user->profile;
 
-        if(config('pixelfed.enforce_account_limit') == true) {
+        if(config_cache('pixelfed.enforce_account_limit') == true) {
             $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
                 return Media::whereUserId($user->id)->sum('size') / 1000;
             });
-            $limit = (int) config('pixelfed.max_account_size');
+            $limit = (int) config_cache('pixelfed.max_account_size');
             if ($size >= $limit) {
                abort(403, 'Account size limit reached.');
             }
@@ -1074,7 +1074,7 @@ class ApiV1Controller extends Controller
 
         $photo = $request->file('file');
 
-        $mimes = explode(',', config('pixelfed.media_types'));
+        $mimes = explode(',', config_cache('pixelfed.media_types'));
         if(in_array($photo->getMimeType(), $mimes) == false) {
             abort(403, 'Invalid or unsupported mime type.');
         }
@@ -1742,7 +1742,7 @@ class ApiV1Controller extends Controller
         $this->validate($request, [
             'status' => 'nullable|string',
             'in_reply_to_id' => 'nullable|integer',
-            'media_ids' => 'array|max:' . config('pixelfed.max_album_length'),
+            'media_ids' => 'array|max:' . config_cache('pixelfed.max_album_length'),
             'media_ids.*' => 'integer|min:1',
             'sensitive' => 'nullable|boolean',
             'visibility' => 'string|in:private,unlisted,public',
@@ -1824,7 +1824,7 @@ class ApiV1Controller extends Controller
             $mimes = [];
 
             foreach($ids as $k => $v) {
-                if($k + 1 > config('pixelfed.max_album_length')) {
+                if($k + 1 > config_cache('pixelfed.max_album_length')) {
                     continue;
                 }
                 $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v);

+ 1 - 1
app/Http/Controllers/Api/InstanceApiController.php

@@ -34,7 +34,7 @@ class InstanceApiController extends Controller {
 
 		$res = [
 			'uri' => config('pixelfed.domain.app'),
-			'title' => config('app.name'),
+			'title' => config_cache('app.name'),
 			'description' => '',
 			'version' => config('pixelfed.version'),
 			'urls' => [],

+ 179 - 179
app/Http/Controllers/Auth/RegisterController.php

@@ -14,183 +14,183 @@ use App\Services\EmailService;
 
 class RegisterController extends Controller
 {
-    /*
-    |--------------------------------------------------------------------------
-    | Register Controller
-    |--------------------------------------------------------------------------
-    |
-    | This controller handles the registration of new users as well as their
-    | validation and creation. By default this controller uses a trait to
-    | provide this functionality without requiring any additional code.
-    |
-    */
-
-    use RegistersUsers;
-
-    /**
-     * Where to redirect users after registration.
-     *
-     * @var string
-     */
-    protected $redirectTo = '/';
-
-    /**
-     * Create a new controller instance.
-     *
-     * @return void
-     */
-    public function __construct()
-    {
-        $this->middleware('guest');
-    }
-
-    /**
-     * Get a validator for an incoming registration request.
-     *
-     * @param array $data
-     *
-     * @return \Illuminate\Contracts\Validation\Validator
-     */
-    protected function validator(array $data)
-    {
-        if(config('database.default') == 'pgsql') {
-            $data['username'] = strtolower($data['username']);
-            $data['email'] = strtolower($data['email']);
-        }
-
-        $usernameRules = [
-            'required',
-            'min:2',
-            'max:15',
-            'unique:users',
-            function ($attribute, $value, $fail) {
-                $dash = substr_count($value, '-');
-                $underscore = substr_count($value, '_');
-                $period = substr_count($value, '.');
-
-                if(ends_with($value, ['.php', '.js', '.css'])) {
-                    return $fail('Username is invalid.');
-                }
-
-                if(($dash + $underscore + $period) > 1) {
-                    return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
-                }
-
-                if (!ctype_alpha($value[0])) {
-                    return $fail('Username is invalid. Must start with a letter or number.');
-                }
-
-                if (!ctype_alnum($value[strlen($value) - 1])) {
-                    return $fail('Username is invalid. Must end with a letter or number.');
-                }
-
-                $val = str_replace(['_', '.', '-'], '', $value);
-                if(!ctype_alnum($val)) {
-                    return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
-                }
-
-                $restricted = RestrictedNames::get();
-                if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
-                    return $fail('Username cannot be used.');
-                }
-            },
-        ];
-
-        $emailRules = [
-            'required',
-            'string',
-            'email',
-            'max:255',
-            'unique:users',
-            function ($attribute, $value, $fail) {
-                $banned = EmailService::isBanned($value);
-                if($banned) {
-                    return $fail('Email is invalid.');
-                }
-            },
-        ];
-
-        $rules = [
-            'agecheck' => 'required|accepted',
-            'name'     => 'nullable|string|max:'.config('pixelfed.max_name_length'),
-            'username' => $usernameRules,
-            'email'    => $emailRules,
-            'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
-        ];
-
-        if(config('captcha.enabled')) {
-            $rules['h-captcha-response'] = 'required|captcha';
-        }
-
-        return Validator::make($data, $rules);
-    }
-
-    /**
-     * Create a new user instance after a valid registration.
-     *
-     * @param array $data
-     *
-     * @return \App\User
-     */
-    protected function create(array $data)
-    {
-        if(config('database.default') == 'pgsql') {
-            $data['username'] = strtolower($data['username']);
-            $data['email'] = strtolower($data['email']);
-        }
-
-        return User::create([
-            'name'     => $data['name'],
-            'username' => $data['username'],
-            'email'    => $data['email'],
-            'password' => Hash::make($data['password']),
-        ]);
-    }
-
-    /**
-     * Show the application registration form.
-     *
-     * @return \Illuminate\Http\Response
-     */
-    public function showRegistrationForm()
-    {
-        if(config('pixelfed.open_registration')) {
-            $limit = config('pixelfed.max_users');
-            if($limit) {
-                abort_if($limit <= User::count(), 404);
-                return view('auth.register');
-            } else {
-                return view('auth.register');
-            }
-        } else {
-            abort(404);
-        }
-    }
-
-    /**
-     * Handle a registration request for the application.
-     *
-     * @param  \Illuminate\Http\Request  $request
-     * @return \Illuminate\Http\Response
-     */
-    public function register(Request $request)
-    {
-        abort_if(config('pixelfed.open_registration') == false, 400);
-
-        $count = User::count();
-        $limit = config('pixelfed.max_users');
-
-        if(false == config('pixelfed.open_registration') || $limit && $limit <= $count) {
-            return abort(403);
-        }
-
-        $this->validator($request->all())->validate();
-
-        event(new Registered($user = $this->create($request->all())));
-
-        $this->guard()->login($user);
-
-        return $this->registered($request, $user)
-            ?: redirect($this->redirectPath());
-    }
+	/*
+	|--------------------------------------------------------------------------
+	| Register Controller
+	|--------------------------------------------------------------------------
+	|
+	| This controller handles the registration of new users as well as their
+	| validation and creation. By default this controller uses a trait to
+	| provide this functionality without requiring any additional code.
+	|
+	*/
+
+	use RegistersUsers;
+
+	/**
+	 * Where to redirect users after registration.
+	 *
+	 * @var string
+	 */
+	protected $redirectTo = '/';
+
+	/**
+	 * Create a new controller instance.
+	 *
+	 * @return void
+	 */
+	public function __construct()
+	{
+		$this->middleware('guest');
+	}
+
+	/**
+	 * Get a validator for an incoming registration request.
+	 *
+	 * @param array $data
+	 *
+	 * @return \Illuminate\Contracts\Validation\Validator
+	 */
+	protected function validator(array $data)
+	{
+		if(config('database.default') == 'pgsql') {
+			$data['username'] = strtolower($data['username']);
+			$data['email'] = strtolower($data['email']);
+		}
+
+		$usernameRules = [
+			'required',
+			'min:2',
+			'max:15',
+			'unique:users',
+			function ($attribute, $value, $fail) {
+				$dash = substr_count($value, '-');
+				$underscore = substr_count($value, '_');
+				$period = substr_count($value, '.');
+
+				if(ends_with($value, ['.php', '.js', '.css'])) {
+					return $fail('Username is invalid.');
+				}
+
+				if(($dash + $underscore + $period) > 1) {
+					return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+				}
+
+				if (!ctype_alpha($value[0])) {
+					return $fail('Username is invalid. Must start with a letter or number.');
+				}
+
+				if (!ctype_alnum($value[strlen($value) - 1])) {
+					return $fail('Username is invalid. Must end with a letter or number.');
+				}
+
+				$val = str_replace(['_', '.', '-'], '', $value);
+				if(!ctype_alnum($val)) {
+					return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+				}
+
+				$restricted = RestrictedNames::get();
+				if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+					return $fail('Username cannot be used.');
+				}
+			},
+		];
+
+		$emailRules = [
+			'required',
+			'string',
+			'email',
+			'max:255',
+			'unique:users',
+			function ($attribute, $value, $fail) {
+				$banned = EmailService::isBanned($value);
+				if($banned) {
+					return $fail('Email is invalid.');
+				}
+			},
+		];
+
+		$rules = [
+			'agecheck' => 'required|accepted',
+			'name'     => 'nullable|string|max:'.config('pixelfed.max_name_length'),
+			'username' => $usernameRules,
+			'email'    => $emailRules,
+			'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
+		];
+
+		if(config('captcha.enabled')) {
+			$rules['h-captcha-response'] = 'required|captcha';
+		}
+
+		return Validator::make($data, $rules);
+	}
+
+	/**
+	 * Create a new user instance after a valid registration.
+	 *
+	 * @param array $data
+	 *
+	 * @return \App\User
+	 */
+	protected function create(array $data)
+	{
+		if(config('database.default') == 'pgsql') {
+			$data['username'] = strtolower($data['username']);
+			$data['email'] = strtolower($data['email']);
+		}
+
+		return User::create([
+			'name'     => $data['name'],
+			'username' => $data['username'],
+			'email'    => $data['email'],
+			'password' => Hash::make($data['password']),
+		]);
+	}
+
+	/**
+	 * Show the application registration form.
+	 *
+	 * @return \Illuminate\Http\Response
+	 */
+	public function showRegistrationForm()
+	{
+		if(config_cache('pixelfed.open_registration')) {
+			$limit = config('pixelfed.max_users');
+			if($limit) {
+				abort_if($limit <= User::count(), 404);
+				return view('auth.register');
+			} else {
+				return view('auth.register');
+			}
+		} else {
+			abort(404);
+		}
+	}
+
+	/**
+	 * Handle a registration request for the application.
+	 *
+	 * @param  \Illuminate\Http\Request  $request
+	 * @return \Illuminate\Http\Response
+	 */
+	public function register(Request $request)
+	{
+		abort_if(config_cache('pixelfed.open_registration') == false, 400);
+
+		$count = User::count();
+		$limit = config('pixelfed.max_users');
+
+		if(false == config_cache('pixelfed.open_registration') || $limit && $limit <= $count) {
+			return abort(403);
+		}
+
+		$this->validator($request->all())->validate();
+
+		event(new Registered($user = $this->create($request->all())));
+
+		$this->guard()->login($user);
+
+		return $this->registered($request, $user)
+			?: redirect($this->redirectPath());
+	}
 }

+ 11 - 11
app/Http/Controllers/ComposeController.php

@@ -71,8 +71,8 @@ class ComposeController extends Controller
 			'file.*' => function() {
 				return [
 					'required',
-					'mimes:' . config('pixelfed.media_types'),
-					'max:' . config('pixelfed.max_photo_size'),
+					'mimes:' . config_cache('pixelfed.media_types'),
+					'max:' . config_cache('pixelfed.max_photo_size'),
 				];
 			},
 			'filter_name' => 'nullable|string|max:24',
@@ -92,11 +92,11 @@ class ComposeController extends Controller
 
 		abort_if($limitReached == true, 429);
 
-		if(config('pixelfed.enforce_account_limit') == true) {
+		if(config_cache('pixelfed.enforce_account_limit') == true) {
 			$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
 				return Media::whereUserId($user->id)->sum('size') / 1000;
-			}); 
-			$limit = (int) config('pixelfed.max_account_size');
+			});
+			$limit = (int) config_cache('pixelfed.max_account_size');
 			if ($size >= $limit) {
 				abort(403, 'Account size limit reached.');
 			}
@@ -107,7 +107,7 @@ class ComposeController extends Controller
 
 		$photo = $request->file('file');
 
-		$mimes = explode(',', config('pixelfed.media_types'));
+		$mimes = explode(',', config_cache('pixelfed.media_types'));
 
 		abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
 
@@ -132,7 +132,7 @@ class ComposeController extends Controller
 
 		$preview_url = $media->url() . '?v=' . time();
 		$url = $media->url() . '?v=' . time();
-		
+
 		switch ($media->mime) {
 			case 'image/jpeg':
 			case 'image/png':
@@ -164,8 +164,8 @@ class ComposeController extends Controller
 			'file' => function() {
 				return [
 					'required',
-					'mimes:' . config('pixelfed.media_types'),
-					'max:' . config('pixelfed.max_photo_size'),
+					'mimes:' . config_cache('pixelfed.media_types'),
+					'max:' . config_cache('pixelfed.max_photo_size'),
 				];
 			},
 		]);
@@ -454,7 +454,7 @@ class ComposeController extends Controller
 		$optimize_media = (bool) $request->input('optimize_media');
 
 		foreach($medias as $k => $media) {
-			if($k + 1 > config('pixelfed.max_album_length')) {
+			if($k + 1 > config_cache('pixelfed.max_album_length')) {
 				continue;
 			}
 			$m = Media::findOrFail($media['id']);
@@ -648,7 +648,7 @@ class ComposeController extends Controller
 			case 'video/mp4':
 				$finished = config('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
 				break;
-			
+
 			default:
 				# code...
 				break;

+ 8 - 8
app/Http/Controllers/DirectMessageController.php

@@ -160,7 +160,7 @@ class DirectMessageController extends Controller
 						'messages' => []
 					];
 				});
-			} 
+			}
 		} elseif(config('database.default') == 'mysql') {
 			if($action == 'inbox') {
 				$dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
@@ -334,7 +334,7 @@ class DirectMessageController extends Controller
 				$dm->type = 'link';
 				$dm->meta = [
 					'domain' => parse_url($msg, PHP_URL_HOST),
-					'local' => parse_url($msg, PHP_URL_HOST) == 
+					'local' => parse_url($msg, PHP_URL_HOST) ==
 					parse_url(config('app.url'), PHP_URL_HOST)
 				];
 				$dm->save();
@@ -500,8 +500,8 @@ class DirectMessageController extends Controller
 			'file'      => function() {
 				return [
 					'required',
-					'mimes:' . config('pixelfed.media_types'),
-					'max:' . config('pixelfed.max_photo_size'),
+					'mimes:' . config_cache('pixelfed.media_types'),
+					'max:' . config_cache('pixelfed.max_photo_size'),
 				];
 			},
 			'to_id'     => 'required'
@@ -522,18 +522,18 @@ class DirectMessageController extends Controller
 			$hidden = false;
 		}
 
-		if(config('pixelfed.enforce_account_limit') == true) {
+		if(config_cache('pixelfed.enforce_account_limit') == true) {
 			$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
 				return Media::whereUserId($user->id)->sum('size') / 1000;
-			}); 
-			$limit = (int) config('pixelfed.max_account_size');
+			});
+			$limit = (int) config_cache('pixelfed.max_account_size');
 			if ($size >= $limit) {
 				abort(403, 'Account size limit reached.');
 			}
 		}
 		$photo = $request->file('file');
 
-		$mimes = explode(',', config('pixelfed.media_types'));
+		$mimes = explode(',', config_cache('pixelfed.media_types'));
 		if(in_array($photo->getMimeType(), $mimes) == false) {
 			abort(403, 'Invalid or unsupported mime type.');
 		}

+ 8 - 8
app/Http/Controllers/FederationController.php

@@ -79,7 +79,7 @@ class FederationController extends Controller
 
     public function userOutbox(Request $request, $username)
     {
-        abort_if(!config('federation.activitypub.enabled'), 404);
+        abort_if(!config_cache('federation.activitypub.enabled'), 404);
         abort_if(!config('federation.activitypub.outbox'), 404);
 
         $profile = Profile::whereNull('domain')
@@ -99,7 +99,7 @@ class FederationController extends Controller
 
     public function userInbox(Request $request, $username)
     {
-        abort_if(!config('federation.activitypub.enabled'), 404);
+        abort_if(!config_cache('federation.activitypub.enabled'), 404);
         abort_if(!config('federation.activitypub.inbox'), 404);
 
         $headers = $request->headers->all();
@@ -110,7 +110,7 @@ class FederationController extends Controller
 
     public function sharedInbox(Request $request)
     {
-        abort_if(!config('federation.activitypub.enabled'), 404);
+        abort_if(!config_cache('federation.activitypub.enabled'), 404);
         abort_if(!config('federation.activitypub.sharedInbox'), 404);
 
         $headers = $request->headers->all();
@@ -121,13 +121,13 @@ class FederationController extends Controller
 
     public function userFollowing(Request $request, $username)
     {
-        abort_if(!config('federation.activitypub.enabled'), 404);
+        abort_if(!config_cache('federation.activitypub.enabled'), 404);
 
         $profile = Profile::whereNull('remote_url')
             ->whereUsername($username)
             ->whereIsPrivate(false)
             ->firstOrFail();
-            
+
         if($profile->status != null) {
             abort(404);
         }
@@ -139,12 +139,12 @@ class FederationController extends Controller
             'totalItems' => 0,
             'orderedItems' => []
         ];
-        return response()->json($obj); 
+        return response()->json($obj);
     }
 
     public function userFollowers(Request $request, $username)
     {
-        abort_if(!config('federation.activitypub.enabled'), 404);
+        abort_if(!config_cache('federation.activitypub.enabled'), 404);
 
         $profile = Profile::whereNull('remote_url')
             ->whereUsername($username)
@@ -163,6 +163,6 @@ class FederationController extends Controller
             'orderedItems' => []
         ];
 
-        return response()->json($obj); 
+        return response()->json($obj);
     }
 }

+ 1 - 1
app/Http/Controllers/ImportController.php

@@ -12,7 +12,7 @@ class ImportController extends Controller
 	{
 		$this->middleware('auth');
 
-		if(config('pixelfed.import.instagram.enabled') != true) {
+		if(config_cache('pixelfed.import.instagram.enabled') != true) {
 			abort(404, 'Feature not enabled');
 		}
 	}

+ 225 - 225
app/Http/Controllers/ProfileController.php

@@ -20,229 +20,229 @@ use App\Transformer\ActivityPub\ProfileTransformer;
 
 class ProfileController extends Controller
 {
-    public function show(Request $request, $username)
-    {
-        $user = Profile::whereNull('domain')
-            ->whereNull('status')
-            ->whereUsername($username)
-            ->firstOrFail();
-        
-        if($request->wantsJson() && config('federation.activitypub.enabled')) {
-            return $this->showActivityPub($request, $user);
-        }
-        return $this->buildProfile($request, $user);
-    }
-
-    protected function buildProfile(Request $request, $user)
-    {
-        $username = $user->username;
-        $loggedIn = Auth::check();
-        $isPrivate = false;
-        $isBlocked = false;
-        if(!$loggedIn) {
-            $key = 'profile:settings:' . $user->id;
-            $ttl = now()->addHours(6);
-            $settings = Cache::remember($key, $ttl, function() use($user) {
-                return $user->user->settings;
-            });
-
-            if ($user->is_private == true) {
-                abort(404);
-            }
-
-            $owner = false;
-            $is_following = false;
-
-            $is_admin = $user->user->is_admin;
-            $profile = $user;
-            $settings = [
-                'crawlable' => $settings->crawlable,
-                'following' => [
-                    'count' => $settings->show_profile_following_count,
-                    'list' => $settings->show_profile_following
-                ], 
-                'followers' => [
-                    'count' => $settings->show_profile_follower_count,
-                    'list' => $settings->show_profile_followers
-                ]
-            ];
-            $ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
-
-            return view($ui, compact('profile', 'settings'));
-        } else {
-            $key = 'profile:settings:' . $user->id;
-            $ttl = now()->addHours(6);
-            $settings = Cache::remember($key, $ttl, function() use($user) {
-                return $user->user->settings;
-            });
-
-            if ($user->is_private == true) {
-                $isPrivate = $this->privateProfileCheck($user, $loggedIn);
-            }
-
-            $isBlocked = $this->blockedProfileCheck($user);
-
-            $owner = $loggedIn && Auth::id() === $user->user_id;
-            $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
-
-            if ($isPrivate == true || $isBlocked == true) {
-                $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
-                    ->whereFollowingId($user->id)
-                    ->exists() : false;
-                return view('profile.private', compact('user', 'is_following', 'requested'));
-            } 
-
-            $is_admin = is_null($user->domain) ? $user->user->is_admin : false;
-            $profile = $user;
-            $settings = [
-                'crawlable' => $settings->crawlable,
-                'following' => [
-                    'count' => $settings->show_profile_following_count,
-                    'list' => $settings->show_profile_following
-                ], 
-                'followers' => [
-                    'count' => $settings->show_profile_follower_count,
-                    'list' => $settings->show_profile_followers
-                ]
-            ];
-            $ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
-            return view($ui, compact('profile', 'settings'));
-        }
-    }
-
-    public function permalinkRedirect(Request $request, $username)
-    {
-        $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
-
-        if ($request->wantsJson() && config('federation.activitypub.enabled')) {
-            return $this->showActivityPub($request, $user);
-        }
-
-        return redirect($user->url());
-    }
-
-    protected function privateProfileCheck(Profile $profile, $loggedIn)
-    {
-        if (!Auth::check()) {
-            return true;
-        }
-
-        $user = Auth::user()->profile;
-        if($user->id == $profile->id || !$profile->is_private) {
-            return false;
-        }
-
-        $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
-        if ($follows == false) {
-            return true;
-        }
-        
-        return false;
-    }
-
-    public static function accountCheck(Profile $profile)   
-    {   
-        switch ($profile->status) { 
-            case 'disabled':    
-            case 'suspended':   
-            case 'delete':  
-                return view('profile.disabled');    
-                break;  
-                
-            default:    
-                break;  
-        }   
-        return abort(404);  
-    }
-
-    protected function blockedProfileCheck(Profile $profile)
-    {
-        $pid = Auth::user()->profile->id;
-        $blocks = UserFilter::whereUserId($profile->id)
-                ->whereFilterType('block')
-                ->whereFilterableType('App\Profile')
-                ->pluck('filterable_id')
-                ->toArray();
-        if (in_array($pid, $blocks)) {
-            return true;
-        }
-
-        return false;
-    }
-
-    public function showActivityPub(Request $request, $user)
-    {
-        abort_if(!config('federation.activitypub.enabled'), 404);
-        abort_if($user->domain, 404);
-
-        $fractal = new Fractal\Manager();
-        $resource = new Fractal\Resource\Item($user, new ProfileTransformer);
-        $res = $fractal->createData($resource)->toArray();
-        return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
-    }
-
-    public function showAtomFeed(Request $request, $user)
-    {
-        abort_if(!config('federation.atom.enabled'), 404);
-
-        $profile = $user = Profile::whereNull('status')->whereNull('domain')->whereUsername($user)->whereIsPrivate(false)->firstOrFail();
-        if($profile->status != null) {
-            return $this->accountCheck($profile);
-        }
-        if($profile->is_private || Auth::check()) {
-            $blocked = $this->blockedProfileCheck($profile);
-            $check = $this->privateProfileCheck($profile, null);
-            if($check || $blocked) {
-                return redirect($profile->url());
-            }
-        }
-        $items = $profile->statuses()->whereHas('media')->whereIn('visibility',['public', 'unlisted'])->orderBy('created_at', 'desc')->take(10)->get();
-        return response()->view('atom.user', compact('profile', 'items'))
-        ->header('Content-Type', 'application/atom+xml');
-    }
-
-    public function meRedirect()
-    {
-        abort_if(!Auth::check(), 404);
-        return redirect(Auth::user()->url());
-    }
-
-    public function embed(Request $request, $username)
-    {
-        $res = view('profile.embed-removed');
-
-        if(strlen($username) > 15 || strlen($username) < 2) {
-            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-        }
-
-        $profile = Profile::whereUsername($username)
-            ->whereIsPrivate(false)
-            ->whereNull('status')
-            ->whereNull('domain')
-            ->first();
-
-        if(!$profile) {
-            return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-        }
-
-        $content = Cache::remember('profile:embed:'.$profile->id, now()->addHours(12), function() use($profile) {
-            return View::make('profile.embed')->with(compact('profile'))->render();
-        });
-        
-        return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
-    }
-
-    public function stories(Request $request, $username)
-    {
-        abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
-        $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
-        $pid = $profile->id;
-        $authed = Auth::user()->profile;
-        abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404);
-        $exists = Story::whereProfileId($pid)
-            ->where('expires_at', '>', now())
-            ->count();
-        abort_unless($exists > 0, 404);
-        return view('profile.story', compact('pid', 'profile'));
-    }
+	public function show(Request $request, $username)
+	{
+		$user = Profile::whereNull('domain')
+			->whereNull('status')
+			->whereUsername($username)
+			->firstOrFail();
+
+		if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
+			return $this->showActivityPub($request, $user);
+		}
+		return $this->buildProfile($request, $user);
+	}
+
+	protected function buildProfile(Request $request, $user)
+	{
+		$username = $user->username;
+		$loggedIn = Auth::check();
+		$isPrivate = false;
+		$isBlocked = false;
+		if(!$loggedIn) {
+			$key = 'profile:settings:' . $user->id;
+			$ttl = now()->addHours(6);
+			$settings = Cache::remember($key, $ttl, function() use($user) {
+				return $user->user->settings;
+			});
+
+			if ($user->is_private == true) {
+				abort(404);
+			}
+
+			$owner = false;
+			$is_following = false;
+
+			$is_admin = $user->user->is_admin;
+			$profile = $user;
+			$settings = [
+				'crawlable' => $settings->crawlable,
+				'following' => [
+					'count' => $settings->show_profile_following_count,
+					'list' => $settings->show_profile_following
+				],
+				'followers' => [
+					'count' => $settings->show_profile_follower_count,
+					'list' => $settings->show_profile_followers
+				]
+			];
+			$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
+
+			return view($ui, compact('profile', 'settings'));
+		} else {
+			$key = 'profile:settings:' . $user->id;
+			$ttl = now()->addHours(6);
+			$settings = Cache::remember($key, $ttl, function() use($user) {
+				return $user->user->settings;
+			});
+
+			if ($user->is_private == true) {
+				$isPrivate = $this->privateProfileCheck($user, $loggedIn);
+			}
+
+			$isBlocked = $this->blockedProfileCheck($user);
+
+			$owner = $loggedIn && Auth::id() === $user->user_id;
+			$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
+
+			if ($isPrivate == true || $isBlocked == true) {
+				$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
+					->whereFollowingId($user->id)
+					->exists() : false;
+				return view('profile.private', compact('user', 'is_following', 'requested'));
+			}
+
+			$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
+			$profile = $user;
+			$settings = [
+				'crawlable' => $settings->crawlable,
+				'following' => [
+					'count' => $settings->show_profile_following_count,
+					'list' => $settings->show_profile_following
+				],
+				'followers' => [
+					'count' => $settings->show_profile_follower_count,
+					'list' => $settings->show_profile_followers
+				]
+			];
+			$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
+			return view($ui, compact('profile', 'settings'));
+		}
+	}
+
+	public function permalinkRedirect(Request $request, $username)
+	{
+		$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
+
+		if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
+			return $this->showActivityPub($request, $user);
+		}
+
+		return redirect($user->url());
+	}
+
+	protected function privateProfileCheck(Profile $profile, $loggedIn)
+	{
+		if (!Auth::check()) {
+			return true;
+		}
+
+		$user = Auth::user()->profile;
+		if($user->id == $profile->id || !$profile->is_private) {
+			return false;
+		}
+
+		$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
+		if ($follows == false) {
+			return true;
+		}
+
+		return false;
+	}
+
+	public static function accountCheck(Profile $profile)
+	{
+		switch ($profile->status) {
+			case 'disabled':
+			case 'suspended':
+			case 'delete':
+				return view('profile.disabled');
+				break;
+
+			default:
+				break;
+		}
+		return abort(404);
+	}
+
+	protected function blockedProfileCheck(Profile $profile)
+	{
+		$pid = Auth::user()->profile->id;
+		$blocks = UserFilter::whereUserId($profile->id)
+				->whereFilterType('block')
+				->whereFilterableType('App\Profile')
+				->pluck('filterable_id')
+				->toArray();
+		if (in_array($pid, $blocks)) {
+			return true;
+		}
+
+		return false;
+	}
+
+	public function showActivityPub(Request $request, $user)
+	{
+		abort_if(!config_cache('federation.activitypub.enabled'), 404);
+		abort_if($user->domain, 404);
+
+		$fractal = new Fractal\Manager();
+		$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
+		$res = $fractal->createData($resource)->toArray();
+		return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
+	}
+
+	public function showAtomFeed(Request $request, $user)
+	{
+		abort_if(!config('federation.atom.enabled'), 404);
+
+		$profile = $user = Profile::whereNull('status')->whereNull('domain')->whereUsername($user)->whereIsPrivate(false)->firstOrFail();
+		if($profile->status != null) {
+			return $this->accountCheck($profile);
+		}
+		if($profile->is_private || Auth::check()) {
+			$blocked = $this->blockedProfileCheck($profile);
+			$check = $this->privateProfileCheck($profile, null);
+			if($check || $blocked) {
+				return redirect($profile->url());
+			}
+		}
+		$items = $profile->statuses()->whereHas('media')->whereIn('visibility',['public', 'unlisted'])->orderBy('created_at', 'desc')->take(10)->get();
+		return response()->view('atom.user', compact('profile', 'items'))
+		->header('Content-Type', 'application/atom+xml');
+	}
+
+	public function meRedirect()
+	{
+		abort_if(!Auth::check(), 404);
+		return redirect(Auth::user()->url());
+	}
+
+	public function embed(Request $request, $username)
+	{
+		$res = view('profile.embed-removed');
+
+		if(strlen($username) > 15 || strlen($username) < 2) {
+			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+		}
+
+		$profile = Profile::whereUsername($username)
+			->whereIsPrivate(false)
+			->whereNull('status')
+			->whereNull('domain')
+			->first();
+
+		if(!$profile) {
+			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+		}
+
+		$content = Cache::remember('profile:embed:'.$profile->id, now()->addHours(12), function() use($profile) {
+			return View::make('profile.embed')->with(compact('profile'))->render();
+		});
+
+		return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
+	}
+
+	public function stories(Request $request, $username)
+	{
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+		$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
+		$pid = $profile->id;
+		$authed = Auth::user()->profile;
+		abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404);
+		$exists = Story::whereProfileId($pid)
+			->where('expires_at', '>', now())
+			->count();
+		abort_unless($exists > 0, 404);
+		return view('profile.story', compact('pid', 'profile'));
+	}
 }

+ 2 - 2
app/Http/Controllers/PublicApiController.php

@@ -314,7 +314,7 @@ class PublicApiController extends Controller
                       ->whereNotIn('profile_id', $filtered)
                       ->whereLocal(true)
                       ->whereScope('public')
-                      ->where('created_at', '>', now()->subMonths(3))
+                      ->where('created_at', '>', now()->subMonths(6))
                       ->orderBy('created_at', 'desc')
                       ->limit($limit)
                       ->get();
@@ -343,7 +343,7 @@ class PublicApiController extends Controller
                       ->with('profile', 'hashtags', 'mentions')
                       ->whereLocal(true)
                       ->whereScope('public')
-                      ->where('created_at', '>', now()->subMonths(3))
+                      ->where('created_at', '>', now()->subMonths(6))
                       ->orderBy('created_at', 'desc')
                       ->simplePaginate($limit);
         }

+ 340 - 340
app/Http/Controllers/SearchController.php

@@ -12,348 +12,348 @@ use App\Util\ActivityPub\Helpers;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Str;
 use App\Transformer\Api\{
-    AccountTransformer,
-    HashtagTransformer,
-    StatusTransformer,
+	AccountTransformer,
+	HashtagTransformer,
+	StatusTransformer,
 };
 use App\Services\WebfingerService;
 
 class SearchController extends Controller
 {
-    public $tokens = [];
-    public $term = '';
-    public $hash = '';
-    public $cacheKey = 'api:search:tag:';
-
-    public function __construct()
-    {
-        $this->middleware('auth');
-    }
-
-    public function searchAPI(Request $request)
-    {
-        $this->validate($request, [
-            'q' => 'required|string|min:3|max:120',
-            'src' => 'required|string|in:metro',
-            'v' => 'required|integer|in:2',
-            'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
-        ]);
-
-        $scope = $request->input('scope') ?? 'all';
-        $this->term = e(urldecode($request->input('q')));
-        $this->hash = hash('sha256', $this->term);
-
-        switch ($scope) {
-            case 'all':
-                $this->getHashtags();
-                $this->getPosts();
-                $this->getProfiles();
-                // $this->getPlaces();
-                break;
-
-            case 'hashtag':
-                $this->getHashtags();
-                break;
-
-            case 'profile':
-                $this->getProfiles();
-                break;
-
-            case 'webfinger':
-                $this->webfingerSearch();
-                break;
-
-            case 'remote':
-                $this->remoteLookupSearch();
-                break;
-
-            case 'place':
-                $this->getPlaces();
-                break;
-
-            default:
-                break;
-        }
-
-        return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
-    }
-
-    protected function getPosts()
-    {
-        $tag = $this->term;
-        $hash = hash('sha256', $tag);
-        if( Helpers::validateUrl($tag) != false && 
-            Helpers::validateLocalUrl($tag) != true && 
-            config('federation.activitypub.enabled') == true && 
-            config('federation.activitypub.remoteFollow') == true
-        ) {
-            $remote = Helpers::fetchFromUrl($tag);
-            if( isset($remote['type']) && 
-                $remote['type'] == 'Note') {
-                $item = Helpers::statusFetch($tag);
-                $this->tokens['posts'] = [[
-                    'count'  => 0,
-                    'url'    => $item->url(),
-                    'type'   => 'status',
-                    'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
-                    'tokens' => [$item->caption],
-                    'name'   => $item->caption,
-                    'thumb'  => $item->thumb(),
-                ]];
-            }
-        } else {
-            $posts = Status::select('id', 'profile_id', 'caption', 'created_at')
-                        ->whereHas('media')
-                        ->whereNull('in_reply_to_id')
-                        ->whereNull('reblog_of_id')
-                        ->whereProfileId(Auth::user()->profile_id)
-                        ->where('caption', 'like', '%'.$tag.'%')
-                        ->latest()
-                        ->limit(10)
-                        ->get();
-
-            if($posts->count() > 0) {
-                $posts = $posts->map(function($item, $key) {
-                    return [
-                        'count'  => 0,
-                        'url'    => $item->url(),
-                        'type'   => 'status',
-                        'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
-                        'tokens' => [$item->caption],
-                        'name'   => $item->caption,
-                        'thumb'  => $item->thumb(),
-                        'filter' => $item->firstMedia()->filter_class
-                    ];
-                });
-                $this->tokens['posts'] = $posts;
-            }
-        }
-    }
-
-    protected function getHashtags()
-    {
-        $tag = $this->term;
-        $key = $this->cacheKey . 'hashtags:' . $this->hash;
-        $ttl = now()->addMinutes(1);
-        $tokens = Cache::remember($key, $ttl, function() use($tag) {
-            $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
-            $hashtags = Hashtag::select('id', 'name', 'slug')
-                ->where('slug', 'like', '%'.$htag.'%')
-                ->whereHas('posts')
-                ->limit(20)
-                ->get();
-            if($hashtags->count() > 0) {
-                $tags = $hashtags->map(function ($item, $key) {
-                    return [
-                        'count'  => $item->posts()->count(),
-                        'url'    => $item->url(),
-                        'type'   => 'hashtag',
-                        'value'  => $item->name,
-                        'tokens' => '',
-                        'name'   => null,
-                    ];
-                });
-                return $tags;
-            }
-        });
-        $this->tokens['hashtags'] = $tokens;
-    }
-
-    protected function getPlaces()
-    {
-        $tag = $this->term;
-        // $key = $this->cacheKey . 'places:' . $this->hash;
-        // $ttl = now()->addHours(12);
-        // $tokens = Cache::remember($key, $ttl, function() use($tag) {
-            $htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
-            $hashtags = Place::select('id', 'name', 'slug', 'country')
-                ->where('name', 'like', '%'.$htag[0].'%')
-                ->paginate(20);
-            $tags = [];
-            if($hashtags->count() > 0) {
-                $tags = $hashtags->map(function ($item, $key) {
-                    return [
-                        'count'     => null,
-                        'url'       => $item->url(),
-                        'type'      => 'place',
-                        'value'     => $item->name . ', ' . $item->country,
-                        'tokens'    => '',
-                        'name'      => null,
-                        'city'      => $item->name,
-                        'country'   => $item->country
-                    ];
-                });
-                // return $tags;
-            }
-        // });
-        $this->tokens['places'] = $tags;
-        $this->tokens['placesPagination'] = [
-            'total' => $hashtags->total(),
-            'current_page' => $hashtags->currentPage(),
-            'last_page' => $hashtags->lastPage()
-        ];
-    }
-
-    protected function getProfiles()
-    {
-        $tag = $this->term;
-        $remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
-        $key = $this->cacheKey . 'profiles:' . $this->hash;
-        $remoteTtl = now()->addMinutes(15);
-        $ttl = now()->addHours(2);
-        if( Helpers::validateUrl($tag) != false && 
-            Helpers::validateLocalUrl($tag) != true && 
-            config('federation.activitypub.enabled') == true && 
-            config('federation.activitypub.remoteFollow') == true
-        ) {
-            $remote = Helpers::fetchFromUrl($tag);
-            if( isset($remote['type']) && 
-                $remote['type'] == 'Person'
-            ) {
-                $this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
-                    $item = Helpers::profileFirstOrNew($tag);
-                    $tokens = [[
-                        'count'  => 1,
-                        'url'    => $item->url(),
-                        'type'   => 'profile',
-                        'value'  => $item->username,
-                        'tokens' => [$item->username],
-                        'name'   => $item->name,
-                        'entity' => [
-                            'id' => (string) $item->id,
-                            'following' => $item->followedBy(Auth::user()->profile),
-                            'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
-                            'thumb' => $item->avatarUrl(),
-                            'local' => (bool) !$item->domain,
-                            'post_count' => $item->statuses()->count()
-                        ]
-                    ]];
-                    return $tokens;
-                });
-            }
-        } 
-
-        else {
-            $this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
-                if(Str::startsWith($tag, '@')) {
-                    $tag = substr($tag, 1);
-                }
-                $users = Profile::select('status', 'domain', 'username', 'name', 'id')
-                    ->whereNull('status')
-                    ->where('username', 'like', '%'.$tag.'%')
-                    ->limit(20)
-                    ->orderBy('domain')
-                    ->get();
-
-                if($users->count() > 0) {
-                    return $users->map(function ($item, $key) {
-                        return [
-                            'count'  => 0,
-                            'url'    => $item->url(),
-                            'type'   => 'profile',
-                            'value'  => $item->username,
-                            'tokens' => [$item->username],
-                            'name'   => $item->name,
-                            'avatar' => $item->avatarUrl(),
-                            'id'     =>  (string) $item->id,
-                            'entity' => [
-                                'id' => (string) $item->id,
-                                'following' => $item->followedBy(Auth::user()->profile),
-                                'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
-                                'thumb' => $item->avatarUrl(),
-                                'local' => (bool) !$item->domain,
-                                'post_count' => $item->statuses()->count()
-                            ]
-                        ];
-                    });
-                }
-            });
-        }
-    }
-
-    public function results(Request $request)
-    {
-        $this->validate($request, [
-            'q' => 'required|string|min:1',
-        ]);
-        
-        return view('search.results');
-    }
-
-    protected function webfingerSearch()
-    {
-        $wfs = WebfingerService::lookup($this->term);
-
-        if(empty($wfs)) {
-            return;
-        }
-
-        $this->tokens['profiles'] = [
-            [
-                'count'  => 1,
-                'url'    => $wfs['url'],
-                'type'   => 'profile',
-                'value'  => $wfs['username'],
-                'tokens' => [$wfs['username']],
-                'name'   => $wfs['display_name'],
-                'entity' => [
-                    'id' => (string) $wfs['id'],
-                    'following' => null,
-                    'follow_request' => null,
-                    'thumb' => $wfs['avatar'],
-                    'local' => (bool) $wfs['local']
-                ]
-            ]
-        ];
-        return;
-    }
-
-    protected function remotePostLookup()
-    {
-        $tag = $this->term;
-        $hash = hash('sha256', $tag);
-        $local = Helpers::validateLocalUrl($tag);
-        $valid = Helpers::validateUrl($tag);
-
-        if($valid == false || $local == true) {
-            return;
-        } 
-            
-        if(Status::whereUri($tag)->whereLocal(false)->exists()) {
-            $item = Status::whereUri($tag)->first();
-            $this->tokens['posts'] = [[
-                'count'  => 0,
-                'url'    => "/i/web/post/_/$item->profile_id/$item->id",
-                'type'   => 'status',
-                'username' => $item->profile->username,
-                'caption'   => $item->rendered ?? $item->caption,
-                'thumb'  => $item->firstMedia()->remote_url,
-                'timestamp' => $item->created_at->diffForHumans()
-            ]];
-        }
-
-        $remote = Helpers::fetchFromUrl($tag);
-
-        if(isset($remote['type']) && $remote['type'] == 'Note') {
-            $item = Helpers::statusFetch($tag);
-            $this->tokens['posts'] = [[
-                'count'  => 0,
-                'url'    => "/i/web/post/_/$item->profile_id/$item->id",
-                'type'   => 'status',
-                'username' => $item->profile->username,
-                'caption'   => $item->rendered ?? $item->caption,
-                'thumb'  => $item->firstMedia()->remote_url,
-                'timestamp' => $item->created_at->diffForHumans()
-            ]];
-        }
-    }
-
-    protected function remoteLookupSearch()
-    {
-        if(!Helpers::validateUrl($this->term)) {
-            return;
-        }
-        $this->getProfiles();
-        $this->remotePostLookup();
-    }
-}
+	public $tokens = [];
+	public $term = '';
+	public $hash = '';
+	public $cacheKey = 'api:search:tag:';
+
+	public function __construct()
+	{
+		$this->middleware('auth');
+	}
+
+	public function searchAPI(Request $request)
+	{
+		$this->validate($request, [
+			'q' => 'required|string|min:3|max:120',
+			'src' => 'required|string|in:metro',
+			'v' => 'required|integer|in:2',
+			'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
+		]);
+
+		$scope = $request->input('scope') ?? 'all';
+		$this->term = e(urldecode($request->input('q')));
+		$this->hash = hash('sha256', $this->term);
+
+		switch ($scope) {
+			case 'all':
+				$this->getHashtags();
+				$this->getPosts();
+				$this->getProfiles();
+				// $this->getPlaces();
+				break;
+
+			case 'hashtag':
+				$this->getHashtags();
+				break;
+
+			case 'profile':
+				$this->getProfiles();
+				break;
+
+			case 'webfinger':
+				$this->webfingerSearch();
+				break;
+
+			case 'remote':
+				$this->remoteLookupSearch();
+				break;
+
+			case 'place':
+				$this->getPlaces();
+				break;
+
+			default:
+				break;
+		}
+
+		return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
+	}
+
+	protected function getPosts()
+	{
+		$tag = $this->term;
+		$hash = hash('sha256', $tag);
+		if( Helpers::validateUrl($tag) != false &&
+			Helpers::validateLocalUrl($tag) != true &&
+			config_cache('federation.activitypub.enabled') == true &&
+			config('federation.activitypub.remoteFollow') == true
+		) {
+			$remote = Helpers::fetchFromUrl($tag);
+			if( isset($remote['type']) &&
+				$remote['type'] == 'Note') {
+				$item = Helpers::statusFetch($tag);
+				$this->tokens['posts'] = [[
+					'count'  => 0,
+					'url'    => $item->url(),
+					'type'   => 'status',
+					'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
+					'tokens' => [$item->caption],
+					'name'   => $item->caption,
+					'thumb'  => $item->thumb(),
+				]];
+			}
+		} else {
+			$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
+						->whereHas('media')
+						->whereNull('in_reply_to_id')
+						->whereNull('reblog_of_id')
+						->whereProfileId(Auth::user()->profile_id)
+						->where('caption', 'like', '%'.$tag.'%')
+						->latest()
+						->limit(10)
+						->get();
+
+			if($posts->count() > 0) {
+				$posts = $posts->map(function($item, $key) {
+					return [
+						'count'  => 0,
+						'url'    => $item->url(),
+						'type'   => 'status',
+						'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
+						'tokens' => [$item->caption],
+						'name'   => $item->caption,
+						'thumb'  => $item->thumb(),
+						'filter' => $item->firstMedia()->filter_class
+					];
+				});
+				$this->tokens['posts'] = $posts;
+			}
+		}
+	}
+
+	protected function getHashtags()
+	{
+		$tag = $this->term;
+		$key = $this->cacheKey . 'hashtags:' . $this->hash;
+		$ttl = now()->addMinutes(1);
+		$tokens = Cache::remember($key, $ttl, function() use($tag) {
+			$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
+			$hashtags = Hashtag::select('id', 'name', 'slug')
+				->where('slug', 'like', '%'.$htag.'%')
+				->whereHas('posts')
+				->limit(20)
+				->get();
+			if($hashtags->count() > 0) {
+				$tags = $hashtags->map(function ($item, $key) {
+					return [
+						'count'  => $item->posts()->count(),
+						'url'    => $item->url(),
+						'type'   => 'hashtag',
+						'value'  => $item->name,
+						'tokens' => '',
+						'name'   => null,
+					];
+				});
+				return $tags;
+			}
+		});
+		$this->tokens['hashtags'] = $tokens;
+	}
+
+	protected function getPlaces()
+	{
+		$tag = $this->term;
+		// $key = $this->cacheKey . 'places:' . $this->hash;
+		// $ttl = now()->addHours(12);
+		// $tokens = Cache::remember($key, $ttl, function() use($tag) {
+			$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
+			$hashtags = Place::select('id', 'name', 'slug', 'country')
+				->where('name', 'like', '%'.$htag[0].'%')
+				->paginate(20);
+			$tags = [];
+			if($hashtags->count() > 0) {
+				$tags = $hashtags->map(function ($item, $key) {
+					return [
+						'count'     => null,
+						'url'       => $item->url(),
+						'type'      => 'place',
+						'value'     => $item->name . ', ' . $item->country,
+						'tokens'    => '',
+						'name'      => null,
+						'city'      => $item->name,
+						'country'   => $item->country
+					];
+				});
+				// return $tags;
+			}
+		// });
+		$this->tokens['places'] = $tags;
+		$this->tokens['placesPagination'] = [
+			'total' => $hashtags->total(),
+			'current_page' => $hashtags->currentPage(),
+			'last_page' => $hashtags->lastPage()
+		];
+	}
+
+	protected function getProfiles()
+	{
+		$tag = $this->term;
+		$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
+		$key = $this->cacheKey . 'profiles:' . $this->hash;
+		$remoteTtl = now()->addMinutes(15);
+		$ttl = now()->addHours(2);
+		if( Helpers::validateUrl($tag) != false &&
+			Helpers::validateLocalUrl($tag) != true &&
+			config_cache('federation.activitypub.enabled') == true &&
+			config('federation.activitypub.remoteFollow') == true
+		) {
+			$remote = Helpers::fetchFromUrl($tag);
+			if( isset($remote['type']) &&
+				$remote['type'] == 'Person'
+			) {
+				$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
+					$item = Helpers::profileFirstOrNew($tag);
+					$tokens = [[
+						'count'  => 1,
+						'url'    => $item->url(),
+						'type'   => 'profile',
+						'value'  => $item->username,
+						'tokens' => [$item->username],
+						'name'   => $item->name,
+						'entity' => [
+							'id' => (string) $item->id,
+							'following' => $item->followedBy(Auth::user()->profile),
+							'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
+							'thumb' => $item->avatarUrl(),
+							'local' => (bool) !$item->domain,
+							'post_count' => $item->statuses()->count()
+						]
+					]];
+					return $tokens;
+				});
+			}
+		}
+
+		else {
+			$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
+				if(Str::startsWith($tag, '@')) {
+					$tag = substr($tag, 1);
+				}
+				$users = Profile::select('status', 'domain', 'username', 'name', 'id')
+					->whereNull('status')
+					->where('username', 'like', '%'.$tag.'%')
+					->limit(20)
+					->orderBy('domain')
+					->get();
+
+				if($users->count() > 0) {
+					return $users->map(function ($item, $key) {
+						return [
+							'count'  => 0,
+							'url'    => $item->url(),
+							'type'   => 'profile',
+							'value'  => $item->username,
+							'tokens' => [$item->username],
+							'name'   => $item->name,
+							'avatar' => $item->avatarUrl(),
+							'id'     =>  (string) $item->id,
+							'entity' => [
+								'id' => (string) $item->id,
+								'following' => $item->followedBy(Auth::user()->profile),
+								'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
+								'thumb' => $item->avatarUrl(),
+								'local' => (bool) !$item->domain,
+								'post_count' => $item->statuses()->count()
+							]
+						];
+					});
+				}
+			});
+		}
+	}
+
+	public function results(Request $request)
+	{
+		$this->validate($request, [
+			'q' => 'required|string|min:1',
+		]);
+
+		return view('search.results');
+	}
+
+	protected function webfingerSearch()
+	{
+		$wfs = WebfingerService::lookup($this->term);
+
+		if(empty($wfs)) {
+			return;
+		}
+
+		$this->tokens['profiles'] = [
+			[
+				'count'  => 1,
+				'url'    => $wfs['url'],
+				'type'   => 'profile',
+				'value'  => $wfs['username'],
+				'tokens' => [$wfs['username']],
+				'name'   => $wfs['display_name'],
+				'entity' => [
+					'id' => (string) $wfs['id'],
+					'following' => null,
+					'follow_request' => null,
+					'thumb' => $wfs['avatar'],
+					'local' => (bool) $wfs['local']
+				]
+			]
+		];
+		return;
+	}
+
+	protected function remotePostLookup()
+	{
+		$tag = $this->term;
+		$hash = hash('sha256', $tag);
+		$local = Helpers::validateLocalUrl($tag);
+		$valid = Helpers::validateUrl($tag);
+
+		if($valid == false || $local == true) {
+			return;
+		}
+
+		if(Status::whereUri($tag)->whereLocal(false)->exists()) {
+			$item = Status::whereUri($tag)->first();
+			$this->tokens['posts'] = [[
+				'count'  => 0,
+				'url'    => "/i/web/post/_/$item->profile_id/$item->id",
+				'type'   => 'status',
+				'username' => $item->profile->username,
+				'caption'   => $item->rendered ?? $item->caption,
+				'thumb'  => $item->firstMedia()->remote_url,
+				'timestamp' => $item->created_at->diffForHumans()
+			]];
+		}
+
+		$remote = Helpers::fetchFromUrl($tag);
+
+		if(isset($remote['type']) && $remote['type'] == 'Note') {
+			$item = Helpers::statusFetch($tag);
+			$this->tokens['posts'] = [[
+				'count'  => 0,
+				'url'    => "/i/web/post/_/$item->profile_id/$item->id",
+				'type'   => 'status',
+				'username' => $item->profile->username,
+				'caption'   => $item->rendered ?? $item->caption,
+				'thumb'  => $item->firstMedia()->remote_url,
+				'timestamp' => $item->created_at->diffForHumans()
+			]];
+		}
+	}
+
+	protected function remoteLookupSearch()
+	{
+		if(!Helpers::validateUrl($this->term)) {
+			return;
+		}
+		$this->getProfiles();
+		$this->remotePostLookup();
+	}
+}

+ 24 - 11
app/Http/Controllers/Settings/HomeSettings.php

@@ -16,6 +16,7 @@ use Mail;
 use Purify;
 use App\Mail\PasswordChange;
 use Illuminate\Http\Request;
+use App\Services\PronounService;
 
 trait HomeSettings
 {
@@ -25,23 +26,25 @@ trait HomeSettings
         $id = Auth::user()->profile->id;
         $storage = [];
         $used = Media::whereProfileId($id)->sum('size');
-        $storage['limit'] = config('pixelfed.max_account_size') * 1024;
+        $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
         $storage['used'] = $used;
         $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
         $storage['limitPretty'] = PrettyNumber::size($storage['limit']);
         $storage['usedPretty'] = PrettyNumber::size($storage['used']);
+        $pronouns = PronounService::get($id);
 
-        return view('settings.home', compact('storage'));
+        return view('settings.home', compact('storage', 'pronouns'));
     }
 
     public function homeUpdate(Request $request)
     {
-        $this->validate($request, [
-        'name'    => 'required|string|max:'.config('pixelfed.max_name_length'),
-        'bio'     => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
-        'website' => 'nullable|url',
-        'language' => 'nullable|string|min:2|max:5'
-      ]);
+		$this->validate($request, [
+			'name'    => 'required|string|max:'.config('pixelfed.max_name_length'),
+			'bio'     => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
+			'website' => 'nullable|url',
+			'language' => 'nullable|string|min:2|max:5',
+			'pronouns' => 'nullable|array|max:4'
+		]);
 
         $changes = false;
         $name = strip_tags(Purify::clean($request->input('name')));
@@ -50,12 +53,14 @@ trait HomeSettings
         $language = $request->input('language');
         $user = Auth::user();
         $profile = $user->profile;
+        $pronouns = $request->input('pronouns');
+        $existingPronouns = PronounService::get($profile->id);
         $layout = $request->input('profile_layout');
         if($layout) {
             $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
         }
 
-        $enforceEmailVerification = config('pixelfed.enforce_email_verification');
+        $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
 
         // Only allow email to be updated if not yet verified
         if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
@@ -82,6 +87,14 @@ trait HomeSettings
                 $user->language = $language;
                 session()->put('locale', $language);
             }
+
+            if($existingPronouns != $pronouns) {
+            	if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
+            		PronounService::clear($profile->id);
+            	} else {
+            		PronounService::put($profile->id, $pronouns);
+            	}
+            }
         }
 
         if ($changes === true) {
@@ -152,7 +165,7 @@ trait HomeSettings
         $user = Auth::user();
         $profile = $user->profile;
 
-        $validate = config('pixelfed.enforce_email_verification');
+        $validate = config_cache('pixelfed.enforce_email_verification');
 
         if ($user->email != $email) {
             $changes = true;
@@ -193,4 +206,4 @@ trait HomeSettings
         return view('settings.avatar');
     }
 
-}
+}

+ 2 - 2
app/Http/Controllers/SettingsController.php

@@ -77,13 +77,13 @@ class SettingsController extends Controller
 
 	public function dataImport()
 	{
-		abort_if(!config('pixelfed.import.instagram.enabled'), 404);
+		abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
 		return view('settings.import.home');
 	}
 
 	public function dataImportInstagram()
 	{
-		abort_if(!config('pixelfed.import.instagram.enabled'), 404);
+		abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
 		return view('settings.import.instagram.home');
 	}
 

+ 7 - 2
app/Http/Controllers/StatusController.php

@@ -70,11 +70,16 @@ class StatusController extends Controller
 			]);
 		}
 
-		if ($request->wantsJson() && config('federation.activitypub.enabled')) {
+		if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
 			return $this->showActivityPub($request, $status);
 		}
 
 		$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
+		// $template = $status->type === 'video' &&
+		// 	$request->has('video_beta') &&
+		// 	$request->video_beta == 1 &&
+		// 	$request->user() ?
+		// 	'status.show_video' : 'status.show';
 
 		return view($template, compact('user', 'status'));
 	}
@@ -340,7 +345,7 @@ class StatusController extends Controller
 
 	public static function mimeTypeCheck($mimes)
 	{
-		$allowed = explode(',', config('pixelfed.media_types'));
+		$allowed = explode(',', config_cache('pixelfed.media_types'));
 		$count = count($mimes);
 		$photos = 0;
 		$videos = 0;

+ 17 - 17
app/Http/Controllers/StoryController.php

@@ -21,14 +21,14 @@ class StoryController extends Controller
 {
 	public function apiV1Add(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$this->validate($request, [
 			'file' => function() {
 				return [
 					'required',
 					'mimes:image/jpeg,image/png,video/mp4',
-					'max:' . config('pixelfed.max_photo_size'),
+					'max:' . config_cache('pixelfed.max_photo_size'),
 				];
 			},
 		]);
@@ -78,7 +78,7 @@ class StoryController extends Controller
 
 	protected function storePhoto($photo, $user)
 	{
-		$mimes = explode(',', config('pixelfed.media_types'));
+		$mimes = explode(',', config_cache('pixelfed.media_types'));
 		if(in_array($photo->getMimeType(), [
 			'image/jpeg',
 			'image/png',
@@ -94,7 +94,7 @@ class StoryController extends Controller
 			$fpath = storage_path('app/' . $path);
 			$img = Intervention::make($fpath);
 			$img->orientate();
-			$img->save($fpath, config('pixelfed.image_quality'));
+			$img->save($fpath, config_cache('pixelfed.image_quality'));
 			$img->destroy();
 		}
 		return $path;
@@ -102,7 +102,7 @@ class StoryController extends Controller
 
 	public function cropPhoto(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$this->validate($request, [
 			'media_id' => 'required|integer|min:1',
@@ -133,7 +133,7 @@ class StoryController extends Controller
 			$img->resize(1080, 1920, function ($constraint) {
 				$constraint->aspectRatio();
 			});
-			$img->save($path, config('pixelfed.image_quality'));
+			$img->save($path, config_cache('pixelfed.image_quality'));
 		}
 
 		return [
@@ -144,7 +144,7 @@ class StoryController extends Controller
 
 	public function publishStory(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$this->validate($request, [
 			'media_id' => 'required',
@@ -169,7 +169,7 @@ class StoryController extends Controller
 
 	public function apiV1Delete(Request $request, $id)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$user = $request->user();
 
@@ -190,7 +190,7 @@ class StoryController extends Controller
 
 	public function apiV1Recent(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$profile = $request->user()->profile;
 		$following = $profile->following->pluck('id')->toArray();
@@ -232,7 +232,7 @@ class StoryController extends Controller
 
 	public function apiV1Fetch(Request $request, $id)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$authed = $request->user()->profile;
 		$profile = Profile::findOrFail($id);
@@ -270,7 +270,7 @@ class StoryController extends Controller
 
 	public function apiV1Item(Request $request, $id)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$authed = $request->user()->profile;
 		$story = Story::with('profile')
@@ -304,7 +304,7 @@ class StoryController extends Controller
 
 	public function apiV1Profile(Request $request, $id)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$authed = $request->user()->profile;
 		$profile = Profile::findOrFail($id);
@@ -355,7 +355,7 @@ class StoryController extends Controller
 
 	public function apiV1Viewed(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$this->validate($request, [
 			'id'	=> 'required|integer|min:1|exists:stories',
@@ -391,7 +391,7 @@ class StoryController extends Controller
 
 	public function apiV1Exists(Request $request, $id)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$res = (bool) Story::whereProfileId($id)
 		->whereActive(true)
@@ -403,7 +403,7 @@ class StoryController extends Controller
 
 	public function apiV1Me(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$profile = $request->user()->profile;
 		$stories = Story::whereProfileId($profile->id)
@@ -441,14 +441,14 @@ class StoryController extends Controller
 
 	public function compose(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		return view('stories.compose');
 	}
 
 	public function iRedirect(Request $request)
 	{
-		abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
+		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
 
 		$user = $request->user();
 		abort_if(!$user, 404);

+ 26 - 26
app/Http/Middleware/EmailVerificationCheck.php

@@ -6,31 +6,31 @@ use Closure;
 
 class EmailVerificationCheck
 {
-    /**
-     * Handle an incoming request.
-     *
-     * @param \Illuminate\Http\Request $request
-     * @param \Closure                 $next
-     *
-     * @return mixed
-     */
-    public function handle($request, Closure $next)
-    {
-        if ($request->user() &&
-            config('pixelfed.enforce_email_verification') &&
-            is_null($request->user()->email_verified_at) &&
-            !$request->is(
-                'i/auth/*',
-                'i/verify-email', 
-                'log*', 
-                'i/confirm-email/*', 
-                'settings/home',
-                'settings/email'
-            )
-        ) {
-            return redirect('/i/verify-email');
-        }
+	/**
+	 * Handle an incoming request.
+	 *
+	 * @param \Illuminate\Http\Request $request
+	 * @param \Closure                 $next
+	 *
+	 * @return mixed
+	 */
+	public function handle($request, Closure $next)
+	{
+		if ($request->user() &&
+			config_cache('pixelfed.enforce_email_verification') &&
+			is_null($request->user()->email_verified_at) &&
+			!$request->is(
+				'i/auth/*',
+				'i/verify-email',
+				'log*',
+				'i/confirm-email/*',
+				'settings/home',
+				'settings/email'
+			)
+		) {
+			return redirect('/i/verify-email');
+		}
 
-        return $next($request);
-    }
+		return $next($request);
+	}
 }

+ 56 - 56
app/Jobs/AvatarPipeline/AvatarOptimize.php

@@ -16,67 +16,67 @@ use Image as Intervention;
 
 class AvatarOptimize implements ShouldQueue
 {
-    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
-    protected $profile;
-    protected $current;
+	protected $profile;
+	protected $current;
 
-    /**
-     * Delete the job if its models no longer exist.
-     *
-     * @var bool
-     */
-    public $deleteWhenMissingModels = true;
+	/**
+	 * Delete the job if its models no longer exist.
+	 *
+	 * @var bool
+	 */
+	public $deleteWhenMissingModels = true;
 
-    /**
-     * Create a new job instance.
-     *
-     * @return void
-     */
-    public function __construct(Profile $profile, $current)
-    {
-        $this->profile = $profile;
-        $this->current = $current;
-    }
+	/**
+	 * Create a new job instance.
+	 *
+	 * @return void
+	 */
+	public function __construct(Profile $profile, $current)
+	{
+		$this->profile = $profile;
+		$this->current = $current;
+	}
 
-    /**
-     * Execute the job.
-     *
-     * @return void
-     */
-    public function handle()
-    {
-        $avatar = $this->profile->avatar;
-        $file = storage_path("app/$avatar->media_path");
+	/**
+	 * Execute the job.
+	 *
+	 * @return void
+	 */
+	public function handle()
+	{
+		$avatar = $this->profile->avatar;
+		$file = storage_path("app/$avatar->media_path");
 
-        try {
-            $img = Intervention::make($file)->orientate();
-            $img->fit(200, 200, function ($constraint) {
-                $constraint->upsize();
-            });
-            $quality = config('pixelfed.image_quality');
-            $img->save($file, $quality);
+		try {
+			$img = Intervention::make($file)->orientate();
+			$img->fit(200, 200, function ($constraint) {
+				$constraint->upsize();
+			});
+			$quality = config_cache('pixelfed.image_quality');
+			$img->save($file, $quality);
 
-            $avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
-            $avatar->change_count = ++$avatar->change_count;
-            $avatar->last_processed_at = Carbon::now();
-            $avatar->save();
-            Cache::forget('avatar:' . $avatar->profile_id);
-            $this->deleteOldAvatar($avatar->media_path, $this->current);
-        } catch (Exception $e) {
-        }
-    }
+			$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
+			$avatar->change_count = ++$avatar->change_count;
+			$avatar->last_processed_at = Carbon::now();
+			$avatar->save();
+			Cache::forget('avatar:' . $avatar->profile_id);
+			$this->deleteOldAvatar($avatar->media_path, $this->current);
+		} catch (Exception $e) {
+		}
+	}
 
-    protected function deleteOldAvatar($new, $current)
-    {
-        if ( storage_path('app/'.$new) == $current || 
-             Str::endsWith($current, 'avatars/default.png') || 
-             Str::endsWith($current, 'avatars/default.jpg'))
-        {
-            return;
-        }
-        if (is_file($current)) {
-            @unlink($current);
-        }
-    }
+	protected function deleteOldAvatar($new, $current)
+	{
+		if ( storage_path('app/'.$new) == $current ||
+			 Str::endsWith($current, 'avatars/default.png') ||
+			 Str::endsWith($current, 'avatars/default.jpg'))
+		{
+			return;
+		}
+		if (is_file($current)) {
+			@unlink($current);
+		}
+	}
 }

+ 1 - 1
app/Jobs/ImportPipeline/ImportInstagram.php

@@ -49,7 +49,7 @@ class ImportInstagram implements ShouldQueue
 	 */
 	public function handle()
 	{
-		if(config('pixelfed.import.instagram.enabled') != true) {
+		if(config_cache('pixelfed.import.instagram.enabled') != true) {
 			return;
 		}
 

+ 128 - 128
app/Jobs/SharePipeline/SharePipeline.php

@@ -18,132 +18,132 @@ use App\Util\ActivityPub\HttpSignature;
 
 class SharePipeline implements ShouldQueue
 {
-    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-    protected $status;
-
-    /**
-     * Delete the job if its models no longer exist.
-     *
-     * @var bool
-     */
-    public $deleteWhenMissingModels = true;
-
-    /**
-     * Create a new job instance.
-     *
-     * @return void
-     */
-    public function __construct(Status $status)
-    {
-        $this->status = $status;
-    }
-
-    /**
-     * Execute the job.
-     *
-     * @return void
-     */
-    public function handle()
-    {
-        $status = $this->status;
-        $actor = $status->profile;
-        $target = $status->parent()->profile;
-
-        if ($status->uri !== null) {
-            // Ignore notifications to remote statuses
-            return;
-        }
-
-        $exists = Notification::whereProfileId($target->id)
-                  ->whereActorId($status->profile_id)
-                  ->whereAction('share')
-                  ->whereItemId($status->reblog_of_id)
-                  ->whereItemType('App\Status')
-                  ->count();
-
-        if ($target->id === $status->profile_id) {
-            $this->remoteAnnounceDeliver();
-            return true;
-        }
-
-        if( $exists !== 0) {
-            return true;
-        }
-
-        $this->remoteAnnounceDeliver();
-
-        try {
-            $notification = new Notification;
-            $notification->profile_id = $target->id;
-            $notification->actor_id = $actor->id;
-            $notification->action = 'share';
-            $notification->message = $status->shareToText();
-            $notification->rendered = $status->shareToHtml();
-            $notification->item_id = $status->reblog_of_id ?? $status->id;
-            $notification->item_type = "App\Status";
-            $notification->save();
-
-            $redis = Redis::connection();
-            $key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
-            $redis->lpush($key, $notification->id);
-        } catch (Exception $e) {
-            Log::error($e);
-        }
-    }
-
-    public function remoteAnnounceDeliver()
-    {
-        if(config('federation.activitypub.enabled') == false) {
-            return true;
-        }
-        $status = $this->status;
-        $profile = $status->profile;
-
-        $fractal = new Fractal\Manager();
-        $fractal->setSerializer(new ArraySerializer());
-        $resource = new Fractal\Resource\Item($status, new Announce());
-        $activity = $fractal->createData($resource)->toArray();
-
-        $audience = $status->profile->getAudienceInbox();
-
-        if(empty($audience) || $status->scope != 'public') {
-            // Return on profiles with no remote followers
-            return;
-        }
-
-        $payload = json_encode($activity);
-        
-        $client = new Client([
-            'timeout'  => config('federation.activitypub.delivery.timeout')
-        ]);
-
-        $requests = function($audience) use ($client, $activity, $profile, $payload) {
-            foreach($audience as $url) {
-                $headers = HttpSignature::sign($profile, $url, $activity);
-                yield function() use ($client, $url, $headers, $payload) {
-                    return $client->postAsync($url, [
-                        'curl' => [
-                            CURLOPT_HTTPHEADER => $headers, 
-                            CURLOPT_POSTFIELDS => $payload,
-                            CURLOPT_HEADER => true
-                        ]
-                    ]);
-                };
-            }
-        };
-
-        $pool = new Pool($client, $requests($audience), [
-            'concurrency' => config('federation.activitypub.delivery.concurrency'),
-            'fulfilled' => function ($response, $index) {
-            },
-            'rejected' => function ($reason, $index) {
-            }
-        ]);
-        
-        $promise = $pool->promise();
-
-        $promise->wait();
-
-    }
+	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+	protected $status;
+
+	/**
+	 * Delete the job if its models no longer exist.
+	 *
+	 * @var bool
+	 */
+	public $deleteWhenMissingModels = true;
+
+	/**
+	 * Create a new job instance.
+	 *
+	 * @return void
+	 */
+	public function __construct(Status $status)
+	{
+		$this->status = $status;
+	}
+
+	/**
+	 * Execute the job.
+	 *
+	 * @return void
+	 */
+	public function handle()
+	{
+		$status = $this->status;
+		$actor = $status->profile;
+		$target = $status->parent()->profile;
+
+		if ($status->uri !== null) {
+			// Ignore notifications to remote statuses
+			return;
+		}
+
+		$exists = Notification::whereProfileId($target->id)
+				  ->whereActorId($status->profile_id)
+				  ->whereAction('share')
+				  ->whereItemId($status->reblog_of_id)
+				  ->whereItemType('App\Status')
+				  ->count();
+
+		if ($target->id === $status->profile_id) {
+			$this->remoteAnnounceDeliver();
+			return true;
+		}
+
+		if( $exists !== 0) {
+			return true;
+		}
+
+		$this->remoteAnnounceDeliver();
+
+		try {
+			$notification = new Notification;
+			$notification->profile_id = $target->id;
+			$notification->actor_id = $actor->id;
+			$notification->action = 'share';
+			$notification->message = $status->shareToText();
+			$notification->rendered = $status->shareToHtml();
+			$notification->item_id = $status->reblog_of_id ?? $status->id;
+			$notification->item_type = "App\Status";
+			$notification->save();
+
+			$redis = Redis::connection();
+			$key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
+			$redis->lpush($key, $notification->id);
+		} catch (Exception $e) {
+			Log::error($e);
+		}
+	}
+
+	public function remoteAnnounceDeliver()
+	{
+		if(config_cache('federation.activitypub.enabled') == false) {
+			return true;
+		}
+		$status = $this->status;
+		$profile = $status->profile;
+
+		$fractal = new Fractal\Manager();
+		$fractal->setSerializer(new ArraySerializer());
+		$resource = new Fractal\Resource\Item($status, new Announce());
+		$activity = $fractal->createData($resource)->toArray();
+
+		$audience = $status->profile->getAudienceInbox();
+
+		if(empty($audience) || $status->scope != 'public') {
+			// Return on profiles with no remote followers
+			return;
+		}
+
+		$payload = json_encode($activity);
+
+		$client = new Client([
+			'timeout'  => config('federation.activitypub.delivery.timeout')
+		]);
+
+		$requests = function($audience) use ($client, $activity, $profile, $payload) {
+			foreach($audience as $url) {
+				$headers = HttpSignature::sign($profile, $url, $activity);
+				yield function() use ($client, $url, $headers, $payload) {
+					return $client->postAsync($url, [
+						'curl' => [
+							CURLOPT_HTTPHEADER => $headers,
+							CURLOPT_POSTFIELDS => $payload,
+							CURLOPT_HEADER => true
+						]
+					]);
+				};
+			}
+		};
+
+		$pool = new Pool($client, $requests($audience), [
+			'concurrency' => config('federation.activitypub.delivery.concurrency'),
+			'fulfilled' => function ($response, $index) {
+			},
+			'rejected' => function ($reason, $index) {
+			}
+		]);
+
+		$promise = $pool->promise();
+
+		$promise->wait();
+
+	}
 }

+ 152 - 152
app/Jobs/StatusPipeline/StatusDelete.php

@@ -4,13 +4,14 @@ namespace App\Jobs\StatusPipeline;
 
 use DB, Storage;
 use App\{
-    AccountInterstitial,
-    MediaTag,
-    Notification,
-    Report,
-    Status,
-    StatusHashtag,
+	AccountInterstitial,
+	MediaTag,
+	Notification,
+	Report,
+	Status,
+	StatusHashtag,
 };
+use App\Models\StatusVideo;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
@@ -30,150 +31,149 @@ use App\Services\MediaStorageService;
 
 class StatusDelete implements ShouldQueue
 {
-    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-    protected $status;
-    
-    /**
-     * Delete the job if its models no longer exist.
-     *
-     * @var bool
-     */
-    public $deleteWhenMissingModels = true;
-    
-    /**
-     * Create a new job instance.
-     *
-     * @return void
-     */
-    public function __construct(Status $status)
-    {
-        $this->status = $status;
-    }
-
-    /**
-     * Execute the job.
-     *
-     * @return void
-     */
-    public function handle()
-    {
-        $status = $this->status;
-        $profile = $this->status->profile;
-
-        StatusService::del($status->id);
-        $count = $profile->statuses()
-        ->getQuery()
-        ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
-        ->whereNull('in_reply_to_id')
-        ->whereNull('reblog_of_id')
-        ->count();
-
-        $profile->status_count = ($count - 1);
-        $profile->save();
-
-        if(config('federation.activitypub.enabled') == true) {
-            $this->fanoutDelete($status);
-        } else {
-            $this->unlinkRemoveMedia($status);
-        }
-
-    }
-
-    public function unlinkRemoveMedia($status)
-    {
-        foreach ($status->media as $media) {
-            MediaStorageService::delete($media, true);
-        }
-
-        if($status->in_reply_to_id) {
-            DB::transaction(function() use($status) {
-                $parent = Status::findOrFail($status->in_reply_to_id);
-                --$parent->reply_count;
-                $parent->save();
-            });
-        }
-        DB::transaction(function() use($status) {
-            $comments = Status::where('in_reply_to_id', $status->id)->get();
-            foreach ($comments as $comment) {
-                $comment->in_reply_to_id = null;
-                $comment->save();
-                Notification::whereItemType('App\Status')
-                    ->whereItemId($comment->id)
-                    ->delete();
-            }
-            $status->likes()->delete();
-            Notification::whereItemType('App\Status')
-                ->whereItemId($status->id)
-                ->delete();
-            StatusHashtag::whereStatusId($status->id)->delete();
-            Report::whereObjectType('App\Status')
-                ->whereObjectId($status->id)
-                ->delete();
-
-            MediaTag::where('status_id', $status->id)
-                ->cursor()
-                ->each(function($tag) {
-                    Notification::where('item_type', 'App\MediaTag')
-                        ->where('item_id', $tag->id)
-                        ->forceDelete();
-                    $tag->delete();
-            });
-
-            AccountInterstitial::where('item_type', 'App\Status')
-                ->where('item_id', $status->id)
-                ->delete();
-
-            $status->forceDelete();
-        });
-
-        return true;
-    }
-
-    protected function fanoutDelete($status)
-    {
-        $audience = $status->profile->getAudienceInbox();
-        $profile = $status->profile;
-
-        $fractal = new Fractal\Manager();
-        $fractal->setSerializer(new ArraySerializer());
-        $resource = new Fractal\Resource\Item($status, new DeleteNote());
-        $activity = $fractal->createData($resource)->toArray();
-
-        $this->unlinkRemoveMedia($status);
-        
-        $payload = json_encode($activity);
-        
-        $client = new Client([
-            'timeout'  => config('federation.activitypub.delivery.timeout')
-        ]);
-
-        $requests = function($audience) use ($client, $activity, $profile, $payload) {
-            foreach($audience as $url) {
-                $headers = HttpSignature::sign($profile, $url, $activity);
-                yield function() use ($client, $url, $headers, $payload) {
-                    return $client->postAsync($url, [
-                        'curl' => [
-                            CURLOPT_HTTPHEADER => $headers, 
-                            CURLOPT_POSTFIELDS => $payload,
-                            CURLOPT_HEADER => true
-                        ]
-                    ]);
-                };
-            }
-        };
-
-        $pool = new Pool($client, $requests($audience), [
-            'concurrency' => config('federation.activitypub.delivery.concurrency'),
-            'fulfilled' => function ($response, $index) {
-            },
-            'rejected' => function ($reason, $index) {
-            }
-        ]);
-        
-        $promise = $pool->promise();
-
-        $promise->wait();
-
-    }
+	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+	protected $status;
+
+	/**
+	 * Delete the job if its models no longer exist.
+	 *
+	 * @var bool
+	 */
+	public $deleteWhenMissingModels = true;
+
+	/**
+	 * Create a new job instance.
+	 *
+	 * @return void
+	 */
+	public function __construct(Status $status)
+	{
+		$this->status = $status;
+	}
+
+	/**
+	 * Execute the job.
+	 *
+	 * @return void
+	 */
+	public function handle()
+	{
+		$status = $this->status;
+		$profile = $this->status->profile;
+
+		StatusService::del($status->id);
+		$count = $profile->statuses()
+		->getQuery()
+		->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
+		->whereNull('in_reply_to_id')
+		->whereNull('reblog_of_id')
+		->count();
+
+		$profile->status_count = ($count - 1);
+		$profile->save();
+
+		if(config_cache('federation.activitypub.enabled') == true) {
+			$this->fanoutDelete($status);
+		} else {
+			$this->unlinkRemoveMedia($status);
+		}
+
+	}
+
+	public function unlinkRemoveMedia($status)
+	{
+		foreach ($status->media as $media) {
+			MediaStorageService::delete($media, true);
+		}
+
+		if($status->in_reply_to_id) {
+			DB::transaction(function() use($status) {
+				$parent = Status::findOrFail($status->in_reply_to_id);
+				--$parent->reply_count;
+				$parent->save();
+			});
+		}
+		DB::transaction(function() use($status) {
+			$comments = Status::where('in_reply_to_id', $status->id)->get();
+			foreach ($comments as $comment) {
+				$comment->in_reply_to_id = null;
+				$comment->save();
+				Notification::whereItemType('App\Status')
+					->whereItemId($comment->id)
+					->delete();
+			}
+			$status->likes()->delete();
+			Notification::whereItemType('App\Status')
+				->whereItemId($status->id)
+				->delete();
+			StatusHashtag::whereStatusId($status->id)->delete();
+			Report::whereObjectType('App\Status')
+				->whereObjectId($status->id)
+				->delete();
+			MediaTag::where('status_id', $status->id)
+				->cursor()
+				->each(function($tag) {
+					Notification::where('item_type', 'App\MediaTag')
+						->where('item_id', $tag->id)
+						->forceDelete();
+					$tag->delete();
+			});
+			StatusVideo::whereStatusId($status->id)->delete();
+			AccountInterstitial::where('item_type', 'App\Status')
+				->where('item_id', $status->id)
+				->delete();
+
+			$status->forceDelete();
+		});
+
+		return true;
+	}
+
+	protected function fanoutDelete($status)
+	{
+		$audience = $status->profile->getAudienceInbox();
+		$profile = $status->profile;
+
+		$fractal = new Fractal\Manager();
+		$fractal->setSerializer(new ArraySerializer());
+		$resource = new Fractal\Resource\Item($status, new DeleteNote());
+		$activity = $fractal->createData($resource)->toArray();
+
+		$this->unlinkRemoveMedia($status);
+
+		$payload = json_encode($activity);
+
+		$client = new Client([
+			'timeout'  => config('federation.activitypub.delivery.timeout')
+		]);
+
+		$requests = function($audience) use ($client, $activity, $profile, $payload) {
+			foreach($audience as $url) {
+				$headers = HttpSignature::sign($profile, $url, $activity);
+				yield function() use ($client, $url, $headers, $payload) {
+					return $client->postAsync($url, [
+						'curl' => [
+							CURLOPT_HTTPHEADER => $headers,
+							CURLOPT_POSTFIELDS => $payload,
+							CURLOPT_HEADER => true
+						]
+					]);
+				};
+			}
+		};
+
+		$pool = new Pool($client, $requests($audience), [
+			'concurrency' => config('federation.activitypub.delivery.concurrency'),
+			'fulfilled' => function ($response, $index) {
+			},
+			'rejected' => function ($reason, $index) {
+			}
+		]);
+
+		$promise = $pool->promise();
+
+		$promise->wait();
+
+	}
 }

+ 142 - 142
app/Jobs/StatusPipeline/StatusEntityLexer.php

@@ -21,146 +21,146 @@ use Illuminate\Queue\SerializesModels;
 
 class StatusEntityLexer implements ShouldQueue
 {
-    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-    protected $status;
-    protected $entities;
-    protected $autolink;
-    
-    /**
-     * Delete the job if its models no longer exist.
-     *
-     * @var bool
-     */
-    public $deleteWhenMissingModels = true;
-    
-    /**
-     * Create a new job instance.
-     *
-     * @return void
-     */
-    public function __construct(Status $status)
-    {
-        $this->status = $status;
-    }
-
-    /**
-     * Execute the job.
-     *
-     * @return void
-     */
-    public function handle()
-    {
-        $profile = $this->status->profile;
-
-        $count = $profile->statuses()
-        ->getQuery()
-        ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
-        ->whereNull('in_reply_to_id')
-        ->whereNull('reblog_of_id')
-        ->count();
-
-        $profile->status_count = $count;
-        $profile->save();
-
-        if($profile->no_autolink == false) {
-            $this->parseEntities();
-        }
-    }
-
-    public function parseEntities()
-    {
-        $this->extractEntities();
-    }
-
-    public function extractEntities()
-    {
-        $this->entities = Extractor::create()->extract($this->status->caption);
-        $this->autolinkStatus();
-    }
-
-    public function autolinkStatus()
-    {
-        $this->autolink = Autolink::create()->autolink($this->status->caption);
-        $this->storeEntities();
-    }
-
-    public function storeEntities()
-    {
-        $this->storeHashtags();
-        DB::transaction(function () {
-            $status = $this->status;
-            $status->rendered = nl2br($this->autolink);
-            $status->entities = json_encode($this->entities);
-            $status->save();
-        });
-    }
-
-    public function storeHashtags()
-    {
-        $tags = array_unique($this->entities['hashtags']);
-        $status = $this->status;
-
-        foreach ($tags as $tag) {
-            if(mb_strlen($tag) > 124) {
-                continue;
-            }
-            DB::transaction(function () use ($status, $tag) {
-                $slug = str_slug($tag, '-', false);
-                $hashtag = Hashtag::firstOrCreate(
-                    ['name' => $tag, 'slug' => $slug]
-                );
-                StatusHashtag::firstOrCreate(
-                    [
-                        'status_id' => $status->id, 
-                        'hashtag_id' => $hashtag->id,
-                        'profile_id' => $status->profile_id,
-                        'status_visibility' => $status->visibility,
-                    ]
-                );
-            });
-        }
-        $this->storeMentions();
-    }
-
-    public function storeMentions()
-    {
-        $mentions = array_unique($this->entities['mentions']);
-        $status = $this->status;
-
-        foreach ($mentions as $mention) {
-            $mentioned = Profile::whereUsername($mention)->first();
-
-            if (empty($mentioned) || !isset($mentioned->id)) {
-                continue;
-            }
-
-            DB::transaction(function () use ($status, $mentioned) {
-                $m = new Mention();
-                $m->status_id = $status->id;
-                $m->profile_id = $mentioned->id;
-                $m->save();
-
-                MentionPipeline::dispatch($status, $m);
-            });
-        }
-        $this->deliver();
-    }
-
-    public function deliver()
-    {
-        $status = $this->status;
-
-        if(config('pixelfed.bouncer.enabled')) {
-            Bouncer::get($status);
-        }
-
-        if($status->uri == null && $status->scope == 'public') {
-            PublicTimelineService::add($status->id);
-        }
-
-        if(config('federation.activitypub.enabled') == true && config('app.env') == 'production') {
-            StatusActivityPubDeliver::dispatch($this->status);
-        }
-    }
+	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+	protected $status;
+	protected $entities;
+	protected $autolink;
+
+	/**
+	 * Delete the job if its models no longer exist.
+	 *
+	 * @var bool
+	 */
+	public $deleteWhenMissingModels = true;
+
+	/**
+	 * Create a new job instance.
+	 *
+	 * @return void
+	 */
+	public function __construct(Status $status)
+	{
+		$this->status = $status;
+	}
+
+	/**
+	 * Execute the job.
+	 *
+	 * @return void
+	 */
+	public function handle()
+	{
+		$profile = $this->status->profile;
+
+		$count = $profile->statuses()
+		->getQuery()
+		->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
+		->whereNull('in_reply_to_id')
+		->whereNull('reblog_of_id')
+		->count();
+
+		$profile->status_count = $count;
+		$profile->save();
+
+		if($profile->no_autolink == false) {
+			$this->parseEntities();
+		}
+	}
+
+	public function parseEntities()
+	{
+		$this->extractEntities();
+	}
+
+	public function extractEntities()
+	{
+		$this->entities = Extractor::create()->extract($this->status->caption);
+		$this->autolinkStatus();
+	}
+
+	public function autolinkStatus()
+	{
+		$this->autolink = Autolink::create()->autolink($this->status->caption);
+		$this->storeEntities();
+	}
+
+	public function storeEntities()
+	{
+		$this->storeHashtags();
+		DB::transaction(function () {
+			$status = $this->status;
+			$status->rendered = nl2br($this->autolink);
+			$status->entities = json_encode($this->entities);
+			$status->save();
+		});
+	}
+
+	public function storeHashtags()
+	{
+		$tags = array_unique($this->entities['hashtags']);
+		$status = $this->status;
+
+		foreach ($tags as $tag) {
+			if(mb_strlen($tag) > 124) {
+				continue;
+			}
+			DB::transaction(function () use ($status, $tag) {
+				$slug = str_slug($tag, '-', false);
+				$hashtag = Hashtag::firstOrCreate(
+					['name' => $tag, 'slug' => $slug]
+				);
+				StatusHashtag::firstOrCreate(
+					[
+						'status_id' => $status->id,
+						'hashtag_id' => $hashtag->id,
+						'profile_id' => $status->profile_id,
+						'status_visibility' => $status->visibility,
+					]
+				);
+			});
+		}
+		$this->storeMentions();
+	}
+
+	public function storeMentions()
+	{
+		$mentions = array_unique($this->entities['mentions']);
+		$status = $this->status;
+
+		foreach ($mentions as $mention) {
+			$mentioned = Profile::whereUsername($mention)->first();
+
+			if (empty($mentioned) || !isset($mentioned->id)) {
+				continue;
+			}
+
+			DB::transaction(function () use ($status, $mentioned) {
+				$m = new Mention();
+				$m->status_id = $status->id;
+				$m->profile_id = $mentioned->id;
+				$m->save();
+
+				MentionPipeline::dispatch($status, $m);
+			});
+		}
+		$this->deliver();
+	}
+
+	public function deliver()
+	{
+		$status = $this->status;
+
+		if(config_cache('pixelfed.bouncer.enabled')) {
+			Bouncer::get($status);
+		}
+
+		if($status->uri == null && $status->scope == 'public') {
+			PublicTimelineService::add($status->id);
+		}
+
+		if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
+			StatusActivityPubDeliver::dispatch($this->status);
+		}
+	}
 }

+ 14 - 0
app/Models/ConfigCache.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class ConfigCache extends Model
+{
+    use HasFactory;
+
+    protected $table = 'config_cache';
+    public $fillable = ['*'];
+}

+ 11 - 0
app/Models/UserPronoun.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class UserPronoun extends Model
+{
+    use HasFactory;
+}

+ 1 - 1
app/Providers/AuthServiceProvider.php

@@ -26,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
     {
         $this->registerPolicies();
 
-        if(config('pixelfed.oauth_enabled')) {
+        if(config_cache('pixelfed.oauth_enabled')) {
             Passport::routes(null, ['middleware' => ['twofactor', \Fruitcake\Cors\HandleCors::class]]);
             Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 15)));
             Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 30)));

+ 95 - 0
app/Services/ConfigCacheService.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace App\Services;
+
+use Cache;
+use Config;
+use App\Models\ConfigCache as ConfigCacheModel;
+
+class ConfigCacheService
+{
+	const CACHE_KEY = 'config_cache:_key:';
+
+	public static function get($key)
+	{
+		$cacheKey = "config_cache:_key:{$key}";
+		$ttl = now()->addHours(12);
+		return Cache::remember($cacheKey, $ttl, function() use($key) {
+
+			$allowed = [
+				'app.name',
+				'app.short_description',
+				'app.description',
+				'app.rules',
+
+				'pixelfed.max_photo_size',
+				'pixelfed.max_album_length',
+				'pixelfed.image_quality',
+				'pixelfed.media_types',
+
+				'pixelfed.open_registration',
+				'federation.activitypub.enabled',
+				'pixelfed.oauth_enabled',
+				'instance.stories.enabled',
+				'pixelfed.import.instagram.enabled',
+				'pixelfed.bouncer.enabled',
+
+				'pixelfed.enforce_email_verification',
+				'pixelfed.max_account_size',
+				'pixelfed.enforce_account_limit',
+
+				'uikit.custom.css',
+				'uikit.custom.js',
+				'uikit.show_custom.css',
+				'uikit.show_custom.js'
+			];
+
+			if(!config('instance.enable_cc')) {
+				return config($key);
+			}
+
+			if(!in_array($key, $allowed)) {
+				return config($key);
+			}
+
+			$v = config($key);
+			$c = ConfigCacheModel::where('k', $key)->first();
+
+			if($c) {
+				return $c->v ?? config($key);
+			}
+
+			if(!$v) {
+				return;
+			}
+
+			$cc = new ConfigCacheModel;
+			$cc->k = $key;
+			$cc->v = $v;
+			$cc->save();
+
+			return $v;
+		});
+	}
+
+	public static function put($key, $val)
+	{
+		$exists = ConfigCacheModel::whereK($key)->first();
+
+		if($exists) {
+			$exists->v = $val;
+			$exists->save();
+			Cache::forget(self::CACHE_KEY . $key);
+			return self::get($key);
+		}
+
+		$cc = new ConfigCacheModel;
+		$cc->k = $key;
+		$cc->v = $val;
+		$cc->save();
+
+		Cache::forget(self::CACHE_KEY . $key);
+
+		return self::get($key);
+	}
+}

+ 8 - 8
app/Services/MediaStorageService.php

@@ -43,11 +43,11 @@ class MediaStorageService {
 
 		$h = $r->getHeaders();
 
-		if (isset($h['Content-Length'], $h['Content-Type']) == false || 
+		if (isset($h['Content-Length'], $h['Content-Type']) == false ||
 			empty($h['Content-Length']) ||
-			empty($h['Content-Type']) || 
+			empty($h['Content-Type']) ||
 			$h['Content-Length'] < 10 ||
-			$h['Content-Length'] > (config('pixelfed.max_photo_size') * 1000)
+			$h['Content-Length'] > (config_cache('pixelfed.max_photo_size') * 1000)
 		) {
 			return false;
 		}
@@ -77,7 +77,7 @@ class MediaStorageService {
 		$pt = explode('/', $media->thumbnail_path);
 		$thumbname = array_pop($pt);
 		$storagePath = implode('/', $p);
-		
+
 		$disk = Storage::disk(config('filesystems.cloud'));
 		$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
 		$url = $disk->url($file);
@@ -102,11 +102,11 @@ class MediaStorageService {
 		}
 
 		$head = $this->head($media->remote_url);
-		
+
 		if(!$head) {
 			return;
 		}
-		
+
 		$mimes = [
 			'image/jpeg',
 			'image/png',
@@ -114,7 +114,7 @@ class MediaStorageService {
 		];
 
 		$mime = $head['mime'];
-		$max_size = (int) config('pixelfed.max_photo_size') * 1000;
+		$max_size = (int) config_cache('pixelfed.max_photo_size') * 1000;
 		$media->size = $head['length'];
 		$media->remote_media = true;
 		$media->save();
@@ -247,4 +247,4 @@ class MediaStorageService {
 		}
 		MediaDeletePipeline::dispatch($media);
 	}
-}
+}

+ 102 - 0
app/Services/PronounService.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace App\Services;
+
+use Cache;
+use App\Models\UserPronoun;
+use App\Profile;
+
+class PronounService {
+
+	public static function get($id)
+	{
+		$key = 'user:pronouns:' . $id;
+		$ttl = now()->addHours(12);
+
+		return Cache::remember($key, $ttl, function() use($id) {
+			$res = UserPronoun::whereProfileId($id)->first();
+			return $res ? json_decode($res->pronouns, true) : [];
+		});
+	}
+
+	public static function put($id, $pronouns)
+	{
+		$res = UserPronoun::whereProfileId($id)->first();
+		$key = 'user:pronouns:' . $id;
+
+		if($res) {
+			$res->pronouns = json_encode($pronouns);
+			$res->save();
+			Cache::forget($key);
+			AccountService::del($id);
+			return $res->pronouns;
+		}
+
+		$res = new UserPronoun;
+		$res->profile_id = $id;
+		$res->pronouns = json_encode($pronouns);
+		$res->save();
+		Cache::forget($key);
+		AccountService::del($id);
+		return $res->pronouns;
+	}
+
+	public static function clear($id)
+	{
+		$res = UserPronoun::whereProfileId($id)->first();
+		if($res) {
+			$res->pronouns = null;
+			$res->save();
+		}
+		$key = 'user:pronouns:' . $id;
+		Cache::forget($key);
+		AccountService::del($id);
+	}
+
+	public static function pronouns()
+	{
+		return [
+			'co',
+			'cos',
+			'e',
+			'ey',
+			'em',
+			'eir',
+			'fae',
+			'faer',
+			'he',
+			'him',
+			'his',
+			'her',
+			'hers',
+			'hir',
+			'mer',
+			'mers',
+			'ne',
+			'nir',
+			'nirs',
+			'nee',
+			'ner',
+			'ners',
+			'per',
+			'pers',
+			'she',
+			'they',
+			'them',
+			'theirs',
+			'thon',
+			'thons',
+			've',
+			'ver',
+			'vis',
+			'vi',
+			'vir',
+			'xe',
+			'xem',
+			'xyr',
+			'ze',
+			'zir',
+			'zie'
+		];
+	}
+}

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

@@ -5,6 +5,7 @@ namespace App\Transformer\Api;
 use Auth;
 use App\Profile;
 use League\Fractal;
+use App\Services\PronounService;
 
 class AccountTransformer extends Fractal\TransformerAbstract
 {
@@ -35,7 +36,8 @@ class AccountTransformer extends Fractal\TransformerAbstract
 			'is_admin' => (bool) $is_admin,
 			'created_at' => $profile->created_at->toJSON(),
 			'header_bg' => $profile->header_bg,
-			'last_fetched_at' => optional($profile->last_fetched_at)->toJSON()
+			'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
+			'pronouns' => PronounService::get($profile->id)
 		];
 	}
 

+ 2 - 2
app/Util/ActivityPub/Helpers.php

@@ -63,7 +63,7 @@ class Helpers {
 
 		$activity = $data['object'];
 
-		$mimeTypes = explode(',', config('pixelfed.media_types'));
+		$mimeTypes = explode(',', config_cache('pixelfed.media_types'));
 		$mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image'];
 
 		if(!isset($activity['attachment']) || empty($activity['attachment'])) {
@@ -418,7 +418,7 @@ class Helpers {
 		$attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
 		$user = $status->profile;
 		$storagePath = MediaPathService::get($user, 2);
-		$allowed = explode(',', config('pixelfed.media_types'));
+		$allowed = explode(',', config_cache('pixelfed.media_types'));
 
 		foreach($attachments as $media) {
 			$type = $media['mediaType'];

+ 13 - 13
app/Util/ActivityPub/Inbox.php

@@ -115,8 +115,8 @@ class Inbox
     {
         $activity = $this->payload['object'];
 
-        if(isset($activity['inReplyTo']) && 
-            !empty($activity['inReplyTo']) && 
+        if(isset($activity['inReplyTo']) &&
+            !empty($activity['inReplyTo']) &&
             Helpers::validateUrl($activity['inReplyTo'])
         ) {
             // reply detected, skip attachment check
@@ -147,8 +147,8 @@ class Inbox
         }
         $to = $activity['to'];
         $cc = isset($activity['cc']) ? $activity['cc'] : [];
-        if(count($to) == 1 && 
-            count($cc) == 0 && 
+        if(count($to) == 1 &&
+            count($cc) == 0 &&
             parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
         ) {
             $this->handleDirectMessage();
@@ -175,7 +175,7 @@ class Inbox
 
         $inReplyTo = $activity['inReplyTo'];
         $url = isset($activity['url']) ? $activity['url'] : $activity['id'];
-        
+
         Helpers::statusFirstOrFetch($url, true);
         return;
     }
@@ -251,8 +251,8 @@ class Inbox
         if(count($activity['attachment'])) {
             $photos = 0;
             $videos = 0;
-            $allowed = explode(',', config('pixelfed.media_types'));
-            $activity['attachment'] = array_slice($activity['attachment'], 0, config('pixelfed.max_album_length'));
+            $allowed = explode(',', config_cache('pixelfed.media_types'));
+            $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length'));
             foreach($activity['attachment'] as $a) {
                 $type = $a['mediaType'];
                 $url = $a['url'];
@@ -293,7 +293,7 @@ class Inbox
                 $dm->type = 'link';
                 $dm->meta = [
                     'domain' => parse_url($msgText, PHP_URL_HOST),
-                    'local' => parse_url($msgText, PHP_URL_HOST) == 
+                    'local' => parse_url($msgText, PHP_URL_HOST) ==
                         parse_url(config('app.url'), PHP_URL_HOST)
                 ];
                 $dm->save();
@@ -459,8 +459,8 @@ class Inbox
     public function handleDeleteActivity()
     {
         if(!isset(
-            $this->payload['actor'], 
-            $this->payload['object'] 
+            $this->payload['actor'],
+            $this->payload['object']
         )) {
             return;
         }
@@ -510,7 +510,7 @@ class Inbox
                         $status->delete();
                         return;
                     break;
-                
+
                 default:
                     return;
                     break;
@@ -564,7 +564,7 @@ class Inbox
         switch ($obj['type']) {
             case 'Accept':
                 break;
-                
+
             case 'Announce':
                 $obj = $obj['object'];
                 if(!Helpers::validateLocalUrl($obj)) {
@@ -603,7 +603,7 @@ class Inbox
                     ->whereItemType('App\Profile')
                     ->forceDelete();
                 break;
-                
+
             case 'Like':
                 $status = Helpers::statusFirstOrFetch($obj['object']);
                 if(!$status) {

+ 2 - 2
app/Util/ActivityPub/Outbox.php

@@ -13,7 +13,7 @@ class Outbox {
 
 	public static function get($profile)
 	{
-        abort_if(!config('federation.activitypub.enabled'), 404);
+        abort_if(!config_cache('federation.activitypub.enabled'), 404);
         abort_if(!config('federation.activitypub.outbox'), 404);
 
         if($profile->status != null) {
@@ -48,4 +48,4 @@ class Outbox {
         return $outbox;
 	}
 
-}
+}

+ 2 - 2
app/Util/Media/Image.php

@@ -154,7 +154,7 @@ class Image
 						}
 					}
 					$media->metadata = json_encode($meta);
-				} 
+				}
 
 				$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
 					$constraint->aspectRatio();
@@ -163,7 +163,7 @@ class Image
 			$converted = $this->setBaseName($path, $thumbnail, $img->extension);
 			$newPath = storage_path('app/'.$converted['path']);
 
-			$quality = config('pixelfed.image_quality');
+			$quality = config_cache('pixelfed.image_quality');
 			$img->save($newPath, $quality);
 
 			if ($thumbnail == true) {

+ 13 - 13
app/Util/Site/Config.php

@@ -8,26 +8,26 @@ use Illuminate\Support\Str;
 class Config {
 
 	public static function get() {
-		return Cache::remember('api:site:configuration:_v0.2', now()->addHours(30), function() {
+		return Cache::remember('api:site:configuration:_v0.2', now()->addMinutes(5), function() {
 			return [
-				'open_registration' => config('pixelfed.open_registration'),
+				'open_registration' => (bool) config_cache('pixelfed.open_registration'),
 				'uploader' => [
 					'max_photo_size' => config('pixelfed.max_photo_size'),
 					'max_caption_length' => config('pixelfed.max_caption_length'),
-					'album_limit' => config('pixelfed.max_album_length'),
-					'image_quality' => config('pixelfed.image_quality'),
+					'album_limit' => config_cache('pixelfed.max_album_length'),
+					'image_quality' => config_cache('pixelfed.image_quality'),
 
 					'max_collection_length' => config('pixelfed.max_collection_length', 18),
 
 					'optimize_image' => config('pixelfed.optimize_image'),
 					'optimize_video' => config('pixelfed.optimize_video'),
 
-					'media_types' => config('pixelfed.media_types'),
-					'enforce_account_limit' => config('pixelfed.enforce_account_limit')
+					'media_types' => config_cache('pixelfed.media_types'),
+					'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit')
 				],
 
 				'activitypub' => [
-					'enabled' => config('federation.activitypub.enabled'),
+					'enabled' => config_cache('federation.activitypub.enabled'),
 					'remote_follow' => config('federation.activitypub.remoteFollow')
 				],
 
@@ -39,10 +39,10 @@ class Config {
 				],
 
 				'site' => [
-					'name' => config('app.name', 'pixelfed'),
+					'name' => config_cache('app.name'),
 					'domain' => config('pixelfed.domain.app'),
 					'url'    => config('app.url'),
-					'description' => config('instance.description')
+					'description' => config_cache('app.short_description')
 				],
 
 				'username' => [
@@ -54,12 +54,12 @@ class Config {
 				],
 
 				'features' => [
-					'mobile_apis' => config('pixelfed.oauth_enabled'),
+					'mobile_apis' => config_cache('pixelfed.oauth_enabled'),
 					'circles' => false,
-					'stories' => config('instance.stories.enabled'),
-					'video'	=> Str::contains(config('pixelfed.media_types'), 'video/mp4'),
+					'stories' => config_cache('instance.stories.enabled'),
+					'video'	=> Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
 					'import' => [
-						'instagram' => config('pixelfed.import.instagram.enabled'),
+						'instagram' => config_cache('pixelfed.import.instagram.enabled'),
 						'mastodon' => false,
 						'pixelfed' => false
 					],

+ 63 - 63
app/Util/Site/Nodeinfo.php

@@ -10,68 +10,68 @@ class Nodeinfo {
 
 	public static function get()
 	{
-        $res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () {
-            $activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() {
-                // todo: replace with last_active_at after July 9, 2021 (96afc3e781)
-                $count = collect([]);
-                $likes = Like::select('profile_id')->with('actor')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray();
-                $count = $count->merge($likes);
-                $statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
-                $count = $count->merge($statuses);
-                $profiles = User::select('profile_id', 'last_active_at')
-                    ->whereNotNull('last_active_at')
-                    ->where('last_active_at', '>', now()->subMonths(6))
-                    ->pluck('profile_id')
-                    ->toArray();
-                $newProfiles = User::select('profile_id', 'last_active_at', 'created_at')
-                    ->whereNull('last_active_at')
-                    ->where('created_at', '>', now()->subMonths(6))
-                    ->pluck('profile_id')
-                    ->toArray();
-                $count = $count->merge($newProfiles);
-                $count = $count->merge($profiles);
-                return $count->unique()->count();
-            });
-            $activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(2), function() {
-                return User::select('last_active_at')
-                    ->where('last_active_at', '>', now()->subMonths(1))
-                    ->orWhere('created_at', '>', now()->subMonths(1))
-                    ->count();
-            });
-            return [
-                'metadata' => [
-                    'nodeName' => config('pixelfed.domain.app'),
-                    'software' => [
-                        'homepage'  => 'https://pixelfed.org',
-                        'repo'      => 'https://github.com/pixelfed/pixelfed',
-                    ],
-                    'config' => \App\Util\Site\Config::get()
-                ],
-                'protocols'         => [
-                    'activitypub',
-                ],
-                'services' => [
-                    'inbound'  => [],
-                    'outbound' => [],
-                ],
-                'software' => [
-                    'name'          => 'pixelfed',
-                    'version'       => config('pixelfed.version'),
-                ],
-                'usage' => [
-                    'localPosts'    => Status::whereLocal(true)->count(),
-                    'localComments' => 0,
-                    'users'         => [
-                        'total'          => User::count(),
-                        'activeHalfyear' => (int) $activeHalfYear,
-                        'activeMonth'    => (int) $activeMonth,
-                    ],
-                ],
-                'version' => '2.0',
-            ];
-        });
-        $res['openRegistrations'] = config('pixelfed.open_registration');
-        return $res;
+		$res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () {
+			$activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() {
+				// todo: replace with last_active_at after July 9, 2021 (96afc3e781)
+				$count = collect([]);
+				$likes = Like::select('profile_id')->with('actor')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray();
+				$count = $count->merge($likes);
+				$statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
+				$count = $count->merge($statuses);
+				$profiles = User::select('profile_id', 'last_active_at')
+					->whereNotNull('last_active_at')
+					->where('last_active_at', '>', now()->subMonths(6))
+					->pluck('profile_id')
+					->toArray();
+				$newProfiles = User::select('profile_id', 'last_active_at', 'created_at')
+					->whereNull('last_active_at')
+					->where('created_at', '>', now()->subMonths(6))
+					->pluck('profile_id')
+					->toArray();
+				$count = $count->merge($newProfiles);
+				$count = $count->merge($profiles);
+				return $count->unique()->count();
+			});
+			$activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(2), function() {
+				return User::select('last_active_at')
+					->where('last_active_at', '>', now()->subMonths(1))
+					->orWhere('created_at', '>', now()->subMonths(1))
+					->count();
+			});
+			return [
+				'metadata' => [
+					'nodeName' => config_cache('app.name'),
+					'software' => [
+						'homepage'  => 'https://pixelfed.org',
+						'repo'      => 'https://github.com/pixelfed/pixelfed',
+					],
+					'config' => \App\Util\Site\Config::get()
+				],
+				'protocols'         => [
+					'activitypub',
+				],
+				'services' => [
+					'inbound'  => [],
+					'outbound' => [],
+				],
+				'software' => [
+					'name'          => 'pixelfed',
+					'version'       => config('pixelfed.version'),
+				],
+				'usage' => [
+					'localPosts'    => Status::whereLocal(true)->count(),
+					'localComments' => 0,
+					'users'         => [
+						'total'          => User::count(),
+						'activeHalfyear' => (int) $activeHalfYear,
+						'activeMonth'    => (int) $activeMonth,
+					],
+				],
+				'version' => '2.0',
+			];
+		});
+		$res['openRegistrations'] = (bool) config_cache('pixelfed.open_registration');
+		return $res;
 	}
 
 	public static function wellKnown()
@@ -86,4 +86,4 @@ class Nodeinfo {
 		];
 	}
 
-}
+}

+ 9 - 0
app/helpers.php

@@ -0,0 +1,9 @@
+<?php
+
+use App\Services\ConfigCacheService;
+
+if (!function_exists('config_cache')) {
+	function config_cache($key) {
+		return ConfigCacheService::get($key);
+	}
+}

+ 94 - 91
composer.json

@@ -1,93 +1,96 @@
 {
-    "name": "pixelfed/pixelfed",
-    "description": "Open and ethical photo sharing platform, powered by ActivityPub federation.",
-    "keywords": ["framework", "laravel", "pixelfed", "activitypub", "social", "network", "federation"],
-    "license": "AGPL-3.0-only",
-    "type": "project",
-    "require": {
-        "php": "^7.3",
-        "ext-bcmath": "*",
-        "ext-ctype": "*",
-        "ext-curl": "*",
-        "ext-intl": "*",
-        "ext-json": "*",
-        "ext-mbstring": "*",
-        "ext-openssl": "*",
-        "beyondcode/laravel-self-diagnosis": "^1.0.2",
-        "brick/math": "^0.8",
-        "buzz/laravel-h-captcha": "1.0.2",
-        "doctrine/dbal": "^2.7",
-        "fideloper/proxy": "^4.0",
-        "fruitcake/laravel-cors": "^2.0",
-        "intervention/image": "^2.4",
-        "jenssegers/agent": "^2.6",
-        "laravel/framework": "^8.0",
-        "laravel/helpers": "^1.1",
-        "laravel/horizon": "^5.0",
-        "laravel/passport": "^10.0",
-        "laravel/tinker": "^2.0",
-        "laravel/ui": "^2.0",
-        "league/flysystem-aws-s3-v3": "~1.0",
-        "league/flysystem-cached-adapter": "~1.0",
-        "league/iso3166": "^2.1",
-        "pbmedia/laravel-ffmpeg": "^7.0",
-        "phpseclib/phpseclib": "~2.0",
-        "bacon/bacon-qr-code": "^2.0.3",
-        "pixelfed/fractal": "^0.18.0",
-        "pragmarx/google2fa": "^8.0",
-        "pixelfed/laravel-snowflake": "^2.0",
-        "pixelfed/zttp": "^0.4",
-        "predis/predis": "^1.1",
-        "spatie/laravel-backup": "^6.0.0",
-        "spatie/laravel-image-optimizer": "^1.1",
-        "stevebauman/purify": "3.0.*",
-        "symfony/http-kernel": "5.1.5"
-    },
-    "require-dev": {
-        "brianium/paratest": "^6.1",
-        "facade/ignition": "^2.3.6",
-        "fzaninotto/faker": "^1.4",
-        "mockery/mockery": "^1.0",
-        "nunomaduro/collision": "^5.0",
-        "phpunit/phpunit": "^9.0"
-    },
-    "autoload": {
-        "classmap": [
-            "database/seeds",
-            "database/factories"
-        ],
-        "psr-4": {
-            "App\\": "app/"
-        }
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "Tests\\": "tests/"
-        }
-    },
-    "extra": {
-        "laravel": {
-            "dont-discover": [
-            ]
-        }
-    },
-    "scripts": {
-        "post-root-package-install": [
-            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
-        ],
-        "post-create-project-cmd": [
-            "@php artisan key:generate --ansi"
-        ],
-        "post-autoload-dump": [
-            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
-            "@php artisan package:discover --ansi"
-        ]
-    },
-    "config": {
-        "preferred-install": "dist",
-        "sort-packages": true,
-        "optimize-autoloader": true
-    },
-    "minimum-stability": "dev",
-    "prefer-stable": true
+	"name": "pixelfed/pixelfed",
+	"description": "Open and ethical photo sharing platform, powered by ActivityPub federation.",
+	"keywords": ["framework", "laravel", "pixelfed", "activitypub", "social", "network", "federation"],
+	"license": "AGPL-3.0-only",
+	"type": "project",
+	"require": {
+		"php": "^7.3",
+		"ext-bcmath": "*",
+		"ext-ctype": "*",
+		"ext-curl": "*",
+		"ext-intl": "*",
+		"ext-json": "*",
+		"ext-mbstring": "*",
+		"ext-openssl": "*",
+		"beyondcode/laravel-self-diagnosis": "^1.0.2",
+		"brick/math": "^0.8",
+		"buzz/laravel-h-captcha": "1.0.2",
+		"doctrine/dbal": "^2.7",
+		"fideloper/proxy": "^4.0",
+		"fruitcake/laravel-cors": "^2.0",
+		"intervention/image": "^2.4",
+		"jenssegers/agent": "^2.6",
+		"laravel/framework": "^8.0",
+		"laravel/helpers": "^1.1",
+		"laravel/horizon": "^5.0",
+		"laravel/passport": "^10.0",
+		"laravel/tinker": "^2.0",
+		"laravel/ui": "^2.0",
+		"league/flysystem-aws-s3-v3": "~1.0",
+		"league/flysystem-cached-adapter": "~1.0",
+		"league/iso3166": "^2.1",
+		"pbmedia/laravel-ffmpeg": "^7.0",
+		"phpseclib/phpseclib": "~2.0",
+		"bacon/bacon-qr-code": "^2.0.3",
+		"pixelfed/fractal": "^0.18.0",
+		"pragmarx/google2fa": "^8.0",
+		"pixelfed/laravel-snowflake": "^2.0",
+		"pixelfed/zttp": "^0.4",
+		"predis/predis": "^1.1",
+		"spatie/laravel-backup": "^6.0.0",
+		"spatie/laravel-image-optimizer": "^1.1",
+		"stevebauman/purify": "3.0.*",
+		"symfony/http-kernel": "5.1.5"
+	},
+	"require-dev": {
+		"brianium/paratest": "^6.1",
+		"facade/ignition": "^2.3.6",
+		"fzaninotto/faker": "^1.4",
+		"mockery/mockery": "^1.0",
+		"nunomaduro/collision": "^5.0",
+		"phpunit/phpunit": "^9.0"
+	},
+	"autoload": {
+		"classmap": [
+			"database/seeds",
+			"database/factories"
+		],
+		"psr-4": {
+			"App\\": "app/"
+		}
+	},
+	"autoload-dev": {
+		"psr-4": {
+			"Tests\\": "tests/"
+		},
+		"files": [
+			"app/helpers.php"
+		]
+	},
+	"extra": {
+		"laravel": {
+			"dont-discover": [
+			]
+		}
+	},
+	"scripts": {
+		"post-root-package-install": [
+			"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+		],
+		"post-create-project-cmd": [
+			"@php artisan key:generate --ansi"
+		],
+		"post-autoload-dump": [
+			"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
+			"@php artisan package:discover --ansi"
+		]
+	},
+	"config": {
+		"preferred-install": "dist",
+		"sort-packages": true,
+		"optimize-autoloader": true
+	},
+	"minimum-stability": "dev",
+	"prefer-stable": true
 }

+ 5 - 0
config/app.php

@@ -108,6 +108,11 @@ return [
 
     'cipher' => 'AES-256-CBC',
 
+    'short_description' => 'Pixelfed - Photo sharing for everyone',
+    'description' => 'Pixelfed - Photo sharing for everyone',
+    'rules' => null,
+    'logo' => '/img/pixelfed-icon-color.svg',
+
     /*
     |--------------------------------------------------------------------------
     | Autoloaded Service Providers

+ 2 - 0
config/instance.php

@@ -72,4 +72,6 @@ return [
 			'org' => env('COVID_LABEL_ORG', 'visit the WHO website')
 		]
 	],
+
+	'enable_cc' => env('ENABLE_CONFIG_CACHE', false)
 ];

+ 34 - 0
database/migrations/2021_04_28_060450_create_config_caches_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateConfigCachesTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('config_cache', function (Blueprint $table) {
+			$table->id();
+			$table->string('k')->unique()->index();
+			$table->text('v')->nullable();
+			$table->json('metadata')->nullable();
+			$table->timestamps();
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::dropIfExists('config_cache');
+	}
+}

+ 34 - 0
database/migrations/2021_05_12_042153_create_user_pronouns_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateUserPronounsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('user_pronouns', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedInteger('user_id')->nullable()->unique()->index();
+            $table->bigInteger('profile_id')->unique()->index();
+            $table->json('pronouns')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('user_pronouns');
+    }
+}

BIN
public/js/profile.js


BIN
public/mix-manifest.json


+ 3 - 1
resources/assets/js/components/Profile.vue

@@ -145,7 +145,8 @@
 										</div>
 									</div>
 									<p class="mb-0 d-flex align-items-center">
-										<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
+										<span class="font-weight-bold mr-1">{{profile.display_name}}</span>
+										<span v-if="profile.pronouns" class="text-muted small">{{profile.pronouns.join('/')}}</span>
 									</p>
 									<div v-if="profile.note" class="mb-0" v-html="profile.note"></div>
 									<p v-if="profile.website" class=""><a :href="profile.website" class="profile-website" rel="me external nofollow noopener" target="_blank" @click.prevent="remoteRedirect(profile.website)">{{truncate(profile.website,24)}}</a></p>
@@ -387,6 +388,7 @@
 			</div>
 		</div>
 	</div>
+
 	<b-modal
 		v-if="profile && following"
 		ref="followingModal"

+ 86 - 61
resources/views/admin/partial/topnav.blade.php

@@ -1,67 +1,92 @@
 <nav class="navbar navbar-expand-lg navbar-light">
-  <div class="container">
-    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topbarNav" aria-controls="topbarNav" aria-expanded="false" aria-label="Toggle navigation">
-      <span class="navbar-toggler-icon"></span>
-    </button>
-    <div class="collapse navbar-collapse" id="topbarNav">
-      <ul class="navbar-nav">
-        <li class="nav-item mx-2 {{request()->is('*admin/dashboard')?'active':''}}">
-          <a class="nav-link" href="{{route('admin.home')}}">Dashboard</a>
-        </li>
-        <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
-          <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Messages</a>
-        </li>
-        <li class="nav-item mx-2 {{request()->is('*instances*')?'active':''}}">
-          <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.instances')}}">Instances</a>
-        </li>
-        <li class="nav-item mx-2 {{request()->is('*media*')?'active':''}}">
-          <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.media')}}">Media</a>
-        </li>
-        <li class="nav-item mx-2 {{request()->is('*reports*')?'active':''}}">
-          <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.reports')}}">Moderation</a>
-        </li>
-        <li class="nav-item mx-2 {{request()->is('*profiles*')?'active':''}}">
-          <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.profiles')}}">Profiles</a>
-        </li>
-        <li class="nav-item mx-2 {{request()->is('*statuses*')?'active':''}}">
-          <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.statuses')}}">Statuses</a>
-        </li>
-        <li class="nav-item mx-2 {{request()->is('*users*')?'active':''}}">
-          <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.users')}}">Users</a>
-        </li>
-        <li class="nav-item dropdown mx-3 {{request()->is(['*settings*','*discover*', '*site-news*'])?'active':''}}">
-          <a class="nav-link dropdown-toggle px-4" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-            More
-          </a>
-          <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
-            <a class="dropdown-item font-weight-bold {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}">Apps</a>
-            <a class="dropdown-item font-weight-bold {{request()->is('*discover*')?'active':''}}" href="{{route('admin.discover')}}">Discover</a>
-            <a class="dropdown-item font-weight-bold {{request()->is('*hashtags*')?'active':''}}" href="{{route('admin.hashtags')}}">Hashtags</a>
-            <a class="dropdown-item font-weight-bold {{request()->is('*site-news*')?'active':''}}" href="/i/admin/site-news">Newsroom</a>
-            <div class="dropdown-divider"></div>
-            <a class="dropdown-item font-weight-bold" href="/horizon">Horizon</a>
-            {{-- <a class="dropdown-item font-weight-bold" href="#">Websockets</a> --}}
-            <div class="dropdown-divider"></div>
-            <a class="dropdown-item font-weight-bold {{request()->is('*settings*')?'active':''}}" href="{{route('admin.settings')}}">Settings</a>
-          </div>
-        </li>
-      </ul>
-    </div>
-  </div>
+	<div class="container">
+		<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topbarNav" aria-controls="topbarNav" aria-expanded="false" aria-label="Toggle navigation">
+			<span class="navbar-toggler-icon"></span>
+		</button>
+		<div class="collapse navbar-collapse" id="topbarNav">
+			<ul class="navbar-nav">
+				<li class="nav-item mx-4 {{request()->is('*admin/dashboard')?'active':''}}">
+					<a class="nav-link" href="{{route('admin.home')}}">Dashboard</a>
+				</li>
+				{{--<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Configuration</a>
+				</li>
+				<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Content</a>
+				</li>
+				<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Federation</a>
+				</li>
+				<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Moderation</a>
+				</li>
+				<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Platform</a>
+				</li>
+				<li class="nav-item mx-2 align-self-center text-lighter">|</li>
+				<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Media</a>
+				</li>
+				<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Profiles</a>
+				</li>
+				<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Statuses</a>
+				</li> --}}
+				<li class="nav-item mx-4 {{request()->is('*messages*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Messages</a>
+				</li>
+				{{-- <li class="nav-item mx-4 {{request()->is('*instances*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.instances')}}">Instances</a>
+				</li> --}}
+				<li class="nav-item mx-4 {{request()->is('*reports*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.reports')}}">Moderation</a>
+				</li>
+				{{-- <li class="nav-item mx-2 {{request()->is('*profiles*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.profiles')}}">Profiles</a>
+				</li> --}}
+				<li class="nav-item mx-4 {{request()->is('*statuses*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.statuses')}}">Statuses</a>
+				</li>
+				<li class="nav-item mx-4 {{request()->is('*users*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.users')}}">Users</a>
+				</li>
+				<li class="nav-item mx-4 {{request()->is('*settings*')?'active':''}}">
+					<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.settings')}}">Settings</a>
+				</li>
+				<li class="nav-item dropdown ml-3 {{request()->is(['*discover*', '*site-news*'])?'active':''}}">
+					<a class="nav-link dropdown-toggle px-4" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+						More
+					</a>
+					<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
+						<a class="dropdown-item font-weight-bold {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}">Apps</a>
+						{{-- <a class="dropdown-item font-weight-bold {{request()->is('*discover*')?'active':''}}" href="{{route('admin.discover')}}">Discover</a> --}}
+						<a class="dropdown-item font-weight-bold {{request()->is('*hashtags*')?'active':''}}" href="{{route('admin.hashtags')}}">Hashtags</a>
+						<a class="dropdown-item font-weight-bold {{request()->is('*instances*')?'active':''}}" href="{{route('admin.instances')}}">Instances</a>
+						<a class="dropdown-item font-weight-bold {{request()->is('*media*')?'active':''}}" href="{{route('admin.media')}}">Media</a>
+						<a class="dropdown-item font-weight-bold {{request()->is('*site-news*')?'active':''}}" href="/i/admin/site-news">Newsroom</a>
+						<a class="dropdown-item font-weight-bold {{request()->is('*profiles*')?'active':''}}" href="/i/admin/profiles">Profiles</a>
+						<div class="dropdown-divider"></div>
+						<a class="dropdown-item font-weight-bold" href="/horizon">Horizon</a>
+					</div>
+				</li>
+			</ul>
+		</div>
+	</div>
 </nav>
 
 @push('styles')
 <style type="text/css">
-  #topbarNav .nav-item:hover {
-    border-bottom: 2px solid #08d;
-    margin-bottom: -7px;
-  }
-  #topbarNav .nav-item.active {
-    border-bottom: 2px solid #08d;
-    margin-bottom: -7px;
-  }
-  #topbarNav .nav-item.active .nav-link {
-    font-weight: bold !important;
-  } 
+	#topbarNav .nav-item:hover {
+		border-bottom: 2px solid #08d;
+		margin-bottom: -7px;
+	}
+	#topbarNav .nav-item.active {
+		border-bottom: 2px solid #08d;
+		margin-bottom: -7px;
+	}
+	#topbarNav .nav-item.active .nav-link {
+		font-weight: bold !important;
+	}
 </style>
-@endpush
+@endpush

+ 178 - 58
resources/views/admin/settings/home.blade.php

@@ -3,61 +3,181 @@
 @include('admin.settings.sidebar')
 
 @section('section')
-  <div class="title">
-    <h3 class="font-weight-bold">Settings</h3>
-  </div>
-  <hr>
-  <form method="post">
-    @csrf
-    <div class="form-group row">
-      <label for="app_name" class="col-sm-3 col-form-label font-weight-bold text-right">App Name</label>
-      <div class="col-sm-9">
-        <input type="text" class="form-control" id="app_name" name="APP_NAME" placeholder="Application Name ex: pixelfed" value="{{config('app.name')}}" autocomplete="off">
-        <p class="text-muted small help-text font-weight-bold mb-0">Site name, default: pixelfed</p>
-      </div>
-    </div>
-    <div class="form-group row">
-      <label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">App URL</label>
-      <div class="col-sm-9">
-        <input type="text" class="form-control" id="app_url" name="APP_URL" placeholder="Application URL" value="{{config('app.url')}}">
-        <p class="text-muted small help-text font-weight-bold mb-0">App URL, used for building URLs ex: https://example.org</p>
-      </div>
-    </div>
-
-    <div class="form-group row">
-      <label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">App Domain</label>
-      <div class="col-sm-9">
-        <input type="text" class="form-control" id="app_url" name="app_domain" placeholder="example.org" value="{{config('pixelfed.domain.app')}}">
-        <p class="text-muted small help-text font-weight-bold mb-0">Used for routing ex: example.org</p>
-      </div>
-    </div>
-
-
-    <div class="form-group row">
-      <label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">Admin Domain</label>
-      <div class="col-sm-9">
-        <input type="text" class="form-control" id="admin_domain" name="admin_domain" placeholder="admin.example.org" value="{{config('pixelfed.domain.admin')}}">
-        <p class="text-muted small help-text font-weight-bold mb-0">Used for routing the admin dashboard ex: admin.example.org</p>
-      </div>
-    </div>
-
-    {{-- <div class="alert alert-info border-0">
-      <div class="media d-flex align-items-center">
-      <div class="mr-3">
-        <i class="fas fa-info-circle fa-2x"></i>
-      </div>
-      <div class="media-body">
-        <p class="mb-0 lead">Tip:</p>
-        <p class="mb-0">You can edit the .env file in the <a href="#" class="font-weight-bold">Configuration</a> settings.</p>
-      </div>
-      </div>
-    </div>
-
-    <hr>
-    <div class="form-group row mb-0">
-      <div class="col-12 text-right">
-        <button type="submit" class="btn btn-primary font-weight-bold">Submit</button>
-      </div>
-    </div> --}}
-  </form>
-@endsection
+<div class="title mb-4">
+	<h3 class="font-weight-bold">Settings</h3>
+@if(config('instance.enable_cc'))
+	<p class="lead mb-0">Manage instance settings.</p>
+	<p class="mb-0"><span class="font-weight-bold">Warning</span>: These settings will override .env variables</p>
+</div>
+<form method="post">
+	@csrf
+	<ul class="nav nav-tabs nav-fill border-bottom-0" id="myTab" role="tablist">
+		<li class="nav-item">
+			<a class="nav-link font-weight-bold px-4 active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">General</a>
+		</li>
+		<li class="nav-item border-none">
+			<a class="nav-link font-weight-bold px-4" id="media-tab" data-toggle="tab" href="#media" role="tab" aria-controls="media">Media</a>
+		</li>
+		<li class="nav-item border-none">
+			<a class="nav-link font-weight-bold px-4" id="users-tab" data-toggle="tab" href="#users" role="tab" aria-controls="users">Users</a>
+		</li>
+		<li class="nav-item">
+			<a class="nav-link font-weight-bold px-4" id="advanced-tab" data-toggle="tab" href="#advanced" role="tab" aria-controls="advanced">Advanced</a>
+		</li>
+	</ul>
+	<div class="tab-content" id="myTabContent">
+
+	<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top">
+				<label class="font-weight-bold text-muted">Manage Core Features</label>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="activitypub" class="custom-control-input" id="ap" {{config_cache('federation.activitypub.enabled') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="ap">ActivityPub</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="open_registration" class="custom-control-input" id="openReg" {{config_cache('pixelfed.open_registration') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="openReg">Open Registrations</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="mobile_apis" class="custom-control-input" id="cf2" {{config_cache('pixelfed.oauth_enabled') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="cf2">Mobile APIs</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="stories" class="custom-control-input" id="cf3" {{config_cache('instance.stories.enabled') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="cf3">Stories</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="ig_import" class="custom-control-input" id="cf4" {{config_cache('pixelfed.import.instagram.enabled') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="cf4">Instagram Import</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="spam_detection" class="custom-control-input" id="cf5" {{config_cache('pixelfed.bouncer.enabled') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="cf5">Spam detection</label>
+				</div>
+			</div>
+		</div>
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
+				<label class="font-weight-bold text-muted">Name</label>
+				<input class="form-control col-8" name="name" placeholder="Pixelfed" value="{{config_cache('app.name')}}">
+				<p class="help-text small text-muted mt-3 mb-0">The instance name used in titles, metadata and apis.</p>
+			</div>
+		</div>
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-bottom">
+				<label class="font-weight-bold text-muted">Short Description</label>
+				<textarea class="form-control" rows="3" name="short_description">{{config_cache('app.short_description')}}</textarea>
+				<p class="help-text small text-muted mt-3 mb-0">Short description of instance used on various pages and apis.</p>
+			</div>
+		</div>
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-bottom">
+				<label class="font-weight-bold text-muted">Long Description</label>
+				<textarea class="form-control" rows="3" name="long_description">{{config_cache('app.description')}}</textarea>
+				<p class="help-text small text-muted mt-3 mb-0">Longer description of instance used on about page.</p>
+			</div>
+		</div>
+	</div>
+
+	<div class="tab-pane" id="users" role="tabpanel" aria-labelledby="users-tab">
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top">
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="require_email_verification" class="custom-control-input" id="mailVerification" {{config_cache('pixelfed.enforce_email_verification') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="mailVerification">Require Email Verification</label>
+				</div>
+			</div>
+		</div>
+		<div class="form-group">
+				<div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
+					<div class="custom-control custom-checkbox my-2">
+						<input type="checkbox" name="enforce_account_limit" class="custom-control-input" id="userEnforceLimit" {{config_cache('pixelfed.enforce_account_limit') ? 'checked' : ''}}>
+						<label class="custom-control-label font-weight-bold" for="userEnforceLimit">Enable account storage limit</label>
+						<p class="help-text small text-muted">Set a storage limit per user account.</p>
+					</div>
+					<label class="font-weight-bold text-muted">Account Limit</label>
+					<input class="form-control" name="account_limit" placeholder="Pixelfed" value="{{config_cache('pixelfed.max_account_size')}}">
+					<p class="help-text small text-muted mt-3 mb-0">Account limit size in KB.</p>
+					<p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_account_size')}} KB = {{floor(config_cache('pixelfed.max_account_size') / 1024)}} MB</p>
+				</div>
+			</div>
+	</div>
+
+	<div class="tab-pane" id="media" role="tabpanel" aria-labelledby="media-tab">
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top">
+				<label class="font-weight-bold text-muted">Max Size</label>
+				<input class="form-control" name="max_photo_size" value="{{config_cache('pixelfed.max_photo_size')}}">
+				<p class="help-text small text-muted mt-3 mb-0">Maximum file upload size in KB</p>
+				<p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_photo_size')}} KB = {{number_format(config_cache('pixelfed.max_photo_size') / 1024)}} MB</p>
+			</div>
+		</div>
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top">
+				<label class="font-weight-bold text-muted">Photo Album Limit</label>
+				<input class="form-control" name="max_album_length" value="{{config_cache('pixelfed.max_album_length')}}">
+				<p class="help-text small text-muted mt-3 mb-0">The maximum number of photos or videos per album</p>
+			</div>
+		</div>
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top">
+				<label class="font-weight-bold text-muted">Image Quality</label>
+				<input class="form-control" name="image_quality" value="{{config_cache('pixelfed.image_quality')}}">
+				<p class="help-text small text-muted mt-3 mb-0">Image optimization quality from 0-100%. Set to 0 to disable image optimization.</p>
+			</div>
+		</div>
+		<div class="form-group">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
+				<label class="font-weight-bold text-muted">Media Types</label>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="type_jpeg" class="custom-control-input" id="mediaType1" {{$jpeg ? 'checked' : ''}}>
+					<label class="custom-control-label" for="mediaType1">Allow <span class="border border-dark px-1 rounded font-weight-bold">JPEG</span> images (image/jpg)</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="type_png" class="custom-control-input" id="mediaType2" {{$png ? 'checked' : ''}}>
+					<label class="custom-control-label" for="mediaType2">Allow <span class="border border-dark px-1 rounded font-weight-bold">PNG</span> images (image/png)</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="type_gif" class="custom-control-input" id="mediaType3" {{$gif ? 'checked' : ''}}>
+					<label class="custom-control-label" for="mediaType3">Allow <span class="border border-dark px-1 rounded font-weight-bold">GIF</span> images (image/gif)</label>
+				</div>
+				<div class="custom-control custom-checkbox mt-2">
+					<input type="checkbox" name="type_mp4" class="custom-control-input" id="mediaType4" {{$mp4 ? 'checked' : ''}}>
+					<label class="custom-control-label" for="mediaType4">Allow <span class="border border-dark px-1 rounded font-weight-bold">MP4</span> video (video/mp4)</label>
+				</div>
+				<p class="help-text small text-muted mt-3 mb-0">Allowed media types.</p>
+			</div>
+		</div>
+	</div>
+
+	<div class="tab-pane" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
+		<div class="form-group mb-0">
+			<div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
+				<label class="font-weight-bold text-muted">Custom CSS</label>
+				<div class="custom-control custom-checkbox my-2">
+					<input type="checkbox" name="show_custom_css" class="custom-control-input" id="showCustomCss" {{config_cache('uikit.show_custom.css') ? 'checked' : ''}}>
+					<label class="custom-control-label font-weight-bold" for="showCustomCss">Enable custom CSS</label>
+				</div>
+				<textarea class="form-control" name="custom_css" rows="3">{{config_cache('uikit.custom.css')}}</textarea>
+				<p class="help-text small text-muted mt-3 mb-0">Add custom CSS, will be used on all pages</p>
+			</div>
+		</div>
+	</div>
+
+	</div>
+
+	<div class="form-group row mb-0 mt-4">
+		<div class="col-12 text-right">
+			<button type="submit" class="btn btn-primary font-weight-bold px-5">Save</button>
+		</div>
+	</div>
+</form>
+@else
+</div>
+<div class="py-5">
+	<p class="lead text-center font-weight-bold">Not enabled</p>
+	<p class="text-center">Add <code>ENABLE_CONFIG_CACHE=true</code> in your <span class="font-weight-bold">.env</span> file <br /> and run <span class="font-weight-bold">php artisan config:cache</span></p>
+</div>
+@endif
+@endsection

+ 14 - 11
resources/views/admin/settings/sidebar.blade.php

@@ -3,26 +3,29 @@
   <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings') ? 'font-weight-bold':''}}" href="{{route('admin.settings')}}">Home</a>
   </li>
-  <li class="nav-item pl-3">
+{{--   <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings/backups') ? 'font-weight-bold':''}}" href="{{route('admin.settings.backups')}}">Backups</a>
-  </li>
-  <li class="nav-item pl-3">
+  </li> --}}
+  {{-- <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings/config') ? 'font-weight-bold':''}}" href="{{route('admin.settings.config')}}">Configuration</a>
-  </li>
-  <li class="nav-item pl-3">
+  </li> --}}
+  {{-- <li class="nav-item pl-3">
+    <a class="nav-link text-muted {{request()->is('*settings/customize') ? 'font-weight-bold':''}}" href="{{route('admin.settings.customize')}}">Customize</a>
+  </li> --}}
+  {{-- <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings/features') ? 'font-weight-bold':''}}" href="{{route('admin.settings.features')}}">Features</a>
-  </li>
-  <li class="nav-item pl-3">
+  </li> --}}
+  {{-- <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings/maintenance') ? 'font-weight-bold':''}}" href="{{route('admin.settings.maintenance')}}">Maintenance</a>
-  </li>
+  </li> --}}
   <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings/page*') ? 'font-weight-bold':''}}" href="{{route('admin.settings.pages')}}">Pages</a>
   </li>
-  <li class="nav-item pl-3">
+  {{-- <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings/storage') ? 'font-weight-bold':''}}" href="{{route('admin.settings.storage')}}">Storage</a>
-  </li>
+  </li> --}}
   <li class="nav-item pl-3">
     <a class="nav-link text-muted {{request()->is('*settings/system') ? 'font-weight-bold':''}}" href="{{route('admin.settings.system')}}">System</a>
   </li>
     </ul>
-@endsection
+@endsection

+ 2 - 2
resources/views/admin/users/show.blade.php

@@ -90,7 +90,7 @@
 					</tr>
 					<tr>
 						<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">storage used</th>
-						<td class="text-right font-weight-bold">{{PrettyNumber::size($profile->media()->sum('size'))}}<span class="text-muted"> / {{PrettyNumber::size(config('pixelfed.max_account_size') * 1000)}}</span></td>
+						<td class="text-right font-weight-bold">{{PrettyNumber::size($profile->media()->sum('size'))}}<span class="text-muted"> / {{PrettyNumber::size(config_cache('pixelfed.max_account_size') * 1000)}}</span></td>
 					</tr>
 				</tbody>
 			</table>
@@ -118,4 +118,4 @@
 		</div>
 	</div>
 </div>
-@endsection
+@endsection

+ 4 - 4
resources/views/layouts/anon.blade.php

@@ -1,7 +1,7 @@
 <!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">
@@ -9,10 +9,10 @@
     <meta name="robots" content="noimageindex, noarchive">
     <meta name="mobile-web-app-capable" content="yes">
 
-    <title>{{ $title ?? config('app.name', 'Laravel') }}</title>
+    <title>{{ $title ?? config_cache('app.name') }}</title>
 
-    @if(isset($title))<meta property="og:site_name" content="{{ config('app.name', 'Laravel') }}">
-    <meta property="og:title" content="{{ $title ?? config('app.name', 'Laravel') }}">
+    @if(isset($title))<meta property="og:site_name" content="{{ config_cache('app.name') }}">
+    <meta property="og:title" content="{{ $title ?? config_cache('app.name') }}">
     <meta property="og:type" content="article">
     <meta property="og:url" content="{{request()->url()}}">
     @endif

+ 104 - 100
resources/views/layouts/app.blade.php

@@ -2,82 +2,86 @@
 @auth
 <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="csrf-token" content="{{ csrf_token() }}">
+	<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="csrf-token" content="{{ csrf_token() }}">
 
-    <meta name="mobile-web-app-capable" content="yes">
+	<meta name="mobile-web-app-capable" content="yes">
 
-    <title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
-    <link rel="manifest" href="/manifest.json">
+	<title>{{ $title ?? config_cache('app.name') }}</title>
+	<link rel="manifest" href="/manifest.json">
 
-    <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="{{url(request()->url())}}">
-    @stack('meta')
+	<meta property="og:site_name" content="{{ config_cache('app.name') }}">
+	<meta property="og:title" content="{{ $title ?? config_cache('app.name') }}">
+	<meta property="og:type" content="article">
+	<meta property="og:url" content="{{url(request()->url())}}">
+	@stack('meta')
+
+	<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 rel="canonical" href="{{url(request()->url())}}">
+	@if(request()->cookie('dark-mode'))
+
+	<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
+	@else
+
+	<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
+	@endif
+
+	@stack('styles')
+
+	@if(config_cache('uikit.show_custom.css'))
+	<style type="text/css">{!!config_cache('uikit.custom.css')!!}</style>
+	@endif
+
+	<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
 
-    <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 rel="canonical" href="{{url(request()->url())}}">
-    @if(request()->cookie('dark-mode')) 
-    
-    <link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
-    @else
-    
-    <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
-    @endif
-    
-    @stack('styles')
-    
-    <script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
-    
 </head>
 <body class="loggedIn">
-    @include('layouts.partial.nav')
-    <main id="content">
-        @yield('content')
-        <noscript>
-            <div class="container">
-                <p class="pt-5 text-center lead">Please enable javascript to view this content.</p>
-            </div>
-        
-        </noscript>
-    </main>
-    @include('layouts.partial.footer')
-    <script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
-    @stack('scripts')
-    <div class="d-block d-sm-none mt-5"></div>
-    <div class="d-block d-sm-none fixed-bottom">
-        <div class="card card-body rounded-0 py-2 box-shadow" style="border-top:1px solid #F1F5F8">
-            <ul class="nav nav-pills nav-fill d-flex align-items-middle">
-              <li class="nav-item">
-                <a class="nav-link text-dark" href="/"><i class="fas fa-home fa-lg"></i></a>
-              </li>
-              <li class="nav-item">
-                <a class="nav-link text-dark" href="/discover"><i class="fas fa-search fa-lg"></i></a>
-              </li>
-              <li class="nav-item">
-                <div class="nav-link cursor-pointer text-dark" onclick="App.util.compose.post()">
-                    <i class="far fa-plus-square fa-2x"></i>
-                </div>
-              </li>
-              <li class="nav-item">
-                <a class="nav-link text-dark" href="/account/activity"><i class="far fa-bell fa-lg"></i></a>
-              </li>
-              <li class="nav-item">
-                <a class="nav-link text-dark" href="/i/me"><i class="far fa-user fa-lg"></i></a>
-              </li>
-            </ul>
-        </div>
-    </div>
+	@include('layouts.partial.nav')
+	<main id="content">
+		@yield('content')
+		<noscript>
+			<div class="container">
+				<p class="pt-5 text-center lead">Please enable javascript to view this content.</p>
+			</div>
+
+		</noscript>
+	</main>
+	@include('layouts.partial.footer')
+	<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
+	@stack('scripts')
+	<div class="mobile-footer-spacer d-block d-sm-none mt-5"></div>
+	<div class="mobile-footer d-block d-sm-none fixed-bottom">
+		<div class="card card-body rounded-0 py-2 box-shadow" style="border-top:1px solid #F1F5F8">
+			<ul class="nav nav-pills nav-fill d-flex align-items-middle">
+			  <li class="nav-item">
+				<a class="nav-link text-dark" href="/"><i class="fas fa-home fa-lg"></i></a>
+			  </li>
+			  <li class="nav-item">
+				<a class="nav-link text-dark" href="/discover"><i class="fas fa-search fa-lg"></i></a>
+			  </li>
+			  <li class="nav-item">
+				<div class="nav-link cursor-pointer text-dark" onclick="App.util.compose.post()">
+					<i class="far fa-plus-square fa-2x"></i>
+				</div>
+			  </li>
+			  <li class="nav-item">
+				<a class="nav-link text-dark" href="/account/activity"><i class="far fa-bell fa-lg"></i></a>
+			  </li>
+			  <li class="nav-item">
+				<a class="nav-link text-dark" href="/i/me"><i class="far fa-user fa-lg"></i></a>
+			  </li>
+			</ul>
+		</div>
+	</div>
 </body>
 </html>
 @endauth
@@ -85,41 +89,41 @@
 @guest
 <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">
+	<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>
-    <link rel="manifest" href="/manifest.json">
+	<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
+	<link rel="manifest" href="/manifest.json">
 
-    <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="{{url(request()->url())}}">
-    @stack('meta')
+	<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="{{url(request()->url())}}">
+	@stack('meta')
 
-    <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 rel="canonical" href="{{url(request()->url())}}">
-    <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
-    <script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
-    @stack('styles')
+	<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 rel="canonical" href="{{url(request()->url())}}">
+	<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
+	<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
+	@stack('styles')
 </head>
 <body>
-    @include('layouts.partial.nav')
-    <main id="content">
-        @yield('content')
-    </main>
-    @include('layouts.partial.footer')
-    <script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
-    @stack('scripts')
+	@include('layouts.partial.nav')
+	<main id="content">
+		@yield('content')
+	</main>
+	@include('layouts.partial.footer')
+	<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
+	@stack('scripts')
 </body>
 </html>
 @endguest

+ 41 - 41
resources/views/layouts/blank.blade.php

@@ -1,49 +1,49 @@
 <!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="csrf-token" content="{{ csrf_token() }}">
-
-    <meta name="mobile-web-app-capable" content="yes">
-
-    <title>{{ $title ?? config('app.name', 'Laravel') }}</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="{{request()->url()}}">
-    @stack('meta')
-
-    <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 rel="canonical" href="{{request()->url()}}">
-        @if(request()->cookie('dark-mode')) 
-    
-    <link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
-    @else
-    
-    <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
-    @endif
-    
-    @stack('styles')
-
-    <script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
+
+	<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="csrf-token" content="{{ csrf_token() }}">
+
+	<meta name="mobile-web-app-capable" content="yes">
+
+	<title>{{ $title ?? config_cache('app.name') }}</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="{{request()->url()}}">
+	@stack('meta')
+
+	<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 rel="canonical" href="{{request()->url()}}">
+		@if(request()->cookie('dark-mode'))
+
+	<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
+	@else
+
+	<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
+	@endif
+
+	@stack('styles')
+
+	<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
 
 </head>
 <body class="w-100 h-100">
-    <main id="content" class="w-100 h-100">
-        @yield('content')
-    </main>
-    <script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
-    <script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
-    @stack('scripts')
+	<main id="content" class="w-100 h-100">
+		@yield('content')
+	</main>
+	<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
+	<script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
+	@stack('scripts')
 </body>
 </html>

+ 2 - 2
resources/views/layouts/partial/nav.blade.php

@@ -2,7 +2,7 @@
     <div class="container">
             <a class="navbar-brand d-flex align-items-center" href="{{ route('timeline.personal') }}" title="Logo">
                 <img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager" alt="Pixelfed logo">
-                <span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config('app.name', 'pixelfed') }}</span>
+                <span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config_cache('app.name') }}</span>
             </a>
 
             <div class="collapse navbar-collapse">
@@ -22,7 +22,7 @@
                             {{ __('Login') }}
                         </a>
                     </li>
-                @if(config('pixelfed.open_registration') && config('instance.restricted.enabled') == false)
+                @if(config_cache('pixelfed.open_registration') && config('instance.restricted.enabled') == false)
                     <li>
                         <a class="ml-3 nav-link font-weight-bold text-dark" href="{{ route('register') }}" title="Register">
                             {{ __('Register') }}

+ 2 - 2
resources/views/settings/applications.blade.php

@@ -6,7 +6,7 @@
 	<h3 class="font-weight-bold">Applications</h3>
 </div>
 <hr>
-@if(config('pixelfed.oauth_enabled') == true)
+@if(config_cache('pixelfed.oauth_enabled') == true)
 	<passport-authorized-clients></passport-authorized-clients>
 	<passport-personal-access-tokens></passport-personal-access-tokens>
 @else
@@ -16,4 +16,4 @@
 
 @push('scripts')
 <script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
-@endpush
+@endpush

+ 2 - 2
resources/views/settings/developers.blade.php

@@ -6,7 +6,7 @@
 	<h3 class="font-weight-bold">Developers</h3>
 </div>
 <hr>
-@if(config('pixelfed.oauth_enabled') == true)
+@if(config_cache('pixelfed.oauth_enabled') == true)
 	<passport-clients></passport-clients>
 @else
 	<p class="lead">OAuth has not been enabled on this instance.</p>
@@ -16,4 +16,4 @@
 
 @push('scripts')
 <script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
-@endpush
+@endpush

+ 171 - 159
resources/views/settings/home.blade.php

@@ -2,99 +2,111 @@
 
 @section('section')
 
-  <div class="title">
-    <h3 class="font-weight-bold">Account Settings</h3>
-  </div>
-  <hr>
-  <div class="form-group row">
-    <div class="col-sm-3">
-      <img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" height="38px" class="rounded-circle float-right">
-    </div>
-    <div class="col-sm-9">
-      <p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p>
-      <p class="">
-        <a href="#" class="font-weight-bold change-profile-photo" data-toggle="collapse" data-target="#avatarCollapse" aria-expanded="false" aria-controls="avatarCollapse">Change Profile Photo</a>
-      </p>
-      <div class="collapse" id="avatarCollapse">
-        <form method="post" action="/settings/avatar" enctype="multipart/form-data">
-        @csrf
-        <div class="card card-body">
-          <div class="custom-file mb-1">
-            <input type="file" name="avatar" class="custom-file-input" id="avatarInput">
-            <label class="custom-file-label" for="avatarInput">Select a profile photo</label>
-          </div>
-          <p><span class="small font-weight-bold">Must be a jpeg or png. Max avatar size: <span id="maxAvatarSize"></span></span></p>
-          <div id="previewAvatar"></div>
-          <p class="mb-0"><button type="submit" class="btn btn-primary px-4 py-0 font-weight-bold">Upload</button></p>
-        </div>
-        </form>
-      </div>
-      <p class="">
-        <a class="font-weight-bold text-muted delete-profile-photo" href="#">Delete Profile Photo</a>
-      </p>
-    </div>
-  </div>
-  <form method="post">
-    @csrf
-    <div class="form-group row">
-      <label for="name" class="col-sm-3 col-form-label font-weight-bold">Name</label>
-      <div class="col-sm-9">
-        <input type="text" class="form-control" id="name" name="name" placeholder="Your Name" value="{{Auth::user()->profile->name}}" v-pre>
-      </div>
-    </div>
-    <div class="form-group row">
-      <label for="website" class="col-sm-3 col-form-label font-weight-bold">Website</label>
-      <div class="col-sm-9">
-        <input type="text" class="form-control" id="website" name="website" placeholder="Website" value="{{Auth::user()->profile->website}}" v-pre>
-      </div>
-    </div>
-    <div class="form-group row">
-      <label for="bio" class="col-sm-3 col-form-label font-weight-bold">Bio</label>
-      <div class="col-sm-9">
-        <textarea class="form-control" id="bio" name="bio" placeholder="Add a bio here" rows="2" data-max-length="{{config('pixelfed.max_bio_length')}}" v-pre>{{Auth::user()->profile->bio}}</textarea>
-        <p class="form-text">
-          <span class="bio-counter float-right small text-muted">0/{{config('pixelfed.max_bio_length')}}</span>
-        </p>
-      </div>
-    </div>
-    <div class="form-group row">
-      <label for="language" class="col-sm-3 col-form-label font-weight-bold">Language</label>
-      <div class="col-sm-9">
-        <select class="form-control" name="language">
-        @foreach(App\Util\Localization\Localization::languages() as $lang)
-          <option value="{{$lang}}" {{(Auth::user()->language ?? 'en') == $lang ? 'selected':''}}>{{locale_get_display_language($lang, 'en')}} - {{locale_get_display_language($lang, $lang)}}</option>
-        @endforeach
-        </select>
-      </div>
-    </div>
-    @if(config('pixelfed.enforce_account_limit'))
-    <div class="pt-3">
-      <p class="font-weight-bold text-muted text-center">Storage Usage</p>
-    </div>
-    <div class="form-group row">
-      <label class="col-sm-3 col-form-label font-weight-bold">Storage Used</label>
-      <div class="col-sm-9">
-        <div class="progress mt-2">
-          <div class="progress-bar" role="progressbar" style="width: {{$storage['percentUsed']}}%"  aria-valuenow="{{$storage['percentUsed']}}" aria-valuemin="0" aria-valuemax="100"></div>
-        </div>
-        <div class="help-text">
-          <span class="small text-muted">
-            {{$storage['percentUsed']}}% used
-          </span>
-          <span class="small text-muted float-right">
-            {{$storage['usedPretty']}} / {{$storage['limitPretty']}}
-          </span>
-        </div>
-      </div>
-    </div>
-    @endif
-    <hr>
-    <div class="form-group row">
-      <div class="col-12 text-right">
-        <button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
-      </div>
-    </div>
-  </form>
+	<div class="title">
+		<h3 class="font-weight-bold">Account Settings</h3>
+	</div>
+	<hr>
+	<div class="form-group row">
+		<div class="col-sm-3">
+			<img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" height="38px" class="rounded-circle float-right">
+		</div>
+		<div class="col-sm-9">
+			<p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p>
+			<p class="">
+				<a href="#" class="font-weight-bold change-profile-photo" data-toggle="collapse" data-target="#avatarCollapse" aria-expanded="false" aria-controls="avatarCollapse">Change Profile Photo</a>
+			</p>
+			<div class="collapse" id="avatarCollapse">
+				<form method="post" action="/settings/avatar" enctype="multipart/form-data">
+				@csrf
+				<div class="card card-body">
+					<div class="custom-file mb-1">
+						<input type="file" name="avatar" class="custom-file-input" id="avatarInput">
+						<label class="custom-file-label" for="avatarInput">Select a profile photo</label>
+					</div>
+					<p><span class="small font-weight-bold">Must be a jpeg or png. Max avatar size: <span id="maxAvatarSize"></span></span></p>
+					<div id="previewAvatar"></div>
+					<p class="mb-0"><button type="submit" class="btn btn-primary px-4 py-0 font-weight-bold">Upload</button></p>
+				</div>
+				</form>
+			</div>
+			<p class="">
+				<a class="font-weight-bold text-muted delete-profile-photo" href="#">Delete Profile Photo</a>
+			</p>
+		</div>
+	</div>
+	<form method="post">
+		@csrf
+		<div class="form-group row">
+			<label for="name" class="col-sm-3 col-form-label font-weight-bold">Name</label>
+			<div class="col-sm-9">
+				<input type="text" class="form-control" id="name" name="name" placeholder="Your Name" value="{{Auth::user()->profile->name}}" v-pre>
+			</div>
+		</div>
+		<div class="form-group row">
+			<label for="website" class="col-sm-3 col-form-label font-weight-bold">Website</label>
+			<div class="col-sm-9">
+				<input type="text" class="form-control" id="website" name="website" placeholder="Website" value="{{Auth::user()->profile->website}}" v-pre>
+			</div>
+		</div>
+		<div class="form-group row">
+			<label for="bio" class="col-sm-3 col-form-label font-weight-bold">Bio</label>
+			<div class="col-sm-9">
+				<textarea class="form-control" id="bio" name="bio" placeholder="Add a bio here" rows="2" data-max-length="{{config('pixelfed.max_bio_length')}}" v-pre>{{Auth::user()->profile->bio}}</textarea>
+				<p class="form-text">
+					<span class="bio-counter float-right small text-muted">0/{{config('pixelfed.max_bio_length')}}</span>
+				</p>
+			</div>
+		</div>
+		<div class="form-group row">
+			<label for="language" class="col-sm-3 col-form-label font-weight-bold">Language</label>
+			<div class="col-sm-9">
+				<select class="form-control" name="language">
+				@foreach(App\Util\Localization\Localization::languages() as $lang)
+					<option value="{{$lang}}" {{(Auth::user()->language ?? 'en') == $lang ? 'selected':''}}>{{locale_get_display_language($lang, 'en')}} - {{locale_get_display_language($lang, $lang)}}</option>
+				@endforeach
+				</select>
+			</div>
+		</div>
+		<div class="form-group row">
+			<label for="pronouns" class="col-sm-3 col-form-label font-weight-bold">Pronouns</label>
+			<div class="col-sm-9">
+				<select class="form-control" name="pronouns[]" multiple="" id="pronouns">
+					<option>Select Pronoun(s)</option>
+				@foreach(\App\Services\PronounService::pronouns() as $val)
+					<option value="{{$val}}" {{$pronouns && in_array($val, $pronouns) ? 'selected' : ''}}>{{$val}}</option>
+				@endforeach
+				</select>
+				<p class="help-text text-muted small">Select up to 4 pronouns that will appear on your profile.</p>
+			</div>
+		</div>
+		@if(config_cache('pixelfed.enforce_account_limit'))
+		<div class="pt-3">
+			<p class="font-weight-bold text-muted text-center">Storage Usage</p>
+		</div>
+		<div class="form-group row">
+			<label class="col-sm-3 col-form-label font-weight-bold">Storage Used</label>
+			<div class="col-sm-9">
+				<div class="progress mt-2">
+					<div class="progress-bar" role="progressbar" style="width: {{$storage['percentUsed']}}%"  aria-valuenow="{{$storage['percentUsed']}}" aria-valuemin="0" aria-valuemax="100"></div>
+				</div>
+				<div class="help-text">
+					<span class="small text-muted">
+						{{$storage['percentUsed']}}% used
+					</span>
+					<span class="small text-muted float-right">
+						{{$storage['usedPretty']}} / {{$storage['limitPretty']}}
+					</span>
+				</div>
+			</div>
+		</div>
+		@endif
+		<hr>
+		<div class="form-group row">
+			<div class="col-12 text-right">
+				<button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
+			</div>
+		</div>
+	</form>
 
 @endsection
 
@@ -102,72 +114,72 @@
 <script type="text/javascript">
 
 $(document).ready(function() {
-    let el = $('#bio');
-    let len = el.val().length;
-    let limit = el.data('max-length');
-
-    if(len > 100) {
-      el.attr('rows', '4');
-    }
-
-    let val = len + ' / ' + limit;
-
-    if(len > limit) {
-      let diff = len - limit;
-      val = '<span class="text-danger">-' + diff + '</span> / ' + limit;
-    }
-
-    $('.bio-counter').html(val);
-
-    $('#bio').on('change keyup paste', function(e) {
-      let el = $(this);
-      let len = el.val().length;
-      let limit = el.data('max-length');
-
-      if(len > 100) {
-        el.attr('rows', '4');
-      }
-
-      let val = len + ' / ' + limit;
-
-      if(len > limit) {
-        let diff = len - limit;
-        val = '<span class="text-danger">-' + diff + '</span> / ' + limit;
-      }
-
-      $('.bio-counter').html(val);
-    });
-
-    $(document).on('click', '.modal-close', function(e) {
-      swal.close();
-    });
-
-    $('#maxAvatarSize').text(filesize({{config('pixelfed.max_avatar_size') * 1024}}, {round: 0}));
-
-    $('#avatarInput').on('change', function(e) {
-        var file = document.getElementById('avatarInput').files[0];
-        var reader = new FileReader();
-
-        reader.addEventListener("load", function() {
-            $('#previewAvatar').html('<img src="' + reader.result + '" class="rounded-circle box-shadow mb-3" width="100%" height="100%"/>');
-        }, false);
-
-        if (file) {
-            reader.readAsDataURL(file);
-        }
-    });
-
-    $('.delete-profile-photo').on('click', function(e) {
-      e.preventDefault();
-      if(window.confirm('Are you sure you want to delete your profile photo.') == false) {
-        return;
-      }
-      axios.delete('/settings/avatar').then(res => {
-        window.location.href = window.location.href;
-      }).catch(err => {
-        swal('Error', 'An error occured, please try again later', 'error');
-      });
-    });
+		let el = $('#bio');
+		let len = el.val().length;
+		let limit = el.data('max-length');
+
+		if(len > 100) {
+			el.attr('rows', '4');
+		}
+
+		let val = len + ' / ' + limit;
+
+		if(len > limit) {
+			let diff = len - limit;
+			val = '<span class="text-danger">-' + diff + '</span> / ' + limit;
+		}
+
+		$('.bio-counter').html(val);
+
+		$('#bio').on('change keyup paste', function(e) {
+			let el = $(this);
+			let len = el.val().length;
+			let limit = el.data('max-length');
+
+			if(len > 100) {
+				el.attr('rows', '4');
+			}
+
+			let val = len + ' / ' + limit;
+
+			if(len > limit) {
+				let diff = len - limit;
+				val = '<span class="text-danger">-' + diff + '</span> / ' + limit;
+			}
+
+			$('.bio-counter').html(val);
+		});
+
+		$(document).on('click', '.modal-close', function(e) {
+			swal.close();
+		});
+
+		$('#maxAvatarSize').text(filesize({{config('pixelfed.max_avatar_size') * 1024}}, {round: 0}));
+
+		$('#avatarInput').on('change', function(e) {
+				var file = document.getElementById('avatarInput').files[0];
+				var reader = new FileReader();
+
+				reader.addEventListener("load", function() {
+						$('#previewAvatar').html('<img src="' + reader.result + '" class="rounded-circle box-shadow mb-3" width="100%" height="100%"/>');
+				}, false);
+
+				if (file) {
+						reader.readAsDataURL(file);
+				}
+		});
+
+		$('.delete-profile-photo').on('click', function(e) {
+			e.preventDefault();
+			if(window.confirm('Are you sure you want to delete your profile photo.') == false) {
+				return;
+			}
+			axios.delete('/settings/avatar').then(res => {
+				window.location.href = window.location.href;
+			}).catch(err => {
+				swal('Error', 'An error occured, please try again later', 'error');
+			});
+		});
 })
 
 </script>

+ 2 - 2
resources/views/settings/partial/sidebar.blade.php

@@ -32,7 +32,7 @@
       <li class="nav-item">
         <hr>
       </li>
-      @if(config('pixelfed.import.instagram.enabled'))
+      @if(config_cache('pixelfed.import.instagram.enabled'))
       <li class="nav-item pl-3 {{request()->is('*import*')?'active':''}}">
         <a class="nav-link font-weight-light text-muted" href="{{route('settings.import')}}">Import</a>
       </li>
@@ -41,7 +41,7 @@
         <a class="nav-link font-weight-light text-muted" href="{{route('settings.dataexport')}}">Data Export</a>
       </li>
 
-      @if(config('pixelfed.oauth_enabled') == true)
+      @if(config_cache('pixelfed.oauth_enabled') == true)
       <li class="nav-item">
       <hr>
       </li>

+ 8 - 8
resources/views/site/help/getting-started.blade.php

@@ -20,14 +20,14 @@
 				<li>Go to <a href="{{config('app.url')}}">{{config('app.url')}}</a>.</li>
 				<li>Click on the register link at the top of the page.</li>
 				<li>Enter your name, email address, username and password.</li>
-				@if(config('pixelfed.enforce_email_verification') != true)
+				@if(config_cache('pixelfed.enforce_email_verification') != true)
 				<li>Wait for an account verification email, it may take a few minutes.</li>
 				@endif
 			</ol>
 		</div>
 	</div>
 </p>
-<p>	
+<p>
 	<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse2" role="button" aria-expanded="false" aria-controls="collapse2">
 		<i class="fas fa-chevron-down mr-2"></i>
 		How to I update profile info like name, bio, email?
@@ -38,7 +38,7 @@
 		</div>
 	</div>
 </p>
-<p>	
+<p>
 	<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse3" role="button" aria-expanded="false" aria-controls="collapse3">
 		<i class="fas fa-chevron-down mr-2"></i>
 		What can I do if a username I want is taken but seems inactive?
@@ -49,18 +49,18 @@
 		</div>
 	</div>
 </p>
-<p>	
+<p>
 	<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse4" role="button" aria-expanded="false" aria-controls="collapse4">
 		<i class="fas fa-chevron-down mr-2"></i>
 		Why can't I change my username?
 	</a>
 	<div class="collapse" id="collapse4">
 		<div class="mt-2">
-			Pixelfed is a federated application, changing your username is not supported in every <a href="https://en.wikipedia.org/wiki/ActivityPub">federated software</a> so we cannot allow username changes. Your best option is to create a new account with your desired username. 
+			Pixelfed is a federated application, changing your username is not supported in every <a href="https://en.wikipedia.org/wiki/ActivityPub">federated software</a> so we cannot allow username changes. Your best option is to create a new account with your desired username.
 		</div>
 	</div>
 </p>
-<p>	
+<p>
 	<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse5" role="button" aria-expanded="false" aria-controls="collapse5">
 		<i class="fas fa-chevron-down mr-2"></i>
 		I received an email that I created an account, but I never signed up for one.
@@ -71,7 +71,7 @@
 		</div>
 	</div>
 </p>
-<p>	
+<p>
 	<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse6" role="button" aria-expanded="false" aria-controls="collapse6">
 		<i class="fas fa-chevron-down mr-2"></i>
 		I can't create a new account because an account with this email already exists.
@@ -83,4 +83,4 @@
 	</div>
 </p>
 
-@endsection
+@endsection

+ 1 - 1
resources/views/site/partial/template.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.anon',['title' => 'About ' . config('app.name')])
+@extends('layouts.anon',['title' => 'About ' . config_cache('app.name')])
 
 @section('content')