forked from mirror/pixelfed
commit
b8ad9fe5e6
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Redis;
|
||||
|
||||
class Installer extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'install';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'CLI Installer';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->welcome();
|
||||
}
|
||||
|
||||
protected function welcome()
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Welcome to the Pixelfed Installer!');
|
||||
$this->info(' ');
|
||||
$this->info(' ');
|
||||
$this->info('Pixelfed version: ' . config('pixelfed.version'));
|
||||
$this->line(' ');
|
||||
$this->info('Scanning system...');
|
||||
$this->preflightCheck();
|
||||
}
|
||||
protected function preflightCheck()
|
||||
{
|
||||
$this->line(' ');
|
||||
$this->info('Checking for installed dependencies...');
|
||||
$redis = Redis::connection();
|
||||
if($redis->ping()) {
|
||||
$this->info('- Found redis!');
|
||||
} else {
|
||||
$this->error('- Redis not found, aborting installation');
|
||||
exit;
|
||||
}
|
||||
$this->checkPhpDependencies();
|
||||
$this->checkPermissions();
|
||||
$this->envCheck();
|
||||
}
|
||||
|
||||
protected function checkPhpDependencies()
|
||||
{
|
||||
$extensions = [
|
||||
'bcmath',
|
||||
'ctype',
|
||||
'curl',
|
||||
'json',
|
||||
'mbstring',
|
||||
'openssl'
|
||||
];
|
||||
$this->line('');
|
||||
$this->info('Checking for required php extensions...');
|
||||
foreach($extensions as $ext) {
|
||||
if(extension_loaded($ext) == false) {
|
||||
$this->error("- {$ext} extension not found, aborting installation");
|
||||
exit;
|
||||
} else {
|
||||
$this->info("- {$ext} extension found!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkPermissions()
|
||||
{
|
||||
$this->line('');
|
||||
$this->info('Checking for proper filesystem permissions...');
|
||||
|
||||
$paths = [
|
||||
base_path('bootstrap'),
|
||||
base_path('storage')
|
||||
];
|
||||
|
||||
foreach($paths as $path) {
|
||||
if(is_writeable($path) == false) {
|
||||
$this->error("- Invalid permission found! Aborting installation.");
|
||||
$this->error(" Please make the following path writeable by the web server:");
|
||||
$this->error(" $path");
|
||||
exit;
|
||||
} else {
|
||||
$this->info("- Found valid permissions for {$path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function envCheck()
|
||||
{
|
||||
if(!file_exists(base_path('.env'))) {
|
||||
$this->line('');
|
||||
$this->info('No .env configuration file found. We will create one now!');
|
||||
$this->createEnv();
|
||||
} else {
|
||||
$confirm = $this->confirm('Found .env file, do you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
$confirm = $this->confirm('Are you really sure you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
$this->error('Warning ... if you did not backup your .env before its overwritten it will be permanently deleted.');
|
||||
$confirm = $this->confirm('The application may be installed already, are you really sure you want to overwrite it?');
|
||||
if(!$confirm) {
|
||||
$this->info('Cancelling installation.');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$this->postInstall();
|
||||
}
|
||||
|
||||
protected function createEnv()
|
||||
{
|
||||
$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);
|
||||
|
||||
}
|
||||
|
||||
protected function postInstall()
|
||||
{
|
||||
$this->callSilent('config:cache');
|
||||
//$this->call('route:cache');
|
||||
$this->info('Pixelfed has been successfully installed!');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use App\User;
|
||||
|
||||
class NewMention implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
protected $user;
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user, $data)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'notification.new.mention';
|
||||
}
|
||||
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new PrivateChannel('App.User.' . $this->user->id);
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
return ['id' => $this->user->id];
|
||||
}
|
||||
|
||||
public function via()
|
||||
{
|
||||
return 'broadcast';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\Notification;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use App\Status;
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
||||
class NewPublicPost implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'status';
|
||||
}
|
||||
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new Channel('firehost.public');
|
||||
}
|
||||
|
||||
public function broadcastWith()
|
||||
{
|
||||
$resource = new Fractal\Resource\Item($this->status, new StatusTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
return [
|
||||
'entity' => $res
|
||||
];
|
||||
}
|
||||
|
||||
public function via()
|
||||
{
|
||||
return 'broadcast';
|
||||
}
|
||||
}
|
|
@ -128,7 +128,7 @@ class AccountController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
public function fetchNotifications($id)
|
||||
public function fetchNotifications(int $id)
|
||||
{
|
||||
$key = config('cache.prefix').":user.{$id}.notifications";
|
||||
$redis = Redis::connection();
|
||||
|
@ -167,14 +167,14 @@ class AccountController extends Controller
|
|||
public function mute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = "{$type}.mute";
|
||||
$action = $type . '.mute';
|
||||
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
|
@ -211,17 +211,71 @@ class AccountController extends Controller
|
|||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function block(Request $request)
|
||||
public function unmute(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = "{$type}.block";
|
||||
$action = $type . '.mute';
|
||||
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
|
||||
default:
|
||||
abort(400);
|
||||
break;
|
||||
}
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('mute')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:people:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json([200]);
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
public function block(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type.'.block';
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
|
@ -259,6 +313,56 @@ class AccountController extends Controller
|
|||
return redirect()->back();
|
||||
}
|
||||
|
||||
|
||||
public function unblock(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
$action = $type . '.block';
|
||||
if (!in_array($action, $this->filters)) {
|
||||
return abort(406);
|
||||
}
|
||||
$filterable = [];
|
||||
switch ($type) {
|
||||
case 'user':
|
||||
$profile = Profile::findOrFail($item);
|
||||
if ($profile->id == $user->id) {
|
||||
return abort(403);
|
||||
}
|
||||
$class = get_class($profile);
|
||||
$filterable['id'] = $profile->id;
|
||||
$filterable['type'] = $class;
|
||||
break;
|
||||
|
||||
default:
|
||||
abort(400);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$filter = UserFilter::whereUserId($user->id)
|
||||
->whereFilterableId($filterable['id'])
|
||||
->whereFilterableType($filterable['type'])
|
||||
->whereFilterType('block')
|
||||
->first();
|
||||
|
||||
if($filter) {
|
||||
$filter->delete();
|
||||
}
|
||||
|
||||
$pid = $user->id;
|
||||
Cache::forget("user:filter:list:$pid");
|
||||
Cache::forget("feature:discover:people:$pid");
|
||||
Cache::forget("feature:discover:posts:$pid");
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function followRequests(Request $request)
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
|
|
@ -31,6 +31,10 @@ class ApiController extends BaseApiController
|
|||
|
||||
'media_types' => config('pixelfed.media_types'),
|
||||
'enforce_account_limit' => config('pixelfed.enforce_account_limit')
|
||||
],
|
||||
'activitypub' => [
|
||||
'enabled' => config('pixelfed.activitypub_enabled'),
|
||||
'remote_follow' => config('pixelfed.remote_follow_enabled')
|
||||
]
|
||||
];
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Cache;
|
||||
|
||||
use App\Comment;
|
||||
|
@ -58,14 +59,21 @@ class CommentController extends Controller
|
|||
|
||||
Cache::forget('transform:status:'.$status->url());
|
||||
|
||||
$autolink = Autolink::create()->autolink($comment);
|
||||
$reply = new Status();
|
||||
$reply->profile_id = $profile->id;
|
||||
$reply->caption = e($comment);
|
||||
$reply->rendered = $autolink;
|
||||
$reply->in_reply_to_id = $status->id;
|
||||
$reply->in_reply_to_profile_id = $status->profile_id;
|
||||
$reply->save();
|
||||
$reply = DB::transaction(function() use($comment, $status, $profile) {
|
||||
$autolink = Autolink::create()->autolink($comment);
|
||||
$reply = new Status();
|
||||
$reply->profile_id = $profile->id;
|
||||
$reply->caption = e($comment);
|
||||
$reply->rendered = $autolink;
|
||||
$reply->in_reply_to_id = $status->id;
|
||||
$reply->in_reply_to_profile_id = $status->profile_id;
|
||||
$reply->save();
|
||||
|
||||
$status->reply_count++;
|
||||
$status->save();
|
||||
|
||||
return $reply;
|
||||
});
|
||||
|
||||
NewStatusPipeline::dispatch($reply, false);
|
||||
CommentPipeline::dispatch($status, $reply);
|
||||
|
|
|
@ -82,9 +82,10 @@ trait Instagram
|
|||
->whereStage(1)
|
||||
->firstOrFail();
|
||||
|
||||
$limit = config('pixelfed.import.instagram.limits.posts');
|
||||
foreach ($media as $k => $v) {
|
||||
$original = $v->getClientOriginalName();
|
||||
if(strlen($original) < 32 || $k > 100) {
|
||||
if(strlen($original) < 32 || $k > $limit) {
|
||||
continue;
|
||||
}
|
||||
$storagePath = "import/{$job->uuid}";
|
||||
|
@ -105,7 +106,6 @@ trait Instagram
|
|||
$job->save();
|
||||
});
|
||||
return redirect($job->url());
|
||||
return view('settings.import.instagram.step-one', compact('profile', 'job'));
|
||||
}
|
||||
|
||||
public function instagramStepTwo(Request $request, $uuid)
|
||||
|
@ -148,6 +148,7 @@ trait Instagram
|
|||
{
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereService('instagram')
|
||||
->whereNull('completed_at')
|
||||
->whereUuid($uuid)
|
||||
->whereStage(3)
|
||||
|
@ -159,14 +160,21 @@ trait Instagram
|
|||
{
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
|
||||
try {
|
||||
$import = ImportJob::whereProfileId($profile->id)
|
||||
->where('uuid', $uuid)
|
||||
->whereNotNull('media_json')
|
||||
->whereNull('completed_at')
|
||||
->whereUuid($uuid)
|
||||
->whereStage(3)
|
||||
->firstOrFail();
|
||||
ImportInstagram::dispatch($import);
|
||||
} catch (Exception $e) {
|
||||
\Log::info($e);
|
||||
}
|
||||
|
||||
ImportInstagram::dispatchNow($job);
|
||||
|
||||
return redirect($profile->url());
|
||||
return redirect(route('settings'))->with(['status' => [
|
||||
'Import successful! It may take a few minutes to finish.'
|
||||
]]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,6 +108,7 @@ class PublicApiController extends Controller
|
|||
'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
||||
'limit' => 'nullable|integer|min:5|max:50'
|
||||
]);
|
||||
|
||||
$limit = $request->limit ?? 10;
|
||||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId);
|
||||
|
@ -116,7 +117,7 @@ class PublicApiController extends Controller
|
|||
if($request->filled('min_id')) {
|
||||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->where('id', '>=', $request->min_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
|
@ -124,7 +125,7 @@ class PublicApiController extends Controller
|
|||
if($request->filled('max_id')) {
|
||||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->where('id', '<=', $request->max_id)
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
|
@ -132,7 +133,7 @@ class PublicApiController extends Controller
|
|||
} else {
|
||||
$replies = $status->comments()
|
||||
->whereNull('reblog_of_id')
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
|
||||
->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($limit);
|
||||
}
|
||||
|
@ -180,8 +181,8 @@ class PublicApiController extends Controller
|
|||
if(!$user) {
|
||||
abort(403);
|
||||
} else {
|
||||
$follows = $profile->followedBy(Auth::user()->profile);
|
||||
if($follows == false && $profile->id !== $user->profile->id) {
|
||||
$follows = $profile->followedBy($user->profile);
|
||||
if($follows == false && $profile->id !== $user->profile->id && $user->is_admin == false) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
@ -357,8 +358,6 @@ class PublicApiController extends Controller
|
|||
'created_at',
|
||||
'updated_at'
|
||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->whereLocal(true)
|
||||
->whereNull('uri')
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
|
@ -386,8 +385,6 @@ class PublicApiController extends Controller
|
|||
'created_at',
|
||||
'updated_at'
|
||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->whereLocal(true)
|
||||
->whereNull('uri')
|
||||
->whereIn('profile_id', $following)
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereNull('in_reply_to_id')
|
||||
|
@ -453,14 +450,18 @@ class PublicApiController extends Controller
|
|||
'is_nsfw',
|
||||
'scope',
|
||||
'local',
|
||||
'reply_count',
|
||||
'comments_disabled',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->where('id', $dir, $id)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereNotNull('uri')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereVisibility('public')
|
||||
->orderBy('created_at', 'desc')
|
||||
->latest()
|
||||
->limit($limit)
|
||||
->get();
|
||||
} else {
|
||||
|
@ -476,14 +477,17 @@ class PublicApiController extends Controller
|
|||
'is_nsfw',
|
||||
'scope',
|
||||
'local',
|
||||
'reply_count',
|
||||
'comments_disabled',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
||||
->whereNotIn('profile_id', $filtered)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereNotNull('uri')
|
||||
->whereVisibility('public')
|
||||
->orderBy('created_at', 'desc')
|
||||
->latest()
|
||||
->simplePaginate($limit);
|
||||
}
|
||||
|
||||
|
@ -524,8 +528,8 @@ class PublicApiController extends Controller
|
|||
{
|
||||
abort_unless(Auth::check(), 403);
|
||||
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
|
||||
if($profile->is_private || !$profile->user->settings->show_profile_followers) {
|
||||
return [];
|
||||
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
|
||||
return response()->json([]);
|
||||
}
|
||||
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
|
||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
||||
|
@ -538,8 +542,8 @@ class PublicApiController extends Controller
|
|||
{
|
||||
abort_unless(Auth::check(), 403);
|
||||
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
|
||||
if($profile->is_private || !$profile->user->settings->show_profile_following) {
|
||||
return [];
|
||||
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_following) {
|
||||
return response()->json([]);
|
||||
}
|
||||
$following = $profile->following()->orderByDesc('followers.created_at')->paginate(10);
|
||||
$resource = new Fractal\Resource\Collection($following, new AccountTransformer());
|
||||
|
|
|
@ -9,6 +9,7 @@ use App\Status;
|
|||
use Illuminate\Http\Request;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
HashtagTransformer,
|
||||
|
@ -22,17 +23,20 @@ class SearchController extends Controller
|
|||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function searchAPI(Request $request, $tag)
|
||||
public function searchAPI(Request $request)
|
||||
{
|
||||
if(mb_strlen($tag) < 3) {
|
||||
return;
|
||||
}
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:3|max:120',
|
||||
'src' => 'required|string|in:metro',
|
||||
'v' => 'required|integer|in:1'
|
||||
]);
|
||||
$tag = $request->input('q');
|
||||
$tag = e(urldecode($tag));
|
||||
|
||||
$hash = hash('sha256', $tag);
|
||||
$tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) {
|
||||
$tokens = [];
|
||||
if(Helpers::validateUrl($tag) != false) {
|
||||
if(Helpers::validateUrl($tag) != false && config('pixelfed.activitypub_enabled') == true && config('pixelfed.remote_follow_enabled') == true) {
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) {
|
||||
$type = $remote['type'];
|
||||
|
@ -65,7 +69,12 @@ class SearchController extends Controller
|
|||
}
|
||||
}
|
||||
}
|
||||
$hashtags = Hashtag::select('id', 'name', 'slug')->where('slug', 'like', '%'.$tag.'%')->whereHas('posts')->limit(20)->get();
|
||||
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
|
||||
$hashtags = Hashtag::select('id', 'name', 'slug')
|
||||
->where('slug', 'like', '%'.$htag.'%')
|
||||
->whereHas('posts')
|
||||
->limit(20)
|
||||
->get();
|
||||
if($hashtags->count() > 0) {
|
||||
$tags = $hashtags->map(function ($item, $key) {
|
||||
return [
|
||||
|
@ -83,9 +92,9 @@ class SearchController extends Controller
|
|||
});
|
||||
$users = Profile::select('username', 'name', 'id')
|
||||
->whereNull('status')
|
||||
->whereNull('domain')
|
||||
->where('id', '!=', Auth::user()->profile->id)
|
||||
->where('username', 'like', '%'.$tag.'%')
|
||||
->whereNull('domain')
|
||||
//->orWhere('remote_url', $tag)
|
||||
->limit(20)
|
||||
->get();
|
||||
|
@ -120,7 +129,6 @@ class SearchController extends Controller
|
|||
->whereNull('reblog_of_id')
|
||||
->whereProfileId(Auth::user()->profile->id)
|
||||
->where('caption', 'like', '%'.$tag.'%')
|
||||
->orWhere('uri', $tag)
|
||||
->latest()
|
||||
->limit(10)
|
||||
->get();
|
||||
|
|
|
@ -47,6 +47,10 @@ trait HomeSettings
|
|||
$email = $request->input('email');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$layout = $request->input('profile_layout');
|
||||
if($layout) {
|
||||
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
|
||||
}
|
||||
|
||||
$validate = config('pixelfed.enforce_email_verification');
|
||||
|
||||
|
@ -89,6 +93,11 @@ trait HomeSettings
|
|||
$changes = true;
|
||||
$profile->bio = $bio;
|
||||
}
|
||||
|
||||
if ($profile->profile_layout != $layout) {
|
||||
$changes = true;
|
||||
$profile->profile_layout = $layout;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changes === true) {
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Media;
|
|||
use App\Profile;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use App\UserDevice;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Auth;
|
||||
use DB;
|
||||
|
@ -20,19 +21,19 @@ trait SecuritySettings
|
|||
|
||||
public function security()
|
||||
{
|
||||
$sessions = DB::table('sessions')
|
||||
->whereUserId(Auth::id())
|
||||
->limit(20)
|
||||
->get();
|
||||
$user = Auth::user();
|
||||
|
||||
$activity = AccountLog::whereUserId(Auth::id())
|
||||
$activity = AccountLog::whereUserId($user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
$user = Auth::user();
|
||||
$devices = UserDevice::whereUserId($user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
return view('settings.security', compact('sessions', 'activity', 'user'));
|
||||
return view('settings.security', compact('activity', 'user', 'devices'));
|
||||
}
|
||||
|
||||
public function securityTwoFactorSetup(Request $request)
|
||||
|
|
|
@ -42,11 +42,11 @@ class StatusController extends Controller
|
|||
|
||||
if($status->visibility == 'private' || $user->is_private) {
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
abort(404);
|
||||
}
|
||||
$pid = Auth::user()->profile;
|
||||
if($user->followedBy($pid) == false && $user->id !== $pid->id) {
|
||||
abort(403);
|
||||
if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,21 @@ class AuthServiceProvider extends ServiceProvider
|
|||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
// Passport::routes();
|
||||
// Passport::tokensExpireIn(now()->addDays(15));
|
||||
// Passport::refreshTokensExpireIn(now()->addDays(30));
|
||||
if(config('pixelfed.oauth_enabled')) {
|
||||
Passport::routes();
|
||||
Passport::tokensExpireIn(now()->addDays(15));
|
||||
Passport::refreshTokensExpireIn(now()->addDays(30));
|
||||
Passport::enableImplicitGrant();
|
||||
|
||||
Passport::setDefaultScope([
|
||||
'user:read',
|
||||
'user:write'
|
||||
]);
|
||||
|
||||
Passport::tokensCan([
|
||||
'user:read' => 'Read a user’s profile info and media',
|
||||
'user:write' => 'This scope lets an app "Change your profile information"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ class CreateNote extends Fractal\TransformerAbstract
|
|||
'Hashtag' => 'as:Hashtag',
|
||||
'sensitive' => 'as:sensitive',
|
||||
'commentsEnabled' => 'sc:Boolean',
|
||||
'capabilities' => [
|
||||
'announce' => ['@type' => '@id'],
|
||||
'like' => ['@type' => '@id'],
|
||||
'reply' => ['@type' => '@id']
|
||||
]
|
||||
]
|
||||
],
|
||||
'id' => $status->permalink(),
|
||||
|
@ -65,6 +70,11 @@ class CreateNote extends Fractal\TransformerAbstract
|
|||
})->toArray(),
|
||||
'tag' => $tags,
|
||||
'commentsEnabled' => (bool) !$status->comments_disabled,
|
||||
'capabilities' => [
|
||||
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'like' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ class Note extends Fractal\TransformerAbstract
|
|||
'Hashtag' => 'as:Hashtag',
|
||||
'sensitive' => 'as:sensitive',
|
||||
'commentsEnabled' => 'sc:Boolean',
|
||||
'capabilities' => [
|
||||
'announce' => ['@type' => '@id'],
|
||||
'like' => ['@type' => '@id'],
|
||||
'reply' => ['@type' => '@id'],
|
||||
]
|
||||
]
|
||||
],
|
||||
'id' => $status->url(),
|
||||
|
@ -58,6 +63,11 @@ class Note extends Fractal\TransformerAbstract
|
|||
})->toArray(),
|
||||
'tag' => $tags,
|
||||
'commentsEnabled' => (bool) !$status->comments_disabled,
|
||||
'capabilities' => [
|
||||
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'like' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
|
|||
'id' => (string) $profile->id,
|
||||
'following' => $user->follows($profile),
|
||||
'followed_by' => $user->followedBy($profile),
|
||||
'blocking' => null,
|
||||
'muting' => null,
|
||||
'blocking' => $user->blockedIds()->contains($profile->id),
|
||||
'muting' => $user->mutedIds()->contains($profile->id),
|
||||
'muting_notifications' => null,
|
||||
'requested' => null,
|
||||
'domain_blocking' => null,
|
||||
|
|
|
@ -23,7 +23,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
|
||||
'reblog' => $status->reblog_of_id || $status->in_reply_to_id ? $this->transform($status->parent()) : null,
|
||||
'reblog' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'created_at' => $status->created_at->format('c'),
|
||||
'emojis' => [],
|
||||
|
@ -42,9 +42,11 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'language' => null,
|
||||
'pinned' => null,
|
||||
|
||||
'pf_type' => $status->type ?? $status->setType(),
|
||||
'reply_count' => $status->reply_count,
|
||||
'comments_disabled' => $status->comments_disabled ? true : false
|
||||
'pf_type' => $status->type ?? $status->setType(),
|
||||
'reply_count' => (int) $status->reply_count,
|
||||
'comments_disabled' => $status->comments_disabled ? true : false,
|
||||
'thread' => false,
|
||||
'replies' => []
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Jenssegers\Agent\Agent;
|
||||
|
||||
class UserDevice extends Model
|
||||
{
|
||||
|
@ -20,4 +21,14 @@ class UserDevice extends Model
|
|||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function getUserAgent()
|
||||
{
|
||||
if(!$this->user_agent) {
|
||||
return 'Unknown';
|
||||
}
|
||||
$agent = new Agent();
|
||||
$agent->setUserAgent($this->user_agent);
|
||||
return $agent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,6 @@ use App\Jobs\AvatarPipeline\CreateAvatar;
|
|||
use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
|
||||
use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Util\HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier};
|
||||
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
@ -30,7 +28,7 @@ class Helpers {
|
|||
|
||||
public static function validateObject($data)
|
||||
{
|
||||
$verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo'];
|
||||
$verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone'];
|
||||
|
||||
$valid = Validator::make($data, [
|
||||
'type' => [
|
||||
|
@ -38,11 +36,11 @@ class Helpers {
|
|||
Rule::in($verbs)
|
||||
],
|
||||
'id' => 'required|string',
|
||||
'actor' => 'required|string',
|
||||
'actor' => 'required|string|url',
|
||||
'object' => 'required',
|
||||
'object.type' => 'required_if:type,Create',
|
||||
'object.attachment' => 'required_if:type,Create',
|
||||
'object.attributedTo' => 'required_if:type,Create',
|
||||
'object.attributedTo' => 'required_if:type,Create|url',
|
||||
'published' => 'required_if:type,Create|date'
|
||||
])->passes();
|
||||
|
||||
|
@ -71,7 +69,7 @@ class Helpers {
|
|||
'string',
|
||||
Rule::in($mediaTypes)
|
||||
],
|
||||
'*.url' => 'required|max:255',
|
||||
'*.url' => 'required|url|max:255',
|
||||
'*.mediaType' => [
|
||||
'required',
|
||||
'string',
|
||||
|
@ -193,6 +191,7 @@ class Helpers {
|
|||
$res = Zttp::withHeaders(self::zttpUserAgent())->get($url);
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if(json_last_error() == JSON_ERROR_NONE) {
|
||||
abort_if(!self::validateObject($res), 422);
|
||||
return $res;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -238,14 +237,26 @@ class Helpers {
|
|||
}
|
||||
|
||||
$scope = 'private';
|
||||
|
||||
$cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
|
||||
|
||||
if(isset($res['to']) == true && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
|
||||
$scope = 'public';
|
||||
if(isset($res['to']) == true) {
|
||||
if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
|
||||
$scope = 'public';
|
||||
}
|
||||
if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
|
||||
$scope = 'public';
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($res['cc']) == true && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
|
||||
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';
|
||||
}
|
||||
if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
|
||||
$scope = 'unlisted';
|
||||
}
|
||||
}
|
||||
|
||||
if(config('costar.enabled') == true) {
|
||||
|
@ -309,7 +320,7 @@ class Helpers {
|
|||
$status->scope = $scope;
|
||||
$status->visibility = $scope;
|
||||
$status->save();
|
||||
self::importNoteAttachment($res, $status);
|
||||
// self::importNoteAttachment($res, $status);
|
||||
return $status;
|
||||
});
|
||||
|
||||
|
@ -320,6 +331,8 @@ class Helpers {
|
|||
|
||||
public static function importNoteAttachment($data, Status $status)
|
||||
{
|
||||
return;
|
||||
|
||||
if(self::verifyAttachments($data) == false) {
|
||||
return;
|
||||
}
|
||||
|
@ -336,28 +349,28 @@ class Helpers {
|
|||
if(in_array($type, $allowed) == false || $valid == false) {
|
||||
continue;
|
||||
}
|
||||
$info = pathinfo($url);
|
||||
// $info = pathinfo($url);
|
||||
|
||||
// pleroma attachment fix
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
// // pleroma attachment fix
|
||||
// $url = str_replace(' ', '%20', $url);
|
||||
|
||||
$img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]]));
|
||||
$file = '/tmp/'.str_random(32);
|
||||
file_put_contents($file, $img);
|
||||
$fdata = new File($file);
|
||||
$path = Storage::putFile($storagePath, $fdata, 'public');
|
||||
$media = new Media();
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $status->profile_id;
|
||||
$media->user_id = null;
|
||||
$media->media_path = $path;
|
||||
$media->size = $fdata->getSize();
|
||||
$media->mime = $fdata->getMimeType();
|
||||
$media->save();
|
||||
// $img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]]));
|
||||
// $file = '/tmp/'.str_random(32);
|
||||
// file_put_contents($file, $img);
|
||||
// $fdata = new File($file);
|
||||
// $path = Storage::putFile($storagePath, $fdata, 'public');
|
||||
// $media = new Media();
|
||||
// $media->status_id = $status->id;
|
||||
// $media->profile_id = $status->profile_id;
|
||||
// $media->user_id = null;
|
||||
// $media->media_path = $path;
|
||||
// $media->size = $fdata->getSize();
|
||||
// $media->mime = $fdata->getMimeType();
|
||||
// $media->save();
|
||||
|
||||
ImageThumbnail::dispatch($media);
|
||||
ImageOptimize::dispatch($media);
|
||||
unlink($file);
|
||||
// ImageThumbnail::dispatch($media);
|
||||
// ImageOptimize::dispatch($media);
|
||||
// unlink($file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -380,15 +393,19 @@ class Helpers {
|
|||
return;
|
||||
}
|
||||
$domain = parse_url($res['id'], PHP_URL_HOST);
|
||||
$username = $res['preferredUsername'];
|
||||
$username = Purify::clean($res['preferredUsername']);
|
||||
$remoteUsername = "@{$username}@{$domain}";
|
||||
|
||||
abort_if(!self::validateUrl($res['inbox']), 400);
|
||||
abort_if(!self::validateUrl($res['outbox']), 400);
|
||||
abort_if(!self::validateUrl($res['id']), 400);
|
||||
|
||||
$profile = Profile::whereRemoteUrl($res['id'])->first();
|
||||
if(!$profile) {
|
||||
$profile = new Profile;
|
||||
$profile->domain = $domain;
|
||||
$profile->username = $remoteUsername;
|
||||
$profile->name = strip_tags($res['name']);
|
||||
$profile->username = Purify::clean($remoteUsername);
|
||||
$profile->name = Purify::clean($res['name']) ?? 'user';
|
||||
$profile->bio = Purify::clean($res['summary']);
|
||||
$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
|
||||
$profile->inbox_url = $res['inbox'];
|
||||
|
@ -407,6 +424,8 @@ class Helpers {
|
|||
|
||||
public static function sendSignedObject($senderProfile, $url, $body)
|
||||
{
|
||||
abort_if(!self::validateUrl($url), 400);
|
||||
|
||||
$payload = json_encode($body);
|
||||
$headers = HttpSignature::sign($senderProfile, $url, $body);
|
||||
|
||||
|
@ -418,42 +437,4 @@ class Helpers {
|
|||
$response = curl_exec($ch);
|
||||
return;
|
||||
}
|
||||
|
||||
private static function _headersToSigningString($headers) {
|
||||
}
|
||||
|
||||
public static function validateSignature($request, $payload = null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static function fetchPublicKey()
|
||||
{
|
||||
$profile = $this->profile;
|
||||
$is_url = $this->is_url;
|
||||
$valid = $this->validateUrl();
|
||||
if (!$valid) {
|
||||
throw new \Exception('Invalid URL provided');
|
||||
}
|
||||
if ($is_url && isset($profile->public_key) && $profile->public_key) {
|
||||
return $profile->public_key;
|
||||
}
|
||||
|
||||
try {
|
||||
$url = $this->profile;
|
||||
$res = Zttp::timeout(30)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org',
|
||||
])->get($url);
|
||||
$actor = json_decode($res->getBody(), true);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('Unable to fetch public key');
|
||||
}
|
||||
if($actor['publicKey']['owner'] != $profile) {
|
||||
throw new Exception('Invalid key match');
|
||||
}
|
||||
$this->public_key = $actor['publicKey']['publicKeyPem'];
|
||||
$this->key_id = $actor['publicKey']['id'];
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -36,6 +36,7 @@ class Inbox
|
|||
|
||||
public function handle()
|
||||
{
|
||||
abort_if(!Helpers::validateObject($this->payload), 400);
|
||||
$this->handleVerb();
|
||||
}
|
||||
|
||||
|
@ -135,6 +136,8 @@ class Inbox
|
|||
|
||||
public function handleNoteCreate()
|
||||
{
|
||||
return;
|
||||
|
||||
$activity = $this->payload['object'];
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
if(!$actor || $actor->domain == null) {
|
||||
|
@ -259,24 +262,24 @@ class Inbox
|
|||
{
|
||||
$actor = $this->payload['actor'];
|
||||
$obj = $this->payload['object'];
|
||||
abort_if(!Helpers::validateUrl($obj), 400);
|
||||
if(is_string($obj) && Helpers::validateUrl($obj)) {
|
||||
// actor object detected
|
||||
// todo delete actor
|
||||
} else if (Helpers::validateUrl($obj['id']) && is_array($obj) && isset($obj['type']) && $obj['type'] == 'Tombstone') {
|
||||
// tombstone detected
|
||||
$status = Status::whereLocal(false)->whereUri($obj['id'])->firstOrFail();
|
||||
$status->forceDelete();
|
||||
} else if (Helpers::validateUrl($obj['id']) && Helpers::validateObject($obj) && $obj['type'] == 'Tombstone') {
|
||||
// todo delete status or object
|
||||
}
|
||||
}
|
||||
|
||||
public function handleLikeActivity()
|
||||
{
|
||||
$actor = $this->payload['actor'];
|
||||
|
||||
abort_if(!Helpers::validateUrl($actor), 400);
|
||||
|
||||
$profile = self::actorFirstOrCreate($actor);
|
||||
$obj = $this->payload['object'];
|
||||
if(Helpers::validateLocalUrl($obj) == false) {
|
||||
return;
|
||||
}
|
||||
abort_if(!Helpers::validateLocalUrl($obj), 400);
|
||||
$status = Helpers::statusFirstOrFetch($obj);
|
||||
if(!$status || !$profile) {
|
||||
return;
|
||||
|
@ -286,10 +289,11 @@ class Inbox
|
|||
'status_id' => $status->id
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated == false) {
|
||||
return;
|
||||
if($like->wasRecentlyCreated == true) {
|
||||
LikePipeline::dispatch($like);
|
||||
}
|
||||
LikePipeline::dispatch($like);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"fideloper/proxy": "^4.0",
|
||||
"greggilbert/recaptcha": "dev-master",
|
||||
"intervention/image": "^2.4",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laravel/framework": "5.8.*",
|
||||
"laravel/horizon": "^3.0",
|
||||
"laravel/passport": "^7.0",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "188c87638a863fd575f41213e72976f5",
|
||||
"content-hash": "702a3ed0b8499d50323723eb4fb41965",
|
||||
"packages": [
|
||||
{
|
||||
"name": "alchemy/binary-driver",
|
||||
|
@ -1558,6 +1558,124 @@
|
|||
"description": "Highlight PHP code in terminal",
|
||||
"time": "2018-09-29T18:48:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jaybizzle/crawler-detect",
|
||||
"version": "v1.2.80",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
|
||||
"reference": "af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847",
|
||||
"reference": "af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8|^5.5|^6.5",
|
||||
"satooshi/php-coveralls": "1.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jaybizzle\\CrawlerDetect\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Beech",
|
||||
"email": "m@rkbee.ch",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "CrawlerDetect is a PHP class for detecting bots/crawlers/spiders via the user agent",
|
||||
"homepage": "https://github.com/JayBizzle/Crawler-Detect/",
|
||||
"keywords": [
|
||||
"crawler",
|
||||
"crawler detect",
|
||||
"crawler detector",
|
||||
"crawlerdetect",
|
||||
"php crawler detect"
|
||||
],
|
||||
"time": "2019-04-05T19:52:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jenssegers/agent",
|
||||
"version": "v2.6.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jenssegers/agent.git",
|
||||
"reference": "bcb895395e460478e101f41cdab139c48dc721ce"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jenssegers/agent/zipball/bcb895395e460478e101f41cdab139c48dc721ce",
|
||||
"reference": "bcb895395e460478e101f41cdab139c48dc721ce",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"jaybizzle/crawler-detect": "^1.2",
|
||||
"mobiledetect/mobiledetectlib": "^2.7.6",
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "^2.1",
|
||||
"phpunit/phpunit": "^5.0|^6.0|^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"illuminate/support": "^4.0|^5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Jenssegers\\Agent\\AgentServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Agent": "Jenssegers\\Agent\\Facades\\Agent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jenssegers\\Agent\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jens Segers",
|
||||
"homepage": "https://jenssegers.com"
|
||||
}
|
||||
],
|
||||
"description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect",
|
||||
"homepage": "https://github.com/jenssegers/agent",
|
||||
"keywords": [
|
||||
"Agent",
|
||||
"browser",
|
||||
"desktop",
|
||||
"laravel",
|
||||
"mobile",
|
||||
"platform",
|
||||
"user agent",
|
||||
"useragent"
|
||||
],
|
||||
"time": "2019-01-19T21:32:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v5.8.10",
|
||||
|
@ -2270,6 +2388,58 @@
|
|||
],
|
||||
"time": "2019-03-29T18:19:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mobiledetect/mobiledetectlib",
|
||||
"version": "2.8.33",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/serbanghita/Mobile-Detect.git",
|
||||
"reference": "cd385290f9a0d609d2eddd165a1e44ec1bf12102"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/cd385290f9a0d609d2eddd165a1e44ec1bf12102",
|
||||
"reference": "cd385290f9a0d609d2eddd165a1e44ec1bf12102",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.8.35||~5.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"Mobile_Detect.php"
|
||||
],
|
||||
"psr-0": {
|
||||
"Detection": "namespaced/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Serban Ghita",
|
||||
"email": "serbanghita@gmail.com",
|
||||
"homepage": "http://mobiledetect.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
|
||||
"homepage": "https://github.com/serbanghita/Mobile-Detect",
|
||||
"keywords": [
|
||||
"detect mobile devices",
|
||||
"mobile",
|
||||
"mobile detect",
|
||||
"mobile detector",
|
||||
"php mobile detect"
|
||||
],
|
||||
"time": "2018-09-01T15:05:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "1.24.0",
|
||||
|
|
|
@ -23,7 +23,7 @@ return [
|
|||
| This value is the version of your PixelFed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.8.6',
|
||||
'version' => '0.9.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -46,7 +46,7 @@ return [
|
|||
| default memory_limit php.ini is used for the rest of the app.
|
||||
|
|
||||
*/
|
||||
'memory_limit' => '1024M',
|
||||
'memory_limit' => env('MEMORY_LIMIT', '1024M'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -259,7 +259,9 @@ return [
|
|||
|
||||
|
||||
'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'),
|
||||
|
||||
'enforce_account_limit' => env('LIMIT_ACCOUNT_SIZE', true),
|
||||
|
||||
'ap_inbox' => env('ACTIVITYPUB_INBOX', false),
|
||||
'ap_shared' => env('ACTIVITYPUB_SHAREDINBOX', false),
|
||||
'ap_delivery_timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),
|
||||
|
@ -267,11 +269,13 @@ return [
|
|||
|
||||
'import' => [
|
||||
'instagram' => [
|
||||
'enabled' => env('IMPORT_INSTAGRAM_ENABLED', false),
|
||||
'enabled' => false,
|
||||
'limits' => [
|
||||
'posts' => (int) env('IMPORT_INSTAGRAM_POST_LIMIT', 100),
|
||||
'size' => (int) env('IMPORT_INSTAGRAM_SIZE_LIMIT', 250)
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
'oauth_enabled' => env('OAUTH_ENABLED', false),
|
||||
];
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddLayoutToProfilesTable extends Migration
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('profiles', function (Blueprint $table) {
|
||||
$table->string('profile_layout')->nullable()->after('website');
|
||||
$table->string('post_layout')->nullable()->after('profile_layout');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('profiles', function (Blueprint $table) {
|
||||
$table->dropColumn('profile_layout');
|
||||
$table->dropColumn('post_layout');
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -4,11 +4,12 @@
|
|||
"/css/app.css": "/css/app.css?id=cc8780fa2f1c0e8156cc",
|
||||
"/css/appdark.css": "/css/appdark.css?id=c98702344aa5c1af2dda",
|
||||
"/css/landing.css": "/css/landing.css?id=36c1fca7fbdc4cdf5e7c",
|
||||
"/js/components.js": "/js/components.js?id=ae830db50efb9df181df",
|
||||
"/js/compose.js": "/js/compose.js?id=e988873b96c16cd9b764",
|
||||
"/js/components.js": "/js/components.js?id=6697417fd9d6250562c6",
|
||||
"/js/compose.js": "/js/compose.js?id=9436fc6ceea845e5bd62",
|
||||
"/js/developers.js": "/js/developers.js?id=1359f11c7349301903f8",
|
||||
"/js/discover.js": "/js/discover.js?id=75fb12b06ee23fa05186",
|
||||
"/js/profile.js": "/js/profile.js?id=e0f48175e05bcf64a43c",
|
||||
"/js/search.js": "/js/search.js?id=df9027d746934eb442f0",
|
||||
"/js/status.js": "/js/status.js?id=cb809b4d63324bca6b14",
|
||||
"/js/timeline.js": "/js/timeline.js?id=db7c841e880fc6585ef7"
|
||||
"/js/profile.js": "/js/profile.js?id=22b0a1974c9458efa4c8",
|
||||
"/js/search.js": "/js/search.js?id=0d3d080dc05f4f49b204",
|
||||
"/js/status.js": "/js/status.js?id=f93eb768e942f3fb1d5f",
|
||||
"/js/timeline.js": "/js/timeline.js?id=f83a9c543e3f2015b8b2"
|
||||
}
|
||||
|
|
|
@ -18,10 +18,10 @@ pixelfed.readmore = () => {
|
|||
return;
|
||||
}
|
||||
el.readmore({
|
||||
collapsedHeight: 44,
|
||||
heightMargin: 20,
|
||||
moreLink: '<a href="#" class="font-weight-bold small">Read more</a>',
|
||||
lessLink: '<a href="#" class="font-weight-bold small">Hide</a>',
|
||||
collapsedHeight: 45,
|
||||
heightMargin: 48,
|
||||
moreLink: '<a href="#" class="d-block font-weight-lighter small text-dark text-center">Read more ...</a>',
|
||||
lessLink: '<a href="#" class="d-block font-weight-lighter small text-dark text-center">Hide</a>',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -25,43 +25,56 @@
|
|||
</div>
|
||||
|
||||
<div class="postPresenterContainer">
|
||||
<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia()">
|
||||
<p class="text-center mb-0 font-weight-bold p-5">Click here to add photos.</p>
|
||||
<div v-if="uploading">
|
||||
<div class="w-100 h-100 bg-light py-5" style="border-bottom: 1px solid #f1f1f1">
|
||||
<div class="p-5">
|
||||
<b-progress :value="uploadProgress" :max="100" striped :animated="true"></b-progress>
|
||||
<p class="text-center mb-0 font-weight-bold">Uploading ... ({{uploadProgress}}%)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="ids.length > 0">
|
||||
|
||||
<b-carousel id="p-carousel"
|
||||
style="text-shadow: 1px 1px 2px #333;"
|
||||
controls
|
||||
indicators
|
||||
background="#ffffff"
|
||||
:interval="0"
|
||||
v-model="carouselCursor"
|
||||
>
|
||||
<b-carousel-slide v-if="ids.length > 0" v-for="(preview, index) in media" :key="'preview_media_'+index">
|
||||
<div slot="img" :class="[media[index].filter_class?media[index].filter_class + ' cursor-pointer':' cursor-pointer']" v-on:click="addMedia()">
|
||||
<img class="d-block img-fluid w-100" :src="preview.url" :alt="preview.description" :title="preview.description">
|
||||
</div>
|
||||
</b-carousel-slide>
|
||||
</b-carousel>
|
||||
</div>
|
||||
<div v-if="mediaDrawer" class="bg-dark align-items-center">
|
||||
<ul class="nav media-drawer-filters text-center">
|
||||
<li class="nav-item">
|
||||
<div class="p-1 pt-3">
|
||||
<img :src="media[carouselCursor].url" width="100px" height="60px" v-on:click.prevent="toggleFilter($event, null)" class="cursor-pointer">
|
||||
</div>
|
||||
<a :class="[media[carouselCursor].filter_class == null ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, null)">No Filter</a>
|
||||
</li>
|
||||
<li class="nav-item" v-for="(filter, index) in filters">
|
||||
<div class="p-1 pt-3">
|
||||
<div :class="filter[1]" v-on:click.prevent="toggleFilter($event, filter[1])">
|
||||
<img :src="media[carouselCursor].url" width="100px" height="60px" class="">
|
||||
<div v-else>
|
||||
<div v-if="ids.length > 0 && ids.length != config.uploader.album_limit" class="card-header py-2 bg-primary m-2 rounded cursor-pointer" v-on:click="addMedia()">
|
||||
<p class="text-center mb-0 font-weight-bold text-white"><i class="fas fa-plus mr-1"></i> Add Photo</p>
|
||||
</div>
|
||||
<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia()">
|
||||
<p class="text-center mb-0 font-weight-bold p-5">Click here to add photos</p>
|
||||
</div>
|
||||
<div v-if="ids.length > 0">
|
||||
|
||||
<b-carousel id="p-carousel"
|
||||
style="text-shadow: 1px 1px 2px #333;"
|
||||
controls
|
||||
indicators
|
||||
background="#ffffff"
|
||||
:interval="0"
|
||||
v-model="carouselCursor"
|
||||
>
|
||||
<b-carousel-slide v-if="ids.length > 0" v-for="(preview, index) in media" :key="'preview_media_'+index">
|
||||
<div slot="img" :class="[media[index].filter_class?media[index].filter_class:'']" style="display:flex;min-height: 320px;align-items: center;">
|
||||
<img class="d-block img-fluid w-100" :src="preview.url" :alt="preview.description" :title="preview.description">
|
||||
</div>
|
||||
</div>
|
||||
<a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</b-carousel-slide>
|
||||
</b-carousel>
|
||||
</div>
|
||||
<div v-if="ids.length > 0" class="bg-dark align-items-center">
|
||||
<ul class="nav media-drawer-filters text-center">
|
||||
<li class="nav-item">
|
||||
<div class="p-1 pt-3">
|
||||
<img :src="media[carouselCursor].url" width="100px" height="60px" v-on:click.prevent="toggleFilter($event, null)" class="cursor-pointer">
|
||||
</div>
|
||||
<a :class="[media[carouselCursor].filter_class == null ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, null)">No Filter</a>
|
||||
</li>
|
||||
<li class="nav-item" v-for="(filter, index) in filters">
|
||||
<div class="p-1 pt-3">
|
||||
<div :class="filter[1]" v-on:click.prevent="toggleFilter($event, filter[1])">
|
||||
<img :src="media[carouselCursor].url" width="100px" height="60px" class="">
|
||||
</div>
|
||||
</div>
|
||||
<a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mediaDrawer" class="bg-lighter p-2 row">
|
||||
<div class="col-12">
|
||||
|
@ -84,24 +97,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="[mediaDrawer?'d-none':'card-body']">
|
||||
<div class="card-body p-0">
|
||||
<div class="caption">
|
||||
<p class="mb-2">
|
||||
<textarea class="form-control d-inline-block" rows="3" placeholder="Add an optional caption" v-model="composeText"></textarea>
|
||||
</p>
|
||||
</div>
|
||||
<div class="comments">
|
||||
</div>
|
||||
<div class="timestamp pt-1">
|
||||
<p class="small text-uppercase mb-0">
|
||||
<span class="text-muted">
|
||||
Draft
|
||||
</span>
|
||||
</p>
|
||||
<textarea class="form-control mb-0 border-0 rounded-0" rows="3" placeholder="Add an optional caption" v-model="composeText"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="[mediaDrawer?'d-none':'card-footer']">
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="custom-control custom-switch d-inline mr-3">
|
||||
|
@ -135,7 +137,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a :class="[visibility=='private'?'dropdown-item active':'dropdown-item']" href="#" data-id="private" data-title="Followers Only" v-on:click.prevent="visibility = 'unlisted'">
|
||||
<a :class="[visibility=='unlisted'?'dropdown-item active':'dropdown-item']" href="#" data-id="private" data-title="Unlisted" v-on:click.prevent="visibility = 'unlisted'">
|
||||
<div class="row">
|
||||
<div class="d-none d-block-sm col-sm-2 px-0 text-center">
|
||||
<i class="fas fa-lock"></i>
|
||||
|
@ -192,6 +194,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer py-1">
|
||||
<p class="text-center mb-0 font-weight-bold text-muted small">Having issues? You can also use the <a href="/i/compose">Classic Compose UI</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -234,7 +239,9 @@ export default {
|
|||
carouselCursor: 0,
|
||||
visibility: 'public',
|
||||
mediaDrawer: false,
|
||||
composeState: 'publish'
|
||||
composeState: 'publish',
|
||||
uploading: false,
|
||||
uploadProgress: 0
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -301,6 +308,9 @@ export default {
|
|||
fetchProfile() {
|
||||
axios.get('/api/v1/accounts/verify_credentials').then(res => {
|
||||
this.profile = res.data;
|
||||
if(res.data.locked == true) {
|
||||
this.visibility = 'private';
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
});
|
||||
|
@ -320,6 +330,7 @@ export default {
|
|||
$(document).on('change', '.file-input', function(e) {
|
||||
let io = document.querySelector('.file-input');
|
||||
Array.prototype.forEach.call(io.files, function(io, i) {
|
||||
self.uploading = true;
|
||||
if(self.media && self.media.length + i >= self.config.uploader.album_limit) {
|
||||
swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');
|
||||
return;
|
||||
|
@ -338,20 +349,25 @@ export default {
|
|||
let xhrConfig = {
|
||||
onUploadProgress: function(e) {
|
||||
let progress = Math.round( (e.loaded * 100) / e.total );
|
||||
self.uploadProgress = progress;
|
||||
}
|
||||
};
|
||||
|
||||
axios.post('/api/v1/media', form, xhrConfig)
|
||||
.then(function(e) {
|
||||
self.uploadProgress = 100;
|
||||
self.ids.push(e.data.id);
|
||||
self.media.push(e.data);
|
||||
setTimeout(function() {
|
||||
self.mediaDrawer = true;
|
||||
self.uploading = false;
|
||||
}, 1000);
|
||||
}).catch(function(e) {
|
||||
self.uploading = false;
|
||||
io.value = null;
|
||||
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
|
||||
});
|
||||
io.value = null;
|
||||
self.uploadProgress = 0;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,195 +1,285 @@
|
|||
<template>
|
||||
<div class="postComponent d-none">
|
||||
<div class="container px-0">
|
||||
<div class="card card-md-rounded-0 status-container orientation-unknown">
|
||||
<div class="row px-0 mx-0">
|
||||
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
|
||||
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
|
||||
<div class="status-avatar mr-2">
|
||||
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
|
||||
</div>
|
||||
<div class="username">
|
||||
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div v-if="user != false" class="float-right">
|
||||
<div class="post-actions">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<div v-if="!owner()">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile()">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
|
||||
</div>
|
||||
<div v-if="ownerOrAdmin()">
|
||||
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
|
||||
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="loaded && warning" class="bg-white pt-3 border-bottom">
|
||||
<div class="container">
|
||||
<p class="text-center font-weight-bold">You are blocking this account</p>
|
||||
<p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false; fetchData()">here</a> to view this status</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loaded && warning == false" class="postComponent">
|
||||
<div v-if="profileLayout == 'metro'" class="container px-0">
|
||||
<div class="card card-md-rounded-0 status-container orientation-unknown">
|
||||
<div class="row px-0 mx-0">
|
||||
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
|
||||
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
|
||||
<div class="status-avatar mr-2">
|
||||
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 px-0 mx-0">
|
||||
<div class="postPresenterLoader text-center">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
<div class="username">
|
||||
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
|
||||
</div>
|
||||
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center">
|
||||
<div v-if="status.pf_type === 'photo'" class="w-100">
|
||||
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video'" class="w-100">
|
||||
<video-presenter :status="status"></video-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
|
||||
<video-album-presenter :status="status"></video-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
|
||||
<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-100">
|
||||
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
|
||||
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
|
||||
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
|
||||
<div class="status-avatar mr-2">
|
||||
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
|
||||
</div>
|
||||
<div class="username">
|
||||
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
<div class="post-actions">
|
||||
<div v-if="user != false" class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<span v-if="!owner()">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
|
||||
</span>
|
||||
<span v-if="ownerOrAdmin()">
|
||||
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
|
||||
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<div v-if="user != false" class="float-right">
|
||||
<div class="post-actions">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<div v-if="!owner()">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile()">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
|
||||
</div>
|
||||
<div v-if="ownerOrAdmin()">
|
||||
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
|
||||
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-md-column flex-column-reverse h-100">
|
||||
<div class="card-body status-comments pb-5">
|
||||
<div class="status-comment">
|
||||
<p class="mb-1 read-more" style="overflow: hidden;">
|
||||
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
|
||||
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 px-0 mx-0">
|
||||
<div class="postPresenterLoader text-center">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
</div>
|
||||
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center">
|
||||
<div v-if="status.pf_type === 'photo'" class="w-100">
|
||||
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
|
||||
</div>
|
||||
|
||||
<div v-if="showComments">
|
||||
<div class="postCommentsLoader text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="postCommentsContainer d-none pt-3">
|
||||
<p class="mb-1 text-center load-more-link d-none"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
|
||||
<div class="comments" data-min-id="0" data-max-id="0">
|
||||
<div v-for="(reply, index) in results" class="pb-3">
|
||||
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
|
||||
<span>
|
||||
<a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
|
||||
<span class="text-break" v-html="reply.content"></span>
|
||||
<div v-else-if="status.pf_type === 'video'" class="w-100">
|
||||
<video-presenter :status="status"></video-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
|
||||
<video-album-presenter :status="status"></video-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
|
||||
<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-100">
|
||||
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
|
||||
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
|
||||
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
|
||||
<div class="status-avatar mr-2">
|
||||
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
|
||||
</div>
|
||||
<div class="username">
|
||||
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
<div class="post-actions">
|
||||
<div v-if="user != false" class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<span v-if="!owner()">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
|
||||
</span>
|
||||
<span class="pl-2" style="min-width:38px">
|
||||
<span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
|
||||
<post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
|
||||
<span v-if="ownerOrAdmin()">
|
||||
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
|
||||
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
|
||||
</span>
|
||||
</p>
|
||||
<p class="">
|
||||
<span class="text-muted mr-3" style="width: 20px;" v-text="timeAgo(reply.created_at)"></span>
|
||||
<span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
|
||||
<span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply)">Reply</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-md-column flex-column-reverse h-100">
|
||||
<div class="card-body status-comments pb-5">
|
||||
<div class="status-comment">
|
||||
<p class="mb-1 read-more" style="overflow: hidden;">
|
||||
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
|
||||
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
|
||||
</p>
|
||||
|
||||
<div v-if="showComments">
|
||||
<div class="postCommentsLoader text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="postCommentsContainer d-none pt-3">
|
||||
<p v-if="status.reply_count > 10"class="mb-1 text-center load-more-link d-none"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
|
||||
<div class="comments" data-min-id="0" data-max-id="0">
|
||||
<div v-for="(reply, index) in results" class="pb-3">
|
||||
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
|
||||
<span>
|
||||
<a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
|
||||
<span class="text-break" v-html="reply.content"></span>
|
||||
</span>
|
||||
<span class="pl-2" style="min-width:38px">
|
||||
<span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
|
||||
<post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
|
||||
</span>
|
||||
</p>
|
||||
<p class="">
|
||||
<span class="text-muted mr-3" style="width: 20px;" v-text="timeAgo(reply.created_at)"></span>
|
||||
<span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
|
||||
<span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply)">Reply</span>
|
||||
</p>
|
||||
<div v-if="reply.reply_count > 0" class="cursor-pointer" style="margin-left:30px;" v-on:click="toggleReplies(reply)">
|
||||
<span class="show-reply-bar"></span>
|
||||
<span class="comment-reaction font-weight-bold text-muted">{{reply.thread ? 'Hide' : 'View'}} Replies ({{reply.reply_count}})</span>
|
||||
</div>
|
||||
<div v-if="reply.thread == true" class="comment-thread">
|
||||
<p class="d-flex justify-content-between align-items-top read-more pb-3" style="overflow-y: hidden;" v-for="(s, index) in reply.replies">
|
||||
<span>
|
||||
<a class="text-dark font-weight-bold mr-1" :href="s.account.url" :title="s.account.username">{{s.account.username}}</a>
|
||||
<span class="text-break" v-html="s.content"></span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body flex-grow-0 py-1">
|
||||
<div class="reactions my-1">
|
||||
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
|
||||
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
|
||||
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
|
||||
<h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
|
||||
</div>
|
||||
<div class="reaction-counts font-weight-bold mb-0">
|
||||
<span style="cursor:pointer;" v-on:click="likesModal">
|
||||
<span class="like-count">{{status.favourites_count || 0}}</span> likes
|
||||
</span>
|
||||
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
|
||||
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
|
||||
</span>
|
||||
</div>
|
||||
<div class="timestamp pt-2 d-flex align-items-bottom justify-content-between">
|
||||
<a v-bind:href="statusUrl" class="small text-muted">
|
||||
{{timestampFormat()}}
|
||||
</a>
|
||||
<span class="small text-muted text-capitalize cursor-pointer" v-on:click="visibilityModal">{{status.visibility}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body flex-grow-0 py-1">
|
||||
<div class="reactions my-1">
|
||||
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
|
||||
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
|
||||
<li class="nav-item" v-on:click="emojiReaction">😂</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">💯</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">❤️</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">🙌</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">👏</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😍</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😯</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😢</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😅</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😁</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">🙂</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😎</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
|
||||
<div v-if="user.length == 0" class="comment-form-guest p-3">
|
||||
<a href="/login">Login</a> to like or comment.
|
||||
</div>
|
||||
<form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
|
||||
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
|
||||
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="profileLayout == 'moment'" class="momentui">
|
||||
<div class="bg-dark mt-md-n4">
|
||||
<div class="container">
|
||||
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center">
|
||||
<div v-if="status.pf_type === 'photo'" class="w-100">
|
||||
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video'" class="w-100">
|
||||
<video-presenter :status="status"></video-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
|
||||
<video-album-presenter :status="status"></video-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
|
||||
<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-100">
|
||||
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<div class="container">
|
||||
<div class="row py-5">
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="reactions py-2">
|
||||
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
|
||||
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
|
||||
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
|
||||
<h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
|
||||
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary float-right cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn float-right cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
|
||||
</div>
|
||||
<div class="reaction-counts font-weight-bold mb-0">
|
||||
<span style="cursor:pointer;" v-on:click="likesModal">
|
||||
<span class="like-count">{{status.favourites_count || 0}}</span> likes
|
||||
</span>
|
||||
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
|
||||
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
|
||||
</span>
|
||||
<div class="reaction-counts font-weight-bold mb-0">
|
||||
<span style="cursor:pointer;" v-on:click="likesModal">
|
||||
<span class="like-count">{{status.favourites_count || 0}}</span> likes
|
||||
</span>
|
||||
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
|
||||
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
|
||||
</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="media align-items-center">
|
||||
<img :src="statusAvatar" class="rounded-circle shadow-lg mr-3" alt="avatar" width="72px" height="72px">
|
||||
<div class="media-body lead">
|
||||
by <a href="#">{{statusUsername}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timestamp">
|
||||
<a v-bind:href="statusUrl" class="small text-muted">
|
||||
{{timestampFormat()}}
|
||||
</a>
|
||||
<hr>
|
||||
<div>
|
||||
<p class="lead"><i class="far fa-clock"></i> {{timestampFormat()}}</p>
|
||||
<div class="lead" v-html="status.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
|
||||
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
|
||||
<li class="nav-item" v-on:click="emojiReaction">😂</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">💯</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">❤️</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">🙌</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">👏</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😍</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😯</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😢</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😅</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😁</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">🙂</li>
|
||||
<li class="nav-item" v-on:click="emojiReaction">😎</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
|
||||
<div v-if="user.length == 0" class="comment-form-guest p-3">
|
||||
<a href="/login">Login</a> to like or comment.
|
||||
<div class="col-12 col-md-4">
|
||||
<div v-if="status.comments_disabled" class="bg-light p-5 text-center lead">
|
||||
<p class="mb-0">Comments have been disabled on this post.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
|
||||
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
|
||||
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-modal ref="likesModal"
|
||||
id="l-modal"
|
||||
hide-footer
|
||||
|
@ -323,7 +413,7 @@
|
|||
}
|
||||
.emoji-reactions .nav-item {
|
||||
font-size: 1.2rem;
|
||||
padding: 7px;
|
||||
padding: 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.emoji-reactions::-webkit-scrollbar {
|
||||
|
@ -332,13 +422,31 @@
|
|||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
.momentui .bg-dark {
|
||||
background: #000 !important;
|
||||
}
|
||||
.momentui .carousel.slide,
|
||||
.momentui .carousel-item {
|
||||
background: #000 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
pixelfed.postComponent = {};
|
||||
|
||||
export default {
|
||||
props: ['status-id', 'status-username', 'status-template', 'status-url', 'status-profile-url', 'status-avatar'],
|
||||
props: [
|
||||
'status-id',
|
||||
'status-username',
|
||||
'status-template',
|
||||
'status-url',
|
||||
'status-profile-url',
|
||||
'status-avatar',
|
||||
'status-profile-id',
|
||||
'profile-layout'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
status: false,
|
||||
|
@ -354,20 +462,24 @@ export default {
|
|||
sharesPage: 1,
|
||||
lightboxMedia: false,
|
||||
replyText: '',
|
||||
|
||||
relationship: {},
|
||||
results: [],
|
||||
pagination: {},
|
||||
min_id: 0,
|
||||
max_id: 0,
|
||||
reply_to_profile_id: 0,
|
||||
thread: false,
|
||||
showComments: false
|
||||
showComments: false,
|
||||
warning: false,
|
||||
loaded: false,
|
||||
loading: null,
|
||||
replyingToId: this.statusId,
|
||||
emoji: ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','😗','😙','😚','☺️','🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹️','🙁','😖','😞','😟','😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🙌','👏','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👌','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'],
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
this.authCheck();
|
||||
this.fetchRelationships();
|
||||
let token = $('meta[name="csrf-token"]').attr('content');
|
||||
$('input[name="_token"]').each(function(k, v) {
|
||||
let el = $(v);
|
||||
|
@ -395,14 +507,6 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
authCheck() {
|
||||
let authed = $('body').hasClass('loggedIn');
|
||||
if(authed == true) {
|
||||
$('.comment-form-guest').addClass('d-none');
|
||||
$('.comment-form').removeClass('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
showMuteBlock() {
|
||||
let sid = this.status.account.id;
|
||||
let uid = this.user.id;
|
||||
|
@ -427,10 +531,6 @@ export default {
|
|||
},
|
||||
|
||||
fetchData() {
|
||||
let loader = this.$loading.show({
|
||||
'opacity': 0,
|
||||
'background-color': '#f5f8fa'
|
||||
});
|
||||
axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId)
|
||||
.then(response => {
|
||||
let self = this;
|
||||
|
@ -444,7 +544,6 @@ export default {
|
|||
self.sharesPage = 2;
|
||||
//this.buildPresenter();
|
||||
this.showMuteBlock();
|
||||
loader.hide();
|
||||
pixelfed.readmore();
|
||||
if(self.status.comments_disabled == false) {
|
||||
self.showComments = true;
|
||||
|
@ -671,16 +770,20 @@ export default {
|
|||
return;
|
||||
}
|
||||
let data = {
|
||||
item: this.statusId,
|
||||
item: this.replyingToId,
|
||||
comment: this.replyText
|
||||
}
|
||||
axios.post('/i/comment', data)
|
||||
.then(function(res) {
|
||||
let entity = res.data.entity;
|
||||
self.results.push(entity);
|
||||
if(entity.in_reply_to_id == self.status.id) {
|
||||
self.results.push(entity);
|
||||
let elem = $('.status-comments')[0];
|
||||
elem.scrollTop = elem.clientHeight;
|
||||
} else {
|
||||
|
||||
}
|
||||
self.replyText = '';
|
||||
let elem = $('.status-comments')[0];
|
||||
elem.scrollTop = elem.clientHeight;
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -694,16 +797,20 @@ export default {
|
|||
swal('Something went wrong!', 'Please try again later', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
l(e) {
|
||||
let len = e.length;
|
||||
if(len < 10) { return e; }
|
||||
return e.substr(0, 10)+'...';
|
||||
},
|
||||
|
||||
replyFocus(e) {
|
||||
this.replyingToId = e.id;
|
||||
this.reply_to_profile_id = e.account.id;
|
||||
this.replyText = '@' + e.account.username + ' ';
|
||||
$('textarea[name="comment"]').focus();
|
||||
},
|
||||
|
||||
fetchComments() {
|
||||
let url = '/api/v2/comments/'+this.statusUsername+'/status/'+this.statusId;
|
||||
axios.get(url)
|
||||
|
@ -741,6 +848,7 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMore(e) {
|
||||
e.preventDefault();
|
||||
if(this.pagination.total_pages == 1 || this.pagination.current_page == this.pagination.total_pages) {
|
||||
|
@ -760,6 +868,7 @@ export default {
|
|||
this.pagination = response.data.meta.pagination;
|
||||
});
|
||||
},
|
||||
|
||||
likeReply(status, $event) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
|
@ -778,11 +887,13 @@ export default {
|
|||
swal('Error', 'Something went wrong, please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
truncate(str,lim) {
|
||||
return _.truncate(str,{
|
||||
length: lim
|
||||
});
|
||||
},
|
||||
|
||||
timeAgo(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
|
@ -852,6 +963,77 @@ export default {
|
|||
return;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchRelationships() {
|
||||
let loader = this.$loading.show({
|
||||
'opacity': 0,
|
||||
'background-color': '#f5f8fa'
|
||||
});
|
||||
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
|
||||
this.loaded = true;
|
||||
loader.hide();
|
||||
this.fetchData();
|
||||
return;
|
||||
} else {
|
||||
axios.get('/api/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.statusProfileId
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data[0] == null) {
|
||||
this.loaded = true;
|
||||
loader.hide();
|
||||
this.fetchData();
|
||||
return;
|
||||
}
|
||||
this.relationship = res.data[0];
|
||||
if(res.data[0].blocking == true) {
|
||||
this.loaded = true;
|
||||
loader.hide();
|
||||
this.warning = true;
|
||||
return;
|
||||
} else {
|
||||
this.loaded = true;
|
||||
loader.hide();
|
||||
this.fetchData();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
visibilityModal() {
|
||||
switch(this.status.visibility) {
|
||||
case 'public':
|
||||
swal('Public Post', 'This post is visible to everyone.', 'info');
|
||||
break;
|
||||
|
||||
case 'unlisted':
|
||||
swal('Unlisted Post', 'This post is visible on profiles and with a direct links. It is not displayed on timelines.', 'info');
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
swal('Private Post', 'This post is only visible to followers.', 'info');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
toggleReplies(reply) {
|
||||
if(reply.thread) {
|
||||
reply.thread = false;
|
||||
} else {
|
||||
if(reply.replies.length > 0) {
|
||||
reply.thread = true;
|
||||
return;
|
||||
}
|
||||
let url = '/api/v2/comments/'+reply.account.username+'/status/'+reply.id;
|
||||
axios.get(url)
|
||||
.then(response => {
|
||||
reply.replies = _.reverse(response.data.data);
|
||||
reply.thread = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
|
|
@ -1,278 +1,347 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="d-flex justify-content-center py-5 my-5" v-if="loading">
|
||||
<div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom">
|
||||
<div class="container">
|
||||
<p class="text-center font-weight-bold">You are blocking this account</p>
|
||||
<p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false;">here</a> to view profile</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="d-flex justify-content-center py-5 my-5">
|
||||
<img src="/img/pixelfed-icon-grey.svg" class="">
|
||||
</div>
|
||||
<div v-if="!loading">
|
||||
<div class="bg-white py-5 border-bottom">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 d-flex">
|
||||
<div class="profile-avatar mx-md-auto">
|
||||
<div class="d-block d-md-none">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px">
|
||||
</div>
|
||||
<div class="col-7 pl-2">
|
||||
<p class="font-weight-ultralight h3 mb-0">{{profile.username}}</p>
|
||||
<p v-if="profile.id == user.id && user.hasOwnProperty('id')">
|
||||
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
|
||||
</p>
|
||||
<div v-if="profile.id != user.id && user.hasOwnProperty('id')">
|
||||
<p class="mt-3 mb-0" v-if="relationship.following == true">
|
||||
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow">Unfollow</button>
|
||||
<div v-if="!loading && !warning">
|
||||
<div v-if="profileLayout == 'metro'">
|
||||
<div class="bg-white py-5 border-bottom">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 d-flex">
|
||||
<div class="profile-avatar mx-md-auto">
|
||||
<div class="d-block d-md-none">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px">
|
||||
</div>
|
||||
<div class="col-7 pl-2">
|
||||
<p class="align-middle">
|
||||
|
||||
<span class="font-weight-ultralight h3 mb-0">{{profile.username}}</span>
|
||||
<span class="float-right mb-0" v-if="profile.id != user.id && user.hasOwnProperty('id')">
|
||||
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
|
||||
</span>
|
||||
</p>
|
||||
<p class="mt-3 mb-0" v-if="!relationship.following">
|
||||
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Follow">Follow</button>
|
||||
<p v-if="profile.id == user.id && user.hasOwnProperty('id')">
|
||||
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
|
||||
</p>
|
||||
<div v-if="profile.id != user.id && user.hasOwnProperty('id')">
|
||||
<p class="mt-3 mb-0" v-if="relationship.following == true">
|
||||
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow">Unfollow</button>
|
||||
</p>
|
||||
<p class="mt-3 mb-0" v-if="!relationship.following">
|
||||
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Follow">Follow</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-md-block">
|
||||
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-md-block">
|
||||
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
|
||||
</div>
|
||||
<div class="col-12 col-md-8 d-flex align-items-center">
|
||||
<div class="profile-details">
|
||||
<div class="d-none d-md-flex username-bar pb-2 align-items-center">
|
||||
<span class="font-weight-ultralight h3">{{profile.username}}</span>
|
||||
<span class="pl-4" v-if="profile.is_admin">
|
||||
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span>
|
||||
</span>
|
||||
<span class="pl-4">
|
||||
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a>
|
||||
</span>
|
||||
<span class="pl-4" v-if="owner">
|
||||
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a>
|
||||
</span>
|
||||
<span class="pl-4" v-if="profile.id != user.id && user.hasOwnProperty('id')">
|
||||
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
|
||||
</span>
|
||||
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
|
||||
<span class="pl-4" v-if="relationship.following == true">
|
||||
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-minus"></i></button>
|
||||
</span>
|
||||
<span class="pl-4" v-if="!relationship.following">
|
||||
<button type="button" class="btn btn-primary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Follow"><i class="fas fa-user-plus"></i></button>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-none d-md-inline-flex profile-stats pb-3 lead">
|
||||
<div class="font-weight-light pr-5">
|
||||
<a class="text-dark" :href="profile.url">
|
||||
<span class="font-weight-bold">{{profile.statuses_count}}</span>
|
||||
Posts
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
|
||||
<a class="text-dark cursor-pointer" v-on:click="followersModal()">
|
||||
<span class="font-weight-bold">{{profile.followers_count}}</span>
|
||||
Followers
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="profileSettings.following.count" class="font-weight-light">
|
||||
<a class="text-dark cursor-pointer" v-on:click="followingModal()">
|
||||
<span class="font-weight-bold">{{profile.following_count}}</span>
|
||||
Following
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="lead mb-0 d-flex align-items-center pt-3">
|
||||
<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
|
||||
</p>
|
||||
<div v-if="profile.note" class="mb-0 lead" v-html="profile.note"></div>
|
||||
<p v-if="profile.website" class="mb-0"><a :href="profile.website" class="font-weight-bold" rel="me external nofollow noopener" target="_blank">{{profile.website}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 d-flex align-items-center">
|
||||
<div class="profile-details">
|
||||
<div class="d-none d-md-flex username-bar pb-2 align-items-center">
|
||||
<span class="font-weight-ultralight h3">{{profile.username}}</span>
|
||||
<span class="pl-4" v-if="profile.is_admin">
|
||||
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span>
|
||||
</span>
|
||||
<span class="pl-4">
|
||||
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a>
|
||||
</span>
|
||||
<span class="pl-4" v-if="owner">
|
||||
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a>
|
||||
</span>
|
||||
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
|
||||
<span class="pl-4" v-if="relationship.following == true">
|
||||
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-minus"></i></button>
|
||||
</span>
|
||||
<span class="pl-4" v-if="!relationship.following">
|
||||
<button type="button" class="btn btn-primary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Follow"><i class="fas fa-user-plus"></i></button>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-block d-md-none bg-white my-0 py-2 border-bottom">
|
||||
<ul class="nav d-flex justify-content-center">
|
||||
<li class="nav-item">
|
||||
<div class="font-weight-light">
|
||||
<span class="text-dark text-center">
|
||||
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
|
||||
<p class="text-muted mb-0">Posts</p>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item px-5">
|
||||
<div v-if="profileSettings.followers.count" class="font-weight-light">
|
||||
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
|
||||
<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
|
||||
<p class="text-muted mb-0">Followers</p>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div v-if="profileSettings.following.count" class="font-weight-light">
|
||||
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
|
||||
<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
|
||||
<p class="text-muted mb-0">Following</p>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<ul class="nav nav-topbar d-flex justify-content-center border-0">
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link active font-weight-bold text-uppercase" :href="profile.url">Posts</a>
|
||||
</li>
|
||||
-->
|
||||
<li class="nav-item">
|
||||
<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th fa-lg"></i></a>
|
||||
</li>
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
<a :class="this.mode == 'masonry' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('masonry')"><i class="fas fa-th-large"></i></a>
|
||||
</li> -->
|
||||
|
||||
<li class="nav-item px-3">
|
||||
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" v-if="owner">
|
||||
<a class="nav-link font-weight-bold text-uppercase" :href="profile.url + '/saved'">Saved</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="profile-timeline mt-md-4">
|
||||
<div class="row" v-if="mode == 'grid'">
|
||||
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="(s, index) in timeline">
|
||||
<a class="card info-overlay card-md-border-0" :href="s.url">
|
||||
<div class="square">
|
||||
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
|
||||
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
|
||||
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
|
||||
<div class="square-content" v-bind:style="previewBackground(s)">
|
||||
</div>
|
||||
<div class="info-overlay-text">
|
||||
<h5 class="text-white m-auto font-weight-bold">
|
||||
<span>
|
||||
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
|
||||
<span class="d-flex-inline">{{s.favourites_count}}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
|
||||
<span class="d-flex-inline">{{s.reblogs_count}}</span>
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="mode == 'list'">
|
||||
<div class="col-md-8 col-lg-8 offset-md-2 px-0 mb-3 timeline">
|
||||
<div class="card status-card card-md-rounded-0 my-sm-2 my-md-3 my-lg-4" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
|
||||
|
||||
<div class="card-header d-inline-flex align-items-center bg-white">
|
||||
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
|
||||
<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
|
||||
{{status.account.username}}
|
||||
</a>
|
||||
<div v-if="user.hasOwnProperty('id')" class="text-right" style="flex-grow:1;">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
|
||||
<span v-if="status.account.id != user.id">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
|
||||
</span>
|
||||
<span v-if="status.account.id == user.id || user.is_admin == true">
|
||||
<a class="dropdown-item font-weight-bold" :href="editUrl(status)">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="postPresenterContainer">
|
||||
<div v-if="status.pf_type === 'photo'" class="w-100">
|
||||
<photo-presenter :status="status"></photo-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video'" class="w-100">
|
||||
<video-presenter :status="status"></video-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
<photo-album-presenter :status="status"></photo-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
|
||||
<video-album-presenter :status="status"></video-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
|
||||
<mixed-album-presenter :status="status"></mixed-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-100">
|
||||
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="reactions my-1" v-if="user.hasOwnProperty('id')">
|
||||
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
|
||||
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
|
||||
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
|
||||
</div>
|
||||
|
||||
<div class="likes font-weight-bold">
|
||||
<span class="like-count">{{status.favourites_count}}</span> {{status.favourites_count == 1 ? 'like' : 'likes'}}
|
||||
</div>
|
||||
<div class="caption">
|
||||
<p class="mb-2 read-more" style="overflow: hidden;">
|
||||
<span class="username font-weight-bold">
|
||||
<bdi><a class="text-dark" :href="status.account.url">{{status.account.username}}</a></bdi>
|
||||
</span>
|
||||
<span v-html="status.content"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="comments">
|
||||
</div>
|
||||
<div class="timestamp pt-1">
|
||||
<p class="small text-uppercase mb-0">
|
||||
<a :href="status.url" class="text-muted">
|
||||
<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-white d-none">
|
||||
<form class="" v-on:submit.prevent="commentSubmit(status, $event)">
|
||||
<input type="hidden" name="item" value="">
|
||||
<input class="form-control status-reply-input" name="comment" placeholder="Add a comment…" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-md-inline-flex profile-stats pb-3 lead">
|
||||
<div class="font-weight-light pr-5">
|
||||
<a class="text-dark" :href="profile.url">
|
||||
</div>
|
||||
</div>
|
||||
<div class="masonry-grid" v-if="mode == 'masonry'">
|
||||
<div class="d-inline p-0 p-sm-2 p-md-3 masonry-item" v-for="(status, index) in timeline">
|
||||
<a class="" v-on:click.prevent="statusModal(status)" :href="status.url">
|
||||
<img :src="previewUrl(status)" :class="'o-'+masonryOrientation(status)">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="timeline.length">
|
||||
<infinite-loading @infinite="infiniteTimeline">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="profileLayout == 'moment'">
|
||||
<div class="w-100 h-100 mt-n3 bg-pixelfed" style="width:100%;min-height:274px;">
|
||||
</div>
|
||||
<div class="bg-white border-bottom">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px" style="margin-top:-90px; border: 5px solid #fff">
|
||||
</div>
|
||||
|
||||
<div class="col-12 text-center">
|
||||
<div class="profile-details my-3">
|
||||
<p class="font-weight-ultralight h2 text-center">{{profile.username}}</p>
|
||||
<div v-if="profile.note" class="text-center text-muted p-3" v-html="profile.note"></div>
|
||||
<div class="pb-3 text-muted text-center">
|
||||
<a class="text-lighter" :href="profile.url">
|
||||
<span class="font-weight-bold">{{profile.statuses_count}}</span>
|
||||
Posts
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
|
||||
<a class="text-dark cursor-pointer" v-on:click="followersModal()">
|
||||
<a v-if="profileSettings.followers.count" class="text-lighter cursor-pointer px-3" v-on:click="followersModal()">
|
||||
<span class="font-weight-bold">{{profile.followers_count}}</span>
|
||||
Followers
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="profileSettings.following.count" class="font-weight-light">
|
||||
<a class="text-dark cursor-pointer" v-on:click="followingModal()">
|
||||
<a v-if="profileSettings.following.count" class="text-lighter cursor-pointer" v-on:click="followingModal()">
|
||||
<span class="font-weight-bold">{{profile.following_count}}</span>
|
||||
Following
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="lead mb-0 d-flex align-items-center pt-3">
|
||||
<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
|
||||
</p>
|
||||
<div v-if="profile.note" class="mb-0 lead" v-html="profile.note"></div>
|
||||
<p v-if="profile.website" class="mb-0"><a :href="profile.website" class="font-weight-bold" rel="me external nofollow noopener" target="_blank">{{profile.website}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-block d-md-none bg-white my-0 py-2 border-bottom">
|
||||
<ul class="nav d-flex justify-content-center">
|
||||
<li class="nav-item">
|
||||
<div class="font-weight-light">
|
||||
<span class="text-dark text-center">
|
||||
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
|
||||
<p class="text-muted mb-0">Posts</p>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item px-5">
|
||||
<div v-if="profileSettings.followers.count" class="font-weight-light">
|
||||
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
|
||||
<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
|
||||
<p class="text-muted mb-0">Followers</p>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div v-if="profileSettings.following.count" class="font-weight-light">
|
||||
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
|
||||
<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
|
||||
<p class="text-muted mb-0">Following</p>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<ul class="nav nav-topbar d-flex justify-content-center border-0">
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link active font-weight-bold text-uppercase" :href="profile.url">Posts</a>
|
||||
</li>
|
||||
-->
|
||||
<li class="nav-item">
|
||||
<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th fa-lg"></i></a>
|
||||
</li>
|
||||
|
||||
<!-- <li class="nav-item">
|
||||
<a :class="this.mode == 'masonry' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('masonry')"><i class="fas fa-th-large"></i></a>
|
||||
</li> -->
|
||||
|
||||
<li class="nav-item px-3">
|
||||
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" v-if="owner">
|
||||
<a class="nav-link font-weight-bold text-uppercase" :href="profile.url + '/saved'">Saved</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="profile-timeline mt-md-4">
|
||||
<div class="row" v-if="mode == 'grid'">
|
||||
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="(s, index) in timeline">
|
||||
<a class="card info-overlay card-md-border-0" :href="s.url">
|
||||
<div class="square">
|
||||
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
|
||||
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
|
||||
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
|
||||
<div class="square-content" v-bind:style="previewBackground(s)">
|
||||
</div>
|
||||
<div class="info-overlay-text">
|
||||
<h5 class="text-white m-auto font-weight-bold">
|
||||
<span>
|
||||
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
|
||||
<span class="d-flex-inline">{{s.favourites_count}}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
|
||||
<span class="d-flex-inline">{{s.reblogs_count}}</span>
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="mode == 'list'">
|
||||
<div class="col-md-8 col-lg-8 offset-md-2 px-0 mb-3 timeline">
|
||||
<div class="card status-card card-md-rounded-0 my-sm-2 my-md-3 my-lg-4" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
|
||||
|
||||
<div class="card-header d-inline-flex align-items-center bg-white">
|
||||
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
|
||||
<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
|
||||
{{status.account.username}}
|
||||
</a>
|
||||
<div class="text-right" style="flex-grow:1;">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
|
||||
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
|
||||
<span v-bind:class="[statusOwner(status) ? 'd-none' : '']">
|
||||
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
|
||||
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
|
||||
</span>
|
||||
<span v-bind:class="[statusOwner(status) ? '' : 'd-none']">
|
||||
<a class="dropdown-item font-weight-bold" :href="editUrl(status)">Edit</a>
|
||||
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="postPresenterContainer">
|
||||
<div v-if="status.pf_type === 'photo'" class="w-100">
|
||||
<photo-presenter :status="status"></photo-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video'" class="w-100">
|
||||
<video-presenter :status="status"></video-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
<photo-album-presenter :status="status"></photo-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
|
||||
<video-album-presenter :status="status"></video-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
|
||||
<mixed-album-presenter :status="status"></mixed-album-presenter>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-100">
|
||||
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="reactions my-1">
|
||||
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
|
||||
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
|
||||
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
|
||||
</div>
|
||||
|
||||
<div class="likes font-weight-bold">
|
||||
<span class="like-count">{{status.favourites_count}}</span> {{status.favourites_count == 1 ? 'like' : 'likes'}}
|
||||
</div>
|
||||
<div class="caption">
|
||||
<p class="mb-2 read-more" style="overflow: hidden;">
|
||||
<span class="username font-weight-bold">
|
||||
<bdi><a class="text-dark" :href="status.account.url">{{status.account.username}}</a></bdi>
|
||||
</span>
|
||||
<span v-html="status.content"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="comments">
|
||||
</div>
|
||||
<div class="timestamp pt-1">
|
||||
<p class="small text-uppercase mb-0">
|
||||
<a :href="status.url" class="text-muted">
|
||||
<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-white d-none">
|
||||
<form class="" v-on:submit.prevent="commentSubmit(status, $event)">
|
||||
<input type="hidden" name="item" value="">
|
||||
<input class="form-control status-reply-input" name="comment" placeholder="Add a comment…" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="profile-timeline mt-md-4">
|
||||
<div class="card-columns" v-if="mode == 'grid'">
|
||||
<div class="p-sm-2 p-md-3" v-for="(s, index) in timeline">
|
||||
<a class="card info-overlay card-md-border-0" :href="s.url">
|
||||
<img :src="s.media_attachments[0].url" class="img-fluid">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="masonry-grid" v-if="mode == 'masonry'">
|
||||
<div class="d-inline p-0 p-sm-2 p-md-3 masonry-item" v-for="(status, index) in timeline">
|
||||
<a class="" v-on:click.prevent="statusModal(status)" :href="status.url">
|
||||
<img :src="previewUrl(status)" :class="'o-'+masonryOrientation(status)">
|
||||
</a>
|
||||
<div v-if="timeline.length">
|
||||
<infinite-loading @infinite="infiniteTimeline">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="timeline.length">
|
||||
<infinite-loading @infinite="infiniteTimeline">
|
||||
<div slot="no-more"></div>
|
||||
<div slot="no-results"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -289,7 +358,7 @@
|
|||
<div class="list-group-item border-0" v-for="(user, index) in following" :key="'following_'+index">
|
||||
<div class="media">
|
||||
<a :href="user.url">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
|
@ -318,7 +387,7 @@
|
|||
<div class="list-group-item border-0" v-for="(user, index) in followers" :key="'follower_'+index">
|
||||
<div class="media">
|
||||
<a :href="user.url">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
|
@ -337,6 +406,40 @@
|
|||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="visitorContextMenu"
|
||||
id="visitor-context-menu"
|
||||
hide-footer
|
||||
hide-header
|
||||
centered
|
||||
size="sm"
|
||||
body-class="list-group-flush p-0">
|
||||
<div class="list-group" v-if="relationship">
|
||||
<div v-if="!owner && !relationship.following" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-primary" @click="followProfile">
|
||||
Follow
|
||||
</div>
|
||||
<div v-if="!owner && relationship.following" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="followProfile">
|
||||
Unfollow
|
||||
</div>
|
||||
<div v-if="!owner && !relationship.muting" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="muteProfile">
|
||||
Mute
|
||||
</div>
|
||||
<div v-if="!owner && relationship.muting" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="unmuteProfile">
|
||||
Unmute
|
||||
</div>
|
||||
<div v-if="!owner" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="reportProfile">
|
||||
Report User
|
||||
</div>
|
||||
<div v-if="!owner && !relationship.blocking" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="blockProfile">
|
||||
Block
|
||||
</div>
|
||||
<div v-if="!owner && relationship.blocking" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="unblockProfile">
|
||||
Unblock
|
||||
</div>
|
||||
<div class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-muted" @click="$refs.visitorContextMenu.hide()">
|
||||
Close
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <style type="text/css" scoped="">
|
||||
|
@ -373,7 +476,8 @@
|
|||
export default {
|
||||
props: [
|
||||
'profile-id',
|
||||
'profile-settings'
|
||||
'profile-settings',
|
||||
'profile-layout'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
|
@ -394,7 +498,8 @@ export default {
|
|||
followerMore: true,
|
||||
following: [],
|
||||
followingCursor: 1,
|
||||
followingMore: true
|
||||
followingMore: true,
|
||||
warning: false
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -412,16 +517,12 @@ export default {
|
|||
axios.get('/api/v1/accounts/' + this.profileId).then(res => {
|
||||
this.profile = res.data;
|
||||
});
|
||||
axios.get('/api/v1/accounts/verify_credentials').then(res => {
|
||||
this.user = res.data;
|
||||
});
|
||||
axios.get('/api/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.profileId
|
||||
}
|
||||
}).then(res => {
|
||||
this.relationship = res.data[0];
|
||||
});
|
||||
if($('body').hasClass('loggedIn') == true) {
|
||||
axios.get('/api/v1/accounts/verify_credentials').then(res => {
|
||||
this.user = res.data;
|
||||
});
|
||||
this.fetchRelationships();
|
||||
}
|
||||
let apiUrl = '/api/v1/accounts/' + this.profileId + '/statuses';
|
||||
axios.get(apiUrl, {
|
||||
params: {
|
||||
|
@ -491,6 +592,11 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
reportProfile() {
|
||||
let id = this.profile.id;
|
||||
window.location.href = '/i/report?type=user&id=' + id;
|
||||
},
|
||||
|
||||
reportUrl(status) {
|
||||
let type = status.in_reply_to ? 'comment' : 'post';
|
||||
let id = status.id;
|
||||
|
@ -617,32 +723,90 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
muteProfile(status) {
|
||||
fetchRelationships() {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
axios.get('/api/v1/accounts/relationships', {
|
||||
params: {
|
||||
'id[]': this.profileId
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.length) {
|
||||
this.relationship = res.data[0];
|
||||
if(res.data[0].blocking == true) {
|
||||
this.warning = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
muteProfile(status = null) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
let id = this.profileId;
|
||||
axios.post('/i/mute', {
|
||||
type: 'user',
|
||||
item: status.account.id
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.feed = this.feed.filter(s => s.account.id !== status.account.id);
|
||||
swal('Success', 'You have successfully muted ' + status.account.acct, 'success');
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
blockProfile(status) {
|
||||
|
||||
unmuteProfile(status = null) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
let id = this.profileId;
|
||||
axios.post('/i/unmute', {
|
||||
type: 'user',
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully unmuted ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
blockProfile(status = null) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
let id = this.profileId;
|
||||
axios.post('/i/block', {
|
||||
type: 'user',
|
||||
item: status.account.id
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.feed = this.feed.filter(s => s.account.id !== status.account.id);
|
||||
swal('Success', 'You have successfully blocked ' + status.account.acct, 'success');
|
||||
this.warning = true;
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
unblockProfile(status = null) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
let id = this.profileId;
|
||||
axios.post('/i/unblock', {
|
||||
type: 'user',
|
||||
item: id
|
||||
}).then(res => {
|
||||
this.fetchRelationships();
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
swal('Success', 'You have successfully unblocked ' + this.profile.acct, 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
});
|
||||
|
@ -657,7 +821,7 @@ export default {
|
|||
type: 'status',
|
||||
item: status.id
|
||||
}).then(res => {
|
||||
this.feed.splice(index,1);
|
||||
this.timeline.splice(index,1);
|
||||
swal('Success', 'You have successfully deleted this post', 'success');
|
||||
}).catch(err => {
|
||||
swal('Error', 'Something went wrong. Please try again later.', 'error');
|
||||
|
@ -665,6 +829,9 @@ export default {
|
|||
},
|
||||
|
||||
commentSubmit(status, $event) {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
let id = status.id;
|
||||
let form = $event.target;
|
||||
let input = $(form).find('input[name="comment"]');
|
||||
|
@ -710,9 +877,13 @@ export default {
|
|||
},
|
||||
|
||||
followProfile() {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
axios.post('/i/follow', {
|
||||
item: this.profileId
|
||||
}).then(res => {
|
||||
this.$refs.visitorContextMenu.hide();
|
||||
if(this.relationship.following) {
|
||||
this.profile.followers_count--;
|
||||
if(this.profile.locked == true) {
|
||||
|
@ -726,6 +897,10 @@ export default {
|
|||
},
|
||||
|
||||
followingModal() {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
|
||||
return;
|
||||
}
|
||||
if(this.profileSettings.following.list == false) {
|
||||
return;
|
||||
}
|
||||
|
@ -749,6 +924,10 @@ export default {
|
|||
},
|
||||
|
||||
followersModal() {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
|
||||
return;
|
||||
}
|
||||
if(this.profileSettings.followers.list == false) {
|
||||
return;
|
||||
}
|
||||
|
@ -772,6 +951,10 @@ export default {
|
|||
},
|
||||
|
||||
followingLoadMore() {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
|
||||
return;
|
||||
}
|
||||
axios.get('/api/v1/accounts/'+this.profile.id+'/following', {
|
||||
params: {
|
||||
page: this.followingCursor
|
||||
|
@ -790,6 +973,9 @@ export default {
|
|||
|
||||
|
||||
followersLoadMore() {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
axios.get('/api/v1/accounts/'+this.profile.id+'/followers', {
|
||||
params: {
|
||||
page: this.followerCursor
|
||||
|
@ -804,6 +990,13 @@ export default {
|
|||
this.followerMore = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
visitorMenu() {
|
||||
if($('body').hasClass('loggedIn') == false) {
|
||||
return;
|
||||
}
|
||||
this.$refs.visitorContextMenu.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,19 +12,19 @@
|
|||
<div v-if="!loading && !networkError" class="mt-5 row">
|
||||
|
||||
<div class="col-12 col-md-3 mb-4">
|
||||
<div>
|
||||
<div v-if="results.hashtags || results.profiles || results.statuses">
|
||||
<p class="font-weight-bold">Filters</p>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="filter1" v-model="filters.hashtags">
|
||||
<label class="custom-control-label text-muted" for="filter1">Show Hashtags</label>
|
||||
<label class="custom-control-label text-muted font-weight-light" for="filter1">Show Hashtags</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="filter2" v-model="filters.profiles">
|
||||
<label class="custom-control-label text-muted" for="filter2">Show Profiles</label>
|
||||
<label class="custom-control-label text-muted font-weight-light" for="filter2">Show Profiles</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="filter3" v-model="filters.statuses">
|
||||
<label class="custom-control-label text-muted" for="filter3">Show Statuses</label>
|
||||
<label class="custom-control-label text-muted font-weight-light" for="filter3">Show Statuses</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,11 +56,11 @@
|
|||
<p class="font-weight-bold text-truncate text-dark">
|
||||
{{profile.value}}
|
||||
</p>
|
||||
<!-- <p class="mb-0 text-center">
|
||||
<p class="mb-0 text-center">
|
||||
<button :class="[profile.entity.following ? 'btn btn-secondary btn-sm py-1 font-weight-bold' : 'btn btn-primary btn-sm py-1 font-weight-bold']" v-on:click="followProfile(profile.entity.id)">
|
||||
{{profile.entity.following ? 'Unfollow' : 'Follow'}}
|
||||
</button>
|
||||
</p> -->
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -124,27 +124,31 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchSearchResults() {
|
||||
axios.get('/api/search/' + encodeURI(this.query))
|
||||
.then(res => {
|
||||
let results = res.data;
|
||||
this.results.hashtags = results.hashtags;
|
||||
this.results.profiles = results.profiles;
|
||||
this.results.statuses = results.posts;
|
||||
this.loading = false;
|
||||
}).catch(err => {
|
||||
this.loading = false;
|
||||
// this.networkError = true;
|
||||
})
|
||||
axios.get('/api/search', {
|
||||
params: {
|
||||
'q': this.query,
|
||||
'src': 'metro',
|
||||
'v': 1
|
||||
}
|
||||
}).then(res => {
|
||||
let results = res.data;
|
||||
this.results.hashtags = results.hashtags;
|
||||
this.results.profiles = results.profiles;
|
||||
this.results.statuses = results.posts;
|
||||
this.loading = false;
|
||||
}).catch(err => {
|
||||
this.loading = false;
|
||||
// this.networkError = true;
|
||||
})
|
||||
},
|
||||
|
||||
followProfile(id) {
|
||||
// todo: finish AP Accept handling to enable remote follows
|
||||
return;
|
||||
// axios.post('/i/follow', {
|
||||
// item: id
|
||||
// }).then(res => {
|
||||
// window.location.href = window.location.href;
|
||||
// });
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
window.location.href = window.location.href;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</video>
|
||||
|
||||
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class" v-on:click="$emit('lightbox', media)">
|
||||
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description">
|
||||
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description" loading="lazy">
|
||||
</div>
|
||||
|
||||
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</video>
|
||||
|
||||
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class" v-on:click="$emit('lightbox', media)">
|
||||
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description">
|
||||
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description" loading="lazy">
|
||||
</div>
|
||||
|
||||
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
</summary>
|
||||
<b-carousel :id="status.id + '-carousel'"
|
||||
v-model="cursor"
|
||||
style="text-shadow: 1px 1px 2px #333;"
|
||||
style="text-shadow: 1px 1px 2px #333;min-height: 330px;display: flex;align-items: center;"
|
||||
controls
|
||||
background="#ffffff"
|
||||
:interval="0"
|
||||
>
|
||||
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id">
|
||||
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', img)">
|
||||
<img class="img-fluid" style="max-height: 600px;" :src="img.url" :alt="img.description" :title="img.description">
|
||||
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', status.media_attachments[index])">
|
||||
<img class="img-fluid" style="max-height: 600px;" :src="img.url" :alt="img.description" :title="img.description" loading="lazy" v-on:click="$emit('lightbox', status.media_attachments[index])">
|
||||
</div>
|
||||
</b-carousel-slide>
|
||||
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">
|
||||
|
@ -26,14 +26,14 @@
|
|||
<div v-else>
|
||||
<b-carousel :id="status.id + '-carousel'"
|
||||
v-model="cursor"
|
||||
style="text-shadow: 1px 1px 2px #333;"
|
||||
style="text-shadow: 1px 1px 2px #333;min-height: 330px;display: flex;align-items: center;"
|
||||
controls
|
||||
background="#ffffff"
|
||||
:interval="0"
|
||||
>
|
||||
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id" :alt="img.description" :title="img.description">
|
||||
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', img)">
|
||||
<img class="img-fluid" style="max-height: 600px;" :src="img.url">
|
||||
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', status.media_attachments[index])">
|
||||
<img class="img-fluid" style="max-height: 600px;" :src="img.url" loading="lazy">
|
||||
</div>
|
||||
</b-carousel-slide>
|
||||
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
<p class="font-weight-light">(click to show)</p>
|
||||
</summary>
|
||||
<div class="max-hide-overflow" v-on:click="$emit('lightbox', status.media_attachments[0])" :class="status.media_attachments[0].filter_class" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
|
||||
<img class="card-img-top" :src="status.media_attachments[0].url">
|
||||
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy">
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div :class="status.media_attachments[0].filter_class" v-on:click="$emit('lightbox', status.media_attachments[0])" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
|
||||
<img class="card-img-top" :src="status.media_attachments[0].url">
|
||||
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
Vue.component(
|
||||
'passport-clients',
|
||||
require('./components/passport/Clients.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'passport-authorized-clients',
|
||||
require('./components/passport/AuthorizedClients.vue').default
|
||||
);
|
||||
|
||||
Vue.component(
|
||||
'passport-personal-access-tokens',
|
||||
require('./components/passport/PersonalAccessTokens.vue').default
|
||||
);
|
|
@ -6,13 +6,8 @@
|
|||
<div class="card mt-3">
|
||||
<div class="card-body p-0">
|
||||
<ul class="nav nav-pills d-flex text-center">
|
||||
|
||||
{{-- <li class="nav-item flex-fill">
|
||||
<a class="nav-link font-weight-bold text-uppercase" href="#">Following</a>
|
||||
</li> --}}
|
||||
|
||||
<li class="nav-item flex-fill">
|
||||
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">My Notifications</a>
|
||||
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">Notifications</a>
|
||||
</li>
|
||||
<li class="nav-item flex-fill">
|
||||
<a class="nav-link font-weight-bold text-uppercase" href="{{route('follow-requests')}}">Follow Requests</a>
|
||||
|
@ -93,7 +88,7 @@
|
|||
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
|
||||
</span>
|
||||
<span class="float-right notification-action">
|
||||
@if($notification->item_id)
|
||||
@if(false == true && $notification->item_id && $notification->item_type == 'App\Status')
|
||||
<a href="{{$notification->status->parent()->url()}}">
|
||||
<div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div>
|
||||
</a>
|
||||
|
@ -142,5 +137,10 @@
|
|||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{mix('js/activity.js')}}"></script>
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<div class="alert alert-info">Cache information is read only, to make changes please edit the .env</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="app_name" class="col-sm-3 col-form-label font-weight-bold text-right">Driver</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" disabled>
|
||||
<option {{config('cache.default') == 'apc' ? 'selected=""':''}}>APC</option>
|
||||
<option {{config('cache.default') == 'array' ? 'selected=""':''}}>Array</option>
|
||||
<option {{config('cache.default') == 'database' ? 'selected=""':''}}>Database</option>
|
||||
<option {{config('cache.default') == 'file' ? 'selected=""':''}}>File</option>
|
||||
<option {{config('cache.default') == 'memcached' ? 'selected=""':''}}>Memcached</option>
|
||||
<option {{config('cache.default') == 'redis' ? 'selected=""':''}}>Redis</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="db_host" class="col-sm-3 col-form-label font-weight-bold text-right">Cache Prefix</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" disabled value="{{config('cache.prefix')}}">
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,37 @@
|
|||
<div class="alert alert-info">Database information is read only, to make changes please edit the .env</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="app_name" class="col-sm-3 col-form-label font-weight-bold text-right">Driver</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" disabled>
|
||||
<option {{config('database.default') == 'mysql' ? 'selected=""':''}}>MySQL</option>
|
||||
<option {{config('database.default') == 'pgsql' ? 'selected=""':''}}>Postgres</option>
|
||||
<option {{config('database.default') == 'sqlite' ? 'selected=""':''}}>SQLite</option>
|
||||
<option {{config('database.default') == 'sqlsrv' ? 'selected=""':''}}>MSSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="db_host" class="col-sm-3 col-form-label font-weight-bold text-right">Host</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="" name="db_host" disabled value="{{config('database.connections.mysql.host')}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="db_port" class="col-sm-3 col-form-label font-weight-bold text-right">Port</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="db_port" name="db_port" disabled value="{{config('database.connections.mysql.port')}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="db_database" class="col-sm-3 col-form-label font-weight-bold text-right">Database</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="db_database" name="db_database" disabled value="{{config('database.connections.mysql.database')}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="db_username" class="col-sm-3 col-form-label font-weight-bold text-right">Username</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="db_username" name="db_username" disabled value="{{config('database.connections.mysql.username')}}">
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
<div class="alert alert-info">Filesystems information is read only, to make changes please edit the .env</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="app_name" class="col-sm-3 col-form-label font-weight-bold text-right">Driver</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" disabled>
|
||||
<option {{config('filesystems.default') == 'local' ? 'selected=""':''}}>Local</option>
|
||||
<option {{config('filesystems.default') == 's3' ? 'selected=""':''}}>S3</option>
|
||||
<option {{config('filesystems.default') == 'spaces' ? 'selected=""':''}}>Digital Ocean Spaces</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,103 @@
|
|||
<form method="post">
|
||||
@csrf
|
||||
<div class="form-group row">
|
||||
<label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">Registration</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-check pb-2">
|
||||
<input class="form-check-input" type="checkbox" id="open_registration" name="open_registration" {{config('pixelfed.open_registration') === true ? 'checked=""' : '' }}>
|
||||
<label class="form-check-label font-weight-bold" for="open_registration">
|
||||
{{config('pixelfed.open_registration') === true ? 'Open' : 'Closed' }}
|
||||
</label>
|
||||
<p class="text-muted small help-text font-weight-bold">When this option is enabled, new user registration is open.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">Email Validation</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-check pb-2">
|
||||
<input class="form-check-input" type="checkbox" id="enforce_email_verification" name="enforce_email_verification" {{config('pixelfed.enforce_email_verification') === true ? 'checked=""' : '' }}>
|
||||
<label class="form-check-label font-weight-bold" for="open_registration">
|
||||
{{config('pixelfed.enforce_email_verification') == true ? 'Enabled' : 'Disabled' }}
|
||||
</label>
|
||||
<p class="text-muted small help-text font-weight-bold">Enforce email validation for new user registration.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">Recaptcha</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-check pb-2">
|
||||
<input class="form-check-input" type="checkbox" id="recaptcha" name="recaptcha" {{config('pixelfed.recaptcha') === true ? 'checked=""' : '' }}>
|
||||
<label class="form-check-label font-weight-bold" for="open_registration">
|
||||
{{config('pixelfed.recaptcha') == true ? 'Enabled' : 'Disabled' }}
|
||||
</label>
|
||||
<p class="text-muted small help-text font-weight-bold">When this option is enabled, new user registration is open.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">ActivityPub</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="form-check pb-2">
|
||||
<input class="form-check-input" type="checkbox" id="activitypub_enabled" name="activitypub_enabled" {{config('pixelfed.activitypub_enabled') === true ? 'checked=""' : '' }}>
|
||||
<label class="form-check-label font-weight-bold" for="activitypub_enabled">
|
||||
{{config('pixelfed.activitypub_enabled') === true ? 'Enabled' : 'Disabled' }}
|
||||
</label>
|
||||
<p class="text-muted small help-text font-weight-bold">Enable for federation support.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label font-weight-bold text-right">Account Size</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" placeholder="1000000" name="max_account_size" value="{{config('pixelfed.max_account_size')}}">
|
||||
<span class="help-text font-weight-bold text-muted small">
|
||||
Max account size for users, in KB.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label font-weight-bold text-right">Max Upload Size</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" placeholder="15000" name="max_photo_size" value="{{config('pixelfed.max_photo_size')}}">
|
||||
<span class="help-text font-weight-bold text-muted small">
|
||||
Max file size for uploads, in KB.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label font-weight-bold text-right">Caption Length</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" placeholder="500" name="caption_limit" value="{{config('pixelfed.max_caption_length')}}">
|
||||
<span class="help-text font-weight-bold text-muted small">
|
||||
Character limit for captions and comments.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label font-weight-bold text-right">Max Album Size</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" placeholder="3" name="album_limit" value="{{config('pixelfed.max_album_length')}}">
|
||||
<span class="help-text font-weight-bold text-muted small">
|
||||
Limit # of media per post.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label font-weight-bold text-right">Image Quality</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" placeholder="80" name="image_quality" value="{{config('pixelfed.image_quality')}}">
|
||||
<span class="help-text font-weight-bold text-muted small">
|
||||
Image quality. Must be a value between 1 (worst) - 100 (best).
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row mb-0">
|
||||
<div class="col-12 text-right">
|
||||
<button type="submit" class="btn btn-primary font-weight-bold">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -45,8 +45,8 @@
|
|||
</li>
|
||||
|
||||
{{-- <li class="pr-2">
|
||||
<a class="nav-link font-weight-bold" href="/" title="Home">
|
||||
{{ __('Network') }}
|
||||
<a class="nav-link font-weight-bold {{request()->is('timeline/network') ?'text-primary':''}}" href="{{route('timeline.network')}}" title="Network Timeline">
|
||||
<i class="fas fa-globe fa-lg"></i>
|
||||
</a>
|
||||
</li> --}}
|
||||
<div class="d-none d-md-block">
|
||||
|
@ -87,7 +87,11 @@
|
|||
<span class="far fa-map pr-1"></span>
|
||||
{{__('navmenu.publicTimeline')}}
|
||||
</a>
|
||||
|
||||
{{-- <a class="dropdown-item font-weight-bold" href="{{route('timeline.network')}}">
|
||||
<span class="fas fa-globe pr-1"></span>
|
||||
Network Timeline
|
||||
</a> --}}
|
||||
<div class="d-block d-md-none dropdown-divider"></div>
|
||||
<a class="d-block d-md-none dropdown-item font-weight-bold" href="{{route('discover')}}">
|
||||
<span class="far fa-compass pr-1"></span>
|
||||
{{__('navmenu.discover')}}
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<profile profile-id="{{$profile->id}}" :profile-settings="{{json_encode($settings)}}"></profile>
|
||||
|
||||
<profile profile-id="{{$profile->id}}" :profile-settings="{{json_encode($settings)}}" profile-layout="{{$profile->profile_layout ?? 'metro'}}"></profile>
|
||||
@if($profile->website)
|
||||
<a class="d-none" href="{{$profile->website}}" rel="me">{{$profile->website}}</a>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@push('meta')<meta property="og:description" content="{{$profile->bio}}">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Abusive/Harmful Comment
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Abusive/Harmful Post
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Abusive/Harmful Profile
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
I'm not interested in this content
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="p-5 text-center">
|
||||
<p class="lead">You can <b class="font-weight-bold">unfollow</b> or <b class="font-weight-bold">mute</b> a user or hashtag from appearing in your timeline. Unless the content violates our terms of service, there is nothing we can do to remove it.</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
{{-- <div class="col-12 col-md-8 offset-md-2">
|
||||
<p><a class="font-weight-bold" href="#">
|
||||
Learn more
|
||||
</a> about our reporting guidelines and policy.</p>
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Sensitive Comment
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Sensitive Post
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Sensitive Profile
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Comment Spam
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Post Spam
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container mt-4 mb-5 pb-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0 col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header lead font-weight-bold">
|
||||
<div class="card-header lead font-weight-bold bg-white">
|
||||
Report Profile Spam
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
|
|
|
@ -98,6 +98,22 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-5">
|
||||
<p class="font-weight-bold text-muted text-center">Layout</p>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Profile Layout</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="profileLayout1" name="profile_layout" class="custom-control-input" {{Auth::user()->profile->profile_layout != 'moment' ? 'checked':''}} value="metro">
|
||||
<label class="custom-control-label" for="profileLayout1">Metro</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="profileLayout2" name="profile_layout" class="custom-control-input" {{Auth::user()->profile->profile_layout == 'moment' ? 'checked':''}} value="moment">
|
||||
<label class="custom-control-label" for="profileLayout2">Moment</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col-12 d-flex align-items-center justify-content-between">
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
@include('settings.security.2fa.partial.log-panel')
|
||||
@include('settings.security.log-panel')
|
||||
|
||||
@include('settings.security.device-panel')
|
||||
</section>
|
||||
|
||||
@endsection
|
|
@ -0,0 +1,47 @@
|
|||
<div class="mb-4 pb-4">
|
||||
<h4 class="font-weight-bold">Devices</h4>
|
||||
<hr>
|
||||
<ul class="list-group">
|
||||
@foreach($devices as $device)
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center p-3">
|
||||
<div>
|
||||
@if($device->getUserAgent()->isMobile())
|
||||
<i class="fas fa-mobile fa-5x text-muted"></i>
|
||||
@else
|
||||
<i class="fas fa-desktop fa-5x text-muted"></i>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 font-weight-bold">
|
||||
<span class="text-muted">IP:</span>
|
||||
<span class="text-truncate">{{$device->ip}}</span>
|
||||
</p>
|
||||
<p class="mb-0 font-weight-bold">
|
||||
<span class="text-muted">Device:</span>
|
||||
<span>{{$device->getUserAgent()->device()}}</span>
|
||||
</p>
|
||||
<p class="mb-0 font-weight-bold">
|
||||
<span class="text-muted">Browser:</span>
|
||||
<span>{{$device->getUserAgent()->browser()}}</span>
|
||||
</p>
|
||||
{{-- <p class="mb-0 font-weight-bold">
|
||||
<span class="text-muted">Country:</span>
|
||||
<span>Canada</span>
|
||||
</p> --}}
|
||||
<p class="mb-0 font-weight-bold">
|
||||
<span class="text-muted">Last Login:</span>
|
||||
<span>{{$device->updated_at->diffForHumans()}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group">
|
||||
{{-- <a class="btn btn-success font-weight-bold py-0 btn-sm" href="#">Trust</a>
|
||||
<a class="btn btn-outline-secondary font-weight-bold py-0 btn-sm" href="#">Remove Device</a> --}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
|
@ -1,12 +1,12 @@
|
|||
<div class="mb-4 pb-4">
|
||||
<h4 class="font-weight-bold">Account Log</h4>
|
||||
<hr>
|
||||
<ul class="list-group" style="max-height: 400px;overflow-y: scroll;">
|
||||
<ul class="list-group border" style="max-height: 400px;overflow-y: auto;">
|
||||
@if($activity->count() == 0)
|
||||
<p class="alert alert-info font-weight-bold">No activity logs found!</p>
|
||||
@endif
|
||||
@foreach($activity as $log)
|
||||
<li class="list-group-item">
|
||||
<li class="list-group-item rounded-0 border-0">
|
||||
<div class="media">
|
||||
<div class="media-body">
|
||||
<span class="my-0 font-weight-bold text-muted">
|
|
@ -31,4 +31,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
});
|
||||
</script>
|
||||
@endpush
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<div class="col-12">
|
||||
<div class="card mt-5">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0">
|
||||
<div class="card mt-md-5 px-0 mx-md-3">
|
||||
<div class="card-header font-weight-bold text-muted bg-white py-4">
|
||||
<a href="{{route('site.help')}}" class="text-muted">{{__('helpcenter.helpcenter')}}</a>
|
||||
<span class="px-2 font-weight-light">—</span>
|
||||
{{ $breadcrumb ?? ''}}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="row">
|
||||
<div class="row px-0">
|
||||
@include('site.help.partial.sidebar')
|
||||
<div class="col-12 col-md-9 p-5">
|
||||
@if (session('status'))
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<div class="volume"></div>
|
||||
<div class="camera"></div>
|
||||
<div class="screen">
|
||||
<img src="/img/landing/android_1.jpg" class="img-fluid">
|
||||
<img src="/img/landing/android_1.jpg" class="img-fluid" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
<div class="marvel-device iphone-x" style="position: absolute;z-index: 20;margin: 99px 0 0 151px;">
|
||||
|
@ -63,10 +63,10 @@
|
|||
<div class="inner-shadow"></div>
|
||||
<div class="screen">
|
||||
<div id="iosDevice">
|
||||
<img v-if="!loading" src="/img/landing/ios_4.jpg" class="img-fluid">
|
||||
<img v-if="!loading" src="/img/landing/ios_3.jpg" class="img-fluid">
|
||||
<img v-if="!loading" src="/img/landing/ios_2.jpg" class="img-fluid">
|
||||
<img src="/img/landing/ios_1.jpg" class="img-fluid">
|
||||
<img src="/img/landing/ios_4.jpg" class="img-fluid" loading="lazy">
|
||||
<img src="/img/landing/ios_3.jpg" class="img-fluid" loading="lazy">
|
||||
<img src="/img/landing/ios_2.jpg" class="img-fluid" loading="lazy">
|
||||
<img src="/img/landing/ios_1.jpg" class="img-fluid" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<div class="col-12">
|
||||
<div class="card mt-5">
|
||||
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
|
||||
<div class="col-12 px-0">
|
||||
<div class="card mt-md-5">
|
||||
<div class="card-body p-0">
|
||||
<div class="row">
|
||||
<div class="row px-0">
|
||||
@include('site.partial.sidebar')
|
||||
<div class="col-12 col-md-9 p-5">
|
||||
@if (session('status'))
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</noscript>
|
||||
<div class="mt-md-4"></div>
|
||||
<post-component status-template="{{$status->viewType()}}" status-id="{{$status->id}}" status-username="{{$status->profile->username}}" status-url="{{$status->url()}}" status-profile-url="{{$status->profile->url()}}" status-avatar="{{$status->profile->avatarUrl()}}"></post-component>
|
||||
<post-component status-template="{{$status->viewType()}}" status-id="{{$status->id}}" status-username="{{$status->profile->username}}" status-url="{{$status->url()}}" status-profile-url="{{$status->profile->url()}}" status-avatar="{{$status->profile->avatarUrl()}}" status-profile-id="{{$status->profile_id}}" profile-layout="{{$status->profile->profile_layout ?? 'metro'}}"></post-component>
|
||||
|
||||
|
||||
@endsection
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
|
|
|
@ -34,9 +34,15 @@
|
|||
<label class="font-weight-bold text-muted small">Visibility</label>
|
||||
<div class="switch switch-sm">
|
||||
<select class="form-control" name="visibility">
|
||||
<option value="public" selected="">Public</option>
|
||||
<option value="unlisted">Unlisted (hidden from public timelines)</option>
|
||||
<option value="private">Followers Only</option>
|
||||
@if(Auth::user()->profile->is_private)
|
||||
<option value="public">Public</option>
|
||||
<option value="unlisted">Unlisted (hidden from public timelines)</option>
|
||||
<option value="private" selected="">Followers Only</option>
|
||||
@else
|
||||
<option value="public" selected="">Public</option>
|
||||
<option value="unlisted">Unlisted (hidden from public timelines)</option>
|
||||
<option value="private">Followers Only</option>
|
||||
@endif
|
||||
</select>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>{{ config('app.name') }} - Authorization</title>
|
||||
|
||||
<!-- Styles -->
|
||||
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
.passport-authorize .container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.passport-authorize .scopes {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.passport-authorize .buttons {
|
||||
margin-top: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.passport-authorize .btn {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
.passport-authorize .btn-approve {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.passport-authorize form {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="passport-authorize">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="text-center mb-5">
|
||||
<img src="/img/pixelfed-icon-grey.svg">
|
||||
</div>
|
||||
<div class="card card-default">
|
||||
<div class="card-header text-center font-weight-bold bg-white">
|
||||
Authorization Request
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Introduction -->
|
||||
<p><strong>{{ $client->name }}</strong> is requesting permission to access your account.</p>
|
||||
|
||||
<!-- Scope List -->
|
||||
@if (count($scopes) > 0)
|
||||
<div class="scopes">
|
||||
<p><strong>This application will be able to:</strong></p>
|
||||
|
||||
<ul>
|
||||
@foreach ($scopes as $scope)
|
||||
<li><b class="pr-3">{{$scope->id}}</b> {{ $scope->description }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="buttons">
|
||||
<!-- Authorize Button -->
|
||||
<form method="post" action="{{ route('passport.authorizations.approve') }}">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<input type="hidden" name="state" value="{{ $request->state }}">
|
||||
<input type="hidden" name="client_id" value="{{ $client->id }}">
|
||||
<button type="submit" class="btn btn-success font-weight-bold btn-approve">Authorize</button>
|
||||
</form>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<form method="post" action="{{ route('passport.authorizations.deny') }}">
|
||||
{{ csrf_field() }}
|
||||
{{ method_field('DELETE') }}
|
||||
|
||||
<input type="hidden" name="state" value="{{ $request->state }}">
|
||||
<input type="hidden" name="client_id" value="{{ $client->id }}">
|
||||
<button class="btn btn-outline-danger font-weight-bold">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -64,9 +64,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('discover', 'DiscoverController@home')->name('discover');
|
||||
|
||||
Route::group(['prefix' => 'api'], function () {
|
||||
Route::get('search/{tag}', 'SearchController@searchAPI')
|
||||
//->where('tag', '.*');
|
||||
->where('tag', '[A-Za-z0-9]+');
|
||||
Route::get('search', 'SearchController@searchAPI');
|
||||
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
|
||||
|
||||
Route::group(['prefix' => 'v1'], function () {
|
||||
|
@ -83,6 +81,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('notifications', 'ApiController@notifications');
|
||||
Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
|
||||
Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
|
||||
// Route::get('timelines/network', 'PublicApiController@homeTimelineApi');
|
||||
});
|
||||
Route::group(['prefix' => 'v2'], function() {
|
||||
Route::get('config', 'ApiController@siteConfiguration');
|
||||
|
@ -111,7 +110,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::post('comment', 'CommentController@store');
|
||||
Route::post('delete', 'StatusController@delete');
|
||||
Route::post('mute', 'AccountController@mute');
|
||||
Route::post('unmute', 'AccountController@unmute');
|
||||
Route::post('block', 'AccountController@block');
|
||||
Route::post('unblock', 'AccountController@unblock');
|
||||
Route::post('like', 'LikeController@store');
|
||||
Route::post('share', 'StatusController@storeShare');
|
||||
Route::post('follow', 'FollowerController@store');
|
||||
|
@ -266,6 +267,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::redirect('/', '/');
|
||||
Route::get('public', 'TimelineController@local')->name('timeline.public');
|
||||
Route::post('public', 'StatusController@store');
|
||||
// Route::get('network', 'TimelineController@network')->name('timeline.network');
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'users'], function () {
|
||||
|
|
|
@ -22,12 +22,6 @@ class NoteAttachmentTest extends TestCase
|
|||
$this->invalidMime = json_decode('{"id":"https://mastodon.social/users/dansup/statuses/100889802384218791/activity","type":"Create","actor":"https://mastodon.social/users/dansup","published":"2018-10-13T18:43:33Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mastodon.social/users/dansup/followers"],"object":{"id":"https://mastodon.social/users/dansup/statuses/100889802384218791","type":"Note","summary":null,"inReplyTo":null,"published":"2018-10-13T18:43:33Z","url":"https://mastodon.social/@dansup/100889802384218791","attributedTo":"https://mastodon.social/users/dansup","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mastodon.social/users/dansup/followers"],"sensitive":false,"atomUri":"https://mastodon.social/users/dansup/statuses/100889802384218791","inReplyToAtomUri":null,"conversation":"tag:mastodon.social,2018-10-13:objectId=59103420:objectType=Conversation","content":"<p>Good Morning! <a href=\"https://mastodon.social/tags/coffee\" class=\"mention hashtag\" rel=\"tag\">#<span>coffee</span></a></p>","contentMap":{"en":"<p>Good Morning! <a href=\"https://mastodon.social/tags/coffee\" class=\"mention hashtag\" rel=\"tag\">#<span>coffee</span></a></p>"},"attachment":[{"type":"Document","mediaType":"image/webp","url":"https://files.mastodon.social/media_attachments/files/007/110/573/original/96a196885a77c9a4.jpg","name":null}],"tag":[{"type":"Hashtag","href":"https://mastodon.social/tags/coffee","name":"#coffee"}]}}', true, 9);
|
||||
}
|
||||
|
||||
public function testPleroma()
|
||||
{
|
||||
$valid = Helpers::verifyAttachments($this->pleroma);
|
||||
$this->assertTrue($valid);
|
||||
}
|
||||
|
||||
public function testMastodon()
|
||||
{
|
||||
$valid = Helpers::verifyAttachments($this->mastodon);
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Purify;
|
||||
use Tests\TestCase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
class PurifierTest extends TestCase
|
||||
{
|
||||
/** @test */
|
||||
public function puckTest()
|
||||
{
|
||||
$actual = Purify::clean("<span class=\"fa-spin fa\">catgirl spinning around in the interblag</span>");
|
||||
$expected = 'catgirl spinning around in the interblag';
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$actual = Purify::clean("<p class=\"fa-spin fa\">catgirl spinning around in the interblag</p>");
|
||||
$expected = '<p>catgirl spinning around in the interblag</p>';
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
}
|
|
@ -34,6 +34,9 @@ mix.js('resources/assets/js/app.js', 'public/js')
|
|||
// SearchResults component
|
||||
.js('resources/assets/js/search.js', 'public/js')
|
||||
|
||||
// Developer Components
|
||||
.js('resources/assets/js/developers.js', 'public/js')
|
||||
|
||||
.sass('resources/assets/sass/app.scss', 'public/css', {
|
||||
implementation: require('node-sass')
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue