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";
$redis = Redis::connection();
@ -167,14 +167,14 @@ class AccountController extends Controller
public function mute(Request $request)
{
$this->validate($request, [
'type' => 'required|string',
'type' => 'required|alpha_dash',
'item' => 'required|integer|min:1',
]);
$user = Auth::user()->profile;
$type = $request->input('type');
$item = $request->input('item');
$action = "{$type}.mute";
$action = $type . '.mute';
if (!in_array($action, $this->filters)) {
return abort(406);
@ -211,17 +211,71 @@ class AccountController extends Controller
return redirect()->back();
}
public function block(Request $request)
public function unmute(Request $request)
{
$this->validate($request, [
'type' => 'required|string',
'type' => 'required|alpha_dash',
'item' => 'required|integer|min:1',
]);
$user = Auth::user()->profile;
$type = $request->input('type');
$item = $request->input('item');
$action = "{$type}.block";
$action = $type . '.mute';
if (!in_array($action, $this->filters)) {
return abort(406);
}
$filterable = [];
switch ($type) {
case 'user':
$profile = Profile::findOrFail($item);
if ($profile->id == $user->id) {
return abort(403);
}
$class = get_class($profile);
$filterable['id'] = $profile->id;
$filterable['type'] = $class;
break;
default:
abort(400);
break;
}
$filter = UserFilter::whereUserId($user->id)
->whereFilterableId($filterable['id'])
->whereFilterableType($filterable['type'])
->whereFilterType('mute')
->first();
if($filter) {
$filter->delete();
}
$pid = $user->id;
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid");
if($request->wantsJson()) {
return response()->json([200]);
} else {
return redirect()->back();
}
}
public function block(Request $request)
{
$this->validate($request, [
'type' => 'required|alpha_dash',
'item' => 'required|integer|min:1',
]);
$user = Auth::user()->profile;
$type = $request->input('type');
$item = $request->input('item');
$action = $type.'.block';
if (!in_array($action, $this->filters)) {
return abort(406);
}
@ -259,6 +313,56 @@ class AccountController extends Controller
return redirect()->back();
}
public function unblock(Request $request)
{
$this->validate($request, [
'type' => 'required|alpha_dash',
'item' => 'required|integer|min:1',
]);
$user = Auth::user()->profile;
$type = $request->input('type');
$item = $request->input('item');
$action = $type . '.block';
if (!in_array($action, $this->filters)) {
return abort(406);
}
$filterable = [];
switch ($type) {
case 'user':
$profile = Profile::findOrFail($item);
if ($profile->id == $user->id) {
return abort(403);
}
$class = get_class($profile);
$filterable['id'] = $profile->id;
$filterable['type'] = $class;
break;
default:
abort(400);
break;
}
$filter = UserFilter::whereUserId($user->id)
->whereFilterableId($filterable['id'])
->whereFilterableType($filterable['type'])
->whereFilterType('block')
->first();
if($filter) {
$filter->delete();
}
$pid = $user->id;
Cache::forget("user:filter:list:$pid");
Cache::forget("feature:discover:people:$pid");
Cache::forget("feature:discover:posts:$pid");
return redirect()->back();
}
public function followRequests(Request $request)
{
$pid = Auth::user()->profile->id;

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ use App\Status;
use Illuminate\Http\Request;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use App\Transformer\Api\{
AccountTransformer,
HashtagTransformer,
@ -22,17 +23,20 @@ class SearchController extends Controller
$this->middleware('auth');
}
public function searchAPI(Request $request, $tag)
public function searchAPI(Request $request)
{
if(mb_strlen($tag) < 3) {
return;
}
$this->validate($request, [
'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro',
'v' => 'required|integer|in:1'
]);
$tag = $request->input('q');
$tag = e(urldecode($tag));
$hash = hash('sha256', $tag);
$tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) {
$tokens = [];
if(Helpers::validateUrl($tag) != false) {
if(Helpers::validateUrl($tag) != false && config('pixelfed.activitypub_enabled') == true && config('pixelfed.remote_follow_enabled') == true) {
$remote = Helpers::fetchFromUrl($tag);
if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) {
$type = $remote['type'];
@ -65,7 +69,12 @@ class SearchController extends Controller
}
}
}
$hashtags = Hashtag::select('id', 'name', 'slug')->where('slug', 'like', '%'.$tag.'%')->whereHas('posts')->limit(20)->get();
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
$hashtags = Hashtag::select('id', 'name', 'slug')
->where('slug', 'like', '%'.$htag.'%')
->whereHas('posts')
->limit(20)
->get();
if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
@ -83,9 +92,9 @@ class SearchController extends Controller
});
$users = Profile::select('username', 'name', 'id')
->whereNull('status')
->whereNull('domain')
->where('id', '!=', Auth::user()->profile->id)
->where('username', 'like', '%'.$tag.'%')
->whereNull('domain')
//->orWhere('remote_url', $tag)
->limit(20)
->get();
@ -120,7 +129,6 @@ class SearchController extends Controller
->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile->id)
->where('caption', 'like', '%'.$tag.'%')
->orWhere('uri', $tag)
->latest()
->limit(10)
->get();

View file

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

View file

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

View file

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

View file

@ -25,8 +25,21 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
// Passport::routes();
// Passport::tokensExpireIn(now()->addDays(15));
// Passport::refreshTokensExpireIn(now()->addDays(30));
if(config('pixelfed.oauth_enabled')) {
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::enableImplicitGrant();
Passport::setDefaultScope([
'user:read',
'user:write'
]);
Passport::tokensCan([
'user:read' => 'Read a 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',
'sensitive' => 'as:sensitive',
'commentsEnabled' => 'sc:Boolean',
'capabilities' => [
'announce' => ['@type' => '@id'],
'like' => ['@type' => '@id'],
'reply' => ['@type' => '@id']
]
]
],
'id' => $status->permalink(),
@ -65,6 +70,11 @@ class CreateNote extends Fractal\TransformerAbstract
})->toArray(),
'tag' => $tags,
'commentsEnabled' => (bool) !$status->comments_disabled,
'capabilities' => [
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public'
]
]
];
}

View file

@ -35,6 +35,11 @@ class Note extends Fractal\TransformerAbstract
'Hashtag' => 'as:Hashtag',
'sensitive' => 'as:sensitive',
'commentsEnabled' => 'sc:Boolean',
'capabilities' => [
'announce' => ['@type' => '@id'],
'like' => ['@type' => '@id'],
'reply' => ['@type' => '@id'],
]
]
],
'id' => $status->url(),
@ -58,6 +63,11 @@ class Note extends Fractal\TransformerAbstract
})->toArray(),
'tag' => $tags,
'commentsEnabled' => (bool) !$status->comments_disabled,
'capabilities' => [
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public'
]
];
}
}

View file

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

View file

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

View file

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

View file

@ -21,8 +21,6 @@ use App\Jobs\AvatarPipeline\CreateAvatar;
use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Util\HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier};
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use App\Util\ActivityPub\HttpSignature;
use Illuminate\Support\Str;
@ -30,7 +28,7 @@ class Helpers {
public static function validateObject($data)
{
$verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo'];
$verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone'];
$valid = Validator::make($data, [
'type' => [
@ -38,11 +36,11 @@ class Helpers {
Rule::in($verbs)
],
'id' => 'required|string',
'actor' => 'required|string',
'actor' => 'required|string|url',
'object' => 'required',
'object.type' => 'required_if:type,Create',
'object.attachment' => 'required_if:type,Create',
'object.attributedTo' => 'required_if:type,Create',
'object.attributedTo' => 'required_if:type,Create|url',
'published' => 'required_if:type,Create|date'
])->passes();
@ -71,7 +69,7 @@ class Helpers {
'string',
Rule::in($mediaTypes)
],
'*.url' => 'required|max:255',
'*.url' => 'required|url|max:255',
'*.mediaType' => [
'required',
'string',
@ -193,6 +191,7 @@ class Helpers {
$res = Zttp::withHeaders(self::zttpUserAgent())->get($url);
$res = json_decode($res->body(), true, 8);
if(json_last_error() == JSON_ERROR_NONE) {
abort_if(!self::validateObject($res), 422);
return $res;
} else {
return false;
@ -238,14 +237,26 @@ class Helpers {
}
$scope = 'private';
$cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
if(isset($res['to']) == true && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
$scope = 'public';
if(isset($res['to']) == true) {
if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
$scope = 'public';
}
if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
$scope = 'public';
}
}
if(isset($res['cc']) == true && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
if(isset($res['cc']) == true) {
$scope = 'unlisted';
if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
$scope = 'unlisted';
}
if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
$scope = 'unlisted';
}
}
if(config('costar.enabled') == true) {
@ -309,7 +320,7 @@ class Helpers {
$status->scope = $scope;
$status->visibility = $scope;
$status->save();
self::importNoteAttachment($res, $status);
// self::importNoteAttachment($res, $status);
return $status;
});
@ -320,6 +331,8 @@ class Helpers {
public static function importNoteAttachment($data, Status $status)
{
return;
if(self::verifyAttachments($data) == false) {
return;
}
@ -336,28 +349,28 @@ class Helpers {
if(in_array($type, $allowed) == false || $valid == false) {
continue;
}
$info = pathinfo($url);
// $info = pathinfo($url);
// pleroma attachment fix
$url = str_replace(' ', '%20', $url);
// // pleroma attachment fix
// $url = str_replace(' ', '%20', $url);
$img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]]));
$file = '/tmp/'.str_random(32);
file_put_contents($file, $img);
$fdata = new File($file);
$path = Storage::putFile($storagePath, $fdata, 'public');
$media = new Media();
$media->status_id = $status->id;
$media->profile_id = $status->profile_id;
$media->user_id = null;
$media->media_path = $path;
$media->size = $fdata->getSize();
$media->mime = $fdata->getMimeType();
$media->save();
// $img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]]));
// $file = '/tmp/'.str_random(32);
// file_put_contents($file, $img);
// $fdata = new File($file);
// $path = Storage::putFile($storagePath, $fdata, 'public');
// $media = new Media();
// $media->status_id = $status->id;
// $media->profile_id = $status->profile_id;
// $media->user_id = null;
// $media->media_path = $path;
// $media->size = $fdata->getSize();
// $media->mime = $fdata->getMimeType();
// $media->save();
ImageThumbnail::dispatch($media);
ImageOptimize::dispatch($media);
unlink($file);
// ImageThumbnail::dispatch($media);
// ImageOptimize::dispatch($media);
// unlink($file);
}
return;
}
@ -380,15 +393,19 @@ class Helpers {
return;
}
$domain = parse_url($res['id'], PHP_URL_HOST);
$username = $res['preferredUsername'];
$username = Purify::clean($res['preferredUsername']);
$remoteUsername = "@{$username}@{$domain}";
abort_if(!self::validateUrl($res['inbox']), 400);
abort_if(!self::validateUrl($res['outbox']), 400);
abort_if(!self::validateUrl($res['id']), 400);
$profile = Profile::whereRemoteUrl($res['id'])->first();
if(!$profile) {
$profile = new Profile;
$profile->domain = $domain;
$profile->username = $remoteUsername;
$profile->name = strip_tags($res['name']);
$profile->username = Purify::clean($remoteUsername);
$profile->name = Purify::clean($res['name']) ?? 'user';
$profile->bio = Purify::clean($res['summary']);
$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
$profile->inbox_url = $res['inbox'];
@ -407,6 +424,8 @@ class Helpers {
public static function sendSignedObject($senderProfile, $url, $body)
{
abort_if(!self::validateUrl($url), 400);
$payload = json_encode($body);
$headers = HttpSignature::sign($senderProfile, $url, $body);
@ -418,42 +437,4 @@ class Helpers {
$response = curl_exec($ch);
return;
}
private static function _headersToSigningString($headers) {
}
public static function validateSignature($request, $payload = null)
{
}
public static function fetchPublicKey()
{
$profile = $this->profile;
$is_url = $this->is_url;
$valid = $this->validateUrl();
if (!$valid) {
throw new \Exception('Invalid URL provided');
}
if ($is_url && isset($profile->public_key) && $profile->public_key) {
return $profile->public_key;
}
try {
$url = $this->profile;
$res = Zttp::timeout(30)->withHeaders([
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org',
])->get($url);
$actor = json_decode($res->getBody(), true);
} catch (Exception $e) {
throw new Exception('Unable to fetch public key');
}
if($actor['publicKey']['owner'] != $profile) {
throw new Exception('Invalid key match');
}
$this->public_key = $actor['publicKey']['publicKeyPem'];
$this->key_id = $actor['publicKey']['id'];
return $this;
}
}

View file

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

View file

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

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",
"This file is @generated automatically"
],
"content-hash": "188c87638a863fd575f41213e72976f5",
"content-hash": "702a3ed0b8499d50323723eb4fb41965",
"packages": [
{
"name": "alchemy/binary-driver",
@ -1558,6 +1558,124 @@
"description": "Highlight PHP code in terminal",
"time": "2018-09-29T18:48:56+00:00"
},
{
"name": "jaybizzle/crawler-detect",
"version": "v1.2.80",
"source": {
"type": "git",
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
"reference": "af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847",
"reference": "af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8|^5.5|^6.5",
"satooshi/php-coveralls": "1.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Jaybizzle\\CrawlerDetect\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Beech",
"email": "m@rkbee.ch",
"role": "Developer"
}
],
"description": "CrawlerDetect is a PHP class for detecting bots/crawlers/spiders via the user agent",
"homepage": "https://github.com/JayBizzle/Crawler-Detect/",
"keywords": [
"crawler",
"crawler detect",
"crawler detector",
"crawlerdetect",
"php crawler detect"
],
"time": "2019-04-05T19:52:02+00:00"
},
{
"name": "jenssegers/agent",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/jenssegers/agent.git",
"reference": "bcb895395e460478e101f41cdab139c48dc721ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jenssegers/agent/zipball/bcb895395e460478e101f41cdab139c48dc721ce",
"reference": "bcb895395e460478e101f41cdab139c48dc721ce",
"shasum": ""
},
"require": {
"jaybizzle/crawler-detect": "^1.2",
"mobiledetect/mobiledetectlib": "^2.7.6",
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5.0|^6.0|^7.0"
},
"suggest": {
"illuminate/support": "^4.0|^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
},
"laravel": {
"providers": [
"Jenssegers\\Agent\\AgentServiceProvider"
],
"aliases": {
"Agent": "Jenssegers\\Agent\\Facades\\Agent"
}
}
},
"autoload": {
"psr-4": {
"Jenssegers\\Agent\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jens Segers",
"homepage": "https://jenssegers.com"
}
],
"description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect",
"homepage": "https://github.com/jenssegers/agent",
"keywords": [
"Agent",
"browser",
"desktop",
"laravel",
"mobile",
"platform",
"user agent",
"useragent"
],
"time": "2019-01-19T21:32:55+00:00"
},
{
"name": "laravel/framework",
"version": "v5.8.10",
@ -2270,6 +2388,58 @@
],
"time": "2019-03-29T18:19:35+00:00"
},
{
"name": "mobiledetect/mobiledetectlib",
"version": "2.8.33",
"source": {
"type": "git",
"url": "https://github.com/serbanghita/Mobile-Detect.git",
"reference": "cd385290f9a0d609d2eddd165a1e44ec1bf12102"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/cd385290f9a0d609d2eddd165a1e44ec1bf12102",
"reference": "cd385290f9a0d609d2eddd165a1e44ec1bf12102",
"shasum": ""
},
"require": {
"php": ">=5.0.0"
},
"require-dev": {
"phpunit/phpunit": "~4.8.35||~5.7"
},
"type": "library",
"autoload": {
"classmap": [
"Mobile_Detect.php"
],
"psr-0": {
"Detection": "namespaced/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Serban Ghita",
"email": "serbanghita@gmail.com",
"homepage": "http://mobiledetect.net",
"role": "Developer"
}
],
"description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
"homepage": "https://github.com/serbanghita/Mobile-Detect",
"keywords": [
"detect mobile devices",
"mobile",
"mobile detect",
"mobile detector",
"php mobile detect"
],
"time": "2018-09-01T15:05:15+00:00"
},
{
"name": "monolog/monolog",
"version": "1.24.0",

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.8.6',
'version' => '0.9.0',
/*
|--------------------------------------------------------------------------
@ -46,7 +46,7 @@ return [
| default memory_limit php.ini is used for the rest of the app.
|
*/
'memory_limit' => '1024M',
'memory_limit' => env('MEMORY_LIMIT', '1024M'),
/*
|--------------------------------------------------------------------------
@ -259,7 +259,9 @@ return [
'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'),
'enforce_account_limit' => env('LIMIT_ACCOUNT_SIZE', true),
'ap_inbox' => env('ACTIVITYPUB_INBOX', false),
'ap_shared' => env('ACTIVITYPUB_SHAREDINBOX', false),
'ap_delivery_timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),
@ -267,11 +269,13 @@ return [
'import' => [
'instagram' => [
'enabled' => env('IMPORT_INSTAGRAM_ENABLED', false),
'enabled' => false,
'limits' => [
'posts' => (int) env('IMPORT_INSTAGRAM_POST_LIMIT', 100),
'size' => (int) env('IMPORT_INSTAGRAM_SIZE_LIMIT', 250)
]
]
],
'oauth_enabled' => env('OAUTH_ENABLED', false),
];

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;
}
el.readmore({
collapsedHeight: 44,
heightMargin: 20,
moreLink: '<a href="#" class="font-weight-bold small">Read more</a>',
lessLink: '<a href="#" class="font-weight-bold small">Hide</a>',
collapsedHeight: 45,
heightMargin: 48,
moreLink: '<a href="#" class="d-block font-weight-lighter small text-dark text-center">Read more ...</a>',
lessLink: '<a href="#" class="d-block font-weight-lighter small text-dark text-center">Hide</a>',
});
});
};

View file

@ -25,43 +25,56 @@
</div>
<div class="postPresenterContainer">
<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia()">
<p class="text-center mb-0 font-weight-bold p-5">Click here to add photos.</p>
<div v-if="uploading">
<div class="w-100 h-100 bg-light py-5" style="border-bottom: 1px solid #f1f1f1">
<div class="p-5">
<b-progress :value="uploadProgress" :max="100" striped :animated="true"></b-progress>
<p class="text-center mb-0 font-weight-bold">Uploading ... ({{uploadProgress}}%)</p>
</div>
</div>
</div>
<div v-if="ids.length > 0">
<b-carousel id="p-carousel"
style="text-shadow: 1px 1px 2px #333;"
controls
indicators
background="#ffffff"
:interval="0"
v-model="carouselCursor"
>
<b-carousel-slide v-if="ids.length > 0" v-for="(preview, index) in media" :key="'preview_media_'+index">
<div slot="img" :class="[media[index].filter_class?media[index].filter_class + ' cursor-pointer':' cursor-pointer']" v-on:click="addMedia()">
<img class="d-block img-fluid w-100" :src="preview.url" :alt="preview.description" :title="preview.description">
</div>
</b-carousel-slide>
</b-carousel>
</div>
<div v-if="mediaDrawer" class="bg-dark align-items-center">
<ul class="nav media-drawer-filters text-center">
<li class="nav-item">
<div class="p-1 pt-3">
<img :src="media[carouselCursor].url" width="100px" height="60px" v-on:click.prevent="toggleFilter($event, null)" class="cursor-pointer">
</div>
<a :class="[media[carouselCursor].filter_class == null ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, null)">No Filter</a>
</li>
<li class="nav-item" v-for="(filter, index) in filters">
<div class="p-1 pt-3">
<div :class="filter[1]" v-on:click.prevent="toggleFilter($event, filter[1])">
<img :src="media[carouselCursor].url" width="100px" height="60px" class="">
<div v-else>
<div v-if="ids.length > 0 && ids.length != config.uploader.album_limit" class="card-header py-2 bg-primary m-2 rounded cursor-pointer" v-on:click="addMedia()">
<p class="text-center mb-0 font-weight-bold text-white"><i class="fas fa-plus mr-1"></i> Add Photo</p>
</div>
<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia()">
<p class="text-center mb-0 font-weight-bold p-5">Click here to add photos</p>
</div>
<div v-if="ids.length > 0">
<b-carousel id="p-carousel"
style="text-shadow: 1px 1px 2px #333;"
controls
indicators
background="#ffffff"
:interval="0"
v-model="carouselCursor"
>
<b-carousel-slide v-if="ids.length > 0" v-for="(preview, index) in media" :key="'preview_media_'+index">
<div slot="img" :class="[media[index].filter_class?media[index].filter_class:'']" style="display:flex;min-height: 320px;align-items: center;">
<img class="d-block img-fluid w-100" :src="preview.url" :alt="preview.description" :title="preview.description">
</div>
</div>
<a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a>
</li>
</ul>
</b-carousel-slide>
</b-carousel>
</div>
<div v-if="ids.length > 0" class="bg-dark align-items-center">
<ul class="nav media-drawer-filters text-center">
<li class="nav-item">
<div class="p-1 pt-3">
<img :src="media[carouselCursor].url" width="100px" height="60px" v-on:click.prevent="toggleFilter($event, null)" class="cursor-pointer">
</div>
<a :class="[media[carouselCursor].filter_class == null ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, null)">No Filter</a>
</li>
<li class="nav-item" v-for="(filter, index) in filters">
<div class="p-1 pt-3">
<div :class="filter[1]" v-on:click.prevent="toggleFilter($event, filter[1])">
<img :src="media[carouselCursor].url" width="100px" height="60px" class="">
</div>
</div>
<a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a>
</li>
</ul>
</div>
</div>
<div v-if="mediaDrawer" class="bg-lighter p-2 row">
<div class="col-12">
@ -84,24 +97,13 @@
</div>
</div>
<div :class="[mediaDrawer?'d-none':'card-body']">
<div class="card-body p-0">
<div class="caption">
<p class="mb-2">
<textarea class="form-control d-inline-block" rows="3" placeholder="Add an optional caption" v-model="composeText"></textarea>
</p>
</div>
<div class="comments">
</div>
<div class="timestamp pt-1">
<p class="small text-uppercase mb-0">
<span class="text-muted">
Draft
</span>
</p>
<textarea class="form-control mb-0 border-0 rounded-0" rows="3" placeholder="Add an optional caption" v-model="composeText"></textarea>
</div>
</div>
<div :class="[mediaDrawer?'d-none':'card-footer']">
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="custom-control custom-switch d-inline mr-3">
@ -135,7 +137,7 @@
</div>
</div>
</a>
<a :class="[visibility=='private'?'dropdown-item active':'dropdown-item']" href="#" data-id="private" data-title="Followers Only" v-on:click.prevent="visibility = 'unlisted'">
<a :class="[visibility=='unlisted'?'dropdown-item active':'dropdown-item']" href="#" data-id="private" data-title="Unlisted" v-on:click.prevent="visibility = 'unlisted'">
<div class="row">
<div class="d-none d-block-sm col-sm-2 px-0 text-center">
<i class="fas fa-lock"></i>
@ -192,6 +194,9 @@
</div>
</div>
<div class="card-footer py-1">
<p class="text-center mb-0 font-weight-bold text-muted small">Having issues? You can also use the <a href="/i/compose">Classic Compose UI</a>.</p>
</div>
</div>
</div>
</div>
@ -234,7 +239,9 @@ export default {
carouselCursor: 0,
visibility: 'public',
mediaDrawer: false,
composeState: 'publish'
composeState: 'publish',
uploading: false,
uploadProgress: 0
}
},
@ -301,6 +308,9 @@ export default {
fetchProfile() {
axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.profile = res.data;
if(res.data.locked == true) {
this.visibility = 'private';
}
}).catch(err => {
console.log(err)
});
@ -320,6 +330,7 @@ export default {
$(document).on('change', '.file-input', function(e) {
let io = document.querySelector('.file-input');
Array.prototype.forEach.call(io.files, function(io, i) {
self.uploading = true;
if(self.media && self.media.length + i >= self.config.uploader.album_limit) {
swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');
return;
@ -338,20 +349,25 @@ export default {
let xhrConfig = {
onUploadProgress: function(e) {
let progress = Math.round( (e.loaded * 100) / e.total );
self.uploadProgress = progress;
}
};
axios.post('/api/v1/media', form, xhrConfig)
.then(function(e) {
self.uploadProgress = 100;
self.ids.push(e.data.id);
self.media.push(e.data);
setTimeout(function() {
self.mediaDrawer = true;
self.uploading = false;
}, 1000);
}).catch(function(e) {
self.uploading = false;
io.value = null;
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
});
io.value = null;
self.uploadProgress = 0;
});
});
},

View file

@ -1,195 +1,285 @@
<template>
<div class="postComponent d-none">
<div class="container px-0">
<div class="card card-md-rounded-0 status-container orientation-unknown">
<div class="row px-0 mx-0">
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div class="status-avatar mr-2">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
</div>
</a>
<div v-if="user != false" class="float-right">
<div class="post-actions">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<div v-if="!owner()">
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile()">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
</div>
<div v-if="ownerOrAdmin()">
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</div>
</div>
<div>
<div v-if="loaded && warning" class="bg-white pt-3 border-bottom">
<div class="container">
<p class="text-center font-weight-bold">You are blocking this account</p>
<p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false; fetchData()">here</a> to view this status</p>
</div>
</div>
<div v-if="loaded && warning == false" class="postComponent">
<div v-if="profileLayout == 'metro'" class="container px-0">
<div class="card card-md-rounded-0 status-container orientation-unknown">
<div class="row px-0 mx-0">
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div class="status-avatar mr-2">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
</div>
</div>
</div>
</div>
<div class="col-12 col-md-8 px-0 mx-0">
<div class="postPresenterLoader text-center">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
</div>
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-presenter :status="status"></video-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter :status="status"></video-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
</div>
<div v-else class="w-100">
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
</div>
</div>
</div>
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div class="status-avatar mr-2">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
</div>
</a>
<div class="float-right">
<div class="post-actions">
<div v-if="user != false" class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<span v-if="!owner()">
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
</span>
<span v-if="ownerOrAdmin()">
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
</span>
</div>
</a>
<div v-if="user != false" class="float-right">
<div class="post-actions">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<div v-if="!owner()">
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile()">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
</div>
<div v-if="ownerOrAdmin()">
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex flex-md-column flex-column-reverse h-100">
<div class="card-body status-comments pb-5">
<div class="status-comment">
<p class="mb-1 read-more" style="overflow: hidden;">
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
</p>
</div>
<div class="col-12 col-md-8 px-0 mx-0">
<div class="postPresenterLoader text-center">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
</div>
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
</div>
<div v-if="showComments">
<div class="postCommentsLoader text-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="postCommentsContainer d-none pt-3">
<p class="mb-1 text-center load-more-link d-none"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
<div class="comments" data-min-id="0" data-max-id="0">
<div v-for="(reply, index) in results" class="pb-3">
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span>
<a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
<span class="text-break" v-html="reply.content"></span>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-presenter :status="status"></video-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter :status="status"></video-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
</div>
<div v-else class="w-100">
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
</div>
</div>
</div>
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div class="status-avatar mr-2">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
</div>
</a>
<div class="float-right">
<div class="post-actions">
<div v-if="user != false" class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<span v-if="!owner()">
<a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
</span>
<span class="pl-2" style="min-width:38px">
<span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
<post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
<span v-if="ownerOrAdmin()">
<a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
<a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
</span>
</p>
<p class="">
<span class="text-muted mr-3" style="width: 20px;" v-text="timeAgo(reply.created_at)"></span>
<span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
<span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply)">Reply</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex flex-md-column flex-column-reverse h-100">
<div class="card-body status-comments pb-5">
<div class="status-comment">
<p class="mb-1 read-more" style="overflow: hidden;">
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
</p>
<div v-if="showComments">
<div class="postCommentsLoader text-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="postCommentsContainer d-none pt-3">
<p v-if="status.reply_count > 10"class="mb-1 text-center load-more-link d-none"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
<div class="comments" data-min-id="0" data-max-id="0">
<div v-for="(reply, index) in results" class="pb-3">
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span>
<a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
<span class="text-break" v-html="reply.content"></span>
</span>
<span class="pl-2" style="min-width:38px">
<span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
<post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
</span>
</p>
<p class="">
<span class="text-muted mr-3" style="width: 20px;" v-text="timeAgo(reply.created_at)"></span>
<span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
<span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply)">Reply</span>
</p>
<div v-if="reply.reply_count > 0" class="cursor-pointer" style="margin-left:30px;" v-on:click="toggleReplies(reply)">
<span class="show-reply-bar"></span>
<span class="comment-reaction font-weight-bold text-muted">{{reply.thread ? 'Hide' : 'View'}} Replies ({{reply.reply_count}})</span>
</div>
<div v-if="reply.thread == true" class="comment-thread">
<p class="d-flex justify-content-between align-items-top read-more pb-3" style="overflow-y: hidden;" v-for="(s, index) in reply.replies">
<span>
<a class="text-dark font-weight-bold mr-1" :href="s.account.url" :title="s.account.username">{{s.account.username}}</a>
<span class="text-break" v-html="s.content"></span>
</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body flex-grow-0 py-1">
<div class="reactions my-1">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
</div>
<div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
</div>
<div class="timestamp pt-2 d-flex align-items-bottom justify-content-between">
<a v-bind:href="statusUrl" class="small text-muted">
{{timestampFormat()}}
</a>
<span class="small text-muted text-capitalize cursor-pointer" v-on:click="visibilityModal">{{status.visibility}}</span>
</div>
</div>
</div>
<div class="card-body flex-grow-0 py-1">
<div class="reactions my-1">
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction">😂</li>
<li class="nav-item" v-on:click="emojiReaction">💯</li>
<li class="nav-item" v-on:click="emojiReaction"></li>
<li class="nav-item" v-on:click="emojiReaction">🙌</li>
<li class="nav-item" v-on:click="emojiReaction">👏</li>
<li class="nav-item" v-on:click="emojiReaction">😍</li>
<li class="nav-item" v-on:click="emojiReaction">😯</li>
<li class="nav-item" v-on:click="emojiReaction">😢</li>
<li class="nav-item" v-on:click="emojiReaction">😅</li>
<li class="nav-item" v-on:click="emojiReaction">😁</li>
<li class="nav-item" v-on:click="emojiReaction">🙂</li>
<li class="nav-item" v-on:click="emojiReaction">😎</li>
</ul>
</div>
<div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
<div v-if="user.length == 0" class="comment-form-guest p-3">
<a href="/login">Login</a> to like or comment.
</div>
<form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/>
</form>
</div>
</div>
</div>
</div>
</div>
<div v-if="profileLayout == 'moment'" class="momentui">
<div class="bg-dark mt-md-n4">
<div class="container">
<div class="postPresenterContainer d-none d-flex justify-content-center align-items-center">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-presenter :status="status"></video-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter :status="status" v-on:lightbox="lightbox"></photo-album-presenter>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter :status="status"></video-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter :status="status" v-on:lightbox="lightbox"></mixed-album-presenter>
</div>
<div v-else class="w-100">
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
</div>
</div>
</div>
</div>
<div class="bg-white">
<div class="container">
<div class="row py-5">
<div class="col-12 col-md-8">
<div class="reactions py-2">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary float-right cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn float-right cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
</div>
<div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
<div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
</div>
<hr>
<div class="media align-items-center">
<img :src="statusAvatar" class="rounded-circle shadow-lg mr-3" alt="avatar" width="72px" height="72px">
<div class="media-body lead">
by <a href="#">{{statusUsername}}</a>
</div>
</div>
<div class="timestamp">
<a v-bind:href="statusUrl" class="small text-muted">
{{timestampFormat()}}
</a>
<hr>
<div>
<p class="lead"><i class="far fa-clock"></i> {{timestampFormat()}}</p>
<div class="lead" v-html="status.content"></div>
</div>
</div>
</div>
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction">😂</li>
<li class="nav-item" v-on:click="emojiReaction">💯</li>
<li class="nav-item" v-on:click="emojiReaction"></li>
<li class="nav-item" v-on:click="emojiReaction">🙌</li>
<li class="nav-item" v-on:click="emojiReaction">👏</li>
<li class="nav-item" v-on:click="emojiReaction">😍</li>
<li class="nav-item" v-on:click="emojiReaction">😯</li>
<li class="nav-item" v-on:click="emojiReaction">😢</li>
<li class="nav-item" v-on:click="emojiReaction">😅</li>
<li class="nav-item" v-on:click="emojiReaction">😁</li>
<li class="nav-item" v-on:click="emojiReaction">🙂</li>
<li class="nav-item" v-on:click="emojiReaction">😎</li>
</ul>
</div>
<div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
<div v-if="user.length == 0" class="comment-form-guest p-3">
<a href="/login">Login</a> to like or comment.
<div class="col-12 col-md-4">
<div v-if="status.comments_disabled" class="bg-light p-5 text-center lead">
<p class="mb-0">Comments have been disabled on this post.</p>
</div>
</div>
<form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/>
</form>
</div>
</div>
</div>
</div>
</div>
<b-modal ref="likesModal"
id="l-modal"
hide-footer
@ -323,7 +413,7 @@
}
.emoji-reactions .nav-item {
font-size: 1.2rem;
padding: 7px;
padding: 9px;
cursor: pointer;
}
.emoji-reactions::-webkit-scrollbar {
@ -332,13 +422,31 @@
background: transparent;
}
</style>
<style type="text/css">
.momentui .bg-dark {
background: #000 !important;
}
.momentui .carousel.slide,
.momentui .carousel-item {
background: #000 !important;
}
</style>
<script>
pixelfed.postComponent = {};
export default {
props: ['status-id', 'status-username', 'status-template', 'status-url', 'status-profile-url', 'status-avatar'],
props: [
'status-id',
'status-username',
'status-template',
'status-url',
'status-profile-url',
'status-avatar',
'status-profile-id',
'profile-layout'
],
data() {
return {
status: false,
@ -354,20 +462,24 @@ export default {
sharesPage: 1,
lightboxMedia: false,
replyText: '',
relationship: {},
results: [],
pagination: {},
min_id: 0,
max_id: 0,
reply_to_profile_id: 0,
thread: false,
showComments: false
showComments: false,
warning: false,
loaded: false,
loading: null,
replyingToId: this.statusId,
emoji: ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','😗','😙','😚','☺️','🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹️','🙁','😖','😞','😟','😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🙌','👏','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👌','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'],
}
},
mounted() {
this.fetchData();
this.authCheck();
this.fetchRelationships();
let token = $('meta[name="csrf-token"]').attr('content');
$('input[name="_token"]').each(function(k, v) {
let el = $(v);
@ -395,14 +507,6 @@ export default {
},
methods: {
authCheck() {
let authed = $('body').hasClass('loggedIn');
if(authed == true) {
$('.comment-form-guest').addClass('d-none');
$('.comment-form').removeClass('d-none');
}
},
showMuteBlock() {
let sid = this.status.account.id;
let uid = this.user.id;
@ -427,10 +531,6 @@ export default {
},
fetchData() {
let loader = this.$loading.show({
'opacity': 0,
'background-color': '#f5f8fa'
});
axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId)
.then(response => {
let self = this;
@ -444,7 +544,6 @@ export default {
self.sharesPage = 2;
//this.buildPresenter();
this.showMuteBlock();
loader.hide();
pixelfed.readmore();
if(self.status.comments_disabled == false) {
self.showComments = true;
@ -671,16 +770,20 @@ export default {
return;
}
let data = {
item: this.statusId,
item: this.replyingToId,
comment: this.replyText
}
axios.post('/i/comment', data)
.then(function(res) {
let entity = res.data.entity;
self.results.push(entity);
if(entity.in_reply_to_id == self.status.id) {
self.results.push(entity);
let elem = $('.status-comments')[0];
elem.scrollTop = elem.clientHeight;
} else {
}
self.replyText = '';
let elem = $('.status-comments')[0];
elem.scrollTop = elem.clientHeight;
});
},
@ -694,16 +797,20 @@ export default {
swal('Something went wrong!', 'Please try again later', 'error');
});
},
l(e) {
let len = e.length;
if(len < 10) { return e; }
return e.substr(0, 10)+'...';
},
replyFocus(e) {
this.replyingToId = e.id;
this.reply_to_profile_id = e.account.id;
this.replyText = '@' + e.account.username + ' ';
$('textarea[name="comment"]').focus();
},
fetchComments() {
let url = '/api/v2/comments/'+this.statusUsername+'/status/'+this.statusId;
axios.get(url)
@ -741,6 +848,7 @@ export default {
}
});
},
loadMore(e) {
e.preventDefault();
if(this.pagination.total_pages == 1 || this.pagination.current_page == this.pagination.total_pages) {
@ -760,6 +868,7 @@ export default {
this.pagination = response.data.meta.pagination;
});
},
likeReply(status, $event) {
if($('body').hasClass('loggedIn') == false) {
return;
@ -778,11 +887,13 @@ export default {
swal('Error', 'Something went wrong, please try again later.', 'error');
});
},
truncate(str,lim) {
return _.truncate(str,{
length: lim
});
},
timeAgo(ts) {
let date = Date.parse(ts);
let seconds = Math.floor((new Date() - date) / 1000);
@ -852,6 +963,77 @@ export default {
return;
});
}
},
fetchRelationships() {
let loader = this.$loading.show({
'opacity': 0,
'background-color': '#f5f8fa'
});
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
this.loaded = true;
loader.hide();
this.fetchData();
return;
} else {
axios.get('/api/v1/accounts/relationships', {
params: {
'id[]': this.statusProfileId
}
}).then(res => {
if(res.data[0] == null) {
this.loaded = true;
loader.hide();
this.fetchData();
return;
}
this.relationship = res.data[0];
if(res.data[0].blocking == true) {
this.loaded = true;
loader.hide();
this.warning = true;
return;
} else {
this.loaded = true;
loader.hide();
this.fetchData();
return;
}
});
}
},
visibilityModal() {
switch(this.status.visibility) {
case 'public':
swal('Public Post', 'This post is visible to everyone.', 'info');
break;
case 'unlisted':
swal('Unlisted Post', 'This post is visible on profiles and with a direct links. It is not displayed on timelines.', 'info');
break;
case 'private':
swal('Private Post', 'This post is only visible to followers.', 'info');
break;
}
},
toggleReplies(reply) {
if(reply.thread) {
reply.thread = false;
} else {
if(reply.replies.length > 0) {
reply.thread = true;
return;
}
let url = '/api/v2/comments/'+reply.account.username+'/status/'+reply.id;
axios.get(url)
.then(response => {
reply.replies = _.reverse(response.data.data);
reply.thread = true;
});
}
}
},

View file

@ -1,278 +1,347 @@
<template>
<div>
<div class="d-flex justify-content-center py-5 my-5" v-if="loading">
<div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom">
<div class="container">
<p class="text-center font-weight-bold">You are blocking this account</p>
<p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false;">here</a> to view profile</p>
</div>
</div>
<div v-if="loading" class="d-flex justify-content-center py-5 my-5">
<img src="/img/pixelfed-icon-grey.svg" class="">
</div>
<div v-if="!loading">
<div class="bg-white py-5 border-bottom">
<div class="container">
<div class="row">
<div class="col-12 col-md-4 d-flex">
<div class="profile-avatar mx-md-auto">
<div class="d-block d-md-none">
<div class="row">
<div class="col-5">
<img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px">
</div>
<div class="col-7 pl-2">
<p class="font-weight-ultralight h3 mb-0">{{profile.username}}</p>
<p v-if="profile.id == user.id && user.hasOwnProperty('id')">
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
</p>
<div v-if="profile.id != user.id && user.hasOwnProperty('id')">
<p class="mt-3 mb-0" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow">Unfollow</button>
<div v-if="!loading && !warning">
<div v-if="profileLayout == 'metro'">
<div class="bg-white py-5 border-bottom">
<div class="container">
<div class="row">
<div class="col-12 col-md-4 d-flex">
<div class="profile-avatar mx-md-auto">
<div class="d-block d-md-none">
<div class="row">
<div class="col-5">
<img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px">
</div>
<div class="col-7 pl-2">
<p class="align-middle">
<span class="font-weight-ultralight h3 mb-0">{{profile.username}}</span>
<span class="float-right mb-0" v-if="profile.id != user.id && user.hasOwnProperty('id')">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
</span>
</p>
<p class="mt-3 mb-0" v-if="!relationship.following">
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Follow">Follow</button>
<p v-if="profile.id == user.id && user.hasOwnProperty('id')">
<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
</p>
<div v-if="profile.id != user.id && user.hasOwnProperty('id')">
<p class="mt-3 mb-0" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow">Unfollow</button>
</p>
<p class="mt-3 mb-0" v-if="!relationship.following">
<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Follow">Follow</button>
</p>
</div>
</div>
</div>
</div>
<div class="d-none d-md-block">
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
</div>
</div>
<div class="d-none d-md-block">
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
</div>
<div class="col-12 col-md-8 d-flex align-items-center">
<div class="profile-details">
<div class="d-none d-md-flex username-bar pb-2 align-items-center">
<span class="font-weight-ultralight h3">{{profile.username}}</span>
<span class="pl-4" v-if="profile.is_admin">
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span>
</span>
<span class="pl-4">
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a>
</span>
<span class="pl-4" v-if="owner">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a>
</span>
<span class="pl-4" v-if="profile.id != user.id && user.hasOwnProperty('id')">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="#" @click.prevent="visitorMenu"></a>
</span>
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
<span class="pl-4" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-minus"></i></button>
</span>
<span class="pl-4" v-if="!relationship.following">
<button type="button" class="btn btn-primary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Follow"><i class="fas fa-user-plus"></i></button>
</span>
</span>
</div>
<div class="d-none d-md-inline-flex profile-stats pb-3 lead">
<div class="font-weight-light pr-5">
<a class="text-dark" :href="profile.url">
<span class="font-weight-bold">{{profile.statuses_count}}</span>
Posts
</a>
</div>
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
<a class="text-dark cursor-pointer" v-on:click="followersModal()">
<span class="font-weight-bold">{{profile.followers_count}}</span>
Followers
</a>
</div>
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer" v-on:click="followingModal()">
<span class="font-weight-bold">{{profile.following_count}}</span>
Following
</a>
</div>
</div>
<p class="lead mb-0 d-flex align-items-center pt-3">
<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
</p>
<div v-if="profile.note" class="mb-0 lead" v-html="profile.note"></div>
<p v-if="profile.website" class="mb-0"><a :href="profile.website" class="font-weight-bold" rel="me external nofollow noopener" target="_blank">{{profile.website}}</a></p>
</div>
</div>
</div>
<div class="col-12 col-md-8 d-flex align-items-center">
<div class="profile-details">
<div class="d-none d-md-flex username-bar pb-2 align-items-center">
<span class="font-weight-ultralight h3">{{profile.username}}</span>
<span class="pl-4" v-if="profile.is_admin">
<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span>
</span>
<span class="pl-4">
<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a>
</span>
<span class="pl-4" v-if="owner">
<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a>
</span>
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
<span class="pl-4" v-if="relationship.following == true">
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-minus"></i></button>
</span>
<span class="pl-4" v-if="!relationship.following">
<button type="button" class="btn btn-primary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Follow"><i class="fas fa-user-plus"></i></button>
</span>
</span>
</div>
</div>
<div class="d-block d-md-none bg-white my-0 py-2 border-bottom">
<ul class="nav d-flex justify-content-center">
<li class="nav-item">
<div class="font-weight-light">
<span class="text-dark text-center">
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
<p class="text-muted mb-0">Posts</p>
</span>
</div>
</li>
<li class="nav-item px-5">
<div v-if="profileSettings.followers.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
<p class="text-muted mb-0">Followers</p>
</a>
</div>
</li>
<li class="nav-item">
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
<p class="text-muted mb-0">Following</p>
</a>
</div>
</li>
</ul>
</div>
<div class="bg-white">
<ul class="nav nav-topbar d-flex justify-content-center border-0">
<!-- <li class="nav-item">
<a class="nav-link active font-weight-bold text-uppercase" :href="profile.url">Posts</a>
</li>
-->
<li class="nav-item">
<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th fa-lg"></i></a>
</li>
<!-- <li class="nav-item">
<a :class="this.mode == 'masonry' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('masonry')"><i class="fas fa-th-large"></i></a>
</li> -->
<li class="nav-item px-3">
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
</li>
<li class="nav-item" v-if="owner">
<a class="nav-link font-weight-bold text-uppercase" :href="profile.url + '/saved'">Saved</a>
</li>
</ul>
</div>
<div class="container">
<div class="profile-timeline mt-md-4">
<div class="row" v-if="mode == 'grid'">
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="previewBackground(s)">
</div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.favourites_count}}</span>
</span>
<span>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.reblogs_count}}</span>
</span>
</h5>
</div>
</div>
</a>
</div>
</div>
<div class="row" v-if="mode == 'list'">
<div class="col-md-8 col-lg-8 offset-md-2 px-0 mb-3 timeline">
<div class="card status-card card-md-rounded-0 my-sm-2 my-md-3 my-lg-4" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
<div class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
{{status.account.username}}
</a>
<div v-if="user.hasOwnProperty('id')" class="text-right" style="flex-grow:1;">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
<span v-if="status.account.id != user.id">
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
</span>
<span v-if="status.account.id == user.id || user.is_admin == true">
<a class="dropdown-item font-weight-bold" :href="editUrl(status)">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</span>
</div>
</div>
</div>
</div>
<div class="postPresenterContainer">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status"></photo-presenter>
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-presenter :status="status"></video-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter :status="status"></photo-album-presenter>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter :status="status"></video-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter :status="status"></mixed-album-presenter>
</div>
<div v-else class="w-100">
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
</div>
</div>
<div class="card-body">
<div class="reactions my-1" v-if="user.hasOwnProperty('id')">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
</div>
<div class="likes font-weight-bold">
<span class="like-count">{{status.favourites_count}}</span> {{status.favourites_count == 1 ? 'like' : 'likes'}}
</div>
<div class="caption">
<p class="mb-2 read-more" style="overflow: hidden;">
<span class="username font-weight-bold">
<bdi><a class="text-dark" :href="status.account.url">{{status.account.username}}</a></bdi>
</span>
<span v-html="status.content"></span>
</p>
</div>
<div class="comments">
</div>
<div class="timestamp pt-1">
<p class="small text-uppercase mb-0">
<a :href="status.url" class="text-muted">
<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
</a>
</p>
</div>
</div>
<div class="card-footer bg-white d-none">
<form class="" v-on:submit.prevent="commentSubmit(status, $event)">
<input type="hidden" name="item" value="">
<input class="form-control status-reply-input" name="comment" placeholder="Add a comment…" autocomplete="off">
</form>
</div>
</div>
<div class="d-none d-md-inline-flex profile-stats pb-3 lead">
<div class="font-weight-light pr-5">
<a class="text-dark" :href="profile.url">
</div>
</div>
<div class="masonry-grid" v-if="mode == 'masonry'">
<div class="d-inline p-0 p-sm-2 p-md-3 masonry-item" v-for="(status, index) in timeline">
<a class="" v-on:click.prevent="statusModal(status)" :href="status.url">
<img :src="previewUrl(status)" :class="'o-'+masonryOrientation(status)">
</a>
</div>
</div>
<div v-if="timeline.length">
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</div>
</div>
</div>
<div v-if="profileLayout == 'moment'">
<div class="w-100 h-100 mt-n3 bg-pixelfed" style="width:100%;min-height:274px;">
</div>
<div class="bg-white border-bottom">
<div class="container">
<div class="row">
<div class="col-12 d-flex justify-content-center">
<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px" style="margin-top:-90px; border: 5px solid #fff">
</div>
<div class="col-12 text-center">
<div class="profile-details my-3">
<p class="font-weight-ultralight h2 text-center">{{profile.username}}</p>
<div v-if="profile.note" class="text-center text-muted p-3" v-html="profile.note"></div>
<div class="pb-3 text-muted text-center">
<a class="text-lighter" :href="profile.url">
<span class="font-weight-bold">{{profile.statuses_count}}</span>
Posts
</a>
</div>
<div v-if="profileSettings.followers.count" class="font-weight-light pr-5">
<a class="text-dark cursor-pointer" v-on:click="followersModal()">
<a v-if="profileSettings.followers.count" class="text-lighter cursor-pointer px-3" v-on:click="followersModal()">
<span class="font-weight-bold">{{profile.followers_count}}</span>
Followers
</a>
</div>
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer" v-on:click="followingModal()">
<a v-if="profileSettings.following.count" class="text-lighter cursor-pointer" v-on:click="followingModal()">
<span class="font-weight-bold">{{profile.following_count}}</span>
Following
</a>
</div>
</div>
<p class="lead mb-0 d-flex align-items-center pt-3">
<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
</p>
<div v-if="profile.note" class="mb-0 lead" v-html="profile.note"></div>
<p v-if="profile.website" class="mb-0"><a :href="profile.website" class="font-weight-bold" rel="me external nofollow noopener" target="_blank">{{profile.website}}</a></p>
</div>
</div>
</div>
</div>
</div>
<div class="d-block d-md-none bg-white my-0 py-2 border-bottom">
<ul class="nav d-flex justify-content-center">
<li class="nav-item">
<div class="font-weight-light">
<span class="text-dark text-center">
<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
<p class="text-muted mb-0">Posts</p>
</span>
</div>
</li>
<li class="nav-item px-5">
<div v-if="profileSettings.followers.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
<p class="text-muted mb-0">Followers</p>
</a>
</div>
</li>
<li class="nav-item">
<div v-if="profileSettings.following.count" class="font-weight-light">
<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
<p class="text-muted mb-0">Following</p>
</a>
</div>
</li>
</ul>
</div>
<div class="bg-white">
<ul class="nav nav-topbar d-flex justify-content-center border-0">
<!-- <li class="nav-item">
<a class="nav-link active font-weight-bold text-uppercase" :href="profile.url">Posts</a>
</li>
-->
<li class="nav-item">
<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th fa-lg"></i></a>
</li>
<!-- <li class="nav-item">
<a :class="this.mode == 'masonry' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('masonry')"><i class="fas fa-th-large"></i></a>
</li> -->
<li class="nav-item px-3">
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
</li>
<li class="nav-item" v-if="owner">
<a class="nav-link font-weight-bold text-uppercase" :href="profile.url + '/saved'">Saved</a>
</li>
</ul>
</div>
<div class="container">
<div class="profile-timeline mt-md-4">
<div class="row" v-if="mode == 'grid'">
<div class="col-4 p-0 p-sm-2 p-md-3" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="previewBackground(s)">
</div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.favourites_count}}</span>
</span>
<span>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.reblogs_count}}</span>
</span>
</h5>
</div>
</div>
</a>
</div>
</div>
<div class="row" v-if="mode == 'list'">
<div class="col-md-8 col-lg-8 offset-md-2 px-0 mb-3 timeline">
<div class="card status-card card-md-rounded-0 my-sm-2 my-md-3 my-lg-4" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
<div class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
{{status.account.username}}
</a>
<div class="text-right" style="flex-grow:1;">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
<span v-bind:class="[statusOwner(status) ? 'd-none' : '']">
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
</span>
<span v-bind:class="[statusOwner(status) ? '' : 'd-none']">
<a class="dropdown-item font-weight-bold" :href="editUrl(status)">Edit</a>
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</span>
</div>
</div>
</div>
</div>
<div class="postPresenterContainer">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status"></photo-presenter>
</div>
<div v-else-if="status.pf_type === 'video'" class="w-100">
<video-presenter :status="status"></video-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100">
<photo-album-presenter :status="status"></photo-album-presenter>
</div>
<div v-else-if="status.pf_type === 'video:album'" class="w-100">
<video-album-presenter :status="status"></video-album-presenter>
</div>
<div v-else-if="status.pf_type === 'photo:video:album'" class="w-100">
<mixed-album-presenter :status="status"></mixed-album-presenter>
</div>
<div v-else class="w-100">
<p class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
</div>
</div>
<div class="card-body">
<div class="reactions my-1">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
</div>
<div class="likes font-weight-bold">
<span class="like-count">{{status.favourites_count}}</span> {{status.favourites_count == 1 ? 'like' : 'likes'}}
</div>
<div class="caption">
<p class="mb-2 read-more" style="overflow: hidden;">
<span class="username font-weight-bold">
<bdi><a class="text-dark" :href="status.account.url">{{status.account.username}}</a></bdi>
</span>
<span v-html="status.content"></span>
</p>
</div>
<div class="comments">
</div>
<div class="timestamp pt-1">
<p class="small text-uppercase mb-0">
<a :href="status.url" class="text-muted">
<timeago :datetime="status.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.created_at)" v-b-tooltip.hover.bottom></timeago>
</a>
</p>
</div>
</div>
<div class="card-footer bg-white d-none">
<form class="" v-on:submit.prevent="commentSubmit(status, $event)">
<input type="hidden" name="item" value="">
<input class="form-control status-reply-input" name="comment" placeholder="Add a comment…" autocomplete="off">
</form>
</div>
<div class="container-fluid">
<div class="profile-timeline mt-md-4">
<div class="card-columns" v-if="mode == 'grid'">
<div class="p-sm-2 p-md-3" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="s.url">
<img :src="s.media_attachments[0].url" class="img-fluid">
</a>
</div>
</div>
</div>
<div class="masonry-grid" v-if="mode == 'masonry'">
<div class="d-inline p-0 p-sm-2 p-md-3 masonry-item" v-for="(status, index) in timeline">
<a class="" v-on:click.prevent="statusModal(status)" :href="status.url">
<img :src="previewUrl(status)" :class="'o-'+masonryOrientation(status)">
</a>
<div v-if="timeline.length">
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</div>
<div v-if="timeline.length">
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</div>
</div>
</div>
@ -289,7 +358,7 @@
<div class="list-group-item border-0" v-for="(user, index) in following" :key="'following_'+index">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" loading="lazy">
</a>
<div class="media-body">
<p class="mb-0" style="font-size: 14px">
@ -318,7 +387,7 @@
<div class="list-group-item border-0" v-for="(user, index) in followers" :key="'follower_'+index">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" loading="lazy">
</a>
<div class="media-body">
<p class="mb-0" style="font-size: 14px">
@ -337,6 +406,40 @@
</div>
</div>
</b-modal>
<b-modal ref="visitorContextMenu"
id="visitor-context-menu"
hide-footer
hide-header
centered
size="sm"
body-class="list-group-flush p-0">
<div class="list-group" v-if="relationship">
<div v-if="!owner && !relationship.following" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-primary" @click="followProfile">
Follow
</div>
<div v-if="!owner && relationship.following" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="followProfile">
Unfollow
</div>
<div v-if="!owner && !relationship.muting" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="muteProfile">
Mute
</div>
<div v-if="!owner && relationship.muting" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded" @click="unmuteProfile">
Unmute
</div>
<div v-if="!owner" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="reportProfile">
Report User
</div>
<div v-if="!owner && !relationship.blocking" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="blockProfile">
Block
</div>
<div v-if="!owner && relationship.blocking" class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-danger" @click="unblockProfile">
Unblock
</div>
<div class="list-group-item cursor-pointer text-center font-weight-bold lead rounded text-muted" @click="$refs.visitorContextMenu.hide()">
Close
</div>
</div>
</b-modal>
</div>
</template>
<!-- <style type="text/css" scoped="">
@ -373,7 +476,8 @@
export default {
props: [
'profile-id',
'profile-settings'
'profile-settings',
'profile-layout'
],
data() {
return {
@ -394,7 +498,8 @@ export default {
followerMore: true,
following: [],
followingCursor: 1,
followingMore: true
followingMore: true,
warning: false
}
},
beforeMount() {
@ -412,16 +517,12 @@ export default {
axios.get('/api/v1/accounts/' + this.profileId).then(res => {
this.profile = res.data;
});
axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.user = res.data;
});
axios.get('/api/v1/accounts/relationships', {
params: {
'id[]': this.profileId
}
}).then(res => {
this.relationship = res.data[0];
});
if($('body').hasClass('loggedIn') == true) {
axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.user = res.data;
});
this.fetchRelationships();
}
let apiUrl = '/api/v1/accounts/' + this.profileId + '/statuses';
axios.get(apiUrl, {
params: {
@ -491,6 +592,11 @@ export default {
}
},
reportProfile() {
let id = this.profile.id;
window.location.href = '/i/report?type=user&id=' + id;
},
reportUrl(status) {
let type = status.in_reply_to ? 'comment' : 'post';
let id = status.id;
@ -617,32 +723,90 @@ export default {
})
},
muteProfile(status) {
fetchRelationships() {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.get('/api/v1/accounts/relationships', {
params: {
'id[]': this.profileId
}
}).then(res => {
if(res.length) {
this.relationship = res.data[0];
if(res.data[0].blocking == true) {
this.warning = true;
}
}
});
},
muteProfile(status = null) {
if($('body').hasClass('loggedIn') == false) {
return;
}
let id = this.profileId;
axios.post('/i/mute', {
type: 'user',
item: status.account.id
item: id
}).then(res => {
this.feed = this.feed.filter(s => s.account.id !== status.account.id);
swal('Success', 'You have successfully muted ' + status.account.acct, 'success');
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
},
blockProfile(status) {
unmuteProfile(status = null) {
if($('body').hasClass('loggedIn') == false) {
return;
}
let id = this.profileId;
axios.post('/i/unmute', {
type: 'user',
item: id
}).then(res => {
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully unmuted ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
},
blockProfile(status = null) {
if($('body').hasClass('loggedIn') == false) {
return;
}
let id = this.profileId;
axios.post('/i/block', {
type: 'user',
item: status.account.id
item: id
}).then(res => {
this.feed = this.feed.filter(s => s.account.id !== status.account.id);
swal('Success', 'You have successfully blocked ' + status.account.acct, 'success');
this.warning = true;
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
},
unblockProfile(status = null) {
if($('body').hasClass('loggedIn') == false) {
return;
}
let id = this.profileId;
axios.post('/i/unblock', {
type: 'user',
item: id
}).then(res => {
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully unblocked ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
@ -657,7 +821,7 @@ export default {
type: 'status',
item: status.id
}).then(res => {
this.feed.splice(index,1);
this.timeline.splice(index,1);
swal('Success', 'You have successfully deleted this post', 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
@ -665,6 +829,9 @@ export default {
},
commentSubmit(status, $event) {
if($('body').hasClass('loggedIn') == false) {
return;
}
let id = status.id;
let form = $event.target;
let input = $(form).find('input[name="comment"]');
@ -710,9 +877,13 @@ export default {
},
followProfile() {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.post('/i/follow', {
item: this.profileId
}).then(res => {
this.$refs.visitorContextMenu.hide();
if(this.relationship.following) {
this.profile.followers_count--;
if(this.profile.locked == true) {
@ -726,6 +897,10 @@ export default {
},
followingModal() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
return;
}
if(this.profileSettings.following.list == false) {
return;
}
@ -749,6 +924,10 @@ export default {
},
followersModal() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
return;
}
if(this.profileSettings.followers.list == false) {
return;
}
@ -772,6 +951,10 @@ export default {
},
followingLoadMore() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
return;
}
axios.get('/api/v1/accounts/'+this.profile.id+'/following', {
params: {
page: this.followingCursor
@ -790,6 +973,9 @@ export default {
followersLoadMore() {
if($('body').hasClass('loggedIn') == false) {
return;
}
axios.get('/api/v1/accounts/'+this.profile.id+'/followers', {
params: {
page: this.followerCursor
@ -804,6 +990,13 @@ export default {
this.followerMore = false;
}
});
},
visitorMenu() {
if($('body').hasClass('loggedIn') == false) {
return;
}
this.$refs.visitorContextMenu.show();
}
}
}

View file

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

View file

@ -19,7 +19,7 @@
</video>
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class" v-on:click="$emit('lightbox', media)">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description" loading="lazy">
</div>
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
@ -43,7 +43,7 @@
</video>
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class" v-on:click="$emit('lightbox', media)">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description" loading="lazy">
</div>
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>

View file

@ -7,14 +7,14 @@
</summary>
<b-carousel :id="status.id + '-carousel'"
v-model="cursor"
style="text-shadow: 1px 1px 2px #333;"
style="text-shadow: 1px 1px 2px #333;min-height: 330px;display: flex;align-items: center;"
controls
background="#ffffff"
:interval="0"
>
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id">
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', img)">
<img class="img-fluid" style="max-height: 600px;" :src="img.url" :alt="img.description" :title="img.description">
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', status.media_attachments[index])">
<img class="img-fluid" style="max-height: 600px;" :src="img.url" :alt="img.description" :title="img.description" loading="lazy" v-on:click="$emit('lightbox', status.media_attachments[index])">
</div>
</b-carousel-slide>
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">
@ -26,14 +26,14 @@
<div v-else>
<b-carousel :id="status.id + '-carousel'"
v-model="cursor"
style="text-shadow: 1px 1px 2px #333;"
style="text-shadow: 1px 1px 2px #333;min-height: 330px;display: flex;align-items: center;"
controls
background="#ffffff"
:interval="0"
>
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id" :alt="img.description" :title="img.description">
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', img)">
<img class="img-fluid" style="max-height: 600px;" :src="img.url">
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" v-on:click="$emit('lightbox', status.media_attachments[index])">
<img class="img-fluid" style="max-height: 600px;" :src="img.url" loading="lazy">
</div>
</b-carousel-slide>
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">

View file

@ -6,13 +6,13 @@
<p class="font-weight-light">(click to show)</p>
</summary>
<div class="max-hide-overflow" v-on:click="$emit('lightbox', status.media_attachments[0])" :class="status.media_attachments[0].filter_class" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
<img class="card-img-top" :src="status.media_attachments[0].url">
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy">
</div>
</details>
</div>
<div v-else>
<div :class="status.media_attachments[0].filter_class" v-on:click="$emit('lightbox', status.media_attachments[0])" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
<img class="card-img-top" :src="status.media_attachments[0].url">
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy">
</div>
</div>
</template>

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-body p-0">
<ul class="nav nav-pills d-flex text-center">
{{-- <li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase" href="#">Following</a>
</li> --}}
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">My Notifications</a>
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">Notifications</a>
</li>
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase" href="{{route('follow-requests')}}">Follow Requests</a>
@ -93,7 +88,7 @@
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
</span>
<span class="float-right notification-action">
@if($notification->item_id)
@if(false == true && $notification->item_id && $notification->item_type == 'App\Status')
<a href="{{$notification->status->parent()->url()}}">
<div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div>
</a>
@ -142,5 +137,10 @@
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/activity.js')}}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">
new Vue({
el: '#content'
});
</script>
@endpush

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

View file

@ -7,8 +7,10 @@
</div>
@endif
<profile profile-id="{{$profile->id}}" :profile-settings="{{json_encode($settings)}}"></profile>
<profile profile-id="{{$profile->id}}" :profile-settings="{{json_encode($settings)}}" profile-layout="{{$profile->profile_layout ?? 'metro'}}"></profile>
@if($profile->website)
<a class="d-none" href="{{$profile->website}}" rel="me">{{$profile->website}}</a>
@endif
@endsection
@push('meta')<meta property="og:description" content="{{$profile->bio}}">

View file

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

View file

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

View file

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

View file

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

View file

@ -2,21 +2,21 @@
@section('content')
<div class="container mt-4 mb-5 pb-5">
<div class="col-12 col-md-8 offset-md-2">
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12 px-0 col-md-8 offset-md-2">
<div class="card">
<div class="card-header lead font-weight-bold">
<div class="card-header lead font-weight-bold bg-white">
I'm not interested in this content
</div>
<div class="card-body">
<div class="p-5 text-center">
<p class="lead">You can <b class="font-weight-bold">unfollow</b> or <b class="font-weight-bold">mute</b> a user or hashtag from appearing in your timeline. Unless the content violates our terms of service, there is nothing we can do to remove it.</p>
</div>
<div class="col-12 col-md-8 offset-md-2">
{{-- <div class="col-12 col-md-8 offset-md-2">
<p><a class="font-weight-bold" href="#">
Learn more
</a> about our reporting guidelines and policy.</p>
</div>
</div> --}}
</div>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,6 +98,22 @@
</div>
</div>
</div>
<div class="pt-5">
<p class="font-weight-bold text-muted text-center">Layout</p>
</div>
<div class="form-group row">
<label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Profile Layout</label>
<div class="col-sm-9">
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="profileLayout1" name="profile_layout" class="custom-control-input" {{Auth::user()->profile->profile_layout != 'moment' ? 'checked':''}} value="metro">
<label class="custom-control-label" for="profileLayout1">Metro</label>
</div>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="profileLayout2" name="profile_layout" class="custom-control-input" {{Auth::user()->profile->profile_layout == 'moment' ? 'checked':''}} value="moment">
<label class="custom-control-label" for="profileLayout2">Moment</label>
</div>
</div>
</div>
<hr>
<div class="form-group row">
<div class="col-12 d-flex align-items-center justify-content-between">

View file

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

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">
<h4 class="font-weight-bold">Account Log</h4>
<hr>
<ul class="list-group" style="max-height: 400px;overflow-y: scroll;">
<ul class="list-group border" style="max-height: 400px;overflow-y: auto;">
@if($activity->count() == 0)
<p class="alert alert-info font-weight-bold">No activity logs found!</p>
@endif
@foreach($activity as $log)
<li class="list-group-item">
<li class="list-group-item rounded-0 border-0">
<div class="media">
<div class="media-body">
<span class="my-0 font-weight-bold text-muted">

View file

@ -31,4 +31,13 @@
</div>
</div>
@endsection
@endsection
@push('scripts')
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">
new Vue({
el: '#content'
});
</script>
@endpush

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
</div>
</noscript>
<div class="mt-md-4"></div>
<post-component status-template="{{$status->viewType()}}" status-id="{{$status->id}}" status-username="{{$status->profile->username}}" status-url="{{$status->url()}}" status-profile-url="{{$status->profile->url()}}" status-avatar="{{$status->profile->avatarUrl()}}"></post-component>
<post-component status-template="{{$status->viewType()}}" status-id="{{$status->id}}" status-username="{{$status->profile->username}}" status-url="{{$status->url()}}" status-profile-url="{{$status->profile->url()}}" status-avatar="{{$status->profile->avatarUrl()}}" status-profile-id="{{$status->profile_id}}" profile-layout="{{$status->profile->profile_layout ?? 'metro'}}"></post-component>
@endsection

View file

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

View file

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

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

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);
}
public function testPleroma()
{
$valid = Helpers::verifyAttachments($this->pleroma);
$this->assertTrue($valid);
}
public function testMastodon()
{
$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
.js('resources/assets/js/search.js', 'public/js')
// Developer Components
.js('resources/assets/js/developers.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css', {
implementation: require('node-sass')
})