mirror of
https://github.com/YunoHost-Apps/pixelfed_ynh.git
synced 2024-09-03 20:06:04 +02:00
commit
093012a809
27 changed files with 860 additions and 99 deletions
|
@ -10,6 +10,7 @@
|
||||||
- Manually generate in-app registration confirmation links (php artisan user:app-magic-link) ([73eb9e36](https://github.com/pixelfed/pixelfed/commit/73eb9e36))
|
- Manually generate in-app registration confirmation links (php artisan user:app-magic-link) ([73eb9e36](https://github.com/pixelfed/pixelfed/commit/73eb9e36))
|
||||||
- Optional home feed caching ([3328b367](https://github.com/pixelfed/pixelfed/commit/3328b367))
|
- Optional home feed caching ([3328b367](https://github.com/pixelfed/pixelfed/commit/3328b367))
|
||||||
- Admin Invites ([b73ca9a1](https://github.com/pixelfed/pixelfed/commit/b73ca9a1))
|
- Admin Invites ([b73ca9a1](https://github.com/pixelfed/pixelfed/commit/b73ca9a1))
|
||||||
|
- Hashtag administration ([84872311](https://github.com/pixelfed/pixelfed/commit/84872311))
|
||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
|
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2))
|
||||||
|
@ -69,6 +70,8 @@
|
||||||
- Update reply pipelines, restore reply_count logic ([0d780ffb](https://github.com/pixelfed/pixelfed/commit/0d780ffb))
|
- Update reply pipelines, restore reply_count logic ([0d780ffb](https://github.com/pixelfed/pixelfed/commit/0d780ffb))
|
||||||
- Update StatusTagsPipeline, reject if `type` not set ([91085c45](https://github.com/pixelfed/pixelfed/commit/91085c45))
|
- Update StatusTagsPipeline, reject if `type` not set ([91085c45](https://github.com/pixelfed/pixelfed/commit/91085c45))
|
||||||
- Update ReplyPipelines, use more efficent reply count calculation ([d4dfa95c](https://github.com/pixelfed/pixelfed/commit/d4dfa95c))
|
- Update ReplyPipelines, use more efficent reply count calculation ([d4dfa95c](https://github.com/pixelfed/pixelfed/commit/d4dfa95c))
|
||||||
|
- Update StatusDelete pipeline, dispatch async ([257c0949](https://github.com/pixelfed/pixelfed/commit/257c0949))
|
||||||
|
- Update lexer/extractor to handle banned hashtags ([909a8a5a](https://github.com/pixelfed/pixelfed/commit/909a8a5a))
|
||||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||||
|
|
||||||
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)
|
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)
|
||||||
|
|
102
app/Http/Controllers/Admin/AdminHashtagsController.php
Normal file
102
app/Http/Controllers/Admin/AdminHashtagsController.php
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use Cache;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Hashtag;
|
||||||
|
use App\StatusHashtag;
|
||||||
|
use App\Http\Resources\AdminHashtag;
|
||||||
|
use App\Services\TrendingHashtagService;
|
||||||
|
|
||||||
|
trait AdminHashtagsController
|
||||||
|
{
|
||||||
|
public function hashtagsHome(Request $request)
|
||||||
|
{
|
||||||
|
return view('admin.hashtags.home');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hashtagsApi(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'action' => 'sometimes|in:banned,nsfw',
|
||||||
|
'sort' => 'sometimes|in:id,name,cached_count,can_search,can_trend,is_banned,is_nsfw',
|
||||||
|
'dir' => 'sometimes|in:asc,desc'
|
||||||
|
]);
|
||||||
|
$action = $request->input('action');
|
||||||
|
$query = $request->input('q');
|
||||||
|
$sort = $request->input('sort');
|
||||||
|
$order = $request->input('dir');
|
||||||
|
|
||||||
|
$hashtags = Hashtag::when($query, function($q, $query) {
|
||||||
|
return $q->where('name', 'like', $query . '%');
|
||||||
|
})
|
||||||
|
->when($sort, function($q, $sort) use($order) {
|
||||||
|
return $q->orderBy($sort, $order);
|
||||||
|
}, function($q) {
|
||||||
|
return $q->orderByDesc('id');
|
||||||
|
})
|
||||||
|
->when($action, function($q, $action) {
|
||||||
|
if($action === 'banned') {
|
||||||
|
return $q->whereIsBanned(true);
|
||||||
|
} else if ($action === 'nsfw') {
|
||||||
|
return $q->whereIsNsfw(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->cursorPaginate(10)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
|
return AdminHashtag::collection($hashtags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hashtagsStats(Request $request)
|
||||||
|
{
|
||||||
|
$stats = [
|
||||||
|
'total_unique' => Hashtag::count(),
|
||||||
|
'total_posts' => StatusHashtag::count(),
|
||||||
|
'added_14_days' => Hashtag::where('created_at', '>', now()->subDays(14))->count(),
|
||||||
|
'total_banned' => Hashtag::whereIsBanned(true)->count(),
|
||||||
|
'total_nsfw' => Hashtag::whereIsNsfw(true)->count()
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hashtagsGet(Request $request)
|
||||||
|
{
|
||||||
|
return new AdminHashtag(Hashtag::findOrFail($request->input('id')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hashtagsUpdate(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required',
|
||||||
|
'name' => 'required',
|
||||||
|
'slug' => 'required',
|
||||||
|
'can_search' => 'required:boolean',
|
||||||
|
'can_trend' => 'required:boolean',
|
||||||
|
'is_nsfw' => 'required:boolean',
|
||||||
|
'is_banned' => 'required:boolean'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$hashtag = Hashtag::whereSlug($request->input('slug'))->findOrFail($request->input('id'));
|
||||||
|
$canTrendPrev = $hashtag->can_trend == null ? true : $hashtag->can_trend;
|
||||||
|
$hashtag->is_banned = $request->input('is_banned');
|
||||||
|
$hashtag->is_nsfw = $request->input('is_nsfw');
|
||||||
|
$hashtag->can_search = $hashtag->is_banned ? false : $request->input('can_search');
|
||||||
|
$hashtag->can_trend = $hashtag->is_banned ? false : $request->input('can_trend');
|
||||||
|
$hashtag->save();
|
||||||
|
|
||||||
|
TrendingHashtagService::refresh();
|
||||||
|
|
||||||
|
return new AdminHashtag($hashtag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hashtagsClearTrendingCache(Request $request)
|
||||||
|
{
|
||||||
|
TrendingHashtagService::refresh();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ use App\{
|
||||||
Profile,
|
Profile,
|
||||||
Report,
|
Report,
|
||||||
Status,
|
Status,
|
||||||
|
StatusHashtag,
|
||||||
Story,
|
Story,
|
||||||
User
|
User
|
||||||
};
|
};
|
||||||
|
@ -22,6 +23,7 @@ use Illuminate\Support\Facades\Redis;
|
||||||
use App\Http\Controllers\Admin\{
|
use App\Http\Controllers\Admin\{
|
||||||
AdminDirectoryController,
|
AdminDirectoryController,
|
||||||
AdminDiscoverController,
|
AdminDiscoverController,
|
||||||
|
AdminHashtagsController,
|
||||||
AdminInstanceController,
|
AdminInstanceController,
|
||||||
AdminReportController,
|
AdminReportController,
|
||||||
// AdminGroupsController,
|
// AdminGroupsController,
|
||||||
|
@ -43,6 +45,7 @@ class AdminController extends Controller
|
||||||
use AdminReportController,
|
use AdminReportController,
|
||||||
AdminDirectoryController,
|
AdminDirectoryController,
|
||||||
AdminDiscoverController,
|
AdminDiscoverController,
|
||||||
|
AdminHashtagsController,
|
||||||
// AdminGroupsController,
|
// AdminGroupsController,
|
||||||
AdminMediaController,
|
AdminMediaController,
|
||||||
AdminSettingsController,
|
AdminSettingsController,
|
||||||
|
@ -201,12 +204,6 @@ class AdminController extends Controller
|
||||||
return view('admin.apps.home', compact('apps'));
|
return view('admin.apps.home', compact('apps'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hashtagsHome(Request $request)
|
|
||||||
{
|
|
||||||
$hashtags = Hashtag::orderByDesc('id')->paginate(10);
|
|
||||||
return view('admin.hashtags.home', compact('hashtags'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function messagesHome(Request $request)
|
public function messagesHome(Request $request)
|
||||||
{
|
{
|
||||||
$messages = Contact::orderByDesc('id')->paginate(10);
|
$messages = Contact::orderByDesc('id')->paginate(10);
|
||||||
|
|
|
@ -24,6 +24,7 @@ use App\Services\ReblogService;
|
||||||
use App\Services\StatusHashtagService;
|
use App\Services\StatusHashtagService;
|
||||||
use App\Services\SnowflakeService;
|
use App\Services\SnowflakeService;
|
||||||
use App\Services\StatusService;
|
use App\Services\StatusService;
|
||||||
|
use App\Services\TrendingHashtagService;
|
||||||
use App\Services\UserFilterService;
|
use App\Services\UserFilterService;
|
||||||
|
|
||||||
class DiscoverController extends Controller
|
class DiscoverController extends Controller
|
||||||
|
@ -181,33 +182,7 @@ class DiscoverController extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$res = Cache::remember('api:discover:v1.1:trending:hashtags', 43200, function() {
|
$res = TrendingHashtagService::getTrending();
|
||||||
$minId = StatusHashtag::where('created_at', '>', now()->subDays(14))->first();
|
|
||||||
if(!$minId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
|
|
||||||
->where('id', '>', $minId->id)
|
|
||||||
->groupBy('hashtag_id')
|
|
||||||
->orderBy('total','desc')
|
|
||||||
->take(20)
|
|
||||||
->get()
|
|
||||||
->map(function($h) {
|
|
||||||
$hashtag = Hashtag::find($h->hashtag_id);
|
|
||||||
if(!$hashtag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
'id' => $h->hashtag_id,
|
|
||||||
'total' => $h->total,
|
|
||||||
'name' => '#'.$hashtag->name,
|
|
||||||
'hashtag' => $hashtag->name,
|
|
||||||
'url' => $hashtag->url()
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->filter()
|
|
||||||
->values();
|
|
||||||
});
|
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -225,7 +225,7 @@ class StatusController extends Controller
|
||||||
StatusService::del($status->id, true);
|
StatusService::del($status->id, true);
|
||||||
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
|
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
|
||||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||||
StatusDelete::dispatchNow($status);
|
StatusDelete::dispatch($status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($request->wantsJson()) {
|
if($request->wantsJson()) {
|
||||||
|
|
29
app/Http/Resources/AdminHashtag.php
Normal file
29
app/Http/Resources/AdminHashtag.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class AdminHashtag extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||||
|
*/
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'slug' => $this->slug,
|
||||||
|
'can_trend' => $this->can_trend === null ? true : (bool) $this->can_trend,
|
||||||
|
'can_search' => $this->can_search === null ? true : (bool) $this->can_search,
|
||||||
|
'is_nsfw' => (bool) $this->is_nsfw,
|
||||||
|
'is_banned' => (bool) $this->is_banned,
|
||||||
|
'cached_count' => $this->cached_count ?? 0,
|
||||||
|
'created_at' => $this->created_at
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,7 +41,7 @@ class MediaDeletePipeline implements ShouldQueue
|
||||||
array_pop($e);
|
array_pop($e);
|
||||||
$i = implode('/', $e);
|
$i = implode('/', $e);
|
||||||
|
|
||||||
if(config('pixelfed.cloud_storage') == true) {
|
if(config_cache('pixelfed.cloud_storage') == true) {
|
||||||
$disk = Storage::disk(config('filesystems.cloud'));
|
$disk = Storage::disk(config('filesystems.cloud'));
|
||||||
|
|
||||||
if($path && $disk->exists($path)) {
|
if($path && $disk->exists($path)) {
|
||||||
|
@ -63,9 +63,9 @@ class MediaDeletePipeline implements ShouldQueue
|
||||||
$disk->delete($thumb);
|
$disk->delete($thumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
$media->forceDelete();
|
$media->delete();
|
||||||
|
|
||||||
return;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,9 @@ class StatusDelete implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public $deleteWhenMissingModels = true;
|
public $deleteWhenMissingModels = true;
|
||||||
|
|
||||||
|
public $timeout = 900;
|
||||||
|
public $tries = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
*
|
*
|
||||||
|
@ -131,7 +134,7 @@ class StatusDelete implements ShouldQueue
|
||||||
->where('item_id', $status->id)
|
->where('item_id', $status->id)
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
$status->forceDelete();
|
$status->delete();
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ use App\Mention;
|
||||||
use App\Services\AccountService;
|
use App\Services\AccountService;
|
||||||
use App\Hashtag;
|
use App\Hashtag;
|
||||||
use App\StatusHashtag;
|
use App\StatusHashtag;
|
||||||
|
use App\Services\TrendingHashtagService;
|
||||||
|
|
||||||
class StatusTagsPipeline implements ShouldQueue
|
class StatusTagsPipeline implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
@ -61,6 +62,14 @@ class StatusTagsPipeline implements ShouldQueue
|
||||||
$name = substr($tag['name'], 0, 1) == '#' ?
|
$name = substr($tag['name'], 0, 1) == '#' ?
|
||||||
substr($tag['name'], 1) : $tag['name'];
|
substr($tag['name'], 1) : $tag['name'];
|
||||||
|
|
||||||
|
$banned = TrendingHashtagService::getBannedHashtagNames();
|
||||||
|
|
||||||
|
if(count($banned)) {
|
||||||
|
if(in_array(strtolower($name), array_map('strtolower', $banned))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$hashtag = Hashtag::firstOrCreate([
|
$hashtag = Hashtag::firstOrCreate([
|
||||||
'slug' => str_slug($name)
|
'slug' => str_slug($name)
|
||||||
], [
|
], [
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use DB;
|
||||||
use App\StatusHashtag;
|
use App\StatusHashtag;
|
||||||
use App\Services\StatusHashtagService;
|
use App\Services\StatusHashtagService;
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ class StatusHashtagObserver
|
||||||
public function created(StatusHashtag $hashtag)
|
public function created(StatusHashtag $hashtag)
|
||||||
{
|
{
|
||||||
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
|
||||||
|
DB::table('hashtags')->where('id', $hashtag->hashtag_id)->increment('cached_count');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,6 +47,7 @@ class StatusHashtagObserver
|
||||||
public function deleted(StatusHashtag $hashtag)
|
public function deleted(StatusHashtag $hashtag)
|
||||||
{
|
{
|
||||||
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
|
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
|
||||||
|
DB::table('hashtags')->where('id', $hashtag->hashtag_id)->decrement('cached_count');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -96,16 +96,9 @@ class SearchApiV2Service
|
||||||
$query = substr($rawQuery, 1) . '%';
|
$query = substr($rawQuery, 1) . '%';
|
||||||
}
|
}
|
||||||
$banned = InstanceService::getBannedDomains();
|
$banned = InstanceService::getBannedDomains();
|
||||||
$results = Profile::select('profiles.*', 'followers.profile_id', 'followers.created_at')
|
$results = Profile::select('username', 'id', 'followers_count', 'domain')
|
||||||
->whereNull('status')
|
|
||||||
->leftJoin('followers', function($join) use($user) {
|
|
||||||
return $join->on('profiles.id', '=', 'followers.following_id')
|
|
||||||
->where('followers.profile_id', $user->profile_id);
|
|
||||||
})
|
|
||||||
->where('username', 'like', $query)
|
->where('username', 'like', $query)
|
||||||
->orderBy('domain')
|
|
||||||
->orderByDesc('profiles.followers_count')
|
->orderByDesc('profiles.followers_count')
|
||||||
->orderByDesc('followers.created_at')
|
|
||||||
->offset($offset)
|
->offset($offset)
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
->get()
|
->get()
|
||||||
|
@ -131,7 +124,7 @@ class SearchApiV2Service
|
||||||
$limit = $this->query->input('limit') ?? 20;
|
$limit = $this->query->input('limit') ?? 20;
|
||||||
$offset = $this->query->input('offset') ?? 0;
|
$offset = $this->query->input('offset') ?? 0;
|
||||||
$query = '%' . $this->query->input('q') . '%';
|
$query = '%' . $this->query->input('q') . '%';
|
||||||
return Hashtag::whereIsBanned(false)
|
return Hashtag::where('can_search', true)
|
||||||
->where('name', 'like', $query)
|
->where('name', 'like', $query)
|
||||||
->offset($offset)
|
->offset($offset)
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
|
|
103
app/Services/TrendingHashtagService.php
Normal file
103
app/Services/TrendingHashtagService.php
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use App\Hashtag;
|
||||||
|
use App\StatusHashtag;
|
||||||
|
|
||||||
|
class TrendingHashtagService
|
||||||
|
{
|
||||||
|
const CACHE_KEY = 'api:discover:v1.1:trending:hashtags';
|
||||||
|
|
||||||
|
public static function key($k = null)
|
||||||
|
{
|
||||||
|
return self::CACHE_KEY . $k;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getBannedHashtags()
|
||||||
|
{
|
||||||
|
return Cache::remember(self::key(':is_banned'), 1209600, function() {
|
||||||
|
return Hashtag::whereIsBanned(true)->pluck('id')->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getBannedHashtagNames()
|
||||||
|
{
|
||||||
|
return Cache::remember(self::key(':is_banned:names'), 1209600, function() {
|
||||||
|
return Hashtag::find(self::getBannedHashtags())->pluck('name')->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNonTrendingHashtags()
|
||||||
|
{
|
||||||
|
return Cache::remember(self::key(':can_trend'), 1209600, function() {
|
||||||
|
return Hashtag::whereCanTrend(false)->pluck('id')->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNsfwHashtags()
|
||||||
|
{
|
||||||
|
return Cache::remember(self::key(':is_nsfw'), 1209600, function() {
|
||||||
|
return Hashtag::whereIsNsfw(true)->pluck('id')->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMinRecentId()
|
||||||
|
{
|
||||||
|
return Cache::remember(self::key('-min-id'), 86400, function() {
|
||||||
|
$minId = StatusHashtag::where('created_at', '>', now()->subMinutes(config('trending.hashtags.recency_mins')))->first();
|
||||||
|
if(!$minId) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return $minId->id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getTrending()
|
||||||
|
{
|
||||||
|
$minId = self::getMinRecentId();
|
||||||
|
|
||||||
|
$skipIds = array_merge(self::getBannedHashtags(), self::getNonTrendingHashtags(), self::getNsfwHashtags());
|
||||||
|
|
||||||
|
return Cache::remember(self::CACHE_KEY, config('trending.hashtags.ttl'), function() use($minId, $skipIds) {
|
||||||
|
return StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
|
||||||
|
->whereNotIn('hashtag_id', $skipIds)
|
||||||
|
->where('id', '>', $minId)
|
||||||
|
->groupBy('hashtag_id')
|
||||||
|
->orderBy('total', 'desc')
|
||||||
|
->take(config('trending.hashtags.limit'))
|
||||||
|
->get()
|
||||||
|
->map(function($h) {
|
||||||
|
$hashtag = Hashtag::find($h->hashtag_id);
|
||||||
|
if(!$hashtag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $h->hashtag_id,
|
||||||
|
'total' => $h->total,
|
||||||
|
'name' => '#'.$hashtag->name,
|
||||||
|
'hashtag' => $hashtag->name,
|
||||||
|
'url' => $hashtag->url()
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->values();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function del($k)
|
||||||
|
{
|
||||||
|
return Cache::forget(self::key($k));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function refresh()
|
||||||
|
{
|
||||||
|
Cache::forget(self::key(':is_banned'));
|
||||||
|
Cache::forget(self::key(':is_nsfw'));
|
||||||
|
Cache::forget(self::key(':can_trend'));
|
||||||
|
Cache::forget(self::key('-min-id'));
|
||||||
|
Cache::forget(self::key());
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ namespace App\Util\Lexer;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use App\Status;
|
use App\Status;
|
||||||
use App\Services\AutolinkService;
|
use App\Services\AutolinkService;
|
||||||
|
use App\Services\TrendingHashtagService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Twitter Extractor Class.
|
* Twitter Extractor Class.
|
||||||
|
@ -267,6 +268,8 @@ class Extractor extends Regex
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bannedTags = config('app.env') === 'production' ? TrendingHashtagService::getBannedHashtagNames() : [];
|
||||||
|
|
||||||
preg_match_all(self::$patterns['valid_hashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
preg_match_all(self::$patterns['valid_hashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||||
$tags = [];
|
$tags = [];
|
||||||
|
|
||||||
|
@ -278,7 +281,12 @@ 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) {
|
if (count($bannedTags)) {
|
||||||
|
if(in_array(strtolower($hashtag[0]), array_map('strtolower', $bannedTags))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mb_strlen($hashtag[0]) > 124) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$tags[] = [
|
$tags[] = [
|
||||||
|
|
9
config/trending.php
Normal file
9
config/trending.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'hashtags' => [
|
||||||
|
'ttl' => env('PF_HASHTAGS_TRENDING_TTL', 43200),
|
||||||
|
'recency_mins' => env('PF_HASHTAGS_TRENDING_RECENCY_MINS', 20160),
|
||||||
|
'limit' => env('PF_HASHTAGS_TRENDING_LIMIT', 20)
|
||||||
|
]
|
||||||
|
];
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('hashtags', function (Blueprint $table) {
|
||||||
|
$table->unsignedInteger('cached_count')->nullable();
|
||||||
|
$table->boolean('can_trend')->nullable()->index()->after('slug');
|
||||||
|
$table->boolean('can_search')->nullable()->index()->after('can_trend');
|
||||||
|
$table->index('is_nsfw');
|
||||||
|
$table->index('is_banned');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('hashtags', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('cached_count');
|
||||||
|
$table->dropColumn('can_trend');
|
||||||
|
$table->dropColumn('can_search');
|
||||||
|
$table->dropIndex('hashtags_is_nsfw_index');
|
||||||
|
$table->dropIndex('hashtags_is_banned_index');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Hashtag;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Hashtag::withoutEvents(function() {
|
||||||
|
Hashtag::chunkById(50, function($hashtags) {
|
||||||
|
foreach($hashtags as $hashtag) {
|
||||||
|
$count = DB::table('status_hashtags')->whereHashtagId($hashtag->id)->count();
|
||||||
|
$hashtag->cached_count = $count;
|
||||||
|
$hashtag->can_trend = true;
|
||||||
|
$hashtag->can_search = true;
|
||||||
|
$hashtag->save();
|
||||||
|
}
|
||||||
|
}, 'id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
2
public/css/admin.css
vendored
2
public/css/admin.css
vendored
File diff suppressed because one or more lines are too long
2
public/js/admin.js
vendored
2
public/js/admin.js
vendored
File diff suppressed because one or more lines are too long
2
public/js/vendor.js
vendored
2
public/js/vendor.js
vendored
File diff suppressed because one or more lines are too long
|
@ -49,7 +49,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Pusher JavaScript Library v7.5.0
|
* Pusher JavaScript Library v7.6.0
|
||||||
* https://pusher.com/
|
* https://pusher.com/
|
||||||
*
|
*
|
||||||
* Copyright 2020, Pusher
|
* Copyright 2020, Pusher
|
||||||
|
@ -65,14 +65,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Sizzle CSS Selector Engine v2.3.6
|
* Sizzle CSS Selector Engine v2.3.8
|
||||||
* https://sizzlejs.com/
|
* https://sizzlejs.com/
|
||||||
*
|
*
|
||||||
* Copyright JS Foundation and other contributors
|
* Copyright JS Foundation and other contributors
|
||||||
* Released under the MIT license
|
* Released under the MIT license
|
||||||
* https://js.foundation/
|
* https://js.foundation/
|
||||||
*
|
*
|
||||||
* Date: 2021-02-16
|
* Date: 2022-11-16
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* jQuery JavaScript Library v3.6.1
|
* jQuery JavaScript Library v3.6.2
|
||||||
* https://jquery.com/
|
* https://jquery.com/
|
||||||
*
|
*
|
||||||
* Includes Sizzle.js
|
* Includes Sizzle.js
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
* Released under the MIT license
|
* Released under the MIT license
|
||||||
* https://jquery.org/license
|
* https://jquery.org/license
|
||||||
*
|
*
|
||||||
* Date: 2022-08-26T17:52Z
|
* Date: 2022-12-13T14:56Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"/js/profile-directory.js": "/js/profile-directory.js?id=62b575734ca1d8e8b780b5dbcde82680",
|
"/js/profile-directory.js": "/js/profile-directory.js?id=62b575734ca1d8e8b780b5dbcde82680",
|
||||||
"/js/story-compose.js": "/js/story-compose.js?id=9d606ec8de7ba57ed1402c531a3937ed",
|
"/js/story-compose.js": "/js/story-compose.js?id=9d606ec8de7ba57ed1402c531a3937ed",
|
||||||
"/js/direct.js": "/js/direct.js?id=83f62237dcbdcd3c3b0dd97ebb8cf4aa",
|
"/js/direct.js": "/js/direct.js?id=83f62237dcbdcd3c3b0dd97ebb8cf4aa",
|
||||||
"/js/admin.js": "/js/admin.js?id=145e57a8fe4986cf8fce7378284e8c1f",
|
"/js/admin.js": "/js/admin.js?id=09ff5d52a465c7c7e9e04209eeb76df6",
|
||||||
"/js/rempro.js": "/js/rempro.js?id=61bb49ccfe70d28ed788750f9c6279b2",
|
"/js/rempro.js": "/js/rempro.js?id=61bb49ccfe70d28ed788750f9c6279b2",
|
||||||
"/js/rempos.js": "/js/rempos.js?id=da10eddc2edd1d3a29d8ffcd75d239dc",
|
"/js/rempos.js": "/js/rempos.js?id=da10eddc2edd1d3a29d8ffcd75d239dc",
|
||||||
"/js/live-player.js": "/js/live-player.js?id=674d2b72d4cf417d9d7a3953c55f37ca",
|
"/js/live-player.js": "/js/live-player.js?id=674d2b72d4cf417d9d7a3953c55f37ca",
|
||||||
|
@ -43,8 +43,8 @@
|
||||||
"/css/appdark.css": "/css/appdark.css?id=de85ecce91d9ed7afa7714547eb1e26c",
|
"/css/appdark.css": "/css/appdark.css?id=de85ecce91d9ed7afa7714547eb1e26c",
|
||||||
"/css/app.css": "/css/app.css?id=88a0a931d5b0e24b0d9355f548414768",
|
"/css/app.css": "/css/app.css?id=88a0a931d5b0e24b0d9355f548414768",
|
||||||
"/css/portfolio.css": "/css/portfolio.css?id=db2c9929a56d83f9ff2aaf2161d29d36",
|
"/css/portfolio.css": "/css/portfolio.css?id=db2c9929a56d83f9ff2aaf2161d29d36",
|
||||||
"/css/admin.css": "/css/admin.css?id=c39d4fbc91a140c22cf5afe5d9faa827",
|
"/css/admin.css": "/css/admin.css?id=619b6c6613a24e232048856e72110862",
|
||||||
"/css/landing.css": "/css/landing.css?id=e852a642699916fc9ff8208d7e06daa8",
|
"/css/landing.css": "/css/landing.css?id=e852a642699916fc9ff8208d7e06daa8",
|
||||||
"/css/spa.css": "/css/spa.css?id=602c4f74ce800b7bf45a8d8a4d8cb6e5",
|
"/css/spa.css": "/css/spa.css?id=602c4f74ce800b7bf45a8d8a4d8cb6e5",
|
||||||
"/js/vendor.js": "/js/vendor.js?id=cedafb53a2de5dd37758d3009b4b21c1"
|
"/js/vendor.js": "/js/vendor.js?id=be64338fb941b8e58b836490ef0e96be"
|
||||||
}
|
}
|
||||||
|
|
462
resources/assets/components/admin/AdminHashtags.vue
Normal file
462
resources/assets/components/admin/AdminHashtags.vue
Normal file
|
@ -0,0 +1,462 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="header bg-primary pb-3 mt-n4">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="header-body">
|
||||||
|
<div class="row align-items-center py-4">
|
||||||
|
<div class="col-lg-6 col-7">
|
||||||
|
<p class="display-1 text-white d-inline-block mb-0">Hashtags</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-2 col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="text-light text-uppercase mb-0">Unique Hashtags</h5>
|
||||||
|
<span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.total_unique) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-2 col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="text-light text-uppercase mb-0">Total Hashtags</h5>
|
||||||
|
<span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.total_posts) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-2 col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="text-light text-uppercase mb-0">New (past 14 days)</h5>
|
||||||
|
<span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.added_14_days) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-2 col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="text-light text-uppercase mb-0">Banned Hashtags</h5>
|
||||||
|
<span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.total_banned) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-2 col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="text-light text-uppercase mb-0">NSFW Hashtags</h5>
|
||||||
|
<span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.total_nsfw) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-2 col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="text-light text-uppercase mb-0">Clear Trending Cache</h5>
|
||||||
|
<button class="btn btn-outline-white btn-block btn-sm py-0 mt-1" @click="clearTrendingCache">Clear Cache</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loaded" class="my-5 text-center">
|
||||||
|
<b-spinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="m-n2 m-lg-4">
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row mb-3 justify-content-between">
|
||||||
|
<div class="col-12 col-md-8">
|
||||||
|
<ul class="nav nav-pills">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button :class="['nav-link', { active: tabIndex == 0}]" @click="toggleTab(0)">All</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button :class="['nav-link', { active: tabIndex == 1}]" @click="toggleTab(1)">Trending</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button :class="['nav-link', { active: tabIndex == 2}]" @click="toggleTab(2)">Banned</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button :class="['nav-link', { active: tabIndex == 3}]" @click="toggleTab(3)">NSFW</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<autocomplete
|
||||||
|
:search="composeSearch"
|
||||||
|
:disabled="searchLoading"
|
||||||
|
placeholder="Search hashtags"
|
||||||
|
aria-label="Search hashtags"
|
||||||
|
:get-result-value="getTagResultValue"
|
||||||
|
@submit="onSearchResultClick"
|
||||||
|
ref="autocomplete"
|
||||||
|
>
|
||||||
|
<template #result="{ result, props }">
|
||||||
|
<li
|
||||||
|
v-bind="props"
|
||||||
|
class="autocomplete-result d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<div class="font-weight-bold" :class="{ 'text-danger': result.is_banned }">
|
||||||
|
#{{ result.name }}
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
{{ prettyCount(result.cached_count) }} posts
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</autocomplete>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="[0, 2, 3].includes(this.tabIndex)" class="table-responsive">
|
||||||
|
<table class="table table-dark">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="cursor-pointer" v-html="buildColumn('ID', 'id')" @click="toggleCol('id')"></th>
|
||||||
|
<th scope="col" class="cursor-pointer" v-html="buildColumn('Hashtag', 'name')" @click="toggleCol('name')"></th>
|
||||||
|
<th scope="col" class="cursor-pointer" v-html="buildColumn('Count', 'cached_count')" @click="toggleCol('cached_count')"></th>
|
||||||
|
<th scope="col" class="cursor-pointer" v-html="buildColumn('Can Search', 'can_search')" @click="toggleCol('can_search')"></th>
|
||||||
|
<th scope="col" class="cursor-pointer" v-html="buildColumn('Can Trend', 'can_trend')" @click="toggleCol('can_trend')"></th>
|
||||||
|
<th scope="col" class="cursor-pointer" v-html="buildColumn('NSFW', 'is_nsfw')" @click="toggleCol('is_nsfw')"></th>
|
||||||
|
<th scope="col" class="cursor-pointer" v-html="buildColumn('Banned', 'is_banned')" @click="toggleCol('is_banned')"></th>
|
||||||
|
<th scope="col">Created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(hashtag, idx) in hashtags">
|
||||||
|
<td class="font-weight-bold text-monospace text-muted">
|
||||||
|
<a href="#" @click.prevent="openEditHashtagModal(hashtag, idx)">
|
||||||
|
{{ hashtag.id }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="font-weight-bold">{{ hashtag.name }}</td>
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<a :href="`/i/web/hashtag/${hashtag.slug}`">
|
||||||
|
{{ hashtag.cached_count ?? 0 }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="font-weight-bold" v-html="boolIcon(hashtag.can_search, 'text-success', 'text-danger')"></td>
|
||||||
|
<td class="font-weight-bold" v-html="boolIcon(hashtag.can_trend, 'text-success', 'text-danger')"></td>
|
||||||
|
<td class="font-weight-bold" v-html="boolIcon(hashtag.is_nsfw, 'text-danger')"></td>
|
||||||
|
<td class="font-weight-bold" v-html="boolIcon(hashtag.is_banned, 'text-danger')"></td>
|
||||||
|
<td class="font-weight-bold">{{ timeAgo(hashtag.created_at) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="[0, 2, 3].includes(this.tabIndex)" class="d-flex align-items-center justify-content-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary rounded-pill"
|
||||||
|
:disabled="!pagination.prev"
|
||||||
|
@click="paginate('prev')">
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary rounded-pill"
|
||||||
|
:disabled="!pagination.next"
|
||||||
|
@click="paginate('next')">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="this.tabIndex == 1" class="table-responsive">
|
||||||
|
<table class="table table-dark">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">Hashtag</th>
|
||||||
|
<th scope="col">Trending Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(hashtag, idx) in trendingTags">
|
||||||
|
<td class="font-weight-bold text-monospace text-muted">
|
||||||
|
<a href="#" @click.prevent="openEditHashtagModal(hashtag, idx)">
|
||||||
|
{{ hashtag.id }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="font-weight-bold">{{ hashtag.hashtag }}</td>
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<a :href="`/i/web/hashtag/${hashtag.hashtag}`">
|
||||||
|
{{ hashtag.total ?? 0 }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-modal v-model="showEditModal" title="Edit Hashtag" :ok-only="true" :lazy="true" :static="true">
|
||||||
|
<div v-if="editingHashtag && editingHashtag.name" class="list-group">
|
||||||
|
<div class="list-group-item d-flex align-items-center justify-content-between">
|
||||||
|
<div class="text-muted small">Name</div>
|
||||||
|
<div class="font-weight-bold">{{ editingHashtag.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex align-items-center justify-content-between">
|
||||||
|
<div class="text-muted small">Total Uses</div>
|
||||||
|
<div class="font-weight-bold">{{ editingHashtag.cached_count.toLocaleString('en-CA', { compactDisplay: "short"}) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex align-items-center justify-content-between">
|
||||||
|
<div class="text-muted small">Can Trend</div>
|
||||||
|
<div class="mr-n2 mb-1">
|
||||||
|
<b-form-checkbox v-model="editingHashtag.can_trend" switch size="lg"></b-form-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex align-items-center justify-content-between">
|
||||||
|
<div class="text-muted small">Can Search</div>
|
||||||
|
<div class="mr-n2 mb-1">
|
||||||
|
<b-form-checkbox v-model="editingHashtag.can_search" switch size="lg"></b-form-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex align-items-center justify-content-between">
|
||||||
|
<div class="text-muted small">Banned</div>
|
||||||
|
<div class="mr-n2 mb-1">
|
||||||
|
<b-form-checkbox v-model="editingHashtag.is_banned" switch size="lg"></b-form-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex align-items-center justify-content-between">
|
||||||
|
<div class="text-muted small">NSFW</div>
|
||||||
|
<div class="mr-n2 mb-1">
|
||||||
|
<b-form-checkbox v-model="editingHashtag.is_nsfw" switch size="lg"></b-form-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="editingHashtag && editingHashtag.name && editSaved">
|
||||||
|
<p class="text-primary small font-weight-bold text-center mt-1 mb-0">Hashtag changes successfully saved!</p>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</b-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
import Autocomplete from '@trevoreyre/autocomplete-vue'
|
||||||
|
import '@trevoreyre/autocomplete-vue/dist/style.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Autocomplete,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
tabIndex: 0,
|
||||||
|
stats: {
|
||||||
|
"total_unique": 0,
|
||||||
|
"total_posts": 0,
|
||||||
|
"added_14_days": 0,
|
||||||
|
"total_banned": 0,
|
||||||
|
"total_nsfw": 0
|
||||||
|
},
|
||||||
|
hashtags: [],
|
||||||
|
pagination: [],
|
||||||
|
sortCol: undefined,
|
||||||
|
sortDir: undefined,
|
||||||
|
trendingTags: [],
|
||||||
|
bannedTags: [],
|
||||||
|
showEditModal: false,
|
||||||
|
editingHashtag: undefined,
|
||||||
|
editSaved: false,
|
||||||
|
editSavedTimeout: undefined,
|
||||||
|
searchLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchStats();
|
||||||
|
this.fetchHashtags();
|
||||||
|
|
||||||
|
this.$root.$on('bv::modal::hidden', (bvEvent, modalId) => {
|
||||||
|
this.editSaved = false;
|
||||||
|
clearTimeout(this.editSavedTimeout);
|
||||||
|
this.editingHashtag = undefined;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
editingHashtag: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler: function(updated, old) {
|
||||||
|
if(updated != null && old != null) {
|
||||||
|
this.storeHashtagEdit(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchStats() {
|
||||||
|
axios.get('/i/admin/api/hashtags/stats')
|
||||||
|
.then(res => {
|
||||||
|
this.stats = res.data;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchHashtags(url = '/i/admin/api/hashtags/query') {
|
||||||
|
axios.get(url)
|
||||||
|
.then(res => {
|
||||||
|
this.hashtags = res.data.data;
|
||||||
|
this.pagination = {
|
||||||
|
next: res.data.links.next,
|
||||||
|
prev: res.data.links.prev
|
||||||
|
};
|
||||||
|
this.loaded = true;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
prettyCount(str) {
|
||||||
|
if(str) {
|
||||||
|
return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
},
|
||||||
|
|
||||||
|
timeAgo(str) {
|
||||||
|
if(!str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return App.util.format.timeAgo(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
boolIcon(val, success = 'text-success', danger = 'text-muted') {
|
||||||
|
if(val) {
|
||||||
|
return `<i class="far fa-check-circle fa-lg ${success}"></i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<i class="far fa-times-circle fa-lg ${danger}"></i>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
paginate(dir) {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
|
||||||
|
this.fetchHashtags(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleCol(col) {
|
||||||
|
this.sortCol = col;
|
||||||
|
|
||||||
|
if(!this.sortDir) {
|
||||||
|
this.sortDir = 'desc';
|
||||||
|
} else {
|
||||||
|
this.sortDir = this.sortDir == 'asc' ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = '/i/admin/api/hashtags/query?sort=' + col + '&dir=' + this.sortDir;
|
||||||
|
this.fetchHashtags(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
buildColumn(name, col) {
|
||||||
|
let icon = `<i class="far fa-sort"></i>`;
|
||||||
|
if(col == this.sortCol) {
|
||||||
|
icon = this.sortDir == 'desc' ?
|
||||||
|
`<i class="far fa-sort-up"></i>` :
|
||||||
|
`<i class="far fa-sort-down"></i>`
|
||||||
|
}
|
||||||
|
return `${name} ${icon}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTab(idx) {
|
||||||
|
this.loaded = false;
|
||||||
|
this.tabIndex = idx;
|
||||||
|
|
||||||
|
if(idx === 0) {
|
||||||
|
this.fetchHashtags();
|
||||||
|
} else if(idx === 1) {
|
||||||
|
axios.get('/api/v1.1/discover/posts/hashtags')
|
||||||
|
.then(res => {
|
||||||
|
this.trendingTags = res.data;
|
||||||
|
this.loaded = true;
|
||||||
|
})
|
||||||
|
} else if(idx === 2) {
|
||||||
|
let url = '/i/admin/api/hashtags/query?action=banned';
|
||||||
|
this.fetchHashtags(url);
|
||||||
|
} else if(idx === 3) {
|
||||||
|
let url = '/i/admin/api/hashtags/query?action=nsfw';
|
||||||
|
this.fetchHashtags(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openEditHashtagModal(hashtag) {
|
||||||
|
this.editSaved = false;
|
||||||
|
clearTimeout(this.editSavedTimeout);
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
axios.get('/i/admin/api/hashtags/get', {
|
||||||
|
params: {
|
||||||
|
id: hashtag.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.editingHashtag = res.data.data;
|
||||||
|
this.showEditModal = true;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
storeHashtagEdit(hashtag, idx) {
|
||||||
|
this.editSaved = false;
|
||||||
|
|
||||||
|
if(hashtag.is_banned && (hashtag.can_trend || hashtag.can_search)) {
|
||||||
|
swal('Banned Hashtag Limits', 'Banned hashtags cannot trend or be searchable, to allow those you need to unban the hashtag', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post('/i/admin/api/hashtags/update', hashtag)
|
||||||
|
.then(res => {
|
||||||
|
this.editSaved = true;
|
||||||
|
|
||||||
|
if(this.tabIndex !== 1) {
|
||||||
|
this.hashtags = this.hashtags.map(h => {
|
||||||
|
if(h.id == hashtag.id) {
|
||||||
|
h = res.data.data
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editSavedTimeout = setTimeout(() => {
|
||||||
|
this.editSaved = false;
|
||||||
|
}, 5000);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
swal('Oops!', 'An error occured, please try again.', 'error');
|
||||||
|
console.log(err);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
composeSearch(input) {
|
||||||
|
if (input.length < 1) { return []; };
|
||||||
|
return axios.get('/i/admin/api/hashtags/query', {
|
||||||
|
params: {
|
||||||
|
q: input,
|
||||||
|
sort: 'cached_count',
|
||||||
|
dir: 'desc'
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
return res.data.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getTagResultValue(result) {
|
||||||
|
return result.name;
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchResultClick(result) {
|
||||||
|
this.openEditHashtagModal(result);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTrendingCache() {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
if(!window.confirm('Are you sure you want to clear the trending hashtags cache?')){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
axios.post('/i/admin/api/hashtags/clear-trending-cache')
|
||||||
|
.then(res => {
|
||||||
|
swal('Cache Cleared!', 'Successfully cleared the trending hashtag cache!', 'success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
10
resources/assets/js/admin.js
vendored
10
resources/assets/js/admin.js
vendored
|
@ -20,3 +20,13 @@ Chart.defaults.global.defaultFontFamily = "-apple-system,BlinkMacSystemFont,Sego
|
||||||
Array.from(document.querySelectorAll('.pagination .page-link'))
|
Array.from(document.querySelectorAll('.pagination .page-link'))
|
||||||
.filter(el => el.textContent === '« Previous' || el.textContent === 'Next »')
|
.filter(el => el.textContent === '« Previous' || el.textContent === 'Next »')
|
||||||
.forEach(el => el.textContent = (el.textContent === 'Next »' ? '›' :'‹'));
|
.forEach(el => el.textContent = (el.textContent === 'Next »' ? '›' :'‹'));
|
||||||
|
|
||||||
|
Vue.component(
|
||||||
|
'admin-directory',
|
||||||
|
require('./../components/admin/AdminDirectory.vue').default
|
||||||
|
);
|
||||||
|
|
||||||
|
Vue.component(
|
||||||
|
'hashtag-component',
|
||||||
|
require('./../components/admin/AdminHashtags.vue').default
|
||||||
|
);
|
||||||
|
|
2
resources/assets/sass/lib/argon.css
vendored
2
resources/assets/sass/lib/argon.css
vendored
|
@ -22193,7 +22193,7 @@ textarea[resize='horizontal']
|
||||||
|
|
||||||
.sidenav
|
.sidenav
|
||||||
{
|
{
|
||||||
z-index: 1050;
|
z-index: 1040;
|
||||||
|
|
||||||
transition: all .4s ease;
|
transition: all .4s ease;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,13 @@
|
||||||
@extends('admin.partial.template-full')
|
@extends('admin.partial.template-full')
|
||||||
|
|
||||||
@section('section')
|
@section('section')
|
||||||
<div class="title">
|
|
||||||
<h3 class="font-weight-bold d-inline-block">Hashtags</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hashtag-component />
|
||||||
<table class="table table-responsive">
|
|
||||||
<thead class="bg-light">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" width="10%">#</th>
|
|
||||||
<th scope="col" width="30%">Hashtag</th>
|
|
||||||
<th scope="col" width="15%">Status Count</th>
|
|
||||||
<th scope="col" width="10%">NSFW</th>
|
|
||||||
<th scope="col" width="10%">Banned</th>
|
|
||||||
<th scope="col" width="15%">Created</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach($hashtags as $tag)
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="/i/admin/apps/show/{{$tag->id}}" class="btn btn-sm btn-outline-primary">
|
|
||||||
{{$tag->id}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">{{$tag->name}}</td>
|
|
||||||
<td class="font-weight-bold text-center">
|
|
||||||
<a href="{{$tag->url()}}">
|
|
||||||
{{$tag->posts()->count()}}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">{{$tag->is_nsfw ? 'true' : 'false'}}</td>
|
|
||||||
<td class="font-weight-bold">{{$tag->is_banned ? 'true' : 'false'}}</td>
|
|
||||||
<td class="font-weight-bold">{{$tag->created_at->diffForHumans()}}</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="d-flex justify-content-center mt-5 small">
|
|
||||||
{{$hashtags->links()}}
|
|
||||||
</div>
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="text/javascript">
|
||||||
|
new Vue({ el: '#panel'});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<a class="nav-link pr-0" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link pr-0" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<div class="media align-items-center">
|
<div class="media align-items-center">
|
||||||
<span class="avatar avatar-sm rounded-circle">
|
<span class="avatar avatar-sm rounded-circle">
|
||||||
<img alt="avatar" src="{{request()->user()->profile->avatarUrl()}}">
|
<img alt="avatar" src="{{request()->user()->profile->avatarUrl()}}" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||||
</span>
|
</span>
|
||||||
<div class="media-body ml-2 d-none d-lg-block">
|
<div class="media-body ml-2 d-none d-lg-block">
|
||||||
<span class="mb-0 text-sm font-weight-bold">{{request()->user()->username}}</span>
|
<span class="mb-0 text-sm font-weight-bold">{{request()->user()->username}}</span>
|
||||||
|
|
|
@ -108,6 +108,11 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
|
||||||
Route::post('directory/testimonial/save', 'AdminController@directorySaveTestimonial');
|
Route::post('directory/testimonial/save', 'AdminController@directorySaveTestimonial');
|
||||||
Route::post('directory/testimonial/delete', 'AdminController@directoryDeleteTestimonial');
|
Route::post('directory/testimonial/delete', 'AdminController@directoryDeleteTestimonial');
|
||||||
Route::post('directory/testimonial/update', 'AdminController@directoryUpdateTestimonial');
|
Route::post('directory/testimonial/update', 'AdminController@directoryUpdateTestimonial');
|
||||||
|
Route::get('hashtags/stats', 'AdminController@hashtagsStats');
|
||||||
|
Route::get('hashtags/query', 'AdminController@hashtagsApi');
|
||||||
|
Route::get('hashtags/get', 'AdminController@hashtagsGet');
|
||||||
|
Route::post('hashtags/update', 'AdminController@hashtagsUpdate');
|
||||||
|
Route::post('hashtags/clear-trending-cache', 'AdminController@hashtagsClearTrendingCache');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue