mirror of https://github.com/pixelfed/pixelfed.git
commit
e4b47d001c
|
@ -56,6 +56,9 @@
|
|||
- Updated NotificationCard, fix typo in mention, share and comments. Fixes #2848. ([b37bb426](https://github.com/pixelfed/pixelfed/commit/b37bb426))
|
||||
- Updated StatusCard.vue, add togglecw events to other presenters. ([9607243f](https://github.com/pixelfed/pixelfed/commit/9607243f))
|
||||
- Updated presenters, fix content warning layout. ([fc56acb8](https://github.com/pixelfed/pixelfed/commit/fc56acb8))
|
||||
- Updated reply blade view, fix missing avatar and media images. ([5fb33772](https://github.com/pixelfed/pixelfed/commit/5fb33772))
|
||||
- Updated components, add fallback default avatar. ([726553f5](https://github.com/pixelfed/pixelfed/commit/726553f5))
|
||||
- Updated job queue, separate deletes into their own queue. ([7f421392](https://github.com/pixelfed/pixelfed/commit/7f421392))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
|
||||
|
|
|
@ -3,16 +3,17 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\InboxPipeline\{
|
||||
InboxWorker,
|
||||
InboxValidator
|
||||
DeleteWorker,
|
||||
InboxWorker,
|
||||
InboxValidator
|
||||
};
|
||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||
use App\{
|
||||
AccountLog,
|
||||
Like,
|
||||
Profile,
|
||||
Status,
|
||||
User
|
||||
AccountLog,
|
||||
Like,
|
||||
Profile,
|
||||
Status,
|
||||
User
|
||||
};
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
|
@ -23,146 +24,158 @@ use Illuminate\Http\Request;
|
|||
use League\Fractal;
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use App\Util\ActivityPub\{
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Outbox
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Outbox
|
||||
};
|
||||
use Zttp\Zttp;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
public function nodeinfoWellKnown()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::wellKnown())
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
public function nodeinfoWellKnown()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::wellKnown())
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
|
||||
public function nodeinfo()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::get())
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
public function nodeinfo()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::get())
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
abort_if(!config('federation.webfinger.enabled'), 400);
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
abort_if(!config('federation.webfinger.enabled'), 400);
|
||||
|
||||
abort_if(!$request->filled('resource'), 400);
|
||||
abort_if(!$request->filled('resource'), 400);
|
||||
|
||||
$resource = $request->input('resource');
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
|
||||
abort(404);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
if($profile->status != null) {
|
||||
return ProfileController::accountCheck($profile);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
$resource = $request->input('resource');
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
|
||||
abort(404);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
if($profile->status != null) {
|
||||
return ProfileController::accountCheck($profile);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
|
||||
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
|
||||
public function hostMeta(Request $request)
|
||||
{
|
||||
abort_if(!config('federation.webfinger.enabled'), 404);
|
||||
public function hostMeta(Request $request)
|
||||
{
|
||||
abort_if(!config('federation.webfinger.enabled'), 404);
|
||||
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
|
||||
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.outbox'), 404);
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.outbox'), 404);
|
||||
|
||||
$profile = Profile::whereNull('domain')
|
||||
->whereNull('status')
|
||||
->whereIsPrivate(false)
|
||||
->whereUsername($username)
|
||||
->firstOrFail();
|
||||
$profile = Profile::whereNull('domain')
|
||||
->whereNull('status')
|
||||
->whereIsPrivate(false)
|
||||
->whereUsername($username)
|
||||
->firstOrFail();
|
||||
|
||||
$key = 'ap:outbox:latest_10:pid:' . $profile->id;
|
||||
$ttl = now()->addMinutes(15);
|
||||
$res = Cache::remember($key, $ttl, function() use($profile) {
|
||||
return Outbox::get($profile);
|
||||
});
|
||||
$key = 'ap:outbox:latest_10:pid:' . $profile->id;
|
||||
$ttl = now()->addMinutes(15);
|
||||
$res = Cache::remember($key, $ttl, function() use($profile) {
|
||||
return Outbox::get($profile);
|
||||
});
|
||||
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.inbox'), 404);
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.inbox'), 404);
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
return;
|
||||
}
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
$obj = json_decode($payload, true, 8);
|
||||
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
} else {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
|
||||
return;
|
||||
}
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
$obj = json_decode($payload, true, 8);
|
||||
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
} else {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if($profile->status != null) {
|
||||
abort(404);
|
||||
}
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
return response()->json($obj);
|
||||
}
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
if($profile->status != null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
return response()->json($obj);
|
||||
}
|
||||
|
||||
if($profile->status != null) {
|
||||
abort(404);
|
||||
}
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
$profile = Profile::whereNull('remote_url')
|
||||
->whereUsername($username)
|
||||
->whereIsPrivate(false)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json($obj);
|
||||
}
|
||||
if($profile->status != null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
|
||||
return response()->json($obj);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -573,9 +573,13 @@ class PublicApiController extends Controller
|
|||
{
|
||||
abort_unless(Auth::check(), 403);
|
||||
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
|
||||
$owner = Auth::id() == $profile->user_id;
|
||||
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
|
||||
return response()->json([]);
|
||||
}
|
||||
if(!$owner && $request->page > 5) {
|
||||
return [];
|
||||
}
|
||||
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
|
||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
@ -600,6 +604,10 @@ class PublicApiController extends Controller
|
|||
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
|
||||
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
|
||||
|
||||
if(!$owner && $request->page > 5) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if($search) {
|
||||
abort_if(!$owner, 404);
|
||||
$following = $profile->following()
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\DeletePipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
|
||||
class FanoutDeletePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $profile;
|
||||
|
||||
public $timeout = 300;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($profile)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->profile;
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$audience = Cache::remember('pf:ap:known_instances', now()->addHours(6), function() {
|
||||
return Profile::whereNotNull('sharedInbox')->groupBy('sharedInbox')->pluck('sharedInbox')->toArray();
|
||||
});
|
||||
|
||||
$activity = [
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => $profile->permalink('#delete'),
|
||||
"type" => "Delete",
|
||||
"actor" => $profile->permalink(),
|
||||
"to" => [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
],
|
||||
"object" => $profile->permalink(),
|
||||
];
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
}
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InboxPipeline;
|
||||
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\{
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Inbox
|
||||
};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Zttp\Zttp;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
|
||||
class DeleteWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $headers;
|
||||
protected $payload;
|
||||
|
||||
public $timeout = 60;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($headers, $payload)
|
||||
{
|
||||
$this->headers = $headers;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = null;
|
||||
$headers = $this->headers;
|
||||
$payload = json_decode($this->payload, true, 8);
|
||||
|
||||
if(isset($payload['id'])) {
|
||||
$lockKey = 'pf:ap:del-lock:' . hash('sha256', $payload['id']);
|
||||
if(Cache::get($lockKey) !== null) {
|
||||
// Job processed already
|
||||
return 1;
|
||||
}
|
||||
Cache::put($lockKey, 1, 300);
|
||||
}
|
||||
|
||||
if(!isset($headers['signature']) || !isset($headers['date'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(empty($headers) || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if( $payload['type'] === 'Delete' &&
|
||||
( ( is_string($payload['object']) &&
|
||||
$payload['object'] === $payload['actor'] ) ||
|
||||
( is_array($payload['object']) &&
|
||||
isset($payload['object']['id'], $payload['object']['type']) &&
|
||||
$payload['object']['type'] === 'Person' &&
|
||||
$payload['actor'] === $payload['object']['id']
|
||||
))
|
||||
) {
|
||||
$actor = $payload['actor'];
|
||||
$hash = strlen($actor) <= 48 ?
|
||||
'b:' . base64_encode($actor) :
|
||||
'h:' . hash('sha256', $actor);
|
||||
|
||||
$lockKey = 'ap:inbox:actor-delete-exists:lock:' . $hash;
|
||||
Cache::lock($lockKey, 10)->block(5, function () use(
|
||||
$headers,
|
||||
$payload,
|
||||
$actor,
|
||||
$hash
|
||||
) {
|
||||
$key = 'ap:inbox:actor-delete-exists:' . $hash;
|
||||
$actorDelete = Cache::remember($key, now()->addMinutes(15), function() use($actor) {
|
||||
return Profile::whereRemoteUrl($actor)
|
||||
->whereNotNull('domain')
|
||||
->exists();
|
||||
});
|
||||
if($actorDelete) {
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
Cache::set($key, false);
|
||||
$profile = Profile::whereNotNull('domain')
|
||||
->whereNull('status')
|
||||
->whereRemoteUrl($actor)
|
||||
->first();
|
||||
if($profile) {
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('delete');
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// Signature verification failed, exit.
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Remote user doesn't exist, exit early.
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$profile = null;
|
||||
|
||||
if($this->verifySignature($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else if($this->blindKeyRotation($headers, $payload) == true) {
|
||||
(new Inbox($headers, $profile, $payload))->handle();
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function verifySignature($headers, $payload)
|
||||
{
|
||||
$body = $this->payload;
|
||||
$bodyDecoded = $payload;
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$id = Helpers::validateUrl($bodyDecoded['id']);
|
||||
$keyDomain = parse_url($keyId, PHP_URL_HOST);
|
||||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
if(isset($bodyDecoded['object'])
|
||||
&& is_array($bodyDecoded['object'])
|
||||
&& isset($bodyDecoded['object']['attributedTo'])
|
||||
) {
|
||||
if(parse_url($bodyDecoded['object']['attributedTo'], PHP_URL_HOST) !== $keyDomain) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
|
||||
return;
|
||||
}
|
||||
$actor = Profile::whereKeyId($keyId)->first();
|
||||
if(!$actor) {
|
||||
$actorUrl = is_array($bodyDecoded['actor']) ? $bodyDecoded['actor'][0] : $bodyDecoded['actor'];
|
||||
$actor = Helpers::profileFirstOrNew($actorUrl);
|
||||
}
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
$pkey = openssl_pkey_get_public($actor->public_key);
|
||||
if(!$pkey) {
|
||||
return 0;
|
||||
}
|
||||
$inboxPath = "/f/inbox";
|
||||
list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
|
||||
if($verified == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function blindKeyRotation($headers, $payload)
|
||||
{
|
||||
$signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
|
||||
$date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
|
||||
if(!$signature) {
|
||||
return;
|
||||
}
|
||||
if(!$date) {
|
||||
return;
|
||||
}
|
||||
if(!now()->parse($date)->gt(now()->subDays(1)) ||
|
||||
!now()->parse($date)->lt(now()->addDays(1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$signatureData = HttpSignature::parseSignatureHeader($signature);
|
||||
$keyId = Helpers::validateUrl($signatureData['keyId']);
|
||||
$actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
|
||||
if(!$actor) {
|
||||
return;
|
||||
}
|
||||
if(Helpers::validateUrl($actor->remote_url) == false) {
|
||||
return;
|
||||
}
|
||||
$res = Zttp::timeout(5)->withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
|
||||
])->get($actor->remote_url);
|
||||
$res = json_decode($res->body(), true, 8);
|
||||
if($res['publicKey']['id'] !== $actor->key_id) {
|
||||
return;
|
||||
}
|
||||
$actor->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$actor->save();
|
||||
return $this->verifySignature($headers, $payload);
|
||||
}
|
||||
}
|
|
@ -6,66 +6,95 @@ use Illuminate\Support\Facades\Redis;
|
|||
|
||||
use App\{
|
||||
Follower,
|
||||
Profile
|
||||
Profile,
|
||||
User
|
||||
};
|
||||
|
||||
class FollowerService {
|
||||
class FollowerService
|
||||
{
|
||||
const FOLLOWING_KEY = 'pf:services:follow:following:id:';
|
||||
const FOLLOWERS_KEY = 'pf:services:follow:followers:id:';
|
||||
|
||||
protected $profile;
|
||||
public static $follower_prefix = 'px:profile:followers-v1.3:';
|
||||
public static $following_prefix = 'px:profile:following-v1.3:';
|
||||
|
||||
public static function build()
|
||||
public static function add($actor, $target)
|
||||
{
|
||||
return new self();
|
||||
Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
|
||||
Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
|
||||
}
|
||||
|
||||
public function profile(Profile $profile)
|
||||
public static function remove($actor, $target)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
self::$follower_prefix .= $profile->id;
|
||||
self::$following_prefix .= $profile->id;
|
||||
return $this;
|
||||
Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
|
||||
Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
|
||||
}
|
||||
|
||||
public function followers($limit = 100, $offset = 1)
|
||||
public static function followers($id, $start = 0, $stop = 10)
|
||||
{
|
||||
if(Redis::zcard(self::$follower_prefix) == 0) {
|
||||
$followers = $this->profile->followers()->pluck('profile_id');
|
||||
$followers->map(function($i) {
|
||||
Redis::zadd(self::$follower_prefix, $i, $i);
|
||||
});
|
||||
return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
|
||||
} else {
|
||||
return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
|
||||
}
|
||||
return Redis::zrange(self::FOLLOWERS_KEY . $id, $start, $stop);
|
||||
}
|
||||
|
||||
|
||||
public function following($limit = 100, $offset = 1)
|
||||
public static function following($id, $start = 0, $stop = 10)
|
||||
{
|
||||
if(Redis::zcard(self::$following_prefix) == 0) {
|
||||
$following = $this->profile->following()->pluck('following_id');
|
||||
$following->map(function($i) {
|
||||
Redis::zadd(self::$following_prefix, $i, $i);
|
||||
});
|
||||
return Redis::zrevrange(self::$following_prefix, $offset, $limit);
|
||||
} else {
|
||||
return Redis::zrevrange(self::$following_prefix, $offset, $limit);
|
||||
}
|
||||
return Redis::zrange(self::FOLLOWING_KEY . $id, $start, $stop);
|
||||
}
|
||||
|
||||
public static function follows(string $actor, string $target)
|
||||
{
|
||||
$key = self::$follower_prefix . $target;
|
||||
if(Redis::zcard($key) == 0) {
|
||||
$p = Profile::findOrFail($target);
|
||||
self::build()->profile($p)->followers(1);
|
||||
self::build()->profile($p)->following(1);
|
||||
return (bool) Redis::zrank($key, $actor);
|
||||
} else {
|
||||
return (bool) Redis::zrank($key, $actor);
|
||||
return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
|
||||
}
|
||||
|
||||
public static function audience($profile)
|
||||
{
|
||||
return (new self)->getAudienceInboxes($profile);
|
||||
}
|
||||
|
||||
protected function getAudienceInboxes($profile)
|
||||
{
|
||||
if($profile instanceOf User) {
|
||||
return $profile
|
||||
->profile
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
if($profile instanceOf Profile) {
|
||||
return $profile
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
if(is_string($profile) || is_integer($profile)) {
|
||||
$profile = Profile::whereNull('domain')->find($profile);
|
||||
if(!$profile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $profile
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,190 +2,191 @@
|
|||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the subdomain where Horizon will be accessible from. If this
|
||||
| setting is null, Horizon will reside under the same domain as the
|
||||
| application. Otherwise, this value will serve as the subdomain.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the subdomain where Horizon will be accessible from. If this
|
||||
| setting is null, Horizon will reside under the same domain as the
|
||||
| application. Otherwise, this value will serve as the subdomain.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => null,
|
||||
'domain' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI path where Horizon will be accessible from. Feel free
|
||||
| to change this path to anything you like. Note that the URI will not
|
||||
| affect the paths of its internal API that aren't exposed to users.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI path where Horizon will be accessible from. Feel free
|
||||
| to change this path to anything you like. Note that the URI will not
|
||||
| affect the paths of its internal API that aren't exposed to users.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => 'horizon',
|
||||
'path' => 'horizon',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the name of the Redis connection where Horizon will store the
|
||||
| meta information required for it to function. It includes the list
|
||||
| of supervisors, failed jobs, job metrics, and other information.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the name of the Redis connection where Horizon will store the
|
||||
| meta information required for it to function. It includes the list
|
||||
| of supervisors, failed jobs, job metrics, and other information.
|
||||
|
|
||||
*/
|
||||
|
||||
'use' => 'default',
|
||||
'use' => 'default',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This prefix will be used when storing all Horizon data in Redis. You
|
||||
| may modify the prefix when you are running multiple installations
|
||||
| of Horizon on the same server so that they don't have problems.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This prefix will be used when storing all Horizon data in Redis. You
|
||||
| may modify the prefix when you are running multiple installations
|
||||
| of Horizon on the same server so that they don't have problems.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('HORIZON_PREFIX', 'horizon-'.str_random(8).':'),
|
||||
'prefix' => env('HORIZON_PREFIX', 'horizon-'.str_random(8).':'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will get attached onto each Horizon route, giving you
|
||||
| the chance to add your own middleware to this list or change any of
|
||||
| the existing middleware. Or, you can simply stick with this list.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will get attached onto each Horizon route, giving you
|
||||
| the chance to add your own middleware to this list or change any of
|
||||
| the existing middleware. Or, you can simply stick with this list.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => ['web'],
|
||||
'middleware' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Wait Time Thresholds
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to configure when the LongWaitDetected event
|
||||
| will be fired. Every connection / queue combination may have its
|
||||
| own, unique threshold (in seconds) before this event is fired.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Wait Time Thresholds
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to configure when the LongWaitDetected event
|
||||
| will be fired. Every connection / queue combination may have its
|
||||
| own, unique threshold (in seconds) before this event is fired.
|
||||
|
|
||||
*/
|
||||
|
||||
'waits' => [
|
||||
'redis:feed' => 30,
|
||||
'redis:default' => 30,
|
||||
'redis:high' => 30,
|
||||
],
|
||||
'waits' => [
|
||||
'redis:feed' => 30,
|
||||
'redis:default' => 30,
|
||||
'redis:high' => 30,
|
||||
'redis:delete' => 30
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Trimming Times
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||
| for one hour while all failed jobs are stored for an entire week.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Trimming Times
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||
| for one hour while all failed jobs are stored for an entire week.
|
||||
|
|
||||
*/
|
||||
|
||||
'trim' => [
|
||||
'recent' => 60,
|
||||
'pending' => 60,
|
||||
'completed' => 60,
|
||||
'recent_failed' => 10080,
|
||||
'failed' => 10080,
|
||||
'monitored' => 10080,
|
||||
],
|
||||
'trim' => [
|
||||
'recent' => 60,
|
||||
'pending' => 60,
|
||||
'completed' => 60,
|
||||
'recent_failed' => 10080,
|
||||
'failed' => 10080,
|
||||
'monitored' => 10080,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Metrics
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure how many snapshots should be kept to display in
|
||||
| the metrics graph. This will get used in combination with Horizon's
|
||||
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Metrics
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure how many snapshots should be kept to display in
|
||||
| the metrics graph. This will get used in combination with Horizon's
|
||||
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||
|
|
||||
*/
|
||||
|
||||
'metrics' => [
|
||||
'trim_snapshots' => [
|
||||
'job' => 24,
|
||||
'queue' => 24,
|
||||
],
|
||||
],
|
||||
'metrics' => [
|
||||
'trim_snapshots' => [
|
||||
'job' => 24,
|
||||
'queue' => 24,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fast Termination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this option is enabled, Horizon's "terminate" command will not
|
||||
| wait on all of the workers to terminate unless the --wait option
|
||||
| is provided. Fast termination can shorten deployment delay by
|
||||
| allowing a new instance of Horizon to start while the last
|
||||
| instance will continue to terminate each of its workers.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fast Termination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this option is enabled, Horizon's "terminate" command will not
|
||||
| wait on all of the workers to terminate unless the --wait option
|
||||
| is provided. Fast termination can shorten deployment delay by
|
||||
| allowing a new instance of Horizon to start while the last
|
||||
| instance will continue to terminate each of its workers.
|
||||
|
|
||||
*/
|
||||
|
||||
'fast_termination' => false,
|
||||
'fast_termination' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Memory Limit (MB)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value describes the maximum amount of memory the Horizon worker
|
||||
| may consume before it is terminated and restarted. You should set
|
||||
| this value according to the resources available to your server.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Memory Limit (MB)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value describes the maximum amount of memory the Horizon worker
|
||||
| may consume before it is terminated and restarted. You should set
|
||||
| this value according to the resources available to your server.
|
||||
|
|
||||
*/
|
||||
|
||||
'memory_limit' => 64,
|
||||
'memory_limit' => 64,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Worker Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the queue worker settings used by your application
|
||||
| in all environments. These supervisors and settings handle all your
|
||||
| queued jobs and will be provisioned by Horizon during deployment.
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Worker Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the queue worker settings used by your application
|
||||
| in all environments. These supervisors and settings handle all your
|
||||
| queued jobs and will be provisioned by Horizon during deployment.
|
||||
|
|
||||
*/
|
||||
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'feed'],
|
||||
'balance' => 'auto',
|
||||
'maxProcesses' => 20,
|
||||
'memory' => 128,
|
||||
'tries' => 3,
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'feed', 'delete'],
|
||||
'balance' => 'auto',
|
||||
'maxProcesses' => 20,
|
||||
'memory' => 128,
|
||||
'tries' => 3,
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
|
||||
'local' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'feed'],
|
||||
'balance' => 'auto',
|
||||
'maxProcesses' => 20,
|
||||
'memory' => 128,
|
||||
'tries' => 3,
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
'local' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'feed', 'delete'],
|
||||
'balance' => 'auto',
|
||||
'maxProcesses' => 20,
|
||||
'memory' => 128,
|
||||
'tries' => 3,
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'darkmode' => env('HORIZON_DARKMODE', false),
|
||||
'darkmode' => env('HORIZON_DARKMODE', false),
|
||||
];
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -16,7 +16,7 @@
|
|||
"/js/compose.js": "/js/compose.js?id=08dfa37755b54cbb05fa",
|
||||
"/js/compose-classic.js": "/js/compose-classic.js?id=c994899e82ff5a905c81",
|
||||
"/js/developers.js": "/js/developers.js?id=9bba3be10491e58de952",
|
||||
"/js/direct.js": "/js/direct.js?id=45d3d8036a46584e962c",
|
||||
"/js/direct.js": "/js/direct.js?id=06d4c4428d06cf130781",
|
||||
"/js/discover.js": "/js/discover.js?id=dcc903e7b89832d4fa59",
|
||||
"/js/hashtag.js": "/js/hashtag.js?id=8137c9aeac44cd972dbc",
|
||||
"/js/loops.js": "/js/loops.js?id=ae34b77c4cfe1824f5a0",
|
||||
|
@ -27,8 +27,8 @@
|
|||
"/js/rempos.js": "/js/rempos.js?id=538f0501722a5f8d8c10",
|
||||
"/js/rempro.js": "/js/rempro.js?id=b0c387fbbaef217dd089",
|
||||
"/js/search.js": "/js/search.js?id=3ba14b0584cb9d408a3f",
|
||||
"/js/status.js": "/js/status.js?id=8dd584499c4b31ccfc86",
|
||||
"/js/status.js": "/js/status.js?id=3f4f64f3b09ef0d2b3aa",
|
||||
"/js/story-compose.js": "/js/story-compose.js?id=b16bcf2adad9651735e1",
|
||||
"/js/theme-monokai.js": "/js/theme-monokai.js?id=8842103833ba4861bcfa",
|
||||
"/js/timeline.js": "/js/timeline.js?id=c40750be8fa9b3a88bf5"
|
||||
"/js/timeline.js": "/js/timeline.js?id=9167e5c8ff055146a933"
|
||||
}
|
||||
|
|
|
@ -26,10 +26,10 @@
|
|||
<div v-if="!messages.inbox.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
||||
<p class="lead mb-0">No messages found :(</p>
|
||||
</div>
|
||||
<div v-else v-for="(thread, index) in messages.inbox">
|
||||
<div v-else v-for="(thread, index) in messages.inbox" :key="'dm_inbox'+index">
|
||||
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" :href="'/account/direct/t/'+thread.id">
|
||||
<div class="media d-flex align-items-center">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';" v-once>
|
||||
<div class="media-body">
|
||||
<p class="mb-0">
|
||||
<span class="font-weight-bold text-truncate">
|
||||
|
@ -62,10 +62,10 @@
|
|||
<div v-if="!messages.sent.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
||||
<p class="lead mb-0">No messages found :(</p>
|
||||
</div>
|
||||
<div v-else v-for="(thread, index) in messages.sent">
|
||||
<div v-else v-for="(thread, index) in messages.sent" :key="'dm_sent'+index">
|
||||
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
|
||||
<div class="media d-flex align-items-center">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';" v-once>
|
||||
<div class="media-body">
|
||||
<p class="mb-0">
|
||||
<span class="font-weight-bold text-truncate">
|
||||
|
@ -98,10 +98,10 @@
|
|||
<div v-if="!messages.filtered.length" class="list-group-item d-flex justify-content-center align-items-center" style="min-height: 40vh;">
|
||||
<p class="lead mb-0">No messages found :(</p>
|
||||
</div>
|
||||
<div v-else v-for="(thread, index) in messages.filtered">
|
||||
<div v-else v-for="(thread, index) in messages.filtered" :key="'dm_filtered'+index">
|
||||
<a class="list-group-item text-dark text-decoration-none border-left-0 border-right-0 border-top-0" href="#" @click.prevent="loadMessage(thread.id)">
|
||||
<div class="media d-flex align-items-center">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="32px">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';" v-once>
|
||||
<div class="media-body">
|
||||
<p class="mb-0">
|
||||
<span class="font-weight-bold text-truncate">
|
||||
|
@ -373,4 +373,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</span>
|
||||
<span>
|
||||
<div class="media">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" alt="Generic placeholder image" width="40px">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" :src="thread.avatar" width="40" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
<div class="media-body">
|
||||
<p class="mb-0">
|
||||
<span class="font-weight-bold">{{thread.name}}</span>
|
||||
|
@ -40,10 +40,10 @@
|
|||
</li>
|
||||
<li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg cursor-pointer" @click="openCtxMenu(convo, index)">
|
||||
<div v-if="!convo.isAuthor" class="media d-inline-flex mb-0">
|
||||
<img v-if="!hideAvatars" class="mr-3 mt-2 rounded-circle img-thumbnail" :src="thread.avatar" alt="avatar" width="32px">
|
||||
<img v-if="!hideAvatars" class="mr-3 mt-2 rounded-circle img-thumbnail" :src="thread.avatar" alt="avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
<div class="media-body">
|
||||
<p v-if="convo.type == 'photo'" class="pill-to p-0 shadow">
|
||||
<img :src="convo.media" width="140px" style="border-radius:20px;">
|
||||
<img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
|
||||
</p>
|
||||
<div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer">
|
||||
<div class="media-body">
|
||||
|
@ -90,7 +90,7 @@
|
|||
<div v-else class="media d-inline-flex float-right mb-0">
|
||||
<div class="media-body">
|
||||
<p v-if="convo.type == 'photo'" class="pill-from p-0 shadow">
|
||||
<img :src="convo.media" width="140px" style="border-radius:20px;">
|
||||
<img :src="convo.media" width="140" style="border-radius:20px;" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
|
||||
</p>
|
||||
<div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer">
|
||||
<div class="media-body">
|
||||
|
@ -134,7 +134,7 @@
|
|||
</p>
|
||||
<p v-else> </p>
|
||||
</div>
|
||||
<img v-if="!hideAvatars" class="ml-3 mt-2 rounded-circle img-thumbnail" :src="profile.avatar" alt="avatar" width="32px">
|
||||
<img v-if="!hideAvatars" class="ml-3 mt-2 rounded-circle img-thumbnail" :src="profile.avatar" alt="avatar" width="32" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
@ -682,4 +682,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -466,7 +466,7 @@
|
|||
<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
|
||||
<div class="media">
|
||||
<a :href="user.url">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg';">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
|
|
|
@ -43,10 +43,10 @@
|
|||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div v-if="hasStory" class="has-story cursor-pointer shadow-sm" @click="storyRedirect()">
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle" :src="profile.avatar" width="77px" height="77px">
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle" :src="profile.avatar" width="77px" height="77px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</div>
|
||||
<div v-else>
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border" :src="profile.avatar" width="77px" height="77px">
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border" :src="profile.avatar" width="77px" height="77px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
|
@ -85,10 +85,10 @@
|
|||
<!-- DESKTOP PROFILE PICTURE -->
|
||||
<div class="d-none d-md-block pb-3">
|
||||
<div v-if="hasStory" class="has-story-lg cursor-pointer shadow-sm" @click="storyRedirect()">
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow cursor-pointer" :src="profile.avatar" width="150px" height="150px">
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow cursor-pointer" :src="profile.avatar" width="150px" height="150px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</div>
|
||||
<div v-else>
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px">
|
||||
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</div>
|
||||
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3">
|
||||
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
|
||||
|
@ -404,53 +404,54 @@
|
|||
title="Following"
|
||||
body-class="list-group-flush py-3 px-0"
|
||||
dialog-class="follow-modal">
|
||||
<div v-if="!loading" class="list-group" style="min-height: 60vh;">
|
||||
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
|
||||
<span class="d-flex px-4 pb-0 align-items-center">
|
||||
<i class="fas fa-search text-lighter"></i>
|
||||
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
|
||||
</span>
|
||||
<div v-if="!followingLoading" class="list-group" style="max-height: 60vh;">
|
||||
<div v-if="!following.length" class="list-group-item border-0">
|
||||
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
||||
<span class="text-dark">{{profileUsername}}</span> is not following yet</p>
|
||||
</div>
|
||||
<div v-if="owner == true" class="btn-group rounded-0 mt-n3 mb-3 border-top" role="group" aria-label="Following">
|
||||
<!-- <button type="button" :class="[followingModalTab == 'following' ? ' btn btn-light py-3 rounded-0 font-weight-bold modal-tab-active' : 'btn btn-light py-3 rounded-0 font-weight-bold']" style="font-size: 12px;">FOLLOWING</button> -->
|
||||
<!-- <button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">MUTED</button>
|
||||
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">BLOCKED</button> -->
|
||||
</div>
|
||||
<div v-else class="btn-group rounded-0 mt-n3 mb-3" role="group" aria-label="Following">
|
||||
<!-- <button type="button" class="btn btn-light py-3 rounded-0 border-primary border-left-0 border-right-0 border-top-0 font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'following'">FOLLOWING</button>
|
||||
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'mutual'">MUTUAL</button>
|
||||
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'blocked'">BLOCKED</button> -->
|
||||
</div>
|
||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in following" :key="'following_'+index">
|
||||
<div class="media">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
</a>
|
||||
<div class="media-body text-truncate">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="!user.local" class="text-muted mb-0 text-truncate mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name}}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="owner">
|
||||
<a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
|
||||
<div v-else>
|
||||
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
|
||||
<span class="d-flex px-4 pb-0 align-items-center">
|
||||
<i class="fas fa-search text-lighter"></i>
|
||||
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
|
||||
</span>
|
||||
</div>
|
||||
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
|
||||
<div class="media">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
</a>
|
||||
<div class="media-body text-truncate">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name ? user.display_name : user.username}}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="owner">
|
||||
<a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
|
||||
<div class="list-group-item border-0 pt-5">
|
||||
<p class="p-3 text-center mb-0 lead">No Results Found</p>
|
||||
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
|
||||
<div class="list-group-item border-0 pt-5">
|
||||
<p class="p-3 text-center mb-0 lead">No Results Found</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
|
||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
|
||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||
</div>
|
||||
<div v-else class="text-center py-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
@ -463,31 +464,42 @@
|
|||
body-class="list-group-flush py-3 px-0"
|
||||
dialog-class="follow-modal"
|
||||
>
|
||||
<div class="list-group">
|
||||
<div v-if="followers.length == 0" class="list-group-item border-0">
|
||||
<div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
|
||||
<div v-if="!followers.length" class="list-group-item border-0">
|
||||
<p class="text-center mb-0 font-weight-bold text-muted py-5">
|
||||
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
|
||||
</div>
|
||||
<div class="list-group-item border-0 py-1" v-for="(user, index) in followers" :key="'follower_'+index">
|
||||
<div class="media mb-0">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
</a>
|
||||
<div class="media-body mb-0">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-secondary mb-0" style="font-size: 13px">
|
||||
{{user.display_name}}
|
||||
</p>
|
||||
|
||||
<div v-else>
|
||||
<div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in followers" :key="'follower_'+index">
|
||||
<div class="media mb-0">
|
||||
<a :href="profileUrlRedirect(user)">
|
||||
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
|
||||
</a>
|
||||
<div class="media-body mb-0">
|
||||
<p class="mb-0" style="font-size: 14px">
|
||||
<a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
|
||||
{{user.username}}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
|
||||
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name ? user.display_name : user.username}}
|
||||
</p>
|
||||
</div>
|
||||
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
|
||||
</div>
|
||||
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
|
||||
</div>
|
||||
<div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()">
|
||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()">
|
||||
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
|
||||
</div>
|
||||
<div v-else class="text-center py-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
@ -558,20 +570,20 @@
|
|||
</div>
|
||||
</b-modal>
|
||||
<b-modal ref="embedModal"
|
||||
id="ctx-embed-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="md"
|
||||
body-class="p-2 rounded">
|
||||
<div>
|
||||
<textarea class="form-control disabled text-monospace" rows="6" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea>
|
||||
<hr>
|
||||
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
|
||||
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
|
||||
</div>
|
||||
</b-modal>
|
||||
id="ctx-embed-modal"
|
||||
hide-header
|
||||
hide-footer
|
||||
centered
|
||||
rounded
|
||||
size="md"
|
||||
body-class="p-2 rounded">
|
||||
<div>
|
||||
<textarea class="form-control disabled text-monospace" rows="6" style="overflow-y:hidden;border: 1px solid #efefef; font-size: 12px; line-height: 18px; margin: 0 0 7px;resize:none;" v-model="ctxEmbedPayload" disabled=""></textarea>
|
||||
<hr>
|
||||
<button :class="copiedEmbed ? 'btn btn-primary btn-block btn-sm py-1 font-weight-bold disabed': 'btn btn-primary btn-block btn-sm py-1 font-weight-bold'" @click="ctxCopyEmbed" :disabled="copiedEmbed">{{copiedEmbed ? 'Embed Code Copied!' : 'Copy Embed Code'}}</button>
|
||||
<p class="mb-0 px-2 small text-muted">By using this embed, you agree to our <a href="/site/terms">Terms of Use</a></p>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<style type="text/css" scoped>
|
||||
|
@ -652,7 +664,6 @@
|
|||
<script type="text/javascript">
|
||||
import VueMasonry from 'vue-masonry-css'
|
||||
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'profile-id',
|
||||
|
@ -679,9 +690,11 @@
|
|||
followers: [],
|
||||
followerCursor: 1,
|
||||
followerMore: true,
|
||||
followerLoading: true,
|
||||
following: [],
|
||||
followingCursor: 1,
|
||||
followingMore: true,
|
||||
followingLoading: true,
|
||||
warning: false,
|
||||
sponsorList: [],
|
||||
bookmarks: [],
|
||||
|
@ -1121,6 +1134,7 @@
|
|||
if(res.data.length < 10) {
|
||||
this.followingMore = false;
|
||||
}
|
||||
this.followingLoading = false;
|
||||
});
|
||||
this.$refs.followingModal.show();
|
||||
return;
|
||||
|
@ -1150,6 +1164,7 @@
|
|||
if(res.data.length < 10) {
|
||||
this.followerMore = false;
|
||||
}
|
||||
this.followerLoading = false;
|
||||
})
|
||||
this.$refs.followerModal.show();
|
||||
return;
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
<a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
|
||||
<div class="pb-3">
|
||||
<div class="media align-items-center py-2 pr-3">
|
||||
<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
|
||||
<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
<div class="media-body">
|
||||
<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
|
||||
{{profile.value}}
|
||||
|
@ -123,8 +123,8 @@
|
|||
<p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
|
||||
</div>
|
||||
<div v-if="results.statuses.length">
|
||||
<a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
|
||||
<img :src="status.thumb" width="90px" height="90px" class="mb-2">
|
||||
<a v-for="(status, index) in results.statuses" :key="'srs:'+index" class="mr-2 result-card" :href="buildUrl('status', status)">
|
||||
<img :src="status.thumb" width="90px" height="90px" class="mb-2" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';" v-once>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<announcements-card v-on:show-tips="showTips = $event"></announcements-card>
|
||||
</div>
|
||||
|
||||
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
|
||||
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card status-card rounded-0 shadow-none border">
|
||||
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
||||
<h6 class="text-muted font-weight-bold mb-0">Suggestions For You</h6>
|
||||
<span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
|
||||
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="index == 4 && showHashtagPosts && hashtagPosts.length" class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
|
||||
<div v-if="index == 4 && showHashtagPosts && hashtagPosts.length" class="card status-card rounded-0 shadow-none border">
|
||||
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
||||
<span></span>
|
||||
<h6 class="text-muted font-weight-bold mb-0"><a :href="'/discover/tags/'+hashtagPostsName+'?src=tr'">#{{hashtagPostsName}}</a></h6>
|
||||
|
@ -104,6 +104,7 @@
|
|||
</div>
|
||||
|
||||
<status-card
|
||||
:class="{ 'border-top': index === 0 }"
|
||||
:status="status"
|
||||
:reaction-bar="reactionBar"
|
||||
v-on:status-delete="deleteStatus"
|
||||
|
@ -112,7 +113,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="!loading && feed.length">
|
||||
<div class="card rounded-0 border-top-0 status-card card-md-rounded-0 shadow-none border">
|
||||
<div class="card rounded-0 border-top-0 status-card rounded-0 shadow-none border">
|
||||
<div class="card-body py-5 my-5">
|
||||
<infinite-loading @infinite="infiniteTimeline" :distance="800">
|
||||
<div slot="no-more">
|
||||
|
@ -157,8 +158,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && scope == 'home' && feed.length == 0">
|
||||
<div class="card rounded-0 mt-4 status-card card-md-rounded-0 shadow-none border">
|
||||
<div class="card rounded-0 mt-4 status-card rounded-0 shadow-none border">
|
||||
<div v-if="profile.following_count != '0'" class="card-body py-5 my-5">
|
||||
<p class="text-center"><i class="far fa-check-circle fa-8x text-lighter"></i></p>
|
||||
<p class="text-center h3 font-weight-light">You're All Caught Up!</p>
|
||||
|
@ -194,7 +196,7 @@
|
|||
v-for="(status, index) in discover_feed"
|
||||
:key="`discover_feed-${index}-${status.id}`">
|
||||
|
||||
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border">
|
||||
<div v-if="index == 2 && showSuggestions == true && suggestions.length" class="card status-card rounded-0 shadow-none border">
|
||||
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
|
||||
<h6 class="text-muted font-weight-bold mb-0">Suggestions For You</h6>
|
||||
<span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
|
||||
|
@ -273,7 +275,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<status-card :status="status" :recommended="true" />
|
||||
<status-card
|
||||
:class="{'border-top': index === 0}"
|
||||
:status="status"
|
||||
:recommended="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
<div class="card-body p-0 m-0 bg-light border-bottom">
|
||||
<div class="d-flex p-0 m-0 align-items-center">
|
||||
@if($status->parent()->parent()->media()->count())
|
||||
<img src="{{$status->parent()->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail">
|
||||
<img src="{{$status->parent()->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';">
|
||||
@endif
|
||||
<div class="p-4 w-100">
|
||||
<div class="">
|
||||
<div class="media">
|
||||
<img src="{{$status->parent()->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px">
|
||||
<img src="{{$status->parent()->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
<div class="media-body">
|
||||
<span class="font-weight-bold" v-pre>{{$status->parent()->parent()->profile->username}}</span>
|
||||
<div class="">
|
||||
|
@ -36,12 +36,12 @@
|
|||
<div class="card-body p-0 m-0 bg-light border-bottom">
|
||||
<div class="d-flex p-0 m-0 align-items-center">
|
||||
@if($status->parent()->media()->count())
|
||||
<img src="{{$status->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail">
|
||||
<img src="{{$status->parent()->thumb()}}" width="150px" height="150px" class="post-thumbnail" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';">
|
||||
@endif
|
||||
<div class="p-4 w-100">
|
||||
<div class="">
|
||||
<div class="media">
|
||||
<img src="{{$status->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px">
|
||||
<img src="{{$status->parent()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
<div class="media-body">
|
||||
<span class="font-weight-bold" v-pre>{{$status->parent()->profile->username}}</span>
|
||||
<div class="">
|
||||
|
@ -66,7 +66,7 @@
|
|||
<p class="py-5 mb-0 text-center">This comment may contain sensitive content. <span class="float-right font-weight-bold text-primary">Show</span></p>
|
||||
</summary>
|
||||
<div class="media py-5">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
<div class="media-body">
|
||||
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
|
||||
<p class="" v-pre>{!! $status->rendered !!}</p>
|
||||
|
@ -80,7 +80,7 @@
|
|||
</details>
|
||||
@else
|
||||
<div class="media py-5">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px">
|
||||
<img class="mr-3 rounded-circle img-thumbnail" src="{{$status->profile->avatarUrl()}}" width="60px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
<div class="media-body">
|
||||
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
|
||||
<p class="" v-pre>{!! $status->rendered !!}</p>
|
||||
|
@ -105,12 +105,12 @@
|
|||
<div class="card-body p-0 m-0 bg-light border-bottom">
|
||||
<div class="d-flex p-0 m-0 align-items-center">
|
||||
@if($status->comments()->first()->media()->count())
|
||||
<img src="{{$status->comments()->first()->thumb()}}" width="150px" height="150px" class="post-thumbnail">
|
||||
<img src="{{$status->comments()->first()->thumb()}}" width="150px" height="150px" class="post-thumbnail" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0';">
|
||||
@endif
|
||||
<div class="p-4 w-100">
|
||||
<div class="">
|
||||
<div class="media">
|
||||
<img src="{{$status->comments()->first()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px">
|
||||
<img src="{{$status->comments()->first()->profile->avatarUrl()}}" class="rounded-circle img-thumbnail mb-1 mr-3" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
<div class="media-body">
|
||||
<span class="font-weight-bold" v-pre>{{$status->comments()->first()->profile->username}}</span>
|
||||
<div class="">
|
||||
|
|
Loading…
Reference in New Issue