diff --git a/app/Http/Controllers/Admin/AdminDirectoryController.php b/app/Http/Controllers/Admin/AdminDirectoryController.php
new file mode 100644
index 000000000..1e4db7d2d
--- /dev/null
+++ b/app/Http/Controllers/Admin/AdminDirectoryController.php
@@ -0,0 +1,453 @@
+all())->pluck('name');
+ $res['admins'] = User::whereIsAdmin(true)
+ ->where('2fa_enabled', true)
+ ->get()->map(function($user) {
+ return [
+ 'uid' => (string) $user->id,
+ 'pid' => (string) $user->profile_id,
+ 'username' => $user->username,
+ 'created_at' => $user->created_at
+ ];
+ });
+ $config = ConfigCache::whereK('pixelfed.directory')->first();
+ if($config) {
+ $data = $config->v ? json_decode($config->v, true) : [];
+ $res = array_merge($res, $data);
+ }
+
+ if(empty($res['summary'])) {
+ $summary = ConfigCache::whereK('app.short_description')->pluck('v');
+ $res['summary'] = $summary ? $summary[0] : null;
+ }
+
+ if(isset($res['banner_image']) && !empty($res['banner_image'])) {
+ $res['banner_image'] = url(Storage::url($res['banner_image']));
+ }
+
+ if(isset($res['favourite_posts'])) {
+ $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
+ return StatusService::get($id);
+ })
+ ->filter(function($post) {
+ return $post && isset($post['account']);
+ })
+ ->values();
+ }
+
+ $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
+ $res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
+ $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
+
+ $res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled');
+
+ $res['feature_config'] = [
+ 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
+ 'image_quality' => config_cache('pixelfed.image_quality'),
+ 'optimize_image' => config_cache('pixelfed.optimize_image'),
+ 'max_photo_size' => config_cache('pixelfed.max_photo_size'),
+ 'max_caption_length' => config_cache('pixelfed.max_caption_length'),
+ 'max_altext_length' => config_cache('pixelfed.max_altext_length'),
+ 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
+ 'max_account_size' => config_cache('pixelfed.max_account_size'),
+ 'max_album_length' => config_cache('pixelfed.max_album_length'),
+ 'account_deletion' => config_cache('pixelfed.account_deletion'),
+ ];
+
+ if(config_cache('pixelfed.directory.testimonials')) {
+ $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true))
+ ->map(function($t) {
+ return [
+ 'profile' => AccountService::get($t['profile_id']),
+ 'body' => $t['body']
+ ];
+ });
+ $res['testimonials'] = $testimonials;
+ }
+
+ $validator = Validator::make($res['feature_config'], [
+ 'media_types' => [
+ 'required',
+ function ($attribute, $value, $fail) {
+ if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
+ $fail('You must enable image/jpeg and image/png support.');
+ }
+ },
+ ],
+ 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100',
+ 'max_altext_length' => 'required|integer|min:1000|max:5000',
+ 'max_photo_size' => 'required|integer|min:15000|max:100000',
+ 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
+ 'max_album_length' => 'required|integer|min:4|max:20',
+ 'account_deletion' => 'required|accepted',
+ 'max_caption_length' => 'required|integer|min:500|max:10000'
+ ]);
+
+ $res['requirements_validator'] = $validator->errors();
+
+ $res['is_eligible'] = $res['open_registration'] &&
+ $res['oauth_enabled'] &&
+ $res['activitypub_enabled'] &&
+ count($res['requirements_validator']) === 0 &&
+ $this->validVal($res, 'admin') &&
+ $this->validVal($res, 'summary', null, 10) &&
+ $this->validVal($res, 'favourite_posts', 3) &&
+ $this->validVal($res, 'contact_email') &&
+ $this->validVal($res, 'privacy_pledge') &&
+ $this->validVal($res, 'location');
+
+ $res['has_submitted'] = config_cache('pixelfed.directory.has_submitted') ?? false;
+ $res['synced'] = config_cache('pixelfed.directory.is_synced') ?? false;
+ $res['latest_response'] = config_cache('pixelfed.directory.latest_response') ?? null;
+
+ $path = base_path('resources/lang');
+ $langs = collect([]);
+
+ foreach (new \DirectoryIterator($path) as $io) {
+ $name = $io->getFilename();
+ $skip = ['vendor'];
+ if($io->isDot() || in_array($name, $skip)) {
+ continue;
+ }
+
+ if($io->isDir()) {
+ $langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
+ }
+ }
+
+ $res['available_languages'] = $langs->sortBy('name')->values();
+ $res['primary_locale'] = config('app.locale');
+
+ $submissionState = Http::withoutVerifying()
+ ->post('https://pixelfed.org/api/v1/directory/check-submission', [
+ 'domain' => config('pixelfed.domain.app')
+ ]);
+
+ $res['submission_state'] = $submissionState->json();
+ return $res;
+ }
+
+ protected function validVal($res, $val, $count = false, $minLen = false)
+ {
+ if(!isset($res[$val])) {
+ return false;
+ }
+
+ if($count) {
+ return count($res[$val]) >= $count;
+ }
+
+ if($minLen) {
+ return strlen($res[$val]) >= $minLen;
+ }
+
+ return $res[$val];
+ }
+
+ public function directoryStore(Request $request)
+ {
+ $this->validate($request, [
+ 'location' => 'string|min:1|max:53',
+ 'summary' => 'string|nullable|max:140',
+ 'admin_uid' => 'sometimes|nullable',
+ 'contact_email' => 'sometimes|nullable|email:rfc,dns',
+ 'favourite_posts' => 'array|max:12',
+ 'favourite_posts.*' => 'distinct',
+ 'privacy_pledge' => 'sometimes',
+ 'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000'
+ ]);
+
+ $config = ConfigCache::firstOrNew([
+ 'k' => 'pixelfed.directory'
+ ]);
+
+ $res = $config->v ? json_decode($config->v, true) : [];
+ $res['summary'] = strip_tags($request->input('summary'));
+ $res['favourite_posts'] = $request->input('favourite_posts');
+ $res['admin'] = (string) $request->input('admin_uid');
+ $res['contact_email'] = $request->input('contact_email');
+ $res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
+
+ if($request->filled('location')) {
+ $exists = (new ISO3166)->name($request->location);
+ if($exists) {
+ $res['location'] = $request->input('location');
+ }
+ }
+
+ if($request->hasFile('banner_image')) {
+ collect(Storage::files('public/headers'))
+ ->filter(function($name) {
+ $protected = [
+ 'public/headers/.gitignore',
+ 'public/headers/default.jpg',
+ 'public/headers/missing.png'
+ ];
+ return !in_array($name, $protected);
+ })
+ ->each(function($name) {
+ Storage::delete($name);
+ });
+ $path = $request->file('banner_image')->store('public/headers');
+ $res['banner_image'] = $path;
+ ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
+
+ Cache::forget('api:v1:instance-data-response-v1');
+ }
+
+ $config->v = json_encode($res);
+ $config->save();
+
+ ConfigCacheService::put('pixelfed.directory', $config->v);
+ $updated = json_decode($config->v, true);
+ if(isset($updated['banner_image'])) {
+ $updated['banner_image'] = url(Storage::url($updated['banner_image']));
+ }
+ return $updated;
+ }
+
+ public function directoryHandleServerSubmission(Request $request)
+ {
+ $reqs = [];
+ $reqs['feature_config'] = [
+ 'open_registration' => config_cache('pixelfed.open_registration'),
+ 'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
+ 'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
+ 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
+ 'image_quality' => config_cache('pixelfed.image_quality'),
+ 'optimize_image' => config_cache('pixelfed.optimize_image'),
+ 'max_photo_size' => config_cache('pixelfed.max_photo_size'),
+ 'max_caption_length' => config_cache('pixelfed.max_caption_length'),
+ 'max_altext_length' => config_cache('pixelfed.max_altext_length'),
+ 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
+ 'max_account_size' => config_cache('pixelfed.max_account_size'),
+ 'max_album_length' => config_cache('pixelfed.max_album_length'),
+ 'account_deletion' => config_cache('pixelfed.account_deletion'),
+ ];
+
+ $validator = Validator::make($reqs['feature_config'], [
+ 'open_registration' => 'required|accepted',
+ 'activitypub_enabled' => 'required|accepted',
+ 'oauth_enabled' => 'required|accepted',
+ 'media_types' => [
+ 'required',
+ function ($attribute, $value, $fail) {
+ if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
+ $fail('You must enable image/jpeg and image/png support.');
+ }
+ },
+ ],
+ 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100',
+ 'max_altext_length' => 'required|integer|min:1000|max:5000',
+ 'max_photo_size' => 'required|integer|min:15000|max:100000',
+ 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
+ 'max_album_length' => 'required|integer|min:4|max:20',
+ 'account_deletion' => 'required|accepted',
+ 'max_caption_length' => 'required|integer|min:500|max:10000'
+ ]);
+
+ if(!$validator->validate()) {
+ return response()->json($validator->errors(), 422);
+ }
+
+ ConfigCacheService::put('pixelfed.directory.submission-key', Str::random(random_int(40, 69)));
+ ConfigCacheService::put('pixelfed.directory.submission-ts', now());
+
+ $data = (new PixelfedDirectoryController())->buildListing();
+ $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
+ return 200;
+ }
+
+ public function directoryDeleteBannerImage(Request $request)
+ {
+ $bannerImage = ConfigCache::whereK('app.banner_image')->first();
+ $directory = ConfigCache::whereK('pixelfed.directory')->first();
+ if(!$bannerImage && !$directory || empty($directory->v)) {
+ return;
+ }
+ $directoryArr = json_decode($directory->v, true);
+ $path = isset($directoryArr['banner_image']) ? $directoryArr['banner_image'] : false;
+ $protected = [
+ 'public/headers/.gitignore',
+ 'public/headers/default.jpg',
+ 'public/headers/missing.png'
+ ];
+ if(!$path || in_array($path, $protected)) {
+ return;
+ }
+ if(Storage::exists($directoryArr['banner_image'])) {
+ Storage::delete($directoryArr['banner_image']);
+ }
+
+ $directoryArr['banner_image'] = 'public/headers/default.jpg';
+ $directory->v = $directoryArr;
+ $directory->save();
+ $bannerImage->v = url(Storage::url('public/headers/default.jpg'));
+ $bannerImage->save();
+ Cache::forget('api:v1:instance-data-response-v1');
+ ConfigCacheService::put('pixelfed.directory', $directory);
+ return $bannerImage->v;
+ }
+
+ public function directoryGetPopularPosts(Request $request)
+ {
+ $ids = Cache::remember('admin:api:popular_posts', 86400, function() {
+ return Status::whereLocal(true)
+ ->whereScope('public')
+ ->whereType('photo')
+ ->whereNull(['in_reply_to_id', 'reblog_of_id'])
+ ->orderByDesc('likes_count')
+ ->take(50)
+ ->pluck('id');
+ });
+
+ $res = $ids->map(function($id) {
+ return StatusService::get($id);
+ })
+ ->filter(function($post) {
+ return $post && isset($post['account']);
+ })
+ ->values();
+
+ return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+ }
+
+ public function directoryGetAddPostByIdSearch(Request $request)
+ {
+ $this->validate($request, [
+ 'q' => 'required|integer'
+ ]);
+
+ $id = $request->input('q');
+
+ $status = Status::whereLocal(true)
+ ->whereType('photo')
+ ->whereNull(['in_reply_to_id', 'reblog_of_id'])
+ ->findOrFail($id);
+
+ $res = StatusService::get($status->id);
+
+ return $res;
+ }
+
+ public function directoryDeleteTestimonial(Request $request)
+ {
+ $this->validate($request, [
+ 'profile_id' => 'required',
+ ]);
+ $profile_id = $request->input('profile_id');
+ $testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail();
+ $existing = collect(json_decode($testimonials->v, true))
+ ->filter(function($t) use($profile_id) {
+ return $t['profile_id'] !== $profile_id;
+ })
+ ->values();
+ ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
+ return $existing;
+ }
+
+ public function directorySaveTestimonial(Request $request)
+ {
+ $this->validate($request, [
+ 'username' => 'required',
+ 'body' => 'required|string|min:5|max:500'
+ ]);
+
+ $user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
+
+ $configCache = ConfigCache::firstOrCreate([
+ 'k' => 'pixelfed.directory.testimonials'
+ ]);
+
+ $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
+
+ abort_if($testimonials->contains('profile_id', $user->profile_id), 422, 'Testimonial already exists');
+ abort_if($testimonials->count() == 10, 422, 'You can only have 10 active testimonials');
+
+ $testimonials->push([
+ 'profile_id' => (string) $user->profile_id,
+ 'username' => $request->input('username'),
+ 'body' => $request->input('body')
+ ]);
+
+ $configCache->v = json_encode($testimonials->toArray());
+ $configCache->save();
+ ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
+ $res = [
+ 'profile' => AccountService::get($user->profile_id),
+ 'body' => $request->input('body')
+ ];
+ return $res;
+ }
+
+ public function directoryUpdateTestimonial(Request $request)
+ {
+ $this->validate($request, [
+ 'profile_id' => 'required',
+ 'body' => 'required|string|min:5|max:500'
+ ]);
+
+ $profile_id = $request->input('profile_id');
+ $body = $request->input('body');
+ $user = User::whereProfileId($profile_id)->firstOrFail();
+
+ $configCache = ConfigCache::firstOrCreate([
+ 'k' => 'pixelfed.directory.testimonials'
+ ]);
+
+ $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
+
+ $updated = $testimonials->map(function($t) use($profile_id, $body) {
+ if($t['profile_id'] == $profile_id) {
+ $t['body'] = $body;
+ }
+ return $t;
+ })
+ ->values();
+
+ $configCache->v = json_encode($updated);
+ $configCache->save();
+ ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
+
+ return $updated;
+ }
+}
diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php
index 37e3a7c0d..8a6f019ef 100644
--- a/app/Http/Controllers/AdminController.php
+++ b/app/Http/Controllers/AdminController.php
@@ -20,6 +20,7 @@ use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use App\Http\Controllers\Admin\{
+ AdminDirectoryController,
AdminDiscoverController,
AdminInstanceController,
AdminReportController,
@@ -40,6 +41,7 @@ use App\Models\CustomEmoji;
class AdminController extends Controller
{
use AdminReportController,
+ AdminDirectoryController,
AdminDiscoverController,
// AdminGroupsController,
AdminMediaController,
diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php
index 3387cd273..c2ab4ac1e 100644
--- a/app/Http/Controllers/Api/ApiV1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Controller.php
@@ -8,7 +8,7 @@ use Illuminate\Support\Str;
use App\Util\ActivityPub\Helpers;
use App\Util\Media\Filter;
use Laravel\Passport\Passport;
-use Auth, Cache, DB, URL;
+use Auth, Cache, DB, Storage, URL;
use App\{
Avatar,
Bookmark,
@@ -1375,7 +1375,7 @@ class ApiV1Controller extends Controller
'streaming_api' => 'wss://' . config('pixelfed.domain.app')
],
'stats' => $stats,
- 'thumbnail' => url('img/pixelfed-icon-color.png'),
+ 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
'languages' => [config('app.locale')],
'registrations' => (bool) config_cache('pixelfed.open_registration'),
'approval_required' => false,
diff --git a/app/Http/Controllers/PixelfedDirectoryController.php b/app/Http/Controllers/PixelfedDirectoryController.php
new file mode 100644
index 000000000..6290cd398
--- /dev/null
+++ b/app/Http/Controllers/PixelfedDirectoryController.php
@@ -0,0 +1,167 @@
+filled('sk')) {
+ abort(404);
+ }
+
+ if(!config_cache('pixelfed.directory.submission-key')) {
+ abort(404);
+ }
+
+ if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
+ abort(403);
+ }
+
+ $res = $this->buildListing();
+ return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+ }
+
+ public function buildListing()
+ {
+ $res = config_cache('pixelfed.directory');
+ if($res) {
+ $res = is_string($res) ? json_decode($res, true) : $res;
+ }
+
+ $res['_domain'] = config_cache('pixelfed.domain.app');
+ $res['_sk'] = config_cache('pixelfed.directory.submission-key');
+ $res['_ts'] = config_cache('pixelfed.directory.submission-ts');
+ $res['version'] = config_cache('pixelfed.version');
+
+ if(empty($res['summary'])) {
+ $summary = ConfigCache::whereK('app.short_description')->pluck('v');
+ $res['summary'] = $summary ? $summary[0] : null;
+ }
+
+ if(isset($res['admin'])) {
+ $res['admin'] = AccountService::get($res['admin'], true);
+ }
+
+ if(isset($res['banner_image']) && !empty($res['banner_image'])) {
+ $res['banner_image'] = url(Storage::url($res['banner_image']));
+ }
+
+ if(isset($res['favourite_posts'])) {
+ $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
+ return StatusService::get($id);
+ })
+ ->filter(function($post) {
+ return $post && isset($post['account']);
+ })
+ ->map(function($post) {
+ return [
+ 'avatar' => $post['account']['avatar'],
+ 'display_name' => $post['account']['display_name'],
+ 'username' => $post['account']['username'],
+ 'media' => $post['media_attachments'][0]['url'],
+ 'url' => $post['url']
+ ];
+ })
+ ->values();
+ }
+
+ $guidelines = ConfigCache::whereK('app.rules')->first();
+ if($guidelines) {
+ $res['community_guidelines'] = json_decode($guidelines->v, true);
+ }
+
+ $openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first();
+ if($openRegistration) {
+ $res['open_registration'] = (bool) $openRegistration;
+ }
+
+ $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first();
+ if($oauthEnabled) {
+ $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
+ $res['oauth_enabled'] = (bool) $oauthEnabled && $keys;
+ }
+
+ $activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first();
+ if($activityPubEnabled) {
+ $res['activitypub_enabled'] = (bool) $activityPubEnabled;
+ }
+
+ $res['feature_config'] = [
+ 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
+ 'image_quality' => config_cache('pixelfed.image_quality'),
+ 'optimize_image' => config_cache('pixelfed.optimize_image'),
+ 'max_photo_size' => config_cache('pixelfed.max_photo_size'),
+ 'max_caption_length' => config_cache('pixelfed.max_caption_length'),
+ 'max_altext_length' => config_cache('pixelfed.max_altext_length'),
+ 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
+ 'max_account_size' => config_cache('pixelfed.max_account_size'),
+ 'max_album_length' => config_cache('pixelfed.max_album_length'),
+ 'account_deletion' => config_cache('pixelfed.account_deletion'),
+ ];
+
+ $res['is_eligible'] = $this->validVal($res, 'admin') &&
+ $this->validVal($res, 'summary', null, 10) &&
+ $this->validVal($res, 'favourite_posts', 3) &&
+ $this->validVal($res, 'contact_email') &&
+ $this->validVal($res, 'privacy_pledge') &&
+ $this->validVal($res, 'location');
+
+ if(config_cache('pixelfed.directory.testimonials')) {
+ $res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
+ ->map(function($testimonial) {
+ $profile = AccountService::get($testimonial['profile_id']);
+ return [
+ 'profile' => [
+ 'username' => $profile['username'],
+ 'display_name' => $profile['display_name'],
+ 'avatar' => $profile['avatar'],
+ 'created_at' => $profile['created_at']
+ ],
+ 'body' => $testimonial['body']
+ ];
+ });
+ }
+
+ $res['features_enabled'] = [
+ 'stories' => (bool) config_cache('instance.stories.enabled')
+ ];
+
+ $res['stats'] = [
+ 'user_count' => \App\User::count(),
+ 'post_count' => \App\Status::whereNull('uri')->count(),
+ ];
+
+ $res['primary_locale'] = config('app.locale');
+ $hash = hash('sha256', json_encode($res));
+ $res['_hash'] = $hash;
+ ksort($res);
+
+ return $res;
+ }
+
+ protected function validVal($res, $val, $count = false, $minLen = false)
+ {
+ if(!isset($res[$val])) {
+ return false;
+ }
+
+ if($count) {
+ return count($res[$val]) >= $count;
+ }
+
+ if($minLen) {
+ return strlen($res[$val]) >= $minLen;
+ }
+
+ return $res[$val];
+ }
+
+}
diff --git a/app/Models/ConfigCache.php b/app/Models/ConfigCache.php
index 4698b1c6b..1b4a18108 100644
--- a/app/Models/ConfigCache.php
+++ b/app/Models/ConfigCache.php
@@ -10,5 +10,5 @@ class ConfigCache extends Model
use HasFactory;
protected $table = 'config_cache';
- public $fillable = ['*'];
+ public $guarded = [];
}
diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php
index b334e9e33..0a9055287 100644
--- a/app/Services/ConfigCacheService.php
+++ b/app/Services/ConfigCacheService.php
@@ -55,6 +55,15 @@ class ConfigCacheService
'config.discover.features',
'instance.has_legal_notice',
+
+ 'pixelfed.directory',
+ 'app.banner_image',
+ 'pixelfed.directory.submission-key',
+ 'pixelfed.directory.submission-ts',
+ 'pixelfed.directory.has_submitted',
+ 'pixelfed.directory.latest_response',
+ 'pixelfed.directory.is_synced',
+ 'pixelfed.directory.testimonials',
// 'system.user_mode'
];
diff --git a/public/css/admin.css b/public/css/admin.css
index 1f0afb047..2df11cc56 100644
Binary files a/public/css/admin.css and b/public/css/admin.css differ
diff --git a/public/js/admin.js b/public/js/admin.js
index a9626e9fc..c84e6e2fe 100644
Binary files a/public/js/admin.js and b/public/js/admin.js differ
diff --git a/public/mix-manifest.json b/public/mix-manifest.json
index 2f76f51b0..7cada2bac 100644
Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ
diff --git a/resources/views/admin/directory/home.blade.php b/resources/views/admin/directory/home.blade.php
new file mode 100644
index 000000000..dcc5e050e
--- /dev/null
+++ b/resources/views/admin/directory/home.blade.php
@@ -0,0 +1,12 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+
+