Merge branch 'frontend-ui-refactor' into gdpr-privacy-policy

This commit is contained in:
daniel 2018-08-09 15:33:41 -06:00 committed by GitHub
commit 921ff1ae61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 4988 additions and 1033 deletions

View file

@ -1,46 +1,52 @@
APP_NAME=Laravel
APP_NAME="PixelFed Test"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
ADMIN_DOMAIN="localhost"
APP_DOMAIN="localhost"
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
CACHE_DRIVER=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120
QUEUE_DRIVER=sync
QUEUE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_DRIVER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="pixelfed@example.com
MAIL_FROM_NAME="Pixelfed"
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
SESSION_DOMAIN=".pixelfed.dev"
SESSION_DOMAIN="${APP_DOMAIN}"
SESSION_SECURE_COOKIE=true
API_BASE="/api/1/"
API_SEARCH="/api/search"
OPEN_REGISTRATION=true
RECAPTCHA_ENABLED=false
ENFORCE_EMAIL_VERIFICATION=true
MAX_PHOTO_SIZE=15000
MAX_CAPTION_LENGTH=150
MAX_ALBUM_LENGTH=4
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View file

@ -1,4 +1,76 @@
# PixelFed
Federated Image Sharing
# PixelFed: Federated Image Sharing
> This project is still in active development and not yet ready for use.
PixelFed is a federated social image sharing platform, similar to instagram.
Federation is done using the [ActivityPub](https://activitypub.rocks/) protocol,
which is used by [Mastodon](http://joinmastodon.org/), [PeerTube](https://joinpeertube.org/en/),
[Pleroma](https://pleroma.social/), and more. Through ActivityPub PixelFed can share
and interact with these platforms, as well as other instances of PixelFed.
**_Please note this is alpha software, not recommended for production use,
and federation is not supported yet._**
PixelFed is very early into the development stage. If you would like to have a
permanent instance with minimal breakage, **do not use this software until
there is a stable release**. The following setup instructions are intended for
testing and development.
## Requirements
- PHP >= 7.1.3 (7.2+ recommended for stable version)
- MySQL, Postgres (MariaDB and sqlite are not supported yet)
- Redis
- Composer
- GD or ImageMagick
- OpenSSL PHP Extension
- PDO PHP Extension
- Mbstring PHP Extension
- Tokenizer PHP Extension
- XML PHP Extension
- Ctype PHP Extension
- JSON PHP Extension
- JpegOptim
- Optipng
- Pngquant 2
- SVGO
- Gifsicle
## Installation
This guide assumes you have NGINX/Apache installed, along with the dependencies.
Those will not be covered in these early docs.
```bash
git clone https://github.com/dansup/pixelfed.git
cd pixelfed
composer install
cp .env.example .env
```
**Edit .env file with proper values**
```bash
php artisan key:generate
```
```bash
php artisan storage:link
php artisan migrate
php artisan horizon
php artisan serve --host=localhost --port=80
```
Check your browser at http://localhost
## Communication
The ways you can communicate on the project are below. Before interacting, please
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
* IRC: #pixelfed on irc.freenode.net ([#freenode_#pixelfed:matrix.org through
Matrix](https://matrix.to/#/#freenode_#pixelfed:matrix.org)
* Project on Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
## Support
The lead maintainer is on Patreon! You can become a Patron at
https://www.patreon.com/dansup

10
app/AccountLog.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AccountLog extends Model
{
//
}

View file

@ -3,8 +3,16 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Avatar extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
}

15
app/EmailVerification.php Normal file
View file

@ -0,0 +1,15 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class EmailVerification extends Model
{
public function url()
{
$base = config('app.url');
$path = '/i/confirm-email/' . $this->user_token . '/' . $this->random_token;
return "{$base}{$path}";
}
}

View file

@ -0,0 +1,36 @@
<?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\ShouldBroadcast;
use App\{User, UserSetting};
class AuthLoginEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
public function handle(User $user)
{
if(empty($user->settings)) {
$settings = new UserSetting;
$settings->user_id = $user->id;
$settings->save();
}
}
}

View file

@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
class Hashtag extends Model
{
protected $fillable = ['name','slug'];
public $fillable = ['name','slug'];
public function posts()
{

View file

@ -4,8 +4,9 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Carbon\Carbon;
use Auth, Cache, Redis;
use App\{Notification, Profile, User};
use App\Mail\ConfirmEmail;
use Auth, DB, Cache, Mail, Redis;
use App\{EmailVerification, Notification, Profile, User};
class AccountController extends Controller
{
@ -17,19 +18,78 @@ class AccountController extends Controller
public function notifications(Request $request)
{
$this->validate($request, [
'page' => 'nullable|min:1|max:3'
'page' => 'nullable|min:1|max:3',
'a' => 'nullable|alpha_dash',
]);
$profile = Auth::user()->profile;
$action = $request->input('a');
$timeago = Carbon::now()->subMonths(6);
$notifications = Notification::whereProfileId($profile->id)
->whereDate('created_at', '>', $timeago)
->orderBy('id','desc')
->take(30)
->simplePaginate();
if($action && in_array($action, ['comment', 'follow', 'mention'])) {
$notifications = Notification::whereProfileId($profile->id)
->whereAction($action)
->whereDate('created_at', '>', $timeago)
->orderBy('id','desc')
->simplePaginate(30);
} else {
$notifications = Notification::whereProfileId($profile->id)
->whereDate('created_at', '>', $timeago)
->orderBy('id','desc')
->simplePaginate(30);
}
return view('account.activity', compact('profile', 'notifications'));
}
public function verifyEmail(Request $request)
{
return view('account.verify_email');
}
public function sendVerifyEmail(Request $request)
{
$timeLimit = Carbon::now()->subDays(1)->toDateTimeString();
$recentAttempt = EmailVerification::whereUserId(Auth::id())
->where('created_at', '>', $timeLimit)->count();
$exists = EmailVerification::whereUserId(Auth::id())->count();
if($recentAttempt == 1 && $exists == 1) {
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
} elseif ($recentAttempt == 0 && $exists !== 0) {
// Delete old verification and send new one.
EmailVerification::whereUserId(Auth::id())->delete();
}
$user = User::whereNull('email_verified_at')->find(Auth::id());
$utoken = hash('sha512', $user->id);
$rtoken = str_random(40);
$verify = new EmailVerification;
$verify->user_id = $user->id;
$verify->email = $user->email;
$verify->user_token = $utoken;
$verify->random_token = $rtoken;
$verify->save();
Mail::to($user->email)->send(new ConfirmEmail($verify));
return redirect()->back()->with('status', 'Email verification email sent!');
}
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
{
$verify = EmailVerification::where('user_token', $userToken)
->where('random_token', $randomToken)
->firstOrFail();
if(Auth::id() === $verify->user_id) {
$user = User::find(Auth::id());
$user->email_verified_at = Carbon::now();
$user->save();
return redirect('/');
}
}
public function fetchNotifications($id)
{
$key = config('cache.prefix') . ":user.{$id}.notifications";
@ -54,4 +114,5 @@ class AccountController extends Controller
}
return $notifications;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api;
use Auth;
use App\{Like, Profile, Status};
use League\Fractal;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Util\Webfinger\Webfinger;
use App\Transformer\Api\{
AccountTransformer,
StatusTransformer
};
use League\Fractal\Serializer\ArraySerializer;
class BaseApiController extends Controller
{
protected $fractal;
public function __construct()
{
$this->middleware('auth');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
public function accounts(Request $request, $id)
{
$profile = Profile::findOrFail($id);
$resource = new Fractal\Resource\Item($profile, new AccountTransformer);
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function accountFollowers(Request $request, $id)
{
$profile = Profile::findOrFail($id);
$followers = $profile->followers;
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer);
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function accountFollowing(Request $request, $id)
{
$profile = Profile::findOrFail($id);
$following = $profile->following;
$resource = new Fractal\Resource\Collection($following, new AccountTransformer);
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function accountStatuses(Request $request, $id)
{
$profile = Profile::findOrFail($id);
$statuses = $profile->statuses()->orderBy('id', 'desc')->paginate(20);
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer);
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function followSuggestions(Request $request)
{
$followers = Auth::user()->profile->recommendFollowers();
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer);
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth;
use App\{AccountLog, User};
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
@ -25,7 +26,7 @@ class LoginController extends Controller
*
* @var string
*/
protected $redirectTo = '/home';
protected $redirectTo = '/';
/**
* Create a new controller instance.
@ -56,4 +57,25 @@ class LoginController extends Controller
$this->validate($request, $rules);
}
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated($request, $user)
{
$log = new AccountLog;
$log->user_id = $user->id;
$log->item_id = $user->id;
$log->item_type = 'App\User';
$log->action = 'auth.login';
$log->message = 'Account Login';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->save();
}
}

View file

@ -16,23 +16,27 @@ class BookmarkController extends Controller
public function store(Request $request)
{
$this->validate($request, [
'item' => 'required|integer|min:1'
'item' => 'required|integer|min:1'
]);
$profile = Auth::user()->profile;
$status = Status::findOrFail($request->input('item'));
$bookmark = Bookmark::firstOrCreate(
['status_id' => $status->id], ['profile_id' => $profile->id]
['status_id' => $status->id], ['profile_id' => $profile->id]
);
if(!$bookmark->wasRecentlyCreated) {
$bookmark->delete();
}
if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
} else {
} else {
$response = redirect()->back();
}
}
return $response;
}
return $response;
}
}

View file

@ -34,8 +34,8 @@ class CommentController extends Controller
$reply = new Status();
$reply->profile_id = $profile->id;
$reply->caption = $comment;
$reply->rendered = e($comment);
$reply->caption = e($comment);
$reply->rendered = $comment;
$reply->in_reply_to_id = $status->id;
$reply->in_reply_to_profile_id = $status->profile_id;
$reply->save();
@ -44,7 +44,7 @@ class CommentController extends Controller
CommentPipeline::dispatch($status, $reply);
if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url()];
$response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url(), 'comment' => $reply->caption];
} else {
$response = redirect($status->url());
}

View file

@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use Auth;
use Auth, Cache;
use App\Profile;
use Carbon\Carbon;
use League\Fractal;
use Illuminate\Http\Request;
use App\Util\Lexer\Nickname;
@ -13,15 +14,26 @@ use App\Transformer\ActivityPub\{
ProfileTransformer
};
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
use App\Jobs\InboxPipeline\InboxWorker;
class FederationController extends Controller
{
public function authCheck()
{
if(!Auth::check()) {
abort(403);
return abort(403);
}
return;
}
public function authorizeFollow(Request $request)
{
$this->authCheck();
$this->validate($request, [
'acct' => 'required|string|min:3|max:255'
]);
$acct = $request->input('acct');
$nickname = Nickname::normalizeProfileUrl($acct);
return view('federation.authorizefollow', compact('acct', 'nickname'));
}
public function remoteFollow()
@ -64,61 +76,58 @@ class FederationController extends Controller
public function nodeinfo()
{
$res = [
'metadata' => [
'nodeName' => config('app.name'),
'software' => [
'homepage' => 'https://pixelfed.org',
'github' => 'https://github.com/pixelfed',
'follow' => 'https://mastodon.social/@pixelfed'
],
/*
TODO: Custom Features for Trending
'customFeatures' => [
'trending' => [
'description' => 'Trending API for federated discovery',
'api' => [
'url' => null,
'docs' => null
],
$res = Cache::remember('api:nodeinfo', 60, function() {
return [
'metadata' => [
'nodeName' => config('app.name'),
'software' => [
'homepage' => 'https://pixelfed.org',
'github' => 'https://github.com/pixelfed',
'follow' => 'https://mastodon.social/@pixelfed'
],
],
*/
],
'openRegistrations' => config('pixelfed.open_registration'),
'protocols' => [
'activitypub'
],
'services' => [
'inbound' => [],
'outbound' => []
],
'software' => [
'name' => 'pixelfed',
'version' => config('pixelfed.version')
],
'usage' => [
'localPosts' => \App\Status::whereLocal(true)->count(),
'users' => [
'total' => \App\User::count()
]
],
'version' => '2.0'
];
return response()->json($res);
'openRegistrations' => config('pixelfed.open_registration'),
'protocols' => [
'activitypub'
],
'services' => [
'inbound' => [],
'outbound' => []
],
'software' => [
'name' => 'pixelfed',
'version' => config('pixelfed.version')
],
'usage' => [
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
'users' => [
'total' => \App\User::count(),
'activeHalfyear' => \App\User::where('updated_at', '>', Carbon::now()->subMonths(6)->toDateTimeString())->count(),
'activeMonth' => \App\User::where('updated_at', '>', Carbon::now()->subMonths(1)->toDateTimeString())->count(),
]
],
'version' => '2.0'
];
});
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function webfinger(Request $request)
{
$this->validate($request, ['resource'=>'required']);
$resource = $request->input('resource');
$parsed = Nickname::normalizeProfileUrl($resource);
$username = $parsed['username'];
$user = Profile::whereUsername($username)->firstOrFail();
$webfinger = (new Webfinger($user))->generate();
return response()->json($webfinger);
$this->validate($request, ['resource'=>'required|string|min:3|max:255']);
$hash = hash('sha512', $request->input('resource'));
$webfinger = Cache::remember('api:webfinger:'.$hash, 1440, function() use($request) {
$resource = $request->input('resource');
$parsed = Nickname::normalizeProfileUrl($resource);
$username = $parsed['username'];
$user = Profile::whereUsername($username)->firstOrFail();
return (new Webfinger($user))->generate();
});
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
}
public function userOutbox(Request $request, $username)
@ -135,4 +144,20 @@ class FederationController extends Controller
return response()->json($res['data']);
}
public function userInbox(Request $request, $username)
{
if(config('pixelfed.activitypub_enabled') == false) {
abort(403);
}
$mimes = [
'application/activity+json',
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
];
if(!in_array($request->header('Content-Type'), $mimes)) {
abort(500, 'Invalid request');
}
$profile = Profile::whereUsername($username)->firstOrFail();
InboxWorker::dispatch($request, $profile, $request->all());
}
}

View file

@ -1,10 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ImportDataController extends Controller
{
//
}

View file

@ -3,7 +3,7 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth, Hashids;
use Auth, Cache, Hashids;
use App\{Like, Profile, Status, User};
use App\Jobs\LikePipeline\LikePipeline;
@ -27,7 +27,7 @@ class LikeController extends Controller
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
$like->delete();
$like->forceDelete();
$count--;
} else {
$like = new Like;
@ -35,9 +35,15 @@ class LikeController extends Controller
$like->status_id = $status->id;
$like->save();
$count++;
LikePipeline::dispatch($like);
}
LikePipeline::dispatch($like);
$likes = Like::whereProfileId($profile->id)
->orderBy('id', 'desc')
->take(1000)
->pluck('status_id');
Cache::put('api:like-ids:user:'.$profile->id, $likes, 1440);
if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];

View file

@ -18,17 +18,22 @@ class ProfileController extends Controller
public function show(Request $request, $username)
{
$user = Profile::whereUsername($username)->firstOrFail();
$settings = User::whereUsername($username)->firstOrFail()->settings;
$mimes = [
'application/activity+json',
'application/ld+json',
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
];
if(in_array($request->header('accept'), $mimes) && config('pixelfed.activitypub_enabled')) {
return $this->showActivityPub($request, $user);
}
if($user->is_private == true) {
$can_access = $this->privateProfileCheck($user);
if($can_access !== true) {
abort(403);
}
}
// TODO: refactor this mess
$owner = Auth::check() && Auth::id() === $user->user_id;
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
@ -36,11 +41,26 @@ class ProfileController extends Controller
$timeline = $user->statuses()
->whereHas('media')
->whereNull('in_reply_to_id')
->orderBy('id','desc')
->orderBy('created_at','desc')
->withCount(['comments', 'likes'])
->simplePaginate(21);
return view('profile.show', compact('user', 'owner', 'is_following', 'is_admin', 'timeline'));
return view('profile.show', compact('user', 'settings', 'owner', 'is_following', 'is_admin', 'timeline'));
}
protected function privateProfileCheck(Profile $profile)
{
if(Auth::check() === false) {
return false;
}
$follower_ids = (array) $profile->followers()->pluck('followers.profile_id');
$pid = Auth::user()->profile->id;
if(!in_array($pid, $follower_ids) && $pid !== $profile->id) {
return false;
}
return true;
}
public function showActivityPub(Request $request, $user)
@ -89,11 +109,12 @@ class ProfileController extends Controller
abort(403);
}
$user = Auth::user()->profile;
$settings = User::whereUsername($username)->firstOrFail()->settings;
$owner = true;
$following = false;
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at','desc')->simplePaginate(10);
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
return view('profile.show', compact('user', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
return view('profile.show', compact('user', 'settings', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
}
}

View file

@ -3,8 +3,8 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{Profile, User};
use Auth;
use App\{AccountLog, Profile, User};
use Auth, DB;
class SettingsController extends Controller
{
@ -89,6 +89,34 @@ class SettingsController extends Controller
return view('settings.avatar');
}
public function accessibility()
{
$settings = Auth::user()->settings;
return view('settings.accessibility', compact('settings'));
}
public function accessibilityStore(Request $request)
{
$settings = Auth::user()->settings;
$fields = [
'compose_media_descriptions',
'reduce_motion',
'optimize_screen_reader',
'high_contrast_mode',
'video_autoplay'
];
foreach($fields as $field) {
$form = $request->input($field);
if($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
$settings->save();
}
return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!');
}
public function notifications()
{
return view('settings.notifications');
@ -96,12 +124,61 @@ class SettingsController extends Controller
public function privacy()
{
return view('settings.privacy');
$settings = Auth::user()->settings;
$is_private = Auth::user()->profile->is_private;
$settings['is_private'] = (bool) $is_private;
return view('settings.privacy', compact('settings'));
}
public function privacyStore(Request $request)
{
$settings = Auth::user()->settings;
$profile = Auth::user()->profile;
$fields = [
'is_private',
'crawlable',
];
foreach($fields as $field) {
$form = $request->input($field);
if($field == 'is_private') {
if($form == 'on') {
$profile->{$field} = true;
$settings->show_guests = false;
$settings->show_discover = false;
$profile->save();
} else {
$profile->{$field} = false;
$profile->save();
}
} elseif($field == 'crawlable') {
if($form == 'on') {
$settings->{$field} = false;
} else {
$settings->{$field} = true;
}
} else {
if($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
}
$settings->save();
}
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
}
public function security()
{
return view('settings.security');
$sessions = DB::table('sessions')
->whereUserId(Auth::id())
->limit(20)
->get();
$activity = AccountLog::whereUserId(Auth::id())
->orderBy('created_at','desc')
->limit(50)
->get();
return view('settings.security', compact('sessions', 'activity'));
}
public function applications()
@ -121,7 +198,7 @@ class SettingsController extends Controller
public function dataImportInstagram()
{
return view('settings.import.ig');
return view('settings.import.instagram.home');
}
public function developers()

View file

@ -2,11 +2,39 @@
namespace App\Http\Controllers;
use App;
use App, Auth;
use Illuminate\Http\Request;
use App\{Follower, Status, User};
class SiteController extends Controller
{
public function home()
{
if(Auth::check()) {
return $this->homeTimeline();
} else {
return $this->homeGuest();
}
}
public function homeGuest()
{
return view('site.index');
}
public function homeTimeline()
{
// TODO: Use redis for timelines
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
$following->push(Auth::user()->profile->id);
$timeline = Status::whereIn('profile_id', $following)
->orderBy('id','desc')
->withCount(['comments', 'likes', 'shares'])
->simplePaginate(10);
return view('timeline.template', compact('timeline'));
}
public function changeLocale(Request $request, $locale)
{
if(!App::isLocale($locale)) {

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use Auth, Cache;
use App\Jobs\StatusPipeline\{NewStatusPipeline, StatusDelete};
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use Illuminate\Http\Request;
use App\{Media, Profile, Status, User};
use Vinkla\Hashids\Facades\Hashids;
@ -14,7 +15,7 @@ class StatusController extends Controller
{
$user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id)
->withCount(['likes', 'comments'])
->withCount(['likes', 'comments', 'media'])
->findOrFail($id);
if(!$status->media_path && $status->in_reply_to_id) {
return redirect($status->url());
@ -32,36 +33,51 @@ class StatusController extends Controller
$user = Auth::user();
$this->validate($request, [
'photo' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
'cw' => 'nullable|string'
'cw' => 'nullable|string',
'filter_class' => 'nullable|string',
'filter_name' => 'nullable|string',
]);
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
}
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
$monthHash = hash('sha1', date('Y') . date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);
$storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $request->photo->store($storagePath);
$profile = $user->profile;
$status = new Status;
$status->profile_id = $profile->id;
$status->caption = $request->caption;
$status->caption = strip_tags($request->caption);
$status->is_nsfw = $cw;
$status->save();
$media = new Media;
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->size = $request->file('photo')->getClientSize();
$media->mime = $request->file('photo')->getClientMimeType();
$media->save();
NewStatusPipeline::dispatch($status, $media);
$photos = $request->file('photo');
$order = 1;
foreach ($photos as $k => $v) {
$storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $v->store($storagePath);
$media = new Media;
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$media->user_id = $user->id;
$media->media_path = $path;
$media->size = $v->getClientSize();
$media->mime = $v->getClientMimeType();
$media->filter_class = $request->input('filter_class');
$media->filter_name = $request->input('filter_name');
$media->order = $order;
$media->save();
ImageOptimize::dispatch($media);
$order++;
}
NewStatusPipeline::dispatch($status);
// TODO: Parse Caption
// TODO: Send to subscribers
return redirect($status->url());

View file

@ -60,5 +60,6 @@ class Kernel extends HttpKernel
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'validemail' => \App\Http\Middleware\EmailVerificationCheck::class,
];
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Auth, Closure;
class EmailVerificationCheck
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if($request->user() &&
config('pixelfed.enforce_email_verification') &&
is_null($request->user()->email_verified_at) &&
!$request->is('i/verify-email') && !$request->is('log*') &&
!$request->is('i/confirm-email/*')
) {
return redirect('/i/verify-email');
}
return $next($request);
}
}

View file

@ -4,7 +4,7 @@ namespace App;
use Illuminate\Database\Eloquent\Model;
class Report extends Model
class ImportJob extends Model
{
//
}

View file

@ -38,7 +38,10 @@ class ImageUpdate implements ShouldQueue
$thumb = storage_path('app/'. $media->thumbnail_path);
try {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);
if($media->mime !== 'image/gif')
{
ImageOptimizer::optimize($path);
}
} catch (Exception $e) {
return;
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Jobs\InboxPipeline;
use App\Profile;
use App\Util\ActivityPub\Inbox;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class InboxWorker implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $request;
protected $profile;
protected $payload;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($request, Profile $profile, $payload)
{
$this->request = $request;
$this->profile = $profile;
$this->payload = $payload;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
(new Inbox($this->request, $this->profile, $this->payload))->handle();
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Jobs\InboxPipeline;
use App\Profile;
use App\Util\ActivityPub\Inbox;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class SharedInboxWorker implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $request;
protected $profile;
protected $payload;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($request, $payload)
{
$this->request = $request;
$this->payload = $payload;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
(new Inbox($this->request, null, $this->payload))->handleSharedInbox();
}
}

View file

@ -37,6 +37,11 @@ class LikePipeline implements ShouldQueue
$status = $this->like->status;
$actor = $this->like->actor;
if($status->url !== null) {
// Ignore notifications to remote statuses
return;
}
$exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id)
->whereAction('like')

View file

@ -16,17 +16,15 @@ class NewStatusPipeline implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
protected $media;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status, $media = false)
public function __construct(Status $status)
{
$this->status = $status;
$this->media = $media;
}
/**
@ -37,13 +35,10 @@ class NewStatusPipeline implements ShouldQueue
public function handle()
{
$status = $this->status;
$media = $this->media;
StatusEntityLexer::dispatch($status);
StatusActivityPubDeliver::dispatch($status);
if($media) {
ImageOptimize::dispatch($media);
}
//StatusActivityPubDeliver::dispatch($status);
Cache::forever('post.' . $status->id, $status);
$redis = Redis::connection();

View file

@ -2,7 +2,7 @@
namespace App\Jobs\StatusPipeline;
use App\{Media, StatusHashtag, Status};
use App\{Media, Notification, StatusHashtag, Status};
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
@ -58,8 +58,19 @@ class StatusDelete implements ShouldQueue
}
}
$comments = Status::where('in_reply_to_id', $status->id)->get();
foreach($comments as $comment) {
$comment->in_reply_to_id = null;
$comment->save();
Notification::whereItemType('App\Status')
->whereItemId($comment->id)
->delete();
}
$status->likes()->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->delete();
StatusHashtag::whereStatusId($status->id)->delete();
$status->delete();

View file

@ -2,25 +2,32 @@
namespace App\Jobs\StatusPipeline;
use Cache;
use DB, Cache;
use App\{
Hashtag,
Media,
Mention,
Profile,
Status,
StatusHashtag
};
use App\Util\Lexer\Hashtag as HashtagLexer;
use App\Util\Lexer\{Autolink, Extractor};
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Jobs\MentionPipeline\MentionPipeline;
class StatusEntityLexer implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
protected $entities;
protected $autolink;
/**
* Create a new job instance.
*
@ -39,36 +46,77 @@ class StatusEntityLexer implements ShouldQueue
public function handle()
{
$status = $this->status;
$this->parseHashtags();
$this->parseEntities();
}
public function parseHashtags()
public function parseEntities()
{
$this->extractEntities();
}
public function extractEntities()
{
$this->entities = Extractor::create()->extract($this->status->caption);
$this->autolinkStatus();
}
public function autolinkStatus()
{
$this->autolink = Autolink::create()->autolink($this->status->caption);
$this->storeEntities();
}
public function storeEntities()
{
$this->storeHashtags();
$this->storeMentions();
DB::transaction(function () {
$status = $this->status;
$status->rendered = $this->autolink;
$status->entities = json_encode($this->entities);
$status->save();
});
}
public function storeHashtags()
{
$tags = array_unique($this->entities['hashtags']);
$status = $this->status;
$text = e($status->caption);
$tags = HashtagLexer::getHashtags($text);
$rendered = $text;
if(count($tags) > 0) {
$rendered = HashtagLexer::replaceHashtagsWithLinks($text);
}
$status->rendered = $rendered;
$status->save();
Cache::forever('post.' . $status->id, $status);
foreach($tags as $tag) {
$slug = str_slug($tag);
$htag = Hashtag::firstOrCreate(
['name' => $tag],
['slug' => $slug]
);
$stag = new StatusHashtag;
$stag->status_id = $status->id;
$stag->hashtag_id = $htag->id;
$stag->save();
DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag);
$hashtag = Hashtag::firstOrCreate(
['name' => $tag, 'slug' => $slug]
);
StatusHashtag::firstOrCreate(
['status_id' => $status->id, 'hashtag_id' => $hashtag->id]
);
});
}
}
public function storeMentions()
{
$mentions = array_unique($this->entities['mentions']);
$status = $this->status;
foreach($mentions as $mention) {
$mentioned = Profile::whereUsername($mention)->firstOrFail();
if(empty($mentioned) || !isset($mentioned->id)) {
continue;
}
DB::transaction(function () use ($status, $mentioned) {
$m = new Mention;
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
});
MentionPipeline::dispatch($status, $m);
}
}
}

View file

@ -3,9 +3,19 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Like extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function actor()
{
return $this->belongsTo(Profile::class, 'profile_id', 'id');

34
app/Mail/ConfirmEmail.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace App\Mail;
use App\EmailVerification;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class ConfirmEmail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(EmailVerification $verify)
{
$this->verify = $verify;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown('emails.confirm_email')->with(['verify'=>$this->verify]);
}
}

View file

@ -2,15 +2,32 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Storage;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Media extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function url()
{
$path = $this->media_path;
$url = Storage::url($path);
return url($url);
}
public function thumbnailUrl()
{
$path = $this->thumbnail_path;
$url = Storage::url($path);
return url($url);
}
}

View file

@ -3,9 +3,18 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Mention extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function profile()
{

View file

@ -3,28 +3,37 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Notification extends Model
{
use SoftDeletes;
public function actor()
{
return $this->belongsTo(Profile::class, 'actor_id', 'id');
}
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function actor()
{
return $this->belongsTo(Profile::class, 'actor_id', 'id');
}
public function profile()
{
return $this->belongsTo(Profile::class, 'profile_id', 'id');
}
public function profile()
{
return $this->belongsTo(Profile::class, 'profile_id', 'id');
}
public function item()
{
return $this->morphTo();
}
public function item()
{
return $this->morphTo();
}
public function status()
{
return $this->belongsTo(Status::class, 'item_id', 'id');
}
public function status()
{
return $this->belongsTo(Status::class, 'item_id', 'id');
}
}

View file

@ -2,7 +2,7 @@
namespace App\Observers;
use App\{Profile, User};
use App\{Profile, User, UserSetting};
use App\Jobs\AvatarPipeline\CreateAvatar;
class UserObserver
@ -36,6 +36,12 @@ class UserObserver
CreateAvatar::dispatch($profile);
}
if(empty($user->settings)) {
$settings = new UserSetting;
$settings->user_id = $user->id;
$settings->save();
}
}
}

View file

@ -5,9 +5,18 @@ namespace App;
use Storage;
use App\Util\Lexer\PrettyNumber;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Profile extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
protected $hidden = [
'private_key',
];
@ -20,6 +29,15 @@ class Profile extends Model
}
public function url($suffix = '')
{
if($this->remote_url) {
return $this->remote_url;
} else {
return url($this->username . $suffix);
}
}
public function localUrl($suffix = '')
{
return url($this->username . $suffix);
}
@ -115,4 +133,9 @@ class Profile extends Model
$url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png'));
return $url;
}
public function statusCount()
{
return $this->statuses()->whereHas('media')->count();
}
}

View file

@ -16,6 +16,9 @@ class EventServiceProvider extends ServiceProvider
'App\Events\Event' => [
'App\Listeners\EventListener',
],
'auth.login' => [
'App\Events\AuthLoginEvent',
],
];
/**

10
app/ReportComment.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ReportComment extends Model
{
//
}

10
app/ReportLog.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ReportLog extends Model
{
//
}

View file

@ -2,12 +2,21 @@
namespace App;
use Auth, Storage;
use Illuminate\Database\Eloquent\Model;
use Storage;
use Vinkla\Hashids\Facades\Hashids;
use Illuminate\Database\Eloquent\SoftDeletes;
class Status extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function profile()
{
return $this->belongsTo(Profile::class);
@ -25,7 +34,7 @@ class Status extends Model
public function thumb()
{
if($this->media->count() == 0) {
if($this->media->count() == 0 || $this->is_nsfw) {
return "data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
}
return url(Storage::url($this->firstMedia()->thumbnail_path));
@ -43,6 +52,11 @@ class Status extends Model
return url($path);
}
public function editUrl()
{
return $this->url() . '/edit';
}
public function mediaUrl()
{
$media = $this->firstMedia();
@ -57,15 +71,39 @@ class Status extends Model
return $this->hasMany(Like::class);
}
public function liked() : bool
{
$profile = Auth::user()->profile;
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
}
public function comments()
{
return $this->hasMany(Status::class, 'in_reply_to_id');
}
public function bookmarked()
{
$profile = Auth::user()->profile;
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
}
public function shares()
{
return $this->hasMany(Status::class, 'reblog_of_id');
}
public function shared() : bool
{
$profile = Auth::user()->profile;
return Status::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
}
public function parent()
{
if(!empty($this->in_reply_to_id)) {
return Status::findOrFail($this->in_reply_to_id);
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
if(!empty($parent)) {
return Status::findOrFail($parent);
}
}
@ -86,6 +124,23 @@ class Status extends Model
);
}
public function mentions()
{
return $this->hasManyThrough(
Profile::class,
Mention::class,
'status_id',
'id',
'id',
'profile_id'
);
}
public function reportUrl()
{
return route('report.form') . "?type=post&id={$this->id}";
}
public function toActivityStream()
{
$media = $this->media;

View file

@ -6,5 +6,5 @@ use Illuminate\Database\Eloquent\Model;
class StatusHashtag extends Model
{
protected $fillable = ['status_id', 'hashtag_id'];
public $fillable = ['status_id', 'hashtag_id'];
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Transformer\Api;
use App\Profile;
use League\Fractal;
class AccountTransformer extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
return [
'id' => $profile->id,
'username' => $profile->username,
'acct' => $profile->username,
'display_name' => $profile->name,
'locked' => (bool) $profile->is_private,
'created_at' => $profile->created_at->format('c'),
'followers_count' => $profile->followerCount(),
'following_count' => $profile->followingCount(),
'statuses_count' => $profile->statusCount(),
'note' => $profile->bio,
'url' => $profile->url(),
'avatar' => $profile->avatarUrl(),
'avatar_static' => $profile->avatarUrl(),
'header' => '',
'header_static' => '',
'moved' => null,
'fields' => null,
'bot' => null
];
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Transformer\Api;
use League\Fractal;
class ApplicationTransformer extends Fractal\TransformerAbstract
{
public function transform()
{
return [
'name' => '',
'website' => null
];
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Transformer\Api;
use App\Hashtag;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class HashtagTransformer extends Fractal\TransformerAbstract
{
public function transform(Hashtag $hashtag)
{
return [
'name' => $hashtag->name,
'url' => $hashtag->url(),
];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Transformer\Api;
use App\Media;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class MediaTransformer extends Fractal\TransformerAbstract
{
public function transform(Media $media)
{
return [
'id' => $media->id,
'type' => 'image',
'url' => $media->url(),
'remote_url' => null,
'preview_url' => $media->thumbnailUrl(),
'text_url' => null,
'meta' => null,
'description' => null
];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Transformer\Api;
use App\Profile;
use League\Fractal;
class MentionTransformer extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
return [
'id' => $profile->id,
'url' => $profile->url(),
'username' => $profile->username,
'acct' => $profile->username,
];
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Transformer\Api;
use App\Status;
use League\Fractal;
class StatusTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
'account',
'mentions',
'media_attachments',
'tags'
];
public function transform(Status $status)
{
return [
'id' => $status->id,
'uri' => $status->url(),
'url' => $status->url(),
'in_reply_to_id' => $status->in_reply_to_id,
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
// TODO: fixme
'reblog' => null,
'content' => "<p>$status->rendered</p>",
'created_at' => $status->created_at->format('c'),
'emojis' => [],
'reblogs_count' => $status->shares()->count(),
'favourites_count' => $status->likes()->count(),
'reblogged' => $status->shared(),
'favourited' => $status->liked(),
'muted' => null,
'sensitive' => (bool) $status->is_nsfw,
'spoiler_text' => '',
'visibility' => $status->visibility,
'application' => null,
'language' => null,
'pinned' => null
];
}
public function includeAccount(Status $status)
{
$account = $status->profile;
return $this->item($account, new AccountTransformer);
}
public function includeMentions(Status $status)
{
$mentions = $status->mentions;
return $this->collection($mentions, new MentionTransformer);
}
public function includeMediaAttachments(Status $status)
{
$media = $status->media;
return $this->collection($media, new MediaTransformer);
}
public function includeTags(Status $status)
{
$tags = $status->hashtags;
return $this->collection($tags, new HashtagTransformer);
}
}

View file

@ -3,11 +3,19 @@
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
use Notifiable, SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at', 'email_verified_at'];
/**
* The attributes that are mass assignable.
@ -36,4 +44,9 @@ class User extends Authenticatable
{
return url(config('app.url') . '/' . $this->username);
}
public function settings()
{
return $this->hasOne(UserSetting::class);
}
}

10
app/UserFilter.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class UserFilter extends Model
{
//
}

10
app/UserSetting.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class UserSetting extends Model
{
//
}

View file

@ -118,6 +118,7 @@ class RestrictedNames {
// Static Assets
"assets",
"storage",
// Laravel Horizon
"horizon",
@ -127,14 +128,19 @@ class RestrictedNames {
"api",
"auth",
"i",
"dashboard",
"discover",
"docs",
"home",
"login",
"logout",
"media",
"p",
"password",
"reports",
"search",
"settings",
"statuses",
"site",
"timeline",
"user",

View file

@ -103,6 +103,10 @@ class Image {
$ratio = $this->getAspectRatio($file, $thumbnail);
$aspect = $ratio['dimensions'];
$orientation = $ratio['orientation'];
if($media->mime === 'image/gif' && !$thumbnail)
{
return;
}
try {
$img = Intervention::make($file)->orientate();

View file

@ -31,13 +31,9 @@ class Webfinger {
public function generateAliases()
{
$host = parse_url(config('app.url'), PHP_URL_HOST);
$username = $this->user->username;
$url = $this->user->url();
$this->aliases = [
'acct:'.$username.'@'.$host,
$url
$this->user->url(),
$this->user->permalink()
];
return $this;
}
@ -55,24 +51,12 @@ class Webfinger {
[
'rel' => 'http://schemas.google.com/g/2010#updates-from',
'type' => 'application/atom+xml',
'href' => url("/users/{$user->username}.atom")
'href' => $user->permalink('.atom')
],
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $user->permalink()
],
[
'rel' => 'magic-public-key',
'href' => null//$user->public_key
],
[
'rel' => 'salmon',
'href' => $user->permalink('/salmon')
],
[
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
'href' => url('/main/ostatussub?profile={uri}')
]
];
return $this;

View file

@ -6,18 +6,23 @@
"type": "project",
"require": {
"php": "^7.1.3",
"99designs/http-signatures-guzzlehttp": "^2.0",
"beyondcode/laravel-self-diagnosis": "^0.4.0",
"bitverse/identicon": "^1.1",
"doctrine/dbal": "^2.7",
"fideloper/proxy": "^4.0",
"greggilbert/recaptcha": "dev-master",
"intervention/image": "^2.4",
"kitetail/zttp": "^0.3.0",
"pixelfed/zttp": "^0.4",
"laravel/framework": "5.6.*",
"laravel/horizon": "^1.2",
"laravel/passport": "^6.0",
"laravel/tinker": "^1.0",
"league/fractal": "^0.17.0",
"moontoast/math": "^1.1",
"phpseclib/phpseclib": "~2.0",
"pixelfed/dotenv-editor": "^2.0",
"pixelfed/fractal": "^0.18.0",
"pixelfed/google2fa-laravel": "^2.0",
"pixelfed/http-signatures-guzzlehttp": "^4.0",
"predis/predis": "^1.1",
"spatie/laravel-backup": "^5.0.0",
"spatie/laravel-image-optimizer": "^1.1",
@ -25,6 +30,7 @@
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.1",
"beyondcode/laravel-er-diagram-generator": "^0.2.2",
"filp/whoops": "^2.0",
"fzaninotto/faker": "^1.4",
"mockery/mockery": "^1.0",

2148
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,7 @@ return [
|
*/
'timezone' => 'UTC',
'timezone' => env('APP_TIMEZONE', 'UTC'),
/*
|--------------------------------------------------------------------------
@ -78,7 +78,7 @@ return [
|
*/
'locale' => 'en',
'locale' => env('APP_LOCALE', 'en'),
/*
|--------------------------------------------------------------------------
@ -91,7 +91,7 @@ return [
|
*/
'fallback_locale' => 'en',
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
/*
|--------------------------------------------------------------------------
@ -151,6 +151,7 @@ return [
* Package Service Providers...
*/
Greggilbert\Recaptcha\RecaptchaServiceProvider::class,
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
/*
* Application Service Providers...
@ -211,6 +212,7 @@ return [
'View' => Illuminate\Support\Facades\View::class,
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::class,
'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
],
];

27
config/dotenv-editor.php Normal file
View file

@ -0,0 +1,27 @@
<?php
return array(
/*
|----------------------------------------------------------------------
| Auto backup mode
|----------------------------------------------------------------------
|
| This value is used when you save your file content. If value is true,
| the original file will be backed up before save.
*/
'autoBackup' => true,
/*
|----------------------------------------------------------------------
| Backup location
|----------------------------------------------------------------------
|
| This value is used when you backup your file. This value is the sub
| path from root folder of project application.
*/
'backupPath' => base_path('storage/dotenv-editor/backups/')
);

View file

@ -49,5 +49,5 @@ return [
* If set to `true` all output of the optimizer binaries will be appended to the default log.
* You can also set this to a class that implements `Psr\Log\LoggerInterface`.
*/
'log_optimizer_activity' => true,
'log_optimizer_activity' => false,
];

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.1.0',
'version' => '0.1.2',
/*
|--------------------------------------------------------------------------
@ -77,6 +77,17 @@ return [
'activitypub_enabled' => env('ACTIVITY_PUB', false),
/*
|--------------------------------------------------------------------------
| Account file size limit
|--------------------------------------------------------------------------
|
| Update the max account size, the per user limit of files in KB.
|
|
*/
'max_account_size' => env('MAX_ACCOUNT_SIZE', 100000),
/*
|--------------------------------------------------------------------------
| Photo file size limit
@ -96,5 +107,25 @@ return [
|
*/
'max_caption_length' => env('MAX_CAPTION_LENGTH', 150),
/*
|--------------------------------------------------------------------------
| Album size limit
|--------------------------------------------------------------------------
|
| The max number of photos allowed per post.
|
*/
'max_album_length' => env('MAX_ALBUM_LENGTH', 4),
/*
|--------------------------------------------------------------------------
| Email Verification
|--------------------------------------------------------------------------
|
| Require email verification before a new user can do anything.
|
*/
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
];

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateWebSubsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('web_subs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('follower_id')->unsigned()->index();
$table->bigInteger('following_id')->unsigned()->index();
$table->string('profile_url')->index();
$table->timestamp('approved_at')->nullable();
$table->unique(['follower_id', 'following_id', 'profile_url']);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('web_subs');
}
}

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateImportJobsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('import_jobs', function (Blueprint $table) {
$table->increments('id');
$table->bigInteger('profile_id')->unsigned();
$table->string('service')->default('instagram');
$table->string('uuid')->nullable();
$table->string('storage_path')->nullable();
$table->tinyInteger('stage')->unsigned()->default(0);
$table->text('media_json')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('import_jobs');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddFiltersToMediaTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('media', function (Blueprint $table) {
$table->string('filter_name')->nullable()->after('orientation');
$table->string('filter_class')->nullable()->after('filter_name');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('media', function (Blueprint $table) {
$table->dropColumn('filter_name');
$table->dropColumn('filter_class');
});
}
}

View file

@ -0,0 +1,58 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSoftDeletesToModels extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('avatars', function ($table) {
$table->softDeletes();
});
Schema::table('likes', function ($table) {
$table->softDeletes();
});
Schema::table('media', function ($table) {
$table->softDeletes();
});
Schema::table('mentions', function ($table) {
$table->softDeletes();
});
Schema::table('notifications', function ($table) {
$table->softDeletes();
});
Schema::table('profiles', function ($table) {
$table->softDeletes();
});
Schema::table('statuses', function ($table) {
$table->softDeletes();
});
Schema::table('users', function ($table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateEmailVerificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('email_verifications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned();
$table->string('email')->nullable();
$table->string('user_token')->index();
$table->string('random_token')->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('email_verifications');
}
}

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateReportCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('report_comments', function (Blueprint $table) {
$table->increments('id');
$table->bigInteger('report_id')->unsigned()->index();
$table->bigInteger('profile_id')->unsigned();
$table->bigInteger('user_id')->unsigned();
$table->text('comment');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('report_comments');
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateReportLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('report_logs', function (Blueprint $table) {
$table->increments('id');
$table->bigInteger('profile_id')->unsigned();
$table->bigInteger('item_id')->unsigned()->nullable();
$table->string('item_type')->nullable();
$table->string('action')->nullable();
$table->boolean('system_message')->default(false);
$table->json('metadata')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('report_logs');
}
}

View file

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAccountLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('account_logs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned()->index();
$table->bigInteger('item_id')->unsigned()->nullable();
$table->string('item_type')->nullable();
$table->string('action')->nullable();
$table->string('message')->nullable();
$table->string('link')->nullable();
$table->string('ip_address')->nullable();
$table->string('user_agent')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('account_logs');
}
}

View file

@ -0,0 +1,50 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUserSettingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_settings', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned()->unique();
$table->string('role')->default('user');
$table->boolean('crawlable')->default(true);
$table->boolean('show_guests')->default(true);
$table->boolean('show_discover')->default(true);
$table->boolean('public_dm')->default(false);
$table->boolean('hide_cw_search')->default(true);
$table->boolean('hide_blocked_search')->default(true);
$table->boolean('always_show_cw')->default(false);
$table->boolean('compose_media_descriptions')->default(false);
$table->boolean('reduce_motion')->default(false);
$table->boolean('optimize_screen_reader')->default(false);
$table->boolean('high_contrast_mode')->default(false);
$table->boolean('video_autoplay')->default(false);
$table->boolean('send_email_new_follower')->default(false);
$table->boolean('send_email_new_follower_request')->default(true);
$table->boolean('send_email_on_share')->default(false);
$table->boolean('send_email_on_like')->default(false);
$table->boolean('send_email_on_mention')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_settings');
}
}

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Add2faToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('2fa_enabled')->default(false);
$table->string('2fa_secret')->nullable();
$table->json('2fa_backup_codes')->nullable();
$table->timestamp('2fa_setup_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('2fa_enabled');
$table->dropColumn('2fa_secret');
$table->dropColumn('2fa_backup_codes');
$table->dropColumn('2fa_setup_at');
});
}
}

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUserFiltersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_filters', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned()->index();
$table->bigInteger('filterable_id')->unsigned();
$table->string('filterable_type');
$table->string('filter_type')->default('block')->index();
$table->unique([
'user_id',
'filterable_id',
'filterable_type',
'filter_type'
], 'filter_unique');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_filters');
}
}

View file

@ -46,7 +46,7 @@ services:
- "mysql-data:/var/lib/mysql"
redis:
image: redis:alpine
image: redis:4-alpine
volumes:
- "redis-data:/data"
networks:

BIN
public/css/app.css vendored

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -22,6 +22,7 @@ try {
require('./components/commentform');
require('./components/searchform');
require('./components/bookmarkform');
require('./components/statusform');
} catch (e) {}
/**

View file

@ -14,12 +14,14 @@ $(document).ready(function() {
let commenttext = commentform.val();
let item = {item: id, comment: commenttext};
commentform.prop('disabled', true);
axios.post('/i/comment', item)
.then(function (res) {
var username = res.data.username;
var permalink = res.data.url;
var profile = res.data.profile;
var reply = res.data.comment;
if($('.status-container').length == 1) {
var comments = el.parents().eq(3).find('.comments');
@ -27,12 +29,13 @@ $(document).ready(function() {
var comments = el.parents().eq(1).find('.comments');
}
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ commenttext + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ reply + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
comments.prepend(comment);
commentform.val('');
commentform.blur();
commentform.prop('disabled', false);
})
.catch(function (res) {

View file

@ -0,0 +1,107 @@
<style scoped>
.action-link {
cursor: pointer;
}
</style>
<template>
<div>
<div v-if="tokens.length > 0">
<div class="card card-default mb-4">
<div class="card-header font-weight-bold bg-white">Authorized Applications</div>
<div class="card-body">
<!-- Authorized Tokens -->
<table class="table table-borderless mb-0">
<thead>
<tr>
<th>Name</th>
<th>Scopes</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="token in tokens">
<!-- Client Name -->
<td style="vertical-align: middle;">
{{ token.client.name }}
</td>
<!-- Scopes -->
<td style="vertical-align: middle;">
<span v-if="token.scopes.length > 0">
{{ token.scopes.join(', ') }}
</span>
</td>
<!-- Revoke Button -->
<td style="vertical-align: middle;">
<a class="action-link text-danger" @click="revoke(token)">
Revoke
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
/*
* The component's data.
*/
data() {
return {
tokens: []
};
},
/**
* Prepare the component (Vue 1.x).
*/
ready() {
this.prepareComponent();
},
/**
* Prepare the component (Vue 2.x).
*/
mounted() {
this.prepareComponent();
},
methods: {
/**
* Prepare the component (Vue 2.x).
*/
prepareComponent() {
this.getTokens();
},
/**
* Get all of the authorized tokens for the user.
*/
getTokens() {
axios.get('/oauth/tokens')
.then(response => {
this.tokens = response.data;
});
},
/**
* Revoke the given token.
*/
revoke(token) {
axios.delete('/oauth/tokens/' + token.id)
.then(response => {
this.getTokens();
});
}
}
}
</script>

View file

@ -0,0 +1,350 @@
<style scoped>
.action-link {
cursor: pointer;
}
</style>
<template>
<div>
<div class="card card-default mb-4">
<div class="card-header font-weight-bold bg-white">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>
OAuth Clients
</span>
<a class="action-link" tabindex="-1" @click="showCreateClientForm">
Create New Client
</a>
</div>
</div>
<div class="card-body">
<!-- Current Clients -->
<p class="mb-0" v-if="clients.length === 0">
You have not created any OAuth clients.
</p>
<table class="table table-borderless mb-0" v-if="clients.length > 0">
<thead>
<tr>
<th>Client ID</th>
<th>Name</th>
<th>Secret</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="client in clients">
<!-- ID -->
<td style="vertical-align: middle;">
{{ client.id }}
</td>
<!-- Name -->
<td style="vertical-align: middle;">
{{ client.name }}
</td>
<!-- Secret -->
<td style="vertical-align: middle;">
<code>{{ client.secret }}</code>
</td>
<!-- Edit Button -->
<td style="vertical-align: middle;">
<a class="action-link" tabindex="-1" @click="edit(client)">
Edit
</a>
</td>
<!-- Delete Button -->
<td style="vertical-align: middle;">
<a class="action-link text-danger" @click="destroy(client)">
Delete
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Create Client Modal -->
<div class="modal fade" id="modal-create-client" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
Create Client
</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<!-- Form Errors -->
<div class="alert alert-danger" v-if="createForm.errors.length > 0">
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
<br>
<ul>
<li v-for="error in createForm.errors">
{{ error }}
</li>
</ul>
</div>
<!-- Create Client Form -->
<form role="form">
<!-- Name -->
<div class="form-group row">
<label class="col-md-3 col-form-label">Name</label>
<div class="col-md-9">
<input id="create-client-name" type="text" class="form-control" autocomplete="off"
@keyup.enter="store" v-model="createForm.name">
<span class="form-text text-muted">
Something your users will recognize and trust.
</span>
</div>
</div>
<!-- Redirect URL -->
<div class="form-group row">
<label class="col-md-3 col-form-label">Redirect URL</label>
<div class="col-md-9">
<input type="text" class="form-control" name="redirect"
@keyup.enter="store" v-model="createForm.redirect">
<span class="form-text text-muted">
Your application's authorization callback URL.
</span>
</div>
</div>
</form>
</div>
<!-- Modal Actions -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary font-weight-bold" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary font-weight-bold" @click="store">
Create
</button>
</div>
</div>
</div>
</div>
<!-- Edit Client Modal -->
<div class="modal fade" id="modal-edit-client" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
Edit Client
</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<!-- Form Errors -->
<div class="alert alert-danger" v-if="editForm.errors.length > 0">
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
<br>
<ul>
<li v-for="error in editForm.errors">
{{ error }}
</li>
</ul>
</div>
<!-- Edit Client Form -->
<form role="form">
<!-- Name -->
<div class="form-group row">
<label class="col-md-3 col-form-label">Name</label>
<div class="col-md-9">
<input id="edit-client-name" type="text" class="form-control"
@keyup.enter="update" v-model="editForm.name">
<span class="form-text text-muted">
Something your users will recognize and trust.
</span>
</div>
</div>
<!-- Redirect URL -->
<div class="form-group row">
<label class="col-md-3 col-form-label">Redirect URL</label>
<div class="col-md-9">
<input type="text" class="form-control" name="redirect"
@keyup.enter="update" v-model="editForm.redirect">
<span class="form-text text-muted">
Your application's authorization callback URL.
</span>
</div>
</div>
</form>
</div>
<!-- Modal Actions -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="update">
Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
/*
* The component's data.
*/
data() {
return {
clients: [],
createForm: {
errors: [],
name: '',
redirect: ''
},
editForm: {
errors: [],
name: '',
redirect: ''
}
};
},
/**
* Prepare the component (Vue 1.x).
*/
ready() {
this.prepareComponent();
},
/**
* Prepare the component (Vue 2.x).
*/
mounted() {
this.prepareComponent();
},
methods: {
/**
* Prepare the component.
*/
prepareComponent() {
this.getClients();
$('#modal-create-client').on('shown.bs.modal', () => {
$('#create-client-name').focus();
});
$('#modal-edit-client').on('shown.bs.modal', () => {
$('#edit-client-name').focus();
});
},
/**
* Get all of the OAuth clients for the user.
*/
getClients() {
axios.get('/oauth/clients')
.then(response => {
this.clients = response.data;
});
},
/**
* Show the form for creating new clients.
*/
showCreateClientForm() {
$('#modal-create-client').modal('show');
},
/**
* Create a new OAuth client for the user.
*/
store() {
this.persistClient(
'post', '/oauth/clients',
this.createForm, '#modal-create-client'
);
},
/**
* Edit the given client.
*/
edit(client) {
this.editForm.id = client.id;
this.editForm.name = client.name;
this.editForm.redirect = client.redirect;
$('#modal-edit-client').modal('show');
},
/**
* Update the client being edited.
*/
update() {
this.persistClient(
'put', '/oauth/clients/' + this.editForm.id,
this.editForm, '#modal-edit-client'
);
},
/**
* Persist the client to storage using the given form.
*/
persistClient(method, uri, form, modal) {
form.errors = [];
axios[method](uri, form)
.then(response => {
this.getClients();
form.name = '';
form.redirect = '';
form.errors = [];
$(modal).modal('hide');
})
.catch(error => {
if (typeof error.response.data === 'object') {
form.errors = _.flatten(_.toArray(error.response.data.errors));
} else {
form.errors = ['Something went wrong. Please try again.'];
}
});
},
/**
* Destroy the given client.
*/
destroy(client) {
axios.delete('/oauth/clients/' + client.id)
.then(response => {
this.getClients();
});
}
}
}
</script>

View file

@ -0,0 +1,298 @@
<style scoped>
.action-link {
cursor: pointer;
}
</style>
<template>
<div>
<div>
<div class="card card-default mb-4">
<div class="card-header font-weight-bold bg-white">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>
Personal Access Tokens
</span>
<a class="action-link" tabindex="-1" @click="showCreateTokenForm">
Create New Token
</a>
</div>
</div>
<div class="card-body">
<!-- No Tokens Notice -->
<p class="mb-0" v-if="tokens.length === 0">
You have not created any personal access tokens.
</p>
<!-- Personal Access Tokens -->
<table class="table table-borderless mb-0" v-if="tokens.length > 0">
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="token in tokens">
<!-- Client Name -->
<td style="vertical-align: middle;">
{{ token.name }}
</td>
<!-- Delete Button -->
<td style="vertical-align: middle;">
<a class="action-link text-danger" @click="revoke(token)">
Delete
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create Token Modal -->
<div class="modal fade" id="modal-create-token" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
Create Token
</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<!-- Form Errors -->
<div class="alert alert-danger" v-if="form.errors.length > 0">
<p class="mb-0"><strong>Whoops!</strong> Something went wrong!</p>
<br>
<ul>
<li v-for="error in form.errors">
{{ error }}
</li>
</ul>
</div>
<!-- Create Token Form -->
<form role="form" @submit.prevent="store">
<!-- Name -->
<div class="form-group row">
<label class="col-md-4 col-form-label">Name</label>
<div class="col-md-6">
<input id="create-token-name" type="text" class="form-control" name="name" v-model="form.name" autocomplete="off">
</div>
</div>
<!-- Scopes -->
<div class="form-group row" v-if="scopes.length > 0">
<label class="col-md-4 col-form-label">Scopes</label>
<div class="col-md-6">
<div v-for="scope in scopes">
<div class="checkbox">
<label>
<input type="checkbox"
@click="toggleScope(scope.id)"
:checked="scopeIsAssigned(scope.id)">
{{ scope.id }}
</label>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Modal Actions -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary font-weight-bold" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary font-weight-bold" @click="store">
Create
</button>
</div>
</div>
</div>
</div>
<!-- Access Token Modal -->
<div class="modal fade" id="modal-access-token" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
Personal Access Token
</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<p>
Here is your new personal access token. This is the only time it will be shown so don't lose it!
You may now use this token to make API requests.
</p>
<textarea class="form-control" rows="10">{{ accessToken }}</textarea>
</div>
<!-- Modal Actions -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
/*
* The component's data.
*/
data() {
return {
accessToken: null,
tokens: [],
scopes: [],
form: {
name: '',
scopes: [],
errors: []
}
};
},
/**
* Prepare the component (Vue 1.x).
*/
ready() {
this.prepareComponent();
},
/**
* Prepare the component (Vue 2.x).
*/
mounted() {
this.prepareComponent();
},
methods: {
/**
* Prepare the component.
*/
prepareComponent() {
this.getTokens();
this.getScopes();
$('#modal-create-token').on('shown.bs.modal', () => {
$('#create-token-name').focus();
});
},
/**
* Get all of the personal access tokens for the user.
*/
getTokens() {
axios.get('/oauth/personal-access-tokens')
.then(response => {
this.tokens = response.data;
});
},
/**
* Get all of the available scopes.
*/
getScopes() {
axios.get('/oauth/scopes')
.then(response => {
this.scopes = response.data;
});
},
/**
* Show the form for creating new tokens.
*/
showCreateTokenForm() {
$('#modal-create-token').modal('show');
},
/**
* Create a new personal access token.
*/
store() {
this.accessToken = null;
this.form.errors = [];
axios.post('/oauth/personal-access-tokens', this.form)
.then(response => {
this.form.name = '';
this.form.scopes = [];
this.form.errors = [];
this.tokens.push(response.data.token);
this.showAccessToken(response.data.accessToken);
})
.catch(error => {
if (typeof error.response.data === 'object') {
this.form.errors = _.flatten(_.toArray(error.response.data.errors));
} else {
this.form.errors = ['Something went wrong. Please try again.'];
}
});
},
/**
* Toggle the given scope in the list of assigned scopes.
*/
toggleScope(scope) {
if (this.scopeIsAssigned(scope)) {
this.form.scopes = _.reject(this.form.scopes, s => s == scope);
} else {
this.form.scopes.push(scope);
}
},
/**
* Determine if the given scope has been assigned to the token.
*/
scopeIsAssigned(scope) {
return _.indexOf(this.form.scopes, scope) >= 0;
},
/**
* Show the given access token to the user.
*/
showAccessToken(accessToken) {
$('#modal-create-token').modal('hide');
this.accessToken = accessToken;
$('#modal-access-token').modal('show');
},
/**
* Revoke the given token.
*/
revoke(token) {
axios.delete('/oauth/personal-access-tokens/' + token.id)
.then(response => {
this.getTokens();
});
}
}
}
</script>

View file

@ -0,0 +1,110 @@
$(document).ready(function() {
$('#statusForm .btn-filter-select').on('click', function(e) {
let el = $(this);
});
pixelfed.create = {};
pixelfed.filters = {};
pixelfed.create.hasGeneratedSelect = false;
pixelfed.create.selectedFilter = false;
pixelfed.create.currentFilterName = false;
pixelfed.create.currentFilterClass = false;
pixelfed.filters.list = [
['1977','filter-1977'],
['Aden','filter-aden'],
['Amaro','filter-amaro'],
['Ashby','filter-ashby'],
['Brannan','filter-brannan'],
['Brooklyn','filter-brooklyn'],
['Charmes','filter-charmes'],
['Clarendon','filter-clarendon'],
['Crema','filter-crema'],
['Dogpatch','filter-dogpatch'],
['Earlybird','filter-earlybird'],
['Gingham','filter-gingham'],
['Ginza','filter-ginza'],
['Hefe','filter-hefe'],
['Helena','filter-helena'],
['Hudson','filter-hudson'],
['Inkwell','filter-inkwell'],
['Kelvin','filter-kelvin'],
['Kuno','filter-juno'],
['Lark','filter-lark'],
['Lo-Fi','filter-lofi'],
['Ludwig','filter-ludwig'],
['Maven','filter-maven'],
['Mayfair','filter-mayfair'],
['Moon','filter-moon'],
['Nashville','filter-nashville'],
['Perpetua','filter-perpetua'],
['Poprocket','filter-poprocket'],
['Reyes','filter-reyes'],
['Rise','filter-rise'],
['Sierra','filter-sierra'],
['Skyline','filter-skyline'],
['Slumber','filter-slumber'],
['Stinson','filter-stinson'],
['Sutro','filter-sutro'],
['Toaster','filter-toaster'],
['Valencia','filter-valencia'],
['Vesper','filter-vesper'],
['Walden','filter-walden'],
['Willow','filter-willow'],
['X-Pro II','filter-xpro-ii']
];
function previewImage(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('.filterPreview').attr('src', e.target.result);
}
reader.readAsDataURL(input.files[0]);
}
}
function generateFilterSelect() {
let filters = pixelfed.filters.list;
for(var i = 0, len = filters.length; i < len; i++) {
let filter = filters[i];
let name = filter[0];
let className = filter[1];
let select = $('#filterSelectDropdown');
var template = '<option value="' + className + '">' + name + '</option>';
select.append(template);
}
pixelfed.create.hasGeneratedSelect = true;
}
$('#fileInput').on('change', function() {
previewImage(this);
$('#statusForm .form-filters.d-none').removeClass('d-none');
$('#statusForm .form-preview.d-none').removeClass('d-none');
$('#statusForm #collapsePreview').collapse('show');
if(!pixelfed.create.hasGeneratedSelect) {
generateFilterSelect();
}
});
$('#filterSelectDropdown').on('change', function() {
let el = $(this);
let filter = el.val();
let oldFilter = pixelfed.create.currentFilterClass;
if(filter == 'none') {
$('.filterContainer').removeClass(oldFilter);
pixelfed.create.currentFilterClass = false;
pixelfed.create.currentFilterName = 'None';
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
return;
}
$('.filterContainer').removeClass(oldFilter).addClass(filter);
pixelfed.create.currentFilterClass = filter;
pixelfed.create.currentFilterName = el.find(':selected').text();
$('.form-group.form-preview .form-text').text('Current Filter: ' + pixelfed.create.currentFilterName);
$('input[name=filter_class]').val(pixelfed.create.currentFilterClass);
$('input[name=filter_name]').val(pixelfed.create.currentFilterName);
});
});

View file

@ -11,6 +11,8 @@
@import "custom";
@import "components/filters";
@import "components/typeahead";
@import "components/notifications";

View file

@ -0,0 +1,445 @@
/*! Instagram.css v0.1.3 | MIT License | github.com/picturepan2/instagram.css */
[class*="filter"] {
position: relative;
}
[class*="filter"]::before {
display: block;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: 1;
}
.filter-1977 {
-webkit-filter: sepia(.5) hue-rotate(-30deg) saturate(1.4);
filter: sepia(.5) hue-rotate(-30deg) saturate(1.4);
}
.filter-aden {
-webkit-filter: sepia(.2) brightness(1.15) saturate(1.4);
filter: sepia(.2) brightness(1.15) saturate(1.4);
}
.filter-aden::before {
background: rgba(125, 105, 24, .1);
content: "";
mix-blend-mode: multiply;
}
.filter-amaro {
-webkit-filter: sepia(.35) contrast(1.1) brightness(1.2) saturate(1.3);
filter: sepia(.35) contrast(1.1) brightness(1.2) saturate(1.3);
}
.filter-amaro::before {
background: rgba(125, 105, 24, .2);
content: "";
mix-blend-mode: overlay;
}
.filter-ashby {
-webkit-filter: sepia(.5) contrast(1.2) saturate(1.8);
filter: sepia(.5) contrast(1.2) saturate(1.8);
}
.filter-ashby::before {
background: rgba(125, 105, 24, .35);
content: "";
mix-blend-mode: lighten;
}
.filter-brannan {
-webkit-filter: sepia(.4) contrast(1.25) brightness(1.1) saturate(.9) hue-rotate(-2deg);
filter: sepia(.4) contrast(1.25) brightness(1.1) saturate(.9) hue-rotate(-2deg);
}
.filter-brooklyn {
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.25) hue-rotate(5deg);
filter: sepia(.25) contrast(1.25) brightness(1.25) hue-rotate(5deg);
}
.filter-brooklyn::before {
background: rgba(127, 187, 227, .2);
content: "";
mix-blend-mode: overlay;
}
.filter-charmes {
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.25) saturate(1.35) hue-rotate(-5deg);
filter: sepia(.25) contrast(1.25) brightness(1.25) saturate(1.35) hue-rotate(-5deg);
}
.filter-charmes::before {
background: rgba(125, 105, 24, .25);
content: "";
mix-blend-mode: darken;
}
.filter-clarendon {
-webkit-filter: sepia(.15) contrast(1.25) brightness(1.25) hue-rotate(5deg);
filter: sepia(.15) contrast(1.25) brightness(1.25) hue-rotate(5deg);
}
.filter-clarendon::before {
background: rgba(127, 187, 227, .4);
content: "";
mix-blend-mode: overlay;
}
.filter-crema {
-webkit-filter: sepia(.5) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-2deg);
filter: sepia(.5) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-2deg);
}
.filter-crema::before {
background: rgba(125, 105, 24, .2);
content: "";
mix-blend-mode: multiply;
}
.filter-dogpatch {
-webkit-filter: sepia(.35) saturate(1.1) contrast(1.5);
filter: sepia(.35) saturate(1.1) contrast(1.5);
}
.filter-earlybird {
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-5deg);
filter: sepia(.25) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-5deg);
}
.filter-earlybird::before {
background: radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(125, 105, 24, .2) 100%);
content: "";
mix-blend-mode: multiply;
}
.filter-gingham {
-webkit-filter: contrast(1.1) brightness(1.1);
filter: contrast(1.1) brightness(1.1);
}
.filter-gingham::before {
background: #e6e6e6;
content: "";
mix-blend-mode: soft-light;
}
.filter-ginza {
-webkit-filter: sepia(.25) contrast(1.15) brightness(1.2) saturate(1.35) hue-rotate(-5deg);
filter: sepia(.25) contrast(1.15) brightness(1.2) saturate(1.35) hue-rotate(-5deg);
}
.filter-ginza::before {
background: rgba(125, 105, 24, .15);
content: "";
mix-blend-mode: darken;
}
.filter-hefe {
-webkit-filter: sepia(.4) contrast(1.5) brightness(1.2) saturate(1.4) hue-rotate(-10deg);
filter: sepia(.4) contrast(1.5) brightness(1.2) saturate(1.4) hue-rotate(-10deg);
}
.filter-hefe::before {
background: radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(0, 0, 0, .25) 100%);
content: "";
mix-blend-mode: multiply;
}
.filter-helena {
-webkit-filter: sepia(.5) contrast(1.05) brightness(1.05) saturate(1.35);
filter: sepia(.5) contrast(1.05) brightness(1.05) saturate(1.35);
}
.filter-helena::before {
background: rgba(158, 175, 30, .25);
content: "";
mix-blend-mode: overlay;
}
.filter-hudson {
-webkit-filter: sepia(.25) contrast(1.2) brightness(1.2) saturate(1.05) hue-rotate(-15deg);
filter: sepia(.25) contrast(1.2) brightness(1.2) saturate(1.05) hue-rotate(-15deg);
}
.filter-hudson::before {
background: radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
background: -o-radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
background: -moz-radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
background: -webkit-radial-gradient(circle closest-corner, transparent 25%, rgba(25, 62, 167, .25) 100%);
content: "";
mix-blend-mode: multiply;
}
.filter-inkwell {
-webkit-filter: brightness(1.25) contrast(.85) grayscale(1);
filter: brightness(1.25) contrast(.85) grayscale(1);
}
.filter-juno {
-webkit-filter: sepia(.35) contrast(1.15) brightness(1.15) saturate(1.8);
filter: sepia(.35) contrast(1.15) brightness(1.15) saturate(1.8);
}
.filter-juno::before {
background: rgba(127, 187, 227, .2);
content: "";
mix-blend-mode: overlay;
}
.filter-kelvin {
-webkit-filter: sepia(.15) contrast(1.5) brightness(1.1) hue-rotate(-10deg);
filter: sepia(.15) contrast(1.5) brightness(1.1) hue-rotate(-10deg);
}
.filter-kelvin::before {
background: radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
background: -o-radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
background: -moz-radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
background: -webkit-radial-gradient(circle closest-corner, rgba(128, 78, 15, .25) 0, rgba(128, 78, 15, .5) 100%);
content: "";
mix-blend-mode: overlay;
}
.filter-lark {
-webkit-filter: sepia(.25) contrast(1.2) brightness(1.3) saturate(1.25);
filter: sepia(.25) contrast(1.2) brightness(1.3) saturate(1.25);
}
.filter-lofi {
-webkit-filter: saturate(1.1) contrast(1.5);
filter: saturate(1.1) contrast(1.5);
}
.filter-ludwig {
-webkit-filter: sepia(.25) contrast(1.05) brightness(1.05) saturate(2);
filter: sepia(.25) contrast(1.05) brightness(1.05) saturate(2);
}
.filter-ludwig::before {
background: rgba(125, 105, 24, .1);
content: "";
mix-blend-mode: overlay;
}
.filter-maven {
-webkit-filter: sepia(.35) contrast(1.05) brightness(1.05) saturate(1.75);
filter: sepia(.35) contrast(1.05) brightness(1.05) saturate(1.75);
}
.filter-maven::before {
background: rgba(158, 175, 30, .25);
content: "";
mix-blend-mode: darken;
}
.filter-mayfair {
-webkit-filter: contrast(1.1) brightness(1.15) saturate(1.1);
filter: contrast(1.1) brightness(1.15) saturate(1.1);
}
.filter-mayfair::before {
background: radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(175, 105, 24, .4) 100%);
content: "";
mix-blend-mode: multiply;
}
.filter-moon {
-webkit-filter: brightness(1.4) contrast(.95) saturate(0) sepia(.35);
filter: brightness(1.4) contrast(.95) saturate(0) sepia(.35);
}
.filter-nashville {
-webkit-filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
}
.filter-nashville::before {
background: radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
background: -o-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
background: -moz-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
background: -webkit-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(128, 78, 15, .65) 100%);
content: "";
mix-blend-mode: screen;
}
.filter-perpetua {
-webkit-filter: contrast(1.1) brightness(1.25) saturate(1.1);
filter: contrast(1.1) brightness(1.25) saturate(1.1);
}
.filter-perpetua::before {
background: linear-gradient(to bottom, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
background: -o-linear-gradient(top, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
background: -moz-linear-gradient(top, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
background: -webkit-linear-gradient(top, rgba(0, 91, 154, .25), rgba(230, 193, 61, .25));
background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 91, 154, .25)), to(rgba(230, 193, 61, .25)));
content: "";
mix-blend-mode: multiply;
}
.filter-poprocket {
-webkit-filter: sepia(.15) brightness(1.2);
filter: sepia(.15) brightness(1.2);
}
.filter-poprocket::before {
background: radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
background: -o-radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
background: -moz-radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
background: -webkit-radial-gradient(circle closest-corner, rgba(206, 39, 70, .75) 40%, black 80%);
content: "";
mix-blend-mode: screen;
}
.filter-reyes {
-webkit-filter: sepia(.75) contrast(.75) brightness(1.25) saturate(1.4);
filter: sepia(.75) contrast(.75) brightness(1.25) saturate(1.4);
}
.filter-rise {
-webkit-filter: sepia(.25) contrast(1.25) brightness(1.2) saturate(.9);
filter: sepia(.25) contrast(1.25) brightness(1.2) saturate(.9);
}
.filter-rise::before {
background: radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
background: -o-radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
background: -moz-radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
background: -webkit-radial-gradient(circle closest-corner, transparent 0, rgba(230, 193, 61, .25) 100%);
content: "";
mix-blend-mode: lighten;
}
.filter-sierra {
-webkit-filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
filter: sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg);
}
.filter-sierra::before {
background: radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
background: -o-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
background: -moz-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
background: -webkit-radial-gradient(circle closest-corner, rgba(128, 78, 15, .5) 0, rgba(0, 0, 0, .65) 100%);
content: "";
mix-blend-mode: screen;
}
.filter-skyline {
-webkit-filter: sepia(.15) contrast(1.25) brightness(1.25) saturate(1.2);
filter: sepia(.15) contrast(1.25) brightness(1.25) saturate(1.2);
}
.filter-slumber {
-webkit-filter: sepia(.35) contrast(1.25) saturate(1.25);
filter: sepia(.35) contrast(1.25) saturate(1.25);
}
.filter-slumber::before {
background: rgba(125, 105, 24, .2);
content: "";
mix-blend-mode: darken;
}
.filter-stinson {
-webkit-filter: sepia(.35) contrast(1.25) brightness(1.1) saturate(1.25);
filter: sepia(.35) contrast(1.25) brightness(1.1) saturate(1.25);
}
.filter-stinson::before {
background: rgba(125, 105, 24, .45);
content: "";
mix-blend-mode: lighten;
}
.filter-sutro {
-webkit-filter: sepia(.4) contrast(1.2) brightness(.9) saturate(1.4) hue-rotate(-10deg);
filter: sepia(.4) contrast(1.2) brightness(.9) saturate(1.4) hue-rotate(-10deg);
}
.filter-sutro::before {
background: radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
background: -o-radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
background: -moz-radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
background: -webkit-radial-gradient(circle closest-corner, transparent 50%, rgba(0, 0, 0, .5) 90%);
content: "";
mix-blend-mode: darken;
}
.filter-toaster {
-webkit-filter: sepia(.25) contrast(1.5) brightness(.95) hue-rotate(-15deg);
filter: sepia(.25) contrast(1.5) brightness(.95) hue-rotate(-15deg);
}
.filter-toaster::before {
background: radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
background: -o-radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
background: -moz-radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
background: -webkit-radial-gradient(circle, #804e0f, rgba(0, 0, 0, .25));
content: "";
mix-blend-mode: screen;
}
.filter-valencia {
-webkit-filter: sepia(.25) contrast(1.1) brightness(1.1);
filter: sepia(.25) contrast(1.1) brightness(1.1);
}
.filter-valencia::before {
background: rgba(230, 193, 61, .1);
content: "";
mix-blend-mode: lighten;
}
.filter-vesper {
-webkit-filter: sepia(.35) contrast(1.15) brightness(1.2) saturate(1.3);
filter: sepia(.35) contrast(1.15) brightness(1.2) saturate(1.3);
}
.filter-vesper::before {
background: rgba(125, 105, 24, .25);
content: "";
mix-blend-mode: overlay;
}
.filter-walden {
-webkit-filter: sepia(.35) contrast(.8) brightness(1.25) saturate(1.4);
filter: sepia(.35) contrast(.8) brightness(1.25) saturate(1.4);
}
.filter-walden::before {
background: rgba(229, 240, 128, .5);
content: "";
mix-blend-mode: darken;
}
.filter-willow {
-webkit-filter: brightness(1.2) contrast(.85) saturate(.05) sepia(.2);
filter: brightness(1.2) contrast(.85) saturate(.05) sepia(.2);
}
.filter-xpro-ii {
-webkit-filter: sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg);
filter: sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg);
}
.filter-xpro-ii::before {
background: radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
background: -o-radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
background: -moz-radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
background: -webkit-radial-gradient(circle closest-corner, rgba(0, 91, 154, .35) 0, rgba(0, 0, 0, .65) 100%);
content: "";
mix-blend-mode: multiply;
}

View file

@ -193,6 +193,13 @@ body, button, input, textarea {
}
}
@media (max-width: map-get($grid-breakpoints, "sm")) {
.card-md-rounded-0 {
border-width: 1px 0;
border-radius:0 !important;
}
}
@keyframes loading-bar {
from { background-position: 0 0; }
to { background-position: 100vw 0; }
@ -234,3 +241,25 @@ body, button, input, textarea {
height: 32px;
background-position: 50%;
}
.status-photo img {
object-fit: contain;
max-height: calc(100vh - (6rem));
}
@keyframes fadeInDown {
0% {
opacity: 0;
transform: translateY(-1.25em);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.details-animated[open] {
animation-name: fadeInDown;
animation-duration: 0.5s;
}

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'Mein Profil anschauen',
'myTimeline' => 'Meine Timeline',
'publicTimeline' => '&Ouml;ffentliche Timeline',
'remoteFollow' => 'Aus der Ferne folgen',
'settings' => 'Einstellungen',
'admin' => 'Administration',
'logout' => 'Abmelden',
];

View file

@ -5,5 +5,6 @@ return [
'likedPhoto' => 'gef&auml;llt dein Foto.',
'startedFollowingYou' => 'folgt dir nun.',
'commented' => 'hat deinen Post kommentiert.',
'mentionedYou' => 'hat dich erw&auml;hnt.'
];

View file

@ -4,5 +4,5 @@ return [
'emptyTimeline' => 'This user has no posts yet!',
'emptyFollowers' => 'This user has no followers yet!',
'emptyFollowing' => 'This user is not following anyone yet!',
'savedWarning' => 'Only you can see what you\'ve saved',
];
'savedWarning' => 'Only you can see what youve saved',
];

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'צפה בפרופיל שלי',
'myTimeline' => 'ציר הזמן שלי',
'publicTimeline' => 'ציר הזמן הציבורי',
'remoteFollow' => 'עקיבה מרחוק',
'settings' => 'הגדרות',
'admin' => 'מנהל',
'logout' => 'התנתק',
];

View file

@ -5,5 +5,6 @@ return [
'likedPhoto' => 'אהבו את התמונה שלך.',
'startedFollowingYou' => 'התחיל לעקוב אחריך.',
'commented' => 'הגיב על הפוסט שלך.',
'mentionedYou' => 'הזכיר אותך.'
];

View file

@ -0,0 +1,24 @@
@extends('layouts.app')
@section('content')
<div class="container mt-4">
<div class="col-12 col-md-8 offset-md-2">
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
<div class="card">
<div class="card-header font-weight-bold bg-white">Confirm Email Address</div>
<div class="card-body">
<p class="lead">You need to confirm your email address (<span class="font-weight-bold">{{Auth::user()->email}}</span>) before you can proceed.</p>
<hr>
<form method="post">
@csrf
<button type="submit" class="btn btn-primary btn-block py-1 font-weight-bold">Send Confirmation Email</button>
</form>
</div>
</div>
</div>
</div>
@endsection

View file

@ -26,16 +26,16 @@
<link rel="self" type="application/atom+xml" href="{{$profile->permalink('.atom')}}"/>
@foreach($items as $item)
<entry>
<title><![CDATA[{{ $item->caption }}]]></title>
<title>{{ $item->caption }}</title>
<link rel="alternate" href="{{ $item->url() }}" />
<id>{{ url($item->id) }}</id>
<author>
<name> <![CDATA[{{ $item->profile->username }}]]></name>
</author>
<summary type="html">
<![CDATA[{!! $item->caption !!}]]>
{{ $item->caption }}
</summary>
<updated>{{ $item->updated_at->toAtomString() }}</updated>
</entry>
@endforeach
</feed>
</feed>

View file

@ -0,0 +1,12 @@
@component('mail::message')
# Email Confirmation
Please confirm your email address.
@component('mail::button', ['url' => $verify->url()])
Confirm Email
@endcomponent
Thanks,<br>
{{ config('app.name') }}
@endcomponent

View file

@ -5,9 +5,9 @@
<div class="error-page py-5 my-5">
<div class="card mx-5">
<div class="card-body p-5">
<h1 class="text-center">403 - Forbidden</h1>
<h1 class="text-center">403 Forbidden</h1>
</div>
</div>
</div>
</div>
@endsection
@endsection

View file

@ -5,9 +5,9 @@
<div class="error-page py-5 my-5">
<div class="card mx-5">
<div class="card-body p-5">
<h1 class="text-center">404 - Page Not Found</h1>
<h1 class="text-center">404 Page Not Found</h1>
</div>
</div>
</div>
</div>
@endsection
@endsection

View file

@ -5,10 +5,10 @@
<div class="error-page py-5 my-5">
<div class="card mx-5">
<div class="card-body p-5 text-center">
<h1>503 - Service Unavailable</h1>
<h1>503 Service Unavailable</h1>
<p class="lead mb-0">Our services are overloaded at the moment, please try again later.</p>
</div>
</div>
</div>
</div>
@endsection
@endsection

View file

@ -1,18 +1,16 @@
<footer>
<div class="container py-5">
<p class="mb-0 text-uppercase font-weight-bold small">
<a href="{{route('site.about')}}" class="text-primary pr-2">About Us</a>
<a href="{{route('site.help')}}" class="text-primary pr-2">Support</a>
<a href="{{route('site.opensource')}}" class="text-primary pr-2">Open Source</a>
<a href="{{route('site.language')}}" class="text-primary pr-2">Language</a>
<span class="px-2"></span>
<a href="{{route('site.terms')}}" class="text-primary pr-2 pl-2">Terms</a>
<a href="{{route('site.privacy')}}" class="text-primary pr-2">Privacy</a>
<a href="{{route('site.platform')}}" class="text-primary pr-2">API</a>
<span class="px-2"></span>
<a href="#" class="text-primary pr-2 pl-2">Directory</a>
<a href="#" class="text-primary pr-2">Profiles</a>
<a href="#" class="text-primary">Hashtags</a>
<p class="mb-0 text-uppercase font-weight-bold small text-justify">
<a href="{{route('site.about')}}" class="text-primary pr-3">About Us</a>
<a href="{{route('site.help')}}" class="text-primary pr-3">Support</a>
<a href="{{route('site.opensource')}}" class="text-primary pr-3">Open Source</a>
<a href="{{route('site.terms')}}" class="text-primary pr-3">Terms</a>
<a href="{{route('site.privacy')}}" class="text-primary pr-3">Privacy</a>
<a href="{{route('site.platform')}}" class="text-primary pr-3">API</a>
<a href="#" class="text-primary pr-3">Directory</a>
<a href="#" class="text-primary pr-3">Profiles</a>
<a href="#" class="text-primary pr-3">Hashtags</a>
<a href="{{route('site.language')}}" class="text-primary pr-3">Language</a>
<a href="http://pixelfed.org" class="text-muted float-right" rel="noopener">Powered by PixelFed</a>
</p>
</div>

View file

@ -1,16 +1,18 @@
<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="{{ url('/timeline') }}" title="Logo">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
<strong class="font-weight-bold">{{ config('app.name', 'Laravel') }}</strong>
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2">
<span class="font-weight-bold mb-0" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
@auth
<ul class="navbar-nav ml-auto d-none d-md-block">
<form class="form-inline search-form">
<input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
</form>
</ul>
@endauth
<ul class="navbar-nav ml-auto">
@guest
@ -21,7 +23,9 @@
<a class="nav-link" href="{{route('discover')}}" title="Discover"><i class="far fa-compass fa-lg"></i></a>
</li>
<li class="nav-item px-2">
<a class="nav-link" href="{{route('notifications')}}" title="Notifications"><i class="far fa-heart fa-lg"></i></a>
<a class="nav-link" href="{{route('notifications')}}" title="Notifications">
<i class="far fa-heart fa-lg"></i>
</a>
</li>
<li class="nav-item dropdown px-2">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre title="User Menu">
@ -76,8 +80,10 @@
</div>
</div>
</nav>
@auth
<nav class="breadcrumb d-md-none d-flex">
<form class="form-inline search-form mx-auto">
<input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
</form>
</nav>
@endauth

View file

@ -1,4 +1,4 @@
@extends('layouts.app',['title' => $profile->username . "'s followers"])
@extends('layouts.app',['title' => $profile->username . "s followers"])
@section('content')

View file

@ -1,4 +1,4 @@
@extends('layouts.app',['title' => $profile->username . "'s follows"])
@extends('layouts.app',['title' => $profile->username . "s follows"])
@section('content')

Some files were not shown because too many files have changed in this diff Show more