mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-29 17:53: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_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
ADMIN_DOMAIN="localhost"
|
||||||
|
APP_DOMAIN="localhost"
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=homestead
|
DB_DATABASE=
|
||||||
DB_USERNAME=homestead
|
DB_USERNAME=
|
||||||
DB_PASSWORD=secret
|
DB_PASSWORD=
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
BROADCAST_DRIVER=log
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=redis
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=redis
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
QUEUE_DRIVER=sync
|
QUEUE_DRIVER=redis
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_DRIVER=smtp
|
MAIL_DRIVER=log
|
||||||
MAIL_HOST=smtp.mailtrap.io
|
MAIL_HOST=smtp.mailtrap.io
|
||||||
MAIL_PORT=2525
|
MAIL_PORT=2525
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS="pixelfed@example.com"
|
||||||
|
MAIL_FROM_NAME="Pixelfed"
|
||||||
|
|
||||||
PUSHER_APP_ID=
|
SESSION_DOMAIN="${APP_DOMAIN}"
|
||||||
PUSHER_APP_KEY=
|
|
||||||
PUSHER_APP_SECRET=
|
|
||||||
PUSHER_APP_CLUSTER=mt1
|
|
||||||
|
|
||||||
SESSION_DOMAIN=".pixelfed.dev"
|
|
||||||
SESSION_SECURE_COOKIE=true
|
SESSION_SECURE_COOKIE=true
|
||||||
API_BASE="/api/1/"
|
API_BASE="/api/1/"
|
||||||
API_SEARCH="/api/search"
|
API_SEARCH="/api/search"
|
||||||
|
|
||||||
OPEN_REGISTRATION=true
|
OPEN_REGISTRATION=true
|
||||||
|
RECAPTCHA_ENABLED=false
|
||||||
|
ENFORCE_EMAIL_VERIFICATION=true
|
||||||
|
|
||||||
|
MAX_PHOTO_SIZE=15000
|
||||||
|
MAX_CAPTION_LENGTH=150
|
||||||
|
MAX_ALBUM_LENGTH=4
|
||||||
|
|
||||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
|
|
78
README.md
78
README.md
|
@ -1,4 +1,76 @@
|
||||||
# PixelFed
|
# PixelFed: Federated Image Sharing
|
||||||
Federated Image Sharing
|
|
||||||
|
|
||||||
> This project is still in active development and not yet ready for use.
|
PixelFed is a federated social image sharing platform, similar to Instagram.
|
||||||
|
Federation is done using the [ActivityPub](https://activitypub.rocks/) protocol,
|
||||||
|
which is used by [Mastodon](http://joinmastodon.org/), [PeerTube](https://joinpeertube.org/en/),
|
||||||
|
[Pleroma](https://pleroma.social/), and more. Through ActivityPub PixelFed can share
|
||||||
|
and interact with these platforms, as well as other instances of PixelFed.
|
||||||
|
|
||||||
|
**_Please note this is alpha software, not recommended for production use,
|
||||||
|
and federation is not supported yet._**
|
||||||
|
|
||||||
|
PixelFed is very early into the development stage. If you would like to have a
|
||||||
|
permanent instance with minimal breakage, **do not use this software until
|
||||||
|
there is a stable release**. The following setup instructions are intended for
|
||||||
|
testing and development.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- PHP >= 7.1.3 (7.2+ recommended for stable version)
|
||||||
|
- MySQL, Postgres (MariaDB and sqlite are not supported yet)
|
||||||
|
- Redis
|
||||||
|
- Composer
|
||||||
|
- GD or ImageMagick
|
||||||
|
- OpenSSL PHP Extension
|
||||||
|
- PDO PHP Extension
|
||||||
|
- Mbstring PHP Extension
|
||||||
|
- Tokenizer PHP Extension
|
||||||
|
- XML PHP Extension
|
||||||
|
- Ctype PHP Extension
|
||||||
|
- JSON PHP Extension
|
||||||
|
- JpegOptim
|
||||||
|
- Optipng
|
||||||
|
- Pngquant 2
|
||||||
|
- SVGO
|
||||||
|
- Gifsicle
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This guide assumes you have NGINX/Apache installed, along with the dependencies.
|
||||||
|
Those will not be covered in these early docs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dansup/pixelfed.git
|
||||||
|
cd pixelfed
|
||||||
|
composer install
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Edit .env file with proper values**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan storage:link
|
||||||
|
php artisan migrate
|
||||||
|
php artisan horizon
|
||||||
|
php artisan serve --host=localhost --port=80
|
||||||
|
```
|
||||||
|
|
||||||
|
Check your browser at http://localhost
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
The ways you can communicate on the project are below. Before interacting, please
|
||||||
|
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
* IRC: #pixelfed on irc.freenode.net ([#freenode_#pixelfed:matrix.org through
|
||||||
|
Matrix](https://matrix.to/#/#freenode_#pixelfed:matrix.org)
|
||||||
|
* Project on Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
|
||||||
|
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
The lead maintainer is on Patreon! You can become a Patron at
|
||||||
|
https://www.patreon.com/dansup
|
||||||
|
|
10
app/AccountLog.php
Normal file
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
|
class Hashtag extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['name','slug'];
|
public $fillable = ['name','slug'];
|
||||||
|
|
||||||
public function posts()
|
public function posts()
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,19 +18,47 @@ class AccountController extends Controller
|
||||||
public function notifications(Request $request)
|
public function notifications(Request $request)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'page' => 'nullable|min:1|max:3'
|
'page' => 'nullable|min:1|max:3',
|
||||||
|
'a' => 'nullable|alpha_dash',
|
||||||
]);
|
]);
|
||||||
$profile = Auth::user()->profile;
|
$profile = Auth::user()->profile;
|
||||||
|
$action = $request->input('a');
|
||||||
$timeago = Carbon::now()->subMonths(6);
|
$timeago = Carbon::now()->subMonths(6);
|
||||||
$notifications = Notification::whereProfileId($profile->id)
|
if($action && in_array($action, ['comment', 'follow', 'mention'])) {
|
||||||
->whereDate('created_at', '>', $timeago)
|
$notifications = Notification::whereProfileId($profile->id)
|
||||||
->orderBy('id','desc')
|
->whereAction($action)
|
||||||
->take(30)
|
->whereDate('created_at', '>', $timeago)
|
||||||
->simplePaginate();
|
->orderBy('id','desc')
|
||||||
|
->simplePaginate(30);
|
||||||
|
} else {
|
||||||
|
$notifications = Notification::whereProfileId($profile->id)
|
||||||
|
->whereDate('created_at', '>', $timeago)
|
||||||
|
->orderBy('id','desc')
|
||||||
|
->simplePaginate(30);
|
||||||
|
}
|
||||||
|
|
||||||
return view('account.activity', compact('profile', 'notifications'));
|
return view('account.activity', compact('profile', 'notifications'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function followingActivity(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'page' => 'nullable|min:1|max:3',
|
||||||
|
'a' => 'nullable|alpha_dash',
|
||||||
|
]);
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
$action = $request->input('a');
|
||||||
|
$timeago = Carbon::now()->subMonths(1);
|
||||||
|
$following = $profile->following->pluck('id');
|
||||||
|
$notifications = Notification::whereIn('actor_id', $following)
|
||||||
|
->where('profile_id', '!=', $profile->id)
|
||||||
|
->whereDate('created_at', '>', $timeago)
|
||||||
|
->orderBy('notifications.id','desc')
|
||||||
|
->simplePaginate(30);
|
||||||
|
|
||||||
|
return view('account.following', compact('profile', 'notifications'));
|
||||||
|
}
|
||||||
|
|
||||||
public function verifyEmail(Request $request)
|
public function verifyEmail(Request $request)
|
||||||
{
|
{
|
||||||
return view('account.verify_email');
|
return view('account.verify_email');
|
||||||
|
@ -38,10 +66,19 @@ class AccountController extends Controller
|
||||||
|
|
||||||
public function sendVerifyEmail(Request $request)
|
public function sendVerifyEmail(Request $request)
|
||||||
{
|
{
|
||||||
if(EmailVerification::whereUserId(Auth::id())->count() !== 0) {
|
$timeLimit = Carbon::now()->subDays(1)->toDateTimeString();
|
||||||
return redirect()->back()->with('status', 'A verification email has already been sent! Please check your email.');
|
$recentAttempt = EmailVerification::whereUserId(Auth::id())
|
||||||
|
->where('created_at', '>', $timeLimit)->count();
|
||||||
|
$exists = EmailVerification::whereUserId(Auth::id())->count();
|
||||||
|
|
||||||
|
if($recentAttempt == 1 && $exists == 1) {
|
||||||
|
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
|
||||||
|
} elseif ($recentAttempt == 0 && $exists !== 0) {
|
||||||
|
// Delete old verification and send new one.
|
||||||
|
EmailVerification::whereUserId(Auth::id())->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||||
$utoken = hash('sha512', $user->id);
|
$utoken = hash('sha512', $user->id);
|
||||||
$rtoken = str_random(40);
|
$rtoken = str_random(40);
|
||||||
|
@ -60,14 +97,15 @@ class AccountController extends Controller
|
||||||
|
|
||||||
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||||
{
|
{
|
||||||
$verify = EmailVerification::where(DB::raw('BINARY user_token'), $userToken)
|
$verify = EmailVerification::where('user_token', $userToken)
|
||||||
->where(DB::raw('BINARY random_token'), $randomToken)
|
->where('random_token', $randomToken)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
if(Auth::id() === $verify->user_id) {
|
if(Auth::id() === $verify->user_id) {
|
||||||
$user = User::find(Auth::id());
|
$user = User::find(Auth::id());
|
||||||
$user->email_verified_at = Carbon::now();
|
$user->email_verified_at = Carbon::now();
|
||||||
$user->save();
|
$user->save();
|
||||||
return redirect('/timeline');
|
return redirect('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,4 +133,5 @@ class AccountController extends Controller
|
||||||
}
|
}
|
||||||
return $notifications;
|
return $notifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Auth;
|
use Auth, Cache;
|
||||||
use App\Like;
|
use App\{Like, Status};
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Api\BaseApiController;
|
||||||
|
|
||||||
class ApiController extends Controller
|
class ApiController extends BaseApiController
|
||||||
{
|
{
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->middleware('auth');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hydrateLikes(Request $request)
|
public function hydrateLikes(Request $request)
|
||||||
{
|
{
|
||||||
|
@ -21,12 +18,18 @@ class ApiController extends Controller
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$profile = Auth::user()->profile;
|
$profile = Auth::user()->profile;
|
||||||
|
$res = Cache::remember('api:like-ids:user:'.$profile->id, 1440, function() use ($profile) {
|
||||||
$likes = Like::whereProfileId($profile->id)
|
return Like::whereProfileId($profile->id)
|
||||||
->orderBy('id', 'desc')
|
->orderBy('id', 'desc')
|
||||||
->take(1000)
|
->take(1000)
|
||||||
->pluck('status_id');
|
->pluck('status_id');
|
||||||
|
});
|
||||||
|
|
||||||
return response()->json($likes);
|
return response()->json($res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadMoreComments(Request $request)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\{AccountLog, User};
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ class LoginController extends Controller
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $redirectTo = '/home';
|
protected $redirectTo = '/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new controller instance.
|
* Create a new controller instance.
|
||||||
|
@ -56,4 +57,25 @@ class LoginController extends Controller
|
||||||
|
|
||||||
$this->validate($request, $rules);
|
$this->validate($request, $rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has been authenticated.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param mixed $user
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function authenticated($request, $user)
|
||||||
|
{
|
||||||
|
$log = new AccountLog;
|
||||||
|
$log->user_id = $user->id;
|
||||||
|
$log->item_id = $user->id;
|
||||||
|
$log->item_type = 'App\User';
|
||||||
|
$log->action = 'auth.login';
|
||||||
|
$log->message = 'Account Login';
|
||||||
|
$log->link = null;
|
||||||
|
$log->ip_address = $request->ip();
|
||||||
|
$log->user_agent = $request->userAgent();
|
||||||
|
$log->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,91 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Auth, Log, Storage;
|
||||||
|
use App\Avatar;
|
||||||
|
use App\Jobs\AvatarPipeline\AvatarOptimize;
|
||||||
|
|
||||||
class AvatarController extends Controller
|
class AvatarController extends Controller
|
||||||
{
|
{
|
||||||
//
|
public function __construct()
|
||||||
|
{
|
||||||
|
return $this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'avatar' => 'required|mimes:jpeg,png|max:1000'
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
$user = Auth::user();
|
||||||
|
$file = $request->file('avatar');
|
||||||
|
$path = $this->getPath($user, $file);
|
||||||
|
$dir = $path['root'];
|
||||||
|
$name = $path['name'];
|
||||||
|
$public = $path['storage'];
|
||||||
|
$currentAvatar = storage_path('app/'.$user->profile->avatar->media_path);
|
||||||
|
$loc = $request->file('avatar')->storeAs($public, $name);
|
||||||
|
|
||||||
|
$avatar = Avatar::whereProfileId($user->profile->id)->firstOrFail();
|
||||||
|
$opath = $avatar->media_path;
|
||||||
|
$avatar->media_path = "$public/$name";
|
||||||
|
$avatar->thumb_path = null;
|
||||||
|
$avatar->change_count = ++$avatar->change_count;
|
||||||
|
$avatar->last_processed_at = null;
|
||||||
|
$avatar->save();
|
||||||
|
|
||||||
|
AvatarOptimize::dispatch($user->profile, $currentAvatar);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
}
|
||||||
|
return redirect()->back()->with('status', 'Avatar updated successfully. It may take a few minutes to update across the site.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath($user, $file)
|
||||||
|
{
|
||||||
|
$basePath = storage_path('app/public/avatars');
|
||||||
|
$this->checkDir($basePath);
|
||||||
|
|
||||||
|
$id = $user->profile->id;
|
||||||
|
$path = $this->buildPath($id);
|
||||||
|
$dir = storage_path('app/'.$path);
|
||||||
|
$this->checkDir($dir);
|
||||||
|
$name = 'avatar.' . $file->guessExtension();
|
||||||
|
$res = ['root' => 'storage/app/' . $path, 'name' => $name, 'storage' => $path];
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkDir($path)
|
||||||
|
{
|
||||||
|
if(!is_dir($path)) {
|
||||||
|
mkdir($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildPath($id)
|
||||||
|
{
|
||||||
|
$padded = str_pad($id, 12, 0, STR_PAD_LEFT);
|
||||||
|
$parts = str_split($padded, 3);
|
||||||
|
foreach($parts as $k => $part) {
|
||||||
|
if($k == 0) {
|
||||||
|
$prefix = storage_path('app/public/avatars/'.$parts[0]);
|
||||||
|
$this->checkDir($prefix);
|
||||||
|
}
|
||||||
|
if($k == 1) {
|
||||||
|
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1]);
|
||||||
|
$this->checkDir($prefix);
|
||||||
|
}
|
||||||
|
if($k == 2) {
|
||||||
|
$prefix = storage_path('app/public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2]);
|
||||||
|
$this->checkDir($prefix);
|
||||||
|
}
|
||||||
|
if($k == 3) {
|
||||||
|
$avatarpath = 'public/avatars/'.$parts[0].'/'.$parts[1].'/'.$parts[2].'/'.$parts[3];
|
||||||
|
$prefix = storage_path('app/'.$avatarpath);
|
||||||
|
$this->checkDir($prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $avatarpath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,23 +16,27 @@ class BookmarkController extends Controller
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'item' => 'required|integer|min:1'
|
'item' => 'required|integer|min:1'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$profile = Auth::user()->profile;
|
$profile = Auth::user()->profile;
|
||||||
$status = Status::findOrFail($request->input('item'));
|
$status = Status::findOrFail($request->input('item'));
|
||||||
|
|
||||||
$bookmark = Bookmark::firstOrCreate(
|
$bookmark = Bookmark::firstOrCreate(
|
||||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if(!$bookmark->wasRecentlyCreated) {
|
||||||
|
$bookmark->delete();
|
||||||
|
}
|
||||||
|
|
||||||
if($request->ajax()) {
|
if($request->ajax()) {
|
||||||
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
||||||
} else {
|
} else {
|
||||||
$response = redirect()->back();
|
$response = redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,14 @@ class CommentController extends Controller
|
||||||
return view('status.reply', compact('user', 'status'));
|
return view('status.reply', compact('user', 'status'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function showAll(Request $request, $username, int $id)
|
||||||
|
{
|
||||||
|
$user = Profile::whereUsername($username)->firstOrFail();
|
||||||
|
$status = Status::whereProfileId($user->id)->findOrFail($id);
|
||||||
|
$replies = Status::whereInReplyToId($id)->paginate(40);
|
||||||
|
return view('status.comments', compact('user', 'status', 'replies'));
|
||||||
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
if(Auth::check() === false) { abort(403); }
|
if(Auth::check() === false) { abort(403); }
|
||||||
|
|
|
@ -15,17 +15,44 @@ class DiscoverController extends Controller
|
||||||
|
|
||||||
public function home()
|
public function home()
|
||||||
{
|
{
|
||||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
$pid = Auth::user()->profile->id;
|
||||||
$people = Profile::inRandomOrder()->where('id', '!=', Auth::user()->profile->id)->whereNotIn('id', $following)->take(3)->get();
|
|
||||||
$posts = Status::whereHas('media')->where('profile_id', '!=', Auth::user()->profile->id)->whereNotIn('profile_id', $following)->orderBy('created_at', 'desc')->take('21')->get();
|
$following = Follower::whereProfileId($pid)
|
||||||
|
->pluck('following_id');
|
||||||
|
|
||||||
|
$people = Profile::inRandomOrder()
|
||||||
|
->where('id', '!=', $pid)
|
||||||
|
->whereNotIn('id', $following)
|
||||||
|
->take(3)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$posts = Status::whereHas('media')
|
||||||
|
->where('profile_id', '!=', $pid)
|
||||||
|
->whereNotIn('profile_id', $following)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->simplePaginate(21);
|
||||||
|
|
||||||
return view('discover.home', compact('people', 'posts'));
|
return view('discover.home', compact('people', 'posts'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showTags(Request $request, $hashtag)
|
public function showTags(Request $request, $hashtag)
|
||||||
{
|
{
|
||||||
$tag = Hashtag::whereSlug($hashtag)->firstOrFail();
|
$this->validate($request, [
|
||||||
$posts = $tag->posts()->has('media')->orderBy('id','desc')->paginate(12);
|
'page' => 'nullable|integer|min:1|max:10'
|
||||||
$count = $tag->posts()->has('media')->orderBy('id','desc')->count();
|
]);
|
||||||
return view('discover.tags.show', compact('tag', 'posts', 'count'));
|
|
||||||
|
$tag = Hashtag::with('posts')
|
||||||
|
->withCount('posts')
|
||||||
|
->whereSlug($hashtag)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$posts = $tag->posts()
|
||||||
|
->whereIsNsfw(false)
|
||||||
|
->whereVisibility('public')
|
||||||
|
->has('media')
|
||||||
|
->orderBy('id','desc')
|
||||||
|
->simplePaginate(12);
|
||||||
|
|
||||||
|
return view('discover.tags.show', compact('tag', 'posts'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Auth;
|
use Auth, Cache;
|
||||||
use App\Profile;
|
use App\Profile;
|
||||||
|
use Carbon\Carbon;
|
||||||
use League\Fractal;
|
use League\Fractal;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Util\Lexer\Nickname;
|
use App\Util\Lexer\Nickname;
|
||||||
|
@ -13,15 +14,26 @@ use App\Transformer\ActivityPub\{
|
||||||
ProfileTransformer
|
ProfileTransformer
|
||||||
};
|
};
|
||||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||||
|
use App\Jobs\InboxPipeline\InboxWorker;
|
||||||
|
|
||||||
class FederationController extends Controller
|
class FederationController extends Controller
|
||||||
{
|
{
|
||||||
public function authCheck()
|
public function authCheck()
|
||||||
{
|
{
|
||||||
if(!Auth::check()) {
|
if(!Auth::check()) {
|
||||||
abort(403);
|
return abort(403);
|
||||||
}
|
}
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
public function authorizeFollow(Request $request)
|
||||||
|
{
|
||||||
|
$this->authCheck();
|
||||||
|
$this->validate($request, [
|
||||||
|
'acct' => 'required|string|min:3|max:255'
|
||||||
|
]);
|
||||||
|
$acct = $request->input('acct');
|
||||||
|
$nickname = Nickname::normalizeProfileUrl($acct);
|
||||||
|
return view('federation.authorizefollow', compact('acct', 'nickname'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function remoteFollow()
|
public function remoteFollow()
|
||||||
|
@ -64,61 +76,58 @@ class FederationController extends Controller
|
||||||
|
|
||||||
public function nodeinfo()
|
public function nodeinfo()
|
||||||
{
|
{
|
||||||
$res = [
|
$res = Cache::remember('api:nodeinfo', 60, function() {
|
||||||
'metadata' => [
|
return [
|
||||||
'nodeName' => config('app.name'),
|
'metadata' => [
|
||||||
'software' => [
|
'nodeName' => config('app.name'),
|
||||||
'homepage' => 'https://pixelfed.org',
|
'software' => [
|
||||||
'github' => 'https://github.com/pixelfed',
|
'homepage' => 'https://pixelfed.org',
|
||||||
'follow' => 'https://mastodon.social/@pixelfed'
|
'github' => 'https://github.com/pixelfed',
|
||||||
],
|
'follow' => 'https://mastodon.social/@pixelfed'
|
||||||
/*
|
|
||||||
TODO: Custom Features for Trending
|
|
||||||
'customFeatures' => [
|
|
||||||
'trending' => [
|
|
||||||
'description' => 'Trending API for federated discovery',
|
|
||||||
'api' => [
|
|
||||||
'url' => null,
|
|
||||||
'docs' => null
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
*/
|
'openRegistrations' => config('pixelfed.open_registration'),
|
||||||
],
|
'protocols' => [
|
||||||
'openRegistrations' => config('pixelfed.open_registration'),
|
'activitypub'
|
||||||
'protocols' => [
|
],
|
||||||
'activitypub'
|
'services' => [
|
||||||
],
|
'inbound' => [],
|
||||||
'services' => [
|
'outbound' => []
|
||||||
'inbound' => [],
|
],
|
||||||
'outbound' => []
|
'software' => [
|
||||||
],
|
'name' => 'pixelfed',
|
||||||
'software' => [
|
'version' => config('pixelfed.version')
|
||||||
'name' => 'pixelfed',
|
],
|
||||||
'version' => config('pixelfed.version')
|
'usage' => [
|
||||||
],
|
'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
|
||||||
'usage' => [
|
'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
|
||||||
'localPosts' => \App\Status::whereLocal(true)->count(),
|
'users' => [
|
||||||
'users' => [
|
'total' => \App\User::count(),
|
||||||
'total' => \App\User::count()
|
'activeHalfyear' => \App\User::where('updated_at', '>', Carbon::now()->subMonths(6)->toDateTimeString())->count(),
|
||||||
]
|
'activeMonth' => \App\User::where('updated_at', '>', Carbon::now()->subMonths(1)->toDateTimeString())->count(),
|
||||||
],
|
]
|
||||||
'version' => '2.0'
|
],
|
||||||
];
|
'version' => '2.0'
|
||||||
|
];
|
||||||
return response()->json($res);
|
});
|
||||||
|
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function webfinger(Request $request)
|
public function webfinger(Request $request)
|
||||||
{
|
{
|
||||||
$this->validate($request, ['resource'=>'required']);
|
$this->validate($request, ['resource'=>'required|string|min:3|max:255']);
|
||||||
$resource = $request->input('resource');
|
|
||||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
$hash = hash('sha512', $request->input('resource'));
|
||||||
$username = $parsed['username'];
|
|
||||||
$user = Profile::whereUsername($username)->firstOrFail();
|
$webfinger = Cache::remember('api:webfinger:'.$hash, 1440, function() use($request) {
|
||||||
$webfinger = (new Webfinger($user))->generate();
|
$resource = $request->input('resource');
|
||||||
return response()->json($webfinger);
|
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||||
|
$username = $parsed['username'];
|
||||||
|
$user = Profile::whereUsername($username)->firstOrFail();
|
||||||
|
return (new Webfinger($user))->generate();
|
||||||
|
});
|
||||||
|
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userOutbox(Request $request, $username)
|
public function userOutbox(Request $request, $username)
|
||||||
|
@ -135,4 +144,20 @@ class FederationController extends Controller
|
||||||
return response()->json($res['data']);
|
return response()->json($res['data']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function userInbox(Request $request, $username)
|
||||||
|
{
|
||||||
|
if(config('pixelfed.activitypub_enabled') == false) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
$mimes = [
|
||||||
|
'application/activity+json',
|
||||||
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||||
|
];
|
||||||
|
if(!in_array($request->header('Content-Type'), $mimes)) {
|
||||||
|
abort(500, 'Invalid request');
|
||||||
|
}
|
||||||
|
$profile = Profile::whereUsername($username)->firstOrFail();
|
||||||
|
InboxWorker::dispatch($request, $profile, $request->all());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Auth, Hashids;
|
use Auth, Cache, Hashids;
|
||||||
use App\{Like, Profile, Status, User};
|
use App\{Like, Profile, Status, User};
|
||||||
use App\Jobs\LikePipeline\LikePipeline;
|
use App\Jobs\LikePipeline\LikePipeline;
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class LikeController extends Controller
|
||||||
|
|
||||||
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
||||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||||
$like->delete();
|
$like->forceDelete();
|
||||||
$count--;
|
$count--;
|
||||||
} else {
|
} else {
|
||||||
$like = new Like;
|
$like = new Like;
|
||||||
|
@ -35,9 +35,15 @@ class LikeController extends Controller
|
||||||
$like->status_id = $status->id;
|
$like->status_id = $status->id;
|
||||||
$like->save();
|
$like->save();
|
||||||
$count++;
|
$count++;
|
||||||
|
LikePipeline::dispatch($like);
|
||||||
}
|
}
|
||||||
|
|
||||||
LikePipeline::dispatch($like);
|
$likes = Like::whereProfileId($profile->id)
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->take(1000)
|
||||||
|
->pluck('status_id');
|
||||||
|
|
||||||
|
Cache::put('api:like-ids:user:'.$profile->id, $likes, 1440);
|
||||||
|
|
||||||
if($request->ajax()) {
|
if($request->ajax()) {
|
||||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||||
|
|
|
@ -18,17 +18,22 @@ class ProfileController extends Controller
|
||||||
public function show(Request $request, $username)
|
public function show(Request $request, $username)
|
||||||
{
|
{
|
||||||
$user = Profile::whereUsername($username)->firstOrFail();
|
$user = Profile::whereUsername($username)->firstOrFail();
|
||||||
|
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||||
|
|
||||||
$mimes = [
|
$mimes = [
|
||||||
'application/activity+json',
|
'application/activity+json',
|
||||||
'application/ld+json',
|
|
||||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||||
];
|
];
|
||||||
|
|
||||||
if(in_array($request->header('accept'), $mimes) && config('pixelfed.activitypub_enabled')) {
|
if(in_array($request->header('accept'), $mimes) && config('pixelfed.activitypub_enabled')) {
|
||||||
return $this->showActivityPub($request, $user);
|
return $this->showActivityPub($request, $user);
|
||||||
}
|
}
|
||||||
|
if($user->is_private == true) {
|
||||||
|
$can_access = $this->privateProfileCheck($user);
|
||||||
|
if($can_access !== true) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
// TODO: refactor this mess
|
// TODO: refactor this mess
|
||||||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||||
|
@ -36,11 +41,26 @@ class ProfileController extends Controller
|
||||||
$timeline = $user->statuses()
|
$timeline = $user->statuses()
|
||||||
->whereHas('media')
|
->whereHas('media')
|
||||||
->whereNull('in_reply_to_id')
|
->whereNull('in_reply_to_id')
|
||||||
->orderBy('id','desc')
|
->orderBy('created_at','desc')
|
||||||
->withCount(['comments', 'likes'])
|
->withCount(['comments', 'likes'])
|
||||||
->simplePaginate(21);
|
->simplePaginate(21);
|
||||||
|
|
||||||
return view('profile.show', compact('user', 'owner', 'is_following', 'is_admin', 'timeline'));
|
return view('profile.show', compact('user', 'settings', 'owner', 'is_following', 'is_admin', 'timeline'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function privateProfileCheck(Profile $profile)
|
||||||
|
{
|
||||||
|
if(Auth::check() === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$follower_ids = (array) $profile->followers()->pluck('followers.profile_id');
|
||||||
|
$pid = Auth::user()->profile->id;
|
||||||
|
if(!in_array($pid, $follower_ids) && $pid !== $profile->id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showActivityPub(Request $request, $user)
|
public function showActivityPub(Request $request, $user)
|
||||||
|
@ -89,11 +109,12 @@ class ProfileController extends Controller
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$user = Auth::user()->profile;
|
$user = Auth::user()->profile;
|
||||||
|
$settings = User::whereUsername($username)->firstOrFail()->settings;
|
||||||
$owner = true;
|
$owner = true;
|
||||||
$following = false;
|
$following = false;
|
||||||
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at','desc')->simplePaginate(10);
|
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at','desc')->simplePaginate(10);
|
||||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||||
return view('profile.show', compact('user', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
return view('profile.show', compact('user', 'settings', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,25 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Auth;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\{Avatar, Profile, Report, Status, User};
|
||||||
|
|
||||||
class ReportController extends Controller
|
class ReportController extends Controller
|
||||||
{
|
{
|
||||||
|
protected $profile;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
public function showForm(Request $request)
|
public function showForm(Request $request)
|
||||||
{
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'type' => 'required|alpha_dash',
|
||||||
|
'id' => 'required|integer|min:1'
|
||||||
|
]);
|
||||||
return view('report.form');
|
return view('report.form');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,4 +48,92 @@ class ReportController extends Controller
|
||||||
{
|
{
|
||||||
return view('report.spam.profile');
|
return view('report.spam.profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sensitiveCommentForm(Request $request)
|
||||||
|
{
|
||||||
|
return view('report.sensitive.comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sensitivePostForm(Request $request)
|
||||||
|
{
|
||||||
|
return view('report.sensitive.post');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sensitiveProfileForm(Request $request)
|
||||||
|
{
|
||||||
|
return view('report.sensitive.profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function abusiveCommentForm(Request $request)
|
||||||
|
{
|
||||||
|
return view('report.abusive.comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function abusivePostForm(Request $request)
|
||||||
|
{
|
||||||
|
return view('report.abusive.post');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function abusiveProfileForm(Request $request)
|
||||||
|
{
|
||||||
|
return view('report.abusive.profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formStore(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'report' => 'required|alpha_dash',
|
||||||
|
'type' => 'required|alpha_dash',
|
||||||
|
'id' => 'required|integer|min:1',
|
||||||
|
'msg' => 'nullable|string|max:150'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
$reportType = $request->input('report');
|
||||||
|
$object_id = $request->input('id');
|
||||||
|
$object_type = $request->input('type');
|
||||||
|
$msg = $request->input('msg');
|
||||||
|
$object = null;
|
||||||
|
$types = ['spam', 'sensitive', 'abusive'];
|
||||||
|
|
||||||
|
if(!in_array($reportType, $types)) {
|
||||||
|
return redirect('/timeline')->with('error', 'Invalid report type');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($object_type) {
|
||||||
|
case 'post':
|
||||||
|
$object = Status::findOrFail($object_id);
|
||||||
|
$object_type = 'App\Status';
|
||||||
|
$exists = Report::whereUserId(Auth::id())
|
||||||
|
->whereObjectId($object->id)
|
||||||
|
->whereObjectType('App\Status')
|
||||||
|
->count();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return redirect('/timeline')->with('error', 'Invalid report type');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($exists !== 0) {
|
||||||
|
return redirect('/timeline')->with('error', 'You have already reported this!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($object->profile_id == $profile->id) {
|
||||||
|
return redirect('/timeline')->with('error', 'You cannot report your own content!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = new Report;
|
||||||
|
$report->profile_id = $profile->id;
|
||||||
|
$report->user_id = Auth::id();
|
||||||
|
$report->object_id = $object->id;
|
||||||
|
$report->object_type = $object_type;
|
||||||
|
$report->reported_profile_id = $object->profile_id;
|
||||||
|
$report->type = $request->input('report');
|
||||||
|
$report->message = $request->input('msg');
|
||||||
|
$report->save();
|
||||||
|
|
||||||
|
return redirect('/timeline')->with('status', 'Report successfully sent!');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,28 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\{Profile, User};
|
use App\{AccountLog, Media, Profile, User};
|
||||||
use Auth;
|
use Auth, DB;
|
||||||
|
use App\Util\Lexer\PrettyNumber;
|
||||||
|
|
||||||
class SettingsController extends Controller
|
class SettingsController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
return $this->middleware('auth');
|
$this->middleware('auth');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function home()
|
public function home()
|
||||||
{
|
{
|
||||||
return view('settings.home');
|
$id = Auth::user()->profile->id;
|
||||||
|
$storage = [];
|
||||||
|
$used = Media::whereProfileId($id)->sum('size');
|
||||||
|
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
|
||||||
|
$storage['used'] = $used;
|
||||||
|
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
|
||||||
|
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
|
||||||
|
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
|
||||||
|
return view('settings.home', compact('storage'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function homeUpdate(Request $request)
|
public function homeUpdate(Request $request)
|
||||||
|
@ -89,6 +98,34 @@ class SettingsController extends Controller
|
||||||
return view('settings.avatar');
|
return view('settings.avatar');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function accessibility()
|
||||||
|
{
|
||||||
|
$settings = Auth::user()->settings;
|
||||||
|
return view('settings.accessibility', compact('settings'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accessibilityStore(Request $request)
|
||||||
|
{
|
||||||
|
$settings = Auth::user()->settings;
|
||||||
|
$fields = [
|
||||||
|
'compose_media_descriptions',
|
||||||
|
'reduce_motion',
|
||||||
|
'optimize_screen_reader',
|
||||||
|
'high_contrast_mode',
|
||||||
|
'video_autoplay'
|
||||||
|
];
|
||||||
|
foreach($fields as $field) {
|
||||||
|
$form = $request->input($field);
|
||||||
|
if($form == 'on') {
|
||||||
|
$settings->{$field} = true;
|
||||||
|
} else {
|
||||||
|
$settings->{$field} = false;
|
||||||
|
}
|
||||||
|
$settings->save();
|
||||||
|
}
|
||||||
|
return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!');
|
||||||
|
}
|
||||||
|
|
||||||
public function notifications()
|
public function notifications()
|
||||||
{
|
{
|
||||||
return view('settings.notifications');
|
return view('settings.notifications');
|
||||||
|
@ -96,12 +133,61 @@ class SettingsController extends Controller
|
||||||
|
|
||||||
public function privacy()
|
public function privacy()
|
||||||
{
|
{
|
||||||
return view('settings.privacy');
|
$settings = Auth::user()->settings;
|
||||||
|
$is_private = Auth::user()->profile->is_private;
|
||||||
|
$settings['is_private'] = (bool) $is_private;
|
||||||
|
return view('settings.privacy', compact('settings'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function privacyStore(Request $request)
|
||||||
|
{
|
||||||
|
$settings = Auth::user()->settings;
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
$fields = [
|
||||||
|
'is_private',
|
||||||
|
'crawlable',
|
||||||
|
];
|
||||||
|
foreach($fields as $field) {
|
||||||
|
$form = $request->input($field);
|
||||||
|
if($field == 'is_private') {
|
||||||
|
if($form == 'on') {
|
||||||
|
$profile->{$field} = true;
|
||||||
|
$settings->show_guests = false;
|
||||||
|
$settings->show_discover = false;
|
||||||
|
$profile->save();
|
||||||
|
} else {
|
||||||
|
$profile->{$field} = false;
|
||||||
|
$profile->save();
|
||||||
|
}
|
||||||
|
} elseif($field == 'crawlable') {
|
||||||
|
if($form == 'on') {
|
||||||
|
$settings->{$field} = false;
|
||||||
|
} else {
|
||||||
|
$settings->{$field} = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if($form == 'on') {
|
||||||
|
$settings->{$field} = true;
|
||||||
|
} else {
|
||||||
|
$settings->{$field} = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$settings->save();
|
||||||
|
}
|
||||||
|
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function security()
|
public function security()
|
||||||
{
|
{
|
||||||
return view('settings.security');
|
$sessions = DB::table('sessions')
|
||||||
|
->whereUserId(Auth::id())
|
||||||
|
->limit(20)
|
||||||
|
->get();
|
||||||
|
$activity = AccountLog::whereUserId(Auth::id())
|
||||||
|
->orderBy('created_at','desc')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
return view('settings.security', compact('sessions', 'activity'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applications()
|
public function applications()
|
||||||
|
@ -121,7 +207,7 @@ class SettingsController extends Controller
|
||||||
|
|
||||||
public function dataImportInstagram()
|
public function dataImportInstagram()
|
||||||
{
|
{
|
||||||
return view('settings.import.ig');
|
return view('settings.import.instagram.home');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function developers()
|
public function developers()
|
||||||
|
|
|
@ -2,11 +2,42 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App;
|
use App, Auth, Cache;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\{Follower, Profile, Status, User};
|
||||||
|
use App\Util\Lexer\PrettyNumber;
|
||||||
|
|
||||||
class SiteController extends Controller
|
class SiteController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
|
public function home()
|
||||||
|
{
|
||||||
|
if(Auth::check()) {
|
||||||
|
return $this->homeTimeline();
|
||||||
|
} else {
|
||||||
|
return $this->homeGuest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function homeGuest()
|
||||||
|
{
|
||||||
|
return view('welcome');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function homeTimeline()
|
||||||
|
{
|
||||||
|
// TODO: Use redis for timelines
|
||||||
|
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||||
|
$following->push(Auth::user()->profile->id);
|
||||||
|
$timeline = Status::whereIn('profile_id', $following)
|
||||||
|
->whereHas('media')
|
||||||
|
->orderBy('id','desc')
|
||||||
|
->withCount(['comments', 'likes', 'shares'])
|
||||||
|
->simplePaginate(20);
|
||||||
|
$type = 'personal';
|
||||||
|
return view('timeline.template', compact('timeline', 'type'));
|
||||||
|
}
|
||||||
|
|
||||||
public function changeLocale(Request $request, $locale)
|
public function changeLocale(Request $request, $locale)
|
||||||
{
|
{
|
||||||
if(!App::isLocale($locale)) {
|
if(!App::isLocale($locale)) {
|
||||||
|
@ -15,4 +46,20 @@ class SiteController extends Controller
|
||||||
App::setLocale($locale);
|
App::setLocale($locale);
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function about()
|
||||||
|
{
|
||||||
|
$res = Cache::remember('site:page:about', 15, function() {
|
||||||
|
$statuses = Status::whereHas('media')
|
||||||
|
->whereNull('in_reply_to_id')
|
||||||
|
->whereNull('reblog_of_id')
|
||||||
|
->count();
|
||||||
|
$statusCount = PrettyNumber::convert($statuses);
|
||||||
|
$userCount = PrettyNumber::convert(User::count());
|
||||||
|
$remoteCount = PrettyNumber::convert(Profile::whereNotNull('remote_url')->count());
|
||||||
|
$adminContact = User::whereIsAdmin(true)->first();
|
||||||
|
return view('site.about')->with(compact('statusCount', 'userCount', 'remoteCount', 'adminContact'))->render();
|
||||||
|
});
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,93 +13,147 @@ class StatusController extends Controller
|
||||||
{
|
{
|
||||||
public function show(Request $request, $username, int $id)
|
public function show(Request $request, $username, int $id)
|
||||||
{
|
{
|
||||||
$user = Profile::whereUsername($username)->firstOrFail();
|
$user = Profile::whereUsername($username)->firstOrFail();
|
||||||
$status = Status::whereProfileId($user->id)
|
$status = Status::whereProfileId($user->id)
|
||||||
->withCount(['likes', 'comments', 'media'])
|
->withCount(['likes', 'comments', 'media'])
|
||||||
->findOrFail($id);
|
->findOrFail($id);
|
||||||
if(!$status->media_path && $status->in_reply_to_id) {
|
if(!$status->media_path && $status->in_reply_to_id) {
|
||||||
return redirect($status->url());
|
return redirect($status->url());
|
||||||
}
|
}
|
||||||
return view('status.show', compact('user', 'status'));
|
$replies = Status::whereInReplyToId($status->id)->simplePaginate(30);
|
||||||
|
return view('status.show', compact('user', 'status', 'replies'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose()
|
||||||
|
{
|
||||||
|
if(Auth::check() == false)
|
||||||
|
{
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
return view('status.compose');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
if(Auth::check() == false)
|
if(Auth::check() == false)
|
||||||
{
|
{
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
$this->validate($request, [
|
$size = Media::whereUserId($user->id)->sum('size') / 1000;
|
||||||
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
|
$limit = (int) config('pixelfed.max_account_size');
|
||||||
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
|
if($size >= $limit) {
|
||||||
'cw' => 'nullable|string',
|
return redirect()->back()->with('error', 'You have exceeded your storage limit. Please click <a href="#">here</a> for more info.');
|
||||||
'filter_class' => 'nullable|string',
|
}
|
||||||
'filter_name' => 'nullable|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
|
$this->validate($request, [
|
||||||
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
|
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
|
||||||
}
|
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
|
||||||
|
'cw' => 'nullable|string',
|
||||||
|
'filter_class' => 'nullable|string',
|
||||||
|
'filter_name' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
|
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
|
||||||
$monthHash = hash('sha1', date('Y') . date('m'));
|
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
|
||||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
}
|
||||||
$profile = $user->profile;
|
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
|
||||||
|
$monthHash = hash('sha1', date('Y') . date('m'));
|
||||||
|
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||||
|
$profile = $user->profile;
|
||||||
|
|
||||||
$status = new Status;
|
$status = new Status;
|
||||||
$status->profile_id = $profile->id;
|
$status->profile_id = $profile->id;
|
||||||
$status->caption = strip_tags($request->caption);
|
$status->caption = strip_tags($request->caption);
|
||||||
$status->is_nsfw = $cw;
|
$status->is_nsfw = $cw;
|
||||||
|
|
||||||
$status->save();
|
$status->save();
|
||||||
|
|
||||||
$photos = $request->file('photo');
|
$photos = $request->file('photo');
|
||||||
$order = 1;
|
$order = 1;
|
||||||
foreach ($photos as $k => $v) {
|
foreach ($photos as $k => $v) {
|
||||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||||
$path = $v->store($storagePath);
|
$path = $v->store($storagePath);
|
||||||
$media = new Media;
|
$media = new Media;
|
||||||
$media->status_id = $status->id;
|
$media->status_id = $status->id;
|
||||||
$media->profile_id = $profile->id;
|
$media->profile_id = $profile->id;
|
||||||
$media->user_id = $user->id;
|
$media->user_id = $user->id;
|
||||||
$media->media_path = $path;
|
$media->media_path = $path;
|
||||||
$media->size = $v->getClientSize();
|
$media->size = $v->getClientSize();
|
||||||
$media->mime = $v->getClientMimeType();
|
$media->mime = $v->getClientMimeType();
|
||||||
$media->filter_class = $request->input('filter_class');
|
$media->filter_class = $request->input('filter_class');
|
||||||
$media->filter_name = $request->input('filter_name');
|
$media->filter_name = $request->input('filter_name');
|
||||||
$media->order = $order;
|
$media->order = $order;
|
||||||
$media->save();
|
$media->save();
|
||||||
ImageOptimize::dispatch($media);
|
ImageOptimize::dispatch($media);
|
||||||
$order++;
|
$order++;
|
||||||
}
|
}
|
||||||
|
|
||||||
NewStatusPipeline::dispatch($status);
|
NewStatusPipeline::dispatch($status);
|
||||||
|
|
||||||
// TODO: Send to subscribers
|
// TODO: Send to subscribers
|
||||||
|
|
||||||
return redirect($status->url());
|
return redirect($status->url());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Request $request)
|
public function delete(Request $request)
|
||||||
{
|
{
|
||||||
if(!Auth::check()) {
|
if(!Auth::check()) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'type' => 'required|string',
|
'type' => 'required|string',
|
||||||
'item' => 'required|integer|min:1'
|
'item' => 'required|integer|min:1'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$status = Status::findOrFail($request->input('item'));
|
$status = Status::findOrFail($request->input('item'));
|
||||||
|
|
||||||
if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
|
if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
|
||||||
StatusDelete::dispatch($status);
|
StatusDelete::dispatch($status);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(Auth::user()->url());
|
return redirect(Auth::user()->url());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeShare(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'item' => 'required|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
$status = Status::withCount('shares')->findOrFail($request->input('item'));
|
||||||
|
|
||||||
|
$count = $status->shares_count;
|
||||||
|
|
||||||
|
$exists = Status::whereProfileId(Auth::user()->profile->id)
|
||||||
|
->whereReblogOfId($status->id)
|
||||||
|
->count();
|
||||||
|
if($exists !== 0) {
|
||||||
|
$shares = Status::whereProfileId(Auth::user()->profile->id)
|
||||||
|
->whereReblogOfId($status->id)
|
||||||
|
->get();
|
||||||
|
foreach($shares as $share) {
|
||||||
|
$share->delete();
|
||||||
|
$count--;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$share = new Status;
|
||||||
|
$share->profile_id = $profile->id;
|
||||||
|
$share->reblog_of_id = $status->id;
|
||||||
|
$share->save();
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->ajax()) {
|
||||||
|
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
|
||||||
|
} else {
|
||||||
|
$response = redirect($status->url());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,24 +18,23 @@ class TimelineController extends Controller
|
||||||
// TODO: Use redis for timelines
|
// TODO: Use redis for timelines
|
||||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||||
$following->push(Auth::user()->profile->id);
|
$following->push(Auth::user()->profile->id);
|
||||||
$timeline = Status::whereHas('media')
|
$timeline = Status::whereIn('profile_id', $following)
|
||||||
->whereNull('in_reply_to_id')
|
|
||||||
->whereIn('profile_id', $following)
|
|
||||||
->orderBy('id','desc')
|
->orderBy('id','desc')
|
||||||
->withCount(['comments', 'likes'])
|
->simplePaginate(20);
|
||||||
->simplePaginate(10);
|
$type = 'personal';
|
||||||
return view('timeline.personal', compact('timeline'));
|
return view('timeline.template', compact('timeline', 'type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function local()
|
public function local()
|
||||||
{
|
{
|
||||||
// TODO: Use redis for timelines
|
// TODO: Use redis for timelines
|
||||||
|
// $timeline = Timeline::build()->local();
|
||||||
$timeline = Status::whereHas('media')
|
$timeline = Status::whereHas('media')
|
||||||
->whereNull('in_reply_to_id')
|
->whereNull('in_reply_to_id')
|
||||||
->orderBy('id','desc')
|
->orderBy('id','desc')
|
||||||
->withCount(['comments', 'likes'])
|
->simplePaginate(20);
|
||||||
->simplePaginate(10);
|
$type = 'local';
|
||||||
return view('timeline.public', compact('timeline'));
|
return view('timeline.template', compact('timeline', 'type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ class EmailVerificationCheck
|
||||||
if($request->user() &&
|
if($request->user() &&
|
||||||
config('pixelfed.enforce_email_verification') &&
|
config('pixelfed.enforce_email_verification') &&
|
||||||
is_null($request->user()->email_verified_at) &&
|
is_null($request->user()->email_verified_at) &&
|
||||||
!$request->is('i/verify-email') && !$request->is('login') &&
|
!$request->is('i/verify-email') && !$request->is('log*') &&
|
||||||
!$request->is('i/confirm-email/*')
|
!$request->is('i/confirm-email/*')
|
||||||
) {
|
) {
|
||||||
return redirect('/i/verify-email');
|
return redirect('/i/verify-email');
|
||||||
|
|
10
app/ImportJob.php
Normal file
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;
|
$status = $this->like->status;
|
||||||
$actor = $this->like->actor;
|
$actor = $this->like->actor;
|
||||||
|
|
||||||
|
if($status->url !== null) {
|
||||||
|
// Ignore notifications to remote statuses
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$exists = Notification::whereProfileId($status->profile_id)
|
$exists = Notification::whereProfileId($status->profile_id)
|
||||||
->whereActorId($actor->id)
|
->whereActorId($actor->id)
|
||||||
->whereAction('like')
|
->whereAction('like')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Jobs\StatusPipeline;
|
namespace App\Jobs\StatusPipeline;
|
||||||
|
|
||||||
use Cache;
|
use DB, Cache;
|
||||||
use App\{
|
use App\{
|
||||||
Hashtag,
|
Hashtag,
|
||||||
Media,
|
Media,
|
||||||
|
@ -68,12 +68,14 @@ class StatusEntityLexer implements ShouldQueue
|
||||||
|
|
||||||
public function storeEntities()
|
public function storeEntities()
|
||||||
{
|
{
|
||||||
$status = $this->status;
|
|
||||||
$this->storeHashtags();
|
$this->storeHashtags();
|
||||||
$this->storeMentions();
|
$this->storeMentions();
|
||||||
$status->rendered = $this->autolink;
|
DB::transaction(function () {
|
||||||
$status->entities = json_encode($this->entities);
|
$status = $this->status;
|
||||||
$status->save();
|
$status->rendered = $this->autolink;
|
||||||
|
$status->entities = json_encode($this->entities);
|
||||||
|
$status->save();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function storeHashtags()
|
public function storeHashtags()
|
||||||
|
@ -82,17 +84,15 @@ class StatusEntityLexer implements ShouldQueue
|
||||||
$status = $this->status;
|
$status = $this->status;
|
||||||
|
|
||||||
foreach($tags as $tag) {
|
foreach($tags as $tag) {
|
||||||
$slug = str_slug($tag);
|
DB::transaction(function () use ($status, $tag) {
|
||||||
|
$slug = str_slug($tag);
|
||||||
$htag = Hashtag::firstOrCreate(
|
$hashtag = Hashtag::firstOrCreate(
|
||||||
['name' => $tag],
|
['name' => $tag, 'slug' => $slug]
|
||||||
['slug' => $slug]
|
);
|
||||||
);
|
StatusHashtag::firstOrCreate(
|
||||||
|
['status_id' => $status->id, 'hashtag_id' => $hashtag->id]
|
||||||
StatusHashtag::firstOrCreate(
|
);
|
||||||
['status_id' => $status->id],
|
});
|
||||||
['hashtag_id' => $htag->id]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,18 +102,20 @@ class StatusEntityLexer implements ShouldQueue
|
||||||
$status = $this->status;
|
$status = $this->status;
|
||||||
|
|
||||||
foreach($mentions as $mention) {
|
foreach($mentions as $mention) {
|
||||||
$mentioned = Profile::whereUsername($mention)->first();
|
$mentioned = Profile::whereUsername($mention)->firstOrFail();
|
||||||
|
|
||||||
if(empty($mentioned) || !isset($mentioned->id)) {
|
if(empty($mentioned) || !isset($mentioned->id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$m = new Mention;
|
DB::transaction(function () use ($status, $mentioned) {
|
||||||
$m->status_id = $status->id;
|
$m = new Mention;
|
||||||
$m->profile_id = $mentioned->id;
|
$m->status_id = $status->id;
|
||||||
$m->save();
|
$m->profile_id = $mentioned->id;
|
||||||
|
$m->save();
|
||||||
MentionPipeline::dispatch($status, $m);
|
|
||||||
|
MentionPipeline::dispatch($status, $m);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,4 +23,11 @@ class Media extends Model
|
||||||
$url = Storage::url($path);
|
$url = Storage::url($path);
|
||||||
return url($url);
|
return url($url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function thumbnailUrl()
|
||||||
|
{
|
||||||
|
$path = $this->thumbnail_path;
|
||||||
|
$url = Storage::url($path);
|
||||||
|
return url($url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
use App\{Profile, User};
|
use App\{Profile, User, UserSetting};
|
||||||
use App\Jobs\AvatarPipeline\CreateAvatar;
|
use App\Jobs\AvatarPipeline\CreateAvatar;
|
||||||
|
|
||||||
class UserObserver
|
class UserObserver
|
||||||
|
@ -36,6 +36,12 @@ class UserObserver
|
||||||
|
|
||||||
CreateAvatar::dispatch($profile);
|
CreateAvatar::dispatch($profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(empty($user->settings)) {
|
||||||
|
$settings = new UserSetting;
|
||||||
|
$settings->user_id = $user->id;
|
||||||
|
$settings->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use Storage;
|
use Auth, Storage;
|
||||||
use App\Util\Lexer\PrettyNumber;
|
use App\Util\Lexer\PrettyNumber;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
@ -29,6 +29,15 @@ class Profile extends Model
|
||||||
}
|
}
|
||||||
|
|
||||||
public function url($suffix = '')
|
public function url($suffix = '')
|
||||||
|
{
|
||||||
|
if($this->remote_url) {
|
||||||
|
return $this->remote_url;
|
||||||
|
} else {
|
||||||
|
return url($this->username . $suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function localUrl($suffix = '')
|
||||||
{
|
{
|
||||||
return url($this->username . $suffix);
|
return url($this->username . $suffix);
|
||||||
}
|
}
|
||||||
|
@ -124,4 +133,40 @@ class Profile extends Model
|
||||||
$url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png'));
|
$url = url(Storage::url($this->avatar->media_path ?? 'public/avatars/default.png'));
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function statusCount()
|
||||||
|
{
|
||||||
|
return $this->statuses()->whereHas('media')->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recommendFollowers()
|
||||||
|
{
|
||||||
|
$follows = $this->following()->pluck('followers.id');
|
||||||
|
$following = $this->following()
|
||||||
|
->orderByRaw('rand()')
|
||||||
|
->take(3)
|
||||||
|
->pluck('following_id');
|
||||||
|
$following->push(Auth::id());
|
||||||
|
$following = Follower::whereNotIn('profile_id', $follows)
|
||||||
|
->whereNotIn('following_id', $following)
|
||||||
|
->whereNotIn('following_id', $follows)
|
||||||
|
->whereIn('profile_id', $following)
|
||||||
|
->orderByRaw('rand()')
|
||||||
|
->limit(3)
|
||||||
|
->pluck('following_id');
|
||||||
|
$recommended = [];
|
||||||
|
foreach($following as $follow) {
|
||||||
|
$recommended[] = Profile::findOrFail($follow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $recommended;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function keyId()
|
||||||
|
{
|
||||||
|
if($this->remote_url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return $this->permalink('#main-key');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@ class EventServiceProvider extends ServiceProvider
|
||||||
'App\Events\Event' => [
|
'App\Events\Event' => [
|
||||||
'App\Listeners\EventListener',
|
'App\Listeners\EventListener',
|
||||||
],
|
],
|
||||||
|
'auth.login' => [
|
||||||
|
'App\Events\AuthLoginEvent',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,5 +6,33 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Report extends Model
|
class Report extends Model
|
||||||
{
|
{
|
||||||
//
|
public function url()
|
||||||
|
{
|
||||||
|
return url('/i/admin/reports/show/' . $this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reporter()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'profile_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reported()
|
||||||
|
{
|
||||||
|
$class = $this->object_type;
|
||||||
|
switch ($class) {
|
||||||
|
case 'App\Status':
|
||||||
|
$column = 'id';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$column = 'id';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (new $class())->where($column, $this->object_id)->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportedUser()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Profile::class, 'reported_profile_id', 'id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
10
app/ReportComment.php
Normal file
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;
|
namespace App;
|
||||||
|
|
||||||
use Storage;
|
use Auth, Storage;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
@ -52,6 +52,14 @@ class Status extends Model
|
||||||
return url($path);
|
return url($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function permalink($suffix = '/activity')
|
||||||
|
{
|
||||||
|
$id = $this->id;
|
||||||
|
$username = $this->profile->username;
|
||||||
|
$path = config('app.url') . "/p/{$username}/{$id}{$suffix}";
|
||||||
|
return url($path);
|
||||||
|
}
|
||||||
|
|
||||||
public function editUrl()
|
public function editUrl()
|
||||||
{
|
{
|
||||||
return $this->url() . '/edit';
|
return $this->url() . '/edit';
|
||||||
|
@ -71,15 +79,42 @@ class Status extends Model
|
||||||
return $this->hasMany(Like::class);
|
return $this->hasMany(Like::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function liked() : bool
|
||||||
|
{
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||||
|
}
|
||||||
|
|
||||||
public function comments()
|
public function comments()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Status::class, 'in_reply_to_id');
|
return $this->hasMany(Status::class, 'in_reply_to_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function bookmarked()
|
||||||
|
{
|
||||||
|
if(!Auth::check()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shares()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Status::class, 'reblog_of_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shared() : bool
|
||||||
|
{
|
||||||
|
$profile = Auth::user()->profile;
|
||||||
|
return Status::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
|
||||||
|
}
|
||||||
|
|
||||||
public function parent()
|
public function parent()
|
||||||
{
|
{
|
||||||
if(!empty($this->in_reply_to_id)) {
|
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||||
return Status::findOrFail($this->in_reply_to_id);
|
if(!empty($parent)) {
|
||||||
|
return Status::findOrFail($parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +135,23 @@ class Status extends Model
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function mentions()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
Profile::class,
|
||||||
|
Mention::class,
|
||||||
|
'status_id',
|
||||||
|
'id',
|
||||||
|
'id',
|
||||||
|
'profile_id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportUrl()
|
||||||
|
{
|
||||||
|
return route('report.form') . "?type=post&id={$this->id}";
|
||||||
|
}
|
||||||
|
|
||||||
public function toActivityStream()
|
public function toActivityStream()
|
||||||
{
|
{
|
||||||
$media = $this->media;
|
$media = $this->media;
|
||||||
|
@ -133,4 +185,9 @@ class Status extends Model
|
||||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
||||||
__('notification.commented');
|
__('notification.commented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function recentComments()
|
||||||
|
{
|
||||||
|
return $this->comments()->orderBy('created_at','desc')->take(3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,5 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class StatusHashtag extends Model
|
class StatusHashtag extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['status_id', 'hashtag_id'];
|
public $fillable = ['status_id', 'hashtag_id'];
|
||||||
}
|
}
|
||||||
|
|
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
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $dates = ['deleted_at'];
|
protected $dates = ['deleted_at', 'email_verified_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
@ -44,4 +44,9 @@ class User extends Authenticatable
|
||||||
{
|
{
|
||||||
return url(config('app.url') . '/' . $this->username);
|
return url(config('app.url') . '/' . $this->username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function settings()
|
||||||
|
{
|
||||||
|
return $this->hasOne(UserSetting::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
10
app/UserFilter.php
Normal file
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
|
// Static Assets
|
||||||
"assets",
|
"assets",
|
||||||
|
"storage",
|
||||||
|
|
||||||
// Laravel Horizon
|
// Laravel Horizon
|
||||||
"horizon",
|
"horizon",
|
||||||
|
@ -127,14 +128,19 @@ class RestrictedNames {
|
||||||
"api",
|
"api",
|
||||||
"auth",
|
"auth",
|
||||||
"i",
|
"i",
|
||||||
|
"dashboard",
|
||||||
"discover",
|
"discover",
|
||||||
|
"docs",
|
||||||
"home",
|
"home",
|
||||||
"login",
|
"login",
|
||||||
"logout",
|
"logout",
|
||||||
|
"media",
|
||||||
"p",
|
"p",
|
||||||
"password",
|
"password",
|
||||||
|
"reports",
|
||||||
"search",
|
"search",
|
||||||
"settings",
|
"settings",
|
||||||
|
"statuses",
|
||||||
"site",
|
"site",
|
||||||
"timeline",
|
"timeline",
|
||||||
"user",
|
"user",
|
||||||
|
|
|
@ -115,8 +115,9 @@ class Image {
|
||||||
});
|
});
|
||||||
$converted = $this->setBaseName($path, $thumbnail, $img->extension);
|
$converted = $this->setBaseName($path, $thumbnail, $img->extension);
|
||||||
$newPath = storage_path('app/'.$converted['path']);
|
$newPath = storage_path('app/'.$converted['path']);
|
||||||
|
|
||||||
$img->save($newPath, 75);
|
$quality = config('pixelfed.image_quality');
|
||||||
|
$img->save($newPath, $quality);
|
||||||
|
|
||||||
if(!$thumbnail) {
|
if(!$thumbnail) {
|
||||||
$media->orientation = $orientation;
|
$media->orientation = $orientation;
|
||||||
|
|
|
@ -31,13 +31,9 @@ class Webfinger {
|
||||||
|
|
||||||
public function generateAliases()
|
public function generateAliases()
|
||||||
{
|
{
|
||||||
$host = parse_url(config('app.url'), PHP_URL_HOST);
|
|
||||||
$username = $this->user->username;
|
|
||||||
$url = $this->user->url();
|
|
||||||
|
|
||||||
$this->aliases = [
|
$this->aliases = [
|
||||||
'acct:'.$username.'@'.$host,
|
$this->user->url(),
|
||||||
$url
|
$this->user->permalink()
|
||||||
];
|
];
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -55,24 +51,12 @@ class Webfinger {
|
||||||
[
|
[
|
||||||
'rel' => 'http://schemas.google.com/g/2010#updates-from',
|
'rel' => 'http://schemas.google.com/g/2010#updates-from',
|
||||||
'type' => 'application/atom+xml',
|
'type' => 'application/atom+xml',
|
||||||
'href' => url("/users/{$user->username}.atom")
|
'href' => $user->permalink('.atom')
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'rel' => 'self',
|
'rel' => 'self',
|
||||||
'type' => 'application/activity+json',
|
'type' => 'application/activity+json',
|
||||||
'href' => $user->permalink()
|
'href' => $user->permalink()
|
||||||
],
|
|
||||||
[
|
|
||||||
'rel' => 'magic-public-key',
|
|
||||||
'href' => null//$user->public_key
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'rel' => 'salmon',
|
|
||||||
'href' => $user->permalink('/salmon')
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
|
||||||
'href' => url('/main/ostatussub?profile={uri}')
|
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
@ -6,18 +6,23 @@
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
"99designs/http-signatures-guzzlehttp": "^2.0",
|
"beyondcode/laravel-self-diagnosis": "^0.4.0",
|
||||||
"bitverse/identicon": "^1.1",
|
"bitverse/identicon": "^1.1",
|
||||||
"doctrine/dbal": "^2.7",
|
"doctrine/dbal": "^2.7",
|
||||||
"fideloper/proxy": "^4.0",
|
"fideloper/proxy": "^4.0",
|
||||||
"greggilbert/recaptcha": "dev-master",
|
"greggilbert/recaptcha": "dev-master",
|
||||||
"intervention/image": "^2.4",
|
"intervention/image": "^2.4",
|
||||||
"kitetail/zttp": "^0.3.0",
|
"pixelfed/zttp": "^0.4",
|
||||||
"laravel/framework": "5.6.*",
|
"laravel/framework": "5.6.*",
|
||||||
"laravel/horizon": "^1.2",
|
"laravel/horizon": "^1.2",
|
||||||
|
"laravel/passport": "^6.0",
|
||||||
"laravel/tinker": "^1.0",
|
"laravel/tinker": "^1.0",
|
||||||
"league/fractal": "^0.17.0",
|
"moontoast/math": "^1.1",
|
||||||
"phpseclib/phpseclib": "~2.0",
|
"phpseclib/phpseclib": "~2.0",
|
||||||
|
"pixelfed/dotenv-editor": "^2.0",
|
||||||
|
"pixelfed/fractal": "^0.18.0",
|
||||||
|
"pixelfed/google2fa-laravel": "^2.0",
|
||||||
|
"pixelfed/http-signatures-guzzlehttp": "^4.0",
|
||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
"spatie/laravel-backup": "^5.0.0",
|
"spatie/laravel-backup": "^5.0.0",
|
||||||
"spatie/laravel-image-optimizer": "^1.1",
|
"spatie/laravel-image-optimizer": "^1.1",
|
||||||
|
@ -25,6 +30,7 @@
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.1",
|
"barryvdh/laravel-debugbar": "^3.1",
|
||||||
|
"beyondcode/laravel-er-diagram-generator": "^0.2.2",
|
||||||
"filp/whoops": "^2.0",
|
"filp/whoops": "^2.0",
|
||||||
"fzaninotto/faker": "^1.4",
|
"fzaninotto/faker": "^1.4",
|
||||||
"mockery/mockery": "^1.0",
|
"mockery/mockery": "^1.0",
|
||||||
|
|
2148
composer.lock
generated
2148
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -151,6 +151,7 @@ return [
|
||||||
* Package Service Providers...
|
* Package Service Providers...
|
||||||
*/
|
*/
|
||||||
Greggilbert\Recaptcha\RecaptchaServiceProvider::class,
|
Greggilbert\Recaptcha\RecaptchaServiceProvider::class,
|
||||||
|
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Application Service Providers...
|
* Application Service Providers...
|
||||||
|
@ -211,6 +212,7 @@ return [
|
||||||
'View' => Illuminate\Support\Facades\View::class,
|
'View' => Illuminate\Support\Facades\View::class,
|
||||||
|
|
||||||
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::class,
|
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::class,
|
||||||
|
'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
27
config/dotenv-editor.php
Normal file
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',
|
'connection' => 'redis',
|
||||||
'queue' => ['default'],
|
'queue' => ['default'],
|
||||||
'balance' => 'simple',
|
'balance' => 'simple',
|
||||||
'processes' => 10,
|
'processes' => 20,
|
||||||
'tries' => 3,
|
'tries' => 3,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
|
@ -49,5 +49,5 @@ return [
|
||||||
* If set to `true` all output of the optimizer binaries will be appended to the default log.
|
* If set to `true` all output of the optimizer binaries will be appended to the default log.
|
||||||
* You can also set this to a class that implements `Psr\Log\LoggerInterface`.
|
* You can also set this to a class that implements `Psr\Log\LoggerInterface`.
|
||||||
*/
|
*/
|
||||||
'log_optimizer_activity' => true,
|
'log_optimizer_activity' => false,
|
||||||
];
|
];
|
||||||
|
|
|
@ -23,7 +23,7 @@ return [
|
||||||
| This value is the version of your PixelFed instance.
|
| This value is the version of your PixelFed instance.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
'version' => '0.1.0',
|
'version' => '0.1.4',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -77,6 +77,17 @@ return [
|
||||||
|
|
||||||
'activitypub_enabled' => env('ACTIVITY_PUB', false),
|
'activitypub_enabled' => env('ACTIVITY_PUB', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Account file size limit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Update the max account size, the per user limit of files in KB.
|
||||||
|
|
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'max_account_size' => env('MAX_ACCOUNT_SIZE', 1000000),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Photo file size limit
|
| Photo file size limit
|
||||||
|
@ -95,7 +106,7 @@ return [
|
||||||
| Change the caption length limit for new local posts.
|
| Change the caption length limit for new local posts.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
'max_caption_length' => env('MAX_CAPTION_LENGTH', 150),
|
'max_caption_length' => env('MAX_CAPTION_LENGTH', 500),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -116,5 +127,15 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
|
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Image Quality
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Set the image optimization quality, must be a value between 1-100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'image_quality' => (int) env('IMAGE_QUALITY', 80),
|
||||||
|
|
||||||
];
|
];
|
|
@ -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"
|
- "mysql-data:/var/lib/mysql"
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:4-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- "redis-data:/data"
|
- "redis-data:/data"
|
||||||
networks:
|
networks:
|
||||||
|
|
BIN
public/css/app.css
vendored
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._ = require('lodash');
|
||||||
window.Popper = require('popper.js').default;
|
window.Popper = require('popper.js').default;
|
||||||
|
import swal from 'sweetalert';
|
||||||
/**
|
|
||||||
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
|
|
||||||
* for JavaScript based Bootstrap features such as modals and tabs. This
|
|
||||||
* code may be modified to fit the specific needs of your application.
|
|
||||||
*/
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.pixelfed = {};
|
window.pixelfed = {};
|
||||||
window.$ = window.jQuery = require('jquery');
|
window.$ = window.jQuery = require('jquery');
|
||||||
|
@ -16,6 +9,7 @@ try {
|
||||||
window.filesize = require('filesize');
|
window.filesize = require('filesize');
|
||||||
window.typeahead = require('./lib/typeahead');
|
window.typeahead = require('./lib/typeahead');
|
||||||
window.Bloodhound = require('./lib/bloodhound');
|
window.Bloodhound = require('./lib/bloodhound');
|
||||||
|
window.Vue = require('vue');
|
||||||
|
|
||||||
require('./components/localstorage');
|
require('./components/localstorage');
|
||||||
require('./components/likebutton');
|
require('./components/likebutton');
|
||||||
|
@ -23,45 +17,21 @@ try {
|
||||||
require('./components/searchform');
|
require('./components/searchform');
|
||||||
require('./components/bookmarkform');
|
require('./components/bookmarkform');
|
||||||
require('./components/statusform');
|
require('./components/statusform');
|
||||||
|
|
||||||
|
Vue.component(
|
||||||
|
'follow-suggestions',
|
||||||
|
require('./components/FollowSuggestions.vue')
|
||||||
|
);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
/**
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
|
||||||
* to our Laravel back-end. This library automatically handles sending the
|
|
||||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.axios = require('axios');
|
window.axios = require('axios');
|
||||||
|
|
||||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
||||||
/**
|
|
||||||
* Next we will register the CSRF Token as a common header with Axios so that
|
|
||||||
* all outgoing HTTP requests automatically have it attached. This is just
|
|
||||||
* a simple convenience so we don't have to attach every token manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
let token = document.head.querySelector('meta[name="csrf-token"]');
|
let token = document.head.querySelector('meta[name="csrf-token"]');
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
|
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
|
||||||
} else {
|
} else {
|
||||||
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
|
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Echo exposes an expressive API for subscribing to channels and listening
|
|
||||||
* for events that are broadcast by Laravel. Echo and event broadcasting
|
|
||||||
* allows your team to easily build robust real-time web applications.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// import Echo from 'laravel-echo'
|
|
||||||
|
|
||||||
// window.Pusher = require('pusher-js');
|
|
||||||
|
|
||||||
// window.Echo = new Echo({
|
|
||||||
// broadcaster: 'pusher',
|
|
||||||
// key: process.env.MIX_PUSHER_APP_KEY,
|
|
||||||
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
|
||||||
// encrypted: true
|
|
||||||
// });
|
|
||||||
|
|
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() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
$('.status-comment-focus').on('click', function(el) {
|
$('.status-card > .card-footer').each(function() {
|
||||||
|
$(this).addClass('d-none');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.status-comment-focus', function(el) {
|
||||||
|
var form = $(this).parents().eq(2).find('.card-footer');
|
||||||
|
form.removeClass('d-none');
|
||||||
var el = $(this).parents().eq(2).find('input[name="comment"]');
|
var el = $(this).parents().eq(2).find('input[name="comment"]');
|
||||||
el.focus();
|
el.focus();
|
||||||
});
|
});
|
||||||
|
@ -31,7 +37,7 @@ $(document).ready(function() {
|
||||||
|
|
||||||
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ reply + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
|
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ reply + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
|
||||||
|
|
||||||
comments.prepend(comment);
|
comments.append(comment);
|
||||||
|
|
||||||
commentform.val('');
|
commentform.val('');
|
||||||
commentform.blur();
|
commentform.blur();
|
||||||
|
@ -41,7 +47,5 @@ $(document).ready(function() {
|
||||||
.catch(function (res) {
|
.catch(function (res) {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
30
resources/assets/js/components/likebutton.js
vendored
30
resources/assets/js/components/likebutton.js
vendored
|
@ -1,18 +1,17 @@
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
if(!ls.get('likes')) {
|
pixelfed.fetchLikes = () => {
|
||||||
axios.get('/api/v1/likes')
|
axios.get('/api/v1/likes')
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
ls.set('likes', res.data);
|
ls.set('likes', res.data);
|
||||||
console.log(res);
|
})
|
||||||
})
|
.catch(function (res) {
|
||||||
.catch(function (res) {
|
ls.set('likes', []);
|
||||||
ls.set('likes', []);
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pixelfed.hydrateLikes = function() {
|
pixelfed.hydrateLikes = () => {
|
||||||
var likes = ls.get('likes');
|
var likes = ls.get('likes');
|
||||||
$('.like-form').each(function(i, el) {
|
$('.like-form').each(function(i, el) {
|
||||||
var el = $(el);
|
var el = $(el);
|
||||||
|
@ -20,11 +19,14 @@ $(document).ready(function() {
|
||||||
var heart = el.find('.status-heart');
|
var heart = el.find('.status-heart');
|
||||||
|
|
||||||
if(likes.indexOf(id) != -1) {
|
if(likes.indexOf(id) != -1) {
|
||||||
heart.removeClass('far fa-heart').addClass('fas fa-heart');
|
heart.removeClass('text-dark').addClass('text-primary');
|
||||||
|
} else {
|
||||||
|
heart.removeClass('text-primary').addClass('text-dark');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pixelfed.fetchLikes();
|
||||||
pixelfed.hydrateLikes();
|
pixelfed.hydrateLikes();
|
||||||
|
|
||||||
$(document).on('submit', '.like-form', function(e) {
|
$(document).on('submit', '.like-form', function(e) {
|
||||||
|
@ -33,6 +35,8 @@ $(document).ready(function() {
|
||||||
var id = el.data('id');
|
var id = el.data('id');
|
||||||
axios.post('/i/like', {item: id})
|
axios.post('/i/like', {item: id})
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
|
pixelfed.fetchLikes();
|
||||||
|
pixelfed.hydrateLikes();
|
||||||
var likes = ls.get('likes');
|
var likes = ls.get('likes');
|
||||||
var action = false;
|
var action = false;
|
||||||
var counter = el.parents().eq(1).find('.like-count');
|
var counter = el.parents().eq(1).find('.like-count');
|
||||||
|
@ -40,14 +44,14 @@ $(document).ready(function() {
|
||||||
var heart = el.find('.status-heart');
|
var heart = el.find('.status-heart');
|
||||||
|
|
||||||
if(likes.indexOf(id) > -1) {
|
if(likes.indexOf(id) > -1) {
|
||||||
heart.removeClass('fas fa-heart').addClass('far fa-heart');
|
heart.removeClass('text-primary').addClass('text-dark');
|
||||||
likes = likes.filter(function(item) {
|
likes = likes.filter(function(item) {
|
||||||
return item !== id
|
return item !== id
|
||||||
});
|
});
|
||||||
counter.text(count);
|
counter.text(count);
|
||||||
action = 'unlike';
|
action = 'unlike';
|
||||||
} else {
|
} else {
|
||||||
heart.removeClass('far fa-heart').addClass('fas fa-heart');
|
heart.removeClass('text-dark').addClass('text-primary');
|
||||||
likes.push(id);
|
likes.push(id);
|
||||||
counter.text(count);
|
counter.text(count);
|
||||||
action = 'like';
|
action = 'like';
|
||||||
|
|
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() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
$('#statusForm .btn-filter-select').on('click', function(e) {
|
|
||||||
let el = $(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
pixelfed.create = {};
|
pixelfed.create = {};
|
||||||
pixelfed.filters = {};
|
pixelfed.filters = {};
|
||||||
pixelfed.create.hasGeneratedSelect = false;
|
pixelfed.create.hasGeneratedSelect = false;
|
||||||
|
@ -78,7 +74,7 @@ $(document).ready(function() {
|
||||||
pixelfed.create.hasGeneratedSelect = true;
|
pixelfed.create.hasGeneratedSelect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#fileInput').on('change', function() {
|
$(document).on('change', '#fileInput', function() {
|
||||||
previewImage(this);
|
previewImage(this);
|
||||||
$('#statusForm .form-filters.d-none').removeClass('d-none');
|
$('#statusForm .form-filters.d-none').removeClass('d-none');
|
||||||
$('#statusForm .form-preview.d-none').removeClass('d-none');
|
$('#statusForm .form-preview.d-none').removeClass('d-none');
|
||||||
|
@ -88,23 +84,43 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#filterSelectDropdown').on('change', function() {
|
$(document).on('change', '#filterSelectDropdown', function() {
|
||||||
let el = $(this);
|
let el = $(this);
|
||||||
let filter = el.val();
|
let filter = el.val();
|
||||||
let oldFilter = pixelfed.create.currentFilterClass;
|
let oldFilter = pixelfed.create.currentFilterClass;
|
||||||
if(filter == 'none') {
|
if(filter == 'none') {
|
||||||
$('.filterContainer').removeClass(oldFilter);
|
$('input[name=filter_class]').val('');
|
||||||
pixelfed.create.currentFilterClass = false;
|
$('input[name=filter_name]').val('');
|
||||||
pixelfed.create.currentFilterName = 'None';
|
$('.filterContainer').removeClass(oldFilter);
|
||||||
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
|
pixelfed.create.currentFilterClass = false;
|
||||||
return;
|
pixelfed.create.currentFilterName = 'None';
|
||||||
|
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$('.filterContainer').removeClass(oldFilter).addClass(filter);
|
||||||
|
pixelfed.create.currentFilterClass = filter;
|
||||||
|
pixelfed.create.currentFilterName = el.find(':selected').text();
|
||||||
|
$('.form-group.form-preview .form-text').text('Current Filter: ' + pixelfed.create.currentFilterName);
|
||||||
|
$('input[name=filter_class]').val(pixelfed.create.currentFilterClass);
|
||||||
|
$('input[name=filter_name]').val(pixelfed.create.currentFilterName);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
$('.filterContainer').removeClass(oldFilter).addClass(filter);
|
|
||||||
pixelfed.create.currentFilterClass = filter;
|
|
||||||
pixelfed.create.currentFilterName = el.find(':selected').text();
|
|
||||||
$('.form-group.form-preview .form-text').text('Current Filter: ' + pixelfed.create.currentFilterName);
|
|
||||||
$('input[name=filter_class]').val(pixelfed.create.currentFilterClass);
|
|
||||||
$('input[name=filter_name]').val(pixelfed.create.currentFilterName);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('keyup keydown', '#statusForm textarea[name=caption]', function() {
|
||||||
|
const el = $(this);
|
||||||
|
const len = el.val().length;
|
||||||
|
const limit = el.data('limit');
|
||||||
|
if(len > limit) {
|
||||||
|
const diff = limit - len;
|
||||||
|
$('#statusForm .caption-counter').text(diff).addClass('text-danger');
|
||||||
|
} else {
|
||||||
|
$('#statusForm .caption-counter').text(len).removeClass('text-danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('focus', '#statusForm textarea[name=caption]', function() {
|
||||||
|
const el = $(this);
|
||||||
|
el.attr('rows', '3');
|
||||||
|
});
|
||||||
});
|
});
|
66
resources/assets/js/lib/bloodhound.js
vendored
Normal file → Executable file
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
|
* https://github.com/twitter/typeahead.js
|
||||||
* Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
|
* Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(root, factory) {
|
(function(root, factory) {
|
||||||
if (typeof define === "function" && define.amd) {
|
if (typeof define === "function" && define.amd) {
|
||||||
define("bloodhound", [ "jquery" ], function(a0) {
|
define([ "jquery" ], function(a0) {
|
||||||
return root["Bloodhound"] = factory(a0);
|
return root["Bloodhound"] = factory(a0);
|
||||||
});
|
});
|
||||||
} else if (typeof exports === "object") {
|
} else if (typeof exports === "object") {
|
||||||
module.exports = factory(require("jquery"));
|
module.exports = factory(require("jquery"));
|
||||||
} else {
|
} else {
|
||||||
root["Bloodhound"] = factory(jQuery);
|
root["Bloodhound"] = factory(root["jQuery"]);
|
||||||
}
|
}
|
||||||
})(this, function($) {
|
})(this, function($) {
|
||||||
var _ = function() {
|
var _ = function() {
|
||||||
|
@ -148,18 +148,27 @@
|
||||||
stringify: function(val) {
|
stringify: function(val) {
|
||||||
return _.isString(val) ? val : JSON.stringify(val);
|
return _.isString(val) ? val : JSON.stringify(val);
|
||||||
},
|
},
|
||||||
|
guid: function() {
|
||||||
|
function _p8(s) {
|
||||||
|
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
|
||||||
|
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
|
||||||
|
}
|
||||||
|
return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
|
||||||
|
},
|
||||||
noop: function() {}
|
noop: function() {}
|
||||||
};
|
};
|
||||||
}();
|
}();
|
||||||
var VERSION = "0.11.1";
|
var VERSION = "1.2.0";
|
||||||
var tokenizers = function() {
|
var tokenizers = function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
return {
|
return {
|
||||||
nonword: nonword,
|
nonword: nonword,
|
||||||
whitespace: whitespace,
|
whitespace: whitespace,
|
||||||
|
ngram: ngram,
|
||||||
obj: {
|
obj: {
|
||||||
nonword: getObjTokenizer(nonword),
|
nonword: getObjTokenizer(nonword),
|
||||||
whitespace: getObjTokenizer(whitespace)
|
whitespace: getObjTokenizer(whitespace),
|
||||||
|
ngram: getObjTokenizer(ngram)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function whitespace(str) {
|
function whitespace(str) {
|
||||||
|
@ -170,6 +179,19 @@
|
||||||
str = _.toStr(str);
|
str = _.toStr(str);
|
||||||
return str ? str.split(/\W+/) : [];
|
return str ? str.split(/\W+/) : [];
|
||||||
}
|
}
|
||||||
|
function ngram(str) {
|
||||||
|
str = _.toStr(str);
|
||||||
|
var tokens = [], word = "";
|
||||||
|
_.each(str.split(""), function(char) {
|
||||||
|
if (char.match(/\s+/)) {
|
||||||
|
word = "";
|
||||||
|
} else {
|
||||||
|
tokens.push(word + char);
|
||||||
|
word += char;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
function getObjTokenizer(tokenizer) {
|
function getObjTokenizer(tokenizer) {
|
||||||
return function setKey(keys) {
|
return function setKey(keys) {
|
||||||
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
|
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
|
||||||
|
@ -341,9 +363,10 @@
|
||||||
}();
|
}();
|
||||||
var Transport = function() {
|
var Transport = function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
|
var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10);
|
||||||
function Transport(o) {
|
function Transport(o) {
|
||||||
o = o || {};
|
o = o || {};
|
||||||
|
this.maxPendingRequests = o.maxPendingRequests || 6;
|
||||||
this.cancelled = false;
|
this.cancelled = false;
|
||||||
this.lastReq = null;
|
this.lastReq = null;
|
||||||
this._send = o.transport;
|
this._send = o.transport;
|
||||||
|
@ -351,7 +374,7 @@
|
||||||
this._cache = o.cache === false ? new LruCache(0) : sharedCache;
|
this._cache = o.cache === false ? new LruCache(0) : sharedCache;
|
||||||
}
|
}
|
||||||
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
|
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
|
||||||
maxPendingRequests = num;
|
this.maxPendingRequests = num;
|
||||||
};
|
};
|
||||||
Transport.resetCache = function resetCache() {
|
Transport.resetCache = function resetCache() {
|
||||||
sharedCache.reset();
|
sharedCache.reset();
|
||||||
|
@ -369,7 +392,7 @@
|
||||||
}
|
}
|
||||||
if (jqXhr = pendingRequests[fingerprint]) {
|
if (jqXhr = pendingRequests[fingerprint]) {
|
||||||
jqXhr.done(done).fail(fail);
|
jqXhr.done(done).fail(fail);
|
||||||
} else if (pendingRequestsCount < maxPendingRequests) {
|
} else if (pendingRequestsCount < this.maxPendingRequests) {
|
||||||
pendingRequestsCount++;
|
pendingRequestsCount++;
|
||||||
pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
|
pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
|
||||||
} else {
|
} else {
|
||||||
|
@ -423,6 +446,7 @@
|
||||||
this.identify = o.identify || _.stringify;
|
this.identify = o.identify || _.stringify;
|
||||||
this.datumTokenizer = o.datumTokenizer;
|
this.datumTokenizer = o.datumTokenizer;
|
||||||
this.queryTokenizer = o.queryTokenizer;
|
this.queryTokenizer = o.queryTokenizer;
|
||||||
|
this.matchAnyQueryToken = o.matchAnyQueryToken;
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
_.mixin(SearchIndex.prototype, {
|
_.mixin(SearchIndex.prototype, {
|
||||||
|
@ -459,7 +483,7 @@
|
||||||
tokens = normalizeTokens(this.queryTokenizer(query));
|
tokens = normalizeTokens(this.queryTokenizer(query));
|
||||||
_.each(tokens, function(token) {
|
_.each(tokens, function(token) {
|
||||||
var node, chars, ch, ids;
|
var node, chars, ch, ids;
|
||||||
if (matches && matches.length === 0) {
|
if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
node = that.trie;
|
node = that.trie;
|
||||||
|
@ -471,8 +495,10 @@
|
||||||
ids = node[IDS].slice(0);
|
ids = node[IDS].slice(0);
|
||||||
matches = matches ? getIntersection(matches, ids) : ids;
|
matches = matches ? getIntersection(matches, ids) : ids;
|
||||||
} else {
|
} else {
|
||||||
matches = [];
|
if (!that.matchAnyQueryToken) {
|
||||||
return false;
|
matches = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return matches ? _.map(unique(matches), function(id) {
|
return matches ? _.map(unique(matches), function(id) {
|
||||||
|
@ -614,10 +640,12 @@
|
||||||
this.url = o.url;
|
this.url = o.url;
|
||||||
this.prepare = o.prepare;
|
this.prepare = o.prepare;
|
||||||
this.transform = o.transform;
|
this.transform = o.transform;
|
||||||
|
this.indexResponse = o.indexResponse;
|
||||||
this.transport = new Transport({
|
this.transport = new Transport({
|
||||||
cache: o.cache,
|
cache: o.cache,
|
||||||
limiter: o.limiter,
|
limiter: o.limiter,
|
||||||
transport: o.transport
|
transport: o.transport,
|
||||||
|
maxPendingRequests: o.maxPendingRequests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_.mixin(Remote.prototype, {
|
_.mixin(Remote.prototype, {
|
||||||
|
@ -655,7 +683,9 @@
|
||||||
identify: _.stringify,
|
identify: _.stringify,
|
||||||
datumTokenizer: null,
|
datumTokenizer: null,
|
||||||
queryTokenizer: null,
|
queryTokenizer: null,
|
||||||
|
matchAnyQueryToken: false,
|
||||||
sufficient: 5,
|
sufficient: 5,
|
||||||
|
indexRemote: false,
|
||||||
sorter: null,
|
sorter: null,
|
||||||
local: [],
|
local: [],
|
||||||
prefetch: null,
|
prefetch: null,
|
||||||
|
@ -744,7 +774,7 @@
|
||||||
} else if (o.wildcard) {
|
} else if (o.wildcard) {
|
||||||
prepare = prepareByWildcard;
|
prepare = prepareByWildcard;
|
||||||
} else {
|
} else {
|
||||||
prepare = idenityPrepare;
|
prepare = identityPrepare;
|
||||||
}
|
}
|
||||||
return prepare;
|
return prepare;
|
||||||
function prepareByReplace(query, settings) {
|
function prepareByReplace(query, settings) {
|
||||||
|
@ -755,7 +785,7 @@
|
||||||
settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
|
settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
function idenityPrepare(query, settings) {
|
function identityPrepare(query, settings) {
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -806,6 +836,7 @@
|
||||||
this.sorter = o.sorter;
|
this.sorter = o.sorter;
|
||||||
this.identify = o.identify;
|
this.identify = o.identify;
|
||||||
this.sufficient = o.sufficient;
|
this.sufficient = o.sufficient;
|
||||||
|
this.indexRemote = o.indexRemote;
|
||||||
this.local = o.local;
|
this.local = o.local;
|
||||||
this.remote = o.remote ? new Remote(o.remote) : null;
|
this.remote = o.remote ? new Remote(o.remote) : null;
|
||||||
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
|
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
|
||||||
|
@ -875,6 +906,8 @@
|
||||||
},
|
},
|
||||||
search: function search(query, sync, async) {
|
search: function search(query, sync, async) {
|
||||||
var that = this, local;
|
var that = this, local;
|
||||||
|
sync = sync || _.noop;
|
||||||
|
async = async || _.noop;
|
||||||
local = this.sorter(this.index.search(query));
|
local = this.sorter(this.index.search(query));
|
||||||
sync(this.remote ? local.slice() : local);
|
sync(this.remote ? local.slice() : local);
|
||||||
if (this.remote && local.length < this.sufficient) {
|
if (this.remote && local.length < this.sufficient) {
|
||||||
|
@ -890,7 +923,8 @@
|
||||||
return that.identify(r) === that.identify(l);
|
return that.identify(r) === that.identify(l);
|
||||||
}) && nonDuplicates.push(r);
|
}) && nonDuplicates.push(r);
|
||||||
});
|
});
|
||||||
async && async(nonDuplicates);
|
that.indexRemote && that.add(nonDuplicates);
|
||||||
|
async(nonDuplicates);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
all: function all() {
|
all: function all() {
|
||||||
|
|
228
resources/assets/js/lib/typeahead.js
vendored
Normal file → Executable file
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
|
* https://github.com/twitter/typeahead.js
|
||||||
* Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
|
* Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(root, factory) {
|
(function(root, factory) {
|
||||||
if (typeof define === "function" && define.amd) {
|
if (typeof define === "function" && define.amd) {
|
||||||
define("typeahead.js", [ "jquery" ], function(a0) {
|
define([ "jquery" ], function(a0) {
|
||||||
return factory(a0);
|
return factory(a0);
|
||||||
});
|
});
|
||||||
} else if (typeof exports === "object") {
|
} else if (typeof exports === "object") {
|
||||||
module.exports = factory(require("jquery"));
|
module.exports = factory(require("jquery"));
|
||||||
} else {
|
} else {
|
||||||
factory(jQuery);
|
factory(root["jQuery"]);
|
||||||
}
|
}
|
||||||
})(this, function($) {
|
})(this, function($) {
|
||||||
var _ = function() {
|
var _ = function() {
|
||||||
|
@ -148,6 +148,13 @@
|
||||||
stringify: function(val) {
|
stringify: function(val) {
|
||||||
return _.isString(val) ? val : JSON.stringify(val);
|
return _.isString(val) ? val : JSON.stringify(val);
|
||||||
},
|
},
|
||||||
|
guid: function() {
|
||||||
|
function _p8(s) {
|
||||||
|
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
|
||||||
|
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
|
||||||
|
}
|
||||||
|
return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
|
||||||
|
},
|
||||||
noop: function() {}
|
noop: function() {}
|
||||||
};
|
};
|
||||||
}();
|
}();
|
||||||
|
@ -189,7 +196,7 @@
|
||||||
function buildHtml(c) {
|
function buildHtml(c) {
|
||||||
return {
|
return {
|
||||||
wrapper: '<span class="' + c.wrapper + '"></span>',
|
wrapper: '<span class="' + c.wrapper + '"></span>',
|
||||||
menu: '<div class="' + c.menu + '"></div>'
|
menu: '<div role="listbox" class="' + c.menu + '"></div>'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function buildSelectors(classes) {
|
function buildSelectors(classes) {
|
||||||
|
@ -264,10 +271,8 @@
|
||||||
}
|
}
|
||||||
_.mixin(EventBus.prototype, {
|
_.mixin(EventBus.prototype, {
|
||||||
_trigger: function(type, args) {
|
_trigger: function(type, args) {
|
||||||
var $e;
|
var $e = $.Event(namespace + type);
|
||||||
$e = $.Event(namespace + type);
|
this.$el.trigger.call(this.$el, $e, args || []);
|
||||||
(args = args || []).unshift($e);
|
|
||||||
this.$el.trigger.apply(this.$el, args);
|
|
||||||
return $e;
|
return $e;
|
||||||
},
|
},
|
||||||
before: function(type) {
|
before: function(type) {
|
||||||
|
@ -384,7 +389,36 @@
|
||||||
tagName: "strong",
|
tagName: "strong",
|
||||||
className: null,
|
className: null,
|
||||||
wordsOnly: false,
|
wordsOnly: false,
|
||||||
caseSensitive: false
|
caseSensitive: false,
|
||||||
|
diacriticInsensitive: false
|
||||||
|
};
|
||||||
|
var accented = {
|
||||||
|
A: "[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ℀℁℻⒜Ⓐⓐ㍱-㍴㎀-㎄㎈㎉㎩-㎯㏂㏊㏟㏿Aa]",
|
||||||
|
B: "[BbᴮᵇḂ-ḇℬ⒝Ⓑⓑ㍴㎅-㎇㏃㏈㏔㏝Bb]",
|
||||||
|
C: "[CcÇçĆ-čᶜ℀ℂ℃℅℆ℭⅭⅽ⒞Ⓒⓒ㍶㎈㎉㎝㎠㎤㏄-㏇Cc]",
|
||||||
|
D: "[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟Ⓓⓓ㋏㍲㍷-㍹㎗㎭-㎯㏅㏈Dd]",
|
||||||
|
E: "[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑ℡ℯℰⅇ⒠Ⓔⓔ㉐㋍㋎Ee]",
|
||||||
|
F: "[FfᶠḞḟ℉ℱ℻⒡Ⓕⓕ㎊-㎌㎙ff-fflFf]",
|
||||||
|
G: "[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢Ⓖⓖ㋌㋍㎇㎍-㎏㎓㎬㏆㏉㏒㏿Gg]",
|
||||||
|
H: "[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣Ⓗⓗ㋌㍱㎐-㎔㏊㏋㏗Hh]",
|
||||||
|
I: "[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈⅠ-ⅣⅥ-ⅨⅪⅫⅰ-ⅳⅵ-ⅸⅺⅻ⒤Ⓘⓘ㍺㏌㏕fiffiIi]",
|
||||||
|
J: "[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]",
|
||||||
|
K: "[KkĶķǨǩᴷᵏḰ-ḵK⒦Ⓚⓚ㎄㎅㎉㎏㎑㎘㎞㎢㎦㎪㎸㎾㏀㏆㏍-㏏Kk]",
|
||||||
|
L: "[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓ℡Ⅼⅼ⒧Ⓛⓛ㋏㎈㎉㏐-㏓㏕㏖㏿flfflLl]",
|
||||||
|
M: "[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨Ⓜⓜ㍷-㍹㎃㎆㎎㎒㎖㎙-㎨㎫㎳㎷㎹㎽㎿㏁㏂㏎㏐㏔-㏖㏘㏙㏞㏟Mm]",
|
||||||
|
N: "[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩Ⓝⓝ㎁㎋㎚㎱㎵㎻㏌㏑Nn]",
|
||||||
|
O: "[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒ℅№ℴ⒪Ⓞⓞ㍵㏇㏒㏖Oo]",
|
||||||
|
P: "[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㉐㍱㍶㎀㎊㎩-㎬㎰㎴㎺㏋㏗-㏚Pp]",
|
||||||
|
Q: "[Qqℚ⒬Ⓠⓠ㏃Qq]",
|
||||||
|
R: "[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭Ⓡⓡ㋍㍴㎭-㎯㏚㏛Rr]",
|
||||||
|
S: "[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮Ⓢⓢ㎧㎨㎮-㎳㏛㏜stSs]",
|
||||||
|
T: "[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㉐㋏㎔㏏ſtstTt]",
|
||||||
|
U: "[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]",
|
||||||
|
V: "[VvᵛᵥṼ-ṿⅣ-Ⅷⅳ-ⅷ⒱Ⓥⓥⱽ㋎㍵㎴-㎹㏜㏞Vv]",
|
||||||
|
W: "[WwŴŵʷᵂẀ-ẉẘ⒲Ⓦⓦ㎺-㎿㏝Ww]",
|
||||||
|
X: "[XxˣẊ-ẍₓ℻Ⅸ-Ⅻⅸ-ⅻ⒳Ⓧⓧ㏓Xx]",
|
||||||
|
Y: "[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴Ⓨⓨ㏉Yy]",
|
||||||
|
Z: "[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵Ⓩⓩ㎐-㎔Zz]"
|
||||||
};
|
};
|
||||||
return function hightlight(o) {
|
return function hightlight(o) {
|
||||||
var regex;
|
var regex;
|
||||||
|
@ -393,7 +427,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
|
o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
|
||||||
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
|
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive);
|
||||||
traverse(o.node, hightlightTextNode);
|
traverse(o.node, hightlightTextNode);
|
||||||
function hightlightTextNode(textNode) {
|
function hightlightTextNode(textNode) {
|
||||||
var match, patternNode, wrapperNode;
|
var match, patternNode, wrapperNode;
|
||||||
|
@ -419,10 +453,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function getRegex(patterns, caseSensitive, wordsOnly) {
|
function accent_replacer(chr) {
|
||||||
|
return accented[chr.toUpperCase()] || chr;
|
||||||
|
}
|
||||||
|
function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) {
|
||||||
var escapedPatterns = [], regexStr;
|
var escapedPatterns = [], regexStr;
|
||||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
for (var i = 0, len = patterns.length; i < len; i++) {
|
||||||
escapedPatterns.push(_.escapeRegExChars(patterns[i]));
|
var escapedWord = _.escapeRegExChars(patterns[i]);
|
||||||
|
if (diacriticInsensitive) {
|
||||||
|
escapedWord = escapedWord.replace(/\S/g, accent_replacer);
|
||||||
|
}
|
||||||
|
escapedPatterns.push(escapedWord);
|
||||||
}
|
}
|
||||||
regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
|
regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
|
||||||
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
|
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
|
||||||
|
@ -448,6 +489,14 @@
|
||||||
www.mixin(this);
|
www.mixin(this);
|
||||||
this.$hint = $(o.hint);
|
this.$hint = $(o.hint);
|
||||||
this.$input = $(o.input);
|
this.$input = $(o.input);
|
||||||
|
this.$input.attr({
|
||||||
|
"aria-activedescendant": "",
|
||||||
|
"aria-owns": this.$input.attr("id") + "_listbox",
|
||||||
|
role: "combobox",
|
||||||
|
"aria-readonly": "true",
|
||||||
|
"aria-autocomplete": "list"
|
||||||
|
});
|
||||||
|
$(www.menu).attr("id", this.$input.attr("id") + "_listbox");
|
||||||
this.query = this.$input.val();
|
this.query = this.$input.val();
|
||||||
this.queryWhenFocused = this.hasFocus() ? this.query : null;
|
this.queryWhenFocused = this.hasFocus() ? this.query : null;
|
||||||
this.$overflowHelper = buildOverflowHelper(this.$input);
|
this.$overflowHelper = buildOverflowHelper(this.$input);
|
||||||
|
@ -455,6 +504,7 @@
|
||||||
if (this.$hint.length === 0) {
|
if (this.$hint.length === 0) {
|
||||||
this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
|
this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
|
||||||
}
|
}
|
||||||
|
this.onSync("cursorchange", this._updateDescendent);
|
||||||
}
|
}
|
||||||
Input.normalizeQuery = function(str) {
|
Input.normalizeQuery = function(str) {
|
||||||
return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
|
return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
|
||||||
|
@ -524,6 +574,9 @@
|
||||||
this.trigger("whitespaceChanged", this.query);
|
this.trigger("whitespaceChanged", this.query);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
_updateDescendent: function updateDescendent(event, id) {
|
||||||
|
this.$input.attr("aria-activedescendant", id);
|
||||||
|
},
|
||||||
bind: function() {
|
bind: function() {
|
||||||
var that = this, onBlur, onFocus, onKeydown, onInput;
|
var that = this, onBlur, onFocus, onKeydown, onInput;
|
||||||
onBlur = _.bind(this._onBlur, this);
|
onBlur = _.bind(this._onBlur, this);
|
||||||
|
@ -647,6 +700,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
var keys, nameGenerator;
|
var keys, nameGenerator;
|
||||||
keys = {
|
keys = {
|
||||||
|
dataset: "tt-selectable-dataset",
|
||||||
val: "tt-selectable-display",
|
val: "tt-selectable-display",
|
||||||
obj: "tt-selectable-object"
|
obj: "tt-selectable-object"
|
||||||
};
|
};
|
||||||
|
@ -666,19 +720,20 @@
|
||||||
}
|
}
|
||||||
www.mixin(this);
|
www.mixin(this);
|
||||||
this.highlight = !!o.highlight;
|
this.highlight = !!o.highlight;
|
||||||
this.name = o.name || nameGenerator();
|
this.name = _.toStr(o.name || nameGenerator());
|
||||||
this.limit = o.limit || 5;
|
this.limit = o.limit || 5;
|
||||||
this.displayFn = getDisplayFn(o.display || o.displayKey);
|
this.displayFn = getDisplayFn(o.display || o.displayKey);
|
||||||
this.templates = getTemplates(o.templates, this.displayFn);
|
this.templates = getTemplates(o.templates, this.displayFn);
|
||||||
this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
|
this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
|
||||||
this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
|
this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
|
||||||
this._resetLastSuggestion();
|
this._resetLastSuggestion();
|
||||||
this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
|
this.$el = $(o.node).attr("role", "presentation").addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
|
||||||
}
|
}
|
||||||
Dataset.extractData = function extractData(el) {
|
Dataset.extractData = function extractData(el) {
|
||||||
var $el = $(el);
|
var $el = $(el);
|
||||||
if ($el.data(keys.obj)) {
|
if ($el.data(keys.obj)) {
|
||||||
return {
|
return {
|
||||||
|
dataset: $el.data(keys.dataset) || "",
|
||||||
val: $el.data(keys.val) || "",
|
val: $el.data(keys.val) || "",
|
||||||
obj: $el.data(keys.obj) || null
|
obj: $el.data(keys.obj) || null
|
||||||
};
|
};
|
||||||
|
@ -697,7 +752,7 @@
|
||||||
} else {
|
} else {
|
||||||
this._empty();
|
this._empty();
|
||||||
}
|
}
|
||||||
this.trigger("rendered", this.name, suggestions, false);
|
this.trigger("rendered", suggestions, false, this.name);
|
||||||
},
|
},
|
||||||
_append: function append(query, suggestions) {
|
_append: function append(query, suggestions) {
|
||||||
suggestions = suggestions || [];
|
suggestions = suggestions || [];
|
||||||
|
@ -708,7 +763,7 @@
|
||||||
} else if (!this.$lastSuggestion.length && this.templates.notFound) {
|
} else if (!this.$lastSuggestion.length && this.templates.notFound) {
|
||||||
this._renderNotFound(query);
|
this._renderNotFound(query);
|
||||||
}
|
}
|
||||||
this.trigger("rendered", this.name, suggestions, true);
|
this.trigger("rendered", suggestions, true, this.name);
|
||||||
},
|
},
|
||||||
_renderSuggestions: function renderSuggestions(query, suggestions) {
|
_renderSuggestions: function renderSuggestions(query, suggestions) {
|
||||||
var $fragment;
|
var $fragment;
|
||||||
|
@ -749,7 +804,7 @@
|
||||||
_.each(suggestions, function getSuggestionNode(suggestion) {
|
_.each(suggestions, function getSuggestionNode(suggestion) {
|
||||||
var $el, context;
|
var $el, context;
|
||||||
context = that._injectQuery(query, suggestion);
|
context = that._injectQuery(query, suggestion);
|
||||||
$el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
|
$el = $(that.templates.suggestion(context)).data(keys.dataset, that.name).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
|
||||||
fragment.appendChild($el[0]);
|
fragment.appendChild($el[0]);
|
||||||
});
|
});
|
||||||
this.highlight && highlight({
|
this.highlight && highlight({
|
||||||
|
@ -787,7 +842,7 @@
|
||||||
this.cancel = function cancel() {
|
this.cancel = function cancel() {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
that.cancel = $.noop;
|
that.cancel = $.noop;
|
||||||
that.async && that.trigger("asyncCanceled", query);
|
that.async && that.trigger("asyncCanceled", query, that.name);
|
||||||
};
|
};
|
||||||
this.source(query, sync, async);
|
this.source(query, sync, async);
|
||||||
!syncCalled && sync([]);
|
!syncCalled && sync([]);
|
||||||
|
@ -800,16 +855,17 @@
|
||||||
rendered = suggestions.length;
|
rendered = suggestions.length;
|
||||||
that._overwrite(query, suggestions);
|
that._overwrite(query, suggestions);
|
||||||
if (rendered < that.limit && that.async) {
|
if (rendered < that.limit && that.async) {
|
||||||
that.trigger("asyncRequested", query);
|
that.trigger("asyncRequested", query, that.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function async(suggestions) {
|
function async(suggestions) {
|
||||||
suggestions = suggestions || [];
|
suggestions = suggestions || [];
|
||||||
if (!canceled && rendered < that.limit) {
|
if (!canceled && rendered < that.limit) {
|
||||||
that.cancel = $.noop;
|
that.cancel = $.noop;
|
||||||
rendered += suggestions.length;
|
var idx = Math.abs(rendered - that.limit);
|
||||||
that._append(query, suggestions.slice(0, that.limit - rendered));
|
rendered += idx;
|
||||||
that.async && that.trigger("asyncReceived", query);
|
that._append(query, suggestions.slice(0, idx));
|
||||||
|
that.async && that.trigger("asyncReceived", query, that.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -843,7 +899,7 @@
|
||||||
suggestion: templates.suggestion || suggestionTemplate
|
suggestion: templates.suggestion || suggestionTemplate
|
||||||
};
|
};
|
||||||
function suggestionTemplate(context) {
|
function suggestionTemplate(context) {
|
||||||
return $("<div>").text(displayFn(context));
|
return $('<div role="option">').attr("id", _.guid()).text(displayFn(context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function isValidName(str) {
|
function isValidName(str) {
|
||||||
|
@ -884,10 +940,11 @@
|
||||||
this.trigger.apply(this, arguments);
|
this.trigger.apply(this, arguments);
|
||||||
},
|
},
|
||||||
_allDatasetsEmpty: function allDatasetsEmpty() {
|
_allDatasetsEmpty: function allDatasetsEmpty() {
|
||||||
return _.every(this.datasets, isDatasetEmpty);
|
return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) {
|
||||||
function isDatasetEmpty(dataset) {
|
var isEmpty = dataset.isEmpty();
|
||||||
return dataset.isEmpty();
|
this.$node.attr("aria-expanded", !isEmpty);
|
||||||
}
|
return isEmpty;
|
||||||
|
}, this));
|
||||||
},
|
},
|
||||||
_getSelectables: function getSelectables() {
|
_getSelectables: function getSelectables() {
|
||||||
return this.$node.find(this.selectors.selectable);
|
return this.$node.find(this.selectors.selectable);
|
||||||
|
@ -912,6 +969,12 @@
|
||||||
var that = this, onSelectableClick;
|
var that = this, onSelectableClick;
|
||||||
onSelectableClick = _.bind(this._onSelectableClick, this);
|
onSelectableClick = _.bind(this._onSelectableClick, this);
|
||||||
this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
|
this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
|
||||||
|
this.$node.on("mouseover", this.selectors.selectable, function() {
|
||||||
|
that.setCursor($(this));
|
||||||
|
});
|
||||||
|
this.$node.on("mouseleave", function() {
|
||||||
|
that._removeCursor();
|
||||||
|
});
|
||||||
_.each(this.datasets, function(dataset) {
|
_.each(this.datasets, function(dataset) {
|
||||||
dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
|
dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
|
||||||
});
|
});
|
||||||
|
@ -921,9 +984,11 @@
|
||||||
return this.$node.hasClass(this.classes.open);
|
return this.$node.hasClass(this.classes.open);
|
||||||
},
|
},
|
||||||
open: function open() {
|
open: function open() {
|
||||||
|
this.$node.scrollTop(0);
|
||||||
this.$node.addClass(this.classes.open);
|
this.$node.addClass(this.classes.open);
|
||||||
},
|
},
|
||||||
close: function close() {
|
close: function close() {
|
||||||
|
this.$node.attr("aria-expanded", false);
|
||||||
this.$node.removeClass(this.classes.open);
|
this.$node.removeClass(this.classes.open);
|
||||||
this._removeCursor();
|
this._removeCursor();
|
||||||
},
|
},
|
||||||
|
@ -988,6 +1053,55 @@
|
||||||
});
|
});
|
||||||
return Menu;
|
return Menu;
|
||||||
}();
|
}();
|
||||||
|
var Status = function() {
|
||||||
|
"use strict";
|
||||||
|
function Status(options) {
|
||||||
|
this.$el = $("<span></span>", {
|
||||||
|
role: "status",
|
||||||
|
"aria-live": "polite"
|
||||||
|
}).css({
|
||||||
|
position: "absolute",
|
||||||
|
padding: "0",
|
||||||
|
border: "0",
|
||||||
|
height: "1px",
|
||||||
|
width: "1px",
|
||||||
|
"margin-bottom": "-1px",
|
||||||
|
"margin-right": "-1px",
|
||||||
|
overflow: "hidden",
|
||||||
|
clip: "rect(0 0 0 0)",
|
||||||
|
"white-space": "nowrap"
|
||||||
|
});
|
||||||
|
options.$input.after(this.$el);
|
||||||
|
_.each(options.menu.datasets, _.bind(function(dataset) {
|
||||||
|
if (dataset.onSync) {
|
||||||
|
dataset.onSync("rendered", _.bind(this.update, this));
|
||||||
|
dataset.onSync("cleared", _.bind(this.cleared, this));
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
}
|
||||||
|
_.mixin(Status.prototype, {
|
||||||
|
update: function update(event, suggestions) {
|
||||||
|
var length = suggestions.length;
|
||||||
|
var words;
|
||||||
|
if (length === 1) {
|
||||||
|
words = {
|
||||||
|
result: "result",
|
||||||
|
is: "is"
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
words = {
|
||||||
|
result: "results",
|
||||||
|
is: "are"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.$el.text(length + " " + words.result + " " + words.is + " available, use up and down arrow keys to navigate.");
|
||||||
|
},
|
||||||
|
cleared: function() {
|
||||||
|
this.$el.text("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Status;
|
||||||
|
}();
|
||||||
var DefaultMenu = function() {
|
var DefaultMenu = function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
var s = Menu.prototype;
|
var s = Menu.prototype;
|
||||||
|
@ -1052,6 +1166,7 @@
|
||||||
this.input = o.input;
|
this.input = o.input;
|
||||||
this.menu = o.menu;
|
this.menu = o.menu;
|
||||||
this.enabled = true;
|
this.enabled = true;
|
||||||
|
this.autoselect = !!o.autoselect;
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.input.hasFocus() && this.activate();
|
this.input.hasFocus() && this.activate();
|
||||||
this.dir = this.input.getLangDir();
|
this.dir = this.input.getLangDir();
|
||||||
|
@ -1098,8 +1213,12 @@
|
||||||
_onDatasetCleared: function onDatasetCleared() {
|
_onDatasetCleared: function onDatasetCleared() {
|
||||||
this._updateHint();
|
this._updateHint();
|
||||||
},
|
},
|
||||||
_onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
|
_onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) {
|
||||||
this._updateHint();
|
this._updateHint();
|
||||||
|
if (this.autoselect) {
|
||||||
|
var cursorClass = this.selectors.cursor.substr(1);
|
||||||
|
this.menu.$node.find(this.selectors.suggestion).first().addClass(cursorClass);
|
||||||
|
}
|
||||||
this.eventBus.trigger("render", suggestions, async, dataset);
|
this.eventBus.trigger("render", suggestions, async, dataset);
|
||||||
},
|
},
|
||||||
_onAsyncRequested: function onAsyncRequested(type, dataset, query) {
|
_onAsyncRequested: function onAsyncRequested(type, dataset, query) {
|
||||||
|
@ -1122,7 +1241,15 @@
|
||||||
_onEnterKeyed: function onEnterKeyed(type, $e) {
|
_onEnterKeyed: function onEnterKeyed(type, $e) {
|
||||||
var $selectable;
|
var $selectable;
|
||||||
if ($selectable = this.menu.getActiveSelectable()) {
|
if ($selectable = this.menu.getActiveSelectable()) {
|
||||||
this.select($selectable) && $e.preventDefault();
|
if (this.select($selectable)) {
|
||||||
|
$e.preventDefault();
|
||||||
|
$e.stopPropagation();
|
||||||
|
}
|
||||||
|
} else if (this.autoselect) {
|
||||||
|
if (this.select(this.menu.getTopSelectable())) {
|
||||||
|
$e.preventDefault();
|
||||||
|
$e.stopPropagation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_onTabKeyed: function onTabKeyed(type, $e) {
|
_onTabKeyed: function onTabKeyed(type, $e) {
|
||||||
|
@ -1144,12 +1271,12 @@
|
||||||
},
|
},
|
||||||
_onLeftKeyed: function onLeftKeyed() {
|
_onLeftKeyed: function onLeftKeyed() {
|
||||||
if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
|
if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
|
||||||
this.autocomplete(this.menu.getTopSelectable());
|
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_onRightKeyed: function onRightKeyed() {
|
_onRightKeyed: function onRightKeyed() {
|
||||||
if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
|
if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
|
||||||
this.autocomplete(this.menu.getTopSelectable());
|
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_onQueryChanged: function onQueryChanged(e, query) {
|
_onQueryChanged: function onQueryChanged(e, query) {
|
||||||
|
@ -1249,9 +1376,9 @@
|
||||||
},
|
},
|
||||||
select: function select($selectable) {
|
select: function select($selectable) {
|
||||||
var data = this.menu.getSelectableData($selectable);
|
var data = this.menu.getSelectableData($selectable);
|
||||||
if (data && !this.eventBus.before("select", data.obj)) {
|
if (data && !this.eventBus.before("select", data.obj, data.dataset)) {
|
||||||
this.input.setQuery(data.val, true);
|
this.input.setQuery(data.val, true);
|
||||||
this.eventBus.trigger("select", data.obj);
|
this.eventBus.trigger("select", data.obj, data.dataset);
|
||||||
this.close();
|
this.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1262,21 +1389,24 @@
|
||||||
query = this.input.getQuery();
|
query = this.input.getQuery();
|
||||||
data = this.menu.getSelectableData($selectable);
|
data = this.menu.getSelectableData($selectable);
|
||||||
isValid = data && query !== data.val;
|
isValid = data && query !== data.val;
|
||||||
if (isValid && !this.eventBus.before("autocomplete", data.obj)) {
|
if (isValid && !this.eventBus.before("autocomplete", data.obj, data.dataset)) {
|
||||||
this.input.setQuery(data.val);
|
this.input.setQuery(data.val);
|
||||||
this.eventBus.trigger("autocomplete", data.obj);
|
this.eventBus.trigger("autocomplete", data.obj, data.dataset);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
moveCursor: function moveCursor(delta) {
|
moveCursor: function moveCursor(delta) {
|
||||||
var query, $candidate, data, payload, cancelMove;
|
var query, $candidate, data, suggestion, datasetName, cancelMove, id;
|
||||||
query = this.input.getQuery();
|
query = this.input.getQuery();
|
||||||
$candidate = this.menu.selectableRelativeToCursor(delta);
|
$candidate = this.menu.selectableRelativeToCursor(delta);
|
||||||
data = this.menu.getSelectableData($candidate);
|
data = this.menu.getSelectableData($candidate);
|
||||||
payload = data ? data.obj : null;
|
suggestion = data ? data.obj : null;
|
||||||
|
datasetName = data ? data.dataset : null;
|
||||||
|
id = $candidate ? $candidate.attr("id") : null;
|
||||||
|
this.input.trigger("cursorchange", id);
|
||||||
cancelMove = this._minLengthMet() && this.menu.update(query);
|
cancelMove = this._minLengthMet() && this.menu.update(query);
|
||||||
if (!cancelMove && !this.eventBus.before("cursorchange", payload)) {
|
if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) {
|
||||||
this.menu.setCursor($candidate);
|
this.menu.setCursor($candidate);
|
||||||
if (data) {
|
if (data) {
|
||||||
this.input.setInputValue(data.val);
|
this.input.setInputValue(data.val);
|
||||||
|
@ -1284,7 +1414,7 @@
|
||||||
this.input.resetInputValue();
|
this.input.resetInputValue();
|
||||||
this._updateHint();
|
this._updateHint();
|
||||||
}
|
}
|
||||||
this.eventBus.trigger("cursorchange", payload);
|
this.eventBus.trigger("cursorchange", suggestion, datasetName);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -1322,7 +1452,7 @@
|
||||||
www = WWW(o.classNames);
|
www = WWW(o.classNames);
|
||||||
return this.each(attach);
|
return this.each(attach);
|
||||||
function attach() {
|
function attach() {
|
||||||
var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor;
|
var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, status, typeahead, MenuConstructor;
|
||||||
_.each(datasets, function(d) {
|
_.each(datasets, function(d) {
|
||||||
d.highlight = !!o.highlight;
|
d.highlight = !!o.highlight;
|
||||||
});
|
});
|
||||||
|
@ -1353,11 +1483,16 @@
|
||||||
node: $menu,
|
node: $menu,
|
||||||
datasets: datasets
|
datasets: datasets
|
||||||
}, www);
|
}, www);
|
||||||
|
status = new Status({
|
||||||
|
$input: $input,
|
||||||
|
menu: menu
|
||||||
|
});
|
||||||
typeahead = new Typeahead({
|
typeahead = new Typeahead({
|
||||||
input: input,
|
input: input,
|
||||||
menu: menu,
|
menu: menu,
|
||||||
eventBus: eventBus,
|
eventBus: eventBus,
|
||||||
minLength: o.minLength
|
minLength: o.minLength,
|
||||||
|
autoselect: o.autoselect
|
||||||
}, www);
|
}, www);
|
||||||
$input.data(keys.www, www);
|
$input.data(keys.www, www);
|
||||||
$input.data(keys.typeahead, typeahead);
|
$input.data(keys.typeahead, typeahead);
|
||||||
|
@ -1450,7 +1585,7 @@
|
||||||
return query;
|
return query;
|
||||||
} else {
|
} else {
|
||||||
ttEach(this, function(t) {
|
ttEach(this, function(t) {
|
||||||
t.setVal(newVal);
|
t.setVal(_.toStr(newVal));
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -1481,8 +1616,10 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function buildHintFromInput($input, www) {
|
function buildHintFromInput($input, www) {
|
||||||
return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({
|
return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({
|
||||||
autocomplete: "off",
|
readonly: true,
|
||||||
|
required: false
|
||||||
|
}).removeAttr("id name placeholder").removeClass("required").attr({
|
||||||
spellcheck: "false",
|
spellcheck: "false",
|
||||||
tabindex: -1
|
tabindex: -1
|
||||||
});
|
});
|
||||||
|
@ -1495,7 +1632,6 @@
|
||||||
style: $input.attr("style")
|
style: $input.attr("style")
|
||||||
});
|
});
|
||||||
$input.addClass(www.classes.input).attr({
|
$input.addClass(www.classes.input).attr({
|
||||||
autocomplete: "off",
|
|
||||||
spellcheck: false
|
spellcheck: false
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
|
41
resources/assets/js/timeline.js
vendored
41
resources/assets/js/timeline.js
vendored
|
@ -1,13 +1,54 @@
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('.pagination').hide();
|
$('.pagination').hide();
|
||||||
|
$('.container.timeline-container').removeClass('d-none');
|
||||||
let elem = document.querySelector('.timeline-feed');
|
let elem = document.querySelector('.timeline-feed');
|
||||||
|
pixelfed.fetchLikes();
|
||||||
|
|
||||||
let infScroll = new InfiniteScroll( elem, {
|
let infScroll = new InfiniteScroll( elem, {
|
||||||
path: '.pagination__next',
|
path: '.pagination__next',
|
||||||
append: '.timeline-feed',
|
append: '.timeline-feed',
|
||||||
status: '.page-load-status',
|
status: '.page-load-status',
|
||||||
history: false,
|
history: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
infScroll.on( 'append', function( response, path, items ) {
|
infScroll.on( 'append', function( response, path, items ) {
|
||||||
pixelfed.hydrateLikes();
|
pixelfed.hydrateLikes();
|
||||||
|
$('.status-card > .card-footer').each(function() {
|
||||||
|
var el = $(this);
|
||||||
|
if(!el.hasClass('d-none') && !el.find('input[name="comment"]').val()) {
|
||||||
|
$(this).addClass('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("DOMContentLoaded", function() {
|
||||||
|
|
||||||
|
var active = false;
|
||||||
|
|
||||||
|
var lazyLoad = function() {
|
||||||
|
if (active === false) {
|
||||||
|
active = true;
|
||||||
|
|
||||||
|
var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
|
||||||
|
lazyImages.forEach(function(lazyImage) {
|
||||||
|
if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
|
||||||
|
lazyImage.src = lazyImage.dataset.src;
|
||||||
|
lazyImage.srcset = lazyImage.dataset.srcset;
|
||||||
|
lazyImage.classList.remove("lazy");
|
||||||
|
|
||||||
|
lazyImages = lazyImages.filter(function(image) {
|
||||||
|
return image !== lazyImage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
document.addEventListener("scroll", lazyLoad);
|
||||||
|
window.addEventListener("resize", lazyLoad);
|
||||||
|
window.addEventListener("orientationchange", lazyLoad);
|
||||||
});
|
});
|
||||||
|
|
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")) {
|
@media (max-width: map-get($grid-breakpoints, "md")) {
|
||||||
.border-md-left-0 {
|
.border-md-left-0 {
|
||||||
border-left:0!important
|
border-left:0!important
|
||||||
|
@ -266,3 +261,36 @@ body, button, input, textarea {
|
||||||
animation-name: fadeInDown;
|
animation-name: fadeInDown;
|
||||||
animation-duration: 0.5s;
|
animation-duration: 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow {
|
||||||
|
box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left-primary {
|
||||||
|
border-left: 3px solid $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav .nav-item.active .nav-link {
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary::-webkit-details-marker {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-animated > summary {
|
||||||
|
display: block;
|
||||||
|
background-color: #ECF0F1;
|
||||||
|
padding-top: 50px;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-animated[open] > summary {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
|
|
@ -9,5 +9,6 @@ return [
|
||||||
'settings' => 'Einstellungen',
|
'settings' => 'Einstellungen',
|
||||||
'admin' => 'Administration',
|
'admin' => 'Administration',
|
||||||
'logout' => 'Abmelden',
|
'logout' => 'Abmelden',
|
||||||
|
'directMessages' => 'Privatnachrichten',
|
||||||
|
|
||||||
];
|
];
|
|
@ -5,4 +5,4 @@ return [
|
||||||
'emptyFollowers' => 'Diesem Benutzer folgt noch niemand!',
|
'emptyFollowers' => 'Diesem Benutzer folgt noch niemand!',
|
||||||
'emptyFollowing' => 'Dieser Benutzer folgt noch niemanden!',
|
'emptyFollowing' => 'Dieser Benutzer folgt noch niemanden!',
|
||||||
'savedWarning' => 'Nur du kannst sehen was du gespeichert hast',
|
'savedWarning' => 'Nur du kannst sehen was du gespeichert hast',
|
||||||
];
|
];
|
||||||
|
|
|
@ -9,5 +9,6 @@ return [
|
||||||
'settings' => 'Settings',
|
'settings' => 'Settings',
|
||||||
'admin' => 'Admin',
|
'admin' => 'Admin',
|
||||||
'logout' => 'Logout',
|
'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 [
|
return [
|
||||||
'likedPhoto' => 'a aimé votre photo.',
|
'likedPhoto' => 'a aimé votre photo.',
|
||||||
'startedFollowingYou' => 'a commencé à vous suivre.',
|
'startedFollowingYou' => 'a commencé à vous suivre.',
|
||||||
|
'commented' => 'commenté sur votre post.',
|
||||||
|
'mentionedYou' => 'vous à mentionné.'
|
||||||
];
|
];
|
||||||
|
|
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.',
|
'likedPhoto' => 'a aimat vòstra fòto.',
|
||||||
'startedFollowingYou' => 'a començat de vos seguir.',
|
'startedFollowingYou' => 'a començat de vos seguir.',
|
||||||
|
'commented' => 'a comentat vòstra publicacion.',
|
||||||
|
'mentionedYou' => 'vos a mencionat.'
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'emptyTimeline' => 'Aqueste utilizaire a pas encara de publicacion !',
|
'emptyTimeline' => 'Aqueste utilizaire a pas encara de publicacion !',
|
||||||
|
'emptyFollowers' => 'Aqueste utilizaire a pas encara pas seguidors !',
|
||||||
|
'emptyFollowing' => 'Aqueste utilizaire sèc degun pel moment !',
|
||||||
|
'savedWarning' => 'Solament vos vesètz çò que salvagardatz',
|
||||||
];
|
];
|
||||||
|
|
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.',
|
'likedPhoto' => 'polubił Twoje zdjęcie.',
|
||||||
'startedFollowingYou' => 'zaczął Cię obserwować.',
|
'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')
|
@section('content')
|
||||||
<div class="container notification-page" style="min-height: 60vh;">
|
<div class="container notification-page" style="min-height: 60vh;">
|
||||||
<div class="col-12 col-md-8 offset-md-2">
|
<div class="col-12 col-md-8 offset-md-2">
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<ul class="nav nav-tabs d-flex text-center">
|
||||||
|
{{--
|
||||||
|
<li class="nav-item flex-fill">
|
||||||
|
<a class="nav-link font-weight-bold text-uppercase" href="{{route('notifications.following')}}">Following</a>
|
||||||
|
</li>
|
||||||
|
--}}
|
||||||
|
<li class="nav-item flex-fill">
|
||||||
|
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">My Notifications</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<div class="dropdown text-right mt-2">
|
||||||
|
<a class="btn btn-link btn-sm dropdown-toggle font-weight-bold text-dark" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
Filter
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||||
|
<a href="?a=comment" class="dropdown-item font-weight-bold" title="Commented on your post">
|
||||||
|
Comments only
|
||||||
|
</a>
|
||||||
|
<a href="?a=follow" class="dropdown-item font-weight-bold" title="Followed you">
|
||||||
|
New Followers only
|
||||||
|
</a>
|
||||||
|
<a href="?a=mention" class="dropdown-item font-weight-bold" title="Mentioned you">
|
||||||
|
Mentions only
|
||||||
|
</a>
|
||||||
|
<a href="{{route('notifications')}}" class="dropdown-item font-weight-bold text-dark">
|
||||||
|
View All
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
|
||||||
@if($notifications->count() > 0)
|
@if($notifications->count() > 0)
|
||||||
@foreach($notifications as $notification)
|
@foreach($notifications as $notification)
|
||||||
<li class="list-group-item notification">
|
<li class="list-group-item notification border-0">
|
||||||
@switch($notification->action)
|
@switch($notification->action)
|
||||||
|
|
||||||
@case('like')
|
@case('like')
|
||||||
<span class="notification-icon pr-3">
|
<span class="notification-icon pr-3">
|
||||||
<img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
|
<img src="{{optional($notification->actor, function($actor) {
|
||||||
|
return $actor->avatarUrl(); }) }}" width="32px" class="rounded-circle">
|
||||||
</span>
|
</span>
|
||||||
<span class="notification-text">
|
<span class="notification-text">
|
||||||
{!! $notification->rendered !!}
|
{!! $notification->rendered !!}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue