Merge branch 'frontend-ui-refactor' into patch-23

This commit is contained in:
Stasiek Michalski 2018-08-10 21:16:24 +02:00 committed by GitHub
commit d3daa9b084
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
123 changed files with 5420 additions and 1368 deletions

View file

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

View file

@ -1,4 +1,76 @@
# PixelFed # PixelFed: Federated Image Sharing
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

@ -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 class Hashtag extends Model
{ {
protected $fillable = ['name','slug']; public $fillable = ['name','slug'];
public function posts() public function posts()
{ {

View file

@ -18,19 +18,47 @@ class AccountController extends Controller
public function notifications(Request $request) public function notifications(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'page' => 'nullable|min:1|max:3' 'page' => 'nullable|min:1|max:3',
'a' => 'nullable|alpha_dash',
]); ]);
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$action = $request->input('a');
$timeago = Carbon::now()->subMonths(6); $timeago = Carbon::now()->subMonths(6);
$notifications = Notification::whereProfileId($profile->id) if($action && in_array($action, ['comment', 'follow', 'mention'])) {
->whereDate('created_at', '>', $timeago) $notifications = Notification::whereProfileId($profile->id)
->orderBy('id','desc') ->whereAction($action)
->take(30) ->whereDate('created_at', '>', $timeago)
->simplePaginate(); ->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')); return view('account.activity', compact('profile', 'notifications'));
} }
public function followingActivity(Request $request)
{
$this->validate($request, [
'page' => 'nullable|min:1|max:3',
'a' => 'nullable|alpha_dash',
]);
$profile = Auth::user()->profile;
$action = $request->input('a');
$timeago = Carbon::now()->subMonths(1);
$following = $profile->following->pluck('id');
$notifications = Notification::whereIn('actor_id', $following)
->where('profile_id', '!=', $profile->id)
->whereDate('created_at', '>', $timeago)
->orderBy('notifications.id','desc')
->simplePaginate(30);
return view('account.following', compact('profile', 'notifications'));
}
public function verifyEmail(Request $request) public function verifyEmail(Request $request)
{ {
return view('account.verify_email'); return view('account.verify_email');
@ -38,10 +66,19 @@ class AccountController extends Controller
public function sendVerifyEmail(Request $request) public function sendVerifyEmail(Request $request)
{ {
if(EmailVerification::whereUserId(Auth::id())->count() !== 0) { $timeLimit = Carbon::now()->subDays(1)->toDateTimeString();
return redirect()->back()->with('status', 'A verification email has already been sent! Please check your email.'); $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()); $user = User::whereNull('email_verified_at')->find(Auth::id());
$utoken = hash('sha512', $user->id); $utoken = hash('sha512', $user->id);
$rtoken = str_random(40); $rtoken = str_random(40);
@ -60,14 +97,15 @@ class AccountController extends Controller
public function confirmVerifyEmail(Request $request, $userToken, $randomToken) public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
{ {
$verify = EmailVerification::where(DB::raw('BINARY user_token'), $userToken) $verify = EmailVerification::where('user_token', $userToken)
->where(DB::raw('BINARY random_token'), $randomToken) ->where('random_token', $randomToken)
->firstOrFail(); ->firstOrFail();
if(Auth::id() === $verify->user_id) { if(Auth::id() === $verify->user_id) {
$user = User::find(Auth::id()); $user = User::find(Auth::id());
$user->email_verified_at = Carbon::now(); $user->email_verified_at = Carbon::now();
$user->save(); $user->save();
return redirect('/timeline'); return redirect('/');
} }
} }
@ -95,4 +133,5 @@ class AccountController extends Controller
} }
return $notifications; 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,16 +2,13 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Auth; use Auth, Cache;
use App\Like; use App\{Like, Status};
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Api\BaseApiController;
class ApiController extends Controller class ApiController extends BaseApiController
{ {
public function __construct()
{
$this->middleware('auth');
}
public function hydrateLikes(Request $request) public function hydrateLikes(Request $request)
{ {
@ -21,12 +18,18 @@ class ApiController extends Controller
]); ]);
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$res = Cache::remember('api:like-ids:user:'.$profile->id, 1440, function() use ($profile) {
$likes = Like::whereProfileId($profile->id) return Like::whereProfileId($profile->id)
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->take(1000) ->take(1000)
->pluck('status_id'); ->pluck('status_id');
});
return response()->json($likes); return response()->json($res);
}
public function loadMoreComments(Request $request)
{
return;
} }
} }

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\{AccountLog, User};
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
@ -25,7 +26,7 @@ class LoginController extends Controller
* *
* @var string * @var string
*/ */
protected $redirectTo = '/home'; protected $redirectTo = '/';
/** /**
* Create a new controller instance. * Create a new controller instance.
@ -56,4 +57,25 @@ class LoginController extends Controller
$this->validate($request, $rules); $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

@ -3,8 +3,91 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Auth, Log, Storage;
use App\Avatar;
use App\Jobs\AvatarPipeline\AvatarOptimize;
class AvatarController extends Controller class AvatarController extends Controller
{ {
// public function __construct()
{
return $this->middleware('auth');
}
public function store(Request $request)
{
$this->validate($request, [
'avatar' => 'required|mimes:jpeg,png|max:1000'
]);
try {
$user = Auth::user();
$file = $request->file('avatar');
$path = $this->getPath($user, $file);
$dir = $path['root'];
$name = $path['name'];
$public = $path['storage'];
$currentAvatar = storage_path('app/'.$user->profile->avatar->media_path);
$loc = $request->file('avatar')->storeAs($public, $name);
$avatar = Avatar::whereProfileId($user->profile->id)->firstOrFail();
$opath = $avatar->media_path;
$avatar->media_path = "$public/$name";
$avatar->thumb_path = null;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = null;
$avatar->save();
AvatarOptimize::dispatch($user->profile, $currentAvatar);
} catch (Exception $e) {
}
return redirect()->back()->with('status', 'Avatar updated successfully. It may take a few minutes to update across the site.');
}
public function getPath($user, $file)
{
$basePath = storage_path('app/public/avatars');
$this->checkDir($basePath);
$id = $user->profile->id;
$path = $this->buildPath($id);
$dir = storage_path('app/'.$path);
$this->checkDir($dir);
$name = 'avatar.' . $file->guessExtension();
$res = ['root' => 'storage/app/' . $path, 'name' => $name, 'storage' => $path];
return $res;
}
public function checkDir($path)
{
if(!is_dir($path)) {
mkdir($path);
}
}
public function buildPath($id)
{
$padded = str_pad($id, 12, 0, STR_PAD_LEFT);
$parts = str_split($padded, 3);
foreach($parts as $k => $part) {
if($k == 0) {
$prefix = storage_path('app/public/avatars/'.$parts[0]);
$this->checkDir($prefix);
}
if($k == 1) {
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1]);
$this->checkDir($prefix);
}
if($k == 2) {
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2]);
$this->checkDir($prefix);
}
if($k == 3) {
$avatarpath = 'public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2].'/'.$parts[3];
$prefix = storage_path('app/'.$avatarpath);
$this->checkDir($prefix);
}
}
return $avatarpath;
}
} }

View file

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

View file

@ -18,6 +18,14 @@ class CommentController extends Controller
return view('status.reply', compact('user', 'status')); return view('status.reply', compact('user', 'status'));
} }
public function showAll(Request $request, $username, int $id)
{
$user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id)->findOrFail($id);
$replies = Status::whereInReplyToId($id)->paginate(40);
return view('status.comments', compact('user', 'status', 'replies'));
}
public function store(Request $request) public function store(Request $request)
{ {
if(Auth::check() === false) { abort(403); } if(Auth::check() === false) { abort(403); }

View file

@ -15,17 +15,44 @@ class DiscoverController extends Controller
public function home() public function home()
{ {
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id'); $pid = Auth::user()->profile->id;
$people = Profile::inRandomOrder()->where('id', '!=', Auth::user()->profile->id)->whereNotIn('id', $following)->take(3)->get();
$posts = Status::whereHas('media')->where('profile_id', '!=', Auth::user()->profile->id)->whereNotIn('profile_id', $following)->orderBy('created_at', 'desc')->take('21')->get(); $following = Follower::whereProfileId($pid)
->pluck('following_id');
$people = Profile::inRandomOrder()
->where('id', '!=', $pid)
->whereNotIn('id', $following)
->take(3)
->get();
$posts = Status::whereHas('media')
->where('profile_id', '!=', $pid)
->whereNotIn('profile_id', $following)
->orderBy('created_at', 'desc')
->simplePaginate(21);
return view('discover.home', compact('people', 'posts')); return view('discover.home', compact('people', 'posts'));
} }
public function showTags(Request $request, $hashtag) public function showTags(Request $request, $hashtag)
{ {
$tag = Hashtag::whereSlug($hashtag)->firstOrFail(); $this->validate($request, [
$posts = $tag->posts()->has('media')->orderBy('id','desc')->paginate(12); 'page' => 'nullable|integer|min:1|max:10'
$count = $tag->posts()->has('media')->orderBy('id','desc')->count(); ]);
return view('discover.tags.show', compact('tag', 'posts', 'count'));
$tag = Hashtag::with('posts')
->withCount('posts')
->whereSlug($hashtag)
->firstOrFail();
$posts = $tag->posts()
->whereIsNsfw(false)
->whereVisibility('public')
->has('media')
->orderBy('id','desc')
->simplePaginate(12);
return view('discover.tags.show', compact('tag', 'posts'));
} }
} }

View file

@ -2,8 +2,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Auth; use Auth, Cache;
use App\Profile; use App\Profile;
use Carbon\Carbon;
use League\Fractal; use League\Fractal;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Util\Lexer\Nickname; use App\Util\Lexer\Nickname;
@ -13,15 +14,26 @@ use App\Transformer\ActivityPub\{
ProfileTransformer ProfileTransformer
}; };
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline; use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
use App\Jobs\InboxPipeline\InboxWorker;
class FederationController extends Controller class FederationController extends Controller
{ {
public function authCheck() public function authCheck()
{ {
if(!Auth::check()) { 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() public function remoteFollow()
@ -64,61 +76,58 @@ class FederationController extends Controller
public function nodeinfo() public function nodeinfo()
{ {
$res = [ $res = Cache::remember('api:nodeinfo', 60, function() {
'metadata' => [ return [
'nodeName' => config('app.name'), 'metadata' => [
'software' => [ 'nodeName' => config('app.name'),
'homepage' => 'https://pixelfed.org', 'software' => [
'github' => 'https://github.com/pixelfed', 'homepage' => 'https://pixelfed.org',
'follow' => 'https://mastodon.social/@pixelfed' '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
],
], ],
], ],
*/ 'openRegistrations' => config('pixelfed.open_registration'),
], 'protocols' => [
'openRegistrations' => config('pixelfed.open_registration'), 'activitypub'
'protocols' => [ ],
'activitypub' 'services' => [
], 'inbound' => [],
'services' => [ 'outbound' => []
'inbound' => [], ],
'outbound' => [] 'software' => [
], 'name' => 'pixelfed',
'software' => [ 'version' => config('pixelfed.version')
'name' => 'pixelfed', ],
'version' => config('pixelfed.version') 'usage' => [
], 'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
'usage' => [ 'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
'localPosts' => \App\Status::whereLocal(true)->count(), 'users' => [
'users' => [ 'total' => \App\User::count(),
'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' ],
]; 'version' => '2.0'
];
return response()->json($res); });
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
} }
public function webfinger(Request $request) public function webfinger(Request $request)
{ {
$this->validate($request, ['resource'=>'required']); $this->validate($request, ['resource'=>'required|string|min:3|max:255']);
$resource = $request->input('resource');
$parsed = Nickname::normalizeProfileUrl($resource); $hash = hash('sha512', $request->input('resource'));
$username = $parsed['username'];
$user = Profile::whereUsername($username)->firstOrFail(); $webfinger = Cache::remember('api:webfinger:'.$hash, 1440, function() use($request) {
$webfinger = (new Webfinger($user))->generate(); $resource = $request->input('resource');
return response()->json($webfinger); $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) public function userOutbox(Request $request, $username)
@ -135,4 +144,20 @@ class FederationController extends Controller
return response()->json($res['data']); 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; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Auth, Hashids; use Auth, Cache, Hashids;
use App\{Like, Profile, Status, User}; use App\{Like, Profile, Status, User};
use App\Jobs\LikePipeline\LikePipeline; use App\Jobs\LikePipeline\LikePipeline;
@ -27,7 +27,7 @@ class LikeController extends Controller
if($status->likes()->whereProfileId($profile->id)->count() !== 0) { if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail(); $like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
$like->delete(); $like->forceDelete();
$count--; $count--;
} else { } else {
$like = new Like; $like = new Like;
@ -35,9 +35,15 @@ class LikeController extends Controller
$like->status_id = $status->id; $like->status_id = $status->id;
$like->save(); $like->save();
$count++; $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()) { if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count]; $response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];

View file

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

@ -2,12 +2,25 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Auth;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\{Avatar, Profile, Report, Status, User};
class ReportController extends Controller class ReportController extends Controller
{ {
protected $profile;
public function __construct()
{
$this->middleware('auth');
}
public function showForm(Request $request) public function showForm(Request $request)
{ {
$this->validate($request, [
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1'
]);
return view('report.form'); return view('report.form');
} }
@ -35,4 +48,92 @@ class ReportController extends Controller
{ {
return view('report.spam.profile'); return view('report.spam.profile');
} }
public function sensitiveCommentForm(Request $request)
{
return view('report.sensitive.comment');
}
public function sensitivePostForm(Request $request)
{
return view('report.sensitive.post');
}
public function sensitiveProfileForm(Request $request)
{
return view('report.sensitive.profile');
}
public function abusiveCommentForm(Request $request)
{
return view('report.abusive.comment');
}
public function abusivePostForm(Request $request)
{
return view('report.abusive.post');
}
public function abusiveProfileForm(Request $request)
{
return view('report.abusive.profile');
}
public function formStore(Request $request)
{
$this->validate($request, [
'report' => 'required|alpha_dash',
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
'msg' => 'nullable|string|max:150'
]);
$profile = Auth::user()->profile;
$reportType = $request->input('report');
$object_id = $request->input('id');
$object_type = $request->input('type');
$msg = $request->input('msg');
$object = null;
$types = ['spam', 'sensitive', 'abusive'];
if(!in_array($reportType, $types)) {
return redirect('/timeline')->with('error', 'Invalid report type');
}
switch ($object_type) {
case 'post':
$object = Status::findOrFail($object_id);
$object_type = 'App\Status';
$exists = Report::whereUserId(Auth::id())
->whereObjectId($object->id)
->whereObjectType('App\Status')
->count();
break;
default:
return redirect('/timeline')->with('error', 'Invalid report type');
break;
}
if($exists !== 0) {
return redirect('/timeline')->with('error', 'You have already reported this!');
}
if($object->profile_id == $profile->id) {
return redirect('/timeline')->with('error', 'You cannot report your own content!');
}
$report = new Report;
$report->profile_id = $profile->id;
$report->user_id = Auth::id();
$report->object_id = $object->id;
$report->object_type = $object_type;
$report->reported_profile_id = $object->profile_id;
$report->type = $request->input('report');
$report->message = $request->input('msg');
$report->save();
return redirect('/timeline')->with('status', 'Report successfully sent!');
}
} }

View file

@ -3,19 +3,28 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\{Profile, User}; use App\{AccountLog, Media, Profile, User};
use Auth; use Auth, DB;
use App\Util\Lexer\PrettyNumber;
class SettingsController extends Controller class SettingsController extends Controller
{ {
public function __construct() public function __construct()
{ {
return $this->middleware('auth'); $this->middleware('auth');
} }
public function home() public function home()
{ {
return view('settings.home'); $id = Auth::user()->profile->id;
$storage = [];
$used = Media::whereProfileId($id)->sum('size');
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
$storage['used'] = $used;
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
return view('settings.home', compact('storage'));
} }
public function homeUpdate(Request $request) public function homeUpdate(Request $request)
@ -89,6 +98,34 @@ class SettingsController extends Controller
return view('settings.avatar'); 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() public function notifications()
{ {
return view('settings.notifications'); return view('settings.notifications');
@ -96,12 +133,61 @@ class SettingsController extends Controller
public function privacy() 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() 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() public function applications()
@ -121,7 +207,7 @@ class SettingsController extends Controller
public function dataImportInstagram() public function dataImportInstagram()
{ {
return view('settings.import.ig'); return view('settings.import.instagram.home');
} }
public function developers() public function developers()

View file

@ -2,11 +2,42 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App; use App, Auth, Cache;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\{Follower, Profile, Status, User};
use App\Util\Lexer\PrettyNumber;
class SiteController extends Controller class SiteController extends Controller
{ {
public function home()
{
if(Auth::check()) {
return $this->homeTimeline();
} else {
return $this->homeGuest();
}
}
public function homeGuest()
{
return view('welcome');
}
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)
->whereHas('media')
->orderBy('id','desc')
->withCount(['comments', 'likes', 'shares'])
->simplePaginate(20);
$type = 'personal';
return view('timeline.template', compact('timeline', 'type'));
}
public function changeLocale(Request $request, $locale) public function changeLocale(Request $request, $locale)
{ {
if(!App::isLocale($locale)) { if(!App::isLocale($locale)) {
@ -15,4 +46,20 @@ class SiteController extends Controller
App::setLocale($locale); App::setLocale($locale);
return redirect()->back(); return redirect()->back();
} }
public function about()
{
$res = Cache::remember('site:page:about', 15, function() {
$statuses = Status::whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->count();
$statusCount = PrettyNumber::convert($statuses);
$userCount = PrettyNumber::convert(User::count());
$remoteCount = PrettyNumber::convert(Profile::whereNotNull('remote_url')->count());
$adminContact = User::whereIsAdmin(true)->first();
return view('site.about')->with(compact('statusCount', 'userCount', 'remoteCount', 'adminContact'))->render();
});
return $res;
}
} }

View file

@ -13,93 +13,147 @@ class StatusController extends Controller
{ {
public function show(Request $request, $username, int $id) public function show(Request $request, $username, int $id)
{ {
$user = Profile::whereUsername($username)->firstOrFail(); $user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id) $status = Status::whereProfileId($user->id)
->withCount(['likes', 'comments', 'media']) ->withCount(['likes', 'comments', 'media'])
->findOrFail($id); ->findOrFail($id);
if(!$status->media_path && $status->in_reply_to_id) { if(!$status->media_path && $status->in_reply_to_id) {
return redirect($status->url()); return redirect($status->url());
} }
return view('status.show', compact('user', 'status')); $replies = Status::whereInReplyToId($status->id)->simplePaginate(30);
return view('status.show', compact('user', 'status', 'replies'));
}
public function compose()
{
if(Auth::check() == false)
{
abort(403);
}
return view('status.compose');
} }
public function store(Request $request) public function store(Request $request)
{ {
if(Auth::check() == false) if(Auth::check() == false)
{ {
abort(403); abort(403);
} }
$user = Auth::user(); $user = Auth::user();
$this->validate($request, [ $size = Media::whereUserId($user->id)->sum('size') / 1000;
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'), $limit = (int) config('pixelfed.max_account_size');
'caption' => 'string|max:' . config('pixelfed.max_caption_length'), if($size >= $limit) {
'cw' => 'nullable|string', return redirect()->back()->with('error', 'You have exceeded your storage limit. Please click <a href="#">here</a> for more info.');
'filter_class' => 'nullable|string', }
'filter_name' => 'nullable|string',
]);
if(count($request->file('photo')) > config('pixelfed.max_album_length')) { $this->validate($request, [
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length')); 'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
} 'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
'cw' => 'nullable|string',
'filter_class' => 'nullable|string',
'filter_name' => 'nullable|string',
]);
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false; if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
$monthHash = hash('sha1', date('Y') . date('m')); return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
$userHash = hash('sha1', $user->id . (string) $user->created_at); }
$profile = $user->profile; $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);
$profile = $user->profile;
$status = new Status; $status = new Status;
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = strip_tags($request->caption); $status->caption = strip_tags($request->caption);
$status->is_nsfw = $cw; $status->is_nsfw = $cw;
$status->save(); $status->save();
$photos = $request->file('photo'); $photos = $request->file('photo');
$order = 1; $order = 1;
foreach ($photos as $k => $v) { foreach ($photos as $k => $v) {
$storagePath = "public/m/{$monthHash}/{$userHash}"; $storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $v->store($storagePath); $path = $v->store($storagePath);
$media = new Media; $media = new Media;
$media->status_id = $status->id; $media->status_id = $status->id;
$media->profile_id = $profile->id; $media->profile_id = $profile->id;
$media->user_id = $user->id; $media->user_id = $user->id;
$media->media_path = $path; $media->media_path = $path;
$media->size = $v->getClientSize(); $media->size = $v->getClientSize();
$media->mime = $v->getClientMimeType(); $media->mime = $v->getClientMimeType();
$media->filter_class = $request->input('filter_class'); $media->filter_class = $request->input('filter_class');
$media->filter_name = $request->input('filter_name'); $media->filter_name = $request->input('filter_name');
$media->order = $order; $media->order = $order;
$media->save(); $media->save();
ImageOptimize::dispatch($media); ImageOptimize::dispatch($media);
$order++; $order++;
} }
NewStatusPipeline::dispatch($status); NewStatusPipeline::dispatch($status);
// TODO: Send to subscribers // TODO: Send to subscribers
return redirect($status->url()); return redirect($status->url());
} }
public function delete(Request $request) public function delete(Request $request)
{ {
if(!Auth::check()) { if(!Auth::check()) {
abort(403); abort(403);
} }
$this->validate($request, [ $this->validate($request, [
'type' => 'required|string', 'type' => 'required|string',
'item' => 'required|integer|min:1' 'item' => 'required|integer|min:1'
]); ]);
$status = Status::findOrFail($request->input('item')); $status = Status::findOrFail($request->input('item'));
if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) { if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
StatusDelete::dispatch($status); StatusDelete::dispatch($status);
} }
return redirect(Auth::user()->url()); return redirect(Auth::user()->url());
}
public function storeShare(Request $request)
{
$this->validate($request, [
'item' => 'required|integer',
]);
$profile = Auth::user()->profile;
$status = Status::withCount('shares')->findOrFail($request->input('item'));
$count = $status->shares_count;
$exists = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->count();
if($exists !== 0) {
$shares = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->get();
foreach($shares as $share) {
$share->delete();
$count--;
}
} else {
$share = new Status;
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->save();
$count++;
}
if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
return $response;
} }
} }

View file

@ -18,24 +18,23 @@ class TimelineController extends Controller
// TODO: Use redis for timelines // TODO: Use redis for timelines
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id'); $following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
$following->push(Auth::user()->profile->id); $following->push(Auth::user()->profile->id);
$timeline = Status::whereHas('media') $timeline = Status::whereIn('profile_id', $following)
->whereNull('in_reply_to_id')
->whereIn('profile_id', $following)
->orderBy('id','desc') ->orderBy('id','desc')
->withCount(['comments', 'likes']) ->simplePaginate(20);
->simplePaginate(10); $type = 'personal';
return view('timeline.personal', compact('timeline')); return view('timeline.template', compact('timeline', 'type'));
} }
public function local() public function local()
{ {
// TODO: Use redis for timelines // TODO: Use redis for timelines
// $timeline = Timeline::build()->local();
$timeline = Status::whereHas('media') $timeline = Status::whereHas('media')
->whereNull('in_reply_to_id') ->whereNull('in_reply_to_id')
->orderBy('id','desc') ->orderBy('id','desc')
->withCount(['comments', 'likes']) ->simplePaginate(20);
->simplePaginate(10); $type = 'local';
return view('timeline.public', compact('timeline')); return view('timeline.template', compact('timeline', 'type'));
} }
} }

View file

@ -18,7 +18,7 @@ class EmailVerificationCheck
if($request->user() && if($request->user() &&
config('pixelfed.enforce_email_verification') && config('pixelfed.enforce_email_verification') &&
is_null($request->user()->email_verified_at) && is_null($request->user()->email_verified_at) &&
!$request->is('i/verify-email') && !$request->is('login') && !$request->is('i/verify-email') && !$request->is('log*') &&
!$request->is('i/confirm-email/*') !$request->is('i/confirm-email/*')
) { ) {
return redirect('/i/verify-email'); return redirect('/i/verify-email');

10
app/ImportJob.php Normal file
View file

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

View file

@ -0,0 +1,70 @@
<?php
namespace App\Jobs\AvatarPipeline;
use \Carbon\Carbon;
use Image as Intervention;
use App\{Avatar, Profile};
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class AvatarOptimize implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile;
protected $current;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Profile $profile, $current)
{
$this->profile = $profile;
$this->current = $current;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$avatar = $this->profile->avatar;
$file = storage_path("app/$avatar->media_path");
try {
$img = Intervention::make($file)->orientate();
$img->fit(200, 200, function ($constraint) {
$constraint->upsize();
});
$quality = config('pixelfed.image_quality');
$img->save($file, $quality);
$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
$avatar->thumb_path = $avatar->media_path;
$avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = Carbon::now();
$avatar->save();
$this->deleteOldAvatar($avatar->media_path, $this->current);
} catch (Exception $e) {
}
}
protected function deleteOldAvatar($new, $current)
{
if(storage_path('app/' . $new) == $current) {
return;
}
if(is_file($current)) {
@unlink($current);
}
}
}

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; $status = $this->like->status;
$actor = $this->like->actor; $actor = $this->like->actor;
if($status->url !== null) {
// Ignore notifications to remote statuses
return;
}
$exists = Notification::whereProfileId($status->profile_id) $exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id) ->whereActorId($actor->id)
->whereAction('like') ->whereAction('like')

View file

@ -2,7 +2,7 @@
namespace App\Jobs\StatusPipeline; namespace App\Jobs\StatusPipeline;
use Cache; use DB, Cache;
use App\{ use App\{
Hashtag, Hashtag,
Media, Media,
@ -68,12 +68,14 @@ class StatusEntityLexer implements ShouldQueue
public function storeEntities() public function storeEntities()
{ {
$status = $this->status;
$this->storeHashtags(); $this->storeHashtags();
$this->storeMentions(); $this->storeMentions();
$status->rendered = $this->autolink; DB::transaction(function () {
$status->entities = json_encode($this->entities); $status = $this->status;
$status->save(); $status->rendered = $this->autolink;
$status->entities = json_encode($this->entities);
$status->save();
});
} }
public function storeHashtags() public function storeHashtags()
@ -82,17 +84,15 @@ class StatusEntityLexer implements ShouldQueue
$status = $this->status; $status = $this->status;
foreach($tags as $tag) { foreach($tags as $tag) {
$slug = str_slug($tag); DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag);
$htag = Hashtag::firstOrCreate( $hashtag = Hashtag::firstOrCreate(
['name' => $tag], ['name' => $tag, 'slug' => $slug]
['slug' => $slug] );
); StatusHashtag::firstOrCreate(
['status_id' => $status->id, 'hashtag_id' => $hashtag->id]
StatusHashtag::firstOrCreate( );
['status_id' => $status->id], });
['hashtag_id' => $htag->id]
);
} }
} }
@ -102,18 +102,20 @@ class StatusEntityLexer implements ShouldQueue
$status = $this->status; $status = $this->status;
foreach($mentions as $mention) { foreach($mentions as $mention) {
$mentioned = Profile::whereUsername($mention)->first(); $mentioned = Profile::whereUsername($mention)->firstOrFail();
if(empty($mentioned) || !isset($mentioned->id)) { if(empty($mentioned) || !isset($mentioned->id)) {
continue; continue;
} }
$m = new Mention; DB::transaction(function () use ($status, $mentioned) {
$m->status_id = $status->id; $m = new Mention;
$m->profile_id = $mentioned->id; $m->status_id = $status->id;
$m->save(); $m->profile_id = $mentioned->id;
$m->save();
MentionPipeline::dispatch($status, $m);
MentionPipeline::dispatch($status, $m);
});
} }
} }

View file

@ -23,4 +23,11 @@ class Media extends Model
$url = Storage::url($path); $url = Storage::url($path);
return url($url); return url($url);
} }
public function thumbnailUrl()
{
$path = $this->thumbnail_path;
$url = Storage::url($path);
return url($url);
}
} }

View file

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

View file

@ -2,7 +2,7 @@
namespace App; namespace App;
use Storage; use Auth, Storage;
use App\Util\Lexer\PrettyNumber; use App\Util\Lexer\PrettyNumber;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -29,6 +29,15 @@ class Profile extends Model
} }
public function url($suffix = '') 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); return url($this->username . $suffix);
} }
@ -124,4 +133,40 @@ class Profile extends Model
$url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png')); $url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png'));
return $url; return $url;
} }
public function statusCount()
{
return $this->statuses()->whereHas('media')->count();
}
public function recommendFollowers()
{
$follows = $this->following()->pluck('followers.id');
$following = $this->following()
->orderByRaw('rand()')
->take(3)
->pluck('following_id');
$following->push(Auth::id());
$following = Follower::whereNotIn('profile_id', $follows)
->whereNotIn('following_id', $following)
->whereNotIn('following_id', $follows)
->whereIn('profile_id', $following)
->orderByRaw('rand()')
->limit(3)
->pluck('following_id');
$recommended = [];
foreach($following as $follow) {
$recommended[] = Profile::findOrFail($follow);
}
return $recommended;
}
public function keyId()
{
if($this->remote_url) {
return;
}
return $this->permalink('#main-key');
}
} }

View file

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

View file

@ -6,5 +6,33 @@ use Illuminate\Database\Eloquent\Model;
class Report extends Model class Report extends Model
{ {
// public function url()
{
return url('/i/admin/reports/show/' . $this->id);
}
public function reporter()
{
return $this->belongsTo(Profile::class, 'profile_id');
}
public function reported()
{
$class = $this->object_type;
switch ($class) {
case 'App\Status':
$column = 'id';
break;
default:
$column = 'id';
break;
}
return (new $class())->where($column, $this->object_id)->firstOrFail();
}
public function reportedUser()
{
return $this->belongsTo(Profile::class, 'reported_profile_id', 'id');
}
} }

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,7 +2,7 @@
namespace App; namespace App;
use Storage; use Auth, Storage;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -52,6 +52,14 @@ class Status extends Model
return url($path); return url($path);
} }
public function permalink($suffix = '/activity')
{
$id = $this->id;
$username = $this->profile->username;
$path = config('app.url') . "/p/{$username}/{$id}{$suffix}";
return url($path);
}
public function editUrl() public function editUrl()
{ {
return $this->url() . '/edit'; return $this->url() . '/edit';
@ -71,15 +79,42 @@ class Status extends Model
return $this->hasMany(Like::class); 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() public function comments()
{ {
return $this->hasMany(Status::class, 'in_reply_to_id'); return $this->hasMany(Status::class, 'in_reply_to_id');
} }
public function bookmarked()
{
if(!Auth::check()) {
return 0;
}
$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() public function parent()
{ {
if(!empty($this->in_reply_to_id)) { $parent = $this->in_reply_to_id ?? $this->reblog_of_id;
return Status::findOrFail($this->in_reply_to_id); if(!empty($parent)) {
return Status::findOrFail($parent);
} }
} }
@ -100,6 +135,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() public function toActivityStream()
{ {
$media = $this->media; $media = $this->media;
@ -133,4 +185,9 @@ class Status extends Model
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " . return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
__('notification.commented'); __('notification.commented');
} }
public function recentComments()
{
return $this->comments()->orderBy('created_at','desc')->take(3);
}
} }

View file

@ -6,5 +6,5 @@ use Illuminate\Database\Eloquent\Model;
class StatusHashtag extends 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

@ -15,7 +15,7 @@ class User extends Authenticatable
* *
* @var array * @var array
*/ */
protected $dates = ['deleted_at']; protected $dates = ['deleted_at', 'email_verified_at'];
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -44,4 +44,9 @@ class User extends Authenticatable
{ {
return url(config('app.url') . '/' . $this->username); 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 // Static Assets
"assets", "assets",
"storage",
// Laravel Horizon // Laravel Horizon
"horizon", "horizon",
@ -127,14 +128,19 @@ class RestrictedNames {
"api", "api",
"auth", "auth",
"i", "i",
"dashboard",
"discover", "discover",
"docs",
"home", "home",
"login", "login",
"logout", "logout",
"media",
"p", "p",
"password", "password",
"reports",
"search", "search",
"settings", "settings",
"statuses",
"site", "site",
"timeline", "timeline",
"user", "user",

View file

@ -115,8 +115,9 @@ class Image {
}); });
$converted = $this->setBaseName($path, $thumbnail, $img->extension); $converted = $this->setBaseName($path, $thumbnail, $img->extension);
$newPath = storage_path('app/'.$converted['path']); $newPath = storage_path('app/'.$converted['path']);
$img->save($newPath, 75); $quality = config('pixelfed.image_quality');
$img->save($newPath, $quality);
if(!$thumbnail) { if(!$thumbnail) {
$media->orientation = $orientation; $media->orientation = $orientation;

View file

@ -31,13 +31,9 @@ class Webfinger {
public function generateAliases() public function generateAliases()
{ {
$host = parse_url(config('app.url'), PHP_URL_HOST);
$username = $this->user->username;
$url = $this->user->url();
$this->aliases = [ $this->aliases = [
'acct:'.$username.'@'.$host, $this->user->url(),
$url $this->user->permalink()
]; ];
return $this; return $this;
} }
@ -55,24 +51,12 @@ class Webfinger {
[ [
'rel' => 'http://schemas.google.com/g/2010#updates-from', 'rel' => 'http://schemas.google.com/g/2010#updates-from',
'type' => 'application/atom+xml', 'type' => 'application/atom+xml',
'href' => url("/users/{$user->username}.atom") 'href' => $user->permalink('.atom')
], ],
[ [
'rel' => 'self', 'rel' => 'self',
'type' => 'application/activity+json', 'type' => 'application/activity+json',
'href' => $user->permalink() '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; return $this;

View file

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

2148
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -151,6 +151,7 @@ return [
* Package Service Providers... * Package Service Providers...
*/ */
Greggilbert\Recaptcha\RecaptchaServiceProvider::class, Greggilbert\Recaptcha\RecaptchaServiceProvider::class,
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
/* /*
* Application Service Providers... * Application Service Providers...
@ -211,6 +212,7 @@ return [
'View' => Illuminate\Support\Facades\View::class, 'View' => Illuminate\Support\Facades\View::class,
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::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

@ -76,7 +76,7 @@ return [
'connection' => 'redis', 'connection' => 'redis',
'queue' => ['default'], 'queue' => ['default'],
'balance' => 'simple', 'balance' => 'simple',
'processes' => 10, 'processes' => 20,
'tries' => 3, 'tries' => 3,
], ],
], ],

View file

@ -49,5 +49,5 @@ return [
* If set to `true` all output of the optimizer binaries will be appended to the default log. * 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`. * 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. | This value is the version of your PixelFed instance.
| |
*/ */
'version' => '0.1.0', 'version' => '0.1.4',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -77,6 +77,17 @@ return [
'activitypub_enabled' => env('ACTIVITY_PUB', false), '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', 1000000),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Photo file size limit | Photo file size limit
@ -95,7 +106,7 @@ return [
| Change the caption length limit for new local posts. | Change the caption length limit for new local posts.
| |
*/ */
'max_caption_length' => env('MAX_CAPTION_LENGTH', 150), 'max_caption_length' => env('MAX_CAPTION_LENGTH', 500),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -116,5 +127,15 @@ return [
| |
*/ */
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true), 'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
/*
|--------------------------------------------------------------------------
| Image Quality
|--------------------------------------------------------------------------
|
| Set the image optimization quality, must be a value between 1-100.
|
*/
'image_quality' => (int) env('IMAGE_QUALITY', 80),
]; ];

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,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" - "mysql-data:/var/lib/mysql"
redis: redis:
image: redis:alpine image: redis:4-alpine
volumes: volumes:
- "redis-data:/data" - "redis-data:/data"
networks: networks:

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/img/fred1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/js/app.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -1,13 +1,6 @@
window._ = require('lodash'); window._ = require('lodash');
window.Popper = require('popper.js').default; window.Popper = require('popper.js').default;
import swal from 'sweetalert';
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try { try {
window.pixelfed = {}; window.pixelfed = {};
window.$ = window.jQuery = require('jquery'); window.$ = window.jQuery = require('jquery');
@ -16,6 +9,7 @@ try {
window.filesize = require('filesize'); window.filesize = require('filesize');
window.typeahead = require('./lib/typeahead'); window.typeahead = require('./lib/typeahead');
window.Bloodhound = require('./lib/bloodhound'); window.Bloodhound = require('./lib/bloodhound');
window.Vue = require('vue');
require('./components/localstorage'); require('./components/localstorage');
require('./components/likebutton'); require('./components/likebutton');
@ -23,45 +17,21 @@ try {
require('./components/searchform'); require('./components/searchform');
require('./components/bookmarkform'); require('./components/bookmarkform');
require('./components/statusform'); require('./components/statusform');
Vue.component(
'follow-suggestions',
require('./components/FollowSuggestions.vue')
);
} catch (e) {} } catch (e) {}
/** $('[data-toggle="tooltip"]').tooltip();
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios'); window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]'); let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) { if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else { } else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
} }
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });

View file

@ -0,0 +1,50 @@
<style scoped>
</style>
<template>
<div class="card mb-4">
<div class="card-header bg-white">
<span class="font-weight-bold h5">Who to follow</span>
<span class="small float-right font-weight-bold">
<a href="javascript:void(0);" class="pr-2" v-on:click="fetchData">Refresh</a>
</span>
</div>
<div class="card-body">
<div v-if="results.length == 0">
<p class="mb-0 font-weight-bold">You are not following anyone yet, try the <a href="/discover">discover</a> feature to find users to follow.</p>
</div>
<div v-for="(user, index) in results">
<div class="media " style="width:100%">
<img class="mr-3" :src="user.avatar" width="40px">
<div class="media-body" style="width:70%">
<p class="my-0 font-weight-bold text-truncate" style="text-overflow: hidden">{{user.acct}} <span class="text-muted font-weight-normal">&commat;{{user.username}}</span></p>
<a class="btn btn-outline-primary px-3 py-0" :href="user.url" style="border-radius:20px;">Follow</a>
</div>
</div>
<div v-if="index != results.length - 1"><hr></div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
results: {},
};
},
mounted() {
this.fetchData();
},
methods: {
fetchData() {
axios.get('/api/local/i/follow-suggestions')
.then(response => {
this.results = response.data;
});
}
}
}
</script>

View file

@ -1,6 +1,12 @@
$(document).ready(function() { $(document).ready(function() {
$('.status-comment-focus').on('click', function(el) { $('.status-card > .card-footer').each(function() {
$(this).addClass('d-none');
});
$(document).on('click', '.status-comment-focus', function(el) {
var form = $(this).parents().eq(2).find('.card-footer');
form.removeClass('d-none');
var el = $(this).parents().eq(2).find('input[name="comment"]'); var el = $(this).parents().eq(2).find('input[name="comment"]');
el.focus(); el.focus();
}); });
@ -31,7 +37,7 @@ $(document).ready(function() {
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>'; 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); comments.append(comment);
commentform.val(''); commentform.val('');
commentform.blur(); commentform.blur();
@ -41,7 +47,5 @@ $(document).ready(function() {
.catch(function (res) { .catch(function (res) {
}); });
}); });
}); });

View file

@ -1,18 +1,17 @@
$(document).ready(function() { $(document).ready(function() {
if(!ls.get('likes')) { pixelfed.fetchLikes = () => {
axios.get('/api/v1/likes') axios.get('/api/v1/likes')
.then(function (res) { .then(function (res) {
ls.set('likes', res.data); ls.set('likes', res.data);
console.log(res); })
}) .catch(function (res) {
.catch(function (res) { ls.set('likes', []);
ls.set('likes', []); })
})
} }
pixelfed.hydrateLikes = function() { pixelfed.hydrateLikes = () => {
var likes = ls.get('likes'); var likes = ls.get('likes');
$('.like-form').each(function(i, el) { $('.like-form').each(function(i, el) {
var el = $(el); var el = $(el);
@ -20,11 +19,14 @@ $(document).ready(function() {
var heart = el.find('.status-heart'); var heart = el.find('.status-heart');
if(likes.indexOf(id) != -1) { if(likes.indexOf(id) != -1) {
heart.removeClass('far fa-heart').addClass('fas fa-heart'); heart.removeClass('text-dark').addClass('text-primary');
} else {
heart.removeClass('text-primary').addClass('text-dark');
} }
}); });
}; };
pixelfed.fetchLikes();
pixelfed.hydrateLikes(); pixelfed.hydrateLikes();
$(document).on('submit', '.like-form', function(e) { $(document).on('submit', '.like-form', function(e) {
@ -33,6 +35,8 @@ $(document).ready(function() {
var id = el.data('id'); var id = el.data('id');
axios.post('/i/like', {item: id}) axios.post('/i/like', {item: id})
.then(function (res) { .then(function (res) {
pixelfed.fetchLikes();
pixelfed.hydrateLikes();
var likes = ls.get('likes'); var likes = ls.get('likes');
var action = false; var action = false;
var counter = el.parents().eq(1).find('.like-count'); var counter = el.parents().eq(1).find('.like-count');
@ -40,14 +44,14 @@ $(document).ready(function() {
var heart = el.find('.status-heart'); var heart = el.find('.status-heart');
if(likes.indexOf(id) > -1) { if(likes.indexOf(id) > -1) {
heart.removeClass('fas fa-heart').addClass('far fa-heart'); heart.removeClass('text-primary').addClass('text-dark');
likes = likes.filter(function(item) { likes = likes.filter(function(item) {
return item !== id return item !== id
}); });
counter.text(count); counter.text(count);
action = 'unlike'; action = 'unlike';
} else { } else {
heart.removeClass('far fa-heart').addClass('fas fa-heart'); heart.removeClass('text-dark').addClass('text-primary');
likes.push(id); likes.push(id);
counter.text(count); counter.text(count);
action = 'like'; action = 'like';

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

@ -1,9 +1,5 @@
$(document).ready(function() { $(document).ready(function() {
$('#statusForm .btn-filter-select').on('click', function(e) {
let el = $(this);
});
pixelfed.create = {}; pixelfed.create = {};
pixelfed.filters = {}; pixelfed.filters = {};
pixelfed.create.hasGeneratedSelect = false; pixelfed.create.hasGeneratedSelect = false;
@ -78,7 +74,7 @@ $(document).ready(function() {
pixelfed.create.hasGeneratedSelect = true; pixelfed.create.hasGeneratedSelect = true;
} }
$('#fileInput').on('change', function() { $(document).on('change', '#fileInput', function() {
previewImage(this); previewImage(this);
$('#statusForm .form-filters.d-none').removeClass('d-none'); $('#statusForm .form-filters.d-none').removeClass('d-none');
$('#statusForm .form-preview.d-none').removeClass('d-none'); $('#statusForm .form-preview.d-none').removeClass('d-none');
@ -88,23 +84,43 @@ $(document).ready(function() {
} }
}); });
$('#filterSelectDropdown').on('change', function() { $(document).on('change', '#filterSelectDropdown', function() {
let el = $(this); let el = $(this);
let filter = el.val(); let filter = el.val();
let oldFilter = pixelfed.create.currentFilterClass; let oldFilter = pixelfed.create.currentFilterClass;
if(filter == 'none') { if(filter == 'none') {
$('.filterContainer').removeClass(oldFilter); $('input[name=filter_class]').val('');
pixelfed.create.currentFilterClass = false; $('input[name=filter_name]').val('');
pixelfed.create.currentFilterName = 'None'; $('.filterContainer').removeClass(oldFilter);
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected'); pixelfed.create.currentFilterClass = false;
return; pixelfed.create.currentFilterName = 'None';
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
return;
} else {
$('.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);
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);
}); });
$(document).on('keyup keydown', '#statusForm textarea[name=caption]', function() {
const el = $(this);
const len = el.val().length;
const limit = el.data('limit');
if(len > limit) {
const diff = limit - len;
$('#statusForm .caption-counter').text(diff).addClass('text-danger');
} else {
$('#statusForm .caption-counter').text(len).removeClass('text-danger');
}
});
$(document).on('focus', '#statusForm textarea[name=caption]', function() {
const el = $(this);
el.attr('rows', '3');
});
}); });

66
resources/assets/js/lib/bloodhound.js vendored Normal file → Executable file
View file

@ -1,18 +1,18 @@
/*! /*!
* typeahead.js 0.11.1 * typeahead.js 1.2.0
* https://github.com/twitter/typeahead.js * https://github.com/twitter/typeahead.js
* Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT * Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
*/ */
(function(root, factory) { (function(root, factory) {
if (typeof define === "function" && define.amd) { if (typeof define === "function" && define.amd) {
define("bloodhound", [ "jquery" ], function(a0) { define([ "jquery" ], function(a0) {
return root["Bloodhound"] = factory(a0); return root["Bloodhound"] = factory(a0);
}); });
} else if (typeof exports === "object") { } else if (typeof exports === "object") {
module.exports = factory(require("jquery")); module.exports = factory(require("jquery"));
} else { } else {
root["Bloodhound"] = factory(jQuery); root["Bloodhound"] = factory(root["jQuery"]);
} }
})(this, function($) { })(this, function($) {
var _ = function() { var _ = function() {
@ -148,18 +148,27 @@
stringify: function(val) { stringify: function(val) {
return _.isString(val) ? val : JSON.stringify(val); return _.isString(val) ? val : JSON.stringify(val);
}, },
guid: function() {
function _p8(s) {
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
}
return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
},
noop: function() {} noop: function() {}
}; };
}(); }();
var VERSION = "0.11.1"; var VERSION = "1.2.0";
var tokenizers = function() { var tokenizers = function() {
"use strict"; "use strict";
return { return {
nonword: nonword, nonword: nonword,
whitespace: whitespace, whitespace: whitespace,
ngram: ngram,
obj: { obj: {
nonword: getObjTokenizer(nonword), nonword: getObjTokenizer(nonword),
whitespace: getObjTokenizer(whitespace) whitespace: getObjTokenizer(whitespace),
ngram: getObjTokenizer(ngram)
} }
}; };
function whitespace(str) { function whitespace(str) {
@ -170,6 +179,19 @@
str = _.toStr(str); str = _.toStr(str);
return str ? str.split(/\W+/) : []; return str ? str.split(/\W+/) : [];
} }
function ngram(str) {
str = _.toStr(str);
var tokens = [], word = "";
_.each(str.split(""), function(char) {
if (char.match(/\s+/)) {
word = "";
} else {
tokens.push(word + char);
word += char;
}
});
return tokens;
}
function getObjTokenizer(tokenizer) { function getObjTokenizer(tokenizer) {
return function setKey(keys) { return function setKey(keys) {
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
@ -341,9 +363,10 @@
}(); }();
var Transport = function() { var Transport = function() {
"use strict"; "use strict";
var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10);
function Transport(o) { function Transport(o) {
o = o || {}; o = o || {};
this.maxPendingRequests = o.maxPendingRequests || 6;
this.cancelled = false; this.cancelled = false;
this.lastReq = null; this.lastReq = null;
this._send = o.transport; this._send = o.transport;
@ -351,7 +374,7 @@
this._cache = o.cache === false ? new LruCache(0) : sharedCache; this._cache = o.cache === false ? new LruCache(0) : sharedCache;
} }
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
maxPendingRequests = num; this.maxPendingRequests = num;
}; };
Transport.resetCache = function resetCache() { Transport.resetCache = function resetCache() {
sharedCache.reset(); sharedCache.reset();
@ -369,7 +392,7 @@
} }
if (jqXhr = pendingRequests[fingerprint]) { if (jqXhr = pendingRequests[fingerprint]) {
jqXhr.done(done).fail(fail); jqXhr.done(done).fail(fail);
} else if (pendingRequestsCount < maxPendingRequests) { } else if (pendingRequestsCount < this.maxPendingRequests) {
pendingRequestsCount++; pendingRequestsCount++;
pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
} else { } else {
@ -423,6 +446,7 @@
this.identify = o.identify || _.stringify; this.identify = o.identify || _.stringify;
this.datumTokenizer = o.datumTokenizer; this.datumTokenizer = o.datumTokenizer;
this.queryTokenizer = o.queryTokenizer; this.queryTokenizer = o.queryTokenizer;
this.matchAnyQueryToken = o.matchAnyQueryToken;
this.reset(); this.reset();
} }
_.mixin(SearchIndex.prototype, { _.mixin(SearchIndex.prototype, {
@ -459,7 +483,7 @@
tokens = normalizeTokens(this.queryTokenizer(query)); tokens = normalizeTokens(this.queryTokenizer(query));
_.each(tokens, function(token) { _.each(tokens, function(token) {
var node, chars, ch, ids; var node, chars, ch, ids;
if (matches && matches.length === 0) { if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
return false; return false;
} }
node = that.trie; node = that.trie;
@ -471,8 +495,10 @@
ids = node[IDS].slice(0); ids = node[IDS].slice(0);
matches = matches ? getIntersection(matches, ids) : ids; matches = matches ? getIntersection(matches, ids) : ids;
} else { } else {
matches = []; if (!that.matchAnyQueryToken) {
return false; matches = [];
return false;
}
} }
}); });
return matches ? _.map(unique(matches), function(id) { return matches ? _.map(unique(matches), function(id) {
@ -614,10 +640,12 @@
this.url = o.url; this.url = o.url;
this.prepare = o.prepare; this.prepare = o.prepare;
this.transform = o.transform; this.transform = o.transform;
this.indexResponse = o.indexResponse;
this.transport = new Transport({ this.transport = new Transport({
cache: o.cache, cache: o.cache,
limiter: o.limiter, limiter: o.limiter,
transport: o.transport transport: o.transport,
maxPendingRequests: o.maxPendingRequests
}); });
} }
_.mixin(Remote.prototype, { _.mixin(Remote.prototype, {
@ -655,7 +683,9 @@
identify: _.stringify, identify: _.stringify,
datumTokenizer: null, datumTokenizer: null,
queryTokenizer: null, queryTokenizer: null,
matchAnyQueryToken: false,
sufficient: 5, sufficient: 5,
indexRemote: false,
sorter: null, sorter: null,
local: [], local: [],
prefetch: null, prefetch: null,
@ -744,7 +774,7 @@
} else if (o.wildcard) { } else if (o.wildcard) {
prepare = prepareByWildcard; prepare = prepareByWildcard;
} else { } else {
prepare = idenityPrepare; prepare = identityPrepare;
} }
return prepare; return prepare;
function prepareByReplace(query, settings) { function prepareByReplace(query, settings) {
@ -755,7 +785,7 @@
settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
return settings; return settings;
} }
function idenityPrepare(query, settings) { function identityPrepare(query, settings) {
return settings; return settings;
} }
} }
@ -806,6 +836,7 @@
this.sorter = o.sorter; this.sorter = o.sorter;
this.identify = o.identify; this.identify = o.identify;
this.sufficient = o.sufficient; this.sufficient = o.sufficient;
this.indexRemote = o.indexRemote;
this.local = o.local; this.local = o.local;
this.remote = o.remote ? new Remote(o.remote) : null; this.remote = o.remote ? new Remote(o.remote) : null;
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
@ -875,6 +906,8 @@
}, },
search: function search(query, sync, async) { search: function search(query, sync, async) {
var that = this, local; var that = this, local;
sync = sync || _.noop;
async = async || _.noop;
local = this.sorter(this.index.search(query)); local = this.sorter(this.index.search(query));
sync(this.remote ? local.slice() : local); sync(this.remote ? local.slice() : local);
if (this.remote && local.length < this.sufficient) { if (this.remote && local.length < this.sufficient) {
@ -890,7 +923,8 @@
return that.identify(r) === that.identify(l); return that.identify(r) === that.identify(l);
}) && nonDuplicates.push(r); }) && nonDuplicates.push(r);
}); });
async && async(nonDuplicates); that.indexRemote && that.add(nonDuplicates);
async(nonDuplicates);
} }
}, },
all: function all() { all: function all() {

228
resources/assets/js/lib/typeahead.js vendored Normal file → Executable file
View file

@ -1,18 +1,18 @@
/*! /*!
* typeahead.js 0.11.1 * typeahead.js 1.2.0
* https://github.com/twitter/typeahead.js * https://github.com/twitter/typeahead.js
* Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT * Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
*/ */
(function(root, factory) { (function(root, factory) {
if (typeof define === "function" && define.amd) { if (typeof define === "function" && define.amd) {
define("typeahead.js", [ "jquery" ], function(a0) { define([ "jquery" ], function(a0) {
return factory(a0); return factory(a0);
}); });
} else if (typeof exports === "object") { } else if (typeof exports === "object") {
module.exports = factory(require("jquery")); module.exports = factory(require("jquery"));
} else { } else {
factory(jQuery); factory(root["jQuery"]);
} }
})(this, function($) { })(this, function($) {
var _ = function() { var _ = function() {
@ -148,6 +148,13 @@
stringify: function(val) { stringify: function(val) {
return _.isString(val) ? val : JSON.stringify(val); return _.isString(val) ? val : JSON.stringify(val);
}, },
guid: function() {
function _p8(s) {
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
}
return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
},
noop: function() {} noop: function() {}
}; };
}(); }();
@ -189,7 +196,7 @@
function buildHtml(c) { function buildHtml(c) {
return { return {
wrapper: '<span class="' + c.wrapper + '"></span>', wrapper: '<span class="' + c.wrapper + '"></span>',
menu: '<div class="' + c.menu + '"></div>' menu: '<div role="listbox" class="' + c.menu + '"></div>'
}; };
} }
function buildSelectors(classes) { function buildSelectors(classes) {
@ -264,10 +271,8 @@
} }
_.mixin(EventBus.prototype, { _.mixin(EventBus.prototype, {
_trigger: function(type, args) { _trigger: function(type, args) {
var $e; var $e = $.Event(namespace + type);
$e = $.Event(namespace + type); this.$el.trigger.call(this.$el, $e, args || []);
(args = args || []).unshift($e);
this.$el.trigger.apply(this.$el, args);
return $e; return $e;
}, },
before: function(type) { before: function(type) {
@ -384,7 +389,36 @@
tagName: "strong", tagName: "strong",
className: null, className: null,
wordsOnly: false, wordsOnly: false,
caseSensitive: false caseSensitive: false,
diacriticInsensitive: false
};
var accented = {
A: "[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ℀℁℻⒜Ⓐⓐ㍱-㍴㎀-㎄㎈㎉㎩-㎯㏂㏊㏟㏿Aa]",
B: "[BbᴮᵇḂ-ḇℬ⒝Ⓑⓑ㍴㎅-㎇㏃㏈㏔㏝Bb]",
C: "[CcÇçĆ-čᶜ℀ℂ℃℅℆ℭⅭⅽ⒞Ⓒⓒ㍶㎈㎉㎝㎠㎤㏄-㏇Cc]",
D: "[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟Ⓓⓓ㋏㍲㍷-㍹㎗㎭-㎯㏅㏈Dd]",
E: "[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑ℡ℯℰⅇ⒠Ⓔⓔ㉐㋍㋎Ee]",
F: "[FfᶠḞḟ℉℻⒡Ⓕⓕ㎊-㎌㎙ff-fflFf]",
G: "[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢Ⓖⓖ㋌㋍㎇㎍-㎏㎓㎬㏆㏉㏒㏿Gg]",
H: "[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣Ⓗⓗ㋌㍱㎐-㎔㏊㏋㏗Hh]",
I: "[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈⅠ-ⅣⅥ-ⅨⅪⅫⅰ-ⅳⅵ-ⅸⅺⅻ⒤Ⓘⓘ㍺㏌㏕fiffiIi]",
J: "[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]",
K: "[KkĶķǨǩᴷᵏḰ-ḵK⒦Ⓚⓚ㎄㎅㎉㎏㎑㎘㎞㎢㎦㎪㎸㎾㏀㏆㏍-㏏Kk]",
L: "[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓ℡Ⅼⅼ⒧Ⓛⓛ㋏㎈㎉㏐-㏓㏕㏖㏿flfflLl]",
M: "[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨Ⓜⓜ㍷-㍹㎃㎆㎎㎒㎖㎙-㎨㎫㎳㎷㎹㎽㎿㏁㏂㏎㏐㏔-㏖㏘㏙㏞㏟Mm]",
N: "[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩Ⓝⓝ㎁㎋㎚㎱㎵㎻㏌㏑Nn]",
O: "[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒ℅№ℴ⒪Ⓞⓞ㍵㏇㏒㏖Oo]",
P: "[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㉐㍱㍶㎀㎊㎩-㎬㎰㎴㎺㏋㏗-㏚Pp]",
Q: "[Qq⒬Ⓠⓠ㏃]",
R: "[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭Ⓡⓡ㋍㍴㎭-㎯㏚㏛Rr]",
S: "[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮Ⓢⓢ㎧㎨㎮-㎳㏛㏜stSs]",
T: "[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㉐㋏㎔㏏ſtstTt]",
U: "[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]",
V: "[VvᵛᵥṼ-ṿⅣ-Ⅷⅳ-ⅷ⒱Ⓥⓥⱽ㋎㍵㎴-㎹㏜㏞Vv]",
W: "[WwŴŵʷᵂẀ-ẉẘ⒲Ⓦⓦ㎺-㎿㏝Ww]",
X: "[XxˣẊ-ẍₓ℻Ⅸ-Ⅻⅸ-ⅻ⒳Ⓧⓧ㏓Xx]",
Y: "[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴Ⓨⓨ㏉Yy]",
Z: "[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵Ⓩⓩ㎐-㎔Zz]"
}; };
return function hightlight(o) { return function hightlight(o) {
var regex; var regex;
@ -393,7 +427,7 @@
return; return;
} }
o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive);
traverse(o.node, hightlightTextNode); traverse(o.node, hightlightTextNode);
function hightlightTextNode(textNode) { function hightlightTextNode(textNode) {
var match, patternNode, wrapperNode; var match, patternNode, wrapperNode;
@ -419,10 +453,17 @@
} }
} }
}; };
function getRegex(patterns, caseSensitive, wordsOnly) { function accent_replacer(chr) {
return accented[chr.toUpperCase()] || chr;
}
function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) {
var escapedPatterns = [], regexStr; var escapedPatterns = [], regexStr;
for (var i = 0, len = patterns.length; i < len; i++) { for (var i = 0, len = patterns.length; i < len; i++) {
escapedPatterns.push(_.escapeRegExChars(patterns[i])); var escapedWord = _.escapeRegExChars(patterns[i]);
if (diacriticInsensitive) {
escapedWord = escapedWord.replace(/\S/g, accent_replacer);
}
escapedPatterns.push(escapedWord);
} }
regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
@ -448,6 +489,14 @@
www.mixin(this); www.mixin(this);
this.$hint = $(o.hint); this.$hint = $(o.hint);
this.$input = $(o.input); this.$input = $(o.input);
this.$input.attr({
"aria-activedescendant": "",
"aria-owns": this.$input.attr("id") + "_listbox",
role: "combobox",
"aria-readonly": "true",
"aria-autocomplete": "list"
});
$(www.menu).attr("id", this.$input.attr("id") + "_listbox");
this.query = this.$input.val(); this.query = this.$input.val();
this.queryWhenFocused = this.hasFocus() ? this.query : null; this.queryWhenFocused = this.hasFocus() ? this.query : null;
this.$overflowHelper = buildOverflowHelper(this.$input); this.$overflowHelper = buildOverflowHelper(this.$input);
@ -455,6 +504,7 @@
if (this.$hint.length === 0) { if (this.$hint.length === 0) {
this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
} }
this.onSync("cursorchange", this._updateDescendent);
} }
Input.normalizeQuery = function(str) { Input.normalizeQuery = function(str) {
return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
@ -524,6 +574,9 @@
this.trigger("whitespaceChanged", this.query); this.trigger("whitespaceChanged", this.query);
} }
}, },
_updateDescendent: function updateDescendent(event, id) {
this.$input.attr("aria-activedescendant", id);
},
bind: function() { bind: function() {
var that = this, onBlur, onFocus, onKeydown, onInput; var that = this, onBlur, onFocus, onKeydown, onInput;
onBlur = _.bind(this._onBlur, this); onBlur = _.bind(this._onBlur, this);
@ -647,6 +700,7 @@
"use strict"; "use strict";
var keys, nameGenerator; var keys, nameGenerator;
keys = { keys = {
dataset: "tt-selectable-dataset",
val: "tt-selectable-display", val: "tt-selectable-display",
obj: "tt-selectable-object" obj: "tt-selectable-object"
}; };
@ -666,19 +720,20 @@
} }
www.mixin(this); www.mixin(this);
this.highlight = !!o.highlight; this.highlight = !!o.highlight;
this.name = o.name || nameGenerator(); this.name = _.toStr(o.name || nameGenerator());
this.limit = o.limit || 5; this.limit = o.limit || 5;
this.displayFn = getDisplayFn(o.display || o.displayKey); this.displayFn = getDisplayFn(o.display || o.displayKey);
this.templates = getTemplates(o.templates, this.displayFn); this.templates = getTemplates(o.templates, this.displayFn);
this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
this._resetLastSuggestion(); this._resetLastSuggestion();
this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); this.$el = $(o.node).attr("role", "presentation").addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
} }
Dataset.extractData = function extractData(el) { Dataset.extractData = function extractData(el) {
var $el = $(el); var $el = $(el);
if ($el.data(keys.obj)) { if ($el.data(keys.obj)) {
return { return {
dataset: $el.data(keys.dataset) || "",
val: $el.data(keys.val) || "", val: $el.data(keys.val) || "",
obj: $el.data(keys.obj) || null obj: $el.data(keys.obj) || null
}; };
@ -697,7 +752,7 @@
} else { } else {
this._empty(); this._empty();
} }
this.trigger("rendered", this.name, suggestions, false); this.trigger("rendered", suggestions, false, this.name);
}, },
_append: function append(query, suggestions) { _append: function append(query, suggestions) {
suggestions = suggestions || []; suggestions = suggestions || [];
@ -708,7 +763,7 @@
} else if (!this.$lastSuggestion.length && this.templates.notFound) { } else if (!this.$lastSuggestion.length && this.templates.notFound) {
this._renderNotFound(query); this._renderNotFound(query);
} }
this.trigger("rendered", this.name, suggestions, true); this.trigger("rendered", suggestions, true, this.name);
}, },
_renderSuggestions: function renderSuggestions(query, suggestions) { _renderSuggestions: function renderSuggestions(query, suggestions) {
var $fragment; var $fragment;
@ -749,7 +804,7 @@
_.each(suggestions, function getSuggestionNode(suggestion) { _.each(suggestions, function getSuggestionNode(suggestion) {
var $el, context; var $el, context;
context = that._injectQuery(query, suggestion); context = that._injectQuery(query, suggestion);
$el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); $el = $(that.templates.suggestion(context)).data(keys.dataset, that.name).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
fragment.appendChild($el[0]); fragment.appendChild($el[0]);
}); });
this.highlight && highlight({ this.highlight && highlight({
@ -787,7 +842,7 @@
this.cancel = function cancel() { this.cancel = function cancel() {
canceled = true; canceled = true;
that.cancel = $.noop; that.cancel = $.noop;
that.async && that.trigger("asyncCanceled", query); that.async && that.trigger("asyncCanceled", query, that.name);
}; };
this.source(query, sync, async); this.source(query, sync, async);
!syncCalled && sync([]); !syncCalled && sync([]);
@ -800,16 +855,17 @@
rendered = suggestions.length; rendered = suggestions.length;
that._overwrite(query, suggestions); that._overwrite(query, suggestions);
if (rendered < that.limit && that.async) { if (rendered < that.limit && that.async) {
that.trigger("asyncRequested", query); that.trigger("asyncRequested", query, that.name);
} }
} }
function async(suggestions) { function async(suggestions) {
suggestions = suggestions || []; suggestions = suggestions || [];
if (!canceled && rendered < that.limit) { if (!canceled && rendered < that.limit) {
that.cancel = $.noop; that.cancel = $.noop;
rendered += suggestions.length; var idx = Math.abs(rendered - that.limit);
that._append(query, suggestions.slice(0, that.limit - rendered)); rendered += idx;
that.async && that.trigger("asyncReceived", query); that._append(query, suggestions.slice(0, idx));
that.async && that.trigger("asyncReceived", query, that.name);
} }
} }
}, },
@ -843,7 +899,7 @@
suggestion: templates.suggestion || suggestionTemplate suggestion: templates.suggestion || suggestionTemplate
}; };
function suggestionTemplate(context) { function suggestionTemplate(context) {
return $("<div>").text(displayFn(context)); return $('<div role="option">').attr("id", _.guid()).text(displayFn(context));
} }
} }
function isValidName(str) { function isValidName(str) {
@ -884,10 +940,11 @@
this.trigger.apply(this, arguments); this.trigger.apply(this, arguments);
}, },
_allDatasetsEmpty: function allDatasetsEmpty() { _allDatasetsEmpty: function allDatasetsEmpty() {
return _.every(this.datasets, isDatasetEmpty); return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) {
function isDatasetEmpty(dataset) { var isEmpty = dataset.isEmpty();
return dataset.isEmpty(); this.$node.attr("aria-expanded", !isEmpty);
} return isEmpty;
}, this));
}, },
_getSelectables: function getSelectables() { _getSelectables: function getSelectables() {
return this.$node.find(this.selectors.selectable); return this.$node.find(this.selectors.selectable);
@ -912,6 +969,12 @@
var that = this, onSelectableClick; var that = this, onSelectableClick;
onSelectableClick = _.bind(this._onSelectableClick, this); onSelectableClick = _.bind(this._onSelectableClick, this);
this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
this.$node.on("mouseover", this.selectors.selectable, function() {
that.setCursor($(this));
});
this.$node.on("mouseleave", function() {
that._removeCursor();
});
_.each(this.datasets, function(dataset) { _.each(this.datasets, function(dataset) {
dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
}); });
@ -921,9 +984,11 @@
return this.$node.hasClass(this.classes.open); return this.$node.hasClass(this.classes.open);
}, },
open: function open() { open: function open() {
this.$node.scrollTop(0);
this.$node.addClass(this.classes.open); this.$node.addClass(this.classes.open);
}, },
close: function close() { close: function close() {
this.$node.attr("aria-expanded", false);
this.$node.removeClass(this.classes.open); this.$node.removeClass(this.classes.open);
this._removeCursor(); this._removeCursor();
}, },
@ -988,6 +1053,55 @@
}); });
return Menu; return Menu;
}(); }();
var Status = function() {
"use strict";
function Status(options) {
this.$el = $("<span></span>", {
role: "status",
"aria-live": "polite"
}).css({
position: "absolute",
padding: "0",
border: "0",
height: "1px",
width: "1px",
"margin-bottom": "-1px",
"margin-right": "-1px",
overflow: "hidden",
clip: "rect(0 0 0 0)",
"white-space": "nowrap"
});
options.$input.after(this.$el);
_.each(options.menu.datasets, _.bind(function(dataset) {
if (dataset.onSync) {
dataset.onSync("rendered", _.bind(this.update, this));
dataset.onSync("cleared", _.bind(this.cleared, this));
}
}, this));
}
_.mixin(Status.prototype, {
update: function update(event, suggestions) {
var length = suggestions.length;
var words;
if (length === 1) {
words = {
result: "result",
is: "is"
};
} else {
words = {
result: "results",
is: "are"
};
}
this.$el.text(length + " " + words.result + " " + words.is + " available, use up and down arrow keys to navigate.");
},
cleared: function() {
this.$el.text("");
}
});
return Status;
}();
var DefaultMenu = function() { var DefaultMenu = function() {
"use strict"; "use strict";
var s = Menu.prototype; var s = Menu.prototype;
@ -1052,6 +1166,7 @@
this.input = o.input; this.input = o.input;
this.menu = o.menu; this.menu = o.menu;
this.enabled = true; this.enabled = true;
this.autoselect = !!o.autoselect;
this.active = false; this.active = false;
this.input.hasFocus() && this.activate(); this.input.hasFocus() && this.activate();
this.dir = this.input.getLangDir(); this.dir = this.input.getLangDir();
@ -1098,8 +1213,12 @@
_onDatasetCleared: function onDatasetCleared() { _onDatasetCleared: function onDatasetCleared() {
this._updateHint(); this._updateHint();
}, },
_onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { _onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) {
this._updateHint(); this._updateHint();
if (this.autoselect) {
var cursorClass = this.selectors.cursor.substr(1);
this.menu.$node.find(this.selectors.suggestion).first().addClass(cursorClass);
}
this.eventBus.trigger("render", suggestions, async, dataset); this.eventBus.trigger("render", suggestions, async, dataset);
}, },
_onAsyncRequested: function onAsyncRequested(type, dataset, query) { _onAsyncRequested: function onAsyncRequested(type, dataset, query) {
@ -1122,7 +1241,15 @@
_onEnterKeyed: function onEnterKeyed(type, $e) { _onEnterKeyed: function onEnterKeyed(type, $e) {
var $selectable; var $selectable;
if ($selectable = this.menu.getActiveSelectable()) { if ($selectable = this.menu.getActiveSelectable()) {
this.select($selectable) && $e.preventDefault(); if (this.select($selectable)) {
$e.preventDefault();
$e.stopPropagation();
}
} else if (this.autoselect) {
if (this.select(this.menu.getTopSelectable())) {
$e.preventDefault();
$e.stopPropagation();
}
} }
}, },
_onTabKeyed: function onTabKeyed(type, $e) { _onTabKeyed: function onTabKeyed(type, $e) {
@ -1144,12 +1271,12 @@
}, },
_onLeftKeyed: function onLeftKeyed() { _onLeftKeyed: function onLeftKeyed() {
if (this.dir === "rtl" && this.input.isCursorAtEnd()) { if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getTopSelectable()); this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
} }
}, },
_onRightKeyed: function onRightKeyed() { _onRightKeyed: function onRightKeyed() {
if (this.dir === "ltr" && this.input.isCursorAtEnd()) { if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getTopSelectable()); this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
} }
}, },
_onQueryChanged: function onQueryChanged(e, query) { _onQueryChanged: function onQueryChanged(e, query) {
@ -1249,9 +1376,9 @@
}, },
select: function select($selectable) { select: function select($selectable) {
var data = this.menu.getSelectableData($selectable); var data = this.menu.getSelectableData($selectable);
if (data && !this.eventBus.before("select", data.obj)) { if (data && !this.eventBus.before("select", data.obj, data.dataset)) {
this.input.setQuery(data.val, true); this.input.setQuery(data.val, true);
this.eventBus.trigger("select", data.obj); this.eventBus.trigger("select", data.obj, data.dataset);
this.close(); this.close();
return true; return true;
} }
@ -1262,21 +1389,24 @@
query = this.input.getQuery(); query = this.input.getQuery();
data = this.menu.getSelectableData($selectable); data = this.menu.getSelectableData($selectable);
isValid = data && query !== data.val; isValid = data && query !== data.val;
if (isValid && !this.eventBus.before("autocomplete", data.obj)) { if (isValid && !this.eventBus.before("autocomplete", data.obj, data.dataset)) {
this.input.setQuery(data.val); this.input.setQuery(data.val);
this.eventBus.trigger("autocomplete", data.obj); this.eventBus.trigger("autocomplete", data.obj, data.dataset);
return true; return true;
} }
return false; return false;
}, },
moveCursor: function moveCursor(delta) { moveCursor: function moveCursor(delta) {
var query, $candidate, data, payload, cancelMove; var query, $candidate, data, suggestion, datasetName, cancelMove, id;
query = this.input.getQuery(); query = this.input.getQuery();
$candidate = this.menu.selectableRelativeToCursor(delta); $candidate = this.menu.selectableRelativeToCursor(delta);
data = this.menu.getSelectableData($candidate); data = this.menu.getSelectableData($candidate);
payload = data ? data.obj : null; suggestion = data ? data.obj : null;
datasetName = data ? data.dataset : null;
id = $candidate ? $candidate.attr("id") : null;
this.input.trigger("cursorchange", id);
cancelMove = this._minLengthMet() && this.menu.update(query); cancelMove = this._minLengthMet() && this.menu.update(query);
if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) {
this.menu.setCursor($candidate); this.menu.setCursor($candidate);
if (data) { if (data) {
this.input.setInputValue(data.val); this.input.setInputValue(data.val);
@ -1284,7 +1414,7 @@
this.input.resetInputValue(); this.input.resetInputValue();
this._updateHint(); this._updateHint();
} }
this.eventBus.trigger("cursorchange", payload); this.eventBus.trigger("cursorchange", suggestion, datasetName);
return true; return true;
} }
return false; return false;
@ -1322,7 +1452,7 @@
www = WWW(o.classNames); www = WWW(o.classNames);
return this.each(attach); return this.each(attach);
function attach() { function attach() {
var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, status, typeahead, MenuConstructor;
_.each(datasets, function(d) { _.each(datasets, function(d) {
d.highlight = !!o.highlight; d.highlight = !!o.highlight;
}); });
@ -1353,11 +1483,16 @@
node: $menu, node: $menu,
datasets: datasets datasets: datasets
}, www); }, www);
status = new Status({
$input: $input,
menu: menu
});
typeahead = new Typeahead({ typeahead = new Typeahead({
input: input, input: input,
menu: menu, menu: menu,
eventBus: eventBus, eventBus: eventBus,
minLength: o.minLength minLength: o.minLength,
autoselect: o.autoselect
}, www); }, www);
$input.data(keys.www, www); $input.data(keys.www, www);
$input.data(keys.typeahead, typeahead); $input.data(keys.typeahead, typeahead);
@ -1450,7 +1585,7 @@
return query; return query;
} else { } else {
ttEach(this, function(t) { ttEach(this, function(t) {
t.setVal(newVal); t.setVal(_.toStr(newVal));
}); });
return this; return this;
} }
@ -1481,8 +1616,10 @@
}); });
} }
function buildHintFromInput($input, www) { function buildHintFromInput($input, www) {
return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({
autocomplete: "off", readonly: true,
required: false
}).removeAttr("id name placeholder").removeClass("required").attr({
spellcheck: "false", spellcheck: "false",
tabindex: -1 tabindex: -1
}); });
@ -1495,7 +1632,6 @@
style: $input.attr("style") style: $input.attr("style")
}); });
$input.addClass(www.classes.input).attr({ $input.addClass(www.classes.input).attr({
autocomplete: "off",
spellcheck: false spellcheck: false
}); });
try { try {

View file

@ -1,13 +1,54 @@
$(document).ready(function() { $(document).ready(function() {
$('.pagination').hide(); $('.pagination').hide();
$('.container.timeline-container').removeClass('d-none');
let elem = document.querySelector('.timeline-feed'); let elem = document.querySelector('.timeline-feed');
pixelfed.fetchLikes();
let infScroll = new InfiniteScroll( elem, { let infScroll = new InfiniteScroll( elem, {
path: '.pagination__next', path: '.pagination__next',
append: '.timeline-feed', append: '.timeline-feed',
status: '.page-load-status', status: '.page-load-status',
history: false, history: false,
}); });
infScroll.on( 'append', function( response, path, items ) { infScroll.on( 'append', function( response, path, items ) {
pixelfed.hydrateLikes(); pixelfed.hydrateLikes();
$('.status-card > .card-footer').each(function() {
var el = $(this);
if(!el.hasClass('d-none') && !el.find('input[name="comment"]').val()) {
$(this).addClass('d-none');
}
});
}); });
});
$(document).on("DOMContentLoaded", function() {
var active = false;
var lazyLoad = function() {
if (active === false) {
active = true;
var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
lazyImages.forEach(function(lazyImage) {
if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
lazyImage.classList.remove("lazy");
lazyImages = lazyImages.filter(function(image) {
return image !== lazyImage;
});
}
});
active = false;
};
}
document.addEventListener("scroll", lazyLoad);
window.addEventListener("resize", lazyLoad);
window.addEventListener("orientationchange", lazyLoad);
}); });

View file

@ -173,11 +173,6 @@ body, button, input, textarea {
} }
} }
.fas.fa-heart {
color: #f70ec4!important;
}
@media (max-width: map-get($grid-breakpoints, "md")) { @media (max-width: map-get($grid-breakpoints, "md")) {
.border-md-left-0 { .border-md-left-0 {
border-left:0!important border-left:0!important
@ -266,3 +261,36 @@ body, button, input, textarea {
animation-name: fadeInDown; animation-name: fadeInDown;
animation-duration: 0.5s; animation-duration: 0.5s;
} }
.card {
box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
border: none;
}
.box-shadow {
box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
}
.border-left-primary {
border-left: 3px solid $primary;
}
.settings-nav .nav-item.active .nav-link {
font-weight: bold !important;
}
details summary::-webkit-details-marker {
display: none!important;
}
.details-animated > summary {
display: block;
background-color: #ECF0F1;
padding-top: 50px;
padding-bottom: 50px;
text-align: center;
}
.details-animated[open] > summary {
display: none!important;
}

View file

@ -9,5 +9,6 @@ return [
'settings' => 'Einstellungen', 'settings' => 'Einstellungen',
'admin' => 'Administration', 'admin' => 'Administration',
'logout' => 'Abmelden', 'logout' => 'Abmelden',
'directMessages' => 'Privatnachrichten',
]; ];

View file

@ -5,4 +5,4 @@ return [
'emptyFollowers' => 'Diesem Benutzer folgt noch niemand!', 'emptyFollowers' => 'Diesem Benutzer folgt noch niemand!',
'emptyFollowing' => 'Dieser Benutzer folgt noch niemanden!', 'emptyFollowing' => 'Dieser Benutzer folgt noch niemanden!',
'savedWarning' => 'Nur du kannst sehen was du gespeichert hast', 'savedWarning' => 'Nur du kannst sehen was du gespeichert hast',
]; ];

View file

@ -9,5 +9,6 @@ return [
'settings' => 'Settings', 'settings' => 'Settings',
'admin' => 'Admin', 'admin' => 'Admin',
'logout' => 'Logout', 'logout' => 'Logout',
'directMessages' => 'Direct Messages',
]; ];

View file

@ -0,0 +1,10 @@
<?php
return [
'viewMyProfile' => 'Voir mon profil',
'myTimeline' => 'Ma chronologie',
'publicTimeline' => 'Chronologie publique',
'remoteFollow' => 'Suivre à distance',
'settings' => 'Paramètres',
'admin' => 'Admin',
'logout' => ' Se déconnecter',
];

View file

@ -2,4 +2,6 @@
return [ return [
'likedPhoto' => 'a aimé votre photo.', 'likedPhoto' => 'a aimé votre photo.',
'startedFollowingYou' => 'a commencé à vous suivre.', 'startedFollowingYou' => 'a commencé à vous suivre.',
'commented' => 'commenté sur votre post.',
'mentionedYou' => 'vous à mentionné.'
]; ];

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'Ver perfil',
'myTimeline' => 'A miña liña temporal',
'publicTimeline' => 'Liña temporal pública',
'remoteFollow' => 'Seguimento remoto',
'settings' => 'Axustes',
'admin' => 'Admin',
'logout' => 'Saír',
];

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'Veire mon perfil',
'myTimeline' => 'Ma cronologia',
'publicTimeline' => 'Cronologia publica',
'remoteFollow' => 'Seguir a distància',
'settings' => 'Paramètres',
'admin' => 'Admin',
'logout' => 'Desconnexion',
];

View file

@ -4,5 +4,7 @@ return [
'likedPhoto' => 'a aimat vòstra fòto.', 'likedPhoto' => 'a aimat vòstra fòto.',
'startedFollowingYou' => 'a començat de vos seguir.', 'startedFollowingYou' => 'a començat de vos seguir.',
'commented' => 'a comentat vòstra publicacion.',
'mentionedYou' => 'vos a mencionat.'
]; ];

View file

@ -1,5 +1,8 @@
<?php <?php
return [ return [
'emptyTimeline' => 'Aqueste utilizaire a pas encara de publicacion !', 'emptyTimeline' => 'Aqueste utilizaire a pas encara de publicacion!',
'emptyFollowers' => 'Aqueste utilizaire a pas encara pas seguidors!',
'emptyFollowing' => 'Aqueste utilizaire sèc degun pel moment!',
'savedWarning' => 'Solament vos vesètz çò que salvagardatz',
]; ];

View file

@ -0,0 +1,7 @@
<?php
return [
'emptyPersonalTimeline' => 'Vòstre cronologia es voida.'
];

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'Pokaż mój profil',
'myTimeline' => 'Moja oś czasu',
'publicTimeline' => 'Publiczna oś czasu',
'remoteFollow' => 'Zdalne śledzenie',
'settings' => 'Ustawienia',
'admin' => 'Administrator',
'logout' => 'Wyloguj się',
];

View file

@ -4,5 +4,7 @@ return [
'likedPhoto' => 'polubił Twoje zdjęcie.', 'likedPhoto' => 'polubił Twoje zdjęcie.',
'startedFollowingYou' => 'zaczął Cię obserwować.', 'startedFollowingYou' => 'zaczął Cię obserwować.',
'commented' => 'skomentował Twój wpis',
'mentionedYou' => 'wspomniał o Tobie.'
]; ];

View file

@ -0,0 +1,35 @@
<?php
return [
'exception_message' => 'Mensaxe da exepción: :message',
'exception_trace' => 'Traza da excepción: :trace',
'exception_message_title' => 'Mensaxe da excepción',
'exception_trace_title' => 'Traza da excepción',
'backup_failed_subject' => 'Erro no respaldo de :application_name',
'backup_failed_body' => 'Importante: Algo fallou ao respaldar :application_name',
'backup_successful_subject' => 'Respaldo realizado correctamente :application_name',
'backup_successful_subject_title' => 'Novo respaldo correcto!',
'backup_successful_body' => 'Parabéns, un novo respaldo de :application_name foi realizado correctamente no disco con nome :disk_name.',
'cleanup_failed_subject' => 'Limpando os respaldos de :application_name failed.',
'cleanup_failed_body' => 'Algo fallou mentras se limpaban os respaldos de :application_name',
'cleanup_successful_subject' => 'Limpeza correcta nos respaldos de :application_name',
'cleanup_successful_subject_title' => 'Limpeza dos respaldos correcta!',
'cleanup_successful_body' => 'Realizouse correctamente a limpeza dos respaldos de :application_name no disco con nome :disk_name.',
'healthy_backup_found_subject' => 'Os respaldos de :application_name no disco :disk_name están en bo estado',
'healthy_backup_found_subject_title' => 'Os respaldos de :application_name están ben!',
'healthy_backup_found_body' => 'Os respaldos de :application_name están en bo estado. Bo traballo!',
'unhealthy_backup_found_subject' => 'Importante: Os respaldos de :application_name non están en bo estado',
'unhealthy_backup_found_subject_title' => 'Importante: Os respaldos de :application_name non están ben. :problem',
'unhealthy_backup_found_body' => 'Os respaldos para :application_name no disco :disk_name non están ben.',
'unhealthy_backup_found_not_reachable' => 'Non se puido alcanzar o disco de destino. :error',
'unhealthy_backup_found_empty' => 'Non existen copias de respaldo para esta aplicación.',
'unhealthy_backup_found_old' => 'O último respaldo realizouse en :date e considerase demasiado antigo.',
'unhealthy_backup_found_unknown' => 'Lamentámolo, non se puido determinar unha causa concreta.',
'unhealthy_backup_found_full' => 'Os respaldos están a utilizar demasiado espazo. A utilización actual de :disk_usage é maior que o límite establecido de :disk_limit.',
];

View file

@ -3,16 +3,53 @@
@section('content') @section('content')
<div class="container notification-page" style="min-height: 60vh;"> <div class="container notification-page" style="min-height: 60vh;">
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 col-md-8 offset-md-2">
<div class="card mt-3">
<div class="card-body p-0">
<ul class="nav nav-tabs d-flex text-center">
{{--
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase" href="{{route('notifications.following')}}">Following</a>
</li>
--}}
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">My Notifications</a>
</li>
</ul>
</div>
</div>
<div class="">
<div class="dropdown text-right mt-2">
<a class="btn btn-link btn-sm dropdown-toggle font-weight-bold text-dark" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Filter
</a>
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a href="?a=comment" class="dropdown-item font-weight-bold" title="Commented on your post">
Comments only
</a>
<a href="?a=follow" class="dropdown-item font-weight-bold" title="Followed you">
New Followers only
</a>
<a href="?a=mention" class="dropdown-item font-weight-bold" title="Mentioned you">
Mentions only
</a>
<a href="{{route('notifications')}}" class="dropdown-item font-weight-bold text-dark">
View All
</a>
</div>
</div>
</div>
<ul class="list-group"> <ul class="list-group">
@if($notifications->count() > 0) @if($notifications->count() > 0)
@foreach($notifications as $notification) @foreach($notifications as $notification)
<li class="list-group-item notification"> <li class="list-group-item notification border-0">
@switch($notification->action) @switch($notification->action)
@case('like') @case('like')
<span class="notification-icon pr-3"> <span class="notification-icon pr-3">
<img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle"> <img src="{{optional($notification->actor, function($actor) {
return $actor->avatarUrl(); }) }}" width="32px" class="rounded-circle">
</span> </span>
<span class="notification-text"> <span class="notification-text">
{!! $notification->rendered !!} {!! $notification->rendered !!}

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