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

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

View file

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

View file

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

10
app/AccountLog.php Normal file
View file

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

View file

@ -0,0 +1,36 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\{User, UserSetting};
class AuthLoginEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
public function handle(User $user)
{
if(empty($user->settings)) {
$settings = new UserSetting;
$settings->user_id = $user->id;
$settings->save();
}
}
}

View file

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

View file

@ -18,19 +18,47 @@ class AccountController extends Controller
public function notifications(Request $request)
{
$this->validate($request, [
'page' => 'nullable|min:1|max:3'
'page' => 'nullable|min:1|max:3',
'a' => 'nullable|alpha_dash',
]);
$profile = Auth::user()->profile;
$action = $request->input('a');
$timeago = Carbon::now()->subMonths(6);
$notifications = Notification::whereProfileId($profile->id)
->whereDate('created_at', '>', $timeago)
->orderBy('id','desc')
->take(30)
->simplePaginate();
if($action && in_array($action, ['comment', 'follow', 'mention'])) {
$notifications = Notification::whereProfileId($profile->id)
->whereAction($action)
->whereDate('created_at', '>', $timeago)
->orderBy('id','desc')
->simplePaginate(30);
} else {
$notifications = Notification::whereProfileId($profile->id)
->whereDate('created_at', '>', $timeago)
->orderBy('id','desc')
->simplePaginate(30);
}
return view('account.activity', compact('profile', 'notifications'));
}
public function followingActivity(Request $request)
{
$this->validate($request, [
'page' => 'nullable|min:1|max:3',
'a' => 'nullable|alpha_dash',
]);
$profile = Auth::user()->profile;
$action = $request->input('a');
$timeago = Carbon::now()->subMonths(1);
$following = $profile->following->pluck('id');
$notifications = Notification::whereIn('actor_id', $following)
->where('profile_id', '!=', $profile->id)
->whereDate('created_at', '>', $timeago)
->orderBy('notifications.id','desc')
->simplePaginate(30);
return view('account.following', compact('profile', 'notifications'));
}
public function verifyEmail(Request $request)
{
return view('account.verify_email');
@ -38,10 +66,19 @@ class AccountController extends Controller
public function sendVerifyEmail(Request $request)
{
if(EmailVerification::whereUserId(Auth::id())->count() !== 0) {
return redirect()->back()->with('status', 'A verification email has already been sent! Please check your email.');
$timeLimit = Carbon::now()->subDays(1)->toDateTimeString();
$recentAttempt = EmailVerification::whereUserId(Auth::id())
->where('created_at', '>', $timeLimit)->count();
$exists = EmailVerification::whereUserId(Auth::id())->count();
if($recentAttempt == 1 && $exists == 1) {
return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
} elseif ($recentAttempt == 0 && $exists !== 0) {
// Delete old verification and send new one.
EmailVerification::whereUserId(Auth::id())->delete();
}
$user = User::whereNull('email_verified_at')->find(Auth::id());
$utoken = hash('sha512', $user->id);
$rtoken = str_random(40);
@ -60,14 +97,15 @@ class AccountController extends Controller
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
{
$verify = EmailVerification::where(DB::raw('BINARY user_token'), $userToken)
->where(DB::raw('BINARY random_token'), $randomToken)
$verify = EmailVerification::where('user_token', $userToken)
->where('random_token', $randomToken)
->firstOrFail();
if(Auth::id() === $verify->user_id) {
$user = User::find(Auth::id());
$user->email_verified_at = Carbon::now();
$user->save();
return redirect('/timeline');
return redirect('/');
}
}
@ -95,4 +133,5 @@ class AccountController extends Controller
}
return $notifications;
}
}

View file

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

View file

@ -2,16 +2,13 @@
namespace App\Http\Controllers;
use Auth;
use App\Like;
use Auth, Cache;
use App\{Like, Status};
use Illuminate\Http\Request;
use App\Http\Controllers\Api\BaseApiController;
class ApiController extends Controller
class ApiController extends BaseApiController
{
public function __construct()
{
$this->middleware('auth');
}
public function hydrateLikes(Request $request)
{
@ -21,12 +18,18 @@ class ApiController extends Controller
]);
$profile = Auth::user()->profile;
$likes = Like::whereProfileId($profile->id)
$res = Cache::remember('api:like-ids:user:'.$profile->id, 1440, function() use ($profile) {
return Like::whereProfileId($profile->id)
->orderBy('id', 'desc')
->take(1000)
->pluck('status_id');
});
return response()->json($likes);
return response()->json($res);
}
public function loadMoreComments(Request $request)
{
return;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,19 +3,28 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{Profile, User};
use Auth;
use App\{AccountLog, Media, Profile, User};
use Auth, DB;
use App\Util\Lexer\PrettyNumber;
class SettingsController extends Controller
{
public function __construct()
{
return $this->middleware('auth');
$this->middleware('auth');
}
public function home()
{
return view('settings.home');
$id = Auth::user()->profile->id;
$storage = [];
$used = Media::whereProfileId($id)->sum('size');
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
$storage['used'] = $used;
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
return view('settings.home', compact('storage'));
}
public function homeUpdate(Request $request)
@ -89,6 +98,34 @@ class SettingsController extends Controller
return view('settings.avatar');
}
public function accessibility()
{
$settings = Auth::user()->settings;
return view('settings.accessibility', compact('settings'));
}
public function accessibilityStore(Request $request)
{
$settings = Auth::user()->settings;
$fields = [
'compose_media_descriptions',
'reduce_motion',
'optimize_screen_reader',
'high_contrast_mode',
'video_autoplay'
];
foreach($fields as $field) {
$form = $request->input($field);
if($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
$settings->save();
}
return redirect(route('settings.accessibility'))->with('status', 'Settings successfully updated!');
}
public function notifications()
{
return view('settings.notifications');
@ -96,12 +133,61 @@ class SettingsController extends Controller
public function privacy()
{
return view('settings.privacy');
$settings = Auth::user()->settings;
$is_private = Auth::user()->profile->is_private;
$settings['is_private'] = (bool) $is_private;
return view('settings.privacy', compact('settings'));
}
public function privacyStore(Request $request)
{
$settings = Auth::user()->settings;
$profile = Auth::user()->profile;
$fields = [
'is_private',
'crawlable',
];
foreach($fields as $field) {
$form = $request->input($field);
if($field == 'is_private') {
if($form == 'on') {
$profile->{$field} = true;
$settings->show_guests = false;
$settings->show_discover = false;
$profile->save();
} else {
$profile->{$field} = false;
$profile->save();
}
} elseif($field == 'crawlable') {
if($form == 'on') {
$settings->{$field} = false;
} else {
$settings->{$field} = true;
}
} else {
if($form == 'on') {
$settings->{$field} = true;
} else {
$settings->{$field} = false;
}
}
$settings->save();
}
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
}
public function security()
{
return view('settings.security');
$sessions = DB::table('sessions')
->whereUserId(Auth::id())
->limit(20)
->get();
$activity = AccountLog::whereUserId(Auth::id())
->orderBy('created_at','desc')
->limit(50)
->get();
return view('settings.security', compact('sessions', 'activity'));
}
public function applications()
@ -121,7 +207,7 @@ class SettingsController extends Controller
public function dataImportInstagram()
{
return view('settings.import.ig');
return view('settings.import.instagram.home');
}
public function developers()

View file

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

View file

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

View file

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

View file

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

10
app/ImportJob.php Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

10
app/ReportComment.php Normal file
View file

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

10
app/ReportLog.php Normal file
View file

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

View file

@ -2,7 +2,7 @@
namespace App;
use Storage;
use Auth, Storage;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -52,6 +52,14 @@ class Status extends Model
return url($path);
}
public function permalink($suffix = '/activity')
{
$id = $this->id;
$username = $this->profile->username;
$path = config('app.url') . "/p/{$username}/{$id}{$suffix}";
return url($path);
}
public function editUrl()
{
return $this->url() . '/edit';
@ -71,15 +79,42 @@ class Status extends Model
return $this->hasMany(Like::class);
}
public function liked() : bool
{
$profile = Auth::user()->profile;
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
}
public function comments()
{
return $this->hasMany(Status::class, 'in_reply_to_id');
}
public function bookmarked()
{
if(!Auth::check()) {
return 0;
}
$profile = Auth::user()->profile;
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
}
public function shares()
{
return $this->hasMany(Status::class, 'reblog_of_id');
}
public function shared() : bool
{
$profile = Auth::user()->profile;
return Status::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
}
public function parent()
{
if(!empty($this->in_reply_to_id)) {
return Status::findOrFail($this->in_reply_to_id);
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
if(!empty($parent)) {
return Status::findOrFail($parent);
}
}
@ -100,6 +135,23 @@ class Status extends Model
);
}
public function mentions()
{
return $this->hasManyThrough(
Profile::class,
Mention::class,
'status_id',
'id',
'id',
'profile_id'
);
}
public function reportUrl()
{
return route('report.form') . "?type=post&id={$this->id}";
}
public function toActivityStream()
{
$media = $this->media;
@ -133,4 +185,9 @@ class Status extends Model
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
__('notification.commented');
}
public function recentComments()
{
return $this->comments()->orderBy('created_at','desc')->take(3);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ class User extends Authenticatable
*
* @var array
*/
protected $dates = ['deleted_at'];
protected $dates = ['deleted_at', 'email_verified_at'];
/**
* The attributes that are mass assignable.
@ -44,4 +44,9 @@ class User extends Authenticatable
{
return url(config('app.url') . '/' . $this->username);
}
public function settings()
{
return $this->hasOne(UserSetting::class);
}
}

10
app/UserFilter.php Normal file
View file

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

10
app/UserSetting.php Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

2148
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

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

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.1.0',
'version' => '0.1.4',
/*
|--------------------------------------------------------------------------
@ -77,6 +77,17 @@ return [
'activitypub_enabled' => env('ACTIVITY_PUB', false),
/*
|--------------------------------------------------------------------------
| Account file size limit
|--------------------------------------------------------------------------
|
| Update the max account size, the per user limit of files in KB.
|
|
*/
'max_account_size' => env('MAX_ACCOUNT_SIZE', 1000000),
/*
|--------------------------------------------------------------------------
| Photo file size limit
@ -95,7 +106,7 @@ return [
| Change the caption length limit for new local posts.
|
*/
'max_caption_length' => env('MAX_CAPTION_LENGTH', 150),
'max_caption_length' => env('MAX_CAPTION_LENGTH', 500),
/*
|--------------------------------------------------------------------------
@ -116,5 +127,15 @@ return [
|
*/
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
/*
|--------------------------------------------------------------------------
| Image Quality
|--------------------------------------------------------------------------
|
| Set the image optimization quality, must be a value between 1-100.
|
*/
'image_quality' => (int) env('IMAGE_QUALITY', 80),
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/img/fred1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/js/app.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

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

View file

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

View file

@ -1,6 +1,12 @@
$(document).ready(function() {
$('.status-comment-focus').on('click', function(el) {
$('.status-card > .card-footer').each(function() {
$(this).addClass('d-none');
});
$(document).on('click', '.status-comment-focus', function(el) {
var form = $(this).parents().eq(2).find('.card-footer');
form.removeClass('d-none');
var el = $(this).parents().eq(2).find('input[name="comment"]');
el.focus();
});
@ -31,7 +37,7 @@ $(document).ready(function() {
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ reply + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
comments.prepend(comment);
comments.append(comment);
commentform.val('');
commentform.blur();
@ -41,7 +47,5 @@ $(document).ready(function() {
.catch(function (res) {
});
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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