Merge pull request #1388 from pixelfed/frontend-ui-refactor

Add Remote Follows
This commit is contained in:
daniel 2019-06-16 16:32:38 -06:00 committed by GitHub
commit fe98b65d16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 667 additions and 170 deletions

View file

@ -15,9 +15,9 @@ LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE=pixelfed
DB_USERNAME=pixelfed
DB_PASSWORD=pixelfed
BROADCAST_DRIVER=log
CACHE_DRIVER=redis

View file

@ -71,7 +71,7 @@ class FixUsernames extends Command
foreach($affected as $u) {
$old = $u->username;
$this->info("Found user: {$old}");
$opt = $this->choice('Select fix method:', $opts, 0);
$opt = $this->choice('Select fix method:', $opts, 3);
switch ($opt) {
case $opts[0]:

View file

@ -83,6 +83,14 @@ class Installer extends Command
'mbstring',
'openssl'
];
$ffmpeg = exec('which ffmpeg');
if(empty($ffmpeg)) {
$this->error('FFmpeg not found, please install it.');
$this->error('Cancelling installation.');
exit;
} else {
$this->info('- Found FFmpeg!');
}
$this->line('');
$this->info('Checking for required php extensions...');
foreach($extensions as $ext) {
@ -90,9 +98,9 @@ class Installer extends Command
$this->error("- {$ext} extension not found, aborting installation");
exit;
} else {
$this->info("- {$ext} extension found!");
}
}
$this->info("- Required PHP extensions found!");
}
protected function checkPermissions()
@ -119,7 +127,7 @@ class Installer extends Command
protected function envCheck()
{
if(!file_exists(base_path('.env'))) {
if(!file_exists(base_path('.env')) || filesize(base_path('.env')) == 0) {
$this->line('');
$this->info('No .env configuration file found. We will create one now!');
$this->createEnv();
@ -148,18 +156,100 @@ class Installer extends Command
{
$this->line('');
// copy env
$name = $this->ask('Site name [ex: Pixelfed]');
$domain = $this->ask('Site Domain [ex: pixelfed.com]');
$tls = $this->choice('Use HTTPS/TLS?', ['https', 'http'], 0);
$dbDrive = $this->choice('Select database driver', ['mysql', 'pgsql'/*, 'sqlite', 'sqlsrv'*/], 0);
$ws = $this->choice('Select cache driver', ["apc", "array", "database", "file", "memcached", "redis"], 5);
if(!file_exists(app()->environmentFilePath())) {
exec('cp .env.example .env');
$this->call('key:generate');
}
$name = $this->ask('Site name [ex: Pixelfed]');
$this->updateEnvFile('APP_NAME', $name ?? 'pixelfed');
$domain = $this->ask('Site Domain [ex: pixelfed.com]');
$this->updateEnvFile('APP_DOMAIN', $domain ?? 'example.org');
$this->updateEnvFile('ADMIN_DOMAIN', $domain ?? 'example.org');
$this->updateEnvFile('SESSION_DOMAIN', $domain ?? 'example.org');
$this->updateEnvFile('APP_URL', 'https://' . $domain ?? 'https://example.org');
$database = $this->choice('Select database driver', ['mysql', 'pgsql'], 0);
$this->updateEnvFile('DB_CONNECTION', $database ?? 'mysql');
switch ($database) {
case 'mysql':
$database_host = $this->ask('Select database host', '127.0.0.1');
$this->updateEnvFile('DB_HOST', $database_host ?? 'mysql');
$database_port = $this->ask('Select database port', 3306);
$this->updateEnvFile('DB_PORT', $database_port ?? 3306);
$database_db = $this->ask('Select database', 'pixelfed');
$this->updateEnvFile('DB_DATABASE', $database_db ?? 'pixelfed');
$database_username = $this->ask('Select database username', 'pixelfed');
$this->updateEnvFile('DB_USERNAME', $database_username ?? 'pixelfed');
$db_pass = str_random(64);
$database_password = $this->secret('Select database password', $db_pass);
$this->updateEnvFile('DB_PASSWORD', $database_password);
break;
}
$cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
$this->updateEnvFile('CACHE_DRIVER', $cache ?? 'redis');
$session = $this->choice('Select session driver', ["redis", "file", "cookie", "database", "apc", "memcached", "array"], 0);
$this->updateEnvFile('SESSION_DRIVER', $cache ?? 'redis');
$redis_host = $this->ask('Set redis host', 'localhost');
$this->updateEnvFile('REDIS_HOST', $redis_host);
$redis_password = $this->ask('Set redis password', 'null');
$this->updateEnvFile('REDIS_PASSWORD', $redis_password);
$redis_port = $this->ask('Set redis port', 6379);
$this->updateEnvFile('REDIS_PORT', $redis_port);
$open_registration = $this->choice('Allow new registrations?', ['true', 'false'], 1);
$this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
$enforce_email_verification = $this->choice('Enforce email verification?', ['true', 'false'], 0);
$this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
}
protected function updateEnvFile($key, $value)
{
$envPath = app()->environmentFilePath();
$payload = file_get_contents($envPath);
if ($existing = $this->existingEnv($key, $payload)) {
$payload = str_replace("{$key}={$existing}", "{$key}=\"{$value}\"", $payload);
$this->storeEnv($payload);
} else {
$payload = $payload . "\n{$key}=\"{$value}\"\n";
$this->storeEnv($payload);
}
}
protected function existingEnv($needle, $haystack)
{
preg_match("/^{$needle}=[^\r\n]*/m", $haystack, $matches);
if ($matches && count($matches)) {
return substr($matches[0], strlen($needle) + 1);
}
return false;
}
protected function storeEnv($payload)
{
$file = fopen(app()->environmentFilePath(), 'w');
fwrite($file, $payload);
fclose($file);
}
protected function postInstall()
{
$this->callSilent('config:cache');
//$this->call('route:cache');
//$this->callSilent('route:cache');
$this->info('Pixelfed has been successfully installed!');
}
}

View file

@ -7,7 +7,6 @@ use Illuminate\Http\Request;
use Carbon\Carbon;
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
use App\Http\Controllers\Controller;
use Jackiedo\DotenvEditor\Facades\DotenvEditor;
use App\Util\Lexer\PrettyNumber;
trait AdminSettingsController
@ -24,9 +23,11 @@ trait AdminSettingsController
return view('admin.settings.backups', compact('files'));
}
public function settingsConfig(Request $request, DotenvEditor $editor)
public function settingsConfig(Request $request)
{
return view('admin.settings.config', compact('editor'));
$editor = [];
$config = file_get_contents(base_path('.env'));
return view('admin.settings.config', compact('editor', 'config'));
}
public function settingsMaintenance(Request $request)
@ -50,9 +51,7 @@ trait AdminSettingsController
$this->validate($request, [
'APP_NAME' => 'required|string',
]);
Artisan::call('config:clear');
DotenvEditor::setKey('APP_NAME', $request->input('APP_NAME'));
DotenvEditor::save();
// Artisan::call('config:clear');
return redirect()->back();
}

View file

@ -17,7 +17,6 @@ use App\{
use DB, Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Jackiedo\DotenvEditor\DotenvEditor;
use App\Http\Controllers\Admin\{
AdminDiscoverController,
AdminInstanceController,

View file

@ -11,6 +11,7 @@ use App\{
use Auth, Cache;
use Illuminate\Http\Request;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Util\ActivityPub\Helpers;
class FollowerController extends Controller
{
@ -55,12 +56,20 @@ class FollowerController extends Controller
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->count();
if($private == true && $isFollowing == 0 || $remote == true) {
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
}
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
}
$follow = FollowRequest::firstOrCreate([
'follower_id' => $user->id,
'following_id' => $target->id
]);
if($remote == true) {
if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target);
}
} elseif ($isFollowing == 0) {
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
@ -77,6 +86,9 @@ class FollowerController extends Controller
FollowPipeline::dispatch($follower);
} else {
$follower = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->firstOrFail();
if($remote == true) {
$this->sendUndoFollow($user, $target);
}
$follower->delete();
}
@ -88,4 +100,46 @@ class FollowerController extends Controller
Cache::forget('user:account:id:'.$target->user_id);
Cache::forget('user:account:id:'.$user->user_id);
}
protected function sendFollow($user, $target)
{
if($target->domain == null || $user->domain != null) {
return;
}
$payload = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'type' => 'Follow',
'actor' => $user->permalink(),
'object' => $target->permalink()
];
$inbox = $target->sharedInbox ?? $target->inbox_url;
Helpers::sendSignedObject($user, $inbox, $payload);
}
protected function sendUndoFollow($user, $target)
{
if($target->domain == null || $user->domain != null) {
return;
}
$payload = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $user->permalink('#follow/'.$target->id.'/undo'),
'type' => 'Undo',
'actor' => $user->permalink(),
'object' => [
'id' => $user->permalink('#follows/'.$target->id),
'actor' => $user->permalink(),
'object' => $target->permalink(),
'type' => 'Follow'
]
];
$inbox = $target->sharedInbox ?? $target->inbox_url;
Helpers::sendSignedObject($user, $inbox, $payload);
}
}

View file

@ -134,16 +134,7 @@ trait PrivacySettings
public function blockedInstanceStore(Request $request)
{
$this->validate($request, [
'domain' => [
'required',
'min:3',
'max:100',
function($attribute, $value, $fail) {
if(!filter_var($value, FILTER_VALIDATE_DOMAIN)) {
$fail($attribute. 'is invalid');
}
}
]
'domain' => 'required|active_url'
]);
$domain = $request->input('domain');
$instance = Instance::firstOrCreate(['domain' => $domain]);

View file

@ -18,7 +18,11 @@ trait RelationshipSettings
public function relationshipsHome()
{
return view('settings.relationships.home');
$profile = Auth::user()->profile;
$following = $profile->following()->simplePaginate(10);
$followers = $profile->followers()->simplePaginate(10);
return view('settings.relationships.home', compact('profile', 'following', 'followers'));
}
}

View file

@ -14,6 +14,7 @@ use App\Http\Controllers\Settings\{
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings
};
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
@ -24,6 +25,7 @@ class SettingsController extends Controller
LabsSettings,
HomeSettings,
PrivacySettings,
RelationshipSettings,
SecuritySettings;
public function __construct()

View file

@ -56,7 +56,7 @@ class RemoteFollowImportRecent implements ShouldQueue
*/
public function handle()
{
$outbox = $this->fetchOutbox();
// $outbox = $this->fetchOutbox();
}
public function fetchOutbox($url = false)
@ -216,7 +216,7 @@ class RemoteFollowImportRecent implements ShouldQueue
$info = pathinfo($url);
$url = str_replace(' ', '%20', $url);
$img = file_get_contents($url);
$file = '/tmp/'.str_random(12).$info['basename'];
$file = '/tmp/'.str_random(64);
file_put_contents($file, $img);
$path = Storage::putFile($storagePath, new File($file), 'public');
@ -231,6 +231,8 @@ class RemoteFollowImportRecent implements ShouldQueue
ImageThumbnail::dispatch($media);
@unlink($file);
return true;
} catch (Exception $e) {
return false;

View file

@ -31,9 +31,39 @@ class AuthLogin
return;
}
$this->userProfile($user);
$this->userSettings($user);
$this->userState($user);
$this->userDevice($user);
$this->userProfileId($user);
}
protected function userProfile($user)
{
if (empty($user->profile)) {
DB::transaction(function() use($user) {
$profile = new Profile();
$profile->user_id = $user->id;
$profile->username = $user->username;
$profile->name = $user->name;
$pkiConfig = [
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
$pki = openssl_pkey_new($pkiConfig);
openssl_pkey_export($pki, $pki_private);
$pki_public = openssl_pkey_get_details($pki);
$pki_public = $pki_public['key'];
$profile->private_key = $pki_private;
$profile->public_key = $pki_public;
$profile->save();
CreateAvatar::dispatch($profile);
});
}
}
protected function userSettings($user)
@ -88,4 +118,15 @@ class AuthLogin
]);
});
}
protected function userProfileId($user)
{
if($user->profile_id == null) {
DB::transaction(function() use($user) {
$profile = $user->profile;
$user->profile_id = $profile->id;
$user->save();
});
}
}
}

View file

@ -20,7 +20,7 @@ class UserObserver
public function saved(User $user)
{
if (empty($user->profile)) {
DB::transaction(function() use($user) {
$profile = DB::transaction(function() use($user) {
$profile = new Profile();
$profile->user_id = $user->id;
$profile->username = $user->username;
@ -38,9 +38,16 @@ class UserObserver
$profile->private_key = $pki_private;
$profile->public_key = $pki_public;
$profile->save();
return $profile;
});
DB::transaction(function() use($user, $profile) {
$user = User::findOrFail($user->id);
$user->profile_id = $profile->id;
$user->save();
CreateAvatar::dispatch($profile);
});
}
if (empty($user->settings)) {

View file

@ -65,7 +65,6 @@ class Status extends Model
return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
}
// todo: deprecate after 0.6.0
public function viewType()
{
if($this->type) {
@ -74,7 +73,6 @@ class Status extends Model
return $this->setType();
}
// todo: deprecate after 0.6.0
public function setType()
{
if(in_array($this->type, self::STATUS_TYPES)) {

View file

@ -55,8 +55,8 @@ class Helpers {
$activity = $data['object'];
$mediaTypes = ['Document', 'Image', 'Video'];
$mimeTypes = ['image/jpeg', 'image/png', 'video/mp4'];
$mimeTypes = explode(',', config('pixelfed.media_types'));
$mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image'];
if(!isset($activity['attachment']) || empty($activity['attachment'])) {
return false;
@ -249,7 +249,6 @@ class Helpers {
}
if(isset($res['cc']) == true) {
$scope = 'unlisted';
if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
$scope = 'unlisted';
}
@ -285,6 +284,12 @@ class Helpers {
}
}
if(!self::validateUrl($res['id']) ||
!self::validateUrl($activity['object']['attributedTo'])
) {
abort(400, 'Invalid object url');
}
$idDomain = parse_url($res['id'], PHP_URL_HOST);
$urlDomain = parse_url($url, PHP_URL_HOST);
$actorDomain = parse_url($activity['object']['attributedTo'], PHP_URL_HOST);
@ -339,6 +344,7 @@ class Helpers {
$userHash = hash('sha1', $user->id.(string) $user->created_at);
$storagePath = "public/m/{$monthHash}/{$userHash}";
$allowed = explode(',', config('pixelfed.media_types'));
foreach($attachments as $media) {
$type = $media['mediaType'];
$url = $media['url'];
@ -370,6 +376,8 @@ class Helpers {
ImageOptimize::dispatch($media);
unlink($file);
}
$status->viewType();
return;
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Util\ActivityPub\Validator;
use Validator;
use Illuminate\Validation\Rule;
class UndoFollow {
public static function validate($payload)
{
$valid = Validator::make($payload, [
'@context' => 'required',
'id' => 'required|string',
'type' => [
'required',
Rule::in(['Undo'])
],
'actor' => 'required|url',
'object.actor' => 'required|url',
'object.object' => 'required|url',
'object.type' => [
'required',
Rule::in(['Follow'])
],
])->passes();
return $valid;
}
}

View file

@ -17,7 +17,7 @@ return [
'inbox' => env('AP_INBOX', true),
'sharedInbox' => env('AP_SHAREDINBOX', false),
'remoteFollow' => env('AP_REMOTEFOLLOW', false),
'remoteFollow' => false,
'delivery' => [
'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddProfileIdsToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->bigInteger('profile_id')->unique()->unsigned()->nullable()->index()->after('id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('profile_id');
});
}
}

60
package-lock.json generated
View file

@ -783,6 +783,23 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@nuxt/opencollective": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.2.2.tgz",
"integrity": "sha512-ie50SpS47L+0gLsW4yP23zI/PtjsDRglyozX2G09jeiUazC1AJlGPZo0JUs9iuCDUoIgsDEf66y7/bSfig0BpA==",
"requires": {
"chalk": "^2.4.1",
"consola": "^2.3.0",
"node-fetch": "^2.3.0"
},
"dependencies": {
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
}
}
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -1684,37 +1701,22 @@
"integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag=="
},
"bootstrap-vue": {
"version": "2.0.0-rc.22",
"resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.0.0-rc.22.tgz",
"integrity": "sha512-QA363prZJZ5HFzAPAlj93yy7FLOwPU0P465kaz8l9COa5t5q5az2X+lMgmlp5c1Fe8yBUhc316KM1itbJqzVUg==",
"version": "2.0.0-rc.23",
"resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.0.0-rc.23.tgz",
"integrity": "sha512-N8D4yjTZ6nTBiw2mtv3xutg46V/eLK5VJpSuC/WJZmeGie34Qls3FtVv7QK5OH4nAG+H6O0qyz4mxOLC1C35Mw==",
"requires": {
"@nuxt/opencollective": "^0.2.2",
"bootstrap": "^4.3.1",
"core-js": ">=2.6.5 <3.0.0",
"popper.js": "^1.15.0",
"portal-vue": "^2.1.4",
"vue-functional-data-merge": "^2.0.7"
"portal-vue": "^2.1.5",
"vue-functional-data-merge": "^3.1.0"
},
"dependencies": {
"@nuxt/opencollective": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.2.2.tgz",
"integrity": "sha512-ie50SpS47L+0gLsW4yP23zI/PtjsDRglyozX2G09jeiUazC1AJlGPZo0JUs9iuCDUoIgsDEf66y7/bSfig0BpA==",
"requires": {
"chalk": "^2.4.1",
"consola": "^2.3.0",
"node-fetch": "^2.3.0"
}
},
"core-js": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
}
}
},
@ -5514,9 +5516,9 @@
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
},
"laravel-echo": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.5.3.tgz",
"integrity": "sha512-CFm0Kruz2zqAwTFSA5X9X5BmIvXYEmrHhcVp5nu4uIdhyObHohWAUBNUD34J4QsIR2J4nGSzB+wi/wsACwlPcg=="
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.5.4.tgz",
"integrity": "sha512-FTfgLQopGTPxIthYqFLbaZQ14kDuRH0AJa7N7HJNmWM5zJ4/qtZcP6zsfvATGazF+5Sr0M7IWgtF+OaNIUHqcw=="
},
"laravel-mix": {
"version": "4.0.16",
@ -9631,17 +9633,17 @@
"dev": true
},
"vue-content-loader": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-0.2.1.tgz",
"integrity": "sha1-DrMy4qcmQ9V/sgnXLWUmVzsZH1o=",
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-0.2.2.tgz",
"integrity": "sha512-8jcb0dJFiVAz7EPwpQjOd/GnswUiSDeKihEABkq/iAYxAI2MHSS7+VWlRblQDH3D1rm3Lewt7fDJoOpJKbYHjw==",
"requires": {
"babel-helper-vue-jsx-merge-props": "^2.0.3"
}
},
"vue-functional-data-merge": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-2.0.7.tgz",
"integrity": "sha512-pvLc+H+x2prwBj/uSEIITyxjz/7ZUVVK8uYbrYMmhDvMXnzh9OvQvVEwcOSBQjsubd4Eq41/CSJaWzy4hemMNQ=="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
"integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA=="
},
"vue-hot-reload-api": {
"version": "2.3.3",

View file

@ -17,8 +17,6 @@
"jquery": "^3.4.1",
"lodash": "^4.17.11",
"popper.js": "^1.15.0",
"purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0",
"resolve-url-loader": "^2.3.2",
"sass": "^1.21.0",
"sass-loader": "^7.1.0",
@ -26,12 +24,12 @@
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"bootstrap-vue": "^2.0.0-rc.22",
"bootstrap-vue": "^2.0.0-rc.23",
"emoji-mart-vue": "^2.6.6",
"filesize": "^3.6.1",
"howler": "^2.1.2",
"infinite-scroll": "^3.0.6",
"laravel-echo": "^1.5.3",
"laravel-echo": "^1.5.4",
"laravel-mix": "^4.0.16",
"node-sass": "^4.12.0",
"opencollective": "^1.0.3",
@ -44,7 +42,7 @@
"socket.io-client": "^2.2.0",
"sweetalert": "^2.1.2",
"twitter-text": "^2.0.5",
"vue-content-loader": "^0.2.1",
"vue-content-loader": "^0.2.2",
"vue-infinite-loading": "^2.4.4",
"vue-loading-overlay": "^3.2.0",
"vue-timeago": "^5.1.2"

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/css/appdark.css vendored

Binary file not shown.

BIN
public/css/landing.css vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -474,6 +474,10 @@ export default {
compose() {
let state = this.composeState;
if(this.uploadProgress != 100 || this.ids.length == 0) {
return;
}
switch(state) {
case 'publish' :
if(this.media.length == 0) {

View file

@ -104,7 +104,7 @@
</div>
</div>
</div>
<div class="d-flex flex-md-column flex-column-reverse h-100">
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
<div class="card-body status-comments pb-5">
<div class="status-comment">
<p :class="[status.content.length > 420 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
@ -433,7 +433,7 @@
background: transparent;
}
</style>
<style type="text/css">
<style type="text/css" scoped>
.momentui .bg-dark {
background: #000 !important;
}

View file

@ -579,7 +579,7 @@ export default {
}
})
.then(res => {
let data = res.data;
let data = res.data.filter(status => status.media_attachments.length > 0);
let ids = data.map(status => status.id);
this.ids = ids;
this.min_id = Math.max(...ids);

View file

@ -48,7 +48,7 @@
<label class="form-check-label font-weight-bold" for="video_autoplay">
{{__('Disable video autoplay')}}
</label>
<p class="text-muted small help-text">Prevent videos from autoplaying. <a href="#">Learn more</a>.</p>
<p class="text-muted small help-text">Prevent videos from autoplaying.</p>
</div>
<div class="form-group row mt-5 pt-5">
<div class="col-12 text-right">

View file

@ -6,8 +6,31 @@
<h3 class="font-weight-bold">Email Settings</h3>
</div>
<hr>
<div class="alert alert-danger">
Coming Soon
</div>
<form method="post" action="{{route('settings')}}">
@csrf
<input type="hidden" class="form-control" name="name" value="{{Auth::user()->profile->name}}">
<input type="hidden" class="form-control" name="username" value="{{Auth::user()->profile->username}}">
<input type="hidden" class="form-control" name="website" value="{{Auth::user()->profile->website}}">
<div class="form-group row">
<label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Email</label>
<div class="col-sm-9">
<input type="email" class="form-control" id="email" name="email" placeholder="Email Address" value="{{Auth::user()->email}}">
<p class="help-text small text-muted font-weight-bold">
@if(Auth::user()->email_verified_at)
<span class="text-success">Verified</span> {{Auth::user()->email_verified_at->diffForHumans()}}
@else
<span class="text-danger">Unverified</span> You need to <a href="/i/verify-email">verify your email</a>.
@endif
</p>
</div>
</div>
<hr>
<div class="form-group row">
<div class="col-12 text-right">
<button type="submit" class="btn btn-primary font-weight-bold float-right">Submit</button>
</div>
</div>
</form>
@endsection

View file

@ -63,23 +63,7 @@
</p>
</div>
</div>
<div class="pt-5">
<p class="font-weight-bold text-muted text-center">Private Information</p>
</div>
<div class="form-group row">
<label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Email</label>
<div class="col-sm-9">
<input type="email" class="form-control" id="email" name="email" placeholder="Email Address" value="{{Auth::user()->email}}">
<p class="help-text small text-muted font-weight-bold">
@if(Auth::user()->email_verified_at)
<span class="text-success">Verified</span> {{Auth::user()->email_verified_at->diffForHumans()}}
@else
<span class="text-danger">Unverified</span> You need to <a href="/i/verify-email">verify your email</a>.
@endif
</p>
</div>
</div>
<div class="pt-5">
<div class="pt-3">
<p class="font-weight-bold text-muted text-center">Storage Usage</p>
</div>
<div class="form-group row">
@ -98,30 +82,12 @@
</div>
</div>
</div>
<div class="pt-5">
<p class="font-weight-bold text-muted text-center">Layout</p>
</div>
<div class="alert alert-primary font-weight-bold text-center">Experimental features have been moved to the <a href="/settings/labs">Labs</a> settings page.</div>
<hr>
@if(config('pixelfed.account_deletion') == true)
<div class="form-group row py-3">
<div class="col-12 d-flex align-items-center justify-content-between">
<a class="font-weight-bold" href="{{route('settings.remove.temporary')}}">Temporarily Disable Account</a>
<button type="submit" class="btn btn-primary font-weight-bold float-right">Submit</button>
</div>
</div>
<hr>
<p class="mb-0 text-center pt-4">
<a class="font-weight-bold text-danger" href="{{route('settings.remove.permanent')}}">Delete Account</a>
</p>
@else
<div class="form-group row">
<div class="col-12 d-flex align-items-center justify-content-between">
<a class="font-weight-bold" href="{{route('settings.remove.temporary')}}">Temporarily Disable Account</a>
<div class="col-12 text-right">
<button type="submit" class="btn btn-primary font-weight-bold float-right">Submit</button>
</div>
</div>
@endif
</form>
@endsection

View file

@ -1,18 +1,17 @@
<div class="col-12 col-md-3 py-3" style="border-right:1px solid #ccc;">
<ul class="nav flex-column settings-nav">
<li class="nav-item pl-3 {{request()->is('settings/home')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings')}}">Profile</a>
<a class="nav-link font-weight-light text-muted" href="{{route('settings')}}">Account</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/password')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.password')}}">Password</a>
</li>
{{--
<li class="nav-item pl-3 {{request()->is('settings/accessibility')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.accessibility')}}">Accessibility</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/email')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.email')}}">Email</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/relationships*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.relationships')}}">Followers</a>
</li>
@if(config('pixelfed.user_invites.enabled'))
<li class="nav-item pl-3 {{request()->is('settings/invites*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.invites')}}">Invites</a>
@ -21,14 +20,16 @@
<li class="nav-item pl-3 {{request()->is('settings/notifications')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.notifications')}}">Notifications</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/reports*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.reports')}}">Reports</a>
<li class="nav-item pl-3 {{request()->is('settings/password')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.password')}}">Password</a>
</li>
--}}
<li class="nav-item pl-3 {{request()->is('settings/privacy*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.privacy')}}">Privacy</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/reports*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.reports')}}">Reports</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/security*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.security')}}">Security</a>
</li>
@ -41,16 +42,16 @@
<li class="nav-item pl-3 {{request()->is('settings/data-export')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.dataexport')}}">Data Export</a>
</li>
{{--
<li class="nav-item">
<hr>
</li>
<li class="nav-item pl-3 {{request()->is('settings/applications')?'active':''}}">
{{-- <li class="nav-item pl-3 {{request()->is('settings/applications')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.applications')}}">Applications</a>
</li>
</li> --}}
<li class="nav-item pl-3 {{request()->is('settings/developers')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.developers')}}">Developers</a>
</li> --}}
</li>
<li class="nav-item">
<hr>
</li>

View file

@ -0,0 +1,117 @@
@extends('settings.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold">Followers & Following</h3>
</div>
<hr>
@if(empty($following) && empty($followers))
<p class="text-center lead pt-5 mt-5">You are not following anyone, or followed by anyone.</p>
@else
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="pt-0 pb-1 mt-0">
<input type="checkbox" name="check" class="form-control check-all">
</th>
<th scope="col">Username</th>
<th scope="col">Relationship</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
@foreach($followers as $follower)
<tr>
<th scope="row" class="pb-0 pt-1 my-0">
{{-- <input type="checkbox" class="form-control mr-1 check-row"> --}}
</th>
<td class="font-weight-bold">
<img src="{{$follower->avatarUrl()}}" width="20px" height="20px" class="rounded-circle border mr-2">{{$follower->username}}
</td>
<td class="text-center">Follower</td>
<td class="text-center">
<a class="btn btn-outline-primary btn-sm py-0 action-btn" href="#" data-id="{{$follower->id}}" data-action="mute">Mute</a>
<a class="btn btn-outline-danger btn-sm py-0 action-btn" href="#" data-id="{{$follower->id}}" data-action="block">Block</a>
</td>
</tr>
@endforeach
@foreach($following as $follower)
<tr>
<th scope="row" class="pb-0 pt-1 my-0">
<input type="checkbox" class="form-control mr-1 check-row">
</th>
<td class="font-weight-bold">
<img src="{{$follower->avatarUrl()}}" width="20px" height="20px" class="rounded-circle border mr-2">{{$follower->username}}
</td>
<td class="text-success text-center">Following</td>
<td class="text-center">
<a class="btn btn-outline-danger btn-sm py-0 action-btn" href="#" data-id="{{$follower->id}}" data-action="unfollow">Unfollow</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="d-flex justify-content-center">{{$following->links() ?? $followers->links()}}</div>
@endif
@endsection
@push('scripts')
<script type="text/javascript">
$(document).ready(() => {
$('.action-btn').on('click', e => {
e.preventDefault();
let action = e.target.getAttribute('data-action');
let id = e.target.getAttribute('data-id');
switch(action) {
case 'mute':
axios.post('/i/mute', {
type: 'user',
item: id
}).then(res => {
swal(
'Mute Successful',
'You have successfully muted that user',
'success'
);
});
break;
case 'block':
axios.post('/i/block', {
type: 'user',
item: id
}).then(res => {
swal(
'Block Successful',
'You have successfully blocked that user',
'success'
);
});
break;
case 'unfollow':
axios.post('/i/follow', {
item: id
}).then(res => {
swal(
'Unfollow Successful',
'You have successfully unfollowed that user',
'success'
);
});
break;
}
setTimeout(function() {
window.location.href = window.location.href;
}, 3000);
});
$('.check-all').on('click', e => {
})
});
</script>
@endpush

View file

@ -26,6 +26,32 @@
@include('settings.security.log-panel')
@include('settings.security.device-panel')
@if(config('pixelfed.account_deletion') == true)
<h4 class="font-weight-bold pt-3">Danger Zone</h4>
<div class="mb-4 border rounded border-danger">
<ul class="list-group mb-0 pb-0">
<li class="list-group-item border-left-0 border-right-0 py-3 d-flex justify-content-between">
<div>
<p class="font-weight-bold mb-1">Temporarily Disable Account</p>
<p class="mb-0 small">Disable your account to hide your posts until next log in.</p>
</div>
<div>
<a class="btn btn-outline-danger font-weight-bold py-1" href="{{route('settings.remove.temporary')}}">Disable</a>
</div>
</li>
<li class="list-group-item border-left-0 border-right-0 py-3 d-flex justify-content-between">
<div>
<p class="font-weight-bold mb-1">Delete this Account</p>
<p class="mb-0 small">Once you delete your account, there is no going back. Please be certain.</p>
</div>
<div>
<a class="btn btn-outline-danger font-weight-bold py-1" href="{{route('settings.remove.permanent')}}">Delete</a>
</div>
</li>
</ul>
</div>
@endif
</section>
@endsection

View file

@ -153,7 +153,9 @@
<ol class="">
<li>Log into <a href="{{config('app.url')}}">{{config('pixelfed.domain.app')}}</a></li>
<li>Tap or click the <i class="far fa-user text-dark"></i> menu and select <span class="font-weight-bold text-dark"><i class="fas fa-cog pr-1"></i> Settings</span></li>
<li>Scroll down and click on the <span class="font-weight-bold">Temporarily Disable Account</span> link.</li>
<li>Navigate to the <a href="{{route('settings.security')}}">Security Settings</a></li>
<li>Confirm your account password.</li>
<li>Scroll down to the Danger Zone section and click on the <span class="btn btn-sm btn-outline-danger py-1 font-weight-bold">Disable</span> button.</li>
<li>Follow the instructions on the next page.</li>
</ol>
</div>
@ -180,8 +182,10 @@
<p>To permanently delete your account:</p>
<ol class="">
<li>Go to <a href="{{route('settings.remove.permanent')}}">the <span class="font-weight-bold">Delete Your Account</span> page</a>. If you're not logged into pixelfed on the web, you'll be asked to log in first. You can't delete your account from within a mobile app.</li>
<li>Navigate to the <a href="{{route('settings.security')}}">Security Settings</a></li>
<li>Confirm your account password.</li>
<li>On the <span class="font-weight-bold">Delete Your Account</span> page click or tap on the <span>Permanently Delete My Account</span> button.</li>
<li>Scroll down to the Danger Zone section and click on the <span class="btn btn-sm btn-outline-danger py-1 font-weight-bold">Delete</span> button.</li>
<li>Follow the instructions on the next page.</li>
</ol>
</div>
</div>

View file

@ -237,6 +237,11 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('developers', 'SettingsController@developers')->name('settings.developers')->middleware('dangerzone');
Route::get('labs', 'SettingsController@labs')->name('settings.labs');
Route::post('labs', 'SettingsController@labsStore');
Route::group(['prefix' => 'relationships'], function() {
Route::redirect('/', '/settings/relationships/home');
Route::get('home', 'SettingsController@relationshipsHome')->name('settings.relationships');
});
});
Route::group(['prefix' => 'site'], function () {

View file

@ -0,0 +1,27 @@
<?php
namespace Tests\Unit;
use App\Util\ActivityPub\Helpers;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class FollowTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
$this->mastodon = '{"type":"Follow","signature":{"type":"RsaSignature2017","signatureValue":"Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==","creator":"http://mastodon.example.org/users/admin#main-key","created":"2018-02-17T13:29:31Z"},"object":"http://localtesting.pleroma.lol/users/lain","nickname":"lain","id":"http://mastodon.example.org/users/admin#follows/2","actor":"http://mastodon.example.org/users/admin","@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"toot":"http://joinmastodon.org/ns#","sensitive":"as:sensitive","ostatus":"http://ostatus.org#","movedTo":"as:movedTo","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","atomUri":"ostatus:atomUri","Hashtag":"as:Hashtag","Emoji":"toot:Emoji"}]}';
}
/** @test */
public function validateMastodonFollowObject()
{
$mastodon = json_decode($this->mastodon, true);
$mastodon = Helpers::validateObject($mastodon);
$this->assertTrue($mastodon);
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Tests\Unit\ActivityPub\Verb;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Util\ActivityPub\Validator\Accept;
class AcceptVerbTest extends TestCase
{
protected $validAccept;
protected $invalidAccept;
public function setUp(): void
{
parent::setUp();
$this->validAccept = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://example.org/og/b3e4a40b-0b26-4c5a-9079-094bd633fab7',
'type' => 'Accept',
'actor' => 'https://example.org/u/alice',
'object' => [
'id' => 'https://example.net/u/bob#follows/bb27f601-ddb9-4567-8f16-023d90605ca9',
'type' => 'Follow',
'actor' => 'https://example.net/u/bob',
'object' => 'https://example.org/u/alice'
]
];
$this->invalidAccept = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://example.org/og/b3e4a40b-0b26-4c5a-9079-094bd633fab7',
'type' => 'Accept2',
'actor' => 'https://example.org/u/alice',
'object' => [
'id' => 'https://example.net/u/bob#follows/bb27f601-ddb9-4567-8f16-023d90605ca9',
'type' => 'Follow',
'actor' => 'https://example.net/u/bob',
'object' => 'https://example.org/u/alice'
]
];
}
/** @test */
public function basic_accept()
{
$this->assertTrue(Accept::validate($this->validAccept));
}
/** @test */
public function invalid_accept()
{
$this->assertFalse(Accept::validate($this->invalidAccept));
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Tests\Unit\ActivityPub\Verb;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Util\ActivityPub\Validator\UndoFollow;
class UndoFollowTest extends TestCase
{
protected $validUndo;
protected $invalidUndo;
public function setUp(): void
{
parent::setUp();
$this->validUndo = json_decode('{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"toot":"http://joinmastodon.org/ns#","sensitive":"as:sensitive","ostatus":"http://ostatus.org#","movedTo":"as:movedTo","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","atomUri":"ostatus:atomUri","Hashtag":"as:Hashtag","Emoji":"toot:Emoji"}],"signature":{"type":"RsaSignature2017","signatureValue":"Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==","creator":"http://mastodon.example.org/users/admin#main-key","created":"2018-02-17T13:29:31Z"},"type":"Undo","object":{"type":"Follow","object":"http://localtesting.pleroma.lol/users/lain","nickname":"lain","id":"http://mastodon.example.org/users/admin#follows/2","actor":"http://mastodon.example.org/users/admin"},"actor":"http://mastodon.example.org/users/admin","id":"http://mastodon.example.org/users/admin#follow/2/undo"}', true, 8);
$this->invalidUndo = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://example.org/og/b3e4a40b-0b26-4c5a-9079-094bd633fab7',
'type' => 'Undo',
'actor' => 'https://example.org/u/alice',
'object' => [
'id' => 'https://example.net/u/bob#follows/bb27f601-ddb9-4567-8f16-023d90605ca9',
'type' => 'Follow',
]
];
}
/** @test */
public function valid_undo_follow()
{
$this->assertTrue(UndoFollow::validate($this->validUndo));
}
/** @test */
public function invalid_undo_follow()
{
$this->assertFalse(UndoFollow::validate($this->invalidUndo));
}
}

38
webpack.mix.js vendored
View file

@ -1,20 +1,5 @@
let mix = require('laravel-mix');
mix.options({
purifyCss: true,
});
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.sass('resources/assets/sass/app.scss', 'public/css', {
implementation: require('node-sass')
})
@ -28,35 +13,16 @@ mix.sass('resources/assets/sass/app.scss', 'public/css', {
mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/activity.js', 'public/js')
.js('resources/assets/js/components.js', 'public/js')
//.js('resources/assets/js/embed.js', 'public')
// Discover component
.js('resources/assets/js/discover.js', 'public/js')
// Profile component
.js('resources/assets/js/profile.js', 'public/js')
// Status component
.js('resources/assets/js/status.js', 'public/js')
// Timeline component
.js('resources/assets/js/timeline.js', 'public/js')
// ComposeModal component
.js('resources/assets/js/compose.js', 'public/js')
// SearchResults component
.js('resources/assets/js/search.js', 'public/js')
// Developer Components
.js('resources/assets/js/developers.js', 'public/js')
// // Direct Component
// .js('resources/assets/js/direct.js', 'public/js')
// Loops Component
.js('resources/assets/js/loops.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
.extract([
'lodash',
'popper.js',