mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-26 00:03:16 +00:00
Merge branch 'frontend-ui-refactor' into patch-23
This commit is contained in:
commit
d3daa9b084
123 changed files with 5420 additions and 1368 deletions
34
.env.example
34
.env.example
|
@ -1,46 +1,52 @@
|
|||
APP_NAME=Laravel
|
||||
APP_NAME="PixelFed Test"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
ADMIN_DOMAIN="localhost"
|
||||
APP_DOMAIN="localhost"
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=homestead
|
||||
DB_USERNAME=homestead
|
||||
DB_PASSWORD=secret
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
CACHE_DRIVER=redis
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_LIFETIME=120
|
||||
QUEUE_DRIVER=sync
|
||||
QUEUE_DRIVER=redis
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_DRIVER=log
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="pixelfed@example.com"
|
||||
MAIL_FROM_NAME="Pixelfed"
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
SESSION_DOMAIN=".pixelfed.dev"
|
||||
SESSION_DOMAIN="${APP_DOMAIN}"
|
||||
SESSION_SECURE_COOKIE=true
|
||||
API_BASE="/api/1/"
|
||||
API_SEARCH="/api/search"
|
||||
|
||||
OPEN_REGISTRATION=true
|
||||
RECAPTCHA_ENABLED=false
|
||||
ENFORCE_EMAIL_VERIFICATION=true
|
||||
|
||||
MAX_PHOTO_SIZE=15000
|
||||
MAX_CAPTION_LENGTH=150
|
||||
MAX_ALBUM_LENGTH=4
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
|
78
README.md
78
README.md
|
@ -1,4 +1,76 @@
|
|||
# PixelFed
|
||||
Federated Image Sharing
|
||||
# PixelFed: Federated Image Sharing
|
||||
|
||||
> This project is still in active development and not yet ready for use.
|
||||
PixelFed is a federated social image sharing platform, similar to Instagram.
|
||||
Federation is done using the [ActivityPub](https://activitypub.rocks/) protocol,
|
||||
which is used by [Mastodon](http://joinmastodon.org/), [PeerTube](https://joinpeertube.org/en/),
|
||||
[Pleroma](https://pleroma.social/), and more. Through ActivityPub PixelFed can share
|
||||
and interact with these platforms, as well as other instances of PixelFed.
|
||||
|
||||
**_Please note this is alpha software, not recommended for production use,
|
||||
and federation is not supported yet._**
|
||||
|
||||
PixelFed is very early into the development stage. If you would like to have a
|
||||
permanent instance with minimal breakage, **do not use this software until
|
||||
there is a stable release**. The following setup instructions are intended for
|
||||
testing and development.
|
||||
|
||||
## Requirements
|
||||
- PHP >= 7.1.3 (7.2+ recommended for stable version)
|
||||
- MySQL, Postgres (MariaDB and sqlite are not supported yet)
|
||||
- Redis
|
||||
- Composer
|
||||
- GD or ImageMagick
|
||||
- OpenSSL PHP Extension
|
||||
- PDO PHP Extension
|
||||
- Mbstring PHP Extension
|
||||
- Tokenizer PHP Extension
|
||||
- XML PHP Extension
|
||||
- Ctype PHP Extension
|
||||
- JSON PHP Extension
|
||||
- JpegOptim
|
||||
- Optipng
|
||||
- Pngquant 2
|
||||
- SVGO
|
||||
- Gifsicle
|
||||
|
||||
## Installation
|
||||
|
||||
This guide assumes you have NGINX/Apache installed, along with the dependencies.
|
||||
Those will not be covered in these early docs.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dansup/pixelfed.git
|
||||
cd pixelfed
|
||||
composer install
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
**Edit .env file with proper values**
|
||||
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
```bash
|
||||
php artisan storage:link
|
||||
php artisan migrate
|
||||
php artisan horizon
|
||||
php artisan serve --host=localhost --port=80
|
||||
```
|
||||
|
||||
Check your browser at http://localhost
|
||||
|
||||
## Communication
|
||||
|
||||
The ways you can communicate on the project are below. Before interacting, please
|
||||
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
* IRC: #pixelfed on irc.freenode.net ([#freenode_#pixelfed:matrix.org through
|
||||
Matrix](https://matrix.to/#/#freenode_#pixelfed:matrix.org)
|
||||
* Project on Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
|
||||
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
|
||||
|
||||
## Support
|
||||
|
||||
The lead maintainer is on Patreon! You can become a Patron at
|
||||
https://www.patreon.com/dansup
|
||||
|
|
10
app/AccountLog.php
Normal file
10
app/AccountLog.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AccountLog extends Model
|
||||
{
|
||||
//
|
||||
}
|
36
app/Events/AuthLoginEvent.php
Normal file
36
app/Events/AuthLoginEvent.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Hashtag extends Model
|
||||
{
|
||||
protected $fillable = ['name','slug'];
|
||||
public $fillable = ['name','slug'];
|
||||
|
||||
public function posts()
|
||||
{
|
||||
|
|
|
@ -18,19 +18,47 @@ class AccountController extends Controller
|
|||
public function notifications(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3'
|
||||
'page' => 'nullable|min:1|max:3',
|
||||
'a' => 'nullable|alpha_dash',
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$action = $request->input('a');
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->take(30)
|
||||
->simplePaginate();
|
||||
if($action && in_array($action, ['comment', 'follow', 'mention'])) {
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereAction($action)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->simplePaginate(30);
|
||||
} else {
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->simplePaginate(30);
|
||||
}
|
||||
|
||||
return view('account.activity', compact('profile', 'notifications'));
|
||||
}
|
||||
|
||||
public function 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)
|
||||
{
|
||||
return view('account.verify_email');
|
||||
|
@ -38,10 +66,19 @@ class AccountController extends Controller
|
|||
|
||||
public function sendVerifyEmail(Request $request)
|
||||
{
|
||||
if(EmailVerification::whereUserId(Auth::id())->count() !== 0) {
|
||||
return redirect()->back()->with('status', 'A verification email has already been sent! Please check your email.');
|
||||
$timeLimit = Carbon::now()->subDays(1)->toDateTimeString();
|
||||
$recentAttempt = EmailVerification::whereUserId(Auth::id())
|
||||
->where('created_at', '>', $timeLimit)->count();
|
||||
$exists = EmailVerification::whereUserId(Auth::id())->count();
|
||||
|
||||
if($recentAttempt == 1 && $exists == 1) {
|
||||
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
|
||||
} elseif ($recentAttempt == 0 && $exists !== 0) {
|
||||
// Delete old verification and send new one.
|
||||
EmailVerification::whereUserId(Auth::id())->delete();
|
||||
}
|
||||
|
||||
|
||||
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||
$utoken = hash('sha512', $user->id);
|
||||
$rtoken = str_random(40);
|
||||
|
@ -60,14 +97,15 @@ class AccountController extends Controller
|
|||
|
||||
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||
{
|
||||
$verify = EmailVerification::where(DB::raw('BINARY user_token'), $userToken)
|
||||
->where(DB::raw('BINARY random_token'), $randomToken)
|
||||
$verify = EmailVerification::where('user_token', $userToken)
|
||||
->where('random_token', $randomToken)
|
||||
->firstOrFail();
|
||||
|
||||
if(Auth::id() === $verify->user_id) {
|
||||
$user = User::find(Auth::id());
|
||||
$user->email_verified_at = Carbon::now();
|
||||
$user->save();
|
||||
return redirect('/timeline');
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,4 +133,5 @@ class AccountController extends Controller
|
|||
}
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
71
app/Http/Controllers/Api/BaseApiController.php
Normal file
71
app/Http/Controllers/Api/BaseApiController.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -2,16 +2,13 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use App\Like;
|
||||
use Auth, Cache;
|
||||
use App\{Like, Status};
|
||||
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)
|
||||
{
|
||||
|
@ -21,12 +18,18 @@ class ApiController extends Controller
|
|||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$likes = Like::whereProfileId($profile->id)
|
||||
$res = Cache::remember('api:like-ids:user:'.$profile->id, 1440, function() use ($profile) {
|
||||
return Like::whereProfileId($profile->id)
|
||||
->orderBy('id', 'desc')
|
||||
->take(1000)
|
||||
->pluck('status_id');
|
||||
});
|
||||
|
||||
return response()->json($likes);
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function loadMoreComments(Request $request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\{AccountLog, User};
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
|
||||
|
@ -25,7 +26,7 @@ class LoginController extends Controller
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/home';
|
||||
protected $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
@ -56,4 +57,25 @@ class LoginController extends Controller
|
|||
|
||||
$this->validate($request, $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has been authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $user
|
||||
* @return mixed
|
||||
*/
|
||||
protected function authenticated($request, $user)
|
||||
{
|
||||
$log = new AccountLog;
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'auth.login';
|
||||
$log->message = 'Account Login';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,91 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Log, Storage;
|
||||
use App\Avatar;
|
||||
use App\Jobs\AvatarPipeline\AvatarOptimize;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,23 +16,27 @@ class BookmarkController extends Controller
|
|||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1'
|
||||
'item' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
);
|
||||
|
||||
if(!$bookmark->wasRecentlyCreated) {
|
||||
$bookmark->delete();
|
||||
}
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
||||
} else {
|
||||
} else {
|
||||
$response = redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,14 @@ class CommentController extends Controller
|
|||
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)
|
||||
{
|
||||
if(Auth::check() === false) { abort(403); }
|
||||
|
|
|
@ -15,17 +15,44 @@ class DiscoverController extends Controller
|
|||
|
||||
public function home()
|
||||
{
|
||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_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();
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
||||
$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'));
|
||||
}
|
||||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
$tag = Hashtag::whereSlug($hashtag)->firstOrFail();
|
||||
$posts = $tag->posts()->has('media')->orderBy('id','desc')->paginate(12);
|
||||
$count = $tag->posts()->has('media')->orderBy('id','desc')->count();
|
||||
return view('discover.tags.show', compact('tag', 'posts', 'count'));
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|integer|min:1|max:10'
|
||||
]);
|
||||
|
||||
$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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use Auth, Cache;
|
||||
use App\Profile;
|
||||
use Carbon\Carbon;
|
||||
use League\Fractal;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Util\Lexer\Nickname;
|
||||
|
@ -13,15 +14,26 @@ use App\Transformer\ActivityPub\{
|
|||
ProfileTransformer
|
||||
};
|
||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||
use App\Jobs\InboxPipeline\InboxWorker;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
public function authCheck()
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
return abort(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public function authorizeFollow(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
$this->validate($request, [
|
||||
'acct' => 'required|string|min:3|max:255'
|
||||
]);
|
||||
$acct = $request->input('acct');
|
||||
$nickname = Nickname::normalizeProfileUrl($acct);
|
||||
return view('federation.authorizefollow', compact('acct', 'nickname'));
|
||||
}
|
||||
|
||||
public function remoteFollow()
|
||||
|
@ -64,61 +76,58 @@ class FederationController extends Controller
|
|||
|
||||
public function nodeinfo()
|
||||
{
|
||||
$res = [
|
||||
'metadata' => [
|
||||
'nodeName' => config('app.name'),
|
||||
'software' => [
|
||||
'homepage' => 'https://pixelfed.org',
|
||||
'github' => 'https://github.com/pixelfed',
|
||||
'follow' => 'https://mastodon.social/@pixelfed'
|
||||
],
|
||||
/*
|
||||
TODO: Custom Features for Trending
|
||||
'customFeatures' => [
|
||||
'trending' => [
|
||||
'description' => 'Trending API for federated discovery',
|
||||
'api' => [
|
||||
'url' => null,
|
||||
'docs' => null
|
||||
],
|
||||
$res = Cache::remember('api:nodeinfo', 60, function() {
|
||||
return [
|
||||
'metadata' => [
|
||||
'nodeName' => config('app.name'),
|
||||
'software' => [
|
||||
'homepage' => 'https://pixelfed.org',
|
||||
'github' => 'https://github.com/pixelfed',
|
||||
'follow' => 'https://mastodon.social/@pixelfed'
|
||||
],
|
||||
],
|
||||
*/
|
||||
],
|
||||
'openRegistrations' => config('pixelfed.open_registration'),
|
||||
'protocols' => [
|
||||
'activitypub'
|
||||
],
|
||||
'services' => [
|
||||
'inbound' => [],
|
||||
'outbound' => []
|
||||
],
|
||||
'software' => [
|
||||
'name' => 'pixelfed',
|
||||
'version' => config('pixelfed.version')
|
||||
],
|
||||
'usage' => [
|
||||
'localPosts' => \App\Status::whereLocal(true)->count(),
|
||||
'users' => [
|
||||
'total' => \App\User::count()
|
||||
]
|
||||
],
|
||||
'version' => '2.0'
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
'openRegistrations' => config('pixelfed.open_registration'),
|
||||
'protocols' => [
|
||||
'activitypub'
|
||||
],
|
||||
'services' => [
|
||||
'inbound' => [],
|
||||
'outbound' => []
|
||||
],
|
||||
'software' => [
|
||||
'name' => 'pixelfed',
|
||||
'version' => config('pixelfed.version')
|
||||
],
|
||||
'usage' => [
|
||||
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
|
||||
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
|
||||
'users' => [
|
||||
'total' => \App\User::count(),
|
||||
'activeHalfyear' => \App\User::where('updated_at', '>', Carbon::now()->subMonths(6)->toDateTimeString())->count(),
|
||||
'activeMonth' => \App\User::where('updated_at', '>', Carbon::now()->subMonths(1)->toDateTimeString())->count(),
|
||||
]
|
||||
],
|
||||
'version' => '2.0'
|
||||
];
|
||||
});
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
$this->validate($request, ['resource'=>'required']);
|
||||
$resource = $request->input('resource');
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
$username = $parsed['username'];
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$webfinger = (new Webfinger($user))->generate();
|
||||
return response()->json($webfinger);
|
||||
$this->validate($request, ['resource'=>'required|string|min:3|max:255']);
|
||||
|
||||
$hash = hash('sha512', $request->input('resource'));
|
||||
|
||||
$webfinger = Cache::remember('api:webfinger:'.$hash, 1440, function() use($request) {
|
||||
$resource = $request->input('resource');
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
$username = $parsed['username'];
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
return (new Webfinger($user))->generate();
|
||||
});
|
||||
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public function userOutbox(Request $request, $username)
|
||||
|
@ -135,4 +144,20 @@ class FederationController extends Controller
|
|||
return response()->json($res['data']);
|
||||
}
|
||||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
if(config('pixelfed.activitypub_enabled') == false) {
|
||||
abort(403);
|
||||
}
|
||||
$mimes = [
|
||||
'application/activity+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
if(!in_array($request->header('Content-Type'), $mimes)) {
|
||||
abort(500, 'Invalid request');
|
||||
}
|
||||
$profile = Profile::whereUsername($username)->firstOrFail();
|
||||
InboxWorker::dispatch($request, $profile, $request->all());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ImportDataController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Hashids;
|
||||
use Auth, Cache, Hashids;
|
||||
use App\{Like, Profile, Status, User};
|
||||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
|
||||
|
@ -27,7 +27,7 @@ class LikeController extends Controller
|
|||
|
||||
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||
$like->delete();
|
||||
$like->forceDelete();
|
||||
$count--;
|
||||
} else {
|
||||
$like = new Like;
|
||||
|
@ -35,9 +35,15 @@ class LikeController extends Controller
|
|||
$like->status_id = $status->id;
|
||||
$like->save();
|
||||
$count++;
|
||||
LikePipeline::dispatch($like);
|
||||
}
|
||||
|
||||
LikePipeline::dispatch($like);
|
||||
$likes = Like::whereProfileId($profile->id)
|
||||
->orderBy('id', 'desc')
|
||||
->take(1000)
|
||||
->pluck('status_id');
|
||||
|
||||
Cache::put('api:like-ids:user:'.$profile->id, $likes, 1440);
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
|
|
@ -18,17 +18,22 @@ class ProfileController extends Controller
|
|||
public function show(Request $request, $username)
|
||||
{
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||
|
||||
$mimes = [
|
||||
'application/activity+json',
|
||||
'application/ld+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
|
||||
if(in_array($request->header('accept'), $mimes) && config('pixelfed.activitypub_enabled')) {
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
|
||||
if($user->is_private == true) {
|
||||
$can_access = $this->privateProfileCheck($user);
|
||||
if($can_access !== true) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
// TODO: refactor this mess
|
||||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
|
@ -36,11 +41,26 @@ class ProfileController extends Controller
|
|||
$timeline = $user->statuses()
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->orderBy('id','desc')
|
||||
->orderBy('created_at','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(21);
|
||||
|
||||
return view('profile.show', compact('user', 'owner', 'is_following', 'is_admin', 'timeline'));
|
||||
return view('profile.show', compact('user', 'settings', 'owner', 'is_following', 'is_admin', 'timeline'));
|
||||
}
|
||||
|
||||
protected function privateProfileCheck(Profile $profile)
|
||||
{
|
||||
if(Auth::check() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$follower_ids = (array) $profile->followers()->pluck('followers.profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
if(!in_array($pid, $follower_ids) && $pid !== $profile->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function showActivityPub(Request $request, $user)
|
||||
|
@ -89,11 +109,12 @@ class ProfileController extends Controller
|
|||
abort(403);
|
||||
}
|
||||
$user = Auth::user()->profile;
|
||||
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||
$owner = true;
|
||||
$following = false;
|
||||
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at','desc')->simplePaginate(10);
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.show', compact('user', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||
return view('profile.show', compact('user', 'settings', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,25 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Avatar, Profile, Report, Status, User};
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
protected $profile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function showForm(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'id' => 'required|integer|min:1'
|
||||
]);
|
||||
return view('report.form');
|
||||
}
|
||||
|
||||
|
@ -35,4 +48,92 @@ class ReportController extends Controller
|
|||
{
|
||||
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!');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,19 +3,28 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Profile, User};
|
||||
use Auth;
|
||||
use App\{AccountLog, Media, Profile, User};
|
||||
use Auth, DB;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
return $this->middleware('auth');
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -89,6 +98,34 @@ class SettingsController extends Controller
|
|||
return view('settings.avatar');
|
||||
}
|
||||
|
||||
public function accessibility()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
return view('settings.accessibility', compact('settings'));
|
||||
}
|
||||
|
||||
public function accessibilityStore(Request $request)
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$fields = [
|
||||
'compose_media_descriptions',
|
||||
'reduce_motion',
|
||||
'optimize_screen_reader',
|
||||
'high_contrast_mode',
|
||||
'video_autoplay'
|
||||
];
|
||||
foreach($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
if($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
} else {
|
||||
$settings->{$field} = false;
|
||||
}
|
||||
$settings->save();
|
||||
}
|
||||
return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
public function notifications()
|
||||
{
|
||||
return view('settings.notifications');
|
||||
|
@ -96,12 +133,61 @@ class SettingsController extends Controller
|
|||
|
||||
public function privacy()
|
||||
{
|
||||
return view('settings.privacy');
|
||||
$settings = Auth::user()->settings;
|
||||
$is_private = Auth::user()->profile->is_private;
|
||||
$settings['is_private'] = (bool) $is_private;
|
||||
return view('settings.privacy', compact('settings'));
|
||||
}
|
||||
|
||||
public function privacyStore(Request $request)
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$profile = Auth::user()->profile;
|
||||
$fields = [
|
||||
'is_private',
|
||||
'crawlable',
|
||||
];
|
||||
foreach($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
if($field == 'is_private') {
|
||||
if($form == 'on') {
|
||||
$profile->{$field} = true;
|
||||
$settings->show_guests = false;
|
||||
$settings->show_discover = false;
|
||||
$profile->save();
|
||||
} else {
|
||||
$profile->{$field} = false;
|
||||
$profile->save();
|
||||
}
|
||||
} elseif($field == 'crawlable') {
|
||||
if($form == 'on') {
|
||||
$settings->{$field} = false;
|
||||
} else {
|
||||
$settings->{$field} = true;
|
||||
}
|
||||
} else {
|
||||
if($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
} else {
|
||||
$settings->{$field} = false;
|
||||
}
|
||||
}
|
||||
$settings->save();
|
||||
}
|
||||
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
public function security()
|
||||
{
|
||||
return view('settings.security');
|
||||
$sessions = DB::table('sessions')
|
||||
->whereUserId(Auth::id())
|
||||
->limit(20)
|
||||
->get();
|
||||
$activity = AccountLog::whereUserId(Auth::id())
|
||||
->orderBy('created_at','desc')
|
||||
->limit(50)
|
||||
->get();
|
||||
return view('settings.security', compact('sessions', 'activity'));
|
||||
}
|
||||
|
||||
public function applications()
|
||||
|
@ -121,7 +207,7 @@ class SettingsController extends Controller
|
|||
|
||||
public function dataImportInstagram()
|
||||
{
|
||||
return view('settings.import.ig');
|
||||
return view('settings.import.instagram.home');
|
||||
}
|
||||
|
||||
public function developers()
|
||||
|
|
|
@ -2,11 +2,42 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App;
|
||||
use App, Auth, Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Follower, Profile, Status, User};
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
|
||||
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)
|
||||
{
|
||||
if(!App::isLocale($locale)) {
|
||||
|
@ -15,4 +46,20 @@ class SiteController extends Controller
|
|||
App::setLocale($locale);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,93 +13,147 @@ class StatusController extends Controller
|
|||
{
|
||||
public function show(Request $request, $username, int $id)
|
||||
{
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->withCount(['likes', 'comments', 'media'])
|
||||
->findOrFail($id);
|
||||
if(!$status->media_path && $status->in_reply_to_id) {
|
||||
return redirect($status->url());
|
||||
}
|
||||
return view('status.show', compact('user', 'status'));
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->withCount(['likes', 'comments', 'media'])
|
||||
->findOrFail($id);
|
||||
if(!$status->media_path && $status->in_reply_to_id) {
|
||||
return redirect($status->url());
|
||||
}
|
||||
$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)
|
||||
{
|
||||
if(Auth::check() == false)
|
||||
{
|
||||
abort(403);
|
||||
}
|
||||
if(Auth::check() == false)
|
||||
{
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$user = Auth::user();
|
||||
|
||||
$this->validate($request, [
|
||||
'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',
|
||||
]);
|
||||
$size = Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
$limit = (int) config('pixelfed.max_account_size');
|
||||
if($size >= $limit) {
|
||||
return redirect()->back()->with('error', 'You have exceeded your storage limit. Please click <a href="#">here</a> for more info.');
|
||||
}
|
||||
|
||||
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
|
||||
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
|
||||
}
|
||||
$this->validate($request, [
|
||||
'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;
|
||||
$monthHash = hash('sha1', date('Y') . date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
$profile = $user->profile;
|
||||
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
|
||||
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
|
||||
}
|
||||
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
|
||||
$monthHash = hash('sha1', date('Y') . date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
$profile = $user->profile;
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->is_nsfw = $cw;
|
||||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->is_nsfw = $cw;
|
||||
|
||||
$status->save();
|
||||
$status->save();
|
||||
|
||||
$photos = $request->file('photo');
|
||||
$order = 1;
|
||||
foreach ($photos as $k => $v) {
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$path = $v->store($storagePath);
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $v->getClientSize();
|
||||
$media->mime = $v->getClientMimeType();
|
||||
$media->filter_class = $request->input('filter_class');
|
||||
$media->filter_name = $request->input('filter_name');
|
||||
$media->order = $order;
|
||||
$media->save();
|
||||
ImageOptimize::dispatch($media);
|
||||
$order++;
|
||||
}
|
||||
$photos = $request->file('photo');
|
||||
$order = 1;
|
||||
foreach ($photos as $k => $v) {
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$path = $v->store($storagePath);
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $v->getClientSize();
|
||||
$media->mime = $v->getClientMimeType();
|
||||
$media->filter_class = $request->input('filter_class');
|
||||
$media->filter_name = $request->input('filter_name');
|
||||
$media->order = $order;
|
||||
$media->save();
|
||||
ImageOptimize::dispatch($media);
|
||||
$order++;
|
||||
}
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
NewStatusPipeline::dispatch($status);
|
||||
|
||||
// TODO: Send to subscribers
|
||||
|
||||
return redirect($status->url());
|
||||
// TODO: Send to subscribers
|
||||
|
||||
return redirect($status->url());
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
}
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
'item' => 'required|integer|min:1'
|
||||
]);
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
'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) {
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,24 +18,23 @@ class TimelineController extends Controller
|
|||
// TODO: Use redis for timelines
|
||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||
$following->push(Auth::user()->profile->id);
|
||||
$timeline = Status::whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereIn('profile_id', $following)
|
||||
$timeline = Status::whereIn('profile_id', $following)
|
||||
->orderBy('id','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(10);
|
||||
return view('timeline.personal', compact('timeline'));
|
||||
->simplePaginate(20);
|
||||
$type = 'personal';
|
||||
return view('timeline.template', compact('timeline', 'type'));
|
||||
}
|
||||
|
||||
public function local()
|
||||
{
|
||||
// TODO: Use redis for timelines
|
||||
// $timeline = Timeline::build()->local();
|
||||
$timeline = Status::whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->orderBy('id','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(10);
|
||||
return view('timeline.public', compact('timeline'));
|
||||
->simplePaginate(20);
|
||||
$type = 'local';
|
||||
return view('timeline.template', compact('timeline', 'type'));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class EmailVerificationCheck
|
|||
if($request->user() &&
|
||||
config('pixelfed.enforce_email_verification') &&
|
||||
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/*')
|
||||
) {
|
||||
return redirect('/i/verify-email');
|
||||
|
|
10
app/ImportJob.php
Normal file
10
app/ImportJob.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ImportJob extends Model
|
||||
{
|
||||
//
|
||||
}
|
70
app/Jobs/AvatarPipeline/AvatarOptimize.php
Normal file
70
app/Jobs/AvatarPipeline/AvatarOptimize.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
43
app/Jobs/InboxPipeline/InboxWorker.php
Normal file
43
app/Jobs/InboxPipeline/InboxWorker.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
41
app/Jobs/InboxPipeline/SharedInboxWorker.php
Normal file
41
app/Jobs/InboxPipeline/SharedInboxWorker.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -37,6 +37,11 @@ class LikePipeline implements ShouldQueue
|
|||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if($status->url !== null) {
|
||||
// Ignore notifications to remote statuses
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('like')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use Cache;
|
||||
use DB, Cache;
|
||||
use App\{
|
||||
Hashtag,
|
||||
Media,
|
||||
|
@ -68,12 +68,14 @@ class StatusEntityLexer implements ShouldQueue
|
|||
|
||||
public function storeEntities()
|
||||
{
|
||||
$status = $this->status;
|
||||
$this->storeHashtags();
|
||||
$this->storeMentions();
|
||||
$status->rendered = $this->autolink;
|
||||
$status->entities = json_encode($this->entities);
|
||||
$status->save();
|
||||
DB::transaction(function () {
|
||||
$status = $this->status;
|
||||
$status->rendered = $this->autolink;
|
||||
$status->entities = json_encode($this->entities);
|
||||
$status->save();
|
||||
});
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
|
@ -82,17 +84,15 @@ class StatusEntityLexer implements ShouldQueue
|
|||
$status = $this->status;
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$slug = str_slug($tag);
|
||||
|
||||
$htag = Hashtag::firstOrCreate(
|
||||
['name' => $tag],
|
||||
['slug' => $slug]
|
||||
);
|
||||
|
||||
StatusHashtag::firstOrCreate(
|
||||
['status_id' => $status->id],
|
||||
['hashtag_id' => $htag->id]
|
||||
);
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$slug = str_slug($tag);
|
||||
$hashtag = Hashtag::firstOrCreate(
|
||||
['name' => $tag, 'slug' => $slug]
|
||||
);
|
||||
StatusHashtag::firstOrCreate(
|
||||
['status_id' => $status->id, 'hashtag_id' => $hashtag->id]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,18 +102,20 @@ class StatusEntityLexer implements ShouldQueue
|
|||
$status = $this->status;
|
||||
|
||||
foreach($mentions as $mention) {
|
||||
$mentioned = Profile::whereUsername($mention)->first();
|
||||
$mentioned = Profile::whereUsername($mention)->firstOrFail();
|
||||
|
||||
if(empty($mentioned) || !isset($mentioned->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$m = new Mention;
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $mentioned->id;
|
||||
$m->save();
|
||||
|
||||
MentionPipeline::dispatch($status, $m);
|
||||
DB::transaction(function () use ($status, $mentioned) {
|
||||
$m = new Mention;
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $mentioned->id;
|
||||
$m->save();
|
||||
|
||||
MentionPipeline::dispatch($status, $m);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,4 +23,11 @@ class Media extends Model
|
|||
$url = Storage::url($path);
|
||||
return url($url);
|
||||
}
|
||||
|
||||
public function thumbnailUrl()
|
||||
{
|
||||
$path = $this->thumbnail_path;
|
||||
$url = Storage::url($path);
|
||||
return url($url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\{Profile, User};
|
||||
use App\{Profile, User, UserSetting};
|
||||
use App\Jobs\AvatarPipeline\CreateAvatar;
|
||||
|
||||
class UserObserver
|
||||
|
@ -36,6 +36,12 @@ class UserObserver
|
|||
|
||||
CreateAvatar::dispatch($profile);
|
||||
}
|
||||
|
||||
if(empty($user->settings)) {
|
||||
$settings = new UserSetting;
|
||||
$settings->user_id = $user->id;
|
||||
$settings->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Storage;
|
||||
use Auth, Storage;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
@ -29,6 +29,15 @@ class Profile extends Model
|
|||
}
|
||||
|
||||
public function url($suffix = '')
|
||||
{
|
||||
if($this->remote_url) {
|
||||
return $this->remote_url;
|
||||
} else {
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
}
|
||||
|
||||
public function localUrl($suffix = '')
|
||||
{
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
|
@ -124,4 +133,40 @@ class Profile extends Model
|
|||
$url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png'));
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function statusCount()
|
||||
{
|
||||
return $this->statuses()->whereHas('media')->count();
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ class EventServiceProvider extends ServiceProvider
|
|||
'App\Events\Event' => [
|
||||
'App\Listeners\EventListener',
|
||||
],
|
||||
'auth.login' => [
|
||||
'App\Events\AuthLoginEvent',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,5 +6,33 @@ use Illuminate\Database\Eloquent\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
10
app/ReportComment.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReportComment extends Model
|
||||
{
|
||||
//
|
||||
}
|
10
app/ReportLog.php
Normal file
10
app/ReportLog.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReportLog extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Storage;
|
||||
use Auth, Storage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
|
@ -52,6 +52,14 @@ class Status extends Model
|
|||
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()
|
||||
{
|
||||
return $this->url() . '/edit';
|
||||
|
@ -71,15 +79,42 @@ class Status extends Model
|
|||
return $this->hasMany(Like::class);
|
||||
}
|
||||
|
||||
public function liked() : bool
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(Status::class, 'in_reply_to_id');
|
||||
}
|
||||
|
||||
public function bookmarked()
|
||||
{
|
||||
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()
|
||||
{
|
||||
if(!empty($this->in_reply_to_id)) {
|
||||
return Status::findOrFail($this->in_reply_to_id);
|
||||
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||
if(!empty($parent)) {
|
||||
return Status::findOrFail($parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
@ -133,4 +185,9 @@ class Status extends Model
|
|||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
||||
__('notification.commented');
|
||||
}
|
||||
|
||||
public function recentComments()
|
||||
{
|
||||
return $this->comments()->orderBy('created_at','desc')->take(3);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,5 +6,5 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class StatusHashtag extends Model
|
||||
{
|
||||
protected $fillable = ['status_id', 'hashtag_id'];
|
||||
public $fillable = ['status_id', 'hashtag_id'];
|
||||
}
|
||||
|
|
33
app/Transformer/Api/AccountTransformer.php
Normal file
33
app/Transformer/Api/AccountTransformer.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
16
app/Transformer/Api/ApplicationTransformer.php
Normal file
16
app/Transformer/Api/ApplicationTransformer.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
18
app/Transformer/Api/HashtagTransformer.php
Normal file
18
app/Transformer/Api/HashtagTransformer.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
24
app/Transformer/Api/MediaTransformer.php
Normal file
24
app/Transformer/Api/MediaTransformer.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
19
app/Transformer/Api/MentionTransformer.php
Normal file
19
app/Transformer/Api/MentionTransformer.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
69
app/Transformer/Api/StatusTransformer.php
Normal file
69
app/Transformer/Api/StatusTransformer.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ class User extends Authenticatable
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
protected $dates = ['deleted_at', 'email_verified_at'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
@ -44,4 +44,9 @@ class User extends Authenticatable
|
|||
{
|
||||
return url(config('app.url') . '/' . $this->username);
|
||||
}
|
||||
|
||||
public function settings()
|
||||
{
|
||||
return $this->hasOne(UserSetting::class);
|
||||
}
|
||||
}
|
||||
|
|
10
app/UserFilter.php
Normal file
10
app/UserFilter.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserFilter extends Model
|
||||
{
|
||||
//
|
||||
}
|
10
app/UserSetting.php
Normal file
10
app/UserSetting.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserSetting extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -118,6 +118,7 @@ class RestrictedNames {
|
|||
|
||||
// Static Assets
|
||||
"assets",
|
||||
"storage",
|
||||
|
||||
// Laravel Horizon
|
||||
"horizon",
|
||||
|
@ -127,14 +128,19 @@ class RestrictedNames {
|
|||
"api",
|
||||
"auth",
|
||||
"i",
|
||||
"dashboard",
|
||||
"discover",
|
||||
"docs",
|
||||
"home",
|
||||
"login",
|
||||
"logout",
|
||||
"media",
|
||||
"p",
|
||||
"password",
|
||||
"reports",
|
||||
"search",
|
||||
"settings",
|
||||
"statuses",
|
||||
"site",
|
||||
"timeline",
|
||||
"user",
|
||||
|
|
|
@ -115,8 +115,9 @@ class Image {
|
|||
});
|
||||
$converted = $this->setBaseName($path, $thumbnail, $img->extension);
|
||||
$newPath = storage_path('app/'.$converted['path']);
|
||||
|
||||
$img->save($newPath, 75);
|
||||
|
||||
$quality = config('pixelfed.image_quality');
|
||||
$img->save($newPath, $quality);
|
||||
|
||||
if(!$thumbnail) {
|
||||
$media->orientation = $orientation;
|
||||
|
|
|
@ -31,13 +31,9 @@ class Webfinger {
|
|||
|
||||
public function generateAliases()
|
||||
{
|
||||
$host = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
$username = $this->user->username;
|
||||
$url = $this->user->url();
|
||||
|
||||
$this->aliases = [
|
||||
'acct:'.$username.'@'.$host,
|
||||
$url
|
||||
$this->user->url(),
|
||||
$this->user->permalink()
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
@ -55,24 +51,12 @@ class Webfinger {
|
|||
[
|
||||
'rel' => 'http://schemas.google.com/g/2010#updates-from',
|
||||
'type' => 'application/atom+xml',
|
||||
'href' => url("/users/{$user->username}.atom")
|
||||
'href' => $user->permalink('.atom')
|
||||
],
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $user->permalink()
|
||||
],
|
||||
[
|
||||
'rel' => 'magic-public-key',
|
||||
'href' => null//$user->public_key
|
||||
],
|
||||
[
|
||||
'rel' => 'salmon',
|
||||
'href' => $user->permalink('/salmon')
|
||||
],
|
||||
[
|
||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'href' => url('/main/ostatussub?profile={uri}')
|
||||
]
|
||||
];
|
||||
return $this;
|
||||
|
|
|
@ -6,18 +6,23 @@
|
|||
"type": "project",
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"99designs/http-signatures-guzzlehttp": "^2.0",
|
||||
"beyondcode/laravel-self-diagnosis": "^0.4.0",
|
||||
"bitverse/identicon": "^1.1",
|
||||
"doctrine/dbal": "^2.7",
|
||||
"fideloper/proxy": "^4.0",
|
||||
"greggilbert/recaptcha": "dev-master",
|
||||
"intervention/image": "^2.4",
|
||||
"kitetail/zttp": "^0.3.0",
|
||||
"pixelfed/zttp": "^0.4",
|
||||
"laravel/framework": "5.6.*",
|
||||
"laravel/horizon": "^1.2",
|
||||
"laravel/passport": "^6.0",
|
||||
"laravel/tinker": "^1.0",
|
||||
"league/fractal": "^0.17.0",
|
||||
"moontoast/math": "^1.1",
|
||||
"phpseclib/phpseclib": "~2.0",
|
||||
"pixelfed/dotenv-editor": "^2.0",
|
||||
"pixelfed/fractal": "^0.18.0",
|
||||
"pixelfed/google2fa-laravel": "^2.0",
|
||||
"pixelfed/http-signatures-guzzlehttp": "^4.0",
|
||||
"predis/predis": "^1.1",
|
||||
"spatie/laravel-backup": "^5.0.0",
|
||||
"spatie/laravel-image-optimizer": "^1.1",
|
||||
|
@ -25,6 +30,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.1",
|
||||
"beyondcode/laravel-er-diagram-generator": "^0.2.2",
|
||||
"filp/whoops": "^2.0",
|
||||
"fzaninotto/faker": "^1.4",
|
||||
"mockery/mockery": "^1.0",
|
||||
|
|
2148
composer.lock
generated
2148
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -151,6 +151,7 @@ return [
|
|||
* Package Service Providers...
|
||||
*/
|
||||
Greggilbert\Recaptcha\RecaptchaServiceProvider::class,
|
||||
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
|
@ -211,6 +212,7 @@ return [
|
|||
'View' => Illuminate\Support\Facades\View::class,
|
||||
|
||||
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::class,
|
||||
'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
27
config/dotenv-editor.php
Normal file
27
config/dotenv-editor.php
Normal 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/')
|
||||
|
||||
);
|
|
@ -76,7 +76,7 @@ return [
|
|||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'simple',
|
||||
'processes' => 10,
|
||||
'processes' => 20,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
|
|
|
@ -49,5 +49,5 @@ return [
|
|||
* If set to `true` all output of the optimizer binaries will be appended to the default log.
|
||||
* You can also set this to a class that implements `Psr\Log\LoggerInterface`.
|
||||
*/
|
||||
'log_optimizer_activity' => true,
|
||||
'log_optimizer_activity' => false,
|
||||
];
|
||||
|
|
|
@ -23,7 +23,7 @@ return [
|
|||
| 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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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
|
||||
|
@ -95,7 +106,7 @@ return [
|
|||
| 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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Image Quality
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set the image optimization quality, must be a value between 1-100.
|
||||
|
|
||||
*/
|
||||
'image_quality' => (int) env('IMAGE_QUALITY', 80),
|
||||
|
||||
];
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ services:
|
|||
- "mysql-data:/var/lib/mysql"
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
image: redis:4-alpine
|
||||
volumes:
|
||||
- "redis-data:/data"
|
||||
networks:
|
||||
|
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/img/favicon.png
Normal file
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
BIN
public/img/fred1.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
public/img/pixelfed-icon-black.svg
Normal file
BIN
public/img/pixelfed-icon-black.svg
Normal file
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
BIN
public/js/app.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
46
resources/assets/js/bootstrap.js
vendored
46
resources/assets/js/bootstrap.js
vendored
|
@ -1,13 +1,6 @@
|
|||
|
||||
window._ = require('lodash');
|
||||
window.Popper = require('popper.js').default;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import swal from 'sweetalert';
|
||||
try {
|
||||
window.pixelfed = {};
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
|
@ -16,6 +9,7 @@ try {
|
|||
window.filesize = require('filesize');
|
||||
window.typeahead = require('./lib/typeahead');
|
||||
window.Bloodhound = require('./lib/bloodhound');
|
||||
window.Vue = require('vue');
|
||||
|
||||
require('./components/localstorage');
|
||||
require('./components/likebutton');
|
||||
|
@ -23,45 +17,21 @@ try {
|
|||
require('./components/searchform');
|
||||
require('./components/bookmarkform');
|
||||
require('./components/statusform');
|
||||
|
||||
Vue.component(
|
||||
'follow-suggestions',
|
||||
require('./components/FollowSuggestions.vue')
|
||||
);
|
||||
} catch (e) {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
window.axios = require('axios');
|
||||
|
||||
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"]');
|
||||
|
||||
if (token) {
|
||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
|
||||
} else {
|
||||
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
|
||||
// });
|
||||
|
|
50
resources/assets/js/components/FollowSuggestions.vue
Normal file
50
resources/assets/js/components/FollowSuggestions.vue
Normal 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">@{{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>
|
12
resources/assets/js/components/commentform.js
vendored
12
resources/assets/js/components/commentform.js
vendored
|
@ -1,6 +1,12 @@
|
|||
$(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"]');
|
||||
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>';
|
||||
|
||||
comments.prepend(comment);
|
||||
comments.append(comment);
|
||||
|
||||
commentform.val('');
|
||||
commentform.blur();
|
||||
|
@ -41,7 +47,5 @@ $(document).ready(function() {
|
|||
.catch(function (res) {
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
30
resources/assets/js/components/likebutton.js
vendored
30
resources/assets/js/components/likebutton.js
vendored
|
@ -1,18 +1,17 @@
|
|||
$(document).ready(function() {
|
||||
|
||||
if(!ls.get('likes')) {
|
||||
axios.get('/api/v1/likes')
|
||||
.then(function (res) {
|
||||
ls.set('likes', res.data);
|
||||
console.log(res);
|
||||
})
|
||||
.catch(function (res) {
|
||||
ls.set('likes', []);
|
||||
})
|
||||
pixelfed.fetchLikes = () => {
|
||||
axios.get('/api/v1/likes')
|
||||
.then(function (res) {
|
||||
ls.set('likes', res.data);
|
||||
})
|
||||
.catch(function (res) {
|
||||
ls.set('likes', []);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
pixelfed.hydrateLikes = function() {
|
||||
pixelfed.hydrateLikes = () => {
|
||||
var likes = ls.get('likes');
|
||||
$('.like-form').each(function(i, el) {
|
||||
var el = $(el);
|
||||
|
@ -20,11 +19,14 @@ $(document).ready(function() {
|
|||
var heart = el.find('.status-heart');
|
||||
|
||||
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();
|
||||
|
||||
$(document).on('submit', '.like-form', function(e) {
|
||||
|
@ -33,6 +35,8 @@ $(document).ready(function() {
|
|||
var id = el.data('id');
|
||||
axios.post('/i/like', {item: id})
|
||||
.then(function (res) {
|
||||
pixelfed.fetchLikes();
|
||||
pixelfed.hydrateLikes();
|
||||
var likes = ls.get('likes');
|
||||
var action = false;
|
||||
var counter = el.parents().eq(1).find('.like-count');
|
||||
|
@ -40,14 +44,14 @@ $(document).ready(function() {
|
|||
var heart = el.find('.status-heart');
|
||||
|
||||
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) {
|
||||
return item !== id
|
||||
});
|
||||
counter.text(count);
|
||||
action = 'unlike';
|
||||
} else {
|
||||
heart.removeClass('far fa-heart').addClass('fas fa-heart');
|
||||
heart.removeClass('text-dark').addClass('text-primary');
|
||||
likes.push(id);
|
||||
counter.text(count);
|
||||
action = 'like';
|
||||
|
|
107
resources/assets/js/components/passport/AuthorizedClients.vue
Normal file
107
resources/assets/js/components/passport/AuthorizedClients.vue
Normal 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>
|
350
resources/assets/js/components/passport/Clients.vue
Normal file
350
resources/assets/js/components/passport/Clients.vue
Normal 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">×</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">×</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>
|
298
resources/assets/js/components/passport/PersonalAccessTokens.vue
Normal file
298
resources/assets/js/components/passport/PersonalAccessTokens.vue
Normal 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">×</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">×</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>
|
50
resources/assets/js/components/statusform.js
vendored
50
resources/assets/js/components/statusform.js
vendored
|
@ -1,9 +1,5 @@
|
|||
$(document).ready(function() {
|
||||
|
||||
$('#statusForm .btn-filter-select').on('click', function(e) {
|
||||
let el = $(this);
|
||||
});
|
||||
|
||||
pixelfed.create = {};
|
||||
pixelfed.filters = {};
|
||||
pixelfed.create.hasGeneratedSelect = false;
|
||||
|
@ -78,7 +74,7 @@ $(document).ready(function() {
|
|||
pixelfed.create.hasGeneratedSelect = true;
|
||||
}
|
||||
|
||||
$('#fileInput').on('change', function() {
|
||||
$(document).on('change', '#fileInput', function() {
|
||||
previewImage(this);
|
||||
$('#statusForm .form-filters.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 filter = el.val();
|
||||
let oldFilter = pixelfed.create.currentFilterClass;
|
||||
if(filter == 'none') {
|
||||
$('.filterContainer').removeClass(oldFilter);
|
||||
pixelfed.create.currentFilterClass = false;
|
||||
pixelfed.create.currentFilterName = 'None';
|
||||
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
|
||||
return;
|
||||
$('input[name=filter_class]').val('');
|
||||
$('input[name=filter_name]').val('');
|
||||
$('.filterContainer').removeClass(oldFilter);
|
||||
pixelfed.create.currentFilterClass = false;
|
||||
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
66
resources/assets/js/lib/bloodhound.js
vendored
Normal file → Executable file
|
@ -1,18 +1,18 @@
|
|||
/*!
|
||||
* typeahead.js 0.11.1
|
||||
* typeahead.js 1.2.0
|
||||
* 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) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define("bloodhound", [ "jquery" ], function(a0) {
|
||||
define([ "jquery" ], function(a0) {
|
||||
return root["Bloodhound"] = factory(a0);
|
||||
});
|
||||
} else if (typeof exports === "object") {
|
||||
module.exports = factory(require("jquery"));
|
||||
} else {
|
||||
root["Bloodhound"] = factory(jQuery);
|
||||
root["Bloodhound"] = factory(root["jQuery"]);
|
||||
}
|
||||
})(this, function($) {
|
||||
var _ = function() {
|
||||
|
@ -148,18 +148,27 @@
|
|||
stringify: function(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() {}
|
||||
};
|
||||
}();
|
||||
var VERSION = "0.11.1";
|
||||
var VERSION = "1.2.0";
|
||||
var tokenizers = function() {
|
||||
"use strict";
|
||||
return {
|
||||
nonword: nonword,
|
||||
whitespace: whitespace,
|
||||
ngram: ngram,
|
||||
obj: {
|
||||
nonword: getObjTokenizer(nonword),
|
||||
whitespace: getObjTokenizer(whitespace)
|
||||
whitespace: getObjTokenizer(whitespace),
|
||||
ngram: getObjTokenizer(ngram)
|
||||
}
|
||||
};
|
||||
function whitespace(str) {
|
||||
|
@ -170,6 +179,19 @@
|
|||
str = _.toStr(str);
|
||||
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) {
|
||||
return function setKey(keys) {
|
||||
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
|
||||
|
@ -341,9 +363,10 @@
|
|||
}();
|
||||
var Transport = function() {
|
||||
"use strict";
|
||||
var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
|
||||
var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10);
|
||||
function Transport(o) {
|
||||
o = o || {};
|
||||
this.maxPendingRequests = o.maxPendingRequests || 6;
|
||||
this.cancelled = false;
|
||||
this.lastReq = null;
|
||||
this._send = o.transport;
|
||||
|
@ -351,7 +374,7 @@
|
|||
this._cache = o.cache === false ? new LruCache(0) : sharedCache;
|
||||
}
|
||||
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
|
||||
maxPendingRequests = num;
|
||||
this.maxPendingRequests = num;
|
||||
};
|
||||
Transport.resetCache = function resetCache() {
|
||||
sharedCache.reset();
|
||||
|
@ -369,7 +392,7 @@
|
|||
}
|
||||
if (jqXhr = pendingRequests[fingerprint]) {
|
||||
jqXhr.done(done).fail(fail);
|
||||
} else if (pendingRequestsCount < maxPendingRequests) {
|
||||
} else if (pendingRequestsCount < this.maxPendingRequests) {
|
||||
pendingRequestsCount++;
|
||||
pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
|
||||
} else {
|
||||
|
@ -423,6 +446,7 @@
|
|||
this.identify = o.identify || _.stringify;
|
||||
this.datumTokenizer = o.datumTokenizer;
|
||||
this.queryTokenizer = o.queryTokenizer;
|
||||
this.matchAnyQueryToken = o.matchAnyQueryToken;
|
||||
this.reset();
|
||||
}
|
||||
_.mixin(SearchIndex.prototype, {
|
||||
|
@ -459,7 +483,7 @@
|
|||
tokens = normalizeTokens(this.queryTokenizer(query));
|
||||
_.each(tokens, function(token) {
|
||||
var node, chars, ch, ids;
|
||||
if (matches && matches.length === 0) {
|
||||
if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
|
||||
return false;
|
||||
}
|
||||
node = that.trie;
|
||||
|
@ -471,8 +495,10 @@
|
|||
ids = node[IDS].slice(0);
|
||||
matches = matches ? getIntersection(matches, ids) : ids;
|
||||
} else {
|
||||
matches = [];
|
||||
return false;
|
||||
if (!that.matchAnyQueryToken) {
|
||||
matches = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return matches ? _.map(unique(matches), function(id) {
|
||||
|
@ -614,10 +640,12 @@
|
|||
this.url = o.url;
|
||||
this.prepare = o.prepare;
|
||||
this.transform = o.transform;
|
||||
this.indexResponse = o.indexResponse;
|
||||
this.transport = new Transport({
|
||||
cache: o.cache,
|
||||
limiter: o.limiter,
|
||||
transport: o.transport
|
||||
transport: o.transport,
|
||||
maxPendingRequests: o.maxPendingRequests
|
||||
});
|
||||
}
|
||||
_.mixin(Remote.prototype, {
|
||||
|
@ -655,7 +683,9 @@
|
|||
identify: _.stringify,
|
||||
datumTokenizer: null,
|
||||
queryTokenizer: null,
|
||||
matchAnyQueryToken: false,
|
||||
sufficient: 5,
|
||||
indexRemote: false,
|
||||
sorter: null,
|
||||
local: [],
|
||||
prefetch: null,
|
||||
|
@ -744,7 +774,7 @@
|
|||
} else if (o.wildcard) {
|
||||
prepare = prepareByWildcard;
|
||||
} else {
|
||||
prepare = idenityPrepare;
|
||||
prepare = identityPrepare;
|
||||
}
|
||||
return prepare;
|
||||
function prepareByReplace(query, settings) {
|
||||
|
@ -755,7 +785,7 @@
|
|||
settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
|
||||
return settings;
|
||||
}
|
||||
function idenityPrepare(query, settings) {
|
||||
function identityPrepare(query, settings) {
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
@ -806,6 +836,7 @@
|
|||
this.sorter = o.sorter;
|
||||
this.identify = o.identify;
|
||||
this.sufficient = o.sufficient;
|
||||
this.indexRemote = o.indexRemote;
|
||||
this.local = o.local;
|
||||
this.remote = o.remote ? new Remote(o.remote) : null;
|
||||
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
|
||||
|
@ -875,6 +906,8 @@
|
|||
},
|
||||
search: function search(query, sync, async) {
|
||||
var that = this, local;
|
||||
sync = sync || _.noop;
|
||||
async = async || _.noop;
|
||||
local = this.sorter(this.index.search(query));
|
||||
sync(this.remote ? local.slice() : local);
|
||||
if (this.remote && local.length < this.sufficient) {
|
||||
|
@ -890,7 +923,8 @@
|
|||
return that.identify(r) === that.identify(l);
|
||||
}) && nonDuplicates.push(r);
|
||||
});
|
||||
async && async(nonDuplicates);
|
||||
that.indexRemote && that.add(nonDuplicates);
|
||||
async(nonDuplicates);
|
||||
}
|
||||
},
|
||||
all: function all() {
|
||||
|
|
228
resources/assets/js/lib/typeahead.js
vendored
Normal file → Executable file
228
resources/assets/js/lib/typeahead.js
vendored
Normal file → Executable file
|
@ -1,18 +1,18 @@
|
|||
/*!
|
||||
* typeahead.js 0.11.1
|
||||
* typeahead.js 1.2.0
|
||||
* 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) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define("typeahead.js", [ "jquery" ], function(a0) {
|
||||
define([ "jquery" ], function(a0) {
|
||||
return factory(a0);
|
||||
});
|
||||
} else if (typeof exports === "object") {
|
||||
module.exports = factory(require("jquery"));
|
||||
} else {
|
||||
factory(jQuery);
|
||||
factory(root["jQuery"]);
|
||||
}
|
||||
})(this, function($) {
|
||||
var _ = function() {
|
||||
|
@ -148,6 +148,13 @@
|
|||
stringify: function(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() {}
|
||||
};
|
||||
}();
|
||||
|
@ -189,7 +196,7 @@
|
|||
function buildHtml(c) {
|
||||
return {
|
||||
wrapper: '<span class="' + c.wrapper + '"></span>',
|
||||
menu: '<div class="' + c.menu + '"></div>'
|
||||
menu: '<div role="listbox" class="' + c.menu + '"></div>'
|
||||
};
|
||||
}
|
||||
function buildSelectors(classes) {
|
||||
|
@ -264,10 +271,8 @@
|
|||
}
|
||||
_.mixin(EventBus.prototype, {
|
||||
_trigger: function(type, args) {
|
||||
var $e;
|
||||
$e = $.Event(namespace + type);
|
||||
(args = args || []).unshift($e);
|
||||
this.$el.trigger.apply(this.$el, args);
|
||||
var $e = $.Event(namespace + type);
|
||||
this.$el.trigger.call(this.$el, $e, args || []);
|
||||
return $e;
|
||||
},
|
||||
before: function(type) {
|
||||
|
@ -384,7 +389,36 @@
|
|||
tagName: "strong",
|
||||
className: null,
|
||||
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ℚ⒬Ⓠⓠ㏃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) {
|
||||
var regex;
|
||||
|
@ -393,7 +427,7 @@
|
|||
return;
|
||||
}
|
||||
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);
|
||||
function hightlightTextNode(textNode) {
|
||||
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;
|
||||
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("|") + ")";
|
||||
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
|
||||
|
@ -448,6 +489,14 @@
|
|||
www.mixin(this);
|
||||
this.$hint = $(o.hint);
|
||||
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.queryWhenFocused = this.hasFocus() ? this.query : null;
|
||||
this.$overflowHelper = buildOverflowHelper(this.$input);
|
||||
|
@ -455,6 +504,7 @@
|
|||
if (this.$hint.length === 0) {
|
||||
this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
|
||||
}
|
||||
this.onSync("cursorchange", this._updateDescendent);
|
||||
}
|
||||
Input.normalizeQuery = function(str) {
|
||||
return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
|
||||
|
@ -524,6 +574,9 @@
|
|||
this.trigger("whitespaceChanged", this.query);
|
||||
}
|
||||
},
|
||||
_updateDescendent: function updateDescendent(event, id) {
|
||||
this.$input.attr("aria-activedescendant", id);
|
||||
},
|
||||
bind: function() {
|
||||
var that = this, onBlur, onFocus, onKeydown, onInput;
|
||||
onBlur = _.bind(this._onBlur, this);
|
||||
|
@ -647,6 +700,7 @@
|
|||
"use strict";
|
||||
var keys, nameGenerator;
|
||||
keys = {
|
||||
dataset: "tt-selectable-dataset",
|
||||
val: "tt-selectable-display",
|
||||
obj: "tt-selectable-object"
|
||||
};
|
||||
|
@ -666,19 +720,20 @@
|
|||
}
|
||||
www.mixin(this);
|
||||
this.highlight = !!o.highlight;
|
||||
this.name = o.name || nameGenerator();
|
||||
this.name = _.toStr(o.name || nameGenerator());
|
||||
this.limit = o.limit || 5;
|
||||
this.displayFn = getDisplayFn(o.display || o.displayKey);
|
||||
this.templates = getTemplates(o.templates, this.displayFn);
|
||||
this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
|
||||
this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
|
||||
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) {
|
||||
var $el = $(el);
|
||||
if ($el.data(keys.obj)) {
|
||||
return {
|
||||
dataset: $el.data(keys.dataset) || "",
|
||||
val: $el.data(keys.val) || "",
|
||||
obj: $el.data(keys.obj) || null
|
||||
};
|
||||
|
@ -697,7 +752,7 @@
|
|||
} else {
|
||||
this._empty();
|
||||
}
|
||||
this.trigger("rendered", this.name, suggestions, false);
|
||||
this.trigger("rendered", suggestions, false, this.name);
|
||||
},
|
||||
_append: function append(query, suggestions) {
|
||||
suggestions = suggestions || [];
|
||||
|
@ -708,7 +763,7 @@
|
|||
} else if (!this.$lastSuggestion.length && this.templates.notFound) {
|
||||
this._renderNotFound(query);
|
||||
}
|
||||
this.trigger("rendered", this.name, suggestions, true);
|
||||
this.trigger("rendered", suggestions, true, this.name);
|
||||
},
|
||||
_renderSuggestions: function renderSuggestions(query, suggestions) {
|
||||
var $fragment;
|
||||
|
@ -749,7 +804,7 @@
|
|||
_.each(suggestions, function getSuggestionNode(suggestion) {
|
||||
var $el, context;
|
||||
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]);
|
||||
});
|
||||
this.highlight && highlight({
|
||||
|
@ -787,7 +842,7 @@
|
|||
this.cancel = function cancel() {
|
||||
canceled = true;
|
||||
that.cancel = $.noop;
|
||||
that.async && that.trigger("asyncCanceled", query);
|
||||
that.async && that.trigger("asyncCanceled", query, that.name);
|
||||
};
|
||||
this.source(query, sync, async);
|
||||
!syncCalled && sync([]);
|
||||
|
@ -800,16 +855,17 @@
|
|||
rendered = suggestions.length;
|
||||
that._overwrite(query, suggestions);
|
||||
if (rendered < that.limit && that.async) {
|
||||
that.trigger("asyncRequested", query);
|
||||
that.trigger("asyncRequested", query, that.name);
|
||||
}
|
||||
}
|
||||
function async(suggestions) {
|
||||
suggestions = suggestions || [];
|
||||
if (!canceled && rendered < that.limit) {
|
||||
that.cancel = $.noop;
|
||||
rendered += suggestions.length;
|
||||
that._append(query, suggestions.slice(0, that.limit - rendered));
|
||||
that.async && that.trigger("asyncReceived", query);
|
||||
var idx = Math.abs(rendered - that.limit);
|
||||
rendered += idx;
|
||||
that._append(query, suggestions.slice(0, idx));
|
||||
that.async && that.trigger("asyncReceived", query, that.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -843,7 +899,7 @@
|
|||
suggestion: templates.suggestion || suggestionTemplate
|
||||
};
|
||||
function suggestionTemplate(context) {
|
||||
return $("<div>").text(displayFn(context));
|
||||
return $('<div role="option">').attr("id", _.guid()).text(displayFn(context));
|
||||
}
|
||||
}
|
||||
function isValidName(str) {
|
||||
|
@ -884,10 +940,11 @@
|
|||
this.trigger.apply(this, arguments);
|
||||
},
|
||||
_allDatasetsEmpty: function allDatasetsEmpty() {
|
||||
return _.every(this.datasets, isDatasetEmpty);
|
||||
function isDatasetEmpty(dataset) {
|
||||
return dataset.isEmpty();
|
||||
}
|
||||
return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) {
|
||||
var isEmpty = dataset.isEmpty();
|
||||
this.$node.attr("aria-expanded", !isEmpty);
|
||||
return isEmpty;
|
||||
}, this));
|
||||
},
|
||||
_getSelectables: function getSelectables() {
|
||||
return this.$node.find(this.selectors.selectable);
|
||||
|
@ -912,6 +969,12 @@
|
|||
var that = this, onSelectableClick;
|
||||
onSelectableClick = _.bind(this._onSelectableClick, this);
|
||||
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) {
|
||||
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);
|
||||
},
|
||||
open: function open() {
|
||||
this.$node.scrollTop(0);
|
||||
this.$node.addClass(this.classes.open);
|
||||
},
|
||||
close: function close() {
|
||||
this.$node.attr("aria-expanded", false);
|
||||
this.$node.removeClass(this.classes.open);
|
||||
this._removeCursor();
|
||||
},
|
||||
|
@ -988,6 +1053,55 @@
|
|||
});
|
||||
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() {
|
||||
"use strict";
|
||||
var s = Menu.prototype;
|
||||
|
@ -1052,6 +1166,7 @@
|
|||
this.input = o.input;
|
||||
this.menu = o.menu;
|
||||
this.enabled = true;
|
||||
this.autoselect = !!o.autoselect;
|
||||
this.active = false;
|
||||
this.input.hasFocus() && this.activate();
|
||||
this.dir = this.input.getLangDir();
|
||||
|
@ -1098,8 +1213,12 @@
|
|||
_onDatasetCleared: function onDatasetCleared() {
|
||||
this._updateHint();
|
||||
},
|
||||
_onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
|
||||
_onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) {
|
||||
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);
|
||||
},
|
||||
_onAsyncRequested: function onAsyncRequested(type, dataset, query) {
|
||||
|
@ -1122,7 +1241,15 @@
|
|||
_onEnterKeyed: function onEnterKeyed(type, $e) {
|
||||
var $selectable;
|
||||
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) {
|
||||
|
@ -1144,12 +1271,12 @@
|
|||
},
|
||||
_onLeftKeyed: function onLeftKeyed() {
|
||||
if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
|
||||
this.autocomplete(this.menu.getTopSelectable());
|
||||
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
|
||||
}
|
||||
},
|
||||
_onRightKeyed: function onRightKeyed() {
|
||||
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) {
|
||||
|
@ -1249,9 +1376,9 @@
|
|||
},
|
||||
select: function select($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.eventBus.trigger("select", data.obj);
|
||||
this.eventBus.trigger("select", data.obj, data.dataset);
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
|
@ -1262,21 +1389,24 @@
|
|||
query = this.input.getQuery();
|
||||
data = this.menu.getSelectableData($selectable);
|
||||
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.eventBus.trigger("autocomplete", data.obj);
|
||||
this.eventBus.trigger("autocomplete", data.obj, data.dataset);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
moveCursor: function moveCursor(delta) {
|
||||
var query, $candidate, data, payload, cancelMove;
|
||||
var query, $candidate, data, suggestion, datasetName, cancelMove, id;
|
||||
query = this.input.getQuery();
|
||||
$candidate = this.menu.selectableRelativeToCursor(delta);
|
||||
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);
|
||||
if (!cancelMove && !this.eventBus.before("cursorchange", payload)) {
|
||||
if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) {
|
||||
this.menu.setCursor($candidate);
|
||||
if (data) {
|
||||
this.input.setInputValue(data.val);
|
||||
|
@ -1284,7 +1414,7 @@
|
|||
this.input.resetInputValue();
|
||||
this._updateHint();
|
||||
}
|
||||
this.eventBus.trigger("cursorchange", payload);
|
||||
this.eventBus.trigger("cursorchange", suggestion, datasetName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -1322,7 +1452,7 @@
|
|||
www = WWW(o.classNames);
|
||||
return this.each(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) {
|
||||
d.highlight = !!o.highlight;
|
||||
});
|
||||
|
@ -1353,11 +1483,16 @@
|
|||
node: $menu,
|
||||
datasets: datasets
|
||||
}, www);
|
||||
status = new Status({
|
||||
$input: $input,
|
||||
menu: menu
|
||||
});
|
||||
typeahead = new Typeahead({
|
||||
input: input,
|
||||
menu: menu,
|
||||
eventBus: eventBus,
|
||||
minLength: o.minLength
|
||||
minLength: o.minLength,
|
||||
autoselect: o.autoselect
|
||||
}, www);
|
||||
$input.data(keys.www, www);
|
||||
$input.data(keys.typeahead, typeahead);
|
||||
|
@ -1450,7 +1585,7 @@
|
|||
return query;
|
||||
} else {
|
||||
ttEach(this, function(t) {
|
||||
t.setVal(newVal);
|
||||
t.setVal(_.toStr(newVal));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
@ -1481,8 +1616,10 @@
|
|||
});
|
||||
}
|
||||
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({
|
||||
autocomplete: "off",
|
||||
return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({
|
||||
readonly: true,
|
||||
required: false
|
||||
}).removeAttr("id name placeholder").removeClass("required").attr({
|
||||
spellcheck: "false",
|
||||
tabindex: -1
|
||||
});
|
||||
|
@ -1495,7 +1632,6 @@
|
|||
style: $input.attr("style")
|
||||
});
|
||||
$input.addClass(www.classes.input).attr({
|
||||
autocomplete: "off",
|
||||
spellcheck: false
|
||||
});
|
||||
try {
|
||||
|
|
41
resources/assets/js/timeline.js
vendored
41
resources/assets/js/timeline.js
vendored
|
@ -1,13 +1,54 @@
|
|||
$(document).ready(function() {
|
||||
$('.pagination').hide();
|
||||
$('.container.timeline-container').removeClass('d-none');
|
||||
let elem = document.querySelector('.timeline-feed');
|
||||
pixelfed.fetchLikes();
|
||||
|
||||
let infScroll = new InfiniteScroll( elem, {
|
||||
path: '.pagination__next',
|
||||
append: '.timeline-feed',
|
||||
status: '.page-load-status',
|
||||
history: false,
|
||||
});
|
||||
|
||||
infScroll.on( 'append', function( response, path, items ) {
|
||||
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);
|
||||
});
|
||||
|
|
38
resources/assets/sass/custom.scss
vendored
38
resources/assets/sass/custom.scss
vendored
|
@ -173,11 +173,6 @@ body, button, input, textarea {
|
|||
}
|
||||
}
|
||||
|
||||
.fas.fa-heart {
|
||||
color: #f70ec4!important;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, "md")) {
|
||||
.border-md-left-0 {
|
||||
border-left:0!important
|
||||
|
@ -266,3 +261,36 @@ body, button, input, textarea {
|
|||
animation-name: fadeInDown;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ return [
|
|||
'settings' => 'Einstellungen',
|
||||
'admin' => 'Administration',
|
||||
'logout' => 'Abmelden',
|
||||
'directMessages' => 'Privatnachrichten',
|
||||
|
||||
];
|
|
@ -5,4 +5,4 @@ return [
|
|||
'emptyFollowers' => 'Diesem Benutzer folgt noch niemand!',
|
||||
'emptyFollowing' => 'Dieser Benutzer folgt noch niemanden!',
|
||||
'savedWarning' => 'Nur du kannst sehen was du gespeichert hast',
|
||||
];
|
||||
];
|
||||
|
|
|
@ -9,5 +9,6 @@ return [
|
|||
'settings' => 'Settings',
|
||||
'admin' => 'Admin',
|
||||
'logout' => 'Logout',
|
||||
'directMessages' => 'Direct Messages',
|
||||
|
||||
];
|
10
resources/lang/fr/navmenu.php
Normal file
10
resources/lang/fr/navmenu.php
Normal 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',
|
||||
];
|
|
@ -2,4 +2,6 @@
|
|||
return [
|
||||
'likedPhoto' => 'a aimé votre photo.',
|
||||
'startedFollowingYou' => 'a commencé à vous suivre.',
|
||||
'commented' => 'commenté sur votre post.',
|
||||
'mentionedYou' => 'vous à mentionné.'
|
||||
];
|
||||
|
|
13
resources/lang/gl/navmenu.php
Normal file
13
resources/lang/gl/navmenu.php
Normal 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',
|
||||
|
||||
];
|
13
resources/lang/oc/navmenu.php
Normal file
13
resources/lang/oc/navmenu.php
Normal 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',
|
||||
|
||||
];
|
|
@ -4,5 +4,7 @@ return [
|
|||
|
||||
'likedPhoto' => 'a aimat vòstra fòto.',
|
||||
'startedFollowingYou' => 'a començat de vos seguir.',
|
||||
'commented' => 'a comentat vòstra publicacion.',
|
||||
'mentionedYou' => 'vos a mencionat.'
|
||||
|
||||
];
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<?php
|
||||
|
||||
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',
|
||||
];
|
||||
|
|
7
resources/lang/oc/timeline.php
Normal file
7
resources/lang/oc/timeline.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'emptyPersonalTimeline' => 'Vòstre cronologia es voida.'
|
||||
|
||||
];
|
13
resources/lang/pl/navmenu.php
Normal file
13
resources/lang/pl/navmenu.php
Normal 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ę',
|
||||
|
||||
];
|
|
@ -4,5 +4,7 @@ return [
|
|||
|
||||
'likedPhoto' => 'polubił Twoje zdjęcie.',
|
||||
'startedFollowingYou' => 'zaczął Cię obserwować.',
|
||||
'commented' => 'skomentował Twój wpis',
|
||||
'mentionedYou' => 'wspomniał o Tobie.'
|
||||
|
||||
];
|
||||
|
|
35
resources/lang/vendor/backup/gl/notifications.php
vendored
Normal file
35
resources/lang/vendor/backup/gl/notifications.php
vendored
Normal 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.',
|
||||
];
|
|
@ -3,16 +3,53 @@
|
|||
@section('content')
|
||||
<div class="container notification-page" style="min-height: 60vh;">
|
||||
<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">
|
||||
|
||||
@if($notifications->count() > 0)
|
||||
@foreach($notifications as $notification)
|
||||
<li class="list-group-item notification">
|
||||
<li class="list-group-item notification border-0">
|
||||
@switch($notification->action)
|
||||
|
||||
@case('like')
|
||||
<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 class="notification-text">
|
||||
{!! $notification->rendered !!}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue