mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-27 08:43:17 +00:00
Merge pull request #4237 from pixelfed/staging
Portfolio Federation + Customization
This commit is contained in:
commit
88bcb06448
33 changed files with 1261 additions and 103 deletions
|
@ -32,6 +32,7 @@ use App\Mail\PasswordChange;
|
||||||
use App\Mail\ConfirmAppEmail;
|
use App\Mail\ConfirmAppEmail;
|
||||||
use App\Http\Resources\StatusStateless;
|
use App\Http\Resources\StatusStateless;
|
||||||
use App\Jobs\StatusPipeline\StatusDelete;
|
use App\Jobs\StatusPipeline\StatusDelete;
|
||||||
|
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
|
||||||
|
|
||||||
class ApiV1Dot1Controller extends Controller
|
class ApiV1Dot1Controller extends Controller
|
||||||
{
|
{
|
||||||
|
@ -144,6 +145,10 @@ class ApiV1Dot1Controller extends Controller
|
||||||
$report->type = $report_type;
|
$report->type = $report_type;
|
||||||
$report->save();
|
$report->save();
|
||||||
|
|
||||||
|
if(config('instance.reports.email.enabled')) {
|
||||||
|
ReportNotifyAdminViaEmail::dispatch($report)->onQueue('default');
|
||||||
|
}
|
||||||
|
|
||||||
$res = [
|
$res = [
|
||||||
"msg" => "Successfully sent report",
|
"msg" => "Successfully sent report",
|
||||||
"code" => 200
|
"code" => 200
|
||||||
|
@ -399,10 +404,10 @@ class ApiV1Dot1Controller extends Controller
|
||||||
abort_if(!$user, 403);
|
abort_if(!$user, 403);
|
||||||
abort_if($user->status != null, 403);
|
abort_if($user->status != null, 403);
|
||||||
|
|
||||||
$res = $user->tokens->sortByDesc('created_at')->take(10)->map(function($token, $key) {
|
$res = $user->tokens->sortByDesc('created_at')->take(10)->map(function($token, $key) use($request) {
|
||||||
return [
|
return [
|
||||||
'id' => $key + 1,
|
'id' => $token->id,
|
||||||
'did' => encrypt($token->id),
|
'current_session' => $request->user()->token()->id == $token->id,
|
||||||
'name' => $token->client->name,
|
'name' => $token->client->name,
|
||||||
'scopes' => $token->scopes,
|
'scopes' => $token->scopes,
|
||||||
'revoked' => $token->revoked,
|
'revoked' => $token->revoked,
|
||||||
|
|
|
@ -13,6 +13,10 @@ use App\Services\StatusService;
|
||||||
|
|
||||||
class PortfolioController extends Controller
|
class PortfolioController extends Controller
|
||||||
{
|
{
|
||||||
|
const RSS_FEED_KEY = 'pf:portfolio:rss-feed:';
|
||||||
|
const CACHED_FEED_KEY = 'pf:portfolio:cached-feed:';
|
||||||
|
const RECENT_FEED_KEY = 'pf:portfolio:recent-feed:';
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
return view('portfolio.index');
|
return view('portfolio.index');
|
||||||
|
@ -60,11 +64,11 @@ class PortfolioController extends Controller
|
||||||
$user = AccountService::get($post['account']['id']);
|
$user = AccountService::get($post['account']['id']);
|
||||||
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
||||||
|
|
||||||
if($user['locked'] || $portfolio->active != true) {
|
if(!$portfolio || $user['locked'] || $portfolio->active != true) {
|
||||||
return view('portfolio.404');
|
return view('portfolio.404');
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!$post || $post['visibility'] != 'public' || $post['pf_type'] != 'photo' || $user['id'] != $post['account']['id']) {
|
if(!$post || $post['visibility'] != 'public' || !in_array($post['pf_type'], ['photo', 'photo:album']) || $user['id'] != $post['account']['id']) {
|
||||||
return view('portfolio.404');
|
return view('portfolio.404');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +121,7 @@ class PortfolioController extends Controller
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'profile_source' => 'required|in:recent,custom',
|
'profile_source' => 'required|in:recent,custom',
|
||||||
'layout' => 'required|in:grid,masonry',
|
'layout' => 'required|in:grid,masonry',
|
||||||
'layout_container' => 'required|in:fixed,fluid'
|
'layout_container' => 'required|in:fixed,fluid',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
||||||
|
@ -140,6 +144,7 @@ class PortfolioController extends Controller
|
||||||
$portfolio->show_bio = $request->input('show_bio') === 'on';
|
$portfolio->show_bio = $request->input('show_bio') === 'on';
|
||||||
$portfolio->profile_layout = $request->input('layout');
|
$portfolio->profile_layout = $request->input('layout');
|
||||||
$portfolio->profile_container = $request->input('layout_container');
|
$portfolio->profile_container = $request->input('layout_container');
|
||||||
|
$portfolio->metadata = $metadata;
|
||||||
$portfolio->save();
|
$portfolio->save();
|
||||||
|
|
||||||
return redirect('/' . $request->user()->username);
|
return redirect('/' . $request->user()->username);
|
||||||
|
@ -167,20 +172,28 @@ class PortfolioController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getCustomFeed($portfolio) {
|
protected function getCustomFeed($portfolio) {
|
||||||
if(!$portfolio->metadata['posts']) {
|
if(!isset($portfolio->metadata['posts']) || !$portfolio->metadata['posts']) {
|
||||||
return response()->json([], 400);
|
return response()->json([], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$feed = Cache::remember(self::CACHED_FEED_KEY . $portfolio->profile_id, 86400, function() use($portfolio) {
|
||||||
return collect($portfolio->metadata['posts'])->map(function($p) {
|
return collect($portfolio->metadata['posts'])->map(function($p) {
|
||||||
return StatusService::get($p);
|
return StatusService::get($p);
|
||||||
})
|
})
|
||||||
->filter(function($p) {
|
->filter(function($p) {
|
||||||
return $p && isset($p['account']);
|
return $p && isset($p['account']);
|
||||||
})->values();
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if($portfolio->metadata && isset($portfolio->metadata['feed_order']) && $portfolio->metadata['feed_order'] === 'recent') {
|
||||||
|
return $feed->reverse()->values();
|
||||||
|
} else {
|
||||||
|
return $feed->values();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getRecentFeed($id) {
|
protected function getRecentFeed($id) {
|
||||||
$media = Cache::remember('portfolio:recent-feed:' . $id, 3600, function() use($id) {
|
$media = Cache::remember(self::RECENT_FEED_KEY . $id, 3600, function() use($id) {
|
||||||
return DB::table('media')
|
return DB::table('media')
|
||||||
->whereProfileId($id)
|
->whereProfileId($id)
|
||||||
->whereNotNull('status_id')
|
->whereNotNull('status_id')
|
||||||
|
@ -215,6 +228,14 @@ class PortfolioController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
return $res->map(function($p) {
|
return $res->map(function($p) {
|
||||||
|
$metadata = $p->metadata;
|
||||||
|
$bgColor = $metadata && isset($metadata['background_color']) ? $metadata['background_color'] : '#000000';
|
||||||
|
$textColor = $metadata && isset($metadata['text_color']) ? $metadata['text_color'] : '#d4d4d8';
|
||||||
|
$rssEnabled = $metadata && isset($metadata['rss_enabled']) ? $metadata['rss_enabled'] : false;
|
||||||
|
$rssButton = $metadata && isset($metadata['show_rss_button']) ? $metadata['show_rss_button'] : false;
|
||||||
|
$colorScheme = $metadata && isset($metadata['color_scheme']) ? $metadata['color_scheme'] : 'dark';
|
||||||
|
$feedOrder = $metadata && isset($metadata['feed_order']) ? $metadata['feed_order'] : 'oldest';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'url' => $p->url(),
|
'url' => $p->url(),
|
||||||
'pid' => (string) $p->profile_id,
|
'pid' => (string) $p->profile_id,
|
||||||
|
@ -228,6 +249,13 @@ class PortfolioController extends Controller
|
||||||
'show_bio' => (bool) $p->show_bio,
|
'show_bio' => (bool) $p->show_bio,
|
||||||
'profile_layout' => $p->profile_layout,
|
'profile_layout' => $p->profile_layout,
|
||||||
'profile_source' => $p->profile_source,
|
'profile_source' => $p->profile_source,
|
||||||
|
'color_scheme' => $colorScheme,
|
||||||
|
'background_color' => $bgColor,
|
||||||
|
'text_color' => $textColor,
|
||||||
|
'show_profile_button' => true,
|
||||||
|
'rss_enabled' => $rssEnabled,
|
||||||
|
'show_rss_button' => $rssButton,
|
||||||
|
'feed_order' => $feedOrder,
|
||||||
'metadata' => $p->metadata
|
'metadata' => $p->metadata
|
||||||
];
|
];
|
||||||
})->first();
|
})->first();
|
||||||
|
@ -248,8 +276,13 @@ class PortfolioController extends Controller
|
||||||
if(!$p) {
|
if(!$p) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
$metadata = $p->metadata;
|
||||||
|
|
||||||
return [
|
$rssEnabled = $metadata && isset($metadata['rss_enabled']) ? $metadata['rss_enabled'] : false;
|
||||||
|
$rssButton = $metadata && isset($metadata['show_rss_button']) ? $metadata['show_rss_button'] : false;
|
||||||
|
$profileButton = $metadata && isset($metadata['show_profile_button']) ? $metadata['show_profile_button'] : false;
|
||||||
|
|
||||||
|
$res = [
|
||||||
'url' => $p->url(),
|
'url' => $p->url(),
|
||||||
'show_captions' => (bool) $p->show_captions,
|
'show_captions' => (bool) $p->show_captions,
|
||||||
'show_license' => (bool) $p->show_license,
|
'show_license' => (bool) $p->show_license,
|
||||||
|
@ -259,8 +292,27 @@ class PortfolioController extends Controller
|
||||||
'show_avatar' => (bool) $p->show_avatar,
|
'show_avatar' => (bool) $p->show_avatar,
|
||||||
'show_bio' => (bool) $p->show_bio,
|
'show_bio' => (bool) $p->show_bio,
|
||||||
'profile_layout' => $p->profile_layout,
|
'profile_layout' => $p->profile_layout,
|
||||||
'profile_source' => $p->profile_source
|
'profile_source' => $p->profile_source,
|
||||||
|
'show_profile_button' => $profileButton,
|
||||||
|
'rss_enabled' => $rssEnabled,
|
||||||
|
'show_rss_button' => $rssButton,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if($rssEnabled) {
|
||||||
|
$res['rss_feed_url'] = $p->permalink('.rss');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($p->metadata) {
|
||||||
|
if(isset($p->metadata['background_color'])) {
|
||||||
|
$res['background_color'] = $p->metadata['background_color'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($p->metadata['text_color'])) {
|
||||||
|
$res['text_color'] = $p->metadata['text_color'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function storeSettings(Request $request)
|
public function storeSettings(Request $request)
|
||||||
|
@ -268,11 +320,99 @@ class PortfolioController extends Controller
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'profile_layout' => 'sometimes|in:grid,masonry,album'
|
'active' => 'sometimes|boolean',
|
||||||
|
'show_captions' => 'sometimes|boolean',
|
||||||
|
'show_license' => 'sometimes|boolean',
|
||||||
|
'show_location' => 'sometimes|boolean',
|
||||||
|
'show_timestamp' => 'sometimes|boolean',
|
||||||
|
'show_link' => 'sometimes|boolean',
|
||||||
|
'show_avatar' => 'sometimes|boolean',
|
||||||
|
'show_bio' => 'sometimes|boolean',
|
||||||
|
'profile_layout' => 'sometimes|in:grid,masonry,album',
|
||||||
|
'profile_source' => 'sometimes|in:recent,custom',
|
||||||
|
'color_scheme' => 'sometimes|in:light,dark,custom',
|
||||||
|
'show_profile_button' => 'sometimes|boolean',
|
||||||
|
'rss_enabled' => 'sometimes|boolean',
|
||||||
|
'show_rss_button' => 'sometimes|boolean',
|
||||||
|
'feed_order' => 'sometimes|in:oldest,recent',
|
||||||
|
'background_color' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
|
||||||
|
],
|
||||||
|
'text_color' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$res = Portfolio::whereUserId($request->user()->id)
|
$res = Portfolio::whereUserId($request->user()->id)->firstOrFail();
|
||||||
->update($request->only([
|
$pid = $request->user()->profile_id;
|
||||||
|
$metadata = $res->metadata;
|
||||||
|
$clearFeedCache = false;
|
||||||
|
|
||||||
|
if($request->has('color_scheme')) {
|
||||||
|
$metadata['color_scheme'] = $request->input('color_scheme');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('background_color')) {
|
||||||
|
$metadata['background_color'] = $request->input('background_color');
|
||||||
|
$bgc = $request->background_color;
|
||||||
|
if($bgc && $bgc !== '#000000') {
|
||||||
|
$metadata['color_scheme'] = 'custom';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('text_color')) {
|
||||||
|
$metadata['text_color'] = $request->input('text_color');
|
||||||
|
$txc = $request->text_color;
|
||||||
|
if($txc && $txc !== '#d4d4d8') {
|
||||||
|
$metadata['color_scheme'] = 'custom';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('show_profile_button')) {
|
||||||
|
$metadata['show_profile_button'] = $request->input('show_profile_button');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('rss_enabled')) {
|
||||||
|
$metadata['rss_enabled'] = $request->input('rss_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('show_rss_button')) {
|
||||||
|
$metadata['show_rss_button'] = $metadata['rss_enabled'] ? $request->input('show_rss_button') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('feed_order')) {
|
||||||
|
$metadata['feed_order'] = $request->input('feed_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($metadata['background_color']) || isset($metadata['text_color'])) {
|
||||||
|
$bgc = isset($metadata['background_color']) ? $metadata['background_color'] : null;
|
||||||
|
$txc = isset($metadata['text_color']) ? $metadata['text_color'] : null;
|
||||||
|
|
||||||
|
if((!$bgc || $bgc == '#000000') && (!$txc || $txc === '#d4d4d8') && $request->color_scheme != 'light') {
|
||||||
|
$metadata['color_scheme'] = 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('color_scheme') && $request->color_scheme === 'light') {
|
||||||
|
$metadata['background_color'] = '#ffffff';
|
||||||
|
$metadata['text_color'] = '#000000';
|
||||||
|
$metadata['color_scheme'] = 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->metadata !== $metadata) {
|
||||||
|
$res->metadata = $metadata;
|
||||||
|
$res->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->profile_layout != $res->profile_layout) {
|
||||||
|
$clearFeedCache = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$res->update($request->only([
|
||||||
'active',
|
'active',
|
||||||
'show_captions',
|
'show_captions',
|
||||||
'show_license',
|
'show_license',
|
||||||
|
@ -285,7 +425,11 @@ class PortfolioController extends Controller
|
||||||
'profile_source'
|
'profile_source'
|
||||||
]));
|
]));
|
||||||
|
|
||||||
Cache::forget('portfolio:recent-feed:' . $request->user()->profile_id);
|
Cache::forget(self::RECENT_FEED_KEY . $pid);
|
||||||
|
|
||||||
|
if($clearFeedCache) {
|
||||||
|
Cache::forget(self::RSS_FEED_KEY . $pid);
|
||||||
|
}
|
||||||
|
|
||||||
return 200;
|
return 200;
|
||||||
}
|
}
|
||||||
|
@ -295,7 +439,7 @@ class PortfolioController extends Controller
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'ids' => 'required|array|max:24'
|
'ids' => 'required|array|max:100'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
@ -308,11 +452,117 @@ class PortfolioController extends Controller
|
||||||
->findOrFail($ids);
|
->findOrFail($ids);
|
||||||
|
|
||||||
$p = Portfolio::whereProfileId($pid)->firstOrFail();
|
$p = Portfolio::whereProfileId($pid)->firstOrFail();
|
||||||
$p->metadata = ['posts' => $ids];
|
$metadata = $p->metadata;
|
||||||
|
$metadata['posts'] = $ids;
|
||||||
|
$p->metadata = $metadata;
|
||||||
$p->save();
|
$p->save();
|
||||||
|
|
||||||
Cache::forget('portfolio:recent-feed:' . $pid);
|
Cache::forget(self::RECENT_FEED_KEY . $pid);
|
||||||
|
Cache::forget(self::RSS_FEED_KEY . $pid);
|
||||||
|
Cache::forget(self::CACHED_FEED_KEY . $pid);
|
||||||
|
|
||||||
return $request->ids;
|
return $request->ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRssFeed(Request $request, $username)
|
||||||
|
{
|
||||||
|
$user = User::whereUsername($username)->first();
|
||||||
|
|
||||||
|
if(!$user) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereUserId($user->id)->where('active', 1)->firstOrFail();
|
||||||
|
|
||||||
|
$metadata = $portfolio->metadata;
|
||||||
|
|
||||||
|
abort_if(!$metadata || !isset($metadata['rss_enabled']), 404);
|
||||||
|
abort_unless($metadata['rss_enabled'], 404);
|
||||||
|
|
||||||
|
$account = AccountService::get($user->profile_id);
|
||||||
|
$portfolioUrl = $portfolio->url();
|
||||||
|
$portfolioLayout = $portfolio->profile_layout;
|
||||||
|
|
||||||
|
if(!isset($metadata['posts']) || !count($metadata['posts'])) {
|
||||||
|
$feed = [];
|
||||||
|
} else {
|
||||||
|
$feed = Cache::remember(
|
||||||
|
self::RSS_FEED_KEY . $user->profile_id,
|
||||||
|
43200,
|
||||||
|
function() use($portfolio, $portfolioUrl, $portfolioLayout) {
|
||||||
|
return collect($portfolio->metadata['posts'])->map(function($post) {
|
||||||
|
return StatusService::get($post);
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->map(function($post, $idx) use($portfolioLayout, $portfolioUrl) {
|
||||||
|
$ts = now()->parse($post['created_at']);
|
||||||
|
$url = $portfolioLayout == 'album' ? $portfolioUrl . '?slide=' . ($idx + 1) : $portfolioUrl . '/' . $post['id'];
|
||||||
|
return [
|
||||||
|
'title' => 'Post by ' . $post['account']['username'] . ' on ' . $ts->format('D, d M Y'),
|
||||||
|
'description' => $post['content_text'],
|
||||||
|
'pubDate' => date('D, d M Y H:i:s ', strtotime($post['created_at'])) . 'GMT',
|
||||||
|
'url' => $url
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->reverse()
|
||||||
|
->take(10)
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('D, d M Y H:i:s ') . 'GMT';
|
||||||
|
|
||||||
|
return response()
|
||||||
|
->view('portfolio.rss_feed', compact('account', 'now', 'feed', 'portfolioUrl'), 200)
|
||||||
|
->header('Content-Type', 'text/xml');
|
||||||
|
return response($feed)->withHeaders(['Content-Type' => 'text/xml']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getApFeed(Request $request, $username)
|
||||||
|
{
|
||||||
|
$user = User::whereUsername($username)->first();
|
||||||
|
|
||||||
|
if(!$user) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereUserId($user->id)->where('active', 1)->firstOrFail();
|
||||||
|
$metadata = $portfolio->metadata;
|
||||||
|
$baseUrl = config('app.url');
|
||||||
|
$page = $request->input('page');
|
||||||
|
|
||||||
|
$res = [
|
||||||
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id' => $portfolio->permalink('.json'),
|
||||||
|
'type' => 'OrderedCollection',
|
||||||
|
'totalItems' => isset($metadata['posts']) ? count($metadata['posts']) : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if($request->has('page')) {
|
||||||
|
$start = $page == 1 ? 0 : ($page * 10 - 10);
|
||||||
|
$res['id'] = $portfolio->permalink('.json?page=' . $page);
|
||||||
|
$res['type'] = 'OrderedCollectionPage';
|
||||||
|
$res['next'] = $portfolio->permalink('.json?page=' . $page + 1);
|
||||||
|
$res['partOf'] = $portfolio->permalink('.json');
|
||||||
|
$res['orderedItems'] = collect($metadata['posts'])->slice($start)->take(10)->map(function($p) {
|
||||||
|
return StatusService::get($p);
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->map(function($p) {
|
||||||
|
return $p['url'];
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if(!$res['orderedItems'] || $res['orderedItems']->count() != 10) {
|
||||||
|
unset($res['next']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$res['first'] = $portfolio->permalink('.json?page=1');
|
||||||
|
}
|
||||||
|
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)
|
||||||
|
->header('Content-Type', 'application/activity+json');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use App\Status;
|
||||||
use App\User;
|
use App\User;
|
||||||
use Auth;
|
use Auth;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
|
||||||
|
|
||||||
class ReportController extends Controller
|
class ReportController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -165,6 +166,10 @@ class ReportController extends Controller
|
||||||
$report->message = e($request->input('msg'));
|
$report->message = e($request->input('msg'));
|
||||||
$report->save();
|
$report->save();
|
||||||
|
|
||||||
|
if(config('instance.reports.email.enabled')) {
|
||||||
|
ReportNotifyAdminViaEmail::dispatch($report)->onQueue('default');
|
||||||
|
}
|
||||||
|
|
||||||
if($request->wantsJson()) {
|
if($request->wantsJson()) {
|
||||||
return response()->json(200);
|
return response()->json(200);
|
||||||
} else {
|
} else {
|
||||||
|
|
52
app/Jobs/ReportPipeline/AutospamNotifyAdminViaEmail.php
Normal file
52
app/Jobs/ReportPipeline/AutospamNotifyAdminViaEmail.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\ReportPipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Mail\AdminNewAutospam;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class AutospamNotifyAdminViaEmail implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $report;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($report)
|
||||||
|
{
|
||||||
|
$this->report = $report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$addresses = config('instance.reports.email.to');
|
||||||
|
|
||||||
|
if(config('instance.reports.email.enabled') == false || empty($addresses) || !config('instance.reports.email.autospam')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(strpos($addresses, ',')) {
|
||||||
|
$to = explode(',', $addresses);
|
||||||
|
} else {
|
||||||
|
$to = $addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mail::to($to)->send(new AdminNewAutospam($this->report));
|
||||||
|
}
|
||||||
|
}
|
52
app/Jobs/ReportPipeline/ReportNotifyAdminViaEmail.php
Normal file
52
app/Jobs/ReportPipeline/ReportNotifyAdminViaEmail.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\ReportPipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Mail\AdminNewReport;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ReportNotifyAdminViaEmail implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $report;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($report)
|
||||||
|
{
|
||||||
|
$this->report = $report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$addresses = config('instance.reports.email.to');
|
||||||
|
|
||||||
|
if(config('instance.reports.email.enabled') == false || empty($addresses)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(strpos($addresses, ',')) {
|
||||||
|
$to = explode(',', $addresses);
|
||||||
|
} else {
|
||||||
|
$to = $addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mail::to($to)->send(new AdminNewReport($this->report));
|
||||||
|
}
|
||||||
|
}
|
79
app/Mail/AdminNewAutospam.php
Normal file
79
app/Mail/AdminNewAutospam.php
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
|
||||||
|
class AdminNewAutospam extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $report;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($report)
|
||||||
|
{
|
||||||
|
$this->report = $report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Envelope
|
||||||
|
*/
|
||||||
|
public function envelope()
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: '[' . config('pixelfed.domain.app') . '] Spam Post Detected',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Content
|
||||||
|
*/
|
||||||
|
public function content()
|
||||||
|
{
|
||||||
|
$data = $this->report->toArray();
|
||||||
|
$reported_status = null;
|
||||||
|
$reported_account = null;
|
||||||
|
$url = url('/i/admin/reports/autospam/' . $this->report->id);
|
||||||
|
|
||||||
|
if($data['item_type'] === 'App\Status') {
|
||||||
|
$reported_status = StatusService::get($this->report->item_id, false);
|
||||||
|
$reported_account = AccountService::get($reported_status['account']['id'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Content(
|
||||||
|
markdown: 'emails.admin.new_autospam',
|
||||||
|
with: [
|
||||||
|
'report' => $data,
|
||||||
|
'url' => $url,
|
||||||
|
'reported_status' => $reported_status,
|
||||||
|
'reported_account' => $reported_account
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function attachments()
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
100
app/Mail/AdminNewReport.php
Normal file
100
app/Mail/AdminNewReport.php
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
|
||||||
|
class AdminNewReport extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $report;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($report)
|
||||||
|
{
|
||||||
|
$this->report = $report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Envelope
|
||||||
|
*/
|
||||||
|
public function envelope()
|
||||||
|
{
|
||||||
|
$type = $this->report->type;
|
||||||
|
$id = $this->report->id;
|
||||||
|
$object_type = last(explode("\\", $this->report->object_type));
|
||||||
|
return new Envelope(
|
||||||
|
subject: '[' . config('pixelfed.domain.app') . '] ' . $object_type . ' Report (#' . $id . '-' . $type . ')',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Mail\Mailables\Content
|
||||||
|
*/
|
||||||
|
public function content()
|
||||||
|
{
|
||||||
|
$report = $this->report;
|
||||||
|
$object_type = last(explode("\\", $this->report->object_type));
|
||||||
|
$reporter = AccountService::get($report->profile_id, true);
|
||||||
|
$reported = AccountService::get($report->reported_profile_id, true);
|
||||||
|
$title = 'New ' . $object_type . ' Report (#' . $report->id . ')';
|
||||||
|
$reportUrl = url('/i/admin/reports/show/' . $report->id . '?ref=email');
|
||||||
|
$data = [
|
||||||
|
'report' => $report,
|
||||||
|
'object_type' => $object_type,
|
||||||
|
'title' => $title,
|
||||||
|
'reporter' => $reporter,
|
||||||
|
'reported' => $reported,
|
||||||
|
'url' => $reportUrl,
|
||||||
|
'message' => 'You have a new moderation report.'
|
||||||
|
];
|
||||||
|
|
||||||
|
if($object_type === 'Status') {
|
||||||
|
$data['reported_status'] = StatusService::get($report['object_id'], false);
|
||||||
|
if($reporter && $reported) {
|
||||||
|
$data['message'] = '<a href="' . url('/i/web/profile/' . $reporter['id']) . '">@' .
|
||||||
|
$reporter['acct'] . '</a> reported a post by <a href="' . url('/i/web/profile/' . $reported['id']) .
|
||||||
|
'">@' . $reported['acct'] . '</a> as ' . $report->type . '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($object_type === 'Profile') {
|
||||||
|
if($reporter && $reported) {
|
||||||
|
$data['message'] = '<a href="' . url('/i/web/profile/' . $reporter['id']) . '">@' .
|
||||||
|
$reporter['acct'] . '</a> reported <a href="' . url('/i/web/profile/' . $reported['id']) .
|
||||||
|
'">@' . $reported['acct'] . '</a>\'s profile as ' . $report->type . '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Content(
|
||||||
|
markdown: 'emails.admin.new_report',
|
||||||
|
with: $data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function attachments()
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,13 +28,20 @@ class Portfolio extends Model
|
||||||
'metadata' => 'json'
|
'metadata' => 'json'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function url()
|
public function url($suffix = '')
|
||||||
{
|
{
|
||||||
$account = AccountService::get($this->profile_id);
|
$account = AccountService::get($this->profile_id);
|
||||||
if(!$account) {
|
if(!$account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'https://' . config('portfolio.domain') . config('portfolio.path') . '/' . $account['username'];
|
return 'https://' . config('portfolio.domain') . config('portfolio.path') . '/' . $account['username'] . $suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permalink($suffix = '')
|
||||||
|
{
|
||||||
|
$account = AccountService::get($this->profile_id);
|
||||||
|
|
||||||
|
return config('app.url') . '/account/portfolio/' . $account['username'] . $suffix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,9 @@ class RestrictedNames
|
||||||
'css',
|
'css',
|
||||||
'd',
|
'd',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
|
'delete',
|
||||||
|
'deleted',
|
||||||
|
'deleting',
|
||||||
'dmca',
|
'dmca',
|
||||||
'db',
|
'db',
|
||||||
'deck',
|
'deck',
|
||||||
|
|
|
@ -7,6 +7,7 @@ use App\Status;
|
||||||
use Cache;
|
use Cache;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use App\Services\StatusService;
|
use App\Services\StatusService;
|
||||||
|
use App\Jobs\ReportPipeline\AutospamNotifyAdminViaEmail;
|
||||||
|
|
||||||
class Bouncer {
|
class Bouncer {
|
||||||
|
|
||||||
|
@ -126,6 +127,10 @@ class Bouncer {
|
||||||
]);
|
]);
|
||||||
$ai->save();
|
$ai->save();
|
||||||
|
|
||||||
|
if(config('instance.reports.email.enabled') && config('instance.reports.email.autospam')) {
|
||||||
|
AutospamNotifyAdminViaEmail::dispatch($ai);
|
||||||
|
}
|
||||||
|
|
||||||
$u = $status->profile->user;
|
$u = $status->profile->user;
|
||||||
$u->has_interstitial = true;
|
$u->has_interstitial = true;
|
||||||
$u->save();
|
$u->save();
|
||||||
|
|
|
@ -111,5 +111,13 @@ return [
|
||||||
'user_filters' => [
|
'user_filters' => [
|
||||||
'max_user_blocks' => env('PF_MAX_USER_BLOCKS', 50),
|
'max_user_blocks' => env('PF_MAX_USER_BLOCKS', 50),
|
||||||
'max_user_mutes' => env('PF_MAX_USER_MUTES', 50)
|
'max_user_mutes' => env('PF_MAX_USER_MUTES', 50)
|
||||||
|
],
|
||||||
|
|
||||||
|
'reports' => [
|
||||||
|
'email' => [
|
||||||
|
'enabled' => env('INSTANCE_REPORTS_EMAIL_ENABLED', false),
|
||||||
|
'to' => env('INSTANCE_REPORTS_EMAIL_ADDRESSES'),
|
||||||
|
'autospam' => env('INSTANCE_REPORTS_EMAIL_AUTOSPAM', false)
|
||||||
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
BIN
public/css/portfolio.css
vendored
BIN
public/css/portfolio.css
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
BIN
public/js/portfolio.js
vendored
BIN
public/js/portfolio.js
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -19,11 +19,11 @@
|
||||||
<div class="col-12 mb-4">
|
<div class="col-12 mb-4">
|
||||||
<p v-if="settings.show_captions && post.content_text">{{ post.content_text }}</p>
|
<p v-if="settings.show_captions && post.content_text">{{ post.content_text }}</p>
|
||||||
<div class="d-md-flex justify-content-between align-items-center">
|
<div class="d-md-flex justify-content-between align-items-center">
|
||||||
<p class="small text-lighter">by <a :href="profileUrl()" class="text-lighter font-weight-bold">@{{profile.username}}</a></p>
|
<p class="small">by <a :href="profileUrl()" class="font-weight-bold link-color">@{{profile.username}}</a></p>
|
||||||
<p v-if="settings.show_license && post.media_attachments[0].license" class="small text-muted">Licensed under {{ post.media_attachments[0].license.title }}</p>
|
<p v-if="settings.show_license && post.media_attachments[0].license" class="small">Licensed under {{ post.media_attachments[0].license.title }}</p>
|
||||||
<p v-if="settings.show_location && post.place" class="small text-muted">{{ post.place.name }}, {{ post.place.country }}</p>
|
<p v-if="settings.show_location && post.place" class="small text-muted">{{ post.place.name }}, {{ post.place.country }}</p>
|
||||||
<p v-if="settings.show_timestamp" class="small text-muted">
|
<p v-if="settings.show_timestamp" class="small">
|
||||||
<a v-if="settings.show_link" :href="post.url" class="text-lighter font-weight-bold" style="z-index: 2">
|
<a v-if="settings.show_link" :href="post.url" class="font-weight-bold link-color" style="z-index: 2">
|
||||||
{{ formatDate(post.created_at) }}
|
{{ formatDate(post.created_at) }}
|
||||||
</a>
|
</a>
|
||||||
<span v-else class="user-select-none">
|
<span v-else class="user-select-none">
|
||||||
|
@ -96,6 +96,15 @@
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.settings = res.data;
|
this.settings = res.data;
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('background_color')) {
|
||||||
|
this.updateCssVariable('--body-bg', res.data.background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('text_color')) {
|
||||||
|
this.updateCssVariable('--text-color', res.data.text_color);
|
||||||
|
this.updateCssVariable('--link-color', res.data.text_color);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -116,6 +125,11 @@
|
||||||
formatDate(ts) {
|
formatDate(ts) {
|
||||||
const dts = new Date(ts);
|
const dts = new Date(ts);
|
||||||
return dts.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric' });
|
return dts.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCssVariable(k, v) {
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,35 @@
|
||||||
<div class="row py-5">
|
<div class="row py-5">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex align-items-center flex-column">
|
<div class="d-flex align-items-center flex-column">
|
||||||
<img :src="profile.avatar" width="60" height="60" class="rounded-circle shadow" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
<img
|
||||||
|
v-if="settings.show_avatar"
|
||||||
|
:src="profile.avatar"
|
||||||
|
width="60"
|
||||||
|
height="60"
|
||||||
|
class="rounded-circle shadow"
|
||||||
|
onerror="this.src='/storage/avatars/default.png?v=0';this.onerror=null;">
|
||||||
|
|
||||||
<div class="py-3 text-center" style="max-width: 60%">
|
<div class="py-3 text-center" style="max-width: 60%">
|
||||||
<h1 class="font-weight-bold">{{ profile.username }}</h1>
|
<h1 class="font-weight-bold">{{ profile.username }}</h1>
|
||||||
<p class="font-weight-light mb-0">{{ profile.note_text }}</p>
|
<p v-if="settings.show_bio" class="font-weight-light mb-0 text-break">{{ profile.note_text }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="settings.show_profile_button || (settings.rss_enabled && settings.show_rss_button)" class="pb-3 text-center d-flex flex-column flex-sm-row" style="max-width: 60%;gap: 1rem;">
|
||||||
|
<a
|
||||||
|
v-if="settings.show_profile_button"
|
||||||
|
class="btn btn-outline-primary btn-custom-color"
|
||||||
|
:href="profile.url"
|
||||||
|
target="_blank">
|
||||||
|
View Profile
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="settings.rss_enabled && settings.show_rss_button"
|
||||||
|
class="btn btn-outline-primary btn-custom-color"
|
||||||
|
:href="settings.rss_feed_url"
|
||||||
|
target="_blank">
|
||||||
|
<i class="far fa-rss"></i> RSS
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,7 +50,23 @@
|
||||||
<div v-for="(res, index) in feed" class="col-12 col-md-4 mb-1 p-1">
|
<div v-for="(res, index) in feed" class="col-12 col-md-4 mb-1 p-1">
|
||||||
<div class="square">
|
<div class="square">
|
||||||
<a :href="postUrl(res)">
|
<a :href="postUrl(res)">
|
||||||
<img :src="res.media_attachments[0].url" width="100%" height="300" style="overflow: hidden;object-fit: cover;" class="square-content pr-1">
|
<div class="lazy-img">
|
||||||
|
<blur-hash-canvas
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
:hash="res.media_attachments[0].blurhash"
|
||||||
|
class="square-content pr-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src=""
|
||||||
|
:data-src="res.media_attachments[0].url"
|
||||||
|
width="100%"
|
||||||
|
height="300"
|
||||||
|
style="overflow: hidden;object-fit: cover;z-index: -1;"
|
||||||
|
class="square-content pr-1 img-placeholder"
|
||||||
|
loading="lazy" />
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,14 +74,14 @@
|
||||||
|
|
||||||
<div v-else-if="settings.profile_layout ==='album'" class="col-12 mb-1 p-1">
|
<div v-else-if="settings.profile_layout ==='album'" class="col-12 mb-1 p-1">
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<p class="text-muted font-weight-bold">{{ albumIndex + 1 }} <span class="font-weight-light">/</span> {{ feed.length }}</p>
|
<p class="text-color font-weight-bold">{{ albumIndex + 1 }} <span class="font-weight-light">/</span> {{ feed.length }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span v-if="albumIndex === 0">
|
<span v-if="albumIndex === 0">
|
||||||
<i class="fa fa-arrow-circle-left fa-3x text-dark" />
|
<i class="fa fa-arrow-circle-left fa-3x text-color-lighter" />
|
||||||
</span>
|
</span>
|
||||||
<a v-else @click.prevent="albumPrev()" href="#">
|
<a v-else @click.prevent="albumPrev()" href="#">
|
||||||
<i class="fa fa-arrow-circle-left fa-3x text-muted"/>
|
<i class="fa fa-arrow-circle-left fa-3x text-color"/>
|
||||||
</a>
|
</a>
|
||||||
<transition name="slide-fade">
|
<transition name="slide-fade">
|
||||||
<a :href="postUrl(feed[albumIndex])" class="mx-4" :key="albumIndex">
|
<a :href="postUrl(feed[albumIndex])" class="mx-4" :key="albumIndex">
|
||||||
|
@ -55,10 +95,10 @@
|
||||||
</a>
|
</a>
|
||||||
</transition>
|
</transition>
|
||||||
<span v-if="albumIndex === feed.length - 1">
|
<span v-if="albumIndex === feed.length - 1">
|
||||||
<i class="fa fa-arrow-circle-right fa-3x text-dark" />
|
<i class="fa fa-arrow-circle-right fa-3x text-color-lighter" />
|
||||||
</span>
|
</span>
|
||||||
<a v-else @click.prevent="albumNext()" href="#">
|
<a v-else @click.prevent="albumNext()" href="#">
|
||||||
<i class="fa fa-arrow-circle-right fa-3x text-muted"/>
|
<i class="fa fa-arrow-circle-right fa-3x text-color"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,7 +127,7 @@
|
||||||
<span class="text-gradient-primary">portfolio</span>
|
<span class="text-gradient-primary">portfolio</span>
|
||||||
</span>
|
</span>
|
||||||
<p v-if="user && user.id == profile.id" class="text-center mb-0">
|
<p v-if="user && user.id == profile.id" class="text-center mb-0">
|
||||||
<a :href="settingsUrl" class="text-muted"><i class="far fa-cog fa-lg"></i></a>
|
<a :href="settingsUrl" class="link-color"><i class="far fa-cog fa-lg"></i></a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +149,7 @@
|
||||||
settings: undefined,
|
settings: undefined,
|
||||||
feed: [],
|
feed: [],
|
||||||
albumIndex: 0,
|
albumIndex: 0,
|
||||||
settingsUrl: window._portfolio.path + '/settings'
|
settingsUrl: window._portfolio.path + '/settings',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -135,6 +175,15 @@
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.settings = res.data;
|
this.settings = res.data;
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('background_color')) {
|
||||||
|
this.updateCssVariable('--body-bg', res.data.background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('text_color')) {
|
||||||
|
this.updateCssVariable('--text-color', res.data.text_color);
|
||||||
|
this.updateCssVariable('--link-color', res.data.text_color);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchFeed();
|
this.fetchFeed();
|
||||||
|
@ -145,7 +194,7 @@
|
||||||
async fetchFeed() {
|
async fetchFeed() {
|
||||||
axios.get('/api/portfolio/' + this.profile.id + '/feed')
|
axios.get('/api/portfolio/' + this.profile.id + '/feed')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.feed = res.data.filter(p => p.pf_type === "photo");
|
this.feed = res.data.filter(p => ['photo', 'photo:album'].includes(p.pf_type));
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setAlbumSlide();
|
this.setAlbumSlide();
|
||||||
|
@ -162,6 +211,14 @@
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.bootIntersectors()
|
||||||
|
}, 500);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
postUrl(res) {
|
postUrl(res) {
|
||||||
|
@ -217,6 +274,38 @@
|
||||||
gutter: 20,
|
gutter: 20,
|
||||||
modal: false,
|
modal: false,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCssVariable(k, v) {
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty(k, v);
|
||||||
|
},
|
||||||
|
|
||||||
|
bootIntersectors() {
|
||||||
|
var lazyImages = [].slice.call(document.querySelectorAll("img.img-placeholder"));
|
||||||
|
|
||||||
|
if ("IntersectionObserver" in window) {
|
||||||
|
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
|
||||||
|
entries.forEach(function(entry) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
let lazyImage = entry.target;
|
||||||
|
lazyImage.src = lazyImage.dataset.src;
|
||||||
|
lazyImage.style.zIndex = 2;
|
||||||
|
lazyImage.classList.remove("img-placeholder");
|
||||||
|
lazyImageObserver.unobserve(lazyImage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyImages.forEach(function(lazyImage) {
|
||||||
|
lazyImageObserver.observe(lazyImage);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
lazyImages.forEach(function(img) {
|
||||||
|
img.src = img.dataset.src;
|
||||||
|
img.style.zIndex = 2;
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,10 @@
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="mt-n2 mb-4">
|
<div class="mt-n2 mb-4">
|
||||||
<p class="text-muted small">Select up to 24 photos from your 100 most recent posts. You can only select public photo posts, videos are not supported at this time.</p>
|
<p class="text-muted small">Select up to 100 photos from your 100 most recent posts. You can only select public photo posts, videos are not supported at this time.</p>
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<p class="font-weight-bold mb-0">Selected {{ selectedRecentPosts.length }}/24</p>
|
<p class="font-weight-bold mb-0">Selected {{ selectedRecentPosts.length }}/100</p>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-link font-weight-bold mr-3 text-decoration-none"
|
class="btn btn-link font-weight-bold mr-3 text-decoration-none"
|
||||||
|
@ -90,11 +90,13 @@
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<img
|
<img
|
||||||
:key="post.id"
|
:key="post.id"
|
||||||
:src="post.media_attachments[0].url"
|
:src="getPreviewUrl(post)"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="300"
|
height="300"
|
||||||
style="overflow: hidden;object-fit: cover;"
|
style="overflow: hidden;object-fit: cover;"
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
|
loading="lazy"
|
||||||
|
onerror="this.src='/storage/no-preview.png';this.onerror=null;"
|
||||||
class="square-content pr-1">
|
class="square-content pr-1">
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
|
@ -112,7 +114,9 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="tabIndex === 'Customize'" class="col-12 col-md-8 mt-3 py-2" key="2">
|
<div v-else-if="tabIndex === 'Customize'" class="col-12 mt-3 py-2" key="2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
<div v-for="setting in customizeSettings" class="card bg-dark mb-5">
|
<div v-for="setting in customizeSettings" class="card bg-dark mb-5">
|
||||||
<div class="card-header">{{ setting.title }}</div>
|
<div class="card-header">{{ setting.title }}</div>
|
||||||
<div class="list-group bg-dark">
|
<div class="list-group bg-dark">
|
||||||
|
@ -135,6 +139,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
<div class="card bg-dark mb-5">
|
<div class="card bg-dark mb-5">
|
||||||
<div class="card-header">Portfolio</div>
|
<div class="card-header">Portfolio</div>
|
||||||
<div class="list-group bg-dark">
|
<div class="list-group bg-dark">
|
||||||
|
@ -149,6 +156,84 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="settings.profile_source === 'custom'" class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Order</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b-form-select
|
||||||
|
v-model="settings.feed_order"
|
||||||
|
:options="profileLayoutFeedOrder" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Color Scheme</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b-form-select
|
||||||
|
v-model="settings.color_scheme"
|
||||||
|
:options="profileLayoutColorSchemeOptions"
|
||||||
|
:disabled="settings.color_scheme === 'custom'"
|
||||||
|
@change="updateColorScheme" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Background Color</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-col sm="2">
|
||||||
|
<b-form-input
|
||||||
|
v-model="settings.background_color"
|
||||||
|
debounce="1000"
|
||||||
|
type="color"
|
||||||
|
@change="updateBackgroundColor" />
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
v-if="!['#000000', null].includes(settings.background_color)"
|
||||||
|
variant="link"
|
||||||
|
@click="resetBackgroundColor">
|
||||||
|
Reset
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Text Color</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-col sm="2">
|
||||||
|
<b-form-input
|
||||||
|
v-model="settings.text_color"
|
||||||
|
debounce="1000"
|
||||||
|
type="color"
|
||||||
|
@change="updateTextColor" />
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
v-if="!['#d4d4d8', null].includes(settings.text_color)"
|
||||||
|
variant="link"
|
||||||
|
@click="resetTextColor">
|
||||||
|
Reset
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -185,6 +270,7 @@
|
||||||
isSavingCurated: false,
|
isSavingCurated: false,
|
||||||
canSaveCurated: false,
|
canSaveCurated: false,
|
||||||
customizeSettings: [],
|
customizeSettings: [],
|
||||||
|
skipWatch: false,
|
||||||
profileSourceOptions: [
|
profileSourceOptions: [
|
||||||
{ value: null, text: 'Please select an option', disabled: true },
|
{ value: null, text: 'Please select an option', disabled: true },
|
||||||
{ value: 'recent', text: 'Most recent posts' },
|
{ value: 'recent', text: 'Most recent posts' },
|
||||||
|
@ -194,6 +280,16 @@
|
||||||
{ value: 'grid', text: 'Grid' },
|
{ value: 'grid', text: 'Grid' },
|
||||||
{ value: 'masonry', text: 'Masonry' },
|
{ value: 'masonry', text: 'Masonry' },
|
||||||
{ value: 'album', text: 'Album' },
|
{ value: 'album', text: 'Album' },
|
||||||
|
],
|
||||||
|
profileLayoutColorSchemeOptions: [
|
||||||
|
{ value: null, text: 'Please select an option', disabled: true },
|
||||||
|
{ value: 'light', text: 'Light mode' },
|
||||||
|
{ value: 'dark', text: 'Dark mode' },
|
||||||
|
{ value: 'custom', text: 'Custom color scheme', disabled: true },
|
||||||
|
],
|
||||||
|
profileLayoutFeedOrder: [
|
||||||
|
{ value: 'oldest', text: 'Oldest first' },
|
||||||
|
{ value: 'recent', text: 'Recent first' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -217,7 +313,7 @@
|
||||||
deep: true,
|
deep: true,
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler: function(o, n) {
|
handler: function(o, n) {
|
||||||
if(this.loading) {
|
if(this.loading || this.skipWatch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!n.show_timestamp) {
|
if(!n.show_timestamp) {
|
||||||
|
@ -260,6 +356,20 @@
|
||||||
if(res.data.metadata && res.data.metadata.posts) {
|
if(res.data.metadata && res.data.metadata.posts) {
|
||||||
this.selectedRecentPosts = res.data.metadata.posts;
|
this.selectedRecentPosts = res.data.metadata.posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(res.data.color_scheme != 'dark') {
|
||||||
|
if(res.data.color_scheme === 'light') {
|
||||||
|
this.updateBackgroundColor('#ffffff');
|
||||||
|
} else {
|
||||||
|
if(res.data.hasOwnProperty('background_color')) {
|
||||||
|
this.updateBackgroundColor(res.data.background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('text_color')) {
|
||||||
|
this.updateTextColor(res.data.text_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.initCustomizeSettings();
|
this.initCustomizeSettings();
|
||||||
|
@ -325,24 +435,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSettings() {
|
updateSettings(silent = false) {
|
||||||
|
if(this.skipWatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
axios.post(this.apiPath('/api/portfolio/self/update-settings.json'), this.settings)
|
axios.post(this.apiPath('/api/portfolio/self/update-settings.json'), this.settings)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.updateTabs();
|
this.updateTabs();
|
||||||
|
if(!silent) {
|
||||||
this.$bvToast.toast(`Your settings have been successfully updated!`, {
|
this.$bvToast.toast(`Your settings have been successfully updated!`, {
|
||||||
variant: 'dark',
|
variant: 'dark',
|
||||||
title: 'Settings Updated',
|
title: 'Settings Updated',
|
||||||
autoHideDelay: 2000,
|
autoHideDelay: 2000,
|
||||||
appendToast: false
|
appendToast: false
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
loadRecentPosts() {
|
loadRecentPosts() {
|
||||||
axios.get('/api/v1/accounts/' + this.user.id + '/statuses?only_media=1&media_types=photo&limit=100')
|
axios.get('/api/v1/accounts/' + this.user.id + '/statuses?only_media=1&media_types=photo&limit=100&_pe=1')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if(res.data.length) {
|
if(res.data.length) {
|
||||||
this.recentPosts = res.data.filter(p => p.visibility === "public");
|
this.recentPosts = res.data.filter(p => ['photo', 'photo:album'].includes(p.pf_type) && p.visibility === "public");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -354,7 +470,7 @@
|
||||||
|
|
||||||
toggleRecentPost(id) {
|
toggleRecentPost(id) {
|
||||||
if(this.selectedRecentPosts.indexOf(id) == -1) {
|
if(this.selectedRecentPosts.indexOf(id) == -1) {
|
||||||
if(this.selectedRecentPosts.length === 24) {
|
if(this.selectedRecentPosts.length === 100) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.selectedRecentPosts.push(id);
|
this.selectedRecentPosts.push(id);
|
||||||
|
@ -449,10 +565,105 @@
|
||||||
{
|
{
|
||||||
label: "Show Bio",
|
label: "Show Bio",
|
||||||
model: "show_bio"
|
model: "show_bio"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
label: "Show View Profile Button",
|
||||||
|
model: "show_profile_button"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enable RSS Feed",
|
||||||
|
description: "Enable your RSS feed with the 10 most recent portfolio items",
|
||||||
|
model: "rss_enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Show RSS Feed Button",
|
||||||
|
model: "show_rss_button",
|
||||||
|
requiredWithTrue: "rss_enabled"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBackgroundColor(e) {
|
||||||
|
this.skipWatch = true;
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty('--body-bg', e);
|
||||||
|
|
||||||
|
if(e !== '#000000' && e !== '#ffffff') {
|
||||||
|
this.settings.color_scheme = 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTextColor(e) {
|
||||||
|
this.skipWatch = true;
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty('--text-color', e);
|
||||||
|
|
||||||
|
if(e !== '#d4d4d8') {
|
||||||
|
this.settings.color_scheme = 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetBackgroundColor() {
|
||||||
|
this.skipWatch = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateBackgroundColor('#000000');
|
||||||
|
this.settings.color_scheme = 'dark';
|
||||||
|
this.settings.background_color = '#000000';
|
||||||
|
this.updateSettings(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
resetTextColor() {
|
||||||
|
this.skipWatch = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateTextColor('#d4d4d8');
|
||||||
|
this.settings.color_scheme = 'dark';
|
||||||
|
this.settings.text_color = '#d4d4d8';
|
||||||
|
this.updateSettings(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateColorScheme(e) {
|
||||||
|
if(e === 'light') {
|
||||||
|
this.updateBackgroundColor('#ffffff');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e === 'dark') {
|
||||||
|
this.updateBackgroundColor('#000000');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPreviewUrl(post) {
|
||||||
|
let media = post.media_attachments[0];
|
||||||
|
if(!media) { return '/storage/no-preview.png'; }
|
||||||
|
|
||||||
|
if(media.preview_url && !media.preview_url.endsWith('/no-preview.png')) {
|
||||||
|
return media.preview_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return media.url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
resources/assets/js/portfolio.js
vendored
5
resources/assets/js/portfolio.js
vendored
|
@ -1,7 +1,10 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
window.Vue = Vue;
|
window.Vue = Vue;
|
||||||
import BootstrapVue from 'bootstrap-vue'
|
import BootstrapVue from 'bootstrap-vue';
|
||||||
|
import VueBlurHash from 'vue-blurhash';
|
||||||
|
import 'vue-blurhash/dist/vue-blurhash.css'
|
||||||
Vue.use(BootstrapVue);
|
Vue.use(BootstrapVue);
|
||||||
|
Vue.use(VueBlurHash);
|
||||||
|
|
||||||
Vue.component(
|
Vue.component(
|
||||||
'portfolio-post',
|
'portfolio-post',
|
||||||
|
|
50
resources/assets/sass/portfolio.scss
vendored
50
resources/assets/sass/portfolio.scss
vendored
|
@ -1,23 +1,67 @@
|
||||||
@import "lib/inter";
|
@import "lib/inter";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--body-bg: #000000;
|
||||||
|
--text-color: #d4d4d8;
|
||||||
|
--link-color: #3B82F6;
|
||||||
|
};
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #000000;
|
background: var(--body-bg);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
color: #d4d4d8;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: #3B82F6 !important;
|
color: #3B82F6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-color {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-color-lighter {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-custom-color {
|
||||||
|
border-color: var(--link-color);
|
||||||
|
color: var(--link-color);
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 7px 30px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--link-color) !important;
|
||||||
|
color: var(--link-color) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-color {
|
||||||
|
color: var(--link-color);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: var(--link-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lead,
|
.lead,
|
||||||
.font-weight-light {
|
.font-weight-light {
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #3B82F6;
|
color: var(--link-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
87
resources/views/emails/admin/new_autospam.blade.php
Normal file
87
resources/views/emails/admin/new_autospam.blade.php
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<x-mail::message>
|
||||||
|
# Autospam Detection (#{{ $report['id'] }})
|
||||||
|
|
||||||
|
We have detected a potential spam post. The post has been unlisted from public feeds.
|
||||||
|
**Action is required to restore post visibility**. <br />
|
||||||
|
Please review this report and handle accordingly.
|
||||||
|
|
||||||
|
<x-mail::button :url="$url">
|
||||||
|
Review Autospam Report
|
||||||
|
</x-mail::button>
|
||||||
|
|
||||||
|
@if($reported_status)
|
||||||
|
<x-mail::panel>
|
||||||
|
<p style="font-size: 13px; color: #cccccc; text-align: center; font-weight: bold;margin-bottom: 10px;">Reported Status</p>
|
||||||
|
<div style="display: flex; align-items: center;gap: 10px;">
|
||||||
|
@if(
|
||||||
|
isset($reported_status['media_attachments']) &&
|
||||||
|
isset($reported_status['pf_type']) &&
|
||||||
|
count($reported_status['media_attachments']) &&
|
||||||
|
in_array($reported_status['pf_type'], ['photo', 'photo:album'])
|
||||||
|
)
|
||||||
|
<img
|
||||||
|
src="{{$reported_status['media_attachments'][0]['url']}}"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
alt="Media preview"
|
||||||
|
style="object-fit: cover; border: 1px solid #cccccc; border-radius: 10px; margin-bottom: 10px;"
|
||||||
|
onerror="this.src='{{url('/storage/no-preview.png')}}';this.onerror=null;" />
|
||||||
|
@endif
|
||||||
|
@if(isset($reported_status['content']))
|
||||||
|
<div style="font-size: 12px !important;">{{ strip_tags(str_replace(["\n", "\r", "\r\n"], ' ', $reported_status['content'])) }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between;margin-top:10px;">
|
||||||
|
<a style="font-size: 11px !important;font-weight: bold;text-decoration: none;" href="{{ url('/i/web/post/' . $reported_status['id'])}}">
|
||||||
|
View status
|
||||||
|
</a>
|
||||||
|
<p style="font-size: 11px !important;font-weight: bold;">
|
||||||
|
Posted {{ now()->parse($reported_status['created_at'])->diffForHumans() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</x-mail::panel>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($reported_account && isset($reported_account['id']))
|
||||||
|
<x-mail::panel>
|
||||||
|
<p style="font-size: 13px; color: #cccccc; text-align: center; font-weight: bold;margin-bottom: 10px;">Reported Account</p>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start;gap: 10px;">
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="{{$reported_account['avatar']}}"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
alt="Avatar"
|
||||||
|
style="border-radius: 10px;min-width: 50px;flex-grow: 1;"
|
||||||
|
onerror="this.src='{{url('/storage/avatars/default.jpg')}}';this.onerror=null;" />
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 0;">
|
||||||
|
<a href="{{ url('/i/web/profile/' . $reported_account['id']) }}" style="text-decoration: none;font-weight: bold">{{ $reported_account['username'] }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 5px;font-size: 10px;opacity: 0.5;">
|
||||||
|
{{ strip_tags(str_replace(["\n", "\r", "\r\n"], ' ', $reported_account['note'])) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reported_account['statuses_count'] }} posts</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reported_account['followers_count'] }} followers</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reported_account['following_count'] }} following</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">Joined {{ now()->parse($reported_account['created_at'])->diffForHumans()}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-mail::panel>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p style="font-size: 12px;color: #cccccc;text-align: center;">
|
||||||
|
This is an automated email that is intended for administrators of {{ config('pixelfed.domain.app')}}.<br />
|
||||||
|
If you received this email by mistake, kindly disregard and delete this email.
|
||||||
|
</p>
|
||||||
|
</x-mail::message>
|
122
resources/views/emails/admin/new_report.blade.php
Normal file
122
resources/views/emails/admin/new_report.blade.php
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<x-mail::message>
|
||||||
|
# {{ $title }}
|
||||||
|
|
||||||
|
## {!! $message !!}
|
||||||
|
|
||||||
|
<x-mail::button :url="$url">
|
||||||
|
View Report
|
||||||
|
</x-mail::button>
|
||||||
|
|
||||||
|
@if($object_type === 'Status' && $reported_status)
|
||||||
|
<x-mail::panel>
|
||||||
|
<p style="font-size: 13px; color: #cccccc; text-align: center; font-weight: bold;margin-bottom: 10px;">Reported Status</p>
|
||||||
|
<div style="display: flex; align-items: center;gap: 10px;">
|
||||||
|
@if(
|
||||||
|
isset($reported_status['media_attachments']) &&
|
||||||
|
isset($reported_status['pf_type']) &&
|
||||||
|
count($reported_status['media_attachments']) &&
|
||||||
|
in_array($reported_status['pf_type'], ['photo', 'photo:album'])
|
||||||
|
)
|
||||||
|
<img
|
||||||
|
src="{{$reported_status['media_attachments'][0]['url']}}"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
alt="Media preview"
|
||||||
|
style="object-fit: cover; border: 1px solid #cccccc; border-radius: 10px; margin-bottom: 10px;"
|
||||||
|
onerror="this.src='{{url('/storage/no-preview.png')}}';this.onerror=null;" />
|
||||||
|
@endif
|
||||||
|
@if(isset($reported_status['content']))
|
||||||
|
<div style="font-size: 12px !important;">{{ strip_tags(str_replace(["\n", "\r", "\r\n"], ' ', $reported_status['content'])) }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between;margin-top:10px;">
|
||||||
|
<a style="font-size: 11px !important;font-weight: bold;text-decoration: none;" href="{{ url('/i/web/post/' . $reported_status['id'])}}">
|
||||||
|
View status
|
||||||
|
</a>
|
||||||
|
<p style="font-size: 11px !important;font-weight: bold;">
|
||||||
|
Posted {{ now()->parse($reported_status['created_at'])->diffForHumans() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</x-mail::panel>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($reported && isset($reported['id']))
|
||||||
|
<x-mail::panel>
|
||||||
|
<p style="font-size: 13px; color: #cccccc; text-align: center; font-weight: bold;margin-bottom: 10px;">Reported Account</p>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start;gap: 10px;">
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="{{$reported['avatar']}}"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
alt="Avatar"
|
||||||
|
style="border-radius: 10px;min-width: 50px;flex-grow: 1;"
|
||||||
|
onerror="this.src='{{url('/storage/avatars/default.jpg')}}';this.onerror=null;" />
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 0;">
|
||||||
|
<a href="{{ url('/i/web/profile/' . $reported['id']) }}" style="text-decoration: none;font-weight: bold">{{ $reported['username'] }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 5px;font-size: 10px;opacity: 0.5;">
|
||||||
|
{{ strip_tags(str_replace(["\n", "\r", "\r\n"], ' ', $reported['note'])) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reported['statuses_count'] }} posts</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reported['followers_count'] }} followers</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reported['following_count'] }} following</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">Joined {{ now()->parse($reported['created_at'])->diffForHumans()}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-mail::panel>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($reporter && isset($reporter['id']))
|
||||||
|
<x-mail::panel>
|
||||||
|
<p style="font-size: 13px; color: #cccccc; text-align: center; font-weight: bold;margin-bottom: 10px;">Reported By</p>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start;gap: 10px;">
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="{{$reporter['avatar']}}"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
alt="Avatar"
|
||||||
|
style="border-radius: 10px;min-width: 50px;flex-grow: 1;"
|
||||||
|
onerror="this.src='{{url('/storage/avatars/default.jpg')}}';this.onerror=null;" />
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 0;"><a href="{{ url('/i/web/profile/' . $reporter['id']) }}" style="text-decoration: none;font-weight: bold">{{ $reporter['username'] }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-bottom: 5px;font-size: 10px;opacity: 0.5;">
|
||||||
|
{{ strip_tags(str_replace(["\n", "\r", "\r\n"], ' ', $reporter['note'])) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reporter['statuses_count'] }} posts</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reporter['followers_count'] }} followers</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">{{ $reporter['following_count'] }} following</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">·</p>
|
||||||
|
<p style="font-size: 10px;margin-bottom: 0;">Joined {{ now()->parse($reporter['created_at'])->diffForHumans()}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-mail::panel>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p style="font-size: 12px;color: #cccccc;text-align: center;">
|
||||||
|
This is an automated email that is intended for administrators of {{ config('pixelfed.domain.app')}}.<br />
|
||||||
|
If you received this email by mistake, kindly disregard and delete this email.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</x-mail::message>
|
20
resources/views/portfolio/rss_feed.blade.php
Normal file
20
resources/views/portfolio/rss_feed.blade.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- RSS generated by pixelfed v{{config('pixelfed.version')}} on {{$now}} -->
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>{{ $account['username'] }}'s Portfolio</title>
|
||||||
|
<link>{{ $portfolioUrl }}</link>
|
||||||
|
<description>The pixelfed portfolio of {{ $account['username'] }} with the {{ count($feed) }} most recent posts</description>
|
||||||
|
<pubDate>{{ $now }}</pubDate>
|
||||||
|
<language>en-us</language>
|
||||||
|
@foreach($feed as $p)
|
||||||
|
<item>
|
||||||
|
<title>{{$p['title']}}</title>
|
||||||
|
<description>{{$p['description']}}</description>
|
||||||
|
<guid>{{$p['url']}}</guid>
|
||||||
|
<link>{{$p['url']}}</link>
|
||||||
|
<pubDate>{{$p['pubDate']}}</pubDate>
|
||||||
|
</item>
|
||||||
|
@endforeach
|
||||||
|
</channel>
|
||||||
|
</rss>
|
|
@ -407,6 +407,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
||||||
Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests');
|
Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests');
|
||||||
Route::post('follow-requests', 'AccountController@followRequestHandle');
|
Route::post('follow-requests', 'AccountController@followRequestHandle');
|
||||||
Route::get('follow-requests.json', 'AccountController@followRequestsJson');
|
Route::get('follow-requests.json', 'AccountController@followRequestsJson');
|
||||||
|
Route::get('portfolio/{username}.json', 'PortfolioController@getApFeed');
|
||||||
|
Route::get('portfolio/{username}.rss', 'PortfolioController@getRssFeed');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::group(['prefix' => 'settings'], function () {
|
Route::group(['prefix' => 'settings'], function () {
|
||||||
|
|
Loading…
Reference in a new issue