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 5442 additions and 1375 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:

4
public/css/app.css vendored

File diff suppressed because one or more lines are too long

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

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>04/icon/black/svg/pixelfed-icon-black</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon-copy-6" fill="#000000">
<path d="M26.1989752,16.1654164 C26.1191375,15.6717246 25.9970712,15.1834955 25.8327784,14.7064947 C25.4032698,13.4577487 24.7071549,12.3267513 23.7688167,11.3882121 L18.8200906,6.43842552 C18.6286052,6.24689918 18.4253978,6.0651186 18.2059577,5.88739016 C18.9457319,2.7779446 21.7493666,0.5 25.0056862,0.5 C28.4355286,0.5 31.3631553,3.02718742 31.9058524,6.39134335 C31.9709938,6.7962157 32.0040616,7.15536036 32.0040616,7.499875 L32.0040616,14.49975 C32.0040616,15.3777768 31.8350727,16.2367131 31.511582,17.0488633 C30.4438253,16.4852131 29.2225744,16.1654164 27.9252137,16.1654164 L26.1989752,16.1654164 Z M23.8073996,31.046185 C23.5834161,32.4635947 23.7071641,33.9247757 24.178594,35.2935053 C24.6081026,36.5422513 25.3042175,37.6732487 26.2425556,38.6117879 L31.1921829,43.5624744 C31.3833579,43.7531345 31.5862081,43.9343531 31.8055418,44.112075 C31.065962,47.2217942 28.2621925,49.5 25.0056862,49.5 C21.5757888,49.5 18.6481233,46.9727317 18.1054939,43.6084951 C18.0403874,43.2038799 18.0073108,42.8446855 18.0073108,42.500125 L18.0073108,35.50025 C18.0073108,35.4938387 18.0073198,35.4874284 18.0073378,35.4810192 L22.6056081,31.046185 L23.8073996,31.046185 Z M35.1468104,25.8777977 C35.1968412,25.8615612 35.2467562,25.8448643 35.2965488,25.8277069 C36.5465838,25.396568 37.6766493,24.7009751 38.6141205,23.763303 L43.5628467,18.8135164 C43.7543612,18.6219608 43.9361291,18.4186771 44.1138454,18.1991515 C47.2222774,18.9383789 49.5,21.7431301 49.5,25.0004375 C49.5,28.4310698 46.9732731,31.3593627 43.6097572,31.9021084 C43.2052287,31.9672288 42.8461113,32.0003125 42.5016246,32.0003125 L35.5032492,32.0003125 C34.4465015,32.0003125 33.4174129,31.75537 32.4642879,31.2907626 C31.9585449,31.0442436 31.4866999,30.7388098 31.0577027,30.3834131 C32.9917905,29.5211241 34.4916783,27.8849489 35.1468104,25.8777977 Z M32.9818651,18.0619641 C33.4869192,16.9416136 33.7536554,15.736333 33.7536554,14.49975 L33.7536554,7.499875 C33.7536554,7.22965928 33.7384248,6.95788037 33.7088174,6.67629632 C36.4300225,5.000963 40.0234106,5.37310623 42.325648,7.67579129 C44.7509518,10.1024315 45.0347176,13.9597464 43.0399586,16.7217074 C42.8019022,17.0521235 42.5708139,17.3300572 42.325697,17.5752267 L37.3769708,22.5250133 C36.8285828,23.0735188 36.1967915,23.5184213 35.5026804,23.8530318 C35.505411,23.7709507 35.506787,23.6885334 35.506787,23.6058007 C35.506787,21.4032822 34.5316098,19.4242869 32.9818651,18.0619641 Z M29.1180777,30.9546188 C29.8495788,31.7387598 30.723016,32.3887119 31.6978107,32.8638651 C32.8860975,33.4431037 34.176657,33.7502812 35.5032492,33.7502812 L42.5016246,33.7502812 C42.7718182,33.7502812 43.043575,33.7350433 43.3251385,33.7054219 C45.0005544,36.4268542 44.6282644,40.021099 42.325697,42.3241597 C39.9008053,44.7495711 36.0434832,45.0332243 33.2816352,43.0384785 C32.9514226,42.8004669 32.6735484,42.5693291 32.4284315,42.3241597 L27.4797053,37.3743731 C26.7330644,36.6275722 26.1784347,35.7261305 25.8326956,34.723272 C25.4247205,33.5387745 25.3418769,32.266466 25.5842066,31.046185 L27.9252137,31.046185 C28.3310733,31.046185 28.7294844,31.0148879 29.1180777,30.9546188 Z M18.0361092,17.005126 C16.9498565,16.5196511 15.7507833,16.2505937 14.4967508,16.2505937 L7.49837538,16.2505937 C7.232029,16.2505937 6.96418507,16.2653384 6.68632324,16.2940088 C5.0111787,13.5720997 5.38321091,9.97879809 7.68567533,7.67584029 C10.1105671,5.25042891 13.9678891,4.96677571 16.7297371,6.96152155 C17.0599498,7.19953306 17.337824,7.43067088 17.5829409,7.67584029 L22.5316124,12.6264472 C23.2782947,13.3732895 23.8329179,14.2747125 24.1786584,15.2775495 C24.2790442,15.5688604 24.3597451,15.8655119 24.4207608,16.1654164 L20.6352393,16.1654164 C19.6618729,16.1654164 18.7627665,16.4774398 18.0361092,17.005126 Z M16.2612547,35.2537259 C16.258899,35.3357513 16.257717,35.4179313 16.257717,35.50025 L16.257717,42.500125 C16.257717,42.7703407 16.2729476,43.0421196 16.3025549,43.3237037 C13.5813306,44.9990489 9.98791079,44.6268885 7.68567533,42.3241597 C5.26078356,39.8987483 4.97719112,36.0405997 6.97150962,33.2781599 C7.20947014,32.9478765 7.44055844,32.6699428 7.68567533,32.4247733 L12.6344015,27.4749867 C13.3810424,26.7281858 14.282291,26.1734373 15.2849346,25.8276241 C15.6048854,25.7173766 15.9312445,25.6308666 16.2612547,25.5680951 L16.2612547,35.2537259 Z M16.2612547,23.7931301 C15.7377606,23.8724201 15.2199939,23.9990983 14.7148236,24.1731681 C13.4647886,24.604307 12.334723,25.2998999 11.3972518,26.237572 L6.44852566,31.1873586 C6.25392808,31.3819979 6.06939348,31.588746 5.88894736,31.812338 C2.78032488,31.0741675 0.5,28.2622334 0.5,25.0004375 C0.5,21.5662743 3.03179045,18.6360332 6.40961211,18.0974198 C6.55156249,18.0732434 7.23969074,18.0005625 7.49837538,18.0005625 L14.4967508,18.0005625 C15.2999569,18.0005625 16.0753514,18.1390919 16.7989258,18.3943464 C16.4561913,19.0066903 16.2612547,19.7099771 16.2612547,20.4579458 L16.2612547,23.7931301 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>04/icon/color/svg/pixelfed-icon-color</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon-copy-8" fill-rule="nonzero">

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=2)}({2:function(e,n,t){e.exports=t("uOOV")},uOOV:function(e,n){$(document).ready(function(){$(".pagination").hide();var e=document.querySelector(".timeline-feed");new InfiniteScroll(e,{path:".pagination__next",append:".timeline-feed",status:".page-load-status",history:!1}).on("append",function(e,n,t){pixelfed.hydrateLikes()})})}});
!function(e){var n={};function t(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,t),i.l=!0,i.exports}t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=2)}({2:function(e,n,t){e.exports=t("uOOV")},uOOV:function(e,n){$(document).ready(function(){$(".pagination").hide(),$(".container.timeline-container").removeClass("d-none");var e=document.querySelector(".timeline-feed");pixelfed.fetchLikes(),new InfiniteScroll(e,{path:".pagination__next",append:".timeline-feed",status:".page-load-status",history:!1}).on("append",function(e,n,t){pixelfed.hydrateLikes(),$(".status-card > .card-footer").each(function(){var e=$(this);e.hasClass("d-none")||e.find('input[name="comment"]').val()||$(this).addClass("d-none")})})}),$(document).on("DOMContentLoaded",function(){var e=!1,n=function(){if(!1===e){e=!0;var n=[].slice.call(document.querySelectorAll("img.lazy"));n.forEach(function(e){e.getBoundingClientRect().top<=window.innerHeight&&e.getBoundingClientRect().bottom>=0&&"none"!==getComputedStyle(e).display&&(e.src=e.dataset.src,e.srcset=e.dataset.srcset,e.classList.remove("lazy"),n=n.filter(function(n){return n!==e}))}),e=!1}};document.addEventListener("scroll",n),window.addEventListener("resize",n),window.addEventListener("orientationchange",n)})}});

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=4c2440700c647b915b2e",
"/css/app.css": "/css/app.css?id=a7c64d139bb04ef8e290",
"/js/timeline.js": "/js/timeline.js?id=d9a3145c0cd21ca09172",
"/js/app.js": "/js/app.js?id=670b2543dcb79503ba92",
"/css/app.css": "/css/app.css?id=909c2fa80940ca721877",
"/js/timeline.js": "/js/timeline.js?id=74c2181f0fcd6fe6933c",
"/js/activity.js": "/js/activity.js?id=723dfb98bbbc96a9d39f"
}

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