Merge pull request #1830 from pixelfed/staging

Staging
This commit is contained in:
daniel 2019-11-24 16:19:12 -07:00 committed by GitHub
commit e3f16c8b16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 863 additions and 217 deletions

View file

@ -4,6 +4,8 @@
### Added ### Added
- Added drafts API endpoint for Camera Roll ([bad2ecde](https://github.com/pixelfed/pixelfed/commit/bad2ecde)) - Added drafts API endpoint for Camera Roll ([bad2ecde](https://github.com/pixelfed/pixelfed/commit/bad2ecde))
- Added AccountService ([885a1258](https://github.com/pixelfed/pixelfed/commit/885a1258))
- Added post embeds ([1fecf717](https://github.com/pixelfed/pixelfed/commit/1fecf717))
### Fixed ### Fixed
- Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09)) - Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09))
@ -45,6 +47,10 @@
- Updated StatusTransformer, added ```local``` attribute ([484bb509](https://github.com/pixelfed/pixelfed/commit/484bb509)) - Updated StatusTransformer, added ```local``` attribute ([484bb509](https://github.com/pixelfed/pixelfed/commit/484bb509))
- Updated PostComponent, fix bug affecting MomentUI and non authenticated users ([7b3fe215](https://github.com/pixelfed/pixelfed/commit/7b3fe215)) - Updated PostComponent, fix bug affecting MomentUI and non authenticated users ([7b3fe215](https://github.com/pixelfed/pixelfed/commit/7b3fe215))
- Updated FixUsernames command to allow usernames containing ```.``` ([e5d77c6d](https://github.com/pixelfed/pixelfed/commit/e5d77c6d)) - Updated FixUsernames command to allow usernames containing ```.``` ([e5d77c6d](https://github.com/pixelfed/pixelfed/commit/e5d77c6d))
- Updated landing page, add age check ([d11e82c3](https://github.com/pixelfed/pixelfed/commit/d11e82c3))
- Updated ApiV1Controller, add ```mobile_apis``` to /api/v1/instance endpoint ([57407463](https://github.com/pixelfed/pixelfed/commit/57407463))
- Updated PublicTimelineService, add video media scopes ([7b00eba3](https://github.com/pixelfed/pixelfed/commit/7b00eba3))
- Updated PublicApiController, add AccountService ([5ebd2c8a](https://github.com/pixelfed/pixelfed/commit/5ebd2c8a))
## Deprecated ## Deprecated

View file

@ -906,7 +906,9 @@ class ApiV1Controller extends Controller
'max_avatar_size' => config('pixelfed.max_avatar_size'), 'max_avatar_size' => config('pixelfed.max_avatar_size'),
'max_caption_length' => config('pixelfed.max_caption_length'), 'max_caption_length' => config('pixelfed.max_caption_length'),
'max_bio_length' => config('pixelfed.max_bio_length'), 'max_bio_length' => config('pixelfed.max_bio_length'),
'max_album_length' => config('pixelfed.max_album_length') 'max_album_length' => config('pixelfed.max_album_length'),
'mobile_apis' => config('pixelfed.oauth_enabled')
] ]
]; ];
return response()->json($res, 200, [], JSON_PRETTY_PRINT); return response()->json($res, 200, [], JSON_PRETTY_PRINT);

View file

@ -63,7 +63,7 @@ class RegisterController extends Controller
'unique:users', 'unique:users',
function ($attribute, $value, $fail) { function ($attribute, $value, $fail) {
if (!ctype_alpha($value[0])) { if (!ctype_alpha($value[0])) {
return $fail('Username is invalid. Username must be alpha-numeric and start with a letter.'); return $fail('Username is invalid. Must start with a letter or number.');
} }
$val = str_replace(['_', '-', '.'], '', $value); $val = str_replace(['_', '-', '.'], '', $value);
if(!ctype_alnum($val)) { if(!ctype_alnum($val)) {
@ -73,6 +73,7 @@ class RegisterController extends Controller
]; ];
$rules = [ $rules = [
'agecheck' => 'required|accepted',
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
'username' => $usernameRules, 'username' => $usernameRules,
'email' => 'required|string|email|max:255|unique:users', 'email' => 'required|string|email|max:255|unique:users',

View file

@ -14,6 +14,8 @@ use App\{
}; };
use Auth, DB, Cache; use Auth, DB, Cache;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Transformer\Api\AccountTransformer;
use App\Transformer\Api\AccountWithStatusesTransformer;
use App\Transformer\Api\StatusStatelessTransformer; use App\Transformer\Api\StatusStatelessTransformer;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
@ -131,7 +133,31 @@ class DiscoverController extends Controller
public function profilesDirectory(Request $request) public function profilesDirectory(Request $request)
{ {
$profiles = Profile::whereNull('domain')->simplePaginate(48); return view('discover.profiles.home');
return view('discover.profiles.home', compact('profiles')); }
public function profilesDirectoryApi(Request $request)
{
$this->validate($request, [
'page' => 'integer|max:10'
]);
$page = $request->input('page') ?? 1;
$key = 'discover:profiles:page:' . $page;
$ttl = now()->addHours(12);
$res = Cache::remember($key, $ttl, function() {
$profiles = Profile::whereNull('domain')
->whereNull('status')
->whereIsPrivate(false)
->has('statuses')
->whereIsSuggestable(true)
// ->inRandomOrder()
->simplePaginate(8);
$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
return $this->fractal->createData($resource)->toArray();
});
return $res;
} }
} }

View file

@ -22,7 +22,11 @@ use App\Transformer\Api\{
RelationshipTransformer, RelationshipTransformer,
StatusTransformer, StatusTransformer,
}; };
use App\Services\UserFilterService; use App\Services\{
AccountService,
PublicTimelineService,
UserFilterService
};
use App\Jobs\StatusPipeline\NewStatusPipeline; use App\Jobs\StatusPipeline\NewStatusPipeline;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@ -38,17 +42,12 @@ class PublicApiController extends Controller
$this->fractal->setSerializer(new ArraySerializer()); $this->fractal->setSerializer(new ArraySerializer());
} }
protected function getUserData() protected function getUserData($user)
{ {
if(false == Auth::check()) { if(!$user) {
return []; return [];
} else { } else {
$profile = Auth::user()->profile; return AccountService::get($user->profile_id);
if($profile->status) {
return [];
}
$user = new Fractal\Resource\Item($profile, new AccountTransformer());
return $this->fractal->createData($user)->toArray();
} }
} }
@ -90,7 +89,7 @@ class PublicApiController extends Controller
$item = new Fractal\Resource\Item($status, new StatusTransformer()); $item = new Fractal\Resource\Item($status, new StatusTransformer());
$res = [ $res = [
'status' => $this->fractal->createData($item)->toArray(), 'status' => $this->fractal->createData($item)->toArray(),
'user' => $this->getUserData(), 'user' => $this->getUserData($request->user()),
'likes' => $this->getLikes($status), 'likes' => $this->getLikes($status),
'shares' => $this->getShares($status), 'shares' => $this->getShares($status),
'reactions' => [ 'reactions' => [
@ -235,12 +234,13 @@ class PublicApiController extends Controller
$max = $request->input('max_id'); $max = $request->input('max_id');
$limit = $request->input('limit') ?? 3; $limit = $request->input('limit') ?? 3;
// $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() { $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
// return Profile::whereIsPrivate(true) return Profile::whereIsPrivate(true)
// ->orWhere('unlisted', true) ->orWhere('unlisted', true)
// ->orWhere('status', '!=', null) ->orWhere('status', '!=', null)
// ->pluck('id'); ->pluck('id')
// }); ->toArray();
});
// if(Auth::check()) { // if(Auth::check()) {
// // $pid = Auth::user()->profile->id; // // $pid = Auth::user()->profile->id;
@ -255,7 +255,17 @@ class PublicApiController extends Controller
// $filtered = []; // $filtered = [];
// } // }
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : []; $filtered = Auth::check() ? array_merge($private, UserFilterService::filters(Auth::user()->profile_id)) : [];
// if($max == 0) {
// $res = PublicTimelineService::count();
// if($res == 0) {
// PublicTimelineService::warmCache();
// $res = PublicTimelineService::get(0,4);
// } else {
// $res = PublicTimelineService::get(0,4);
// }
// return response()->json($res);
// }
if($min || $max) { if($min || $max) {
$dir = $min ? '>' : '<'; $dir = $min ? '>' : '<';
@ -321,7 +331,6 @@ class PublicApiController extends Controller
$fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer()); $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
$res = $this->fractal->createData($fractal)->toArray(); $res = $this->fractal->createData($fractal)->toArray();
// $res = $timeline;
return response()->json($res); return response()->json($res);
} }
@ -439,98 +448,7 @@ class PublicApiController extends Controller
public function networkTimelineApi(Request $request) public function networkTimelineApi(Request $request)
{ {
if(!Auth::check()) { return response()->json([]);
return abort(403);
}
$this->validate($request,[
'page' => 'nullable|integer|max:40',
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'nullable|integer|max:20'
]);
$page = $request->input('page');
$min = $request->input('min_id');
$max = $request->input('max_id');
$limit = $request->input('limit') ?? 3;
// TODO: Use redis for timelines
// $timeline = Timeline::build()->local();
$pid = Auth::user()->profile->id;
$private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
return Profile::whereIsPrivate(true)
->orWhere('unlisted', true)
->orWhere('status', '!=', null)
->pluck('id');
});
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')->toArray();
$filtered = array_merge($private->toArray(), $filters);
if($min || $max) {
$dir = $min ? '>' : '<';
$id = $min ?? $max;
$timeline = Status::select(
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'scope',
'local',
'reply_count',
'comments_disabled',
'created_at',
'updated_at'
)->where('id', $dir, $id)
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereNotIn('profile_id', $filtered)
->whereNotNull('uri')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereVisibility('public')
->latest()
->limit($limit)
->get();
} else {
$timeline = Status::select(
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'scope',
'local',
'reply_count',
'comments_disabled',
'created_at',
'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereNotNull('uri')
->whereVisibility('public')
->latest()
->simplePaginate($limit);
}
$fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
$res = $this->fractal->createData($fractal)->toArray();
return response()->json($res);
} }
public function relationships(Request $request) public function relationships(Request $request)
@ -555,10 +473,7 @@ class PublicApiController extends Controller
public function account(Request $request, $id) public function account(Request $request, $id)
{ {
$profile = Profile::whereNull('status')->findOrFail($id); $res = AccountService::get($id);
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res); return response()->json($res);
} }

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
class SeasonalController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function yearInReview()
{
$profile = Auth::user()->profile;
return view('account.yir', compact('profile'));
}
}

View file

@ -10,10 +10,10 @@ use App\Util\Localization\Localization;
class SiteController extends Controller class SiteController extends Controller
{ {
public function home() public function home(Request $request)
{ {
if (Auth::check()) { if (Auth::check()) {
return $this->homeTimeline(); return $this->homeTimeline($request);
} else { } else {
return $this->homeGuest(); return $this->homeGuest();
} }
@ -24,9 +24,13 @@ class SiteController extends Controller
return view('site.index'); return view('site.index');
} }
public function homeTimeline() public function homeTimeline(Request $request)
{ {
return view('timeline.home'); $this->validate($request, [
'layout' => 'nullable|string|in:grid,feed'
]);
$layout = $request->input('layout', 'feed');
return view('timeline.home', compact('layout'));
} }
public function changeLocale(Request $request, $locale) public function changeLocale(Request $request, $locale)

View file

@ -51,6 +51,12 @@ class StatusController extends Controller
} }
} }
if($status->type == 'archived') {
if(Auth::user()->profile_id !== $status->profile_id) {
abort(404);
}
}
if ($request->wantsJson() && config('federation.activitypub.enabled')) { if ($request->wantsJson() && config('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $status); return $this->showActivityPub($request, $status);
} }
@ -70,13 +76,29 @@ class StatusController extends Controller
public function showEmbed(Request $request, $username, int $id) public function showEmbed(Request $request, $username, int $id)
{ {
abort(404); $profile = Profile::whereNull(['domain','status'])
$profile = Profile::whereNull('status')->whereUsername($username)->first(); ->whereIsPrivate(false)
$status = Status::whereScope('private')->find($id); ->whereUsername($username)
if(!$profile || !$status) { ->first();
return view('status.embed-removed'); if(!$profile) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
} }
return view('status.embed', compact('status')); $status = Status::whereProfileId($profile->id)
->whereNull('uri')
->whereScope('public')
->whereIsNsfw(false)
->whereIn('type', ['photo', 'video'])
->find($id);
if(!$status) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$showLikes = $request->filled('likes') && $request->likes == true;
$showCaption = $request->filled('caption') && $request->caption !== false;
$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
} }
public function showObject(Request $request, $username, int $id) public function showObject(Request $request, $username, int $id)

View file

@ -29,6 +29,7 @@ class Kernel extends HttpKernel
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
\App\Http\Middleware\EncryptCookies::class, \App\Http\Middleware\EncryptCookies::class,
\App\Http\Middleware\FrameGuard::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class,

View file

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
class FrameGuard
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if (!$response->headers->has('X-Frame-Options')) {
$response->headers->set('X-Frame-Options', 'SAMEORIGIN', false);
}
return $response;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Services;
use Cache;
use App\Profile;
use App\Transformer\Api\AccountTransformer;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class AccountService {
const CACHE_KEY = 'pf:services:account:';
public static function get($id)
{
$key = self::CACHE_KEY . ':' . $id;
$ttl = now()->addHours(12);
return Cache::remember($key, $ttl, function() use($id) {
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$profile = Profile::whereNull('status')->findOrFail($id);
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
return $fractal->createData($resource)->toArray();
});
}
}

View file

@ -52,7 +52,7 @@ class PublicTimelineService {
$ids = Status::whereNull('uri') $ids = Status::whereNull('uri')
->whereNull('in_reply_to_id') ->whereNull('in_reply_to_id')
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereIn('type', ['photo', 'photo:album']) ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereScope('public') ->whereScope('public')
->latest() ->latest()
->limit($limit) ->limit($limit)

View file

@ -33,6 +33,7 @@ class AccountTransformer extends Fractal\TransformerAbstract
'website' => $profile->website, 'website' => $profile->website,
'local' => (bool) $local, 'local' => (bool) $local,
'is_admin' => (bool) $is_admin, 'is_admin' => (bool) $is_admin,
'created_at' => $profile->created_at->timestamp
]; ];
} }

View file

@ -0,0 +1,56 @@
<?php
namespace App\Transformer\Api;
use Auth;
use App\Profile;
use League\Fractal;
class AccountWithStatusesTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
// 'relationship',
'posts',
];
public function transform(Profile $profile)
{
$local = $profile->domain == null;
$is_admin = !$local ? false : $profile->user->is_admin;
$acct = $local ? $profile->username : substr($profile->username, 1);
$username = $local ? $profile->username : explode('@', $acct)[0];
return [
'id' => (string) $profile->id,
'username' => $username,
'acct' => $acct,
'display_name' => $profile->name,
'locked' => (bool) $profile->is_private,
'followers_count' => $profile->followerCount(),
'following_count' => $profile->followingCount(),
'statuses_count' => (int) $profile->statusCount(),
'note' => $profile->bio ?? '',
'url' => $profile->url(),
'avatar' => $profile->avatarUrl(),
'website' => $profile->website,
'local' => (bool) $local,
'is_admin' => (bool) $is_admin,
'created_at' => $profile->created_at->timestamp
];
}
protected function includePosts(Profile $profile)
{
$posts = $profile
->statuses()
->whereIsNsfw(false)
->whereType('photo')
->whereScope('public')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->latest()
->take(5)
->get();
return $this->collection($posts, new StatusStatelessTransformer());
}
}

View file

@ -48,6 +48,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
'thread' => false, 'thread' => false,
'replies' => [], 'replies' => [],
'parent' => $status->parent() ? $this->transform($status->parent()) : [], 'parent' => $status->parent() ? $this->transform($status->parent()) : [],
'local' => (bool) $status->local,
]; ];
} }

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/css/appdark.css vendored

Binary file not shown.

BIN
public/css/landing.css vendored

Binary file not shown.

BIN
public/embed.js vendored Normal file

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/discover.js vendored

Binary file not shown.

BIN
public/js/profile-directory.js vendored Normal file

Binary file not shown.

BIN
public/js/quill.js vendored

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -76,7 +76,21 @@ window.App.util = {
['Walden','filter-walden'], ['Walden','filter-walden'],
['Willow','filter-willow'], ['Willow','filter-willow'],
['X-Pro II','filter-xpro-ii'] ['X-Pro II','filter-xpro-ii']
], ],
emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥' emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
], ],
embed: {
post: (function(url, caption = true, likes = false, layout = 'full') {
let u = url + '/embed?';
u += caption ? 'caption=true&' : 'caption=false&';
u += likes ? 'likes=true&' : 'likes=false&';
u += layout == 'compact' ? 'layout=compact' : 'layout=full';
return '<iframe src="'+u+'" class="pixelfed__embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"></iframe><script async defer src="'+window.location.origin +'/embed.js"><\/script>';
}),
profile: (function(url) {
// placeholder
console.error('This method is not supported yet');
})
}
}; };

View file

@ -98,6 +98,22 @@
</div> </div>
</a> </a>
<a class="d-none card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" :click="showAddToStoryCard">
<div class="card-body">
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;background-color: #008DF5">
<i class="fas fa-history text-white fa-lg"></i>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">Add to Story</span>
</p>
<p class="mb-0 text-muted">Add a photo or video to your story.</p>
</div>
</div>
</div>
</a>
<a class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/collections/create"> <a class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/collections/create">
<div class="card-body"> <div class="card-body">
<div class="media"> <div class="media">
@ -132,9 +148,8 @@
</div> </div>
</div> </div>
</div> </div>
<hr> <p class="pt-3">
<p> <a class="font-weight-bold" href="/site/help">Help</a>
<a class="font-weight-bold" href="/site/help">Need Help?</a>
</p> </p>
</div> </div>
</div> </div>
@ -755,10 +770,6 @@ export default {
this.pageTitle = ''; this.pageTitle = '';
switch(this.page) { switch(this.page) {
case 'addToStory':
this.page = 1;
break;
case 'cropPhoto': case 'cropPhoto':
case 'editMedia': case 'editMedia':
this.page = 2; this.page = 2;
@ -906,7 +917,8 @@ export default {
.then(res => { .then(res => {
this.cameraRollMedia = res.data; this.cameraRollMedia = res.data;
}); });
} },
} }
} }
</script> </script>

View file

@ -4,10 +4,10 @@
<img src="/img/pixelfed-icon-grey.svg"> <img src="/img/pixelfed-icon-grey.svg">
</div> </div>
<div v-else> <div v-else>
<div class="d-block d-md-none px-0 border-top-0 mx-n3"> <!-- <div class="d-block d-md-none px-0 border-top-0 mx-n3">
<input class="form-control rounded-0" placeholder="Search" v-model="searchTerm" v-on:keyup.enter="searchSubmit"> <input class="form-control rounded-0" placeholder="Search" v-model="searchTerm" v-on:keyup.enter="searchSubmit">
</div> </div> -->
<section class="d-none d-md-flex mb-md-2 pt-5 discover-bar" style="width:auto; overflow: auto hidden;" v-if="categories.length > 0"> <!-- <section class="d-none d-md-flex mb-md-2 pt-5 discover-bar" style="width:auto; overflow: auto hidden;" v-if="categories.length > 0">
<a v-if="config.ab.loops == true" class="text-decoration-none bg-transparent border border-success rounded d-inline-flex align-items-center justify-content-center mr-3 card-disc" href="/discover/loops"> <a v-if="config.ab.loops == true" class="text-decoration-none bg-transparent border border-success rounded d-inline-flex align-items-center justify-content-center mr-3 card-disc" href="/discover/loops">
<p class="text-success lead font-weight-bold mb-0">Loops</p> <p class="text-success lead font-weight-bold mb-0">Loops</p>
</a> </a>
@ -15,11 +15,39 @@
<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p> <p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p>
</a> </a>
</section> </section> -->
<section class="mb-5 section-explore"> <section class="mb-5 section-explore">
<div class="profile-timeline"> <div class="profile-timeline">
<div class="row p-0"> <div class="row p-0 mt-5">
<div class="col-4 p-1 p-sm-2 p-md-3" v-for="post in posts"> <div class="col-12 col-md-6">
<div class="">
<a class="card info-overlay card-md-border-0" :href="posts[0].url">
<div class="square">
<span v-if="posts[0].type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="posts[0].type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="posts[0].type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="{ 'background-image': 'url(' + posts[0].thumb + ')' }">
</div>
</div>
</a>
</div>
</div>
<div class="col-12 col-md-6 row p-0 m-0">
<div v-for="(post, index) in posts.slice(1,5)" class="col-6" style="margin-bottom:1.8rem;">
<a class="card info-overlay card-md-border-0" :href="post.url">
<div class="square">
<span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="post.type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="post.type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="{ 'background-image': 'url(' + post.thumb + ')' }">
</div>
</div>
</a>
</div>
</div>
</div>
<div class="row p-0" style="display: flex;">
<div v-for="(post, index) in posts.slice(5)" class="col-3 p-1 p-sm-2 p-md-3">
<a class="card info-overlay card-md-border-0" :href="post.url"> <a class="card info-overlay card-md-border-0" :href="post.url">
<div class="square"> <div class="square">
<span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span> <span v-if="post.type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>

View file

@ -234,7 +234,7 @@
</div> </div>
<form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false"> <form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea> <textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/> <input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply" :disabled="replyText.length == 0" />
</form> </form>
</div> </div>
</div> </div>
@ -351,7 +351,7 @@
</span> </span>
<button <button
:class="[replyText.length > 1 ? 'btn btn-sm font-weight-bold float-right btn-outline-dark ':'btn btn-sm font-weight-bold float-right btn-outline-lighter']" :class="[replyText.length > 1 ? 'btn btn-sm font-weight-bold float-right btn-outline-dark ':'btn btn-sm font-weight-bold float-right btn-outline-lighter']"
:disabled="replyText.length < 2" :disabled="replyText.length == 0 ? 'disabled':''"
@click="postReply" @click="postReply"
>Post</button> >Post</button>
</p> </p>
@ -547,6 +547,10 @@
.momentui .carousel-item { .momentui .carousel-item {
background: #000 !important; background: #000 !important;
} }
.reply-btn[disabled] {
opacity: .3;
color: #3897f0;
}
</style> </style>
<script> <script>

View file

@ -0,0 +1,127 @@
<template>
<div>
<div class="col-12">
<p class="font-weight-bold text-lighter text-uppercase">Profiles Directory</p>
<div v-if="loaded" class="">
<div class="row">
<div class="col-12 col-md-6 p-1" v-for="(profile, index) in profiles">
<div class="card card-body border shadow-none py-2">
<div class="media">
<a :href="profile.url"><img :src="profile.avatar" class="rounded-circle border mr-3" alt="..." width="40px" height="40px"></a>
<div class="media-body">
<p class="mt-0 mb-0 font-weight-bold">
<a :href="profile.url" class="text-dark">{{profile.username}}</a>
</p>
<p class="mb-1 small text-lighter d-flex justify-content-between font-weight-bold">
<span>
<span>{{prettyCount(profile.statuses_count)}}</span> POSTS
</span>
<span>
<span>{{postsPerDay(profile)}}</span> POSTS/DAY
</span>
<span>
<span>{{prettyCount(profile.followers_count)}}</span> FOLLOWERS
</span>
</p>
<p class="mb-1">
<span v-for="(post, i) in profile.posts" class="shadow-sm" :key="'profile_posts_'+i">
<a :href="post.url" class="text-decoration-none mr-1">
<img :src="thumbUrl(post)" width="62.3px" height="62.3px" class="border rounded">
</a>
</span>
</p>
</div>
</div>
</div>
</div>
<div v-if="showLoadMore" class="col-12">
<p class="text-center mb-0 pt-3">
<button class="btn btn-outline-secondary btn-sm px-4 py-1 font-weight-bold" @click="loadMore()">Load More</button>
</p>
</div>
</div>
</div>
<div v-else>
<div class="row">
<div class="col-12 d-flex justify-content-center align-items-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style type="text/css" scoped></style>
<script type="text/javascript">
export default {
props: ['profileId'],
data() {
return {
loaded: false,
showLoadMore: true,
profiles: [],
page: 1
}
},
beforeMount() {
this.fetchData();
},
methods: {
fetchData() {
axios.get('/api/pixelfed/v2/discover/profiles', {
params: {
page: this.page
}
})
.then(res => {
if(res.data.length == 0) {
this.showLoadMore = false;
this.loaded = true;
return;
}
this.profiles = res.data;
this.showLoadMore = this.profiles.length == 8;
this.loaded = true;
});
},
prettyCount(val) {
return App.util.format.count(val);
},
loadMore() {
this.loaded = false;
this.page++;
this.fetchData();
},
thumbUrl(p) {
return p.media_attachments[0].url;
},
postsPerDay(profile) {
let created = profile.created_at;
let now = Date.now();
let diff = Math.abs(created, now)
let day = 1000 * 60 * 60 * 24;
let days = Math.round(diff / day);
let statuses = profile.statuses_count;
let perDay = this.prettyCount(Math.floor(statuses / days));
console.log(perDay);
return perDay;
}
}
}
</script>

View file

@ -11,31 +11,31 @@
<div v-if="!loading && !networkError" class="mt-5 row"> <div v-if="!loading && !networkError" class="mt-5 row">
<div class="col-12 col-md-3 mb-4"> <div class="col-12 col-md-2 mb-4">
<div v-if="results.hashtags || results.profiles || results.statuses"> <div v-if="results.hashtags || results.profiles || results.statuses">
<p class="font-weight-bold">Filters</p> <p class="font-weight-bold">Filters</p>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter1" v-model="filters.hashtags"> <input type="checkbox" class="custom-control-input" id="filter1" v-model="filters.hashtags">
<label class="custom-control-label text-muted font-weight-light" for="filter1">Show Hashtags</label> <label class="custom-control-label text-muted font-weight-light" for="filter1">Hashtags</label>
</div> </div>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter2" v-model="filters.profiles"> <input type="checkbox" class="custom-control-input" id="filter2" v-model="filters.profiles">
<label class="custom-control-label text-muted font-weight-light" for="filter2">Show Profiles</label> <label class="custom-control-label text-muted font-weight-light" for="filter2">Profiles</label>
</div> </div>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter3" v-model="filters.statuses"> <input type="checkbox" class="custom-control-input" id="filter3" v-model="filters.statuses">
<label class="custom-control-label text-muted font-weight-light" for="filter3">Show Statuses</label> <label class="custom-control-label text-muted font-weight-light" for="filter3">Statuses</label>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-10">
<p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p> <p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p>
<hr> <hr>
<div v-if="filters.hashtags && results.hashtags" class="row mb-4"> <div v-if="filters.hashtags && results.hashtags" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Hashtags</p> <p class="col-12 font-weight-bold text-muted">Hashtags</p>
<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-3 mb-3" style="text-decoration: none;" :href="hashtag.url"> <a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-3 mb-3" style="text-decoration: none;" :href="hashtag.url">
<div class="card card-body text-center"> <div class="card card-body text-center shadow-none border">
<p class="lead mb-0 text-truncate text-dark" data-toggle="tooltip" :title="hashtag.value"> <p class="lead mb-0 text-truncate text-dark" data-toggle="tooltip" :title="hashtag.value">
#{{hashtag.value}} #{{hashtag.value}}
</p> </p>
@ -49,7 +49,7 @@
<div v-if="filters.profiles && results.profiles" class="row mb-4"> <div v-if="filters.profiles && results.profiles" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Profiles</p> <p class="col-12 font-weight-bold text-muted">Profiles</p>
<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="profile.url"> <a v-for="(profile, index) in results.profiles" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="profile.url">
<div class="card card-body text-center"> <div class="card card-body text-center shadow-none border">
<p class="text-center"> <p class="text-center">
<img :src="profile.entity.thumb" width="32px" height="32px" class="rounded-circle box-shadow"> <img :src="profile.entity.thumb" width="32px" height="32px" class="rounded-circle box-shadow">
</p> </p>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="container" style=""> <div class="container" style="">
<div class="row"> <div v-if="layout === 'feed'" class="row">
<div :class="[modes.distractionFree ? 'col-md-8 col-lg-8 offset-md-2 px-0 my-sm-3 timeline order-2 order-md-1':'col-md-8 col-lg-8 px-0 my-sm-3 timeline order-2 order-md-1']"> <div :class="[modes.distractionFree ? 'col-md-8 col-lg-8 offset-md-2 px-0 my-sm-3 timeline order-2 order-md-1':'col-md-8 col-lg-8 px-0 my-sm-3 timeline order-2 order-md-1']">
<div class="d-none" data-id="StoryTimelineComponent"></div> <div class="d-none" data-id="StoryTimelineComponent"></div>
<div style="padding-top:10px;"> <div style="padding-top:10px;">
@ -211,13 +211,13 @@
<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white sticky-md-bottom p-0"> <div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white sticky-md-bottom p-0">
<form class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="status.id" data-truncate="false"> <form class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="status.id" data-truncate="false">
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea> <textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="commentSubmit(status, $event)"/> <input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="commentSubmit(status, $event)" :disabled="replyText.length == 0" />
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div v-if="!loading && feed.length"> <div v-if="!loading && feed.length">
<div class="card shadow-none border"> <div class="card shadow-none">
<div class="card-body"> <div class="card-body">
<infinite-loading @infinite="infiniteTimeline" :distance="800"> <infinite-loading @infinite="infiniteTimeline" :distance="800">
<div slot="no-more" class="font-weight-bold">No more posts to load</div> <div slot="no-more" class="font-weight-bold">No more posts to load</div>
@ -227,7 +227,7 @@
</div> </div>
</div> </div>
<div v-if="!loading && scope == 'home' && feed.length == 0"> <div v-if="!loading && scope == 'home' && feed.length == 0">
<div class="card"> <div class="card shadow-none border">
<div class="card-body text-center"> <div class="card-body text-center">
<p class="h2 font-weight-lighter p-5">Hello, {{profile.acct}}</p> <p class="h2 font-weight-lighter p-5">Hello, {{profile.acct}}</p>
<p class="text-lighter"><i class="fas fa-camera-retro fa-5x"></i></p> <p class="text-lighter"><i class="fas fa-camera-retro fa-5x"></i></p>
@ -240,7 +240,7 @@
</div> </div>
<div v-if="!modes.distractionFree" class="col-md-4 col-lg-4 my-3 order-1 order-md-2 d-none d-md-block"> <div v-if="!modes.distractionFree" class="col-md-4 col-lg-4 my-3 order-1 order-md-2 d-none d-md-block">
<div class="position-sticky" style="top:68px;"> <div class="position-sticky" style="top:78px;">
<div class="mb-4"> <div class="mb-4">
<div class=""> <div class="">
<div class=""> <div class="">
@ -327,11 +327,11 @@
<p class="mb-0 text-uppercase font-weight-bold text-muted small"> <p class="mb-0 text-uppercase font-weight-bold text-muted small">
<a href="/site/about" class="text-dark pr-2">About Us</a> <a href="/site/about" class="text-dark pr-2">About Us</a>
<a href="/site/help" class="text-dark pr-2">Help</a> <a href="/site/help" class="text-dark pr-2">Help</a>
<a href="/site/open-source" class="text-dark pr-2">Open Source</a>
<a href="/site/language" class="text-dark pr-2">Language</a> <a href="/site/language" class="text-dark pr-2">Language</a>
<a href="/site/terms" class="text-dark pr-2">Terms</a> <a href="/discover/profiles" class="text-dark pr-2">Profiles</a>
<a href="/site/privacy" class="text-dark pr-2">Privacy</a>
<a href="/discover/places" class="text-dark pr-2">Places</a> <a href="/discover/places" class="text-dark pr-2">Places</a>
<a href="/site/privacy" class="text-dark pr-2">Privacy</a>
<a href="/site/terms" class="text-dark pr-2">Terms</a>
</p> </p>
<p class="mb-0 text-uppercase font-weight-bold text-muted small"> <p class="mb-0 text-uppercase font-weight-bold text-muted small">
<a href="http://pixelfed.org" class="text-muted" rel="noopener" title="" data-toggle="tooltip">Powered by Pixelfed</a> <a href="http://pixelfed.org" class="text-muted" rel="noopener" title="" data-toggle="tooltip">Powered by Pixelfed</a>
@ -341,40 +341,91 @@
</div> </div>
</div> </div>
</div> </div>
<b-modal ref="ctxModal" <div v-else class="row pt-2">
id="ctx-modal" <div class="col-12">
hide-header <div v-if="loading" class="text-center">
hide-footer <div class="spinner-border" role="status">
centered <span class="sr-only">Loading...</span>
rounded </div>
size="sm" </div>
body-class="list-group-flush p-0 rounded"> <div v-else class="row">
<div class="list-group text-center"> <div class="col-12 col-md-4 p-1 p-md-3 mb-3" v-for="(s, index) in feed" :key="`${index}-${s.id}`">
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div> <div class="card info-overlay card-md-border-0 shadow-sm border border-light" :href="statusUrl(s)">
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div> <div :class="[s.sensitive ? 'square' : 'square ' + s.media_attachments[0].filter_class]">
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div> <span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">Go to post</div> <span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">Embed</div> <span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</div> --> <div class="square-content" v-bind:style="previewBackground(s)">
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div> </div>
<div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div> <div class="info-overlay-text px-4">
<div v-if="ctxMenuStatus && (profile.is_admin || profile.id == ctxMenuStatus.account.id)" class="list-group-item rounded cursor-pointer" @click="deletePost(ctxMenuStatus)">Delete</div> <p class="text-white m-auto text-center">
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div> {{trimCaption(s.content_text)}}
</div> </p>
</b-modal> </div>
<b-modal ref="ctxModModal" </div>
id="ctx-mod-modal" </div>
hide-header <div class="py-3 media align-items-center">
hide-footer <img :src="s.account.avatar" class="mr-3 rounded-circle shadow-sm" :alt="s.account.username + ' \'s avatar'" width="30px" height="30px">
centered <div class="media-body">
rounded <p class="mb-0 font-weight-bold small">{{s.account.username}}</p>
size="sm" <p class="mb-0" style="line-height: 0.7;">
body-class="list-group-flush p-0 rounded"> <a :href="statusUrl(s)" class="small text-lighter">
<div class="list-group text-center"> <timeago :datetime="s.created_at" :auto-update="60" :converter-options="{includeSeconds:true}" :title="timestampFormat(s.created_at)" v-b-tooltip.hover.bottom></timeago>
<div class="list-group-item rounded cursor-pointer" @click="moderatePost(ctxMenuStatus, 'unlist')">Unlist from Timelines</div> </a>
<div class="list-group-item rounded cursor-pointer" @click="">Add Content Warning</div> </p>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModMenuClose()">Cancel</div> </div>
</div> <div class="ml-3">
<p class="mb-0">
<span class="font-weight-bold small">{{s.favourites_count == 1 ? '1 like' : s.favourites_count+' likes'}}</span>
<span class="px-2"><i v-bind:class="[s.favourited ? 'fas fa-heart text-danger cursor-pointer' : 'far fa-heart like-btn text-lighter cursor-pointer']" v-on:click="likeStatus(s, $event)"></i></span>
<span class="mr-2 cursor-pointer"><i class="fas fa-ellipsis-v" @click="ctxMenu(s)"></i></span>
</p>
</div>
</div>
</div>
</div>
<div v-if="!loading && feed.length">
<infinite-loading @infinite="infiniteTimeline" :distance="800">
<div slot="no-more" class="font-weight-bold">No more posts to load</div>
<div slot="no-results" class="font-weight-bold">No more posts to load</div>
</infinite-loading>
</div>
</div>
</div>
<b-modal ref="ctxModal"
id="ctx-modal"
hide-header
hide-footer
centered
rounded
size="sm"
body-class="list-group-flush p-0 rounded">
<div class="list-group text-center">
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div>
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
<div v-if="ctxMenuStatus && ctxMenuStatus.account.id != profile.id && ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">Go to post</div>
<div v-if="ctxMenuStatus && ctxMenuStatus.local == true" class="list-group-item rounded cursor-pointer" @click="ctxMenuEmbed()">Embed</div>
<!-- <div class="list-group-item rounded cursor-pointer" @click="ctxMenuShare()">Share</div> -->
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
<div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
<div v-if="ctxMenuStatus && (profile.is_admin || profile.id == ctxMenuStatus.account.id)" class="list-group-item rounded cursor-pointer" @click="deletePost(ctxMenuStatus)">Delete</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
</div>
</b-modal>
<b-modal ref="ctxModModal"
id="ctx-mod-modal"
hide-header
hide-footer
centered
rounded
size="sm"
body-class="list-group-flush p-0 rounded">
<div class="list-group text-center">
<div class="list-group-item rounded cursor-pointer" @click="moderatePost(ctxMenuStatus, 'unlist')">Unlist from Timelines</div>
<div class="list-group-item rounded cursor-pointer" @click="">Add Content Warning</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="ctxModMenuClose()">Cancel</div>
</div>
</b-modal> </b-modal>
<b-modal ref="ctxShareModal" <b-modal ref="ctxShareModal"
id="ctx-share-modal" id="ctx-share-modal"
@ -402,10 +453,10 @@
size="md" size="md"
body-class="p-2 rounded"> body-class="p-2 rounded">
<div> <div>
<textarea class="form-control disabled" rows="1" style="border: 1px solid #efefef; font-size: 14px; line-height: 17px; min-height: 37px; margin: 0 0 7px; resize: none; white-space: nowrap;" v-model="ctxEmbedPayload"></textarea> <textarea class="form-control disabled" rows="1" style="border: 1px solid #efefef; font-size: 14px; line-height: 12px; height: 37px; margin: 0 0 7px; resize: none; white-space: nowrap;" v-model="ctxEmbedPayload"></textarea>
<hr> <hr>
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button> <button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="#">API Terms of Use</a>.</p> <p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
</div> </div>
</b-modal> </b-modal>
<b-modal <b-modal
@ -454,11 +505,15 @@
height: 0px; height: 0px;
background: transparent; background: transparent;
} }
.reply-btn[disabled] {
opacity: .3;
color: #3897f0;
}
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
export default { export default {
props: ['scope'], props: ['scope', 'layout'],
data() { data() {
return { return {
ids: [], ids: [],
@ -1177,10 +1232,7 @@
ctxMenu(status) { ctxMenu(status) {
this.ctxMenuStatus = status; this.ctxMenuStatus = status;
// let payload = '<div class="pixlfed-media" data-id="'+ this.ctxMenuStatus.id + '"></div><script '; this.ctxEmbedPayload = window.App.util.embed.post(status.url);
// payload += 'src="https://pixelfed.dev/js/embed.js" async><';
// payload += '/script>';
// this.ctxEmbedPayload = payload;
if(status.account.id == this.profile.id) { if(status.account.id == this.profile.id) {
this.$refs.ctxModal.show(); this.$refs.ctxModal.show();
} else { } else {
@ -1354,6 +1406,21 @@
break; break;
} }
}, },
previewUrl(status) {
return status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].preview_url;
},
previewBackground(status) {
let preview = this.previewUrl(status);
return 'background-image: url(' + preview + ');';
},
trimCaption(caption, len = 60) {
return _.truncate(caption, {
length: len
});
}
} }
} }
</script> </script>

View file

@ -0,0 +1,4 @@
Vue.component(
'profile-directory',
require('./components/ProfileDirectory.vue').default
);

View file

@ -37,10 +37,6 @@ body, button, input, textarea {
color: #212529 !important; color: #212529 !important;
} }
.settings-nav .active {
border-left: 2px solid #6c757d !important
}
.settings-nav .active .nav-link{ .settings-nav .active .nav-link{
font-weight: bold; font-weight: bold;
} }

View file

@ -0,0 +1,17 @@
@extends('layouts.app')
@section('content')
<div class="container mt-5">
<div class="col-12">
<profile-directory profile-id="{{Auth::user()->profile_id}}"></profile-directory>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
<script type="text/javascript" src="{{mix('js/profile-directory.js')}}"></script>
<script type="text/javascript">App.boot();</script>
@endpush

View file

@ -75,7 +75,7 @@
</div> </div>
<div class="col-12 col-md-5 offset-md-1"> <div class="col-12 col-md-5 offset-md-1">
<div> <div>
<div class="card my-4"> <div class="card my-4 shadow-none border">
<div class="card-body px-lg-5"> <div class="card-body px-lg-5">
<div class="text-center pt-3"> <div class="text-center pt-3">
<img src="/img/pixelfed-icon-color.svg"> <img src="/img/pixelfed-icon-color.svg">
@ -86,7 +86,7 @@
</div> </div>
<div> <div>
@if(true === config('pixelfed.open_registration')) @if(true === config('pixelfed.open_registration'))
<form class="px-1" method="POST" action="{{ route('register') }}"> <form class="px-1" method="POST" action="{{ route('register') }}" id="register_form">
@csrf @csrf
<div class="form-group row"> <div class="form-group row">
<div class="col-md-12"> <div class="col-md-12">
@ -102,7 +102,7 @@
<div class="form-group row"> <div class="form-group row">
<div class="col-md-12"> <div class="col-md-12">
<input id="username" type="text" class="form-control{{ $errors->has('username') ? ' is-invalid' : '' }}" name="username" value="{{ old('username') }}" placeholder="{{ __('Username') }}" required> <input id="username" type="text" class="form-control{{ $errors->has('username') ? ' is-invalid' : '' }}" name="username" value="{{ old('username') }}" placeholder="{{ __('Username') }}" required maxlength="15" minlength="2">
@if ($errors->has('username')) @if ($errors->has('username'))
<span class="invalid-feedback"> <span class="invalid-feedback">
@ -141,6 +141,16 @@
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required>
</div> </div>
</div> </div>
<div class="form-group row">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" name="agecheck" type="checkbox" value="true" id="ageCheck" required>
<label class="form-check-label" for="ageCheck">
I am at least 16 years old
</label>
</div>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-md-12"> <div class="col-md-12">
<button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold"> <button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold">
@ -161,7 +171,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card card-body"> <div class="card shadow-none border card-body">
<p class="text-center mb-0 font-weight-bold">Have an account? <a href="/login">Log in</a></p> <p class="text-center mb-0 font-weight-bold">Have an account? <a href="/login">Log in</a></p>
</div> </div>
</div> </div>

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<title>Pixelfed | 404 Embed Not Found</title>
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
<meta name="medium" content="image">
<meta name="theme-color" content="#10c5f8">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
<style type="text/css">
body.embed-card {
background: #fff !important;
margin: 0;
padding-bottom: 0;
}
.status-card-embed {
box-shadow: none;
border-radius: 4px;
overflow: hidden;
}
</style>
</head>
<body class="bg-white">
<div class="embed-card">
<div class="card status-card-embed card-md-rounded-0 border card-body border shadow-none rounded-0 d-flex justify-content-center align-items-center">
<div class="text-center p-5">
<img src="/img/pixelfed-icon-color.svg" width="40px" height="40px">
<p class="h2 py-3 font-weight-bold">Pixelfed</p>
<p style="font-size:14px;font-weight: 500;" class="p-2">The link to this photo or video may be broken, or the post may have been removed.</p>
<p><a href="{{config('app.url')}}" class="font-weight-bold" target="_blank">Visit Pixelfed</a></p>
</div>
</div>
</div>
<script type="text/javascript">window.addEventListener("message",e=>{const t=e.data||{};window.parent&&"setHeight"===t.type&&window.parent.postMessage({type:"setHeight",id:t.id,height:document.getElementsByTagName("html")[0].scrollHeight},"*")});</script>
</body>
</html>

View file

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{$status->url()}}">
<meta name="medium" content="image">
<meta name="theme-color" content="#10c5f8">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
<style type="text/css">
body.embed-card {
background: #fff !important;
margin: 0;
padding-bottom: 0;
}
.status-card-embed {
box-shadow: none;
border-radius: 4px;
overflow: hidden;
}
</style>
</head>
<body class="bg-white">
<div class="embed-card">
@php($item = $status)
<div class="card status-card-embed card-md-rounded-0 border">
<div class="card-header d-inline-flex align-items-center bg-white">
<img src="{{$item->profile->avatarUrl()}}" width="32px" height="32px" target="_blank" style="border-radius: 32px;">
<a class="username font-weight-bold pl-2 text-dark" target="_blank" href="{{$item->profile->url()}}">
{{$item->profile->username}}
</a>
</div>
<a href="{{$status->url()}}" target="_blank">
@php($status = $item)
@switch($status->viewType())
@case('photo')
@case('image')
@if($status->is_nsfw)
<details class="details-animated">
<summary>
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<a class="max-hide-overflow {{$status->firstMedia()->filter_class}}" href="{{$status->url()}}" target="_blank">
<img class="card-img-top" src="{{$status->mediaUrl()}}">
</a>
</details>
@else
<div class="{{$status->firstMedia()->filter_class}}">
<img src="{{$status->mediaUrl()}}" width="100%">
</div>
@endif
@break
@case('album')
@if($status->is_nsfw)
@else
<div id="photo-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="carousel">
<ol class="carousel-indicators">
@for($i = 0; $i < $status->media_count; $i++)
<li data-target="#photo-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
@endfor
</ol>
<div class="carousel-inner">
@foreach($status->media()->orderBy('order')->get() as $media)
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
<figure class="{{$media->filter_class}}">
<span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;">{{$loop->iteration}}/{{$loop->count}}</span>
<img class="d-block w-100" src="{{$media->url()}}" alt="{{$status->caption}}">
</figure>
</div>
@endforeach
</div>
<a class="carousel-control-prev" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
@endif
@break
@case('video')
@if($status->is_nsfw)
<details class="details-animated">
<summary>
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<div class="embed-responsive embed-responsive-16by9">
<video class="video" preload="none" controls loop>
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
</video>
</div>
</details>
@else
<div class="embed-responsive embed-responsive-16by9">
<video class="video" preload="none" controls loop>
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
</video>
</div>
@endif
@break
@case('video-album')
@if($status->is_nsfw)
<details class="details-animated">
<summary>
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<div class="embed-responsive embed-responsive-16by9">
<video class="video" preload="none" controls loop>
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
</video>
</div>
</details>
@else
<div class="embed-responsive embed-responsive-16by9">
<video class="video" preload="none" controls loop>
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
</video>
</div>
@endif
@break
@endswitch
</a>
@if($layout != 'compact')
<div class="card-body">
<div class="view-more mb-2">
<a class="font-weight-bold" href="{{$status->url()}}" target="_blank">View More on Pixelfed</a>
</div>
<hr>
@if($showLikes)
<div class="likes font-weight-bold pb-2">
<span class="like-count">{{$item->likes_count}}</span> likes
</div>
@endif
<div class="caption">
<p class="my-0">
<span class="username font-weight-bold">
<bdi><a class="text-dark" href="{{$item->profile->url()}}" target="_blank">{{$item->profile->username}}</a></bdi>
</span>
@if($showCaption)
<span class="caption-container">{!! $item->rendered ?? e($item->caption) !!}</span>
@endif
</p>
</div>
</div>
@endif
<div class="card-footer bg-white d-inline-flex justify-content-between align-items-center">
<div class="timestamp">
<p class="small text-uppercase mb-0"><a href="{{$item->url()}}" class="text-muted" target="_blank">{{$item->created_at->diffForHumans()}}</a></p>
</div>
<div>
<a class="small font-weight-bold text-muted pr-1" href="{{config('app.url')}}" target="_blank">{{config('pixelfed.domain.app')}}</a>
<a href="https://pixelfed.org" target="_blank"><img src="/img/pixelfed-icon-color.svg" width="26px"></a>
</div>
</div>
</div>
</div>
<script type="text/javascript">window.addEventListener("message",e=>{const t=e.data||{};window.parent&&"setHeight"===t.type&&window.parent.postMessage({type:"setHeight",id:t.id,height:document.getElementsByTagName("html")[0].scrollHeight},"*")});</script>
<script type="text/javascript">document.querySelectorAll('.caption-container a').forEach(function(i) {i.setAttribute('target', '_blank');});</script>
</body>
</html>

View file

@ -2,10 +2,24 @@
@section('content') @section('content')
<timeline scope="home"></timeline> <timeline scope="home" layout="feed"></timeline>
@endsection @endsection
@if($layout == 'grid')
@push('styles')
<style type="text/css">
body {
background: #fff !important;
}
.navbar.border-bottom {
border-bottom: none !important;
}
</style>
@endpush
@endif
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script> <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script> <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>

View file

@ -2,10 +2,23 @@
@section('content') @section('content')
<timeline scope="local"></timeline> <timeline scope="local" layout="feed"></timeline>
@endsection @endsection
@if($layout == 'grid')
@push('styles')
<style type="text/css">
body {
background: #fff !important;
}
.navbar.border-bottom {
border-bottom: none !important;
}
</style>
@endpush
@endif
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script> <script type="text/javascript" src="{{ mix('js/timeline.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script> <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>

View file

@ -71,6 +71,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::redirect('discover/personal', '/discover'); Route::redirect('discover/personal', '/discover');
Route::get('discover', 'DiscoverController@home')->name('discover'); Route::get('discover', 'DiscoverController@home')->name('discover');
Route::get('discover/loops', 'DiscoverController@showLoops'); Route::get('discover/loops', 'DiscoverController@showLoops');
Route::get('discover/profiles', 'DiscoverController@profilesDirectory')->name('discover.profiles');
Route::group(['prefix' => 'api'], function () { Route::group(['prefix' => 'api'], function () {
Route::get('search', 'SearchController@searchAPI'); Route::get('search', 'SearchController@searchAPI');
@ -117,6 +119,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('config', 'ApiController@siteConfiguration'); Route::get('config', 'ApiController@siteConfiguration');
Route::get('discover', 'InternalApiController@discover'); Route::get('discover', 'InternalApiController@discover');
Route::get('discover/posts', 'InternalApiController@discoverPosts'); Route::get('discover/posts', 'InternalApiController@discoverPosts');
Route::get('discover/profiles', 'DiscoverController@profilesDirectoryApi');
Route::get('profile/{username}/status/{postid}', 'PublicApiController@status'); Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments'); Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes'); Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
@ -373,6 +376,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('c/{collection}', 'CollectionController@show'); Route::get('c/{collection}', 'CollectionController@show');
Route::get('p/{username}/{id}/c', 'CommentController@showAll'); Route::get('p/{username}/{id}/c', 'CommentController@showAll');
Route::get('p/{username}/{id}/embed', 'StatusController@showEmbed');
Route::get('p/{username}/{id}/edit', 'StatusController@edit'); Route::get('p/{username}/{id}/edit', 'StatusController@edit');
Route::post('p/{username}/{id}/edit', 'StatusController@editStore'); Route::post('p/{username}/{id}/edit', 'StatusController@editStore');
Route::get('p/{username}/{id}.json', 'StatusController@showObject'); Route::get('p/{username}/{id}.json', 'StatusController@showObject');

8
webpack.mix.js vendored
View file

@ -29,12 +29,14 @@ mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/lib/ace/ace.js', 'public/js') .js('resources/assets/js/lib/ace/ace.js', 'public/js')
.js('resources/assets/js/lib/ace/mode-dot.js', 'public/js') .js('resources/assets/js/lib/ace/mode-dot.js', 'public/js')
.js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js') .js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
.js('resources/assets/js/hashtag.js', 'public/js') .js('resources/assets/js/hashtag.js', 'public/js')
.js('resources/assets/js/collectioncompose.js', 'public/js') .js('resources/assets/js/collectioncompose.js', 'public/js')
.js('resources/assets/js/collections.js', 'public/js') .js('resources/assets/js/collections.js', 'public/js')
//.js('resources/assets/js/admin.js', 'public/js') .js('resources/assets/js/profile-directory.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
// .js('resources/assets/js/admin.js', 'public/js')
// .js('resources/assets/js/micro.js', 'public/js')
.extract([ .extract([
'lodash', 'lodash',