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

v0.9.0
This commit is contained in:
daniel 2019-04-17 23:59:24 -06:00 committed by GitHub
commit b8ad9fe5e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2217 additions and 765 deletions

View file

@ -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!');
}
}

51
app/Events/NewMention.php Normal file
View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -128,7 +128,7 @@ class AccountController extends Controller
} }
} }
public function fetchNotifications($id) public function fetchNotifications(int $id)
{ {
$key = config('cache.prefix').":user.{$id}.notifications"; $key = config('cache.prefix').":user.{$id}.notifications";
$redis = Redis::connection(); $redis = Redis::connection();
@ -167,14 +167,14 @@ class AccountController extends Controller
public function mute(Request $request) public function mute(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'type' => 'required|string', 'type' => 'required|alpha_dash',
'item' => 'required|integer|min:1', 'item' => 'required|integer|min:1',
]); ]);
$user = Auth::user()->profile; $user = Auth::user()->profile;
$type = $request->input('type'); $type = $request->input('type');
$item = $request->input('item'); $item = $request->input('item');
$action = "{$type}.mute"; $action = $type . '.mute';
if (!in_array($action, $this->filters)) { if (!in_array($action, $this->filters)) {
return abort(406); return abort(406);
@ -211,17 +211,71 @@ class AccountController extends Controller
return redirect()->back(); return redirect()->back();
} }
public function block(Request $request) public function unmute(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'type' => 'required|string', 'type' => 'required|alpha_dash',
'item' => 'required|integer|min:1', 'item' => 'required|integer|min:1',
]); ]);
$user = Auth::user()->profile; $user = Auth::user()->profile;
$type = $request->input('type'); $type = $request->input('type');
$item = $request->input('item'); $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)) { if (!in_array($action, $this->filters)) {
return abort(406); return abort(406);
} }
@ -259,6 +313,56 @@ class AccountController extends Controller
return redirect()->back(); 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) public function followRequests(Request $request)
{ {
$pid = Auth::user()->profile->id; $pid = Auth::user()->profile->id;

View file

@ -31,6 +31,10 @@ class ApiController extends BaseApiController
'media_types' => config('pixelfed.media_types'), 'media_types' => config('pixelfed.media_types'),
'enforce_account_limit' => config('pixelfed.enforce_account_limit') 'enforce_account_limit' => config('pixelfed.enforce_account_limit')
],
'activitypub' => [
'enabled' => config('pixelfed.activitypub_enabled'),
'remote_follow' => config('pixelfed.remote_follow_enabled')
] ]
]; ];
}); });

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Auth; use Auth;
use DB;
use Cache; use Cache;
use App\Comment; use App\Comment;
@ -58,14 +59,21 @@ class CommentController extends Controller
Cache::forget('transform:status:'.$status->url()); Cache::forget('transform:status:'.$status->url());
$autolink = Autolink::create()->autolink($comment); $reply = DB::transaction(function() use($comment, $status, $profile) {
$reply = new Status(); $autolink = Autolink::create()->autolink($comment);
$reply->profile_id = $profile->id; $reply = new Status();
$reply->caption = e($comment); $reply->profile_id = $profile->id;
$reply->rendered = $autolink; $reply->caption = e($comment);
$reply->in_reply_to_id = $status->id; $reply->rendered = $autolink;
$reply->in_reply_to_profile_id = $status->profile_id; $reply->in_reply_to_id = $status->id;
$reply->save(); $reply->in_reply_to_profile_id = $status->profile_id;
$reply->save();
$status->reply_count++;
$status->save();
return $reply;
});
NewStatusPipeline::dispatch($reply, false); NewStatusPipeline::dispatch($reply, false);
CommentPipeline::dispatch($status, $reply); CommentPipeline::dispatch($status, $reply);

View file

@ -82,9 +82,10 @@ trait Instagram
->whereStage(1) ->whereStage(1)
->firstOrFail(); ->firstOrFail();
$limit = config('pixelfed.import.instagram.limits.posts');
foreach ($media as $k => $v) { foreach ($media as $k => $v) {
$original = $v->getClientOriginalName(); $original = $v->getClientOriginalName();
if(strlen($original) < 32 || $k > 100) { if(strlen($original) < 32 || $k > $limit) {
continue; continue;
} }
$storagePath = "import/{$job->uuid}"; $storagePath = "import/{$job->uuid}";
@ -105,7 +106,6 @@ trait Instagram
$job->save(); $job->save();
}); });
return redirect($job->url()); return redirect($job->url());
return view('settings.import.instagram.step-one', compact('profile', 'job'));
} }
public function instagramStepTwo(Request $request, $uuid) public function instagramStepTwo(Request $request, $uuid)
@ -148,6 +148,7 @@ trait Instagram
{ {
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id) $job = ImportJob::whereProfileId($profile->id)
->whereService('instagram')
->whereNull('completed_at') ->whereNull('completed_at')
->whereUuid($uuid) ->whereUuid($uuid)
->whereStage(3) ->whereStage(3)
@ -159,14 +160,21 @@ trait Instagram
{ {
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id)
try {
$import = ImportJob::whereProfileId($profile->id)
->where('uuid', $uuid)
->whereNotNull('media_json')
->whereNull('completed_at') ->whereNull('completed_at')
->whereUuid($uuid)
->whereStage(3) ->whereStage(3)
->firstOrFail(); ->firstOrFail();
ImportInstagram::dispatch($import);
} catch (Exception $e) {
\Log::info($e);
}
ImportInstagram::dispatchNow($job); return redirect(route('settings'))->with(['status' => [
'Import successful! It may take a few minutes to finish.'
return redirect($profile->url()); ]]);
} }
} }

View file

@ -108,6 +108,7 @@ class PublicApiController extends Controller
'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
'limit' => 'nullable|integer|min:5|max:50' 'limit' => 'nullable|integer|min:5|max:50'
]); ]);
$limit = $request->limit ?? 10; $limit = $request->limit ?? 10;
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail(); $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
$status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId); $status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId);
@ -116,7 +117,7 @@ class PublicApiController extends Controller
if($request->filled('min_id')) { if($request->filled('min_id')) {
$replies = $status->comments() $replies = $status->comments()
->whereNull('reblog_of_id') ->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) ->where('id', '>=', $request->min_id)
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->paginate($limit); ->paginate($limit);
@ -124,7 +125,7 @@ class PublicApiController extends Controller
if($request->filled('max_id')) { if($request->filled('max_id')) {
$replies = $status->comments() $replies = $status->comments()
->whereNull('reblog_of_id') ->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) ->where('id', '<=', $request->max_id)
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->paginate($limit); ->paginate($limit);
@ -132,7 +133,7 @@ class PublicApiController extends Controller
} else { } else {
$replies = $status->comments() $replies = $status->comments()
->whereNull('reblog_of_id') ->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') ->orderBy('id', 'desc')
->paginate($limit); ->paginate($limit);
} }
@ -180,8 +181,8 @@ class PublicApiController extends Controller
if(!$user) { if(!$user) {
abort(403); abort(403);
} else { } else {
$follows = $profile->followedBy(Auth::user()->profile); $follows = $profile->followedBy($user->profile);
if($follows == false && $profile->id !== $user->profile->id) { if($follows == false && $profile->id !== $user->profile->id && $user->is_admin == false) {
abort(404); abort(404);
} }
} }
@ -357,8 +358,6 @@ class PublicApiController extends Controller
'created_at', 'created_at',
'updated_at' 'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereLocal(true)
->whereNull('uri')
->where('id', $dir, $id) ->where('id', $dir, $id)
->whereIn('profile_id', $following) ->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
@ -386,8 +385,6 @@ class PublicApiController extends Controller
'created_at', 'created_at',
'updated_at' 'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereLocal(true)
->whereNull('uri')
->whereIn('profile_id', $following) ->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id') ->whereNull('in_reply_to_id')
@ -453,14 +450,18 @@ class PublicApiController extends Controller
'is_nsfw', 'is_nsfw',
'scope', 'scope',
'local', 'local',
'reply_count',
'comments_disabled',
'created_at', 'created_at',
'updated_at' 'updated_at'
)->where('id', $dir, $id) )->where('id', $dir, $id)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
->whereNotNull('uri')
->whereNull('in_reply_to_id') ->whereNull('in_reply_to_id')
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereVisibility('public') ->whereVisibility('public')
->orderBy('created_at', 'desc') ->latest()
->limit($limit) ->limit($limit)
->get(); ->get();
} else { } else {
@ -476,14 +477,17 @@ class PublicApiController extends Controller
'is_nsfw', 'is_nsfw',
'scope', 'scope',
'local', 'local',
'reply_count',
'comments_disabled',
'created_at', 'created_at',
'updated_at' 'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id') ->whereNull('in_reply_to_id')
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereNotNull('uri')
->whereVisibility('public') ->whereVisibility('public')
->orderBy('created_at', 'desc') ->latest()
->simplePaginate($limit); ->simplePaginate($limit);
} }
@ -524,8 +528,8 @@ class PublicApiController extends Controller
{ {
abort_unless(Auth::check(), 403); abort_unless(Auth::check(), 403);
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id); $profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
if($profile->is_private || !$profile->user->settings->show_profile_followers) { if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
return []; return response()->json([]);
} }
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10); $followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer()); $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
@ -538,8 +542,8 @@ class PublicApiController extends Controller
{ {
abort_unless(Auth::check(), 403); abort_unless(Auth::check(), 403);
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id); $profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
if($profile->is_private || !$profile->user->settings->show_profile_following) { if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_following) {
return []; return response()->json([]);
} }
$following = $profile->following()->orderByDesc('followers.created_at')->paginate(10); $following = $profile->following()->orderByDesc('followers.created_at')->paginate(10);
$resource = new Fractal\Resource\Collection($following, new AccountTransformer()); $resource = new Fractal\Resource\Collection($following, new AccountTransformer());

View file

@ -9,6 +9,7 @@ use App\Status;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use App\Transformer\Api\{ use App\Transformer\Api\{
AccountTransformer, AccountTransformer,
HashtagTransformer, HashtagTransformer,
@ -22,17 +23,20 @@ class SearchController extends Controller
$this->middleware('auth'); $this->middleware('auth');
} }
public function searchAPI(Request $request, $tag) public function searchAPI(Request $request)
{ {
if(mb_strlen($tag) < 3) { $this->validate($request, [
return; '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)); $tag = e(urldecode($tag));
$hash = hash('sha256', $tag); $hash = hash('sha256', $tag);
$tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) { $tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) {
$tokens = []; $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); $remote = Helpers::fetchFromUrl($tag);
if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) { if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) {
$type = $remote['type']; $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) { if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) { $tags = $hashtags->map(function ($item, $key) {
return [ return [
@ -83,9 +92,9 @@ class SearchController extends Controller
}); });
$users = Profile::select('username', 'name', 'id') $users = Profile::select('username', 'name', 'id')
->whereNull('status') ->whereNull('status')
->whereNull('domain')
->where('id', '!=', Auth::user()->profile->id) ->where('id', '!=', Auth::user()->profile->id)
->where('username', 'like', '%'.$tag.'%') ->where('username', 'like', '%'.$tag.'%')
->whereNull('domain')
//->orWhere('remote_url', $tag) //->orWhere('remote_url', $tag)
->limit(20) ->limit(20)
->get(); ->get();
@ -120,7 +129,6 @@ class SearchController extends Controller
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile->id) ->whereProfileId(Auth::user()->profile->id)
->where('caption', 'like', '%'.$tag.'%') ->where('caption', 'like', '%'.$tag.'%')
->orWhere('uri', $tag)
->latest() ->latest()
->limit(10) ->limit(10)
->get(); ->get();

View file

@ -47,6 +47,10 @@ trait HomeSettings
$email = $request->input('email'); $email = $request->input('email');
$user = Auth::user(); $user = Auth::user();
$profile = $user->profile; $profile = $user->profile;
$layout = $request->input('profile_layout');
if($layout) {
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
}
$validate = config('pixelfed.enforce_email_verification'); $validate = config('pixelfed.enforce_email_verification');
@ -89,6 +93,11 @@ trait HomeSettings
$changes = true; $changes = true;
$profile->bio = $bio; $profile->bio = $bio;
} }
if ($profile->profile_layout != $layout) {
$changes = true;
$profile->profile_layout = $layout;
}
} }
if ($changes === true) { if ($changes === true) {

View file

@ -8,6 +8,7 @@ use App\Media;
use App\Profile; use App\Profile;
use App\User; use App\User;
use App\UserFilter; use App\UserFilter;
use App\UserDevice;
use App\Util\Lexer\PrettyNumber; use App\Util\Lexer\PrettyNumber;
use Auth; use Auth;
use DB; use DB;
@ -20,19 +21,19 @@ trait SecuritySettings
public function security() public function security()
{ {
$sessions = DB::table('sessions') $user = Auth::user();
->whereUserId(Auth::id())
->limit(20)
->get();
$activity = AccountLog::whereUserId(Auth::id()) $activity = AccountLog::whereUserId($user->id)
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->limit(20) ->limit(20)
->get(); ->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) public function securityTwoFactorSetup(Request $request)

View file

@ -42,11 +42,11 @@ class StatusController extends Controller
if($status->visibility == 'private' || $user->is_private) { if($status->visibility == 'private' || $user->is_private) {
if(!Auth::check()) { if(!Auth::check()) {
abort(403); abort(404);
} }
$pid = Auth::user()->profile; $pid = Auth::user()->profile;
if($user->followedBy($pid) == false && $user->id !== $pid->id) { if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
abort(403); abort(404);
} }
} }

View file

@ -25,8 +25,21 @@ class AuthServiceProvider extends ServiceProvider
{ {
$this->registerPolicies(); $this->registerPolicies();
// Passport::routes(); if(config('pixelfed.oauth_enabled')) {
// Passport::tokensExpireIn(now()->addDays(15)); Passport::routes();
// Passport::refreshTokensExpireIn(now()->addDays(30)); Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::enableImplicitGrant();
Passport::setDefaultScope([
'user:read',
'user:write'
]);
Passport::tokensCan([
'user:read' => 'Read a users profile info and media',
'user:write' => 'This scope lets an app "Change your profile information"',
]);
}
} }
} }

View file

@ -35,6 +35,11 @@ class CreateNote extends Fractal\TransformerAbstract
'Hashtag' => 'as:Hashtag', 'Hashtag' => 'as:Hashtag',
'sensitive' => 'as:sensitive', 'sensitive' => 'as:sensitive',
'commentsEnabled' => 'sc:Boolean', 'commentsEnabled' => 'sc:Boolean',
'capabilities' => [
'announce' => ['@type' => '@id'],
'like' => ['@type' => '@id'],
'reply' => ['@type' => '@id']
]
] ]
], ],
'id' => $status->permalink(), 'id' => $status->permalink(),
@ -65,6 +70,11 @@ class CreateNote extends Fractal\TransformerAbstract
})->toArray(), })->toArray(),
'tag' => $tags, 'tag' => $tags,
'commentsEnabled' => (bool) !$status->comments_disabled, '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'
]
] ]
]; ];
} }

View file

@ -35,6 +35,11 @@ class Note extends Fractal\TransformerAbstract
'Hashtag' => 'as:Hashtag', 'Hashtag' => 'as:Hashtag',
'sensitive' => 'as:sensitive', 'sensitive' => 'as:sensitive',
'commentsEnabled' => 'sc:Boolean', 'commentsEnabled' => 'sc:Boolean',
'capabilities' => [
'announce' => ['@type' => '@id'],
'like' => ['@type' => '@id'],
'reply' => ['@type' => '@id'],
]
] ]
], ],
'id' => $status->url(), 'id' => $status->url(),
@ -58,6 +63,11 @@ class Note extends Fractal\TransformerAbstract
})->toArray(), })->toArray(),
'tag' => $tags, 'tag' => $tags,
'commentsEnabled' => (bool) !$status->comments_disabled, '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'
]
]; ];
} }
} }

View file

@ -15,8 +15,8 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
'id' => (string) $profile->id, 'id' => (string) $profile->id,
'following' => $user->follows($profile), 'following' => $user->follows($profile),
'followed_by' => $user->followedBy($profile), 'followed_by' => $user->followedBy($profile),
'blocking' => null, 'blocking' => $user->blockedIds()->contains($profile->id),
'muting' => null, 'muting' => $user->mutedIds()->contains($profile->id),
'muting_notifications' => null, 'muting_notifications' => null,
'requested' => null, 'requested' => null,
'domain_blocking' => null, 'domain_blocking' => null,

View file

@ -23,7 +23,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'url' => $status->url(), 'url' => $status->url(),
'in_reply_to_id' => $status->in_reply_to_id, 'in_reply_to_id' => $status->in_reply_to_id,
'in_reply_to_account_id' => $status->in_reply_to_profile_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, 'content' => $status->rendered ?? $status->caption,
'created_at' => $status->created_at->format('c'), 'created_at' => $status->created_at->format('c'),
'emojis' => [], 'emojis' => [],
@ -42,9 +42,11 @@ class StatusTransformer extends Fractal\TransformerAbstract
'language' => null, 'language' => null,
'pinned' => null, 'pinned' => null,
'pf_type' => $status->type ?? $status->setType(), 'pf_type' => $status->type ?? $status->setType(),
'reply_count' => $status->reply_count, 'reply_count' => (int) $status->reply_count,
'comments_disabled' => $status->comments_disabled ? true : false 'comments_disabled' => $status->comments_disabled ? true : false,
'thread' => false,
'replies' => []
]; ];
} }

View file

@ -3,6 +3,7 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Jenssegers\Agent\Agent;
class UserDevice extends Model class UserDevice extends Model
{ {
@ -20,4 +21,14 @@ class UserDevice extends Model
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
public function getUserAgent()
{
if(!$this->user_agent) {
return 'Unknown';
}
$agent = new Agent();
$agent->setUserAgent($this->user_agent);
return $agent;
}
} }

View file

@ -21,8 +21,6 @@ use App\Jobs\AvatarPipeline\CreateAvatar;
use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent; use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail}; use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
use App\Jobs\StatusPipeline\NewStatusPipeline; 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 App\Util\ActivityPub\HttpSignature;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -30,7 +28,7 @@ class Helpers {
public static function validateObject($data) 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, [ $valid = Validator::make($data, [
'type' => [ 'type' => [
@ -38,11 +36,11 @@ class Helpers {
Rule::in($verbs) Rule::in($verbs)
], ],
'id' => 'required|string', 'id' => 'required|string',
'actor' => 'required|string', 'actor' => 'required|string|url',
'object' => 'required', 'object' => 'required',
'object.type' => 'required_if:type,Create', 'object.type' => 'required_if:type,Create',
'object.attachment' => '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' 'published' => 'required_if:type,Create|date'
])->passes(); ])->passes();
@ -71,7 +69,7 @@ class Helpers {
'string', 'string',
Rule::in($mediaTypes) Rule::in($mediaTypes)
], ],
'*.url' => 'required|max:255', '*.url' => 'required|url|max:255',
'*.mediaType' => [ '*.mediaType' => [
'required', 'required',
'string', 'string',
@ -193,6 +191,7 @@ class Helpers {
$res = Zttp::withHeaders(self::zttpUserAgent())->get($url); $res = Zttp::withHeaders(self::zttpUserAgent())->get($url);
$res = json_decode($res->body(), true, 8); $res = json_decode($res->body(), true, 8);
if(json_last_error() == JSON_ERROR_NONE) { if(json_last_error() == JSON_ERROR_NONE) {
abort_if(!self::validateObject($res), 422);
return $res; return $res;
} else { } else {
return false; return false;
@ -238,14 +237,26 @@ class Helpers {
} }
$scope = 'private'; $scope = 'private';
$cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false; $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
if(isset($res['to']) == true && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) { if(isset($res['to']) == true) {
$scope = 'public'; 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'; $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) { if(config('costar.enabled') == true) {
@ -309,7 +320,7 @@ class Helpers {
$status->scope = $scope; $status->scope = $scope;
$status->visibility = $scope; $status->visibility = $scope;
$status->save(); $status->save();
self::importNoteAttachment($res, $status); // self::importNoteAttachment($res, $status);
return $status; return $status;
}); });
@ -320,6 +331,8 @@ class Helpers {
public static function importNoteAttachment($data, Status $status) public static function importNoteAttachment($data, Status $status)
{ {
return;
if(self::verifyAttachments($data) == false) { if(self::verifyAttachments($data) == false) {
return; return;
} }
@ -336,28 +349,28 @@ class Helpers {
if(in_array($type, $allowed) == false || $valid == false) { if(in_array($type, $allowed) == false || $valid == false) {
continue; continue;
} }
$info = pathinfo($url); // $info = pathinfo($url);
// pleroma attachment fix // // pleroma attachment fix
$url = str_replace(' ', '%20', $url); // $url = str_replace(' ', '%20', $url);
$img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]])); // $img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]]));
$file = '/tmp/'.str_random(32); // $file = '/tmp/'.str_random(32);
file_put_contents($file, $img); // file_put_contents($file, $img);
$fdata = new File($file); // $fdata = new File($file);
$path = Storage::putFile($storagePath, $fdata, 'public'); // $path = Storage::putFile($storagePath, $fdata, 'public');
$media = new Media(); // $media = new Media();
$media->status_id = $status->id; // $media->status_id = $status->id;
$media->profile_id = $status->profile_id; // $media->profile_id = $status->profile_id;
$media->user_id = null; // $media->user_id = null;
$media->media_path = $path; // $media->media_path = $path;
$media->size = $fdata->getSize(); // $media->size = $fdata->getSize();
$media->mime = $fdata->getMimeType(); // $media->mime = $fdata->getMimeType();
$media->save(); // $media->save();
ImageThumbnail::dispatch($media); // ImageThumbnail::dispatch($media);
ImageOptimize::dispatch($media); // ImageOptimize::dispatch($media);
unlink($file); // unlink($file);
} }
return; return;
} }
@ -380,15 +393,19 @@ class Helpers {
return; return;
} }
$domain = parse_url($res['id'], PHP_URL_HOST); $domain = parse_url($res['id'], PHP_URL_HOST);
$username = $res['preferredUsername']; $username = Purify::clean($res['preferredUsername']);
$remoteUsername = "@{$username}@{$domain}"; $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(); $profile = Profile::whereRemoteUrl($res['id'])->first();
if(!$profile) { if(!$profile) {
$profile = new Profile; $profile = new Profile;
$profile->domain = $domain; $profile->domain = $domain;
$profile->username = $remoteUsername; $profile->username = Purify::clean($remoteUsername);
$profile->name = strip_tags($res['name']); $profile->name = Purify::clean($res['name']) ?? 'user';
$profile->bio = Purify::clean($res['summary']); $profile->bio = Purify::clean($res['summary']);
$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null; $profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
$profile->inbox_url = $res['inbox']; $profile->inbox_url = $res['inbox'];
@ -407,6 +424,8 @@ class Helpers {
public static function sendSignedObject($senderProfile, $url, $body) public static function sendSignedObject($senderProfile, $url, $body)
{ {
abort_if(!self::validateUrl($url), 400);
$payload = json_encode($body); $payload = json_encode($body);
$headers = HttpSignature::sign($senderProfile, $url, $body); $headers = HttpSignature::sign($senderProfile, $url, $body);
@ -418,42 +437,4 @@ class Helpers {
$response = curl_exec($ch); $response = curl_exec($ch);
return; 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;
}
} }

View file

@ -36,6 +36,7 @@ class Inbox
public function handle() public function handle()
{ {
abort_if(!Helpers::validateObject($this->payload), 400);
$this->handleVerb(); $this->handleVerb();
} }
@ -135,6 +136,8 @@ class Inbox
public function handleNoteCreate() public function handleNoteCreate()
{ {
return;
$activity = $this->payload['object']; $activity = $this->payload['object'];
$actor = $this->actorFirstOrCreate($this->payload['actor']); $actor = $this->actorFirstOrCreate($this->payload['actor']);
if(!$actor || $actor->domain == null) { if(!$actor || $actor->domain == null) {
@ -259,24 +262,24 @@ class Inbox
{ {
$actor = $this->payload['actor']; $actor = $this->payload['actor'];
$obj = $this->payload['object']; $obj = $this->payload['object'];
abort_if(!Helpers::validateUrl($obj), 400);
if(is_string($obj) && Helpers::validateUrl($obj)) { if(is_string($obj) && Helpers::validateUrl($obj)) {
// actor object detected // actor object detected
// todo delete actor // todo delete actor
} else if (Helpers::validateUrl($obj['id']) && is_array($obj) && isset($obj['type']) && $obj['type'] == 'Tombstone') { } else if (Helpers::validateUrl($obj['id']) && Helpers::validateObject($obj) && $obj['type'] == 'Tombstone') {
// tombstone detected // todo delete status or object
$status = Status::whereLocal(false)->whereUri($obj['id'])->firstOrFail();
$status->forceDelete();
} }
} }
public function handleLikeActivity() public function handleLikeActivity()
{ {
$actor = $this->payload['actor']; $actor = $this->payload['actor'];
abort_if(!Helpers::validateUrl($actor), 400);
$profile = self::actorFirstOrCreate($actor); $profile = self::actorFirstOrCreate($actor);
$obj = $this->payload['object']; $obj = $this->payload['object'];
if(Helpers::validateLocalUrl($obj) == false) { abort_if(!Helpers::validateLocalUrl($obj), 400);
return;
}
$status = Helpers::statusFirstOrFetch($obj); $status = Helpers::statusFirstOrFetch($obj);
if(!$status || !$profile) { if(!$status || !$profile) {
return; return;
@ -286,10 +289,11 @@ class Inbox
'status_id' => $status->id 'status_id' => $status->id
]); ]);
if($like->wasRecentlyCreated == false) { if($like->wasRecentlyCreated == true) {
return; LikePipeline::dispatch($like);
} }
LikePipeline::dispatch($like);
return;
} }

View file

@ -17,6 +17,7 @@
"fideloper/proxy": "^4.0", "fideloper/proxy": "^4.0",
"greggilbert/recaptcha": "dev-master", "greggilbert/recaptcha": "dev-master",
"intervention/image": "^2.4", "intervention/image": "^2.4",
"jenssegers/agent": "^2.6",
"laravel/framework": "5.8.*", "laravel/framework": "5.8.*",
"laravel/horizon": "^3.0", "laravel/horizon": "^3.0",
"laravel/passport": "^7.0", "laravel/passport": "^7.0",

172
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "188c87638a863fd575f41213e72976f5", "content-hash": "702a3ed0b8499d50323723eb4fb41965",
"packages": [ "packages": [
{ {
"name": "alchemy/binary-driver", "name": "alchemy/binary-driver",
@ -1558,6 +1558,124 @@
"description": "Highlight PHP code in terminal", "description": "Highlight PHP code in terminal",
"time": "2018-09-29T18:48:56+00:00" "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", "name": "laravel/framework",
"version": "v5.8.10", "version": "v5.8.10",
@ -2270,6 +2388,58 @@
], ],
"time": "2019-03-29T18:19:35+00:00" "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", "name": "monolog/monolog",
"version": "1.24.0", "version": "1.24.0",

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance. | 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. | 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'), 'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'),
'enforce_account_limit' => env('LIMIT_ACCOUNT_SIZE', true), 'enforce_account_limit' => env('LIMIT_ACCOUNT_SIZE', true),
'ap_inbox' => env('ACTIVITYPUB_INBOX', false), 'ap_inbox' => env('ACTIVITYPUB_INBOX', false),
'ap_shared' => env('ACTIVITYPUB_SHAREDINBOX', false), 'ap_shared' => env('ACTIVITYPUB_SHAREDINBOX', false),
'ap_delivery_timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0), 'ap_delivery_timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),
@ -267,11 +269,13 @@ return [
'import' => [ 'import' => [
'instagram' => [ 'instagram' => [
'enabled' => env('IMPORT_INSTAGRAM_ENABLED', false), 'enabled' => false,
'limits' => [ 'limits' => [
'posts' => (int) env('IMPORT_INSTAGRAM_POST_LIMIT', 100), 'posts' => (int) env('IMPORT_INSTAGRAM_POST_LIMIT', 100),
'size' => (int) env('IMPORT_INSTAGRAM_SIZE_LIMIT', 250) 'size' => (int) env('IMPORT_INSTAGRAM_SIZE_LIMIT', 250)
] ]
] ]
], ],
'oauth_enabled' => env('OAUTH_ENABLED', false),
]; ];

View file

@ -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');
});
}
}

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/developers.js vendored Normal file

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -18,10 +18,10 @@ pixelfed.readmore = () => {
return; return;
} }
el.readmore({ el.readmore({
collapsedHeight: 44, collapsedHeight: 45,
heightMargin: 20, heightMargin: 48,
moreLink: '<a href="#" class="font-weight-bold small">Read more</a>', moreLink: '<a href="#" class="d-block font-weight-lighter small text-dark text-center">Read more ...</a>',
lessLink: '<a href="#" class="font-weight-bold small">Hide</a>', lessLink: '<a href="#" class="d-block font-weight-lighter small text-dark text-center">Hide</a>',
}); });
}); });
}; };

View file

@ -25,43 +25,56 @@
</div> </div>
<div class="postPresenterContainer"> <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()"> <div v-if="uploading">
<p class="text-center mb-0 font-weight-bold p-5">Click here to add photos.</p> <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>
<div v-if="ids.length > 0"> <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" <b-carousel id="p-carousel"
style="text-shadow: 1px 1px 2px #333;" style="text-shadow: 1px 1px 2px #333;"
controls controls
indicators indicators
background="#ffffff" background="#ffffff"
:interval="0" :interval="0"
v-model="carouselCursor" v-model="carouselCursor"
> >
<b-carousel-slide v-if="ids.length > 0" v-for="(preview, index) in media" :key="'preview_media_'+index"> <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()"> <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"> <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> </div>
</div> </b-carousel-slide>
<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> </b-carousel>
</li> </div>
</ul> <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>
<div v-if="mediaDrawer" class="bg-lighter p-2 row"> <div v-if="mediaDrawer" class="bg-lighter p-2 row">
<div class="col-12"> <div class="col-12">
@ -84,24 +97,13 @@
</div> </div>
</div> </div>
<div :class="[mediaDrawer?'d-none':'card-body']"> <div class="card-body p-0">
<div class="caption"> <div class="caption">
<p class="mb-2"> <textarea class="form-control mb-0 border-0 rounded-0" rows="3" placeholder="Add an optional caption" v-model="composeText"></textarea>
<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>
</div> </div>
</div> </div>
<div :class="[mediaDrawer?'d-none':'card-footer']"> <div class="card-footer">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="custom-control custom-switch d-inline mr-3"> <div class="custom-control custom-switch d-inline mr-3">
@ -135,7 +137,7 @@
</div> </div>
</div> </div>
</a> </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="row">
<div class="d-none d-block-sm col-sm-2 px-0 text-center"> <div class="d-none d-block-sm col-sm-2 px-0 text-center">
<i class="fas fa-lock"></i> <i class="fas fa-lock"></i>
@ -192,6 +194,9 @@
</div> </div>
</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> </div>
</div> </div>
@ -234,7 +239,9 @@ export default {
carouselCursor: 0, carouselCursor: 0,
visibility: 'public', visibility: 'public',
mediaDrawer: false, mediaDrawer: false,
composeState: 'publish' composeState: 'publish',
uploading: false,
uploadProgress: 0
} }
}, },
@ -301,6 +308,9 @@ export default {
fetchProfile() { fetchProfile() {
axios.get('/api/v1/accounts/verify_credentials').then(res => { axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.profile = res.data; this.profile = res.data;
if(res.data.locked == true) {
this.visibility = 'private';
}
}).catch(err => { }).catch(err => {
console.log(err) console.log(err)
}); });
@ -320,6 +330,7 @@ export default {
$(document).on('change', '.file-input', function(e) { $(document).on('change', '.file-input', function(e) {
let io = document.querySelector('.file-input'); let io = document.querySelector('.file-input');
Array.prototype.forEach.call(io.files, function(io, i) { Array.prototype.forEach.call(io.files, function(io, i) {
self.uploading = true;
if(self.media && self.media.length + i >= self.config.uploader.album_limit) { 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'); swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');
return; return;
@ -338,20 +349,25 @@ export default {
let xhrConfig = { let xhrConfig = {
onUploadProgress: function(e) { onUploadProgress: function(e) {
let progress = Math.round( (e.loaded * 100) / e.total ); let progress = Math.round( (e.loaded * 100) / e.total );
self.uploadProgress = progress;
} }
}; };
axios.post('/api/v1/media', form, xhrConfig) axios.post('/api/v1/media', form, xhrConfig)
.then(function(e) { .then(function(e) {
self.uploadProgress = 100;
self.ids.push(e.data.id); self.ids.push(e.data.id);
self.media.push(e.data); self.media.push(e.data);
setTimeout(function() { setTimeout(function() {
self.mediaDrawer = true; self.uploading = false;
}, 1000); }, 1000);
}).catch(function(e) { }).catch(function(e) {
self.uploading = false;
io.value = null;
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error'); swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
}); });
io.value = null; io.value = null;
self.uploadProgress = 0;
}); });
}); });
}, },

View file

@ -1,195 +1,285 @@
<template> <template>
<div class="postComponent d-none"> <div>
<div class="container px-0"> <div v-if="loaded && warning" class="bg-white pt-3 border-bottom">
<div class="card card-md-rounded-0 status-container orientation-unknown"> <div class="container">
<div class="row px-0 mx-0"> <p class="text-center font-weight-bold">You are blocking this account</p>
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100"> <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>
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername"> </div>
<div class="status-avatar mr-2"> </div>
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;"> <div v-if="loaded && warning == false" class="postComponent">
</div> <div v-if="profileLayout == 'metro'" class="container px-0">
<div class="username"> <div class="card card-md-rounded-0 status-container orientation-unknown">
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span> <div class="row px-0 mx-0">
</div> <div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
</a> <a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div v-if="user != false" class="float-right"> <div class="status-avatar mr-2">
<div class="post-actions"> <img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
<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="username">
</div> <span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
</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>
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center"> </a>
<div v-if="status.pf_type === 'photo'" class="w-100"> <div v-if="user != false" class="float-right">
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter> <div class="post-actions">
</div> <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">
<div v-else-if="status.pf_type === 'video'" class="w-100"> <span class="fas fa-ellipsis-v text-muted"></span>
<video-presenter :status="status"></video-presenter> </button>
</div> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<div v-if="!owner()">
<div v-else-if="status.pf_type === 'photo:album'" class="w-100"> <a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter> <a class="dropdown-item font-weight-bold" v-on:click="muteProfile()">Mute Profile</a>
</div> <a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100"> <div v-if="ownerOrAdmin()">
<video-album-presenter :status="status"></video-album-presenter> <a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
</div> <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 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>
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="d-flex flex-md-column flex-column-reverse h-100"> </div>
<div class="card-body status-comments pb-5"> <div class="col-12 col-md-8 px-0 mx-0">
<div class="status-comment"> <div class="postPresenterLoader text-center">
<p class="mb-1 read-more" style="overflow: hidden;"> <div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<span class="font-weight-bold pr-1">{{statusUsername}}</span> </div>
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span> <div class="postPresenterContainer d-none d-flex justify-content-center align-items-center">
</p> <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 v-else-if="status.pf_type === 'video'" class="w-100">
<div class="postCommentsLoader text-center"> <video-presenter :status="status"></video-presenter>
<div class="spinner-border" role="status"> </div>
<span class="sr-only">Loading...</span>
</div> <div v-else-if="status.pf_type === 'photo:album'" class="w-100">
</div> <photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
<div class="postCommentsContainer d-none pt-3"> </div>
<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-else-if="status.pf_type === 'video:album'" class="w-100">
<div v-for="(reply, index) in results" class="pb-3"> <video-album-presenter :status="status"></video-album-presenter>
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;"> </div>
<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> <div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<span class="text-break" v-html="reply.content"></span> <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>
<span class="pl-2" style="min-width:38px"> <span v-if="ownerOrAdmin()">
<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> <a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<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> <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> </span>
</p> </div>
<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>
</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> </div>
<div class="card-body flex-grow-0 py-1"> <div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
<div class="reactions my-1"> <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-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-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.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>
<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>
<div class="reaction-counts font-weight-bold mb-0"> <div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal"> <span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes <span class="like-count">{{status.favourites_count || 0}}</span> likes
</span> </span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal"> <span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares <span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span> </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>
<div class="timestamp"> <hr>
<a v-bind:href="statusUrl" class="small text-muted"> <div>
{{timestampFormat()}} <p class="lead"><i class="far fa-clock"></i> {{timestampFormat()}}</p>
</a> <div class="lead" v-html="status.content"></div>
</div> </div>
</div> </div>
</div> <div class="col-12 col-md-4">
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0"> <div v-if="status.comments_disabled" class="bg-light p-5 text-center lead">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;"> <p class="mb-0">Comments have been disabled on this post.</p>
<li class="nav-item" v-on:click="emojiReaction">😂</li> </div>
<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> </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>
</div> </div>
</div> </div>
<b-modal ref="likesModal" <b-modal ref="likesModal"
id="l-modal" id="l-modal"
hide-footer hide-footer
@ -323,7 +413,7 @@
} }
.emoji-reactions .nav-item { .emoji-reactions .nav-item {
font-size: 1.2rem; font-size: 1.2rem;
padding: 7px; padding: 9px;
cursor: pointer; cursor: pointer;
} }
.emoji-reactions::-webkit-scrollbar { .emoji-reactions::-webkit-scrollbar {
@ -332,13 +422,31 @@
background: transparent; background: transparent;
} }
</style> </style>
<style type="text/css">
.momentui .bg-dark {
background: #000 !important;
}
.momentui .carousel.slide,
.momentui .carousel-item {
background: #000 !important;
}
</style>
<script> <script>
pixelfed.postComponent = {}; pixelfed.postComponent = {};
export default { 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() { data() {
return { return {
status: false, status: false,
@ -354,20 +462,24 @@ export default {
sharesPage: 1, sharesPage: 1,
lightboxMedia: false, lightboxMedia: false,
replyText: '', replyText: '',
relationship: {},
results: [], results: [],
pagination: {}, pagination: {},
min_id: 0, min_id: 0,
max_id: 0, max_id: 0,
reply_to_profile_id: 0, reply_to_profile_id: 0,
thread: false, thread: false,
showComments: false showComments: false,
warning: false,
loaded: false,
loading: null,
replyingToId: this.statusId,
emoji: ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','😗','😙','😚','☺️','🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹️','🙁','😖','😞','😟','😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🙌','👏','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👌','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'],
} }
}, },
mounted() { mounted() {
this.fetchData(); this.fetchRelationships();
this.authCheck();
let token = $('meta[name="csrf-token"]').attr('content'); let token = $('meta[name="csrf-token"]').attr('content');
$('input[name="_token"]').each(function(k, v) { $('input[name="_token"]').each(function(k, v) {
let el = $(v); let el = $(v);
@ -395,14 +507,6 @@ export default {
}, },
methods: { methods: {
authCheck() {
let authed = $('body').hasClass('loggedIn');
if(authed == true) {
$('.comment-form-guest').addClass('d-none');
$('.comment-form').removeClass('d-none');
}
},
showMuteBlock() { showMuteBlock() {
let sid = this.status.account.id; let sid = this.status.account.id;
let uid = this.user.id; let uid = this.user.id;
@ -427,10 +531,6 @@ export default {
}, },
fetchData() { fetchData() {
let loader = this.$loading.show({
'opacity': 0,
'background-color': '#f5f8fa'
});
axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId) axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId)
.then(response => { .then(response => {
let self = this; let self = this;
@ -444,7 +544,6 @@ export default {
self.sharesPage = 2; self.sharesPage = 2;
//this.buildPresenter(); //this.buildPresenter();
this.showMuteBlock(); this.showMuteBlock();
loader.hide();
pixelfed.readmore(); pixelfed.readmore();
if(self.status.comments_disabled == false) { if(self.status.comments_disabled == false) {
self.showComments = true; self.showComments = true;
@ -671,16 +770,20 @@ export default {
return; return;
} }
let data = { let data = {
item: this.statusId, item: this.replyingToId,
comment: this.replyText comment: this.replyText
} }
axios.post('/i/comment', data) axios.post('/i/comment', data)
.then(function(res) { .then(function(res) {
let entity = res.data.entity; 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 = ''; 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'); swal('Something went wrong!', 'Please try again later', 'error');
}); });
}, },
l(e) { l(e) {
let len = e.length; let len = e.length;
if(len < 10) { return e; } if(len < 10) { return e; }
return e.substr(0, 10)+'...'; return e.substr(0, 10)+'...';
}, },
replyFocus(e) { replyFocus(e) {
this.replyingToId = e.id;
this.reply_to_profile_id = e.account.id; this.reply_to_profile_id = e.account.id;
this.replyText = '@' + e.account.username + ' '; this.replyText = '@' + e.account.username + ' ';
$('textarea[name="comment"]').focus(); $('textarea[name="comment"]').focus();
}, },
fetchComments() { fetchComments() {
let url = '/api/v2/comments/'+this.statusUsername+'/status/'+this.statusId; let url = '/api/v2/comments/'+this.statusUsername+'/status/'+this.statusId;
axios.get(url) axios.get(url)
@ -741,6 +848,7 @@ export default {
} }
}); });
}, },
loadMore(e) { loadMore(e) {
e.preventDefault(); e.preventDefault();
if(this.pagination.total_pages == 1 || this.pagination.current_page == this.pagination.total_pages) { 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; this.pagination = response.data.meta.pagination;
}); });
}, },
likeReply(status, $event) { likeReply(status, $event) {
if($('body').hasClass('loggedIn') == false) { if($('body').hasClass('loggedIn') == false) {
return; return;
@ -778,11 +887,13 @@ export default {
swal('Error', 'Something went wrong, please try again later.', 'error'); swal('Error', 'Something went wrong, please try again later.', 'error');
}); });
}, },
truncate(str,lim) { truncate(str,lim) {
return _.truncate(str,{ return _.truncate(str,{
length: lim length: lim
}); });
}, },
timeAgo(ts) { timeAgo(ts) {
let date = Date.parse(ts); let date = Date.parse(ts);
let seconds = Math.floor((new Date() - date) / 1000); let seconds = Math.floor((new Date() - date) / 1000);
@ -852,6 +963,77 @@ export default {
return; 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;
});
}
} }
}, },

View file

@ -1,278 +1,347 @@
<template> <template>
<div> <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=""> <img src="/img/pixelfed-icon-grey.svg" class="">
</div> </div>
<div v-if="!loading"> <div v-if="!loading && !warning">
<div class="bg-white py-5 border-bottom"> <div v-if="profileLayout == 'metro'">
<div class="container"> <div class="bg-white py-5 border-bottom">
<div class="row"> <div class="container">
<div class="col-12 col-md-4 d-flex"> <div class="row">
<div class="profile-avatar mx-md-auto"> <div class="col-12 col-md-4 d-flex">
<div class="d-block d-md-none"> <div class="profile-avatar mx-md-auto">
<div class="row"> <div class="d-block d-md-none">
<div class="col-5"> <div class="row">
<img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px"> <div class="col-5">
</div> <img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px">
<div class="col-7 pl-2"> </div>
<p class="font-weight-ultralight h3 mb-0">{{profile.username}}</p> <div class="col-7 pl-2">
<p v-if="profile.id == user.id && user.hasOwnProperty('id')"> <p class="align-middle">
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
</p> <span class="font-weight-ultralight h3 mb-0">{{profile.username}}</span>
<div v-if="profile.id != user.id && user.hasOwnProperty('id')"> <span class="float-right mb-0" v-if="profile.id != user.id && user.hasOwnProperty('id')">
<p class="mt-3 mb-0" v-if="relationship.following == true"> <a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
<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> </span>
</p> </p>
<p class="mt-3 mb-0" v-if="!relationship.following"> <p v-if="profile.id == user.id && user.hasOwnProperty('id')">
<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> <a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
</p> </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>
</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>
<div class="d-none d-md-block"> </div>
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px"> <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>
</div> </div>
<div class="col-12 col-md-8 d-flex align-items-center"> </div>
<div class="profile-details"> </div>
<div class="d-none d-md-flex username-bar pb-2 align-items-center"> <div class="d-block d-md-none bg-white my-0 py-2 border-bottom">
<span class="font-weight-ultralight h3">{{profile.username}}</span> <ul class="nav d-flex justify-content-center">
<span class="pl-4" v-if="profile.is_admin"> <li class="nav-item">
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span> <div class="font-weight-light">
</span> <span class="text-dark text-center">
<span class="pl-4"> <p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a> <p class="text-muted mb-0">Posts</p>
</span> </span>
<span class="pl-4" v-if="owner"> </div>
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a> </li>
</span> <li class="nav-item px-5">
<span v-if="profile.id != user.id && user.hasOwnProperty('id')"> <div v-if="profileSettings.followers.count" class="font-weight-light">
<span class="pl-4" v-if="relationship.following == true"> <a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
<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> <p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
</span> <p class="text-muted mb-0">Followers</p>
<span class="pl-4" v-if="!relationship.following"> </a>
<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> </div>
</span> </li>
</span> <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>
<div class="d-none d-md-inline-flex profile-stats pb-3 lead"> </div>
<div class="font-weight-light pr-5"> </div>
<a class="text-dark" :href="profile.url"> <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> <span class="font-weight-bold">{{profile.statuses_count}}</span>
Posts Posts
</a> </a>
</div> <a v-if="profileSettings.followers.count" class="text-lighter cursor-pointer px-3" v-on:click="followersModal()">
<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> <span class="font-weight-bold">{{profile.followers_count}}</span>
Followers Followers
</a> </a>
</div> <a v-if="profileSettings.following.count" class="text-lighter cursor-pointer" v-on:click="followingModal()">
<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> <span class="font-weight-bold">{{profile.following_count}}</span>
Following Following
</a> </a>
</div> </div>
</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>
</div> </div>
</div> <div class="container-fluid">
<div class="d-block d-md-none bg-white my-0 py-2 border-bottom"> <div class="profile-timeline mt-md-4">
<ul class="nav d-flex justify-content-center"> <div class="card-columns" v-if="mode == 'grid'">
<li class="nav-item"> <div class="p-sm-2 p-md-3" v-for="(s, index) in timeline">
<div class="font-weight-light"> <a class="card info-overlay card-md-border-0" :href="s.url">
<span class="text-dark text-center"> <img :src="s.media_attachments[0].url" class="img-fluid">
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p> </a>
<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> </div>
</div> </div>
</div> <div v-if="timeline.length">
<div class="masonry-grid" v-if="mode == 'masonry'"> <infinite-loading @infinite="infiniteTimeline">
<div class="d-inline p-0 p-sm-2 p-md-3 masonry-item" v-for="(status, index) in timeline"> <div slot="no-more"></div>
<a class="" v-on:click.prevent="statusModal(status)" :href="status.url"> <div slot="no-results"></div>
<img :src="previewUrl(status)" :class="'o-'+masonryOrientation(status)"> </infinite-loading>
</a>
</div> </div>
</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>
</div> </div>
@ -289,7 +358,7 @@
<div class="list-group-item border-0" v-for="(user, index) in following" :key="'following_'+index"> <div class="list-group-item border-0" v-for="(user, index) in following" :key="'following_'+index">
<div class="media"> <div class="media">
<a :href="user.url"> <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> </a>
<div class="media-body"> <div class="media-body">
<p class="mb-0" style="font-size: 14px"> <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="list-group-item border-0" v-for="(user, index) in followers" :key="'follower_'+index">
<div class="media"> <div class="media">
<a :href="user.url"> <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> </a>
<div class="media-body"> <div class="media-body">
<p class="mb-0" style="font-size: 14px"> <p class="mb-0" style="font-size: 14px">
@ -337,6 +406,40 @@
</div> </div>
</div> </div>
</b-modal> </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> </div>
</template> </template>
<!-- <style type="text/css" scoped=""> <!-- <style type="text/css" scoped="">
@ -373,7 +476,8 @@
export default { export default {
props: [ props: [
'profile-id', 'profile-id',
'profile-settings' 'profile-settings',
'profile-layout'
], ],
data() { data() {
return { return {
@ -394,7 +498,8 @@ export default {
followerMore: true, followerMore: true,
following: [], following: [],
followingCursor: 1, followingCursor: 1,
followingMore: true followingMore: true,
warning: false
} }
}, },
beforeMount() { beforeMount() {
@ -412,16 +517,12 @@ export default {
axios.get('/api/v1/accounts/' + this.profileId).then(res => { axios.get('/api/v1/accounts/' + this.profileId).then(res => {
this.profile = res.data; this.profile = res.data;
}); });
axios.get('/api/v1/accounts/verify_credentials').then(res => { if($('body').hasClass('loggedIn') == true) {
this.user = res.data; axios.get('/api/v1/accounts/verify_credentials').then(res => {
}); this.user = res.data;
axios.get('/api/v1/accounts/relationships', { });
params: { this.fetchRelationships();
'id[]': this.profileId }
}
}).then(res => {
this.relationship = res.data[0];
});
let apiUrl = '/api/v1/accounts/' + this.profileId + '/statuses'; let apiUrl = '/api/v1/accounts/' + this.profileId + '/statuses';
axios.get(apiUrl, { axios.get(apiUrl, {
params: { params: {
@ -491,6 +592,11 @@ export default {
} }
}, },
reportProfile() {
let id = this.profile.id;
window.location.href = '/i/report?type=user&id=' + id;
},
reportUrl(status) { reportUrl(status) {
let type = status.in_reply_to ? 'comment' : 'post'; let type = status.in_reply_to ? 'comment' : 'post';
let id = status.id; let id = status.id;
@ -617,32 +723,90 @@ export default {
}) })
}, },
muteProfile(status) { fetchRelationships() {
if($('body').hasClass('loggedIn') == false) { if($('body').hasClass('loggedIn') == false) {
return; 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', { axios.post('/i/mute', {
type: 'user', type: 'user',
item: status.account.id item: id
}).then(res => { }).then(res => {
this.feed = this.feed.filter(s => s.account.id !== status.account.id); this.fetchRelationships();
swal('Success', 'You have successfully muted ' + status.account.acct, 'success'); this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
}).catch(err => { }).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error'); swal('Error', 'Something went wrong. Please try again later.', 'error');
}); });
}, },
blockProfile(status) {
unmuteProfile(status = null) {
if($('body').hasClass('loggedIn') == false) { if($('body').hasClass('loggedIn') == false) {
return; 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', { axios.post('/i/block', {
type: 'user', type: 'user',
item: status.account.id item: id
}).then(res => { }).then(res => {
this.feed = this.feed.filter(s => s.account.id !== status.account.id); this.warning = true;
swal('Success', 'You have successfully blocked ' + status.account.acct, 'success'); 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 => { }).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error'); swal('Error', 'Something went wrong. Please try again later.', 'error');
}); });
@ -657,7 +821,7 @@ export default {
type: 'status', type: 'status',
item: status.id item: status.id
}).then(res => { }).then(res => {
this.feed.splice(index,1); this.timeline.splice(index,1);
swal('Success', 'You have successfully deleted this post', 'success'); swal('Success', 'You have successfully deleted this post', 'success');
}).catch(err => { }).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error'); swal('Error', 'Something went wrong. Please try again later.', 'error');
@ -665,6 +829,9 @@ export default {
}, },
commentSubmit(status, $event) { commentSubmit(status, $event) {
if($('body').hasClass('loggedIn') == false) {
return;
}
let id = status.id; let id = status.id;
let form = $event.target; let form = $event.target;
let input = $(form).find('input[name="comment"]'); let input = $(form).find('input[name="comment"]');
@ -710,9 +877,13 @@ export default {
}, },
followProfile() { followProfile() {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.post('/i/follow', { axios.post('/i/follow', {
item: this.profileId item: this.profileId
}).then(res => { }).then(res => {
this.$refs.visitorContextMenu.hide();
if(this.relationship.following) { if(this.relationship.following) {
this.profile.followers_count--; this.profile.followers_count--;
if(this.profile.locked == true) { if(this.profile.locked == true) {
@ -726,6 +897,10 @@ export default {
}, },
followingModal() { followingModal() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
return;
}
if(this.profileSettings.following.list == false) { if(this.profileSettings.following.list == false) {
return; return;
} }
@ -749,6 +924,10 @@ export default {
}, },
followersModal() { followersModal() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
return;
}
if(this.profileSettings.followers.list == false) { if(this.profileSettings.followers.list == false) {
return; return;
} }
@ -772,6 +951,10 @@ export default {
}, },
followingLoadMore() { 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', { axios.get('/api/v1/accounts/'+this.profile.id+'/following', {
params: { params: {
page: this.followingCursor page: this.followingCursor
@ -790,6 +973,9 @@ export default {
followersLoadMore() { followersLoadMore() {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.get('/api/v1/accounts/'+this.profile.id+'/followers', { axios.get('/api/v1/accounts/'+this.profile.id+'/followers', {
params: { params: {
page: this.followerCursor page: this.followerCursor
@ -804,6 +990,13 @@ export default {
this.followerMore = false; this.followerMore = false;
} }
}); });
},
visitorMenu() {
if($('body').hasClass('loggedIn') == false) {
return;
}
this.$refs.visitorContextMenu.show();
} }
} }
} }

View file

@ -12,19 +12,19 @@
<div v-if="!loading && !networkError" class="mt-5 row"> <div v-if="!loading && !networkError" class="mt-5 row">
<div class="col-12 col-md-3 mb-4"> <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> <p class="font-weight-bold">Filters</p>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter1" v-model="filters.hashtags"> <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>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter2" v-model="filters.profiles"> <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>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter3" v-model="filters.statuses"> <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> </div>
</div> </div>
@ -56,11 +56,11 @@
<p class="font-weight-bold text-truncate text-dark"> <p class="font-weight-bold text-truncate text-dark">
{{profile.value}} {{profile.value}}
</p> </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)"> <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'}} {{profile.entity.following ? 'Unfollow' : 'Follow'}}
</button> </button>
</p> --> </p>
</div> </div>
</a> </a>
</div> </div>
@ -124,27 +124,31 @@ export default {
}, },
methods: { methods: {
fetchSearchResults() { fetchSearchResults() {
axios.get('/api/search/' + encodeURI(this.query)) axios.get('/api/search', {
.then(res => { params: {
let results = res.data; 'q': this.query,
this.results.hashtags = results.hashtags; 'src': 'metro',
this.results.profiles = results.profiles; 'v': 1
this.results.statuses = results.posts; }
this.loading = false; }).then(res => {
}).catch(err => { let results = res.data;
this.loading = false; this.results.hashtags = results.hashtags;
// this.networkError = true; this.results.profiles = results.profiles;
}) this.results.statuses = results.posts;
this.loading = false;
}).catch(err => {
this.loading = false;
// this.networkError = true;
})
}, },
followProfile(id) { followProfile(id) {
// todo: finish AP Accept handling to enable remote follows // todo: finish AP Accept handling to enable remote follows
return; axios.post('/i/follow', {
// axios.post('/i/follow', { item: id
// item: id }).then(res => {
// }).then(res => { window.location.href = window.location.href;
// window.location.href = window.location.href; });
// });
}, },
} }

View file

@ -19,7 +19,7 @@
</video> </video>
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class" v-on:click="$emit('lightbox', media)"> <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> </div>
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p> <p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
@ -43,7 +43,7 @@
</video> </video>
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class" v-on:click="$emit('lightbox', media)"> <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> </div>
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p> <p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>

View file

@ -7,14 +7,14 @@
</summary> </summary>
<b-carousel :id="status.id + '-carousel'" <b-carousel :id="status.id + '-carousel'"
v-model="cursor" 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 controls
background="#ffffff" background="#ffffff"
:interval="0" :interval="0"
> >
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id"> <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)"> <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"> <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> </div>
</b-carousel-slide> </b-carousel-slide>
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;"> <span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">
@ -26,14 +26,14 @@
<div v-else> <div v-else>
<b-carousel :id="status.id + '-carousel'" <b-carousel :id="status.id + '-carousel'"
v-model="cursor" 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 controls
background="#ffffff" background="#ffffff"
:interval="0" :interval="0"
> >
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id" :alt="img.description" :title="img.description"> <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)"> <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"> <img class="img-fluid" style="max-height: 600px;" :src="img.url" loading="lazy">
</div> </div>
</b-carousel-slide> </b-carousel-slide>
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;"> <span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">

View file

@ -6,13 +6,13 @@
<p class="font-weight-light">(click to show)</p> <p class="font-weight-light">(click to show)</p>
</summary> </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"> <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> </div>
</details> </details>
</div> </div>
<div v-else> <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"> <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>
</div> </div>
</template> </template>

14
resources/assets/js/developers.js vendored Normal file
View file

@ -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
);

View file

@ -6,13 +6,8 @@
<div class="card mt-3"> <div class="card mt-3">
<div class="card-body p-0"> <div class="card-body p-0">
<ul class="nav nav-pills d-flex text-center"> <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"> <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>
<li class="nav-item flex-fill"> <li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase" href="{{route('follow-requests')}}">Follow Requests</a> <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 class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
</span> </span>
<span class="float-right notification-action"> <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()}}"> <a href="{{$notification->status->parent()->url()}}">
<div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div> <div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div>
</a> </a>
@ -142,5 +137,10 @@
@endsection @endsection
@push('scripts') @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 @endpush

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -45,8 +45,8 @@
</li> </li>
{{-- <li class="pr-2"> {{-- <li class="pr-2">
<a class="nav-link font-weight-bold" href="/" title="Home"> <a class="nav-link font-weight-bold {{request()->is('timeline/network') ?'text-primary':''}}" href="{{route('timeline.network')}}" title="Network Timeline">
{{ __('Network') }} <i class="fas fa-globe fa-lg"></i>
</a> </a>
</li> --}} </li> --}}
<div class="d-none d-md-block"> <div class="d-none d-md-block">
@ -87,7 +87,11 @@
<span class="far fa-map pr-1"></span> <span class="far fa-map pr-1"></span>
{{__('navmenu.publicTimeline')}} {{__('navmenu.publicTimeline')}}
</a> </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')}}"> <a class="d-block d-md-none dropdown-item font-weight-bold" href="{{route('discover')}}">
<span class="far fa-compass pr-1"></span> <span class="far fa-compass pr-1"></span>
{{__('navmenu.discover')}} {{__('navmenu.discover')}}

View file

@ -7,8 +7,10 @@
</div> </div>
@endif @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 @endsection
@push('meta')<meta property="og:description" content="{{$profile->bio}}"> @push('meta')<meta property="og:description" content="{{$profile->bio}}">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <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 Report Abusive/Harmful Comment
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <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 Report Abusive/Harmful Post
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <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 Report Abusive/Harmful Profile
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,8 +2,8 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <div class="card">
<div class="card-header lead font-weight-bold bg-white"> <div class="card-header lead font-weight-bold bg-white">

View file

@ -2,21 +2,21 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <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 I'm not interested in this content
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="p-5 text-center"> <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> <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>
<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="#"> <p><a class="font-weight-bold" href="#">
Learn more Learn more
</a> about our reporting guidelines and policy.</p> </a> about our reporting guidelines and policy.</p>
</div> </div> --}}
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <div class="card">
<div class="card-header lead font-weight-bold"> <div class="card-header lead font-weight-bold bg-white">
Report Sensitive Comment Report Sensitive Comment
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <div class="card">
<div class="card-header lead font-weight-bold"> <div class="card-header lead font-weight-bold bg-white">
Report Sensitive Post Report Sensitive Post
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <div class="card">
<div class="card-header lead font-weight-bold"> <div class="card-header lead font-weight-bold bg-white">
Report Sensitive Profile Report Sensitive Profile
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <div class="card">
<div class="card-header lead font-weight-bold"> <div class="card-header lead font-weight-bold bg-white">
Report Comment Spam Report Comment Spam
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <div class="card">
<div class="card-header lead font-weight-bold"> <div class="card-header lead font-weight-bold bg-white">
Report Post Spam Report Post Spam
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -2,10 +2,10 @@
@section('content') @section('content')
<div class="container mt-4 mb-5 pb-5"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card"> <div class="card">
<div class="card-header lead font-weight-bold"> <div class="card-header lead font-weight-bold bg-white">
Report Profile Spam Report Profile Spam
</div> </div>
<div class="card-body"> <div class="card-body">

View file

@ -12,6 +12,7 @@
@endsection @endsection
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
<script type="text/javascript"> <script type="text/javascript">
new Vue({ new Vue({
el: '#content' el: '#content'

View file

@ -11,6 +11,7 @@
@endsection @endsection
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
<script type="text/javascript"> <script type="text/javascript">
new Vue({ new Vue({
el: '#content' el: '#content'

View file

@ -98,6 +98,22 @@
</div> </div>
</div> </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> <hr>
<div class="form-group row"> <div class="form-group row">
<div class="col-12 d-flex align-items-center justify-content-between"> <div class="col-12 d-flex align-items-center justify-content-between">

View file

@ -23,7 +23,9 @@
@endif @endif
</div> </div>
@include('settings.security.2fa.partial.log-panel') @include('settings.security.log-panel')
@include('settings.security.device-panel')
</section> </section>
@endsection @endsection

View file

@ -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>

View file

@ -1,12 +1,12 @@
<div class="mb-4 pb-4"> <div class="mb-4 pb-4">
<h4 class="font-weight-bold">Account Log</h4> <h4 class="font-weight-bold">Account Log</h4>
<hr> <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) @if($activity->count() == 0)
<p class="alert alert-info font-weight-bold">No activity logs found!</p> <p class="alert alert-info font-weight-bold">No activity logs found!</p>
@endif @endif
@foreach($activity as $log) @foreach($activity as $log)
<li class="list-group-item"> <li class="list-group-item rounded-0 border-0">
<div class="media"> <div class="media">
<div class="media-body"> <div class="media-body">
<span class="my-0 font-weight-bold text-muted"> <span class="my-0 font-weight-bold text-muted">

View file

@ -32,3 +32,12 @@
</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

View file

@ -2,16 +2,16 @@
@section('content') @section('content')
<div class="container"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12"> <div class="col-12 px-0">
<div class="card mt-5"> <div class="card mt-md-5 px-0 mx-md-3">
<div class="card-header font-weight-bold text-muted bg-white py-4"> <div class="card-header font-weight-bold text-muted bg-white py-4">
<a href="{{route('site.help')}}" class="text-muted">{{__('helpcenter.helpcenter')}}</a> <a href="{{route('site.help')}}" class="text-muted">{{__('helpcenter.helpcenter')}}</a>
<span class="px-2 font-weight-light">&mdash;</span> <span class="px-2 font-weight-light">&mdash;</span>
{{ $breadcrumb ?? ''}} {{ $breadcrumb ?? ''}}
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="row"> <div class="row px-0">
@include('site.help.partial.sidebar') @include('site.help.partial.sidebar')
<div class="col-12 col-md-9 p-5"> <div class="col-12 col-md-9 p-5">
@if (session('status')) @if (session('status'))

View file

@ -42,7 +42,7 @@
<div class="volume"></div> <div class="volume"></div>
<div class="camera"></div> <div class="camera"></div>
<div class="screen"> <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> </div>
<div class="marvel-device iphone-x" style="position: absolute;z-index: 20;margin: 99px 0 0 151px;"> <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="inner-shadow"></div>
<div class="screen"> <div class="screen">
<div id="iosDevice"> <div id="iosDevice">
<img v-if="!loading" src="/img/landing/ios_4.jpg" class="img-fluid"> <img src="/img/landing/ios_4.jpg" class="img-fluid" loading="lazy">
<img v-if="!loading" src="/img/landing/ios_3.jpg" class="img-fluid"> <img src="/img/landing/ios_3.jpg" class="img-fluid" loading="lazy">
<img v-if="!loading" src="/img/landing/ios_2.jpg" class="img-fluid"> <img src="/img/landing/ios_2.jpg" class="img-fluid" loading="lazy">
<img src="/img/landing/ios_1.jpg" class="img-fluid"> <img src="/img/landing/ios_1.jpg" class="img-fluid" loading="lazy">
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,11 +2,11 @@
@section('content') @section('content')
<div class="container"> <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12"> <div class="col-12 px-0">
<div class="card mt-5"> <div class="card mt-md-5">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="row"> <div class="row px-0">
@include('site.partial.sidebar') @include('site.partial.sidebar')
<div class="col-12 col-md-9 p-5"> <div class="col-12 col-md-9 p-5">
@if (session('status')) @if (session('status'))

View file

@ -9,7 +9,7 @@
</div> </div>
</noscript> </noscript>
<div class="mt-md-4"></div> <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 @endsection

View file

@ -8,6 +8,7 @@
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script> <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
new Vue({ new Vue({
el: '#content' el: '#content'

View file

@ -34,9 +34,15 @@
<label class="font-weight-bold text-muted small">Visibility</label> <label class="font-weight-bold text-muted small">Visibility</label>
<div class="switch switch-sm"> <div class="switch switch-sm">
<select class="form-control" name="visibility"> <select class="form-control" name="visibility">
<option value="public" selected="">Public</option> @if(Auth::user()->profile->is_private)
<option value="unlisted">Unlisted (hidden from public timelines)</option> <option value="public">Public</option>
<option value="private">Followers Only</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> </select>
</div> </div>
<small class="form-text text-muted"> <small class="form-text text-muted">

View file

@ -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>

View file

@ -64,9 +64,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover', 'DiscoverController@home')->name('discover'); Route::get('discover', 'DiscoverController@home')->name('discover');
Route::group(['prefix' => 'api'], function () { Route::group(['prefix' => 'api'], function () {
Route::get('search/{tag}', 'SearchController@searchAPI') Route::get('search', 'SearchController@searchAPI');
//->where('tag', '.*');
->where('tag', '[A-Za-z0-9]+');
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo'); Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::group(['prefix' => 'v1'], function () { 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('notifications', 'ApiController@notifications');
Route::get('timelines/public', 'PublicApiController@publicTimelineApi'); Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
Route::get('timelines/home', 'PublicApiController@homeTimelineApi'); Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
// Route::get('timelines/network', 'PublicApiController@homeTimelineApi');
}); });
Route::group(['prefix' => 'v2'], function() { Route::group(['prefix' => 'v2'], function() {
Route::get('config', 'ApiController@siteConfiguration'); 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('comment', 'CommentController@store');
Route::post('delete', 'StatusController@delete'); Route::post('delete', 'StatusController@delete');
Route::post('mute', 'AccountController@mute'); Route::post('mute', 'AccountController@mute');
Route::post('unmute', 'AccountController@unmute');
Route::post('block', 'AccountController@block'); Route::post('block', 'AccountController@block');
Route::post('unblock', 'AccountController@unblock');
Route::post('like', 'LikeController@store'); Route::post('like', 'LikeController@store');
Route::post('share', 'StatusController@storeShare'); Route::post('share', 'StatusController@storeShare');
Route::post('follow', 'FollowerController@store'); Route::post('follow', 'FollowerController@store');
@ -266,6 +267,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::redirect('/', '/'); Route::redirect('/', '/');
Route::get('public', 'TimelineController@local')->name('timeline.public'); Route::get('public', 'TimelineController@local')->name('timeline.public');
Route::post('public', 'StatusController@store'); Route::post('public', 'StatusController@store');
// Route::get('network', 'TimelineController@network')->name('timeline.network');
}); });
Route::group(['prefix' => 'users'], function () { Route::group(['prefix' => 'users'], function () {

View file

@ -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); $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() public function testMastodon()
{ {
$valid = Helpers::verifyAttachments($this->mastodon); $valid = Helpers::verifyAttachments($this->mastodon);

View file

@ -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);
}
}

3
webpack.mix.js vendored
View file

@ -34,6 +34,9 @@ mix.js('resources/assets/js/app.js', 'public/js')
// SearchResults component // SearchResults component
.js('resources/assets/js/search.js', 'public/js') .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', { .sass('resources/assets/sass/app.scss', 'public/css', {
implementation: require('node-sass') implementation: require('node-sass')
}) })