1
0
Fork 0

Merge pull request #7 from pixelfed/dev

Sync September 8
This commit is contained in:
okpierre 2019-09-08 19:34:48 -04:00 committed by GitHub
commit 40a362c1c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 2075 additions and 992 deletions

View File

@ -12,16 +12,16 @@ TRUST_PROXIES="*"
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=
DB_DATABASE='tests/database.sqlite'
DB_USERNAME=
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
SESSION_DRIVER=redis
CACHE_DRIVER=array
SESSION_DRIVER=array
SESSION_LIFETIME=120
QUEUE_DRIVER=redis

7
.gitignore vendored
View File

@ -13,3 +13,10 @@ npm-debug.log
yarn-error.log
.env
.DS_Store
.bash_profile
.bash_history
.bashrc
.gitconfig
.git-credentials
/.composer/
/nginx.conf

View File

@ -213,19 +213,15 @@ class BaseApiController extends Controller
$profile = $user->profile;
if(config('pixelfed.enforce_account_limit') == true) {
$size = Media::whereUserId($user->id)->sum('size') / 1000;
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000;
});
$limit = (int) config('pixelfed.max_account_size');
if ($size >= $limit) {
abort(403, 'Account size limit reached.');
}
}
$recent = Media::whereProfileId($profile->id)->whereNull('status_id')->count();
if($recent > 50) {
abort(403);
}
$monthHash = hash('sha1', date('Y').date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);

View File

@ -81,11 +81,13 @@ class ApiController extends BaseApiController
public function composeLocationSearch(Request $request)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'q' => 'required|string'
]);
$places = Place::where('name', 'like', '%' . $request->input('q') . '%')
$q = filter_var($request->input('q'), FILTER_SANITIZE_STRING);
$q = '%' . $q . '%';
$places = Place::where('name', 'like', $q)
->take(25)
->get()
->map(function($r) {

View File

@ -63,17 +63,17 @@ class RegisterController extends Controller
'unique:users',
function ($attribute, $value, $fail) {
if (!ctype_alpha($value[0])) {
return $fail($attribute.' is invalid. Username must be alpha-numeric and start with a letter.');
return $fail('Username is invalid. Username must be alpha-numeric and start with a letter.');
}
$val = str_replace(['-', '_'], '', $value);
$val = str_replace(['_', '-', '.'], '', $value);
if(!ctype_alnum($val)) {
return $fail($attribute . ' is invalid. Username must be alpha-numeric.');
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
}
},
];
$rules = [
'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
'username' => $usernameRules,
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',

View File

@ -65,27 +65,6 @@ class DiscoverController extends Controller
return view('discover.tags.category', compact('tag', 'posts'));
}
public function showPersonal(Request $request)
{
abort_if(!Auth::check(), 403);
$profile = Auth::user()->profile;
$tags = Cache::remember('profile-'.$profile->id.':hashtags', now()->addMinutes(15), function() use ($profile){
return $profile->hashtags()->groupBy('hashtag_id')->inRandomOrder()->take(8)->get();
});
$following = Cache::remember('profile:following:'.$profile->id, now()->addMinutes(60), function() use ($profile) {
$res = Follower::whereProfileId($profile->id)->pluck('following_id');
return $res->push($profile->id)->toArray();
});
$posts = Cache::remember('profile-'.$profile->id.':hashtag-posts', now()->addMinutes(5), function() use ($profile, $following) {
$posts = Status::whereScope('public')->withCount(['likes','comments'])->whereNotIn('profile_id', $following)->whereHas('media')->whereType('photo')->orderByDesc('created_at')->take(39)->get();
$posts->post_count = Status::whereScope('public')->whereNotIn('profile_id', $following)->whereHas('media')->whereType('photo')->count();
return $posts;
});
return view('discover.personal', compact('posts', 'tags'));
}
public function showLoops(Request $request)
{
if(config('exp.loops') != true) {
@ -148,4 +127,10 @@ class DiscoverController extends Controller
}
return $res;
}
public function profilesDirectory(Request $request)
{
$profiles = Profile::whereNull('domain')->simplePaginate(48);
return view('discover.profiles.home', compact('profiles'));
}
}

View File

@ -228,6 +228,9 @@ class FederationController extends Controller
$id = Helpers::validateUrl($bodyDecoded['id']);
$keyDomain = parse_url($keyId, PHP_URL_HOST);
$idDomain = parse_url($id, PHP_URL_HOST);
if($keyDomain == config('pixelfed.domain.app') || $idDomain == config('pixelfed.domain.app')) {
return false;
}
if(isset($bodyDecoded['object'])
&& is_array($bodyDecoded['object'])
&& isset($bodyDecoded['object']['attributedTo'])
@ -248,7 +251,7 @@ class FederationController extends Controller
}
$pkey = openssl_pkey_get_public($actor->public_key);
$inboxPath = "/users/{$profile->username}/inbox";
list($verified, $headers) = HTTPSignature::verify($pkey, $signatureData, $request->headers->all(), $inboxPath, $body);
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $request->headers->all(), $inboxPath, $body);
if($verified == 1) {
return true;
} else {

View File

@ -240,7 +240,8 @@ class InternalApiController extends Controller
'media.*.license' => 'nullable|string|max:80',
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable'
'place' => 'nullable',
'comments_disabled' => 'nullable|boolean'
]);
if(config('costar.enabled') == true) {
@ -255,7 +256,8 @@ class InternalApiController extends Controller
}
}
$profile = Auth::user()->profile;
$user = Auth::user();
$profile = $user->profile;
$visibility = $request->input('visibility');
$medias = $request->input('media');
$attachments = [];
@ -287,9 +289,15 @@ class InternalApiController extends Controller
if($request->filled('place')) {
$status->place_id = $request->input('place')['id'];
}
if($request->filled('comments_disabled')) {
$status->comments_disabled = $request->input('comments_disabled');
}
$status->caption = strip_tags($request->caption);
$status->scope = 'draft';
$status->profile_id = $profile->id;
$status->save();
foreach($attachments as $media) {
@ -308,6 +316,7 @@ class InternalApiController extends Controller
NewStatusPipeline::dispatch($status);
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('profile:status_count:'.$profile->id);
Cache::forget($user->storageUsedKey());
return $status->url();
}

View File

@ -2,7 +2,56 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth, Storage, URL;
use App\Media;
use Image as Intervention;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
class MediaController extends Controller
{
//
public function __construct()
{
$this->middleware('auth');
}
public function index(Request $request)
{
//return view('settings.drive.index');
}
public function composeUpdate(Request $request, $id)
{
$this->validate($request, [
'file' => function() {
return [
'required',
'mimes:' . config('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'),
];
},
]);
$user = Auth::user();
$photo = $request->file('file');
$media = Media::whereUserId($user->id)
->whereProfileId($user->profile_id)
->whereNull('status_id')
->findOrFail($id);
$fragments = explode('/', $media->media_path);
$name = last($fragments);
array_pop($fragments);
$dir = implode('/', $fragments);
$path = $photo->storeAs($dir, $name);
$res = [];
$res['url'] = URL::temporarySignedRoute(
'temp-media', now()->addHours(1), ['profileId' => $media->profile_id, 'mediaId' => $media->id]
);
ImageOptimize::dispatch($media);
return $res;
}
}

View File

@ -12,12 +12,33 @@ class PlaceController extends Controller
{
public function show(Request $request, $id, $slug)
{
// TODO: Replace with vue component + apis
$place = Place::whereSlug($slug)->findOrFail($id);
$posts = Status::wherePlaceId($place->id)
->whereNull('uri')
->whereScope('public')
->orderByDesc('created_at')
->paginate(10);
->simplePaginate(10);
return view('discover.places.show', compact('place', 'posts'));
}
public function directoryHome(Request $request)
{
$places = Place::select('country')
->distinct('country')
->simplePaginate(48);
return view('discover.places.directory.home', compact('places'));
}
public function directoryCities(Request $request, $country)
{
$country = urldecode($country);
$places = Place::whereCountry($country)
->orderBy('name', 'asc')
->distinct('name')
->simplePaginate(48);
return view('discover.places.directory.cities', compact('places'));
}
}

View File

@ -22,6 +22,7 @@ use App\Transformer\Api\{
RelationshipTransformer,
StatusTransformer,
};
use App\Services\UserFilterService;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@ -234,23 +235,27 @@ class PublicApiController extends Controller
$max = $request->input('max_id');
$limit = $request->input('limit') ?? 3;
$private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
return Profile::whereIsPrivate(true)
->orWhere('unlisted', true)
->orWhere('status', '!=', null)
->pluck('id');
});
// $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
// return Profile::whereIsPrivate(true)
// ->orWhere('unlisted', true)
// ->orWhere('status', '!=', null)
// ->pluck('id');
// });
if(Auth::check()) {
$pid = Auth::user()->profile->id;
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')->toArray();
$filtered = array_merge($private->toArray(), $filters);
} else {
$filtered = $private->toArray();
}
// if(Auth::check()) {
// // $pid = Auth::user()->profile->id;
// // $filters = UserFilter::whereUserId($pid)
// // ->whereFilterableType('App\Profile')
// // ->whereIn('filter_type', ['mute', 'block'])
// // ->pluck('filterable_id')->toArray();
// // $filtered = array_merge($private->toArray(), $filters);
// $filtered = UserFilterService::filters(Auth::user()->profile_id);
// } else {
// // $filtered = $private->toArray();
// $filtered = [];
// }
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
if($min || $max) {
$dir = $min ? '>' : '<';
@ -276,14 +281,12 @@ class PublicApiController extends Controller
->with('profile', 'hashtags', 'mentions')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->whereLocal(true)
->whereNull('uri')
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereVisibility('public')
->orderBy('created_at', 'desc')
->limit($limit)
->get();
//->toSql();
} else {
$timeline = Status::select(
'id',
@ -305,17 +308,16 @@ class PublicApiController extends Controller
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('profile', 'hashtags', 'mentions')
->whereLocal(true)
->whereNull('uri')
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereVisibility('public')
->orderBy('created_at', 'desc')
->simplePaginate($limit);
//->toSql();
}
$fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
$res = $this->fractal->createData($fractal)->toArray();
// $res = $timeline;
return response()->json($res);
}
@ -347,20 +349,22 @@ class PublicApiController extends Controller
return $following->push($pid)->toArray();
});
$private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
return Profile::whereIsPrivate(true)
->orWhere('unlisted', true)
->orWhere('status', '!=', null)
->pluck('id');
});
// $private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
// return Profile::whereIsPrivate(true)
// ->orWhere('unlisted', true)
// ->orWhere('status', '!=', null)
// ->pluck('id');
// });
$private = $private->diff($following)->flatten();
// $private = $private->diff($following)->flatten();
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')
->whereIn('filter_type', ['mute', 'block'])
->pluck('filterable_id')->toArray();
$filtered = array_merge($private->toArray(), $filters);
// $filters = UserFilter::whereUserId($pid)
// ->whereFilterableType('App\Profile')
// ->whereIn('filter_type', ['mute', 'block'])
// ->pluck('filterable_id')->toArray();
// $filtered = array_merge($private->toArray(), $filters);
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
if($min || $max) {
$dir = $min ? '>' : '<';
@ -387,8 +391,6 @@ class PublicApiController extends Controller
->where('id', $dir, $id)
->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereIn('visibility',['public', 'unlisted', 'private'])
->orderBy('created_at', 'desc')
->limit($limit)
@ -415,8 +417,6 @@ class PublicApiController extends Controller
->with('profile', 'hashtags', 'mentions')
->whereIn('profile_id', $following)
->whereNotIn('profile_id', $filtered)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereIn('visibility',['public', 'unlisted', 'private'])
->orderBy('created_at', 'desc')
->simplePaginate($limit);

View File

@ -54,7 +54,8 @@ class SearchController extends Controller
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl()
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain
]
]];
} else if ($type == 'Note') {
@ -92,7 +93,7 @@ class SearchController extends Controller
}
return $tokens;
});
$users = Profile::select('username', 'name', 'id')
$users = Profile::select('domain', 'username', 'name', 'id')
->whereNull('status')
->whereNull('domain')
->where('id', '!=', Auth::user()->profile->id)
@ -113,9 +114,11 @@ class SearchController extends Controller
'avatar' => $item->avatarUrl(),
'id' => $item->id,
'entity' => [
'id' => $item->id,
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'thumb' => $item->avatarUrl()
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain
]
];
});
@ -162,4 +165,5 @@ class SearchController extends Controller
return view('search.results');
}
}

View File

@ -22,7 +22,6 @@ class StatusController extends Controller
{
public function show(Request $request, $username, int $id)
{
// $id = strlen($id) < 17 ? array_first(\Hashids::decode($id)) : $id;
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if($user->status != null) {
@ -60,9 +59,24 @@ class StatusController extends Controller
return view($template, compact('user', 'status'));
}
public function showId(int $id)
{
abort(404);
$status = Status::whereNull('reblog_of_id')
->whereIn('scope', ['public', 'unlisted'])
->findOrFail($id);
return redirect($status->url());
}
public function showEmbed(Request $request, $username, int $id)
{
return;
abort(404);
$profile = Profile::whereNull('status')->whereUsername($username)->first();
$status = Status::whereScope('private')->find($id);
if(!$profile || !$status) {
return view('status.embed-removed');
}
return view('status.embed', compact('status'));
}
public function showObject(Request $request, $username, int $id)
@ -77,13 +91,7 @@ class StatusController extends Controller
->whereNotIn('visibility',['draft','direct'])
->findOrFail($id);
if($status->uri) {
$url = $status->uri;
if(ends_with($url, '/activity')) {
$url = str_replace('/activity', '', $url);
}
return redirect($url);
}
abort_if($status->uri, 404);
if($status->visibility == 'private' || $user->is_private) {
if(!Auth::check()) {
@ -190,7 +198,7 @@ class StatusController extends Controller
$resource = new Fractal\Resource\Item($status, new Note());
$res = $fractal->createData($resource)->toArray();
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT);
}
public function edit(Request $request, $username, $id)

View File

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

View File

@ -54,6 +54,10 @@ class ImageUpdate implements ShouldQueue
$path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path);
if (!is_file($path)) {
return;
}
if (in_array($media->mime, $this->protectedMimes) == true) {
ImageOptimizer::optimize($thumb);
ImageOptimizer::optimize($path);

View File

@ -18,6 +18,9 @@ class InboxWorker implements ShouldQueue
protected $profile;
protected $payload;
public $timeout = 5;
public $tries = 1;
/**
* Create a new job instance.
*

View File

@ -0,0 +1,99 @@
<?php
namespace App\Observers;
use App\UserFilter;
use App\Services\UserFilterService;
class UserFilterObserver
{
/**
* Handle the user filter "created" event.
*
* @param \App\UserFilter $userFilter
* @return void
*/
public function created(UserFilter $userFilter)
{
$this->filterCreate($userFilter);
}
/**
* Handle the user filter "updated" event.
*
* @param \App\UserFilter $userFilter
* @return void
*/
public function updated(UserFilter $userFilter)
{
$this->filterCreate($userFilter);
}
/**
* Handle the user filter "deleted" event.
*
* @param \App\UserFilter $userFilter
* @return void
*/
public function deleted(UserFilter $userFilter)
{
$this->filterDelete($userFilter);
}
/**
* Handle the user filter "restored" event.
*
* @param \App\UserFilter $userFilter
* @return void
*/
public function restored(UserFilter $userFilter)
{
$this->filterCreate($userFilter);
}
/**
* Handle the user filter "force deleted" event.
*
* @param \App\UserFilter $userFilter
* @return void
*/
public function forceDeleted(UserFilter $userFilter)
{
$this->filterDelete($userFilter);
}
protected function filterCreate(UserFilter $userFilter)
{
if($userFilter->filterable_type !== 'App\Profile') {
return;
}
switch ($userFilter->filter_type) {
case 'mute':
UserFilterService::mute($userFilter->user_id, $userFilter->filterable_id);
break;
case 'block':
UserFilterService::block($userFilter->user_id, $userFilter->filterable_id);
break;
}
}
protected function filterDelete(UserFilter $userFilter)
{
if($userFilter->filterable_type !== 'App\Profile') {
return;
}
switch ($userFilter->filter_type) {
case 'mute':
UserFilterService::unmute($userFilter->user_id, $userFilter->filterable_id);
break;
case 'block':
UserFilterService::unblock($userFilter->user_id, $userFilter->filterable_id);
break;
}
}
}

View File

@ -28,4 +28,16 @@ class Place extends Model
{
return $this->hasMany(Status::class, 'id', 'place_id');
}
public function countryUrl()
{
$country = strtolower($this->country);
$country = urlencode($country);
return url('/discover/location/country/' . $country);
}
public function cityUrl()
{
return $this->url();
}
}

View File

@ -6,13 +6,15 @@ use App\Observers\{
AvatarObserver,
NotificationObserver,
StatusHashtagObserver,
UserObserver
UserObserver,
UserFilterObserver,
};
use App\{
Avatar,
Notification,
StatusHashtag,
User
User,
UserFilter
};
use Auth, Horizon, URL;
use Illuminate\Support\Facades\Blade;
@ -35,6 +37,7 @@ class AppServiceProvider extends ServiceProvider
Notification::observe(NotificationObserver::class);
StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class);
UserFilter::observe(UserFilterObserver::class);
Horizon::auth(function ($request) {
return Auth::check() && $request->user()->is_admin;

View File

@ -0,0 +1,100 @@
<?php
namespace App\Services;
use Cache, Redis;
use App\{
Follower,
Profile,
UserFilter
};
class UserFilterService {
const USER_MUTES_KEY = 'pf:services:mutes:ids:';
const USER_BLOCKS_KEY = 'pf:services:blocks:ids:';
public static function mutes(int $profile_id) : array
{
$key = self::USER_MUTES_KEY . $profile_id;
$cached = Redis::zrevrange($key, 0, -1);
if($cached) {
return $cached;
} else {
$ids = UserFilter::whereFilterType('mute')
->whereUserId($profile_id)
->pluck('filterable_id')
->toArray();
foreach ($ids as $muted_id) {
Redis::zadd($key, (int) $muted_id, (int) $muted_id);
}
return $ids;
}
}
public static function blocks(int $profile_id) : array
{
$key = self::USER_BLOCKS_KEY . $profile_id;
$cached = Redis::zrevrange($key, 0, -1);
if($cached) {
return $cached;
} else {
$ids = UserFilter::whereFilterType('block')
->whereUserId($profile_id)
->pluck('filterable_id')
->toArray();
foreach ($ids as $blocked_id) {
Redis::zadd($key, $blocked_id, $blocked_id);
}
return $ids;
}
}
public static function filters(int $profile_id) : array
{
return array_merge(self::mutes($profile_id), self::blocks($profile_id));
}
public static function mute(int $profile_id, int $muted_id)
{
$key = self::USER_MUTES_KEY . $profile_id;
$mutes = self::mutes($profile_id);
$exists = in_array($muted_id, $mutes);
if(!$exists) {
Redis::zadd($key, $muted_id, $muted_id);
}
return true;
}
public static function unmute(int $profile_id, string $muted_id)
{
$key = self::USER_MUTES_KEY . $profile_id;
$mutes = self::mutes($profile_id);
$exists = in_array($muted_id, $mutes);
if($exists) {
Redis::zrem($key, $muted_id);
}
return true;
}
public static function block(int $profile_id, int $blocked_id)
{
$key = self::USER_BLOCKS_KEY . $profile_id;
$exists = in_array($blocked_id, self::blocks($profile_id));
if(!$exists) {
Redis::zadd($key, $blocked_id, $blocked_id);
}
return true;
}
public static function unblock(int $profile_id, string $blocked_id)
{
$key = self::USER_BLOCKS_KEY . $profile_id;
$exists = in_array($blocked_id, self::blocks($profile_id));
if($exists) {
Redis::zrem($key, $blocked_id);
}
return $exists;
}
}

View File

@ -54,6 +54,13 @@ class StatusTransformer extends Fractal\TransformerAbstract
];
}),
'tag' => [],
'location' => $status->place_id ? [
'type' => 'Place',
'name' => $status->place->name,
'longitude' => $status->place->long,
'latitude' => $status->place->lat,
'country' => $status->place->country
] : null,
];
}
}

View File

@ -74,7 +74,14 @@ class CreateNote extends Fractal\TransformerAbstract
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public'
]
],
'location' => $status->place_id ? [
'type' => 'Place',
'name' => $status->place->name,
'longitude' => $status->place->long,
'latitude' => $status->place->lat,
'country' => $status->place->country
] : null,
]
];
}

View File

@ -67,7 +67,14 @@ class Note extends Fractal\TransformerAbstract
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public'
]
],
'location' => $status->place_id ? [
'type' => 'Place',
'name' => $status->place->name,
'longitude' => $status->place->long,
'latitude' => $status->place->lat,
'country' => $status->place->country
] : null,
];
}
}

View File

@ -2,11 +2,16 @@
namespace App\Transformer\Api;
use Auth;
use App\Profile;
use League\Fractal;
class AccountTransformer extends Fractal\TransformerAbstract
{
protected $defaultIncludes = [
'relationship',
];
public function transform(Profile $profile)
{
$is_admin = $profile->domain ? false : $profile->user->is_admin;
@ -32,7 +37,12 @@ class AccountTransformer extends Fractal\TransformerAbstract
'bot' => null,
'website' => $profile->website,
'software' => 'pixelfed',
'is_admin' => (bool) $is_admin
'is_admin' => (bool) $is_admin,
];
}
protected function includeRelationship(Profile $profile)
{
return $this->item($profile, new RelationshipTransformer());
}
}

View File

@ -14,6 +14,9 @@ class RelationshipTransformer extends Fractal\TransformerAbstract
public function transform(Profile $profile)
{
$auth = Auth::check();
if(!$auth) {
return [];
}
$user = $auth ? Auth::user()->profile : false;
$requested = false;
if($user) {

View File

@ -78,4 +78,9 @@ class User extends Authenticatable
return $this->hasMany(UserDevice::class);
}
public function storageUsedKey()
{
return 'profile:storage:used:' . $this->id;
}
}

View File

@ -10,7 +10,8 @@ use App\{
Like,
Notification,
Profile,
Status
Status,
StatusHashtag,
};
use Carbon\Carbon;
use App\Util\ActivityPub\Helpers;
@ -129,7 +130,7 @@ class Inbox
}
$inReplyTo = $activity['inReplyTo'];
$url = $activity['id'];
$url = isset($activity['url']) ? $activity['url'] : $activity['id'];
Helpers::statusFirstOrFetch($url, true);
return;
@ -147,7 +148,7 @@ class Inbox
return;
}
$url = $activity['id'];
$url = isset($activity['url']) ? $activity['url'] : $activity['id'];
if(Status::whereUrl($url)->exists()) {
return;
}
@ -285,28 +286,74 @@ class Inbox
public function handleDeleteActivity()
{
if(!isset(
$this->payload['actor'],
$this->payload['object'],
$this->payload['object']['id'],
$this->payload['object']['type']
)) {
return;
}
$actor = $this->payload['actor'];
$obj = $this->payload['object'];
abort_if(!Helpers::validateUrl($obj), 400);
if(is_string($obj) && Helpers::validateUrl($obj)) {
// actor object detected
// todo delete actor
$type = $this->payload['object']['type'];
$typeCheck = in_array($type, ['Person', 'Tombstone']);
if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) {
return;
} else if (Helpers::validateUrl($obj['id']) && Helpers::validateObject($obj) && $obj['type'] == 'Tombstone') {
// todo delete status or object
}
if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
return;
}
$id = $this->payload['object']['id'];
switch ($type) {
case 'Person':
$profile = Profile::whereNull('domain')
->whereNull('private_key')
->whereRemoteUrl($id)
->first();
if(!$profile) {
return;
}
Notification::whereActorId($profile->id)->delete();
$profile->avatar()->delete();
$profile->followers()->delete();
$profile->following()->delete();
$profile->likes()->delete();
$profile->media()->delete();
$profile->statuses()->delete();
$profile->delete();
return;
break;
case 'Tombstone':
$status = Status::whereUri($id)->first();
if(!$status) {
return;
}
$status->media->delete();
$status->delete();
return;
break;
default:
return;
break;
}
}
public function handleLikeActivity()
{
$actor = $this->payload['actor'];
abort_if(!Helpers::validateUrl($actor), 400);
if(!Helpers::validateUrl($actor)) {
return;
}
$profile = self::actorFirstOrCreate($actor);
$obj = $this->payload['object'];
abort_if(!Helpers::validateLocalUrl($obj), 400);
if(!Helpers::validateUrl($obj)) {
return;
}
$status = Helpers::statusFirstOrFetch($obj);
if(!$status || !$profile) {
return;
@ -343,7 +390,9 @@ class Inbox
case 'Announce':
$obj = $obj['object'];
abort_if(!Helpers::validateLocalUrl($obj), 400);
if(!Helpers::validateLocalUrl($obj)) {
return;
}
$status = Helpers::statusFetch($obj);
if(!$status) {
return;

View File

@ -83,4 +83,19 @@ trait User {
{
return 100;
}
public function getMaxComposeMediaUpdatesPerHourAttribute()
{
return 100;
}
public function getMaxComposeMediaUpdatesPerDayAttribute()
{
return 1000;
}
public function getMaxComposeMediaUpdatesPerMonthAttribute()
{
return 5000;
}
}

617
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,6 @@ return [
|
*/
'driver' => 'gd',
'driver' => env('IMAGE_DRIVER', 'gd'),
];

View File

@ -25,7 +25,7 @@ return [
'timeline' => [
'local' => [
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', true)
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false)
]
],

View File

@ -23,7 +23,7 @@ return [
| This value is the version of your Pixelfed instance.
|
*/
'version' => '0.10.0',
'version' => '0.10.2',
/*
|--------------------------------------------------------------------------

View File

@ -16,7 +16,7 @@ return [
|
*/
'driver' => env('SESSION_DRIVER', 'file'),
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
@ -29,7 +29,7 @@ return [
|
*/
'lifetime' => env('SESSION_LIFETIME', 120),
'lifetime' => env('SESSION_LIFETIME', 2880),
'expire_on_close' => false,
@ -109,7 +109,7 @@ return [
|
*/
'lottery' => [2, 100],
'lottery' => [2, 1000],
/*
|--------------------------------------------------------------------------
@ -122,10 +122,7 @@ return [
|
*/
'cookie' => env(
'SESSION_COOKIE',
str_slug(env('APP_NAME', 'laravel'), '_').'_session'
),
'cookie' => 'pxfs',
/*
|--------------------------------------------------------------------------
@ -151,7 +148,7 @@ return [
|
*/
'domain' => env('SESSION_DOMAIN', null),
'domain' => env('SESSION_DOMAIN', env('APP_DOMAIN', null)),
/*
|--------------------------------------------------------------------------
@ -164,7 +161,7 @@ return [
|
*/
'secure' => env('SESSION_SECURE_COOKIE', false),
'secure' => true,
/*
|--------------------------------------------------------------------------

View File

@ -47,7 +47,7 @@ return [
/*
* This path will be used to register the necessary routes for the package.
*/
'path' => 'laravel-websockets',
'path' => 'pxws',
/*
* Dashboard Routes Middleware
@ -98,18 +98,20 @@ return [
* certificate chain of issuers. The private key also may be contained
* in a separate file specified by local_pk.
*/
'local_cert' => null,
'local_cert' => env('WSS_LOCAL_CERT', null),
/*
* Path to local private key file on filesystem in case of separate files for
* certificate (local_cert) and private key.
*/
'local_pk' => null,
'local_pk' => env('WSS_LOCAL_PK', null),
/*
* Passphrase for your local_cert file.
*/
'passphrase' => null,
'passphrase' => env('WSS_PASSPHRASE', null),
'verify_peer' => env('WSS_VERIFY_PEER', false),
],
/*

83
package-lock.json generated
View File

@ -797,6 +797,11 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@trevoreyre/autocomplete-vue": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@trevoreyre/autocomplete-vue/-/autocomplete-vue-2.0.2.tgz",
"integrity": "sha512-2A/SQjtZOX1/4AAsw/EkblMKw1xH+EJj99K2tRXu8umUpBFWUFk3PXCa7NlBE3fv2YSKf7fGEn1i225xtiulUg=="
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -2241,9 +2246,9 @@
}
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
@ -2516,6 +2521,11 @@
"sha.js": "^2.4.8"
}
},
"cropperjs": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.5.4.tgz",
"integrity": "sha512-2DczDxhqruvDz+oOUjRTXvsIyUnlSDvWG7r7qaUQioPoZAgfA07kaTtdNXAD/8ezA4CHRk1MT7PFMFQVpcD/1Q=="
},
"cross-env": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz",
@ -4020,7 +4030,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -4038,11 +4049,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4055,15 +4068,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -4166,7 +4182,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -4176,6 +4193,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -4188,17 +4206,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -4215,6 +4236,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -4287,7 +4309,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -4297,6 +4320,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -4372,7 +4396,8 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -4402,6 +4427,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -4419,6 +4445,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -4457,11 +4484,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true
"bundled": true,
"optional": true
}
}
},
@ -5715,9 +5744,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash._baseassign": {
"version": "3.2.0",
@ -7654,9 +7683,9 @@
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
},
"psl": {
"version": "1.1.31",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
"integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz",
"integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag=="
},
"public-encrypt": {
"version": "4.0.3",
@ -8947,9 +8976,9 @@
}
},
"spdx-license-ids": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz",
"integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA=="
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q=="
},
"spdy": {
"version": "4.0.0",
@ -9838,6 +9867,14 @@
"babel-helper-vue-jsx-merge-props": "^2.0.3"
}
},
"vue-cropperjs": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/vue-cropperjs/-/vue-cropperjs-4.0.0.tgz",
"integrity": "sha512-9x9HFUN2RPpz+AJ9bvnvbtAytW5Q9WeZcMgXqvaRutaCFNGA29rKkKWpUPRy+gn/rFJ5ZpcC1qZO9Hrd3WeqZA==",
"requires": {
"cropperjs": "^1.1.3"
}
},
"vue-functional-data-merge": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/ace.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/compose-classic.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/loops.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/quill.js vendored

File diff suppressed because one or more lines are too long

2
public/js/search.js vendored

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[18],{14:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)}),ace.require(["ace/theme/monokai"],function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)})}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[14,0]]]);
(window.webpackJsonp=window.webpackJsonp||[]).push([[19],{15:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)}),ace.require(["ace/theme/monokai"],function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)})}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[15,0]]]);

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,26 +1,27 @@
{
"/js/manifest.js": "/js/manifest.js?id=01c8731923a46c30aaed",
"/js/vendor.js": "/js/vendor.js?id=383c6f227a3b8d8d1c71",
"/js/ace.js": "/js/ace.js?id=4a28163d5fd63e64d6af",
"/js/vendor.js": "/js/vendor.js?id=fac92a458473b287c543",
"/js/ace.js": "/js/ace.js?id=585114d8896dc0c24020",
"/js/activity.js": "/js/activity.js?id=a414018a6c03ddd2a492",
"/js/app.js": "/js/app.js?id=e44ab72ffa230a7a56c1",
"/css/app.css": "/css/app.css?id=73af4bf8ced6e3c406ee",
"/css/appdark.css": "/css/appdark.css?id=10d573d6faab9ee2eeca",
"/css/landing.css": "/css/landing.css?id=385cadd3ef179fae26cc",
"/css/app.css": "/css/app.css?id=67710dadf879498b096a",
"/css/appdark.css": "/css/appdark.css?id=6c52e68ca396700f0836",
"/css/landing.css": "/css/landing.css?id=6164f7c333f0ab25b7f3",
"/css/quill.css": "/css/quill.css?id=81604d62610b0dbffad6",
"/js/collectioncompose.js": "/js/collectioncompose.js?id=a117b4e0b1d2fd859de5",
"/js/collections.js": "/js/collections.js?id=5c9926c1f532e17026fc",
"/js/collectioncompose.js": "/js/collectioncompose.js?id=afac91fbc98cde05f305",
"/js/collections.js": "/js/collections.js?id=fecadca26c0cab56015c",
"/js/components.js": "/js/components.js?id=b981ec12e26469676c4e",
"/js/compose.js": "/js/compose.js?id=f27e95963a805f49dd03",
"/js/developers.js": "/js/developers.js?id=a395f12c52bb0eada6ab",
"/js/discover.js": "/js/discover.js?id=e8165d745727c3c71c62",
"/js/hashtag.js": "/js/hashtag.js?id=b4ffe6499880acf0591c",
"/js/loops.js": "/js/loops.js?id=017e807837b173d82724",
"/js/mode-dot.js": "/js/mode-dot.js?id=8224e306cf53e3336620",
"/js/profile.js": "/js/profile.js?id=575df314fa611b303ef7",
"/js/quill.js": "/js/quill.js?id=9edfe94c043a1bc68860",
"/js/search.js": "/js/search.js?id=b1bd588d07e682f8fce5",
"/js/status.js": "/js/status.js?id=709b96bbcc47b605497b",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=344fb8527bb66574e4cd",
"/js/timeline.js": "/js/timeline.js?id=1dbd02afede65e4144c3"
"/js/compose.js": "/js/compose.js?id=17f022113cb2947ccdbd",
"/js/compose-classic.js": "/js/compose-classic.js?id=52685de8819dc6008fc8",
"/js/developers.js": "/js/developers.js?id=9636d4060ca6b359d8a2",
"/js/discover.js": "/js/discover.js?id=0fc453a7630b144fcf4e",
"/js/hashtag.js": "/js/hashtag.js?id=1e5990f89b6bfe7e037b",
"/js/loops.js": "/js/loops.js?id=9c31302552d789d5f35b",
"/js/mode-dot.js": "/js/mode-dot.js?id=993d7fee684361edddbc",
"/js/profile.js": "/js/profile.js?id=c0d5f349300590fefc8a",
"/js/quill.js": "/js/quill.js?id=1ab4119a62cc484034d9",
"/js/search.js": "/js/search.js?id=f312959df65e86a307a3",
"/js/status.js": "/js/status.js?id=0857ea680bd2eb9c0b05",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=700e5dc735365e184e41",
"/js/timeline.js": "/js/timeline.js?id=d8650e1b8a63fd491ccd"
}

View File

@ -1,197 +1,244 @@
<template>
<div>
<input type="file" name="media" class="d-none file-input" multiple="" v-bind:accept="config.uploader.media_types">
<input type="file" id="pf-dz" name="media" class="w-100 h-100 d-none file-input" draggable="true" multiple="true" v-bind:accept="config.uploader.media_types">
<div class="timeline">
<div class="card status-card card-md-rounded-0">
<div class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="profile.avatar" width="32px" height="32px" style="border-radius: 32px;" class="box-shadow">
<a class="username font-weight-bold pl-2 text-dark" v-bind:href="profile.url">
{{profile.username}}
</a>
<div class="text-right" style="flex-grow:1;">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<div class="dropdown-item small font-weight-bold" v-on:click="createCollection">Create Collection</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item small font-weight-bold" v-on:click="about">About</div>
<div class="dropdown-item small font-weight-bold" v-on:click="closeModal">Close</div>
</div>
</div>
<div v-if="uploading">
<div class="card status-card card-md-rounded-0 w-100 h-100 bg-light py-5" style="border-bottom: 1px solid #f1f1f1">
<div class="p-5 mt-2">
<b-progress :value="uploadProgress" :max="100" striped :animated="true"></b-progress>
<p class="text-center mb-0 font-weight-bold">Uploading ... ({{uploadProgress}}%)</p>
</div>
</div>
<div class="postPresenterContainer">
<div v-if="uploading">
<div class="w-100 h-100 bg-light py-5" style="border-bottom: 1px solid #f1f1f1">
<div class="p-5">
<b-progress :value="uploadProgress" :max="100" striped :animated="true"></b-progress>
<p class="text-center mb-0 font-weight-bold">Uploading ... ({{uploadProgress}}%)</p>
</div>
</div>
<div v-else>
<div class="card status-card card-md-rounded-0 w-100 h-100" style="display:flex;">
<div class="card-header d-inline-flex align-items-center bg-white">
<div>
<a v-if="page == 1" href="#" @click.prevent="closeModal()" class="font-weight-bold text-decoration-none text-muted">
<i class="fas fa-times fa-lg"></i>
<span class="font-weight-bold mb-0">{{pageTitle}}</span>
</a>
<span v-else>
<span>
<a class="text-lighter text-decoration-none mr-3" href="#" @click.prevent="goBack()"><i class="fas fa-long-arrow-alt-left fa-lg"></i></a>
</span>
<span class="font-weight-bold mb-0">{{pageTitle}}</span>
</span>
</div>
<div class="text-right" style="flex-grow:1;">
<!-- <a v-if="page > 1" class="font-weight-bold text-decoration-none" href="#" @click.prevent="page--">Back</a> -->
<span v-if="pageLoading">
<div class="spinner-border spinner-border-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
</span>
<a v-if="!pageLoading && (page > 1 && page <= 3) || (page == 1 && ids.length != 0)" class="font-weight-bold text-decoration-none" href="#" @click.prevent="nextPage">Next</a>
<a v-if="!pageLoading && page == 4" class="font-weight-bold text-decoration-none" href="#" @click.prevent="compose">Post</a>
</div>
</div>
<div v-else>
<div v-if="ids.length > 0 && ids.length != config.uploader.album_limit" class="card-header py-2 bg-primary m-2 rounded cursor-pointer" v-on:click="addMedia($event)">
<p class="text-center mb-0 font-weight-bold text-white"><i class="fas fa-plus mr-1"></i> Add Photo</p>
</div>
<div v-if="ids.length == 0" class="w-100 h-100 bg-light py-5 cursor-pointer" style="border-bottom: 1px solid #f1f1f1" v-on:click="addMedia($event)">
<div class="p-5">
<p class="text-center font-weight-bold">{{composeMessage()}}</p>
<p class="text-muted mb-0 small text-center">Accepted Formats: <b>{{acceptedFormats()}}</b></p>
<p class="text-muted mb-0 small text-center">Max File Size: <b>{{maxSize()}}</b></p>
<div class="card-body p-0 border-top">
<div v-if="page == 1" class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: 400px;">
<div class="text-center">
<p>
<a class="btn btn-primary font-weight-bold" href="/i/compose">Compose Post</a>
</p>
<hr>
<p>
<button type="button" class="btn btn-outline-primary font-weight-bold" @click.prevent="addMedia">Compose Post <sup>BETA</sup></button>
</p>
<p>
<button class="btn btn-outline-primary font-weight-bold" @click.prevent="createCollection">New Collection</button>
</p>
<!-- <p>
<button class="btn btn-outline-primary font-weight-bold" @click.prevent="showAddToStoryCard()">Add To My Story</button>
</p> -->
<p>
<a class="font-weight-bold" href="/site/help">Need Help?</a>
</p>
<p class="text-muted mb-0 small text-center">Formats: <b>{{acceptedFormats()}}</b> up to <b>{{maxSize()}}</b></p>
<p class="text-muted mb-0 small text-center">Albums can contain up to <b>{{config.uploader.album_limit}}</b> photos or videos</p>
</div>
</div>
<div v-if="ids.length > 0">
<b-carousel id="p-carousel"
style="text-shadow: 1px 1px 2px #333;"
controls
indicators
background="#ffffff"
:interval="0"
v-model="carouselCursor"
<div v-if="page == 2" class="w-100 h-100">
<div v-if="ids.length > 0">
<vue-cropper
ref="cropper"
:relativeZoom="cropper.zoom"
:aspectRatio="cropper.aspectRatio"
:viewMode="cropper.viewMode"
:zoomable="cropper.zoomable"
:rotatable="true"
:src="media[0].url"
>
</vue-cropper>
</div>
</div>
<div v-if="page == 3" class="w-100 h-100">
<div slot="img" style="display:flex;min-height: 420px;align-items: center;">
<img :class="'d-block img-fluid w-100 ' + [media[carouselCursor].filter_class?media[carouselCursor].filter_class:'']" :src="media[carouselCursor].url" :alt="media[carouselCursor].description" :title="media[carouselCursor].description">
</div>
<hr>
<div v-if="ids.length > 0 && media[carouselCursor].type == 'Image'" class="align-items-center px-2 pt-2">
<ul class="nav media-drawer-filters text-center">
<li class="nav-item">
<div class="p-1 pt-3">
<img :src="media[carouselCursor].url" width="100px" height="60px" v-on:click.prevent="toggleFilter($event, null)" class="cursor-pointer">
</div>
<a :class="[media[carouselCursor].filter_class == null ? 'nav-link text-primary active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, null)">No Filter</a>
</li>
<li class="nav-item" v-for="(filter, index) in filters">
<div class="p-1 pt-3">
<img :src="media[carouselCursor].url" width="100px" height="60px" :class="filter[1]" v-on:click.prevent="toggleFilter($event, filter[1])">
</div>
<a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-primary active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a>
</li>
</ul>
</div>
</div>
<div v-if="page == 4" class="w-100 h-100">
<div class="border-bottom mt-2">
<div class="media px-3">
<img :src="media[0].url" width="42px" height="42px" :class="[media[0].filter_class?'mr-2 ' + media[0].filter_class:'mr-2']">
<div class="media-body">
<div class="form-group">
<label class="font-weight-bold text-muted small d-none">Caption</label>
<textarea class="form-control border-0 rounded-0 no-focus" rows="2" placeholder="Write a caption..." style="resize:none" v-model="composeText"></textarea>
</div>
</div>
</div>
</div>
<div class="border-bottom">
<p class="px-4 mb-0 py-2 cursor-pointer" @click="showTagCard()">Tag people</p>
</div>
<div class="border-bottom">
<p class="px-4 mb-0 py-2 cursor-pointer" @click="showLocationCard()" v-if="!place">Add location</p>
<p v-else class="px-4 mb-0 py-2">
<span class="text-lighter">Location:</span> {{place.name}}, {{place.country}}
<span class="float-right">
<a href="#" @click.prevent="showLocationCard()" class="text-muted font-weight-bold small mr-2">Change</a>
<a href="#" @click.prevent="place = false" class="text-muted font-weight-bold small">Remove</a>
</span>
</p>
</div>
<div class="border-bottom">
<p class="px-4 mb-0 py-2">
<span class="text-lighter">Visibility:</span> {{visibilityTag}}
<span class="float-right">
<a href="#" @click.prevent="showVisibilityCard()" class="text-muted font-weight-bold small mr-2">Change</a>
</span>
</p>
</div>
<div style="min-height: 200px;">
<p class="px-4 mb-0 py-2 small font-weight-bold text-muted cursor-pointer" @click="showAdvancedSettingsCard()">Advanced settings</p>
</div>
</div>
<div v-if="page == 'tagPeople'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div>
<div v-if="page == 'addLocation'" class="w-100 h-100 p-3">
<p class="mb-0">Add Location</p>
<autocomplete
:search="locationSearch"
placeholder="Search locations ..."
aria-label="Search locations ..."
:get-result-value="getResultValue"
@submit="onSubmitLocation"
>
<b-carousel-slide v-if="ids.length > 0" v-for="(preview, index) in media" :key="'preview_media_'+index">
<div slot="img" :class="[media[index].filter_class?media[index].filter_class:'']" style="display:flex;min-height: 320px;align-items: center;">
<img class="d-block img-fluid w-100" :src="preview.url" :alt="preview.description" :title="preview.description">
</div>
</b-carousel-slide>
</b-carousel>
</autocomplete>
</div>
<div v-if="ids.length > 0 && media[carouselCursor].type == 'Image'" class="bg-dark align-items-center">
<ul class="nav media-drawer-filters text-center">
<li class="nav-item">
<div class="p-1 pt-3">
<img :src="media[carouselCursor].url" width="100px" height="60px" v-on:click.prevent="toggleFilter($event, null)" class="cursor-pointer">
</div>
<a :class="[media[carouselCursor].filter_class == null ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, null)">No Filter</a>
</li>
<li class="nav-item" v-for="(filter, index) in filters">
<div class="p-1 pt-3">
<img :src="media[carouselCursor].url" width="100px" height="60px" :class="filter[1]" v-on:click.prevent="toggleFilter($event, filter[1])">
</div>
<a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-white active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a>
</li>
</ul>
</div>
</div>
<div v-if="ids.length > 0 && ['Image', 'Video'].indexOf(media[carouselCursor].type) != -1" class="bg-lighter p-2 row">
<div v-if="media[carouselCursor].type == 'Image'" class="col-12">
<div class="form-group">
<input type="text" class="form-control" v-model="media[carouselCursor].alt" placeholder="Optional image description">
</div>
<div class="form-group">
<input type="text" class="form-control" v-model="media[carouselCursor].license" placeholder="Optional media license">
<div v-if="page == 'advancedSettings'" class="w-100 h-100">
<div class="list-group list-group-flush">
<div class="list-group-item d-flex justify-content-between">
<div>
<div class="text-dark ">Turn off commenting</div>
<p class="text-muted small mb-0">Disables comments for this post, you can change this later.</p>
</div>
<div>
<div class="custom-control custom-switch" style="z-index: 9999;">
<input type="checkbox" class="custom-control-input" id="asdisablecomments" v-model="commentsDisabled">
<label class="custom-control-label" for="asdisablecomments"></label>
</div>
</div>
</div>
<div class="list-group-item d-flex justify-content-between">
<div>
<div class="text-dark ">Contains NSFW Media</div>
</div>
<div>
<div class="custom-control custom-switch" style="z-index: 9999;">
<input type="checkbox" class="custom-control-input" id="asnsfw" v-model="nsfw">
<label class="custom-control-label" for="asnsfw"></label>
</div>
</div>
</div>
<a class="list-group-item" @click.prevent="page = 'altText'">
<div class="text-dark">Write alt text</div>
<p class="text-muted small mb-0">Alt text describes your photos for people with visual impairments.</p>
</a>
<a href="#" class="list-group-item" @click.prevent="page = 'addToCollection'">
<div class="text-dark">Add to Collection</div>
<p class="text-muted small mb-0">Add this post to a collection.</p>
</a>
<a href="#" class="list-group-item" @click.prevent="page = 'schedulePost'">
<div class="text-dark">Schedule</div>
<p class="text-muted small mb-0">Schedule post for a future date.</p>
</a>
<a href="#" class="list-group-item" @click.prevent="page = 'mediaMetadata'">
<div class="text-dark">Metadata</div>
<p class="text-muted small mb-0">Manage media exif and metadata.</p>
</a>
</div>
</div>
<!-- <div class="col-6 pt-2">
<button class="btn btn-outline-secondary btn-sm mr-1"><i class="fas fa-map-marker-alt"></i></button>
<button class="btn btn-outline-secondary btn-sm"><i class="fas fa-tools"></i></button>
</div> -->
<div class="col-12 text-right pt-2">
<button class="btn btn-outline-danger btn-sm font-weight-bold mr-1" v-on:click="deleteMedia()">Delete Media</button>
<div v-if="page == 'visibility'" class="w-100 h-100">
<div class="list-group list-group-flush">
<div :class="'list-group-item lead cursor-pointer ' + [visibility == 'public'?'text-primary':'']" @click="toggleVisibility('public')">Public</div>
<div :class="'list-group-item lead cursor-pointer ' + [visibility == 'unlisted'?'text-primary':'']" @click="toggleVisibility('unlisted')">Unlisted</div>
<div :class="'list-group-item lead cursor-pointer ' + [visibility == 'private'?'text-primary':'']" @click="toggleVisibility('private')">Followers Only</div>
</div>
</div>
</div>
</div>
<div class="card-body p-0 border-top">
<div class="caption">
<textarea class="form-control mb-0 border-0 rounded-0" rows="3" placeholder="Add an optional caption" v-model="composeText"></textarea>
</div>
</div>
<div v-if="page == 'altText'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div v-if="page == 'addToCollection'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div>
<div v-if="page == 'schedulePost'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div>
<div v-if="page == 'mediaMetadata'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div>
<div v-if="page == 'addToStory'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div>
</div>
<!-- card-footers -->
<div v-if="page == 2" class="card-footer bg-white d-flex justify-content-between">
<div>
<div class="custom-control custom-switch d-inline mr-3">
<input type="checkbox" class="custom-control-input" id="nsfwToggle" v-model="nsfw">
<label class="custom-control-label small font-weight-bold text-muted pt-1" for="nsfwToggle">NSFW</label>
</div>
<div class="dropdown d-inline">
<button class="btn btn-outline-secondary btn-sm py-0 dropdown-toggle" type="button" id="visibility" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{visibility[0].toUpperCase() + visibility.slice(1)}}
</button>
<div class="dropdown-menu" aria-labelledby="visibility" style="width: 200px;">
<a :class="[visibility=='public'?'dropdown-item active':'dropdown-item']" href="#" data-id="public" data-title="Public" v-on:click.prevent="visibility = 'public'">
<div class="row">
<div class="d-none d-block-sm col-sm-2 px-0 text-center">
<i class="fas fa-globe"></i>
</div>
<div class="col-12 col-sm-10 pl-2">
<p class="font-weight-bold mb-0">Public</p>
<p class="small mb-0">Anyone can see</p>
</div>
</div>
</a>
<a :class="[visibility=='private'?'dropdown-item active':'dropdown-item']" href="#" data-id="private" data-title="Followers Only" v-on:click.prevent="visibility = 'private'">
<div class="row">
<div class="d-none d-block-sm col-sm-2 px-0 text-center">
<i class="fas fa-lock"></i>
</div>
<div class="col-12 col-sm-10 pl-2">
<p class="font-weight-bold mb-0">Followers Only</p>
<p class="small mb-0">Only followers can see</p>
</div>
</div>
</a>
<a :class="[visibility=='unlisted'?'dropdown-item active':'dropdown-item']" href="#" data-id="private" data-title="Unlisted" v-on:click.prevent="visibility = 'unlisted'">
<div class="row">
<div class="d-none d-block-sm col-sm-2 px-0 text-center">
<i class="fas fa-lock"></i>
</div>
<div class="col-12 col-sm-10 pl-2">
<p class="font-weight-bold mb-0">Unlisted</p>
<p class="small mb-0">Not listed on public timelines</p>
</div>
</div>
</a>
<!-- <a class="dropdown-item" href="#" data-id="circle" data-title="Circle">
<div class="row">
<div class="col-12 col-sm-2 px-0 text-center">
<i class="far fa-circle"></i>
</div>
<div class="col-12 col-sm-10 pl-2">
<p class="font-weight-bold mb-0">Circle</p>
<p class="small mb-0">Select a circle</p>
</div>
</div>
</a>
<a class="dropdown-item" href="#" data-id="direct" data-title="Direct Message">
<div class="row">
<div class="col-12 col-sm-2 px-0 text-center">
<i class="fas fa-envelope"></i>
</div>
<div class="col-12 col-sm-10 pl-2">
<p class="font-weight-bold mb-0">Direct Message</p>
<p class="small mb-0">Recipients only</p>
</div>
</div>
</a> -->
</div>
</div>
<button type="button" class="btn btn-outline-secondary" @click="rotate"><i class="fas fa-undo"></i></button>
</div>
<div class="small text-muted font-weight-bold">
{{composeText.length}} / {{config.uploader.max_caption_length}}
</div>
<div class="pl-md-5">
<!-- <div class="btn-group">
<button type="button" class="btn btn-primary btn-sm font-weight-bold" v-on:click="compose()">{{composeState[0].toUpperCase() + composeState.slice(1)}}</button>
<button type="button" class="btn btn-primary btn-sm dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a :class="[composeState == 'publish' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'publish'">Publish now</a>
<!- - <a :class="[composeState == 'draft' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'draft'">Save as draft</a>
<a :class="[composeState == 'schedule' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'schedule'">Schedule for later</a>
<div class="dropdown-divider"></div>
<a :class="[composeState == 'delete' ?'dropdown-item font-weight-bold active':'dropdown-item font-weight-bold ']" href="#" v-on:click.prevent="composeState = 'delete'">Delete</a> - ->
</div>
</div> -->
<button class="btn btn-primary btn-sm font-weight-bold px-3" v-on:click="compose()">Publish</button>
<div>
<div class="d-inline-block button-group">
<button :class="'btn font-weight-bold ' + [cropper.aspectRatio == 16/9 ? 'btn-primary':'btn-light']" @click.prevent="changeAspect(16/9)">16:9</button>
<button :class="'btn font-weight-bold ' + [cropper.aspectRatio == 4/3 ? 'btn-primary':'btn-light']" @click.prevent="changeAspect(4/3)">4:3</button>
<button :class="'btn font-weight-bold ' + [cropper.aspectRatio == 3/2 ? 'btn-primary':'btn-light']" @click.prevent="changeAspect(3/2)">3:2</button>
<button :class="'btn font-weight-bold ' + [cropper.aspectRatio == 1 ? 'btn-primary':'btn-light']" @click.prevent="changeAspect(1)">1:1</button>
<button :class="'btn font-weight-bold ' + [cropper.aspectRatio == 2/3 ? 'btn-primary':'btn-light']" @click.prevent="changeAspect(2/3)">2:3</button>
</div>
</div>
</div>
</div>
@ -219,12 +266,36 @@
display: none;
}
}
.no-focus {
border-color: none;
outline: 0;
box-shadow: none;
}
a.list-group-item {
text-decoration: none;
}
a.list-group-item:hover {
text-decoration: none;
background-color: #f8f9fa !important;
}
</style>
<script type="text/javascript">
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
import Autocomplete from '@trevoreyre/autocomplete-vue'
import '@trevoreyre/autocomplete-vue/dist/style.css'
export default {
components: {
VueCropper,
Autocomplete
},
data() {
return {
config: window.App.config,
pageLoading: false,
profile: {},
composeText: '',
composeTextLength: 0,
@ -233,17 +304,45 @@ export default {
ids: [],
media: [],
carouselCursor: 0,
visibility: 'public',
mediaDrawer: false,
composeState: 'publish',
uploading: false,
uploadProgress: 0,
composeType: false
uploadProgress: 100,
composeType: false,
page: 1,
composeState: 'publish',
visibility: 'public',
visibilityTag: 'Public',
nsfw: false,
place: false,
commentsDisabled: false,
pageTitle: '',
cropper: {
aspectRatio: 1,
viewMode: 1,
zoomable: true,
zoom: 0
},
taggedUsernames: false,
namedPages: [
'tagPeople',
'addLocation',
'advancedSettings',
'visibility',
'altText',
'addToCollection',
'schedulePost',
'mediaMetadata',
'addToStory'
]
}
},
beforeMount() {
this.fetchProfile();
if(this.config.uploader.media_types.includes('video/mp4') == false) {
this.composeType = 'post'
}
},
mounted() {
@ -297,6 +396,7 @@ export default {
fetchProfile() {
axios.get('/api/v1/accounts/verify_credentials').then(res => {
this.profile = res.data;
window.pixelfed.currentUser = res.data;
if(res.data.locked == true) {
this.visibility = 'private';
}
@ -315,51 +415,88 @@ export default {
mediaWatcher() {
let self = this;
$(document).on('change', '.file-input', function(e) {
let io = document.querySelector('.file-input');
Array.prototype.forEach.call(io.files, function(io, i) {
self.uploading = true;
if(self.media && self.media.length + i >= self.config.uploader.album_limit) {
swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');
return;
}
let type = io.type;
let acceptedMimes = self.config.uploader.media_types.split(',');
let validated = $.inArray(type, acceptedMimes);
if(validated == -1) {
swal('Invalid File Type', 'The file you are trying to add is not a valid mime type. Please upload a '+self.config.uploader.media_types+' only.', 'error');
return;
}
let form = new FormData();
form.append('file', io);
let xhrConfig = {
onUploadProgress: function(e) {
let progress = Math.round( (e.loaded * 100) / e.total );
self.uploadProgress = progress;
}
};
axios.post('/api/v1/media', form, xhrConfig)
.then(function(e) {
self.uploadProgress = 100;
self.ids.push(e.data.id);
self.media.push(e.data);
setTimeout(function() {
self.uploading = false;
}, 1000);
}).catch(function(e) {
self.uploading = false;
io.value = null;
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
});
io.value = null;
self.uploadProgress = 0;
});
self.mediaDragAndDrop();
$(document).on('change', '#pf-dz', function(e) {
self.mediaUpload();
});
},
mediaUpload() {
let self = this;
self.uploading = true;
let io = document.querySelector('#pf-dz');
Array.prototype.forEach.call(io.files, function(io, i) {
if(self.media && self.media.length + i >= self.config.uploader.album_limit) {
swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');
return;
}
let type = io.type;
let acceptedMimes = self.config.uploader.media_types.split(',');
let validated = $.inArray(type, acceptedMimes);
if(validated == -1) {
swal('Invalid File Type', 'The file you are trying to add is not a valid mime type. Please upload a '+self.config.uploader.media_types+' only.', 'error');
return;
}
let form = new FormData();
form.append('file', io);
let xhrConfig = {
onUploadProgress: function(e) {
let progress = Math.round( (e.loaded * 100) / e.total );
self.uploadProgress = progress;
}
};
axios.post('/api/v1/media', form, xhrConfig)
.then(function(e) {
self.uploadProgress = 100;
self.ids.push(e.data.id);
self.media.push(e.data);
self.page = 2;
setTimeout(function() {
self.uploading = false;
}, 1000);
}).catch(function(e) {
self.uploading = false;
io.value = null;
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
});
io.value = null;
self.uploadProgress = 0;
});
},
mediaDragAndDrop() {
let self = this;
let pdz = document.getElementById('content');
function allowDrag(e) {
e.dataTransfer.dropEffect = 'copy';
e.preventDefault();
}
function handleDrop(e) {
e.preventDefault();
let dz = document.querySelector('#pf-dz');
dz.files = e.dataTransfer.files;
$('#composeModal').modal('show');
self.mediaUpload();
}
window.addEventListener('dragenter', function(e) {
});
pdz.addEventListener('dragenter', allowDrag);
pdz.addEventListener('dragover', allowDrag);
pdz.addEventListener('dragleave', function(e) {
//
});
pdz.addEventListener('drop', handleDrop);
},
toggleFilter(e, filter) {
this.media[this.carouselCursor].filter_class = filter;
},
@ -446,18 +583,15 @@ export default {
media: this.media,
caption: this.composeText,
visibility: this.visibility,
cw: this.nsfw
cw: this.nsfw,
comments_disabled: this.commentsDisabled,
place: this.place
};
axios.post('/api/v2/status/compose', data)
axios.post('/api/local/status/compose', data)
.then(res => {
let data = res.data;
window.location.href = data;
}).catch(err => {
let res = err.response.data;
if(res.message == 'Too Many Attempts.') {
swal('You\'re posting too much!', 'We only allow 50 posts per hour or 100 per day. If you\'ve reached that limit, please try again later. If you think this is an error, please contact an administrator.', 'error');
return;
}
swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error');
});
return;
@ -507,6 +641,51 @@ export default {
window.location.href = '/i/collections/create';
},
nextPage() {
switch(this.page) {
case 1:
this.page = 3;
break;
case 2:
this.pageLoading = true;
let self = this;
this.$refs.cropper.getCroppedCanvas({
maxWidth: 4096,
maxHeight: 4096,
fillColor: '#fff',
imageSmoothingEnabled: false,
imageSmoothingQuality: 'high',
}).toBlob(function(blob) {
let data = new FormData();
data.append('file', blob);
let url = '/api/local/compose/media/update/' + self.ids[self.carouselCursor];
axios.post(url, data).then(res => {
self.media[self.carouselCursor].url = res.data.url;
self.pageLoading = false;
self.page++;
}).catch(err => {
});
});
break;
case 3:
case 4:
this.page++;
break;
}
},
rotate() {
this.$refs.cropper.rotate(90);
},
changeAspect(ratio) {
this.cropper.aspectRatio = ratio;
this.$refs.cropper.setAspectRatio(ratio);
},
maxSize() {
let limit = this.config.uploader.max_photo_size;
return limit / 1000 + ' MB';
@ -517,6 +696,75 @@ export default {
return formats.split(',').map(f => {
return ' ' + f.split('/')[1];
}).toString();
},
showTagCard() {
this.pageTitle = 'Tag People';
this.page = 'tagPeople';
},
showLocationCard() {
this.pageTitle = 'Add Location';
this.page = 'addLocation';
},
showAdvancedSettingsCard() {
this.pageTitle = 'Advanced Settings';
this.page = 'advancedSettings';
},
locationSearch(input) {
if (input.length < 1) { return []; };
let results = [];
return axios.get('/api/local/compose/location/search', {
params: {
q: input
}
}).then(res => {
return res.data;
});
},
getResultValue(result) {
return result.name + ', ' + result.country
},
onSubmitLocation(result) {
this.place = result;
this.pageTitle = '';
this.page = 4;
return;
},
goBack() {
this.pageTitle = '';
if(this.page == 'addToStory') {
this.page = 1;
} else {
this.namedPages.indexOf(this.page) != -1 ? this.page = 4 : this.page--;
}
},
showVisibilityCard() {
this.pageTitle = 'Post Visibility';
this.page = 'visibility';
},
showAddToStoryCard() {
this.pageTitle = 'Add to Story';
this.page = 'addToStory';
},
toggleVisibility(state) {
let tags = {
public: 'Public',
private: 'Followers Only',
unlisted: 'Unlisted'
}
this.visibility = state;
this.visibilityTag = tags[state];
this.pageTitle = '';
this.page = 4;
}
}
}

View File

@ -11,7 +11,7 @@
<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>
</a>
<a v-for="(category, index) in categories" :key="index+'_cat_'" class="bg-dark rounded d-inline-flex align-items-end justify-content-center mr-3 box-shadow card-disc" :href="category.url" :style="'background: linear-gradient(rgba(0, 0, 0, 0.3),rgba(0, 0, 0, 0.3)),url('+category.thumb+');'">
<a v-for="(category, index) in categories" :key="index+'_cat_'" class="bg-dark rounded d-inline-flex align-items-end justify-content-center mr-3 box-shadow card-disc text-decoration-none" :href="category.url" :style="'background: linear-gradient(rgba(0, 0, 0, 0.3),rgba(0, 0, 0, 0.3)),url('+category.thumb+');'">
<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p>
</a>

View File

@ -14,14 +14,19 @@
<div class="card card-md-rounded-0 status-container orientation-unknown shadow-none border">
<div class="row px-0 mx-0">
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div class="status-avatar mr-2">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
<div class="d-flex">
<div class="status-avatar mr-2" @click="redirect(statusProfileUrl)">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(statusProfileUrl)">{{ statusUsername }}</span>
<span v-if="status.account.is_admin" class="fa-stack" title="Admin Account" data-toggle="tooltip" style="height:1em; line-height:1em; max-width:19px;">
<i class="fas fa-certificate text-primary fa-stack-1x"></i>
<i class="fas fa-check text-white fa-sm fa-stack-1x" style="font-size:7px;"></i>
</span>
<p v-if="loaded && status.place != null" class="small mb-0 cursor-pointer text-truncate" style="color:#718096" @click="redirect('/discover/places/' + status.place.id + '/' + status.place.slug)">{{status.place.name}}, {{status.place.country}}</p>
</div>
</a>
</div>
<div v-if="user != false" class="float-right">
<div class="post-actions">
<div class="dropdown">
@ -74,14 +79,19 @@
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
<a :href="statusProfileUrl" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div class="status-avatar mr-2">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;">
<div class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" :title="statusUsername">
<div class="status-avatar mr-2" @click="redirect(statusProfileUrl)">
<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{ statusUsername }}</span>
<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(statusProfileUrl)">{{ statusUsername }}</span>
<span v-if="status.account.is_admin" class="fa-stack" title="Admin Account" data-toggle="tooltip" style="height:1em; line-height:1em; max-width:19px;">
<i class="fas fa-certificate text-primary fa-stack-1x"></i>
<i class="fas fa-check text-white fa-sm fa-stack-1x" style="font-size:7px;"></i>
</span>
<p v-if="loaded && status.place != null" class="small mb-0 cursor-pointer text-truncate" style="color:#718096" @click="redirect('/discover/places/' + status.place.id + '/' + status.place.slug)">{{status.place.name}}, {{status.place.country}}</p>
</div>
</a>
</div>
<div class="float-right">
<div class="post-actions">
<div v-if="user != false" class="dropdown">
@ -183,6 +193,7 @@
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 @click="lightbox(status.media_attachments[0])" class="fas fa-expand m-0 cursor-pointer"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
</div>
<div class="reaction-counts font-weight-bold mb-0">
@ -266,38 +277,123 @@
</div>
<div class="bg-white">
<div class="container">
<div class="row py-5">
<div class="col-12 col-md-8">
<div class="reactions py-2">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary float-right cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn float-right cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<div class="row pb-5">
<div class="col-12 col-md-8 py-4">
<div class="reactions d-flex align-items-center">
<div class="text-center mr-5">
<div v-bind:class="[reactions.liked ? 'fas fa-heart text-danger m-0 cursor-pointer' : 'far fa-heart m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus" style="font-size:1.575rem">
</div>
<div class="like-count font-weight-bold mt-2 rounded border" style="cursor:pointer;" v-on:click="likesModal">{{status.favourites_count || 0}}</div>
</div>
<div class="text-center">
<div v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'h3 far fa-share-square m-0 text-primary cursor-pointer' : 'h3 far fa-share-square m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus">
</div>
<div class="share-count font-weight-bold mt-2 rounded border" v-if="status.visibility == 'public'" style="cursor:pointer;" v-on:click="sharesModal">{{status.reblogs_count || 0}}</div>
</div>
</div>
<div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</span>
<span v-if="status.visibility == 'public'" class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
</div>
<hr>
<div class="media align-items-center">
<img :src="statusAvatar" class="rounded-circle shadow-lg mr-3" alt="avatar" width="72px" height="72px">
<div class="media-body lead">
by <a :href="statusProfileUrl">{{statusUsername}}</a>
<!-- <div class="reaction-counts font-weight-bold mb-0">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
<span v-if="status.visibility == 'public'" class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
</div> -->
<div class="media align-items-center mt-3">
<div class="media-body">
<h2 class="font-weight-bold">
{{status.content.length < 100 ? status.content.slice(0,100) : 'Untitled Post'}}
</h2>
<p class="lead mb-0">
by <a :href="statusProfileUrl">{{statusUsername}}</a>
<!-- <span class="px-1 text-lighter"></span>
<a class="font-weight-bold small" href="#">Follow</a> -->
</p>
</div>
<img :src="statusAvatar" class="rounded-circle border mr-3" alt="avatar" width="72px" height="72px">
</div>
<hr>
<div>
<p class="lead"><i class="far fa-clock"></i> {{timestampFormat()}}</p>
<div class="lead" v-html="status.content"></div>
<p class="lead">
<span v-if="status.place" class="text-truncate">
<i class="fas fa-map-marker-alt text-lighter mr-3"></i> {{status.place.name}}, {{status.place.country}}
</span>
<span v-once class="float-right">
<i class="far fa-clock text-lighter mr-3"></i> {{timeAgo(status.created_at)}} ago
</span>
</p>
<!-- <div v-if="status.content.length > 100" class="lead" v-html="status.content"></div> -->
<!-- <div class="pt-4">
<p class="lead">
<span class="mr-3"><i class="fas fa-camera text-lighter"></i></span>
<span>Nikon D850</span>
</p>
<p class="lead">
<span class="mr-3"><i class="fas fa-ruler-horizontal text-lighter"></i></span>
<span>200-500mm</span>
</p>
<p class="lead">
<span class="mr-3"><i class="fas fa-stream text-lighter"></i></span>
<span>500mm <span class="px-1"></span> ƒ/7.1 <span class="px-1"></span> 1/1600s <span class="px-1"></span> ISO 800</span>
</p>
</div> -->
<div v-if="status.tags" class="pt-4">
<p class="lead">
<a v-for="(tag, index) in status.tags" class="btn btn-outline-dark mr-1 mb-1" :href="tag.url + '?src=mp'">{{tag.name}}</a>
</p>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div v-if="status.comments_disabled" class="bg-light p-5 text-center lead">
<p class="mb-0">Comments have been disabled on this post.</p>
</div>
<div class="col-12 col-md-4 pt-4 pl-md-3">
<p class="lead font-weight-bold">Comments</p>
<div v-if="user" class="moment-comments">
<div class="form-group">
<textarea class="form-control" rows="3" placeholder="Add a comment ..." v-model="replyText"></textarea>
<p style="padding-top:4px;">
<span class="small text-lighter font-weight-bold">
{{replyText.length}}/{{config.uploader.max_caption_length}}
</span>
<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']"
:disabled="replyText.length < 2"
@click="postReply"
>Post</button>
</p>
</div>
<hr>
</div>
<div class="comment mt-3" style="max-height: 500px; overflow-y: auto;">
<div v-for="(reply, index) in results" :key="'tl' + reply.id + '_' + index" class="media mb-3">
<img :src="reply.account.avatar" class="rounded-circle border mr-3" alt="avatar" width="32px" height="32px">
<div class="media-body">
<div class="d-flex justify-content-between">
<span class="font-weight-bold">{{reply.account.username}}</span>
<span class="small">
<a class="text-lighter text-decoration-none" :href="reply.url">{{timeAgo(reply.created_at)}}</a>
</span>
</div>
<p v-html="reply.content" style="word-break: break-all;"></p>
</div>
</div>
<!-- <div class="media mb-3">
<img :src="statusAvatar" class="rounded-circle border mr-3" alt="avatar" width="32px" height="32px">
<div class="media-body">
<div class="d-flex justify-content-between">
<span class="font-weight-bold">mona</span>
<span class="text-lighter small">2h ago</span>
</div>
<p>Stunning my friend!</p>
</div>
</div>
<div class="media mb-3">
<img :src="statusAvatar" class="rounded-circle border mr-3" alt="avatar" width="32px" height="32px">
<div class="media-body">
<div class="d-flex justify-content-between">
<span class="font-weight-bold">Andre</span>
<span class="text-lighter small">3h ago</span>
</div>
<p>Wow</p>
</div>
</div> -->
</div>
</div>
</div>
</div>
@ -378,8 +474,8 @@
size="lg"
body-class="p-0"
>
<div v-if="lightboxMedia" :class="lightboxMedia.filter_class">
<img :src="lightboxMedia.url" class="img-fluid" style="min-height: 100%; min-width: 100%">
<div v-if="lightboxMedia" >
<img :src="lightboxMedia.url" :class="lightboxMedia.filter_class + ' img-fluid'" style="min-height: 100%; min-width: 100%">
</div>
</b-modal>
</div>
@ -472,6 +568,7 @@ export default {
],
data() {
return {
config: window.App.config,
status: false,
media: {},
user: false,
@ -787,11 +884,18 @@ export default {
item: this.replyingToId,
comment: this.replyText
}
this.replyText = '';
axios.post('/i/comment', data)
.then(function(res) {
let entity = res.data.entity;
if(entity.in_reply_to_id == self.status.id) {
self.results.push(entity);
if(self.profileLayout == 'metro') {
self.results.push(entity);
} else {
self.results.unshift(entity);
}
let elem = $('.status-comments')[0];
elem.scrollTop = elem.clientHeight;
} else {
@ -801,7 +905,6 @@ export default {
el.reply_count = el.reply_count + 1;
}
}
self.replyText = '';
});
},
@ -847,7 +950,9 @@ export default {
axios.get(url)
.then(response => {
let self = this;
this.results = _.reverse(response.data.data);
this.results = this.profileLayout == 'metro' ?
_.reverse(response.data.data) :
response.data.data;
this.pagination = response.data.meta.pagination;
if(this.results.length > 0) {
$('.load-more-link').removeClass('d-none');
@ -1054,6 +1159,10 @@ export default {
reply.thread = true;
});
}
},
redirect(url) {
window.location.href = url;
}
},

View File

@ -86,10 +86,9 @@
<div class="profile-details">
<div class="d-none d-md-flex username-bar pb-3 align-items-center">
<span class="font-weight-ultralight h3 mb-0">{{profile.username}}</span>
<span class="pl-1 pb-2" v-if="profile.is_admin" title="Admin Account" data-toggle="tooltip">
<i class="fas fa-certificate fa-lg text-primary">
</i>
<i class="fas fa-check text-white fa-sm" style="font-size:9px;margin-left: -1.1rem;padding-bottom: 0.6rem;"></i>
<span class="pl-1 pb-2 fa-stack" v-if="profile.is_admin" title="Admin Account" data-toggle="tooltip">
<i class="fas fa-certificate fa-lg text-primary fa-stack-1x"></i>
<i class="fas fa-check text-white fa-sm fa-stack-1x" style="font-size:9px;"></i>
</span>
<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
<span class="pl-4" v-if="relationship.following == true">
@ -166,7 +165,7 @@
<div class="row" v-if="mode == 'grid'">
<div class="col-4 p-1 p-md-3" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square">
<div :class="[s.sensitive ? 'square' : 'square ' + s.media_attachments[0].filter_class]">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
@ -330,7 +329,7 @@
:gutter="{default: '5px'}"
>
<div class="p-1" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="s.url">
<a :class="[s.sensitive ? 'card info-overlay card-md-border-0' : s.media_attachments[0].filter_class + ' card info-overlay card-md-border-0']" :href="s.url">
<img :src="previewUrl(s)" class="img-fluid w-100">
</a>
</div>
@ -375,7 +374,7 @@
</div>
<div v-if="following.length == 0" class="list-group-item border-0">
<div class="list-group-item border-0">
<p class="p-3 text-center mb-0 lead">You are not following anyone.</p>
<p class="p-3 text-center mb-0 lead"></p>
</div>
</div>
<div v-if="followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
@ -571,6 +570,17 @@
this.mode = u.get('t');
}
}
},
mounted() {
let u = new URLSearchParams(window.location.search);
if(u.has('md') && u.get('md') == 'followers') {
this.followersModal();
}
if(u.has('md') && u.get('md') == 'following') {
this.followingModal();
}
},
updated() {
@ -780,33 +790,6 @@
}
},
fetchStatusComments(status, card) {
axios.get('/api/v2/status/'+status.id+'/replies')
.then(res => {
let comments = card.querySelectorAll('.comments')[0];
let data = res.data;
data.forEach(function(i, k) {
let username = document.createElement('a');
username.classList.add('font-weight-bold');
username.classList.add('text-dark');
username.classList.add('mr-2');
username.setAttribute('href', i.account.url);
username.textContent = i.account.username;
let text = document.createElement('span');
text.innerHTML = i.content;
let comment = document.createElement('p');
comment.classList.add('read-more');
comment.classList.add('mb-0');
comment.appendChild(username);
comment.appendChild(text);
comments.appendChild(comment);
});
}).catch(err => {
})
},
fetchRelationships() {
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
return;
@ -878,7 +861,6 @@
});
},
unblockProfile(status = null) {
if($('body').hasClass('loggedIn') == false) {
return;
@ -912,54 +894,6 @@
});
},
commentSubmit(status, $event) {
if($('body').hasClass('loggedIn') == false) {
return;
}
let id = status.id;
let form = $event.target;
let input = $(form).find('input[name="comment"]');
let comment = input.val();
let comments = form.parentElement.parentElement.getElementsByClassName('comments')[0];
axios.post('/i/comment', {
item: id,
comment: comment
}).then(res => {
input.val('');
input.blur();
let username = document.createElement('a');
username.classList.add('font-weight-bold');
username.classList.add('text-dark');
username.classList.add('mr-2');
username.setAttribute('href', this.user.url);
username.textContent = this.user.username;
let text = document.createElement('span');
text.innerHTML = comment;
let wrapper = document.createElement('p');
wrapper.classList.add('read-more');
wrapper.classList.add('mb-0');
wrapper.appendChild(username);
wrapper.appendChild(text);
comments.insertBefore(wrapper, comments.firstChild);
});
},
statusModal(status) {
this.modalStatus = status;
this.$refs.statusModalRef.show();
},
masonryOrientation(status) {
let o = status.media_attachments[0].orientation;
if(!o) {
o = 'square';
}
return o;
},
followProfile() {
if($('body').hasClass('loggedIn') == false) {
return;
@ -986,56 +920,60 @@
followingModal() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
window.location.href = encodeURI('/login?next=/' + this.profileUsername + '/');
return;
}
if(this.profileSettings.following.list == false) {
return;
}
if(this.following.length > 0) {
if(this.followingCursor > 1) {
this.$refs.followingModal.show();
return;
} else {
axios.get('/api/v1/accounts/'+this.profileId+'/following', {
params: {
page: this.followingCursor
}
})
.then(res => {
this.following = res.data;
this.followingCursor++;
if(res.data.length < 10) {
this.followingMore = false;
}
});
this.$refs.followingModal.show();
return;
}
axios.get('/api/v1/accounts/'+this.profile.id+'/following', {
params: {
page: this.followingCursor
}
})
.then(res => {
this.following = res.data;
this.followingCursor++;
if(res.data.length < 10) {
this.followingMore = false;
}
});
this.$refs.followingModal.show();
},
followersModal() {
if($('body').hasClass('loggedIn') == false) {
window.location.href = encodeURI('/login?next=/' + this.profile.username + '/');
window.location.href = encodeURI('/login?next=/' + this.profileUsername + '/');
return;
}
if(this.profileSettings.followers.list == false) {
return;
}
if(this.followers.length > 0) {
if(this.followerCursor > 1) {
this.$refs.followerModal.show();
return;
} else {
axios.get('/api/v1/accounts/'+this.profileId+'/followers', {
params: {
page: this.followerCursor
}
})
.then(res => {
this.followers.push(...res.data);
this.followerCursor++;
if(res.data.length < 10) {
this.followerMore = false;
}
})
this.$refs.followerModal.show();
return;
}
axios.get('/api/v1/accounts/'+this.profile.id+'/followers', {
params: {
page: this.followerCursor
}
})
.then(res => {
this.followers = res.data;
this.followerCursor++;
if(res.data.length < 10) {
this.followerMore = false;
}
})
this.$refs.followerModal.show();
},
followingLoadMore() {

View File

@ -66,15 +66,28 @@
</div>
</div>
</div>
<div class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
<div v-if="!modes.distractionFree" class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
<a class="username font-weight-bold pl-2 text-dark" v-bind:href="status.account.url">
{{status.account.username}}
</a>
<div class="pl-2">
<!-- <a class="d-block username font-weight-bold text-dark" v-bind:href="status.account.url" style="line-height:0.5;"> -->
<a class="username font-weight-bold text-dark text-decoration-none" v-bind:href="status.account.url">
{{status.account.username}}
</a>
<span v-if="status.account.is_admin" class="fa-stack" title="Admin Account" data-toggle="tooltip" style="height:1em; line-height:1em; max-width:19px;">
<i class="fas fa-certificate text-primary fa-stack-1x"></i>
<i class="fas fa-check text-white fa-sm fa-stack-1x" style="font-size:7px;"></i>
</span>
<span v-if="scope != 'home' && status.account.id != profile.id && status.account.relationship">
<span class="px-1"></span>
<span :class="'font-weight-bold cursor-pointer ' + [status.account.relationship.following == true ? 'text-muted' : 'text-primary']" @click="followAction(status)">{{status.account.relationship.following == true ? 'Following' : 'Follow'}}</span>
</span>
<a v-if="status.place" class="d-block small text-decoration-none" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" style="color:#718096">{{status.place.name}}, {{status.place.country}}</a>
</div>
<div class="text-right" style="flex-grow:1;">
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu(status)">
<span class="fas fa-ellipsis-h text-dark"></span>
<span class="fas fa-ellipsis-h text-lighter"></span>
</button>
<!-- <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
@ -114,7 +127,7 @@
</div>
</div>
<div class="postPresenterContainer" v-on:dblclick="likeStatus(status)">
<div class="postPresenterContainer" @click="lightbox(status)">
<div v-if="status.pf_type === 'photo'" class="w-100">
<photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
</div>
@ -141,10 +154,13 @@
</div>
<div class="card-body">
<div v-if="!modes.distractionFree" class="reactions my-1">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
<div v-if="!modes.distractionFree" class="reactions my-1 pb-2">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn text-lighter cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment text-lighter pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[status.reblogged ? 'fas fa-retweet pr-3 m-0 text-primary cursor-pointer' : 'fas fa-retweet pr-3 m-0 text-lighter share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
<span class="float-right">
<h3 class="fas fa-expand pr-3 m-0 cursor-pointer text-lighter" v-on:click="lightbox(status)"></h3>
</span>
</div>
<div class="likes font-weight-bold" v-if="expLc(status) == true && !modes.distractionFree">
@ -165,8 +181,11 @@
<span v-html="reply.content"></span>
</span>
<span class="mb-0" style="min-width:38px">
<span v-on:click="likeStatus(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
<post-menu :status="reply" :profile="profile" size="sm" :modal="'true'" :feed="feed" class="d-inline-flex pl-2"></post-menu>
<span v-on:click="likeStatus(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger cursor-pointer':'far fa-heart fa-sm text-lighter cursor-pointer']"></i></span>
<!-- <post-menu :status="reply" :profile="profile" size="sm" :modal="'true'" :feed="feed" class="d-inline-flex pl-2"></post-menu> -->
<span class="text-lighter pl-2 cursor-pointer" @click="ctxMenu(reply)">
<span class="fas fa-ellipsis-v text-lighter"></span>
</span>
</span>
</p>
</div>
@ -209,12 +228,12 @@
</div>
</div>
</div>
<div v-if="!loading && feed.length > 0">
<div v-if="!loading && feed.length">
<div class="card">
<div class="card-body">
<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 posts found</div>
<div slot="no-results" class="font-weight-bold">No more posts to load</div>
</infinite-loading>
</div>
</div>
@ -252,22 +271,36 @@
</div>
</div>
</div>
<!-- <div class="card-footer bg-white py-1 d-none">
<div class="card-footer bg-transparent border-0 mt-2 py-1">
<div class="d-flex justify-content-between text-center">
<span class="pl-3 cursor-pointer" v-on:click="redirect(profile.url)">
<span class="cursor-pointer" @click="redirect(profile.url)">
<p class="mb-0 font-weight-bold">{{profile.statuses_count}}</p>
<p class="mb-0 small text-muted">Posts</p>
</span>
<span class="cursor-pointer" v-on:click="followersModal()">
<span class="cursor-pointer" @click="redirect(profile.url+'?md=followers')">
<p class="mb-0 font-weight-bold">{{profile.followers_count}}</p>
<p class="mb-0 small text-muted">Followers</p>
</span>
<span class="pr-3 cursor-pointer" v-on:click="followingModal()">
<span class="cursor-pointer" @click="redirect(profile.url+'?md=following')">
<p class="mb-0 font-weight-bold">{{profile.following_count}}</p>
<p class="mb-0 small text-muted">Following</p>
</span>
</div>
</div> -->
</div>
</div>
</div>
<div v-if="showTips" class="mb-4 card-tips">
<div class="card border shadow-none mb-3" style="max-width: 18rem;">
<div class="card-body">
<div class="card-title">
<span class="font-weight-bold">Tip: Hide follower counts</span>
<span class="float-right cursor-pointer" @click.prevent="hideTips()"><i class="fas fa-times text-lighter"></i></span>
</div>
<p class="card-text">
<span style="font-size:13px;">You can hide followers or following count and lists on your profile.</span>
<br><a href="/settings/privacy/" class="small font-weight-bold">Privacy Settings</a></p>
</div>
</div>
</div>
@ -400,9 +433,25 @@
<!-- <div 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 ref="ctxShareModal"
id="ctx-share-modal"
title="Share"
@ -444,8 +493,8 @@
size="lg"
body-class="p-0"
>
<div v-if="lightboxMedia" :class="lightboxMedia.filter_class">
<img :src="lightboxMedia.url" class="img-fluid" style="min-height: 100%; min-width: 100%">
<div v-if="lightboxMedia" :class="lightboxMedia.filter_class" class="w-100 h-100">
<img :src="lightboxMedia.url" style="max-height: 100%; max-width: 100%">
</div>
</b-modal>
</div>
@ -524,7 +573,8 @@
ctxMenuStatus: false,
ctxMenuRelationship: false,
ctxEmbedPayload: false,
copiedEmbed: false
copiedEmbed: false,
showTips: true,
}
},
@ -563,6 +613,10 @@
this.modes.distractionFree = false;
}
if(localStorage.getItem('metro-tips') == 'false') {
this.showTips = false;
}
this.$nextTick(function () {
$('[data-toggle="tooltip"]').tooltip()
});
@ -623,13 +677,19 @@
this.max_id = Math.min(...ids);
$('.timeline .pagination').removeClass('d-none');
this.loading = false;
this.fetchHashtagPosts();
if(this.feed.length == 6) {
this.fetchHashtagPosts();
this.fetchTimelineApi();
} else {
this.fetchHashtagPosts();
}
}).catch(err => {
});
},
infiniteTimeline($state) {
if(this.loading) {
$state.complete();
return;
}
let apiUrl = false;
@ -649,7 +709,7 @@
axios.get(apiUrl, {
params: {
max_id: this.max_id,
limit: 6
limit: 9
},
}).then(res => {
if (res.data.length && this.loading == false) {
@ -669,6 +729,9 @@
} else {
$state.complete();
}
}).catch(err => {
this.loading = false;
$state.complete();
});
},
@ -826,7 +889,7 @@
});
},
deletePost(status, index) {
deletePost(status) {
if($('body').hasClass('loggedIn') == false || this.ownerOrAdmin(status) == false) {
return;
}
@ -841,8 +904,8 @@
}).then(res => {
this.feed = this.feed.filter(s => {
return s.id != status.id;
})
swal('Success', 'You have successfully deleted this post', 'success');
});
this.$refs.ctxModal.hide();
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
@ -916,6 +979,7 @@
}
});
break;
case 'unlisted':
msg = 'Are you sure you want to unlist from timelines for ' + username + ' ?';
swal({
@ -1073,8 +1137,8 @@
});
},
lightbox(src) {
this.lightboxMedia = src;
lightbox(status) {
this.lightboxMedia = status.media_attachments[0];
this.$refs.lightboxModal.show();
},
@ -1114,13 +1178,26 @@
});
},
followModalAction(id, index, type = 'following') {
followAction(status) {
let id = status.account.id;
axios.post('/i/follow', {
item: id
}).then(res => {
if(type == 'following') {
this.following.splice(index, 1);
this.feed.forEach(s => {
if(s.account.id == id) {
s.account.relationship.following = !s.account.relationship.following;
}
});
let username = status.account.acct;
if(status.account.relationship.following) {
swal('Follow successful!', 'You are now following ' + username, 'success');
} else {
swal('Unfollow successful!', 'You are no longer following ' + username, 'success');
}
}).catch(err => {
if(err.response.data.message) {
swal('Error', err.response.data.message, 'error');
@ -1190,7 +1267,6 @@
},
fetchHashtagPosts() {
axios.get('/api/local/discover/tag/list')
.then(res => {
let tags = res.data;
@ -1210,7 +1286,6 @@
}
})
})
},
ctxMenu(status) {
@ -1255,9 +1330,16 @@
},
ctxMenuFollow() {
let id = this.ctxMenuStatus.account.id;
axios.post('/i/follow', {
item: this.ctxMenuStatus.account.id
item: id
}).then(res => {
this.feed.forEach(s => {
if(s.account.id == id) {
s.account.relationship.following = !s.account.relationship.following;
}
});
let username = this.ctxMenuStatus.account.acct;
this.closeCtxMenu();
setTimeout(function() {
@ -1267,10 +1349,21 @@
},
ctxMenuUnfollow() {
let id = this.ctxMenuStatus.account.id;
axios.post('/i/follow', {
item: this.ctxMenuStatus.account.id
item: id
}).then(res => {
this.feed.forEach(s => {
if(s.account.id == id) {
s.account.relationship.following = !s.account.relationship.following;
}
});
let username = this.ctxMenuStatus.account.acct;
if(this.scope == 'home') {
this.feed = this.feed.filter(s => {
return s.account.id != this.ctxMenuStatus.account.id;
});
}
this.closeCtxMenu();
setTimeout(function() {
swal('Unfollow successful!', 'You are no longer following ' + username, 'success');
@ -1300,6 +1393,26 @@
ctxCopyEmbed() {
navigator.clipboard.writeText(this.ctxEmbedPayload);
this.$refs.ctxEmbedModal.hide();
},
ctxModMenuShow() {
this.$refs.ctxModal.hide();
this.$refs.ctxModModal.show();
},
ctxModMenu() {
this.$refs.ctxModal.hide();
},
ctxModMenuClose() {
this.$refs.ctxModal.hide();
this.$refs.ctxModModal.hide();
},
hideTips() {
this.showTips = false;
let ls = window.localStorage;
ls.setItem('metro-tips', false);
}
}

4
resources/assets/js/micro.js vendored Normal file
View File

@ -0,0 +1,4 @@
Vue.component(
'micro-ui',
require('./components/Micro.vue').default
);

View File

@ -534,6 +534,11 @@ details summary::-webkit-details-marker {
color:#B8C2CC !important;
}
.btn-outline-lighter {
color: #B8C2CC !important;
border-color: #B8C2CC !important;
}
.cursor-pointer {
cursor: pointer;
}

View File

@ -8,6 +8,7 @@ return [
'network' => 'Network',
'discover' => 'Discover',
'viewMyProfile' => 'View my profile',
'myProfile' => 'My Profile',
'myTimeline' => 'My Timeline',
'publicTimeline' => 'Public Timeline',
'remoteFollow' => 'Remote Follow',

View File

@ -0,0 +1,22 @@
@extends('layouts.app')
@section('content')
<div class="container mt-5">
<div class="col-12">
<p class="font-weight-bold text-lighter text-uppercase">Cities in {{$places->first()->country}}</p>
<div class="card border shadow-none">
<div class="card-body row pl-md-5 ml-md-5">
@foreach($places as $place)
<div class="col-12 col-md-4 mb-2">
<a href="{{$place->cityUrl()}}" class="text-dark pr-3 b-3">{{$place->name}}</a>
</div>
@endforeach
</div>
<div class="card-footer bg-white pb-0 d-flex justify-content-center">
{{$places->links()}}
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,22 @@
@extends('layouts.app')
@section('content')
<div class="container mt-5">
<div class="col-12">
<p class="font-weight-bold text-lighter text-uppercase">Countries</p>
<div class="card border shadow-none">
<div class="card-body row pl-md-5 ml-md-5">
@foreach($places as $place)
<div class="col-12 col-md-4 mb-2">
<a href="{{$place->countryUrl()}}" class="text-dark pr-3 b-3">{{$place->country}}</a>
</div>
@endforeach
</div>
<div class="card-footer bg-white pb-0 d-flex justify-content-center">
{{$places->links()}}
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,48 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="profile-header row my-5">
<div class="col-12 col-md-2">
<div class="profile-avatar">
<div class="bg-pixelfed mb-3 d-flex align-items-center justify-content-center display-4 font-weight-bold text-white" style="width: 132px; height: 132px; border-radius: 100%"><i class="fas fa-map-pin"></i></div>
</div>
</div>
<div class="col-12 col-md-9 d-flex align-items-center">
<div class="profile-details">
<div class="username-bar pb-2 d-flex align-items-center">
<div class="ml-4">
<p class="h3 font-weight-lighter">{{$place->name}}, {{$place->country}}</p>
<p class="small text-muted">({{$place->lat}}, {{$place->long}})</p>
</div>
</div>
</div>
</div>
</div>
<div class="tag-timeline">
<div class="row">
@if($posts->count() > 0)
@foreach($posts as $status)
<div class="col-4 p-0 p-sm-2 p-md-3">
<a class="card info-overlay card-md-border-0" href="{{$status->url()}}">
<div class="square {{$status->firstMedia()->filter_class}}">
<div class="square-content" style="background-image: url('{{$status->thumb()}}')"></div>
</div>
</a>
</div>
@endforeach
@else
<div class="col-12 bg-white p-5 border">
<p class="lead text-center text-dark mb-0">No results for this location</p>
</div>
@endif
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">App.boot();</script>
@endpush

View File

@ -7,5 +7,5 @@
@push('scripts')
<script type="text/javascript" src="{{ mix('js/hashtag.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">$(document).ready(function(){new Vue({el: '#content'});});</script>
<script type="text/javascript">App.boot();</script>
@endpush

View File

@ -8,6 +8,7 @@
<a href="{{route('site.help')}}" class="text-primary pr-3">{{__('site.help')}}</a>
<a href="{{route('site.terms')}}" class="text-primary pr-3">{{__('site.terms')}}</a>
<a href="{{route('site.privacy')}}" class="text-primary pr-3">{{__('site.privacy')}}</a>
<a href="{{route('discover.places')}}" class="text-primary pr-3">Places</a>
<a href="{{route('site.language')}}" class="text-primary pr-3">{{__('site.language')}}</a>
<a href="https://pixelfed.org" class="text-muted float-right" rel="noopener" title="version {{config('pixelfed.version')}}" data-toggle="tooltip">Powered by Pixelfed</a>
</p>

View File

@ -1,59 +1,48 @@
<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
<nav class="navbar navbar-expand navbar-light navbar-laravel shadow-none border-bottom border sticky-top py-1">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="{{ route('timeline.personal') }}" title="Logo">
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2">
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager">
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config('app.name', 'pixelfed') }}</span>
</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
@auth
<ul class="navbar-nav d-none d-md-block mx-auto pr-3">
<div class="collapse navbar-collapse">
@auth
<ul class="navbar-nav d-none d-md-block mx-auto">
<form class="form-inline search-bar" method="get" action="/i/results">
<div class="input-group">
<input class="form-control" name="q" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off" required>
<div class="input-group-append">
<button class="btn btn-outline-primary" type="submit"><i class="fas fa-search"></i></button>
</div>
</div>
<input class="form-control form-control-sm" name="q" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off" required style="line-height: 0.6;width:200px">
</form>
</ul>
@endauth
@endauth
@guest
@guest
<ul class="navbar-nav ml-auto">
<li><a class="nav-link font-weight-bold text-primary" href="{{ route('login') }}" title="Login">{{ __('Login') }}</a></li>
@if(config('pixelfed.open_registration'))
<li><a class="nav-link font-weight-bold" href="{{ route('register') }}" title="Register">{{ __('Register') }}</a></li>@endif
@else
<ul class="navbar-nav ml-auto">
<li>
<a class="nav-link font-weight-bold text-primary" href="{{ route('login') }}" title="Login">
{{ __('Login') }}
</a>
</li>
@if(config('pixelfed.open_registration'))
<li>
<a class="nav-link font-weight-bold" href="{{ route('register') }}" title="Register">
{{ __('Register') }}
</a>
</li>
@endif
@else
<div class="ml-auto">
<ul class="navbar-nav">
<div class="d-none d-md-block">
<li class="nav-item px-md-2">
<a class="nav-link font-weight-bold {{request()->is('/') ?'text-dark':'text-muted'}}" href="/" title="Home Timeline" data-toggle="tooltip" data-placement="bottom">
<a class="nav-link font-weight-bold text-muted" href="/" title="Home Timeline" data-toggle="tooltip" data-placement="bottom">
<i class="fas fa-home fa-lg"></i>
</a>
</li>
</div>
{{-- <div class="d-none d-md-block">
<li class="nav-item px-md-2">
<a class="nav-link font-weight-bold {{request()->is('timeline/public') ?'text-primary':''}}" href="/timeline/public" title="Public Timeline" data-toggle="tooltip" data-placement="bottom">
<i class="far fa-map fa-lg"></i>
</a>
</li>
</div> --}}
<li class="d-block d-md-none">
</li>
{{-- <li class="pr-2">
<a class="nav-link font-weight-bold {{request()->is('timeline/network') ?'text-primary':''}}" href="{{route('timeline.network')}}" title="Network Timeline">
<i class="fas fa-globe fa-lg"></i>
</a>
</li> --}}
<li class="d-block d-md-none"></li>
<div class="d-none d-md-block">
<li class="nav-item px-md-2">
<a class="nav-link font-weight-bold {{request()->is('*discover*') ?'text-dark':'text-muted'}}" href="{{route('discover')}}" title="Discover" data-toggle="tooltip" data-placement="bottom">
<a class="nav-link font-weight-bold text-muted" href="{{route('discover')}}" title="Discover" data-toggle="tooltip" data-placement="bottom">
<i class="far fa-compass fa-lg"></i>
</a>
</li>
@ -62,23 +51,20 @@
<li class="nav-item px-md-2">
<div title="Create new post" data-toggle="tooltip" data-placement="bottom">
<a href="{{route('compose')}}" class="nav-link" data-toggle="modal" data-target="#composeModal">
<i class="fas fa-camera-retro fa-lg text-primary"></i>
<i class="fas fa-camera-retro fa-lg text-muted"></i>
</a>
</div>
</li>
</div>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<li class="nav-item dropdown ml-2">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="User Menu" data-toggle="tooltip" data-placement="bottom">
<img class="rounded-circle box-shadow mr-1" src="{{Auth::user()->profile->avatarUrl()}}" width="26px" height="26px">
<i class="far fa-user fa-lg text-muted"></i>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item font-weight-ultralight text-truncate" href="{{Auth::user()->url()}}">
<img class="rounded-circle box-shadow mr-1" src="{{Auth::user()->profile->avatarUrl()}}" width="26px" height="26px">
&commat;{{Auth::user()->username}}
<p class="small mb-0 text-muted text-center">{{__('navmenu.viewMyProfile')}}</p>
<a class="dropdown-item font-weight-bold" href="/i/me">
<span class="far fa-user pr-1"></span>
{{__('navmenu.myProfile')}}
</a>
<div class="dropdown-divider"></div>
<a class="d-block d-md-none dropdown-item font-weight-bold" href="{{route('discover')}}">
@ -98,18 +84,6 @@
<span class="far fa-map pr-1"></span>
{{__('navmenu.publicTimeline')}}
</a>
{{-- <a class="dropdown-item font-weight-bold" href="{{route('timeline.network')}}">
<span class="fas fa-globe pr-1"></span>
Network Timeline
</a> --}}
{{-- <a class="dropdown-item font-weight-bold" href="{{route('messages')}}">
<span class="far fa-envelope pr-1"></span>
{{__('navmenu.directMessages')}}
</a>
<a class="dropdown-item font-weight-bold" href="{{route('account.circles')}}">
<span class="far fa-circle pr-1"></span>
{{__('Circles')}}
</a>--}}
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="{{route('settings')}}">
<span class="fas fa-cog pr-1"></span>
@ -134,7 +108,8 @@
</form>
</div>
</li>
@endguest
</div>
@endguest
</ul>
</div>
</div>

View File

@ -0,0 +1,64 @@
@extends('settings.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold">Send Invite</h3>
<p class="lead">Invite friends or family to join you on <span class="font-weight-bold">{{config('pixelfed.domain.app')}}</span></p>
</div>
<hr>
@if(config('pixelfed.user_invites.limit.daily') != 0)
<div class="alert alert-warning">
<div class="font-weight-bold">Warning</div>
<p class="mb-0">You may only send {{config('pixelfed.user_invites.limit.daily')}} invite(s) per day.</p>
</div>
@endif
<form method="post">
@csrf
<div class="form-group">
<label>Email address</label>
<input type="email" class="form-control" name="email" placeholder="friend@example.org" autocomplete="off">
</div>
<div class="form-group">
<label>Message</label>
<textarea class="form-control" name="message" placeholder="Add an optional message" rows="2"></textarea>
<p class="help-text mb-0 text-right small text-muted"><span class="message-count">0</span>/<span class="message-limit">500</span></p>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="tos" name="tos">
<label class="form-check-label font-weight-bold small" for="tos">I confirm this invitation is not in violation of the <a href="{{route('site.terms')}}">Terms of Service</a> and <a href="{{route('site.privacy')}}">Privacy Policy</a>.</label>
</div>
<hr>
<p class="float-right">
<button type="submit" class="btn btn-primary font-weight-bold py-0 form-submit">Send Invite</button>
</p>
</form>
@endsection
@push('scripts')
<script type="text/javascript">
$('textarea[name="message"]').on('change keyup paste', function(e) {
let el = $(this);
let len = el.val().length;
let limit = $('.message-limit').text();
if(len > 100) {
el.attr('rows', '4');
}
if(len > limit) {
let diff = len - limit;
$('.message-count').addClass('text-danger').text('-'+diff);
$('.form-submit').attr('disabled','');
} else {
$('.message-count').removeClass('text-danger').text(len);
$('.form-submit').removeAttr('disabled');
}
});
</script>
@endpush

View File

@ -0,0 +1,45 @@
@extends('settings.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold">Invites</h3>
<p class="lead">Send email invites to your friends and family!</p>
</div>
<hr>
@if($invites->count() > 0)
<table class="table table-light">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Email</th>
<th scope="col">Valid For</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@foreach($invites as $invite)
<tr>
<th scope="row">{{$invite->id}}</th>
<td>{{$invite->email}}</td>
<td>{{$invite->message}}</td>
<td>
@if($invite->used_at == null)
<button class="btn btn-outline-danger btn-sm">Delete</button>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="d-flex align-items-center justify-content-center text-center pb-4">
<div>
<p class="pt-5"><i class="far fa-envelope-open text-lighter fa-6x"></i></p>
<p class="lead">You haven't invited anyone yet.</p>
<p><a class="btn btn-primary btn-lg py-0 font-weight-bold" href="{{route('settings.invites.create')}}">Invite someone</a></p>
<p class="font-weight-lighter text-muted">You have <b class="font-weight-bold text-dark">{{$limit - $used}}</b> invites left.</p>
</div>
</div>
@endif
@endsection

View File

@ -177,6 +177,11 @@
<label class="form-check-label font-weight-bold">Simple Mode (Timelines only)</label>
<p class="text-muted small help-text">An experimental content-first timeline layout</p>
</div>
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" id="show_tips">
<label class="form-check-label font-weight-bold">Show Tips</label>
<p class="text-muted small help-text">Show Tips on Timelines (Desktop Only)</p>
</div>
<div class="py-3">
<p class="font-weight-bold text-muted text-center">Discovery</p>
<hr>
@ -218,6 +223,10 @@ $(document).ready(function() {
$('#distraction_free').attr('checked', true);
}
if(localStorage.getItem('metro-tips') !== 'false') {
$('#show_tips').attr('checked', true);
}
$('#show_suggestions').on('change', function(e) {
if(e.target.checked) {
localStorage.removeItem('pf_metro_ui.exp.rec');
@ -241,6 +250,14 @@ $(document).ready(function() {
localStorage.removeItem('pf_metro_ui.exp.df');
}
});
$('#show_tips').on('change', function(e) {
if(e.target.checked) {
localStorage.setItem('metro-tips', true);
} else {
localStorage.removeItem('metro-tips');
}
});
});
</script>
@endpush

View File

@ -10,6 +10,8 @@
<p>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a>
<a class="btn btn-outline-primary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a>
{{-- <a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-keywords')}}">Blocked keywords</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-instances')}}">Blocked instances</a> --}}
</p>
</div>
@if($users->count() > 0)
@ -17,7 +19,7 @@
@foreach($users as $user)
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center font-weight-bold">
<span><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px">{{$user->emailUrl()}}</span>
<span><a href="{{$user->url()}}" class="text-decoration-none text-dark"><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px">{{$user->username}}</a></span>
<span class="btn-group">
<form method="post">
@csrf

View File

@ -10,6 +10,8 @@
<p>
<a class="btn btn-outline-primary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a>
{{-- <a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-keywords')}}">Blocked keywords</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-instances')}}">Blocked instances</a> --}}
</p>
</div>
@if($users->count() > 0)
@ -17,7 +19,7 @@
@foreach($users as $user)
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center font-weight-bold">
<span><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px">{{$user->emailUrl()}}</span>
<span><a href="{{$user->url()}}" class="text-decoration-none text-dark"><img class="rounded-circle mr-3" src="{{$user->avatarUrl()}}" width="32px">{{$user->username}}</a></span>
<span class="btn-group">
<form method="post">
@csrf

View File

@ -2,31 +2,32 @@
@section('content')
{{-- <div class="container mt-5">
<div class="compose-container container mt-5 d-none">
<div class="row">
<div class="col-12 col-md-6 offset-md-3">
<p class="lead text-center font-weight-bold">Compose New Post</p>
<p class="lead text-center">
<a href="javascript:void(0)" class="btn btn-primary font-weight-bold" data-toggle="modal" data-target="#composeModal">New Post</a>
</p>
<p class="lead text-center">
<a href="javascript:void(0)" class="btn btn-primary font-weight-bold" data-toggle="modal" data-target="#composeModal">Compose New Post</a>
</p>
</div>
</div>
</div> --}}
</div>
<div class="modal pr-0" tabindex="-1" role="dialog" id="composeModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<compose-classic></compose-classic>
</div>
</div>
<div class="modal-dialog" role="document">
<div class="modal-content">
<compose-classic></compose-classic>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript" src="{{ mix('js/compose-classic.js') }}"></script>
<script type="text/javascript">App.boot();</script>
<script type="text/javascript">
$('#composeModal').modal('show');
</script>
<script type="text/javascript">
App.boot();
$('#composeModal').modal('show');
$('.compose-container').removeClass('d-none');
</script>
@endpush

View File

@ -68,7 +68,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('/home', 'HomeController@index')->name('home');
Route::get('discover/c/{slug}', 'DiscoverController@showCategory');
Route::get('discover/personal', 'DiscoverController@showPersonal');
Route::redirect('discover/personal', '/discover');
Route::get('discover', 'DiscoverController@home')->name('discover');
Route::get('discover/loops', 'DiscoverController@showLoops');
@ -121,6 +121,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::delete('collection/{id}', 'CollectionController@delete')->middleware('throttle:maxCollectionsPerHour,60')->middleware('throttle:maxCollectionsPerDay,1440')->middleware('throttle:maxCollectionsPerMonth,43800');
Route::post('collection/{id}/publish', 'CollectionController@publish')->middleware('throttle:maxCollectionsPerHour,60')->middleware('throttle:maxCollectionsPerDay,1440')->middleware('throttle:maxCollectionsPerMonth,43800');
Route::get('profile/collections/{id}', 'CollectionController@getUserCollections');
Route::post('compose/media/update/{id}', 'MediaController@composeUpdate')->middleware('throttle:maxComposeMediaUpdatesPerHour,60')->middleware('throttle:maxComposeMediaUpdatesPerDay,1440')->middleware('throttle:maxComposeMediaUpdatesPerMonth,43800');
Route::get('compose/location/search', 'ApiController@composeLocationSearch');
});
Route::group(['prefix' => 'admin'], function () {
Route::post('moderate', 'Api\AdminApiController@moderate');
@ -129,6 +132,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
});
Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');
Route::get('discover/places', 'PlaceController@directoryHome')->name('discover.places');
Route::get('discover/places/{id}/{slug}', 'PlaceController@show');
Route::get('discover/location/country/{country}', 'PlaceController@directoryCities');
Route::group(['prefix' => 'i'], function () {
Route::redirect('/', '/');
@ -333,6 +339,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('p/{username}/{id}/c', 'CommentController@showAll');
Route::get('p/{username}/{id}/edit', 'StatusController@edit');
Route::post('p/{username}/{id}/edit', 'StatusController@editStore');
Route::get('p/{username}/{id}.json', 'StatusController@showObject');
Route::get('p/{username}/{id}', 'StatusController@show');
Route::get('{username}/followers', 'ProfileController@followers')->middleware('auth');
Route::get('{username}/following', 'ProfileController@following')->middleware('auth');

0
tests/database.sqlite Normal file
View File