mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-22 21:13:16 +00:00
commit
ee22faeb8b
73 changed files with 1991 additions and 915 deletions
|
@ -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
7
.gitignore
vendored
|
@ -13,3 +13,10 @@ npm-debug.log
|
|||
yarn-error.log
|
||||
.env
|
||||
.DS_Store
|
||||
.bash_profile
|
||||
.bash_history
|
||||
.bashrc
|
||||
.gitconfig
|
||||
.git-credentials
|
||||
/.composer/
|
||||
/nginx.conf
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
99
app/Observers/UserFilterObserver.php
Normal file
99
app/Observers/UserFilterObserver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
100
app/Services/UserFilterService.php
Normal file
100
app/Services/UserFilterService.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -78,4 +78,9 @@ class User extends Authenticatable
|
|||
return $this->hasMany(UserDevice::class);
|
||||
}
|
||||
|
||||
public function storageUsedKey()
|
||||
{
|
||||
return 'profile:storage:used:' . $this->id;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ use App\{
|
|||
Like,
|
||||
Notification,
|
||||
Profile,
|
||||
Status
|
||||
Status,
|
||||
StatusHashtag,
|
||||
};
|
||||
use Carbon\Carbon;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
@ -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) || !$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;
|
||||
|
|
|
@ -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
617
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -15,6 +15,6 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'driver' => 'gd',
|
||||
'driver' => env('IMAGE_DRIVER', 'gd'),
|
||||
|
||||
];
|
||||
|
|
|
@ -25,7 +25,7 @@ return [
|
|||
|
||||
'timeline' => [
|
||||
'local' => [
|
||||
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', true)
|
||||
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false)
|
||||
]
|
||||
],
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
@ -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,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -192,6 +189,6 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'same_site' => null,
|
||||
'same_site' => 'strict',
|
||||
|
||||
];
|
||||
|
|
|
@ -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
83
package-lock.json
generated
|
@ -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",
|
||||
|
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/css/appdark.css
vendored
BIN
public/css/appdark.css
vendored
Binary file not shown.
BIN
public/css/landing.css
vendored
BIN
public/css/landing.css
vendored
Binary file not shown.
BIN
public/js/ace.js
vendored
BIN
public/js/ace.js
vendored
Binary file not shown.
BIN
public/js/collectioncompose.js
vendored
BIN
public/js/collectioncompose.js
vendored
Binary file not shown.
BIN
public/js/collections.js
vendored
BIN
public/js/collections.js
vendored
Binary file not shown.
BIN
public/js/compose-classic.js
vendored
Normal file
BIN
public/js/compose-classic.js
vendored
Normal file
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.js
vendored
Binary file not shown.
BIN
public/js/developers.js
vendored
BIN
public/js/developers.js
vendored
Binary file not shown.
BIN
public/js/discover.js
vendored
BIN
public/js/discover.js
vendored
Binary file not shown.
BIN
public/js/hashtag.js
vendored
BIN
public/js/hashtag.js
vendored
Binary file not shown.
BIN
public/js/loops.js
vendored
BIN
public/js/loops.js
vendored
Binary file not shown.
BIN
public/js/mode-dot.js
vendored
BIN
public/js/mode-dot.js
vendored
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/quill.js
vendored
BIN
public/js/quill.js
vendored
Binary file not shown.
BIN
public/js/search.js
vendored
BIN
public/js/search.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/theme-monokai.js
vendored
BIN
public/js/theme-monokai.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
},
|
||||
|
|
|
@ -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="'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.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() {
|
||||
|
|
|
@ -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
4
resources/assets/js/micro.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'micro-ui',
|
||||
require('./components/Micro.vue').default
|
||||
);
|
2
resources/assets/sass/app.scss
vendored
2
resources/assets/sass/app.scss
vendored
|
@ -25,4 +25,4 @@
|
|||
|
||||
@import '~vue-loading-overlay/dist/vue-loading.css';
|
||||
|
||||
@import "moment";
|
||||
@import "moment";
|
5
resources/assets/sass/custom.scss
vendored
5
resources/assets/sass/custom.scss
vendored
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
22
resources/views/discover/places/directory/cities.blade.php
Normal file
22
resources/views/discover/places/directory/cities.blade.php
Normal 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
|
22
resources/views/discover/places/directory/home.blade.php
Normal file
22
resources/views/discover/places/directory/home.blade.php
Normal 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
|
48
resources/views/discover/places/show.blade.php
Normal file
48
resources/views/discover/places/show.blade.php
Normal 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
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
@{{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>
|
||||
|
|
64
resources/views/settings/invites/create.blade.php
Normal file
64
resources/views/settings/invites/create.blade.php
Normal 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
|
45
resources/views/settings/invites/home.blade.php
Normal file
45
resources/views/settings/invites/home.blade.php
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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('/', '/');
|
||||
|
|
0
tests/database.sqlite
Normal file
0
tests/database.sqlite
Normal file
Loading…
Reference in a new issue