mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-26 08:13:16 +00:00
Merge pull request #1480 from pixelfed/frontend-ui-refactor
Follow Hashtags
This commit is contained in:
commit
da01872796
59 changed files with 886 additions and 969 deletions
109
app/Console/Commands/FixHashtags.php
Normal file
109
app/Console/Commands/FixHashtags.php
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use DB;
|
||||||
|
use App\{
|
||||||
|
Hashtag,
|
||||||
|
Status,
|
||||||
|
StatusHashtag
|
||||||
|
};
|
||||||
|
|
||||||
|
class FixHashtags extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'fix:hashtags';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Fix Hashtags';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->info(' ____ _ ______ __ ');
|
||||||
|
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||||
|
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||||
|
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||||
|
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info('Pixelfed version: ' . config('pixelfed.version'));
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info('Running Fix Hashtags command');
|
||||||
|
$this->info(' ');
|
||||||
|
|
||||||
|
$missingCount = StatusHashtag::doesntHave('profile')->doesntHave('status')->count();
|
||||||
|
if($missingCount > 0) {
|
||||||
|
$this->info("Found {$missingCount} orphaned StatusHashtag records to delete ...");
|
||||||
|
$this->info(' ');
|
||||||
|
$bar = $this->output->createProgressBar($missingCount);
|
||||||
|
$bar->start();
|
||||||
|
foreach(StatusHashtag::doesntHave('profile')->doesntHave('status')->get() as $tag) {
|
||||||
|
$tag->delete();
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
$bar->finish();
|
||||||
|
$this->info(' ');
|
||||||
|
} else {
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info('Found no orphaned hashtags to delete!');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$this->info(' ');
|
||||||
|
|
||||||
|
$count = StatusHashtag::whereNull('status_visibility')->count();
|
||||||
|
if($count > 0) {
|
||||||
|
$this->info("Found {$count} hashtags to fix ...");
|
||||||
|
$this->info(' ');
|
||||||
|
} else {
|
||||||
|
$this->info('Found no hashtags to fix!');
|
||||||
|
$this->info(' ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($count);
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
StatusHashtag::with('status')
|
||||||
|
->whereNull('status_visibility')
|
||||||
|
->chunk(50, function($tags) use($bar) {
|
||||||
|
foreach($tags as $tag) {
|
||||||
|
if(!$tag->status || !$tag->status->scope) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tag->status_visibility = $tag->status->scope;
|
||||||
|
$tag->save();
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->info(' ');
|
||||||
|
$this->info(' ');
|
||||||
|
}
|
||||||
|
}
|
19
app/HashtagFollow.php
Normal file
19
app/HashtagFollow.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class HashtagFollow extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'profile_id',
|
||||||
|
'hashtag_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function hashtag()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Hashtag::class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,14 +59,11 @@ class BaseApiController extends Controller
|
||||||
$res = $this->fractal->createData($resource)->toArray();
|
$res = $this->fractal->createData($resource)->toArray();
|
||||||
} else {
|
} else {
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'page' => 'nullable|integer|min:1',
|
'page' => 'nullable|integer|min:1|max:10',
|
||||||
'limit' => 'nullable|integer|min:1|max:10'
|
'limit' => 'nullable|integer|min:1|max:10'
|
||||||
]);
|
]);
|
||||||
$limit = $request->input('limit') ?? 10;
|
$limit = $request->input('limit') ?? 10;
|
||||||
$page = $request->input('page') ?? 1;
|
$page = $request->input('page') ?? 1;
|
||||||
if($page > 3) {
|
|
||||||
return response()->json([]);
|
|
||||||
}
|
|
||||||
$end = (int) $page * $limit;
|
$end = (int) $page * $limit;
|
||||||
$start = (int) $end - $limit;
|
$start = (int) $end - $limit;
|
||||||
$res = NotificationService::get($pid, $start, $end);
|
$res = NotificationService::get($pid, $start, $end);
|
||||||
|
|
|
@ -6,6 +6,7 @@ use App\{
|
||||||
DiscoverCategory,
|
DiscoverCategory,
|
||||||
Follower,
|
Follower,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
|
HashtagFollow,
|
||||||
Profile,
|
Profile,
|
||||||
Status,
|
Status,
|
||||||
StatusHashtag,
|
StatusHashtag,
|
||||||
|
@ -17,6 +18,7 @@ use App\Transformer\Api\StatusStatelessTransformer;
|
||||||
use League\Fractal;
|
use League\Fractal;
|
||||||
use League\Fractal\Serializer\ArraySerializer;
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||||
|
use App\Services\StatusHashtagService;
|
||||||
|
|
||||||
class DiscoverController extends Controller
|
class DiscoverController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -36,57 +38,11 @@ class DiscoverController extends Controller
|
||||||
|
|
||||||
public function showTags(Request $request, $hashtag)
|
public function showTags(Request $request, $hashtag)
|
||||||
{
|
{
|
||||||
abort_if(!Auth::check(), 403);
|
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
|
||||||
|
|
||||||
$tag = Hashtag::whereSlug($hashtag)
|
$tag = Hashtag::whereSlug($hashtag)->firstOrFail();
|
||||||
->firstOrFail();
|
$tagCount = StatusHashtagService::count($tag->id);
|
||||||
|
return view('discover.tags.show', compact('tag', 'tagCount'));
|
||||||
$page = 1;
|
|
||||||
$key = 'discover:tag-'.$tag->id.':page-'.$page;
|
|
||||||
$keyMinutes = 15;
|
|
||||||
|
|
||||||
$posts = Cache::remember($key, now()->addMinutes($keyMinutes), function() use ($tag, $request) {
|
|
||||||
$tags = StatusHashtag::select('status_id')
|
|
||||||
->whereHashtagId($tag->id)
|
|
||||||
->orderByDesc('id')
|
|
||||||
->take(48)
|
|
||||||
->pluck('status_id');
|
|
||||||
|
|
||||||
return Status::select(
|
|
||||||
'id',
|
|
||||||
'uri',
|
|
||||||
'caption',
|
|
||||||
'rendered',
|
|
||||||
'profile_id',
|
|
||||||
'type',
|
|
||||||
'in_reply_to_id',
|
|
||||||
'reblog_of_id',
|
|
||||||
'is_nsfw',
|
|
||||||
'scope',
|
|
||||||
'local',
|
|
||||||
'created_at',
|
|
||||||
'updated_at'
|
|
||||||
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
|
|
||||||
->with('media')
|
|
||||||
->whereLocal(true)
|
|
||||||
->whereNull('uri')
|
|
||||||
->whereIn('id', $tags)
|
|
||||||
->whereNull('in_reply_to_id')
|
|
||||||
->whereNull('reblog_of_id')
|
|
||||||
->whereNull('url')
|
|
||||||
->whereNull('uri')
|
|
||||||
->withCount(['likes', 'comments'])
|
|
||||||
->whereIsNsfw(false)
|
|
||||||
->whereVisibility('public')
|
|
||||||
->orderBy('id', 'desc')
|
|
||||||
->get();
|
|
||||||
});
|
|
||||||
|
|
||||||
if($posts->count() == 0) {
|
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('discover.tags.show', compact('tag', 'posts'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showCategory(Request $request, $slug)
|
public function showCategory(Request $request, $slug)
|
||||||
|
@ -156,7 +112,6 @@ class DiscoverController extends Controller
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function loopWatch(Request $request)
|
public function loopWatch(Request $request)
|
||||||
{
|
{
|
||||||
abort_if(!Auth::check(), 403);
|
abort_if(!Auth::check(), 403);
|
||||||
|
@ -171,4 +126,26 @@ class DiscoverController extends Controller
|
||||||
|
|
||||||
return response()->json(200);
|
return response()->json(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getHashtags(Request $request)
|
||||||
|
{
|
||||||
|
$auth = Auth::check();
|
||||||
|
abort_if(!config('instance.discover.tags.is_public') && !$auth, 403);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'hashtag' => 'required|alphanum|min:2|max:124',
|
||||||
|
'page' => 'nullable|integer|min:1|max:' . ($auth ? 19 : 3)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$page = $request->input('page') ?? '1';
|
||||||
|
$end = $page > 1 ? $page * 9 : 1;
|
||||||
|
$tag = $request->input('hashtag');
|
||||||
|
|
||||||
|
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||||
|
$res['tags'] = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||||
|
if($page == 1) {
|
||||||
|
$res['follows'] = HashtagFollow::whereUserId(Auth::id())->whereHashtagId($hashtag->id)->exists();
|
||||||
|
}
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
61
app/Http/Controllers/HashtagFollowController.php
Normal file
61
app/Http/Controllers/HashtagFollowController.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Auth;
|
||||||
|
use App\{
|
||||||
|
Hashtag,
|
||||||
|
HashtagFollow,
|
||||||
|
Status
|
||||||
|
};
|
||||||
|
|
||||||
|
class HashtagFollowController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'name' => 'required|alpha_num|min:1|max:124|exists:hashtags,name'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
$profile = $user->profile;
|
||||||
|
$tag = $request->input('name');
|
||||||
|
|
||||||
|
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||||
|
|
||||||
|
$hashtagFollow = HashtagFollow::firstOrCreate([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'profile_id' => $user->profile_id ?? $user->profile->id,
|
||||||
|
'hashtag_id' => $hashtag->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if($hashtagFollow->wasRecentlyCreated) {
|
||||||
|
$state = 'created';
|
||||||
|
// todo: send to HashtagFollowService
|
||||||
|
} else {
|
||||||
|
$state = 'deleted';
|
||||||
|
$hashtagFollow->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'state' => $state
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags(Request $request)
|
||||||
|
{
|
||||||
|
return HashtagFollow::with('hashtag')->whereUserId(Auth::id())
|
||||||
|
->inRandomOrder()
|
||||||
|
->take(3)
|
||||||
|
->get()
|
||||||
|
->map(function($follow, $k) {
|
||||||
|
return $follow->hashtag->name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -211,6 +211,10 @@ class PublicApiController extends Controller
|
||||||
'limit' => 'nullable|integer|max:20'
|
'limit' => 'nullable|integer|max:20'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if(config('instance.timeline.local.is_public') == false && !Auth::check()) {
|
||||||
|
abort(403, 'Authentication required.');
|
||||||
|
}
|
||||||
|
|
||||||
$page = $request->input('page');
|
$page = $request->input('page');
|
||||||
$min = $request->input('min_id');
|
$min = $request->input('min_id');
|
||||||
$max = $request->input('max_id');
|
$max = $request->input('max_id');
|
||||||
|
@ -332,6 +336,8 @@ class PublicApiController extends Controller
|
||||||
->pluck('id');
|
->pluck('id');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$private = $private->diff($following)->flatten();
|
||||||
|
|
||||||
$filters = UserFilter::whereUserId($pid)
|
$filters = UserFilter::whereUserId($pid)
|
||||||
->whereFilterableType('App\Profile')
|
->whereFilterableType('App\Profile')
|
||||||
->whereIn('filter_type', ['mute', 'block'])
|
->whereIn('filter_type', ['mute', 'block'])
|
||||||
|
|
|
@ -143,6 +143,7 @@ class SearchController extends Controller
|
||||||
'tokens' => [$item->caption],
|
'tokens' => [$item->caption],
|
||||||
'name' => $item->caption,
|
'name' => $item->caption,
|
||||||
'thumb' => $item->thumb(),
|
'thumb' => $item->thumb(),
|
||||||
|
'filter' => $item->firstMedia()->filter_class
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
$tokens['posts'] = $posts;
|
$tokens['posts'] = $posts;
|
||||||
|
|
|
@ -80,8 +80,10 @@ class DeleteAccountPipeline implements ShouldQueue
|
||||||
Bookmark::whereProfileId($user->profile->id)->forceDelete();
|
Bookmark::whereProfileId($user->profile->id)->forceDelete();
|
||||||
|
|
||||||
EmailVerification::whereUserId($user->id)->forceDelete();
|
EmailVerification::whereUserId($user->id)->forceDelete();
|
||||||
|
|
||||||
$id = $user->profile->id;
|
$id = $user->profile->id;
|
||||||
|
|
||||||
|
StatusHashtag::whereProfileId($id)->delete();
|
||||||
|
|
||||||
FollowRequest::whereFollowingId($id)->orWhere('follower_id', $id)->forceDelete();
|
FollowRequest::whereFollowingId($id)->orWhere('follower_id', $id)->forceDelete();
|
||||||
|
|
||||||
Follower::whereProfileId($id)->orWhere('following_id', $id)->forceDelete();
|
Follower::whereProfileId($id)->orWhere('following_id', $id)->forceDelete();
|
||||||
|
|
|
@ -89,6 +89,9 @@ class StatusEntityLexer implements ShouldQueue
|
||||||
$status = $this->status;
|
$status = $this->status;
|
||||||
|
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
|
if(mb_strlen($tag) > 124) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
DB::transaction(function () use ($status, $tag) {
|
DB::transaction(function () use ($status, $tag) {
|
||||||
$slug = str_slug($tag, '-', false);
|
$slug = str_slug($tag, '-', false);
|
||||||
$hashtag = Hashtag::firstOrCreate(
|
$hashtag = Hashtag::firstOrCreate(
|
||||||
|
@ -98,7 +101,8 @@ class StatusEntityLexer implements ShouldQueue
|
||||||
[
|
[
|
||||||
'status_id' => $status->id,
|
'status_id' => $status->id,
|
||||||
'hashtag_id' => $hashtag->id,
|
'hashtag_id' => $hashtag->id,
|
||||||
'profile_id' => $status->profile_id
|
'profile_id' => $status->profile_id,
|
||||||
|
'status_visibility' => $status->visibility,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
64
app/Observers/StatusHashtagObserver.php
Normal file
64
app/Observers/StatusHashtagObserver.php
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\StatusHashtag;
|
||||||
|
use App\Services\StatusHashtagService;
|
||||||
|
|
||||||
|
class StatusHashtagObserver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the notification "created" event.
|
||||||
|
*
|
||||||
|
* @param \App\Notification $notification
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function created(StatusHashtag $hashtag)
|
||||||
|
{
|
||||||
|
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the notification "updated" event.
|
||||||
|
*
|
||||||
|
* @param \App\Notification $notification
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function updated(StatusHashtag $hashtag)
|
||||||
|
{
|
||||||
|
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the notification "deleted" event.
|
||||||
|
*
|
||||||
|
* @param \App\Notification $notification
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function deleted(StatusHashtag $hashtag)
|
||||||
|
{
|
||||||
|
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the notification "restored" event.
|
||||||
|
*
|
||||||
|
* @param \App\Notification $notification
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function restored(StatusHashtag $hashtag)
|
||||||
|
{
|
||||||
|
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the notification "force deleted" event.
|
||||||
|
*
|
||||||
|
* @param \App\Notification $notification
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function forceDeleted(StatusHashtag $hashtag)
|
||||||
|
{
|
||||||
|
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,13 @@ namespace App\Providers;
|
||||||
use App\Observers\{
|
use App\Observers\{
|
||||||
AvatarObserver,
|
AvatarObserver,
|
||||||
NotificationObserver,
|
NotificationObserver,
|
||||||
|
StatusHashtagObserver,
|
||||||
UserObserver
|
UserObserver
|
||||||
};
|
};
|
||||||
use App\{
|
use App\{
|
||||||
Avatar,
|
Avatar,
|
||||||
Notification,
|
Notification,
|
||||||
|
StatusHashtag,
|
||||||
User
|
User
|
||||||
};
|
};
|
||||||
use Auth, Horizon, URL;
|
use Auth, Horizon, URL;
|
||||||
|
@ -31,6 +33,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
|
|
||||||
Avatar::observe(AvatarObserver::class);
|
Avatar::observe(AvatarObserver::class);
|
||||||
Notification::observe(NotificationObserver::class);
|
Notification::observe(NotificationObserver::class);
|
||||||
|
StatusHashtag::observe(StatusHashtagObserver::class);
|
||||||
User::observe(UserObserver::class);
|
User::observe(UserObserver::class);
|
||||||
|
|
||||||
Horizon::auth(function ($request) {
|
Horizon::auth(function ($request) {
|
||||||
|
|
80
app/Services/StatusHashtagService.php
Normal file
80
app/Services/StatusHashtagService.php
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Cache, Redis;
|
||||||
|
use App\{Status, StatusHashtag};
|
||||||
|
use App\Transformer\Api\StatusHashtagTransformer;
|
||||||
|
use League\Fractal;
|
||||||
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
|
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||||
|
|
||||||
|
class StatusHashtagService {
|
||||||
|
|
||||||
|
const CACHE_KEY = 'pf:services:status-hashtag:collection:';
|
||||||
|
|
||||||
|
public static function get($id, $page = 1, $stop = 9)
|
||||||
|
{
|
||||||
|
return StatusHashtag::whereHashtagId($id)
|
||||||
|
->whereStatusVisibility('public')
|
||||||
|
->whereHas('media')
|
||||||
|
->skip($stop)
|
||||||
|
->latest()
|
||||||
|
->take(9)
|
||||||
|
->pluck('status_id')
|
||||||
|
->map(function ($i, $k) use ($id) {
|
||||||
|
return self::getStatus($i, $id);
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function coldGet($id, $start = 0, $stop = 2000)
|
||||||
|
{
|
||||||
|
$stop = $stop > 2000 ? 2000 : $stop;
|
||||||
|
$ids = StatusHashtag::whereHashtagId($id)
|
||||||
|
->whereStatusVisibility('public')
|
||||||
|
->whereHas('media')
|
||||||
|
->latest()
|
||||||
|
->skip($start)
|
||||||
|
->take($stop)
|
||||||
|
->pluck('status_id');
|
||||||
|
foreach($ids as $key) {
|
||||||
|
self::set($id, $key);
|
||||||
|
}
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function set($key, $val)
|
||||||
|
{
|
||||||
|
return Redis::zadd(self::CACHE_KEY . $key, $val, $val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function del($key)
|
||||||
|
{
|
||||||
|
return Redis::zrem(self::CACHE_KEY . $key, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function count($id)
|
||||||
|
{
|
||||||
|
$count = Redis::zcount(self::CACHE_KEY . $id, '-inf', '+inf');
|
||||||
|
if(empty($count)) {
|
||||||
|
$count = StatusHashtag::whereHashtagId($id)->count();
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getStatus($statusId, $hashtagId)
|
||||||
|
{
|
||||||
|
return Cache::remember('pf:services:status-hashtag:post:'.$statusId.':hashtag:'.$hashtagId, now()->addMonths(3), function() use($statusId, $hashtagId) {
|
||||||
|
$statusHashtag = StatusHashtag::with('profile', 'status', 'hashtag')
|
||||||
|
->whereStatusVisibility('public')
|
||||||
|
->whereStatusId($statusId)
|
||||||
|
->whereHashtagId($hashtagId)
|
||||||
|
->first();
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Item($statusHashtag, new StatusHashtagTransformer());
|
||||||
|
return $fractal->createData($resource)->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,8 @@ class StatusHashtag extends Model
|
||||||
public $fillable = [
|
public $fillable = [
|
||||||
'status_id',
|
'status_id',
|
||||||
'hashtag_id',
|
'hashtag_id',
|
||||||
'profile_id'
|
'profile_id',
|
||||||
|
'status_visibility'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function status()
|
public function status()
|
||||||
|
@ -26,4 +27,16 @@ class StatusHashtag extends Model
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Profile::class);
|
return $this->belongsTo(Profile::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function media()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
Media::class,
|
||||||
|
Status::class,
|
||||||
|
'id',
|
||||||
|
'status_id',
|
||||||
|
'status_id',
|
||||||
|
'id'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
38
app/Transformer/Api/StatusHashtagTransformer.php
Normal file
38
app/Transformer/Api/StatusHashtagTransformer.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Transformer\Api;
|
||||||
|
|
||||||
|
use App\{Hashtag, Status, StatusHashtag};
|
||||||
|
use League\Fractal;
|
||||||
|
|
||||||
|
class StatusHashtagTransformer extends Fractal\TransformerAbstract
|
||||||
|
{
|
||||||
|
public function transform(StatusHashtag $statusHashtag)
|
||||||
|
{
|
||||||
|
$hashtag = $statusHashtag->hashtag;
|
||||||
|
$status = $statusHashtag->status;
|
||||||
|
$profile = $statusHashtag->profile;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => [
|
||||||
|
'id' => (int) $status->id,
|
||||||
|
'type' => $status->type,
|
||||||
|
'url' => $status->url(),
|
||||||
|
'thumb' => $status->thumb(),
|
||||||
|
'filter' => $status->firstMedia()->filter_class,
|
||||||
|
'sensitive' => (bool) $status->is_nsfw,
|
||||||
|
'like_count' => $status->likes_count,
|
||||||
|
'share_count' => $status->reblogs_count,
|
||||||
|
'user' => [
|
||||||
|
'username' => $profile->username,
|
||||||
|
'url' => $profile->url(),
|
||||||
|
],
|
||||||
|
'visibility' => $status->visibility ?? $status->scope
|
||||||
|
],
|
||||||
|
'hashtag' => [
|
||||||
|
'name' => $hashtag->name,
|
||||||
|
'url' => $hashtag->url(),
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -264,7 +264,9 @@ class Extractor extends Regex
|
||||||
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
|
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if(mb_strlen($hashtag[0]) > 124) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$tags[] = [
|
$tags[] = [
|
||||||
'hashtag' => $hashtag[0],
|
'hashtag' => $hashtag[0],
|
||||||
'indices' => [$start_position, $end_position],
|
'indices' => [$start_position, $end_position],
|
||||||
|
|
|
@ -49,8 +49,23 @@ trait User {
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMaxUserBansPerDayAttribute()
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
public function getMaxInstanceBansPerDayAttribute()
|
public function getMaxInstanceBansPerDayAttribute()
|
||||||
{
|
{
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMaxHashtagFollowsPerHourAttribute()
|
||||||
|
{
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaxHashtagFollowsPerDayAttribute()
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,15 +1,33 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'email' => env('INSTANCE_CONTACT_EMAIL'),
|
|
||||||
|
'announcement' => [
|
||||||
|
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', true),
|
||||||
|
'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.<br><span class="font-weight-normal">Something else here</span>')
|
||||||
|
],
|
||||||
|
|
||||||
'contact' => [
|
'contact' => [
|
||||||
'enabled' => env('INSTANCE_CONTACT_FORM', false),
|
'enabled' => env('INSTANCE_CONTACT_FORM', false),
|
||||||
'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1),
|
'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1),
|
||||||
],
|
],
|
||||||
|
|
||||||
'announcement' => [
|
'discover' => [
|
||||||
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', true),
|
'loops' => [
|
||||||
'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.<br><span class="font-weight-normal">Something else here</span>')
|
'enabled' => false
|
||||||
|
],
|
||||||
|
'tags' => [
|
||||||
|
'is_public' => env('INSTANCE_PUBLIC_HASHTAGS', false)
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'email' => env('INSTANCE_CONTACT_EMAIL'),
|
||||||
|
|
||||||
|
'timeline' => [
|
||||||
|
'local' => [
|
||||||
|
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false)
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
];
|
];
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateHashtagFollowsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('hashtag_follows', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->bigInteger('user_id')->unsigned()->index();
|
||||||
|
$table->bigInteger('profile_id')->unsigned()->index();
|
||||||
|
$table->bigInteger('hashtag_id')->unsigned()->index();
|
||||||
|
$table->unique(['user_id', 'profile_id', 'hashtag_id']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('hashtag_follows');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddStatusVisibilityToStatusHashtagsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('status_hashtags', function (Blueprint $table) {
|
||||||
|
$table->string('status_visibility')->nullable()->index()->after('profile_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('status_hashtags', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('status_visibility');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
BIN
public/js/direct.js
vendored
BIN
public/js/direct.js
vendored
Binary file not shown.
BIN
public/js/hashtag.js
vendored
Normal file
BIN
public/js/hashtag.js
vendored
Normal file
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.
187
resources/assets/js/components/Hashtag.vue
Normal file
187
resources/assets/js/components/Hashtag.vue
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="loaded" class="container">
|
||||||
|
<div class="profile-header row my-5">
|
||||||
|
<div class="col-12 col-md-3">
|
||||||
|
<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: 172px; height: 172px; border-radius: 100%">#</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">
|
||||||
|
<p class="tag-header mb-0">#{{hashtag}}</p>
|
||||||
|
<p class="lead"><span class="font-weight-bold">{{tags.length ? hashtagCount : '0'}}</span> posts</p>
|
||||||
|
<p v-if="authenticated && tags.length" class="pt-3">
|
||||||
|
<button v-if="!following" type="button" class="btn btn-primary font-weight-bold py-1 px-5" @click="followHashtag">
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
<button v-else type="button" class="btn btn-outline-secondary font-weight-bold py-1 px-5" @click="unfollowHashtag">
|
||||||
|
Unfollow
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="tags.length" class="tag-timeline">
|
||||||
|
<p v-if="top.length" class="font-weight-bold text-muted mb-0">Top Posts</p>
|
||||||
|
<div class="row pb-5">
|
||||||
|
<div v-for="(tag, index) in top" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
|
||||||
|
<a class="card info-overlay card-md-border-0" :href="tag.status.url">
|
||||||
|
<div :class="[tag.status.filter ? 'square ' + tag.status.filter : 'square']">
|
||||||
|
<div class="square-content" :style="'background-image: url('+tag.status.thumb+')'"></div>
|
||||||
|
<div class="info-overlay-text">
|
||||||
|
<h5 class="text-white m-auto font-weight-bold">
|
||||||
|
<span class="pr-4">
|
||||||
|
<span class="far fa-heart fa-lg pr-1"></span> {{tag.status.like_count}}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span class="fas fa-retweet fa-lg pr-1"></span> {{tag.status.share_count}}
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="font-weight-bold text-muted mb-0">Most Recent</p>
|
||||||
|
<div class="row">
|
||||||
|
<div v-for="(tag, index) in tags" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
|
||||||
|
<a class="card info-overlay card-md-border-0" :href="tag.status.url">
|
||||||
|
<div :class="[tag.status.filter ? 'square ' + tag.status.filter : 'square']">
|
||||||
|
<div class="square-content" :style="'background-image: url('+tag.status.thumb+')'"></div>
|
||||||
|
<div class="info-overlay-text">
|
||||||
|
<h5 class="text-white m-auto font-weight-bold">
|
||||||
|
<span class="pr-4">
|
||||||
|
<span class="far fa-heart fa-lg pr-1"></span> {{tag.status.like_count}}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span class="fas fa-retweet fa-lg pr-1"></span> {{tag.status.share_count}}
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="tags.length && loaded" class="card card-body text-center shadow-none bg-transparent border-0">
|
||||||
|
<infinite-loading @infinite="infiniteLoader">
|
||||||
|
<div slot="no-results" class="font-weight-bold"></div>
|
||||||
|
<div slot="no-more" class="font-weight-bold"></div>
|
||||||
|
</infinite-loading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p class="text-center lead font-weight-bold">No public posts found.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="container text-center">
|
||||||
|
<div class="mt-5 spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style type="text/css" scoped>
|
||||||
|
.tag-header {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'hashtag',
|
||||||
|
'hashtagCount'
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
page: 1,
|
||||||
|
authenticated: false,
|
||||||
|
following: false,
|
||||||
|
tags: [],
|
||||||
|
top: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.authenticated = $('body').hasClass('loggedIn');
|
||||||
|
this.getResults();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getResults() {
|
||||||
|
axios.get('/api/v2/discover/tag', {
|
||||||
|
params: {
|
||||||
|
hashtag: this.hashtag,
|
||||||
|
page: this.page
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
let data = res.data;
|
||||||
|
let tags = data.tags.filter(n => {
|
||||||
|
if(!n || n.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
this.tags = tags;
|
||||||
|
//this.top = tags.slice(6, 9);
|
||||||
|
this.loaded = true;
|
||||||
|
this.following = data.follows;
|
||||||
|
this.page++;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
infiniteLoader($state) {
|
||||||
|
if(this.page > (this.authenticated ? 19 : 3)) {
|
||||||
|
$state.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
axios.get('/api/v2/discover/tag', {
|
||||||
|
params: {
|
||||||
|
hashtag: this.hashtag,
|
||||||
|
page: this.page,
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
let data = res.data;
|
||||||
|
if(data.tags.length) {
|
||||||
|
let tags = data.tags.filter(n => {
|
||||||
|
if(!n || n.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
this.tags.push(...tags);
|
||||||
|
if(tags.length > 9) {
|
||||||
|
$state.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.page++;
|
||||||
|
$state.loaded();
|
||||||
|
} else {
|
||||||
|
$state.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
followHashtag() {
|
||||||
|
axios.post('/api/local/discover/tag/subscribe', {
|
||||||
|
name: this.hashtag
|
||||||
|
}).then(res => {
|
||||||
|
this.following = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
unfollowHashtag() {
|
||||||
|
axios.post('/api/local/discover/tag/subscribe', {
|
||||||
|
name: this.hashtag
|
||||||
|
}).then(res => {
|
||||||
|
this.following = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -107,8 +107,8 @@
|
||||||
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
|
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
|
||||||
<div class="card-body status-comments pb-5">
|
<div class="card-body status-comments pb-5">
|
||||||
<div class="status-comment">
|
<div class="status-comment">
|
||||||
<p :class="[status.content.length > 420 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
|
<p :class="[status.content.length > 620 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
|
||||||
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
|
<a class="font-weight-bold pr-1 text-dark text-decoration-none" :href="statusProfileUrl">{{statusUsername}}</a>
|
||||||
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
|
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -124,10 +124,13 @@
|
||||||
<div class="comments">
|
<div class="comments">
|
||||||
<div v-for="(reply, index) in results" class="pb-3" :key="'tl' + reply.id + '_' + index">
|
<div v-for="(reply, index) in results" class="pb-3" :key="'tl' + reply.id + '_' + index">
|
||||||
<div v-if="reply.sensitive == true">
|
<div v-if="reply.sensitive == true">
|
||||||
<div class="card card-body shadow-none border border-left-blue py-3 px-1 text-center small">
|
<span class="py-3">
|
||||||
<p class="mb-0">This comment may contain sensitive material</p>
|
<a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
|
||||||
<p class="font-weight-bold text-primary cursor-pointer mb-0" @click="reply.sensitive = false;">Show</p>
|
<span class="text-break">
|
||||||
</div>
|
<span class="font-italic text-muted">This comment may contain sensitive material</span>
|
||||||
|
<span class="text-primary cursor-pointer pl-1" @click="reply.sensitive = false;">Show</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
|
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
<a class="dropdown-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a>
|
<a class="dropdown-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a>
|
||||||
<a class="dropdown-item font-weight-bold text-decoration-none" href="#">Share</a>
|
<!-- <a class="dropdown-item font-weight-bold text-decoration-none" href="#">Share</a>
|
||||||
<a class="dropdown-item font-weight-bold text-decoration-none" href="#">Embed</a>
|
<a class="dropdown-item font-weight-bold text-decoration-none" href="#">Embed</a> -->
|
||||||
<span v-if="statusOwner(status) == false">
|
<span v-if="statusOwner(status) == false">
|
||||||
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
|
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -54,8 +54,9 @@
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a>
|
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a>
|
||||||
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Share</a>
|
<!-- <a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Share</a>
|
||||||
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Embed</a>
|
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Embed</a> -->
|
||||||
|
<a class="list-group-item font-weight-bold text-decoration-none" href="#" @click="hidePost(status)">Hide</a>
|
||||||
<span v-if="statusOwner(status) == false">
|
<span v-if="statusOwner(status) == false">
|
||||||
<a class="list-group-item font-weight-bold text-decoration-none" :href="reportUrl(status)">Report</a>
|
<a class="list-group-item font-weight-bold text-decoration-none" :href="reportUrl(status)">Report</a>
|
||||||
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="muteProfile(status)" href="#">Mute Profile</a>
|
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="muteProfile(status)" href="#">Mute Profile</a>
|
||||||
|
@ -157,6 +158,11 @@
|
||||||
$('#mt_pid_'+this.status.id).modal('hide');
|
$('#mt_pid_'+this.status.id).modal('hide');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hidePost(status) {
|
||||||
|
status.sensitive = true;
|
||||||
|
$('#mt_pid_'+status.id).modal('hide');
|
||||||
|
},
|
||||||
|
|
||||||
moderatePost(status, action, $event) {
|
moderatePost(status, action, $event) {
|
||||||
let username = status.account.username;
|
let username = status.account.username;
|
||||||
switch(action) {
|
switch(action) {
|
||||||
|
|
|
@ -67,16 +67,14 @@
|
||||||
|
|
||||||
<div v-if="filters.statuses && results.statuses" class="row mb-4">
|
<div v-if="filters.statuses && results.statuses" class="row mb-4">
|
||||||
<p class="col-12 font-weight-bold text-muted">Statuses</p>
|
<p class="col-12 font-weight-bold text-muted">Statuses</p>
|
||||||
<a v-for="(status, index) in results.statuses" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="status.url">
|
<div v-for="(status, index) in results.statuses" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
|
||||||
<div class="card">
|
<a class="card info-overlay card-md-border-0" :href="status.url">
|
||||||
<img class="card-img-top img-fluid" :src="status.thumb">
|
<div :class="[status.filter ? 'square ' + status.filter : 'square']">
|
||||||
<div class="card-body text-center ">
|
<div class="square-content" :style="'background-image: url('+status.thumb+')'"></div>
|
||||||
<p class="mb-0 small text-truncate font-weight-bold text-muted" v-html="status.value">
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!results.hashtags && !results.profiles && !results.statuses">
|
<div v-if="!results.hashtags && !results.profiles && !results.statuses">
|
||||||
<p class="text-center lead">No results found!</p>
|
<p class="text-center lead">No results found!</p>
|
||||||
|
|
|
@ -39,6 +39,33 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="index == 4 && showHashtagPosts && hashtagPosts.length" class="card mb-sm-4 status-card card-md-rounded-0">
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
||||||
|
<span></span>
|
||||||
|
<h6 class="text-muted font-weight-bold mb-0"><a :href="'/discover/tags/'+hashtagPostsName+'?src=tr'">#{{hashtagPostsName}}</a></h6>
|
||||||
|
<span class="cursor-pointer text-muted" v-on:click="showHashtagPosts = false"><i class="fas fa-times"></i></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body row mx-0">
|
||||||
|
<div v-for="(tag, index) in hashtagPosts" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
|
||||||
|
<a class="card info-overlay card-md-border-0" :href="tag.status.url">
|
||||||
|
<div :class="[tag.status.filter ? 'square ' + tag.status.filter : 'square']">
|
||||||
|
<div class="square-content" :style="'background-image: url('+tag.status.thumb+')'"></div>
|
||||||
|
<div class="info-overlay-text">
|
||||||
|
<h5 class="text-white m-auto font-weight-bold">
|
||||||
|
<span class="pr-4">
|
||||||
|
<span class="far fa-heart fa-lg pr-1"></span> {{tag.status.like_count}}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span class="fas fa-retweet fa-lg pr-1"></span> {{tag.status.share_count}}
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card mb-sm-4 status-card card-md-rounded-0">
|
<div class="card mb-sm-4 status-card card-md-rounded-0">
|
||||||
<div v-if="!modes.distractionFree" class="card-header d-inline-flex align-items-center bg-white">
|
<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;">
|
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
|
||||||
|
@ -439,7 +466,10 @@
|
||||||
showReadMore: true,
|
showReadMore: true,
|
||||||
replyStatus: {},
|
replyStatus: {},
|
||||||
replyText: '',
|
replyText: '',
|
||||||
emoji: ['😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥']
|
emoji: ['😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'],
|
||||||
|
showHashtagPosts: false,
|
||||||
|
hashtagPosts: [],
|
||||||
|
hashtagPostsName: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -542,6 +572,7 @@
|
||||||
this.max_id = Math.min(...ids);
|
this.max_id = Math.min(...ids);
|
||||||
$('.timeline .pagination').removeClass('d-none');
|
$('.timeline .pagination').removeClass('d-none');
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.fetchHashtagPosts();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1104,6 +1135,30 @@
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchHashtagPosts() {
|
||||||
|
|
||||||
|
axios.get('/api/local/discover/tag/list')
|
||||||
|
.then(res => {
|
||||||
|
let tags = res.data;
|
||||||
|
if(tags.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let hashtag = tags[0];
|
||||||
|
this.hashtagPostsName = hashtag;
|
||||||
|
axios.get('/api/v2/discover/tag', {
|
||||||
|
params: {
|
||||||
|
hashtag: hashtag
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if(res.data.tags.length) {
|
||||||
|
this.showHashtagPosts = true;
|
||||||
|
this.hashtagPosts = res.data.tags.splice(0,3);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
resources/assets/js/hashtag.js
vendored
Normal file
4
resources/assets/js/hashtag.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Vue.component(
|
||||||
|
'hashtag-component',
|
||||||
|
require('./components/Hashtag.vue').default
|
||||||
|
);
|
|
@ -1,53 +1,11 @@
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
<hashtag-component hashtag="{{$tag->name}}" hashtag-count="{{$tagCount}}"></hashtag-component>
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div class="profile-header row my-5">
|
|
||||||
<div class="col-12 col-md-3">
|
|
||||||
<div class="profile-avatar">
|
|
||||||
<img class="rounded-circle card" src="{{$posts->last()->thumb()}}" width="172px" height="172px">
|
|
||||||
</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">
|
|
||||||
<span class="h1">{{$tag->name}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tag-timeline">
|
|
||||||
<div class="row">
|
|
||||||
@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 class="info-overlay-text">
|
|
||||||
<h5 class="text-white m-auto font-weight-bold">
|
|
||||||
<span class="pr-4">
|
|
||||||
<span class="far fa-heart fa-lg pr-1"></span> {{$status->likes_count}}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<span class="far fa-comment fa-lg pr-1"></span> {{$status->comments_count}}
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('scripts')
|
@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" src="{{ mix('js/compose.js') }}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">$(document).ready(function(){new Vue({el: '#content'});});</script>
|
||||||
$(document).ready(function(){new Vue({el: '#content'});});
|
|
||||||
</script>
|
|
||||||
@endpush
|
@endpush
|
10
resources/views/errors/400.blade.php
Normal file
10
resources/views/errors/400.blade.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<div class="error-page py-5 my-5 text-center">
|
||||||
|
<h3 class="font-weight-bold">Sorry, this page isn't available.</h3>
|
||||||
|
<p class="lead">The link you followed may be broken, or the page may have been removed. <a href="/">Go back to Pixelfed.</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
10
resources/views/errors/403.blade.php
Normal file
10
resources/views/errors/403.blade.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<div class="error-page py-5 my-5 text-center">
|
||||||
|
<h3 class="font-weight-bold">Sorry, this page isn't available.</h3>
|
||||||
|
<p class="lead">The link you followed may be broken, or the page may have been removed. <a href="/">Go back to Pixelfed.</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
|
@ -2,13 +2,9 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="error-page py-5 my-5">
|
<div class="error-page py-5 my-5 text-center">
|
||||||
<div class="card mx-5">
|
<h3 class="font-weight-bold">Sorry, this page isn't available.</h3>
|
||||||
<div class="card-body p-5 text-center">
|
<p class="lead">The link you followed may be broken, or the page may have been removed. <a href="/">Go back to Pixelfed.</a></p>
|
||||||
<h1>Page Not Found</h1>
|
|
||||||
<img src="/img/fred1.gif" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
@ -2,14 +2,9 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="error-page py-5 my-5">
|
<div class="error-page py-5 my-5 text-center">
|
||||||
<div class="card mx-5">
|
<h3 class="font-weight-bold">Something went wrong</h3>
|
||||||
<div class="card-body p-5 text-center">
|
<p class="lead">We cannot process your request at this time, please try again later. <a href="/">Go back to Pixelfed.</a></p>
|
||||||
<h1>Whoops! Something went wrong.</h1>
|
|
||||||
<p class="mb-0 text-muted lead">If you keep seeing this message, please contact an admin.</p>
|
|
||||||
<img src="/img/fred1.gif" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
@ -2,14 +2,9 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="error-page py-5 my-5">
|
<div class="error-page py-5 my-5 text-center">
|
||||||
<div class="card mx-5">
|
<h3 class="font-weight-bold">Service Unavailable</h3>
|
||||||
<div class="card-body p-5 text-center">
|
<p class="lead">Our service is in maintenance mode, please try again later. <a href="/">Go back to Pixelfed.</a></p>
|
||||||
<h1>Service Unavailable</h1>
|
|
||||||
<p class="mb-0 text-muted lead">Our services are in maintenance mode, please try again later.</p>
|
|
||||||
<img src="/img/fred1.gif" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
@ -16,6 +16,43 @@
|
||||||
<li class="">You can add up to 30 hashtags to your post or comment.</li>
|
<li class="">You can add up to 30 hashtags to your post or comment.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="py-4">
|
||||||
|
<p>
|
||||||
|
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse0" role="button" aria-expanded="false" aria-controls="collapse0">
|
||||||
|
<i class="fas fa-chevron-down mr-2"></i>
|
||||||
|
How do I use a hashtag on Pixelfed?
|
||||||
|
</a>
|
||||||
|
<div class="collapse" id="collapse0">
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li>You can add hashtags to post captions, if the post is public the hashtag will be discoverable.</li>
|
||||||
|
<li>You can follow hashtags on Pixelfed to stay connected with interests you care about.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse1" role="button" aria-expanded="false" aria-controls="collapse1">
|
||||||
|
<i class="fas fa-chevron-down mr-2"></i>
|
||||||
|
How do I follow a hashtag?
|
||||||
|
</a>
|
||||||
|
<div class="collapse" id="collapse1">
|
||||||
|
<div>
|
||||||
|
<p>You can follow hashtags on Pixelfed to stay connected with interests you care about.</p>
|
||||||
|
<p class="mb-0">To follow a hashtag:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Tap any hashtag (example: #art) you see on Pixelfed.</li>
|
||||||
|
<li>Tap <span class="font-weight-bold">Follow</span>. Once you follow a hashtag, you'll see its photos and videos appear in feed.</li>
|
||||||
|
</ol>
|
||||||
|
<p>To unfollow a hashtag, tap the hashtag and then tap Unfollow to confirm.</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
You can follow up to 20 hashtags per hour or 100 per day.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
<div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;">
|
<div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;">
|
||||||
<div class="card-header text-light font-weight-bold h4 p-4">Hashtag Tips</div>
|
<div class="card-header text-light font-weight-bold h4 p-4">Hashtag Tips</div>
|
||||||
<div class="card-body bg-white p-3">
|
<div class="card-body bg-white p-3">
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
|
|
||||||
<div class="container px-0 mt-md-4">
|
|
||||||
<div class="card card-md-rounded-0 status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
|
|
||||||
<div class="row mx-0">
|
|
||||||
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
|
|
||||||
<a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
|
|
||||||
<div class="status-avatar mr-2">
|
|
||||||
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
|
|
||||||
</div>
|
|
||||||
<div class="username">
|
|
||||||
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="float-right">
|
|
||||||
<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 text-muted"></span>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
|
||||||
<a class="dropdown-item font-weight-bold" href="{{$status->reportUrl()}}">Report</a>
|
|
||||||
{{-- <a class="dropdown-item" href="#">Embed</a> --}}
|
|
||||||
@if(Auth::check())
|
|
||||||
@if(Auth::user()->profile->id !== $status->profile->id)
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<form method="post" action="/i/mute">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="user">
|
|
||||||
<input type="hidden" name="item" value="{{$status->profile_id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Mute this user</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="/i/block">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="user">
|
|
||||||
<input type="hidden" name="item" value="{{$status->profile_id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Block this user</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
{{-- <a class="dropdown-item" href="{{$status->editUrl()}}">Edit</a> --}}
|
|
||||||
<form method="post" action="/i/delete">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="post">
|
|
||||||
<input type="hidden" name="item" value="{{$status->id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Delete</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-8 status-photo px-0">
|
|
||||||
@if($status->is_nsfw)
|
|
||||||
<details class="details-animated">
|
|
||||||
<summary>
|
|
||||||
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
|
|
||||||
<p class="font-weight-light">(click to show)</p>
|
|
||||||
</summary>
|
|
||||||
@endif
|
|
||||||
<div id="photoCarousel" class="carousel slide carousel-fade" data-ride="carousel">
|
|
||||||
<ol class="carousel-indicators">
|
|
||||||
@for($i = 0; $i < $status->media_count; $i++)
|
|
||||||
<li data-target="#photoCarousel" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
|
|
||||||
@endfor
|
|
||||||
</ol>
|
|
||||||
<div class="carousel-inner">
|
|
||||||
@foreach($status->media()->orderBy('order')->get() as $media)
|
|
||||||
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
|
|
||||||
<figure class="{{$media->filter_class}}">
|
|
||||||
<img class="d-block w-100" src="{{$media->url()}}" title="{{$media->caption}}" data-toggle="tooltip" data-placement="bottom">
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<a class="carousel-control-prev" href="#photoCarousel" role="button" data-slide="prev">
|
|
||||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Previous</span>
|
|
||||||
</a>
|
|
||||||
<a class="carousel-control-next" href="#photoCarousel" role="button" data-slide="next">
|
|
||||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Next</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@if($status->is_nsfw)
|
|
||||||
</details>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@include('status.show.sidebar')
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@endsection
|
|
||||||
|
|
||||||
@push('meta')
|
|
||||||
<meta property="og:description" content="{{ $status->caption }}">
|
|
||||||
<meta property="og:image" content="{{$status->mediaUrl()}}">
|
|
||||||
<link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
|
|
||||||
@endpush
|
|
|
@ -1,85 +0,0 @@
|
||||||
@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
|
|
||||||
<div class="container px-0 mt-md-4">
|
|
||||||
<div class="card card-md-rounded-0 status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
|
|
||||||
<div class="row mx-0">
|
|
||||||
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
|
|
||||||
<a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
|
|
||||||
<div class="status-avatar mr-2">
|
|
||||||
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
|
|
||||||
</div>
|
|
||||||
<div class="username">
|
|
||||||
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="float-right">
|
|
||||||
<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 text-muted"></span>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
|
||||||
<a class="dropdown-item font-weight-bold" href="{{$status->reportUrl()}}">Report</a>
|
|
||||||
{{-- <a class="dropdown-item" href="#">Embed</a> --}}
|
|
||||||
@if(Auth::check())
|
|
||||||
@if(Auth::user()->profile->id !== $status->profile->id)
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<form method="post" action="/i/mute">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="user">
|
|
||||||
<input type="hidden" name="item" value="{{$status->profile_id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Mute this user</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="/i/block">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="user">
|
|
||||||
<input type="hidden" name="item" value="{{$status->profile_id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Block this user</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
{{-- <a class="dropdown-item" href="{{$status->editUrl()}}">Edit</a> --}}
|
|
||||||
<form method="post" action="/i/delete">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="post">
|
|
||||||
<input type="hidden" name="item" value="{{$status->id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Delete</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-8 status-photo px-0">
|
|
||||||
@if($status->is_nsfw && $status->media_count == 1)
|
|
||||||
<details class="details-animated">
|
|
||||||
<summary>
|
|
||||||
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
|
|
||||||
<p class="font-weight-light">(click to show)</p>
|
|
||||||
</summary>
|
|
||||||
<a class="max-hide-overflow {{$status->firstMedia()->filter_class}}" href="{{$status->url()}}">
|
|
||||||
<img class="card-img-top" src="{{$status->mediaUrl()}}" title="{{$status->firstMedia()->caption}}" data-toggle="tooltip" data-tooltip-placement="bottom">
|
|
||||||
</a>
|
|
||||||
</details>
|
|
||||||
@elseif(!$status->is_nsfw && $status->media_count == 1)
|
|
||||||
<div class="{{$status->firstMedia()->filter_class}}">
|
|
||||||
<img src="{{$status->mediaUrl()}}" width="100%" title="{{$status->firstMedia()->caption}}" data-toggle="tooltip" data-placement="bottom">
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@include('status.show.sidebar')
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@endsection
|
|
||||||
|
|
||||||
@push('meta')
|
|
||||||
<meta property="og:description" content="{{ $status->caption }}">
|
|
||||||
<meta property="og:image" content="{{$status->mediaUrl()}}">
|
|
||||||
<link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
|
|
||||||
@endpush
|
|
|
@ -1,117 +0,0 @@
|
||||||
<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="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
|
|
||||||
<div class="status-avatar mr-2">
|
|
||||||
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
|
|
||||||
</div>
|
|
||||||
<div class="username">
|
|
||||||
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="float-right">
|
|
||||||
<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 text-muted"></span>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
|
||||||
<a class="dropdown-item font-weight-bold" href="{{$status->reportUrl()}}">Report</a>
|
|
||||||
{{-- <a class="dropdown-item" href="#">Embed</a> --}}
|
|
||||||
@if(Auth::check())
|
|
||||||
@if(Auth::user()->profile->id !== $status->profile->id)
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<form method="post" action="/i/mute">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="user">
|
|
||||||
<input type="hidden" name="item" value="{{$status->profile_id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Mute this user</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="/i/block">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="user">
|
|
||||||
<input type="hidden" name="item" value="{{$status->profile_id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Block this user</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
{{-- <a class="dropdown-item" href="{{$status->editUrl()}}">Edit</a> --}}
|
|
||||||
<form method="post" action="/i/delete">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="type" value="post">
|
|
||||||
<input type="hidden" name="item" value="{{$status->id}}">
|
|
||||||
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Delete</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-md-column flex-column-reverse h-100">
|
|
||||||
<div class="card-body status-comments">
|
|
||||||
<div class="status-comment">
|
|
||||||
<p class="mb-1">
|
|
||||||
<span class="font-weight-bold pr-1">{{$status->profile->username}}</span>
|
|
||||||
<span class="comment-text" v-pre>{!! $status->rendered ?? e($status->caption) !!}</span>
|
|
||||||
</p>
|
|
||||||
<p class="mb-1"><a href="{{$status->url()}}/c" class="text-muted">View all comments</a></p>
|
|
||||||
<div class="comments">
|
|
||||||
@foreach($replies as $item)
|
|
||||||
<p class="mb-1">
|
|
||||||
<span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$item->profile->url()}}">{{ str_limit($item->profile->username, 15)}}</a></bdi></span>
|
|
||||||
<span class="comment-text" v-pre>{!! $item->rendered ?? e($item->caption) !!} <a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right pl-2">{{$item->created_at->diffForHumans(null, true, true ,true)}}</a></span>
|
|
||||||
</p>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body flex-grow-0 py-1">
|
|
||||||
<div class="reactions my-1">
|
|
||||||
@if(Auth::check())
|
|
||||||
<form class="d-inline-flex pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$status->id}}" data-action="like">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="item" value="{{$status->id}}">
|
|
||||||
<button class="btn btn-link text-dark p-0 border-0" type="submit" title="Like!">
|
|
||||||
<h3 class="m-0 {{$status->liked() ? 'fas fa-heart text-danger':'far fa-heart text-dark'}}"></h3>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<h3 class="far fa-comment pr-3 m-0" title="Comment"></h3>
|
|
||||||
<form class="d-inline-flex share-form pr-3" method="post" action="/i/share" style="display: inline;" data-id="{{$status->id}}" data-action="share" data-count="{{$status->shares_count}}">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="item" value="{{$status->id}}">
|
|
||||||
<button class="btn btn-link text-dark p-0" type="submit" title="Share">
|
|
||||||
<h3 class="m-0 {{$status->shared() ? 'fas fa-share-square text-primary':'far fa-share-square '}}"></h3>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
@endif
|
|
||||||
<span class="float-right">
|
|
||||||
<form class="d-inline-flex " method="post" action="/i/bookmark" style="display: inline;" data-id="{{$status->id}}" data-action="bookmark">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="item" value="{{$status->id}}">
|
|
||||||
<button class="btn btn-link text-dark p-0 border-0" type="submit" title="Save">
|
|
||||||
<h3 class="m-0 {{$status->bookmarked() ? 'fas fa-bookmark text-warning':'far fa-bookmark'}}"></h3>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="likes font-weight-bold mb-0">
|
|
||||||
<span class="like-count" data-count="{{$status->likes_count}}">{{$status->likes_count}}</span> likes
|
|
||||||
</div>
|
|
||||||
<div class="timestamp">
|
|
||||||
<a href="{{$status->url()}}" class="small text-muted">
|
|
||||||
{{$status->created_at->format('F j, Y')}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer bg-white sticky-md-bottom">
|
|
||||||
<form class="comment-form" method="post" action="/i/comment" data-id="{{$status->id}}" data-truncate="false">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="item" value="{{$status->id}}">
|
|
||||||
|
|
||||||
<input class="form-control" name="comment" placeholder="Add a comment…" autocomplete="off">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,50 +0,0 @@
|
||||||
@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
|
|
||||||
<div class="container px-0 mt-md-4">
|
|
||||||
<div class="card card-md-rounded-0 status-container orientation-video">
|
|
||||||
<div class="row mx-0">
|
|
||||||
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
|
|
||||||
<a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
|
|
||||||
<div class="status-avatar mr-2">
|
|
||||||
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
|
|
||||||
</div>
|
|
||||||
<div class="username">
|
|
||||||
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-8 status-photo px-0">
|
|
||||||
@if($status->is_nsfw && $status->media_count == 1)
|
|
||||||
<details class="details-animated">
|
|
||||||
<summary>
|
|
||||||
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
|
|
||||||
<p class="font-weight-light">(click to show)</p>
|
|
||||||
</summary>
|
|
||||||
<div class="embed-responsive embed-responsive-16by9">
|
|
||||||
<video class="embed-responsive-item" controls="">
|
|
||||||
<source src="{{$status->mediaUrl()}}" type="video/mp4">
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@elseif(!$status->is_nsfw && $status->media_count == 1)
|
|
||||||
<div class="embed-responsive embed-responsive-16by9">
|
|
||||||
<video class="embed-responsive-item" controls="">
|
|
||||||
<source src="{{$status->mediaUrl()}}" type="video/mp4">
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@include('status.show.sidebar')
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@endsection
|
|
||||||
|
|
||||||
@push('meta')
|
|
||||||
<meta property="og:description" content="{{ $status->caption }}">
|
|
||||||
<meta property="og:image" content="{{$status->mediaUrl()}}">
|
|
||||||
<link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
|
|
||||||
@endpush
|
|
|
@ -91,7 +91,7 @@
|
||||||
<span class="like-count">{{$item->likes_count}}</span> likes
|
<span class="like-count">{{$item->likes_count}}</span> likes
|
||||||
</div>
|
</div>
|
||||||
<div class="caption">
|
<div class="caption">
|
||||||
<p class="mb-1">
|
<p class="mb-1 read-more" style="overflow: hidden;">
|
||||||
<span class="username font-weight-bold">
|
<span class="username font-weight-bold">
|
||||||
<bdi><a class="text-dark" href="{{$item->profile->url()}}" v-pre>{{$item->profile->username}}</a></bdi>
|
<bdi><a class="text-dark" href="{{$item->profile->url()}}" v-pre>{{$item->profile->username}}</a></bdi>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
@if($status->is_nsfw)
|
|
||||||
|
|
||||||
@else
|
|
||||||
<div id="photo-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="carousel">
|
|
||||||
<ol class="carousel-indicators">
|
|
||||||
@for($i = 0; $i < $status->media_count; $i++)
|
|
||||||
<li data-target="#photo-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
|
|
||||||
@endfor
|
|
||||||
</ol>
|
|
||||||
<div class="carousel-inner">
|
|
||||||
@foreach($status->media()->orderBy('order')->get() as $media)
|
|
||||||
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
|
|
||||||
<figure class="{{$media->filter_class}}">
|
|
||||||
<span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;">{{$loop->iteration}}/{{$loop->count}}</span>
|
|
||||||
<img class="d-block w-100" src="{{$media->url()}}" alt="{{$status->caption}}">
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<a class="carousel-control-prev" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
|
|
||||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Previous</span>
|
|
||||||
</a>
|
|
||||||
<a class="carousel-control-next" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
|
|
||||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Next</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
|
@ -1,15 +0,0 @@
|
||||||
@if($status->is_nsfw)
|
|
||||||
<details class="details-animated">
|
|
||||||
<summary>
|
|
||||||
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
|
|
||||||
<p class="font-weight-light">(click to show)</p>
|
|
||||||
</summary>
|
|
||||||
<a class="max-hide-overflow {{$status->firstMedia()->filter_class}}" href="{{$status->url()}}">
|
|
||||||
<img class="card-img-top" src="{{$status->mediaUrl()}}">
|
|
||||||
</a>
|
|
||||||
</details>
|
|
||||||
@else
|
|
||||||
<div class="{{$status->firstMedia()->filter_class}}">
|
|
||||||
<img src="{{$status->mediaUrl()}}" width="100%">
|
|
||||||
</div>
|
|
||||||
@endif
|
|
|
@ -1,57 +0,0 @@
|
||||||
@if($status->is_nsfw)
|
|
||||||
<div id="video-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="false" data-interval="false">
|
|
||||||
<ol class="carousel-indicators">
|
|
||||||
@for($i = 0; $i < $status->media_count; $i++)
|
|
||||||
<li data-target="#video-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
|
|
||||||
@endfor
|
|
||||||
</ol>
|
|
||||||
<div class="carousel-inner">
|
|
||||||
@foreach($status->media()->orderBy('order')->get() as $media)
|
|
||||||
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
|
|
||||||
<span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;z-index: 999;">{{$loop->iteration}}/{{$loop->count}}</span>
|
|
||||||
<div class="embed-responsive embed-responsive-4by3">
|
|
||||||
<video class=" embed-responsive-item" controls loop>
|
|
||||||
<source src="{{$media->url()}}" type="{{$media->mime}}">
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<a class="carousel-control-prev" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
|
|
||||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Previous</span>
|
|
||||||
</a>
|
|
||||||
<a class="carousel-control-next" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
|
|
||||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Next</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div id="video-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="false" data-interval="false">
|
|
||||||
<ol class="carousel-indicators">
|
|
||||||
@for($i = 0; $i < $status->media_count; $i++)
|
|
||||||
<li data-target="#video-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
|
|
||||||
@endfor
|
|
||||||
</ol>
|
|
||||||
<div class="carousel-inner">
|
|
||||||
@foreach($status->media()->orderBy('order')->get() as $media)
|
|
||||||
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
|
|
||||||
<span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;z-index: 999;">{{$loop->iteration}}/{{$loop->count}}</span>
|
|
||||||
<div class="embed-responsive embed-responsive-4by3">
|
|
||||||
<video class=" embed-responsive-item" controls loop>
|
|
||||||
<source src="{{$media->url()}}" type="{{$media->mime}}">
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<a class="carousel-control-prev" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
|
|
||||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Previous</span>
|
|
||||||
</a>
|
|
||||||
<a class="carousel-control-next" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
|
|
||||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
|
||||||
<span class="sr-only">Next</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
|
@ -1,19 +0,0 @@
|
||||||
@if($status->is_nsfw)
|
|
||||||
<details class="details-animated">
|
|
||||||
<summary>
|
|
||||||
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
|
|
||||||
<p class="font-weight-light">(click to show)</p>
|
|
||||||
</summary>
|
|
||||||
<div class="embed-responsive embed-responsive-16by9">
|
|
||||||
<video class="video" preload="none" controls loop>
|
|
||||||
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
@else
|
|
||||||
<div class="embed-responsive embed-responsive-16by9">
|
|
||||||
<video class="video" preload="none" controls loop>
|
|
||||||
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
|
@ -1,82 +0,0 @@
|
||||||
<div class="card card-md-rounded-0 metro-classic-compose">
|
|
||||||
<div class="card-header bg-white font-weight-bold d-inline-flex justify-content-between">
|
|
||||||
<div>{{__('Create New Post')}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="statusForm">
|
|
||||||
|
|
||||||
<form method="post" action="{{route('timeline.personal')}}" enctype="multipart/form-data">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="filter_name" value="">
|
|
||||||
<input type="hidden" name="filter_class" value="">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="custom-file">
|
|
||||||
<input type="file" class="custom-file-input" id="fileInput" name="photo[]" accept="{{config('pixelfed.media_types')}}" multiple="">
|
|
||||||
<label class="custom-file-label" for="fileInput">Upload Image(s)</label>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
Max Size: @maxFileSize(). Supported formats: jpeg, png, gif, bmp. Limited to {{config('pixelfed.max_album_length')}} photos per post.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<textarea class="form-control" name="caption" placeholder="Add optional caption here" autocomplete="off" data-limit="{{config('pixelfed.max_caption_length')}}" rows="1"></textarea>
|
|
||||||
<p class="form-text text-muted small text-right">
|
|
||||||
<span class="caption-counter">0</span>
|
|
||||||
<span>/</span>
|
|
||||||
<span>{{config('pixelfed.max_caption_length')}}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<button class="btn btn-outline-primary btn-sm px-3 py-1 font-weight-bold" type="button" data-toggle="collapse" data-target="#collapsePreview" aria-expanded="false" aria-controls="collapsePreview">
|
|
||||||
Options <i class="fas fa-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
<div class="collapse" id="collapsePreview">
|
|
||||||
<div class="form-group pt-3">
|
|
||||||
<label class="font-weight-bold text-muted small">Visibility</label>
|
|
||||||
<div class="switch switch-sm">
|
|
||||||
<select class="form-control" name="visibility">
|
|
||||||
@if(Auth::user()->profile->is_private)
|
|
||||||
<option value="public">Public</option>
|
|
||||||
<option value="unlisted">Unlisted (hidden from public timelines)</option>
|
|
||||||
<option value="private" selected="">Followers Only</option>
|
|
||||||
@else
|
|
||||||
<option value="public" selected="">Public</option>
|
|
||||||
<option value="unlisted">Unlisted (hidden from public timelines)</option>
|
|
||||||
<option value="private">Followers Only</option>
|
|
||||||
@endif
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
Set the visibility of this post.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="font-weight-bold text-muted small">CW/NSFW</label>
|
|
||||||
<div class="switch switch-sm">
|
|
||||||
<input type="checkbox" class="switch" id="cw-switch" name="cw">
|
|
||||||
<label for="cw-switch" class="small font-weight-bold">(Default off)</label>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
Please mark all NSFW and controversial content, as per our content policy.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group d-none form-preview">
|
|
||||||
<label class="font-weight-bold text-muted small">Photo Preview</label>
|
|
||||||
<figure class="filterContainer">
|
|
||||||
<img class="filterPreview img-fluid">
|
|
||||||
</figure>
|
|
||||||
<small class="form-text text-muted font-weight-bold">
|
|
||||||
No filter selected.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group d-none form-filters">
|
|
||||||
<label for="filterSelectDropdown" class="font-weight-bold text-muted small">Select Filter</label>
|
|
||||||
<select class="form-control" id="filterSelectDropdown">
|
|
||||||
<option value="none" selected="">No Filter</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-outline-primary btn-block font-weight-bold">Create Post</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,68 +0,0 @@
|
||||||
@extends('layouts.app')
|
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
<script type="text/javascript" src="{{mix('js/timeline.js')}}"></script>
|
|
||||||
@endpush
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
|
|
||||||
<div class="container p-0">
|
|
||||||
<div class="col-md-10 col-lg-8 mx-auto pt-4 px-0">
|
|
||||||
@if ($errors->any())
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<ul>
|
|
||||||
@foreach ($errors->all() as $error)
|
|
||||||
<li>{{ $error }}</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@include('timeline.partial.new-form')
|
|
||||||
|
|
||||||
<div class="timeline-feed my-5" data-timeline="personal">
|
|
||||||
@foreach($timeline as $item)
|
|
||||||
|
|
||||||
@include('status.template')
|
|
||||||
|
|
||||||
@endforeach
|
|
||||||
@if($timeline->count() == 0)
|
|
||||||
<div class="card card-md-rounded-0">
|
|
||||||
<div class="card-body py-5">
|
|
||||||
<div class="d-flex justify-content-center align-items-center">
|
|
||||||
<p class="lead font-weight-bold mb-0">{{ __('timeline.emptyPersonalTimeline') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-load-status" style="display: none;">
|
|
||||||
<div class="infinite-scroll-request" style="display: none;">
|
|
||||||
<div class="fixed-top loading-page"></div>
|
|
||||||
</div>
|
|
||||||
<div class="infinite-scroll-last" style="display: none;">
|
|
||||||
<h3>No more content</h3>
|
|
||||||
<p class="text-muted">
|
|
||||||
Maybe you could try
|
|
||||||
<a href="{{route('discover')}}">discovering</a>
|
|
||||||
more people you can follow.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="infinite-scroll-error" style="display: none;">
|
|
||||||
<h3>Whoops, an error</h3>
|
|
||||||
<p class="text-muted">
|
|
||||||
Try reloading the page
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
{{$timeline->links()}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
@endsection
|
|
|
@ -1,59 +0,0 @@
|
||||||
@extends('layouts.app')
|
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
<script type="text/javascript" src="{{mix('js/timeline.js')}}"></script>
|
|
||||||
@endpush
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
|
|
||||||
<div class="container px-0">
|
|
||||||
<div class="col-md-10 col-lg-8 mx-auto pt-4 px-0">
|
|
||||||
@if ($errors->any())
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<ul>
|
|
||||||
@foreach ($errors->all() as $error)
|
|
||||||
<li>{{ $error }}</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@include('timeline.partial.new-form')
|
|
||||||
|
|
||||||
<div class="timeline-feed my-5" data-timeline="public">
|
|
||||||
@foreach($timeline as $item)
|
|
||||||
|
|
||||||
@include('status.template')
|
|
||||||
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-load-status" style="display: none;">
|
|
||||||
<div class="infinite-scroll-request" style="display: none;">
|
|
||||||
<div class="fixed-top loading-page"></div>
|
|
||||||
</div>
|
|
||||||
<div class="infinite-scroll-last" style="display: none;">
|
|
||||||
<h3>No more content</h3>
|
|
||||||
<p class="text-muted">
|
|
||||||
Maybe you could try
|
|
||||||
<a href="{{route('discover')}}">discovering</a>
|
|
||||||
more people you can follow.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="infinite-scroll-error" style="display: none;">
|
|
||||||
<h3>Whoops, an error</h3>
|
|
||||||
<p class="text-muted">
|
|
||||||
Try reloading the page
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
{{$timeline->links()}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
@endsection
|
|
|
@ -1,132 +0,0 @@
|
||||||
@extends('layouts.app')
|
|
||||||
|
|
||||||
@section('content')
|
|
||||||
|
|
||||||
<noscript>
|
|
||||||
<div class="container">
|
|
||||||
<div class="card border-left-blue mt-5">
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="mb-0 font-weight-bold">Javascript is required for an optimized experience, please enable it to use this site.</p>
|
|
||||||
<p class="mb-0 font-weight-bold text-muted">(We are working on a lite version that does not require javascript)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div class="container d-none timeline-container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8 col-lg-8 pt-4 px-0 my-3">
|
|
||||||
@if (session('status'))
|
|
||||||
<div class="alert alert-success">
|
|
||||||
<span class="font-weight-bold">{!! session('status') !!}</span>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@if (session('error'))
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<span class="font-weight-bold">{!! session('error') !!}</span>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
|
|
||||||
<div class="timeline-feed" data-timeline="{{$type}}">
|
|
||||||
|
|
||||||
@foreach($timeline as $item)
|
|
||||||
@if(is_null($item->in_reply_to_id))
|
|
||||||
@include('status.template')
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
|
|
||||||
@if($timeline->count() == 0)
|
|
||||||
<div class="card card-md-rounded-0">
|
|
||||||
<div class="card-body py-5">
|
|
||||||
<div class="d-flex justify-content-center align-items-center">
|
|
||||||
<p class="lead font-weight-bold mb-0">{{ __('timeline.emptyPersonalTimeline') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-load-status" style="display: none;">
|
|
||||||
<div class="infinite-scroll-request" style="display: none;">
|
|
||||||
<div class="fixed-top loading-page"></div>
|
|
||||||
</div>
|
|
||||||
<div class="infinite-scroll-last" style="display: none;">
|
|
||||||
<h3>No more content</h3>
|
|
||||||
<p class="text-muted">
|
|
||||||
Maybe you could try
|
|
||||||
<a href="{{route('discover')}}">discovering</a>
|
|
||||||
more people you can follow.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="infinite-scroll-error" style="display: none;">
|
|
||||||
<h3>Whoops, an error</h3>
|
|
||||||
<p class="text-muted">
|
|
||||||
Try reloading the page
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
{{$timeline->links()}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-lg-4 pt-4 my-3">
|
|
||||||
<div class="media d-flex align-items-center mb-4">
|
|
||||||
<a href="{{Auth::user()->profile->url()}}">
|
|
||||||
<img class="mr-3 rounded-circle box-shadow" src="{{Auth::user()->profile->avatarUrl()}}" alt="{{Auth::user()->username}}'s avatar" width="64px">
|
|
||||||
</a>
|
|
||||||
<div class="media-body">
|
|
||||||
<p class="mb-0 px-0 font-weight-bold"><a href="{{Auth::user()->profile->url()}}">@{{Auth::user()->username}}</a></p>
|
|
||||||
<p class="mb-0 text-muted text-truncate pb-0">{{Auth::user()->name}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<ul class="nav nav-pills flex-column timeline-sidenav" style="max-width: 240px;">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link font-weight-bold" href="/" data-type="personal">
|
|
||||||
<i class="far fa-user pr-1"></i> My Timeline
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link font-weight-bold" href="/timeline/public" data-type="local">
|
|
||||||
<i class="fas fa-bars pr-1"></i> Local Timeline
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" data-toggle="tooltip" data-placement="bottom" title="The network timeline is not available yet.">
|
|
||||||
<span class="nav-link font-weight-bold">
|
|
||||||
<i class="fas fa-globe pr-1"></i> Network Timeline
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- <follow-suggestions></follow-suggestions> --}}
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="container pb-5">
|
|
||||||
<p class="mb-0 text-uppercase font-weight-bold text-muted small">
|
|
||||||
<a href="{{route('site.about')}}" class="text-dark pr-2">About Us</a>
|
|
||||||
<a href="{{route('site.help')}}" class="text-dark pr-2">Support</a>
|
|
||||||
<a href="{{route('site.opensource')}}" class="text-dark pr-2">Open Source</a>
|
|
||||||
<a href="{{route('site.language')}}" class="text-dark pr-2">Language</a>
|
|
||||||
<a href="{{route('site.terms')}}" class="text-dark pr-2">Terms</a>
|
|
||||||
<a href="{{route('site.privacy')}}" class="text-dark pr-2">Privacy</a>
|
|
||||||
<a href="{{route('site.platform')}}" class="text-dark pr-2">API</a>
|
|
||||||
</p>
|
|
||||||
<p class="mb-0 text-uppercase font-weight-bold text-muted small">
|
|
||||||
<a href="http://pixelfed.org" class="text-muted" rel="noopener" title="version {{config('pixelfed.version')}}" data-toggle="tooltip">Powered by Pixelfed</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@endsection
|
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
<script type="text/javascript" src="{{mix('js/timeline.js')}}"></script>
|
|
||||||
@endpush
|
|
|
@ -106,11 +106,14 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
||||||
Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
|
Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
|
||||||
Route::get('loops', 'DiscoverController@loopsApi');
|
Route::get('loops', 'DiscoverController@loopsApi');
|
||||||
Route::post('loops/watch', 'DiscoverController@loopWatch');
|
Route::post('loops/watch', 'DiscoverController@loopWatch');
|
||||||
|
Route::get('discover/tag', 'DiscoverController@getHashtags');
|
||||||
});
|
});
|
||||||
Route::group(['prefix' => 'local'], function () {
|
Route::group(['prefix' => 'local'], function () {
|
||||||
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
|
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
|
||||||
Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
|
Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
|
||||||
Route::get('exp/rec', 'ApiController@userRecommendations');
|
Route::get('exp/rec', 'ApiController@userRecommendations');
|
||||||
|
Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');;
|
||||||
|
Route::get('discover/tag/list', 'HashtagFollowController@getTags');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
1
webpack.mix.js
vendored
1
webpack.mix.js
vendored
|
@ -30,6 +30,7 @@ mix.js('resources/assets/js/app.js', 'public/js')
|
||||||
.js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js')
|
.js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js')
|
||||||
// .js('resources/assets/js/embed.js', 'public')
|
// .js('resources/assets/js/embed.js', 'public')
|
||||||
// .js('resources/assets/js/direct.js', 'public/js')
|
// .js('resources/assets/js/direct.js', 'public/js')
|
||||||
|
.js('resources/assets/js/hashtag.js', 'public/js')
|
||||||
.extract([
|
.extract([
|
||||||
'lodash',
|
'lodash',
|
||||||
'popper.js',
|
'popper.js',
|
||||||
|
|
Loading…
Reference in a new issue