mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-10 00:34:50 +00:00
Merge pull request #1388 from pixelfed/frontend-ui-refactor
Add Remote Follows
This commit is contained in:
commit
fe98b65d16
42 changed files with 667 additions and 170 deletions
|
@ -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
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
30
app/Util/ActivityPub/Validator/UndoFollow.php
Normal file
30
app/Util/ActivityPub/Validator/UndoFollow.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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
60
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/css/appdark.css
vendored
BIN
public/css/appdark.css
vendored
Binary file not shown.
BIN
public/css/landing.css
vendored
BIN
public/css/landing.css
vendored
Binary file not shown.
BIN
public/js/components.js
vendored
BIN
public/js/components.js
vendored
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.js
vendored
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
117
resources/views/settings/relationships/home.blade.php
Normal file
117
resources/views/settings/relationships/home.blade.php
Normal 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
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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 () {
|
||||
|
|
27
tests/Unit/ActivityPub/FollowTest.php
Normal file
27
tests/Unit/ActivityPub/FollowTest.php
Normal 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);
|
||||
}
|
||||
}
|
55
tests/Unit/ActivityPub/Verb/AcceptVerbTest.php
Normal file
55
tests/Unit/ActivityPub/Verb/AcceptVerbTest.php
Normal 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));
|
||||
}
|
||||
}
|
46
tests/Unit/ActivityPub/Verb/UndoFollowTest.php
Normal file
46
tests/Unit/ActivityPub/Verb/UndoFollowTest.php
Normal 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
38
webpack.mix.js
vendored
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue