Merge pull request #2853 from pixelfed/staging

Staging
This commit is contained in:
daniel 2021-07-13 23:31:27 -06:00 committed by GitHub
commit e4b47d001c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 825 additions and 435 deletions

View file

@ -56,6 +56,9 @@
- Updated NotificationCard, fix typo in mention, share and comments. Fixes #2848. ([b37bb426](https://github.com/pixelfed/pixelfed/commit/b37bb426)) - 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 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 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/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0) ## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Jobs\InboxPipeline\{ use App\Jobs\InboxPipeline\{
DeleteWorker,
InboxWorker, InboxWorker,
InboxValidator InboxValidator
}; };
@ -104,7 +105,13 @@ class FederationController extends Controller
$headers = $request->headers->all(); $headers = $request->headers->all();
$payload = $request->getContent(); $payload = $request->getContent();
$obj = json_decode($payload, true, 8);
if(isset($obj['type']) && $obj['type'] === 'Delete') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
} else {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high'); dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
}
return; return;
} }
@ -115,7 +122,13 @@ class FederationController extends Controller
$headers = $request->headers->all(); $headers = $request->headers->all();
$payload = $request->getContent(); $payload = $request->getContent();
$obj = json_decode($payload, true, 8);
if(isset($obj['type']) && $obj['type'] === 'Delete') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
} else {
dispatch(new InboxWorker($headers, $payload))->onQueue('high'); dispatch(new InboxWorker($headers, $payload))->onQueue('high');
}
return; return;
} }

View file

@ -573,9 +573,13 @@ class PublicApiController extends Controller
{ {
abort_unless(Auth::check(), 403); abort_unless(Auth::check(), 403);
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id); $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) { if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
return response()->json([]); return response()->json([]);
} }
if(!$owner && $request->page > 5) {
return [];
}
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10); $followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer()); $resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray(); $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($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404); abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
if(!$owner && $request->page > 5) {
return [];
}
if($search) { if($search) {
abort_if(!$owner, 404); abort_if(!$owner, 404);
$following = $profile->following() $following = $profile->following()

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -6,66 +6,95 @@ use Illuminate\Support\Facades\Redis;
use App\{ use App\{
Follower, Follower,
Profile Profile,
User
}; };
class FollowerService { class FollowerService
protected $profile;
public static $follower_prefix = 'px:profile:followers-v1.3:';
public static $following_prefix = 'px:profile:following-v1.3:';
public static function build()
{ {
return new self(); const FOLLOWING_KEY = 'pf:services:follow:following:id:';
} const FOLLOWERS_KEY = 'pf:services:follow:followers:id:';
public function profile(Profile $profile) public static function add($actor, $target)
{ {
$this->profile = $profile; Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
self::$follower_prefix .= $profile->id; Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
self::$following_prefix .= $profile->id;
return $this;
} }
public function followers($limit = 100, $offset = 1) public static function remove($actor, $target)
{ {
if(Redis::zcard(self::$follower_prefix) == 0) { Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
$followers = $this->profile->followers()->pluck('profile_id'); Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
$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);
}
} }
public static function followers($id, $start = 0, $stop = 10)
public function following($limit = 100, $offset = 1)
{ {
if(Redis::zcard(self::$following_prefix) == 0) { return Redis::zrange(self::FOLLOWERS_KEY . $id, $start, $stop);
$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);
} }
public static function following($id, $start = 0, $stop = 10)
{
return Redis::zrange(self::FOLLOWING_KEY . $id, $start, $stop);
} }
public static function follows(string $actor, string $target) public static function follows(string $actor, string $target)
{ {
$key = self::$follower_prefix . $target; return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
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);
} }
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 [];
} }
} }

View file

@ -82,6 +82,7 @@ return [
'redis:feed' => 30, 'redis:feed' => 30,
'redis:default' => 30, 'redis:default' => 30,
'redis:high' => 30, 'redis:high' => 30,
'redis:delete' => 30
], ],
/* /*
@ -165,7 +166,7 @@ return [
'production' => [ 'production' => [
'supervisor-1' => [ 'supervisor-1' => [
'connection' => 'redis', 'connection' => 'redis',
'queue' => ['high', 'default', 'feed'], 'queue' => ['high', 'default', 'feed', 'delete'],
'balance' => 'auto', 'balance' => 'auto',
'maxProcesses' => 20, 'maxProcesses' => 20,
'memory' => 128, 'memory' => 128,
@ -177,7 +178,7 @@ return [
'local' => [ 'local' => [
'supervisor-1' => [ 'supervisor-1' => [
'connection' => 'redis', 'connection' => 'redis',
'queue' => ['high', 'default', 'feed'], 'queue' => ['high', 'default', 'feed', 'delete'],
'balance' => 'auto', 'balance' => 'auto',
'maxProcesses' => 20, 'maxProcesses' => 20,
'memory' => 128, 'memory' => 128,

BIN
public/js/direct.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -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;"> <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> <p class="lead mb-0">No messages found :(</p>
</div> </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"> <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"> <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"> <div class="media-body">
<p class="mb-0"> <p class="mb-0">
<span class="font-weight-bold text-truncate"> <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;"> <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> <p class="lead mb-0">No messages found :(</p>
</div> </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)"> <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"> <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"> <div class="media-body">
<p class="mb-0"> <p class="mb-0">
<span class="font-weight-bold text-truncate"> <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;"> <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> <p class="lead mb-0">No messages found :(</p>
</div> </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)"> <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"> <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"> <div class="media-body">
<p class="mb-0"> <p class="mb-0">
<span class="font-weight-bold text-truncate"> <span class="font-weight-bold text-truncate">

View file

@ -11,7 +11,7 @@
</span> </span>
<span> <span>
<div class="media"> <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"> <div class="media-body">
<p class="mb-0"> <p class="mb-0">
<span class="font-weight-bold">{{thread.name}}</span> <span class="font-weight-bold">{{thread.name}}</span>
@ -40,10 +40,10 @@
</li> </li>
<li v-for="(convo, index) in thread.messages" class="list-group-item border-0 chat-msg cursor-pointer" @click="openCtxMenu(convo, index)"> <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"> <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"> <div class="media-body">
<p v-if="convo.type == 'photo'" class="pill-to p-0 shadow"> <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> </p>
<div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer"> <div v-else-if="convo.type == 'link'" class="media d-inline-flex mb-0 cursor-pointer">
<div class="media-body"> <div class="media-body">
@ -90,7 +90,7 @@
<div v-else class="media d-inline-flex float-right mb-0"> <div v-else class="media d-inline-flex float-right mb-0">
<div class="media-body"> <div class="media-body">
<p v-if="convo.type == 'photo'" class="pill-from p-0 shadow"> <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> </p>
<div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer"> <div v-else-if="convo.type == 'link'" class="media d-inline-flex float-right mb-0 cursor-pointer">
<div class="media-body"> <div class="media-body">
@ -134,7 +134,7 @@
</p> </p>
<p v-else>&nbsp;</p> <p v-else>&nbsp;</p>
</div> </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> </div>
</li> </li>

View file

@ -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="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
<div class="media"> <div class="media">
<a :href="user.url"> <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> </a>
<div class="media-body"> <div class="media-body">
<p class="mb-0" style="font-size: 14px"> <p class="mb-0" style="font-size: 14px">

View file

@ -43,10 +43,10 @@
<div class="row"> <div class="row">
<div class="col-4"> <div class="col-4">
<div v-if="hasStory" class="has-story cursor-pointer shadow-sm" @click="storyRedirect()"> <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>
<div v-else> <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> </div>
<div class="col-8"> <div class="col-8">
@ -85,10 +85,10 @@
<!-- DESKTOP PROFILE PICTURE --> <!-- DESKTOP PROFILE PICTURE -->
<div class="d-none d-md-block pb-3"> <div class="d-none d-md-block pb-3">
<div v-if="hasStory" class="has-story-lg cursor-pointer shadow-sm" @click="storyRedirect()"> <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>
<div v-else> <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> </div>
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3"> <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"> <button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
@ -404,24 +404,19 @@
title="Following" title="Following"
body-class="list-group-flush py-3 px-0" body-class="list-group-flush py-3 px-0"
dialog-class="follow-modal"> dialog-class="follow-modal">
<div v-if="!loading" class="list-group" style="min-height: 60vh;"> <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-else>
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3"> <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"> <span class="d-flex px-4 pb-0 align-items-center">
<i class="fas fa-search text-lighter"></i> <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"> <input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
</span> </span>
</div> </div>
<div v-if="owner == true" class="btn-group rounded-0 mt-n3 mb-3 border-top" role="group" aria-label="Following"> <div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
<!-- <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"> <div class="media">
<a :href="profileUrlRedirect(user)"> <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'"> <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'">
@ -432,11 +427,11 @@
{{user.username}} {{user.username}}
</a> </a>
</p> </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"> <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">&commat;{{user.acct.split('@')[1]}}</span> <span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span>
</p> </p>
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px"> <p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
{{user.display_name}} {{user.display_name ? user.display_name : user.username}}
</p> </p>
</div> </div>
<div v-if="owner"> <div v-if="owner">
@ -453,6 +448,12 @@
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p> <p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
</div> </div>
</div> </div>
</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> </b-modal>
<b-modal ref="followerModal" <b-modal ref="followerModal"
id="follower-modal" id="follower-modal"
@ -463,12 +464,14 @@
body-class="list-group-flush py-3 px-0" body-class="list-group-flush py-3 px-0"
dialog-class="follow-modal" dialog-class="follow-modal"
> >
<div class="list-group"> <div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
<div v-if="followers.length == 0" class="list-group-item border-0"> <div v-if="!followers.length" class="list-group-item border-0">
<p class="text-center mb-0 font-weight-bold text-muted py-5"> <p class="text-center mb-0 font-weight-bold text-muted py-5">
<span class="text-dark">{{profileUsername}}</span> has no followers yet</p> <span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
</div> </div>
<div class="list-group-item border-0 py-1" v-for="(user, index) in followers" :key="'follower_'+index">
<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"> <div class="media mb-0">
<a :href="profileUrlRedirect(user)"> <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'"> <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'">
@ -479,8 +482,11 @@
{{user.username}} {{user.username}}
</a> </a>
</p> </p>
<p class="text-secondary mb-0" style="font-size: 13px"> <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">
{{user.display_name}} <span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{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> </p>
</div> </div>
<!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> --> <!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
@ -490,6 +496,12 @@
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p> <p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
</div> </div>
</div> </div>
</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> </b-modal>
<b-modal ref="visitorContextMenu" <b-modal ref="visitorContextMenu"
id="visitor-context-menu" id="visitor-context-menu"
@ -652,7 +664,6 @@
<script type="text/javascript"> <script type="text/javascript">
import VueMasonry from 'vue-masonry-css' import VueMasonry from 'vue-masonry-css'
export default { export default {
props: [ props: [
'profile-id', 'profile-id',
@ -679,9 +690,11 @@
followers: [], followers: [],
followerCursor: 1, followerCursor: 1,
followerMore: true, followerMore: true,
followerLoading: true,
following: [], following: [],
followingCursor: 1, followingCursor: 1,
followingMore: true, followingMore: true,
followingLoading: true,
warning: false, warning: false,
sponsorList: [], sponsorList: [],
bookmarks: [], bookmarks: [],
@ -1121,6 +1134,7 @@
if(res.data.length < 10) { if(res.data.length < 10) {
this.followingMore = false; this.followingMore = false;
} }
this.followingLoading = false;
}); });
this.$refs.followingModal.show(); this.$refs.followingModal.show();
return; return;
@ -1150,6 +1164,7 @@
if(res.data.length < 10) { if(res.data.length < 10) {
this.followerMore = false; this.followerMore = false;
} }
this.followerLoading = false;
}) })
this.$refs.followerModal.show(); this.$refs.followerModal.show();
return; return;

View file

@ -97,7 +97,7 @@
<a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)"> <a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
<div class="pb-3"> <div class="pb-3">
<div class="media align-items-center py-2 pr-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"> <div class="media-body">
<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value"> <p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
{{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> <p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
</div> </div>
<div v-if="results.statuses.length"> <div v-if="results.statuses.length">
<a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)"> <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"> <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> </a>
</div> </div>
<div v-else> <div v-else>

View file

@ -24,7 +24,7 @@
<announcements-card v-on:show-tips="showTips = $event"></announcements-card> <announcements-card v-on:show-tips="showTips = $event"></announcements-card>
</div> </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"> <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> <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> <span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
@ -55,7 +55,7 @@
</div> </div>
</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"> <div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
<span></span> <span></span>
<h6 class="text-muted font-weight-bold mb-0"><a :href="'/discover/tags/'+hashtagPostsName+'?src=tr'">#{{hashtagPostsName}}</a></h6> <h6 class="text-muted font-weight-bold mb-0"><a :href="'/discover/tags/'+hashtagPostsName+'?src=tr'">#{{hashtagPostsName}}</a></h6>
@ -104,6 +104,7 @@
</div> </div>
<status-card <status-card
:class="{ 'border-top': index === 0 }"
:status="status" :status="status"
:reaction-bar="reactionBar" :reaction-bar="reactionBar"
v-on:status-delete="deleteStatus" v-on:status-delete="deleteStatus"
@ -112,7 +113,7 @@
</div> </div>
<div v-if="!loading && feed.length"> <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"> <div class="card-body py-5 my-5">
<infinite-loading @infinite="infiniteTimeline" :distance="800"> <infinite-loading @infinite="infiniteTimeline" :distance="800">
<div slot="no-more"> <div slot="no-more">
@ -157,8 +158,9 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="!loading && scope == 'home' && feed.length == 0"> <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"> <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"><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> <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" v-for="(status, index) in discover_feed"
:key="`discover_feed-${index}-${status.id}`"> :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"> <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> <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> <span class="cursor-pointer text-muted" v-on:click="hideSuggestions"><i class="fas fa-times"></i></span>
@ -273,7 +275,10 @@
</div> </div>
</div> </div>
<status-card :status="status" :recommended="true" /> <status-card
:class="{'border-top': index === 0}"
:status="status"
:recommended="true" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -9,12 +9,12 @@
<div class="card-body p-0 m-0 bg-light border-bottom"> <div class="card-body p-0 m-0 bg-light border-bottom">
<div class="d-flex p-0 m-0 align-items-center"> <div class="d-flex p-0 m-0 align-items-center">
@if($status->parent()->parent()->media()->count()) @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 @endif
<div class="p-4 w-100"> <div class="p-4 w-100">
<div class=""> <div class="">
<div class="media"> <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"> <div class="media-body">
<span class="font-weight-bold" v-pre>{{$status->parent()->parent()->profile->username}}</span> <span class="font-weight-bold" v-pre>{{$status->parent()->parent()->profile->username}}</span>
<div class=""> <div class="">
@ -36,12 +36,12 @@
<div class="card-body p-0 m-0 bg-light border-bottom"> <div class="card-body p-0 m-0 bg-light border-bottom">
<div class="d-flex p-0 m-0 align-items-center"> <div class="d-flex p-0 m-0 align-items-center">
@if($status->parent()->media()->count()) @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 @endif
<div class="p-4 w-100"> <div class="p-4 w-100">
<div class=""> <div class="">
<div class="media"> <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"> <div class="media-body">
<span class="font-weight-bold" v-pre>{{$status->parent()->profile->username}}</span> <span class="font-weight-bold" v-pre>{{$status->parent()->profile->username}}</span>
<div class=""> <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> <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> </summary>
<div class="media py-5"> <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"> <div class="media-body">
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5> <h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
<p class="" v-pre>{!! $status->rendered !!}</p> <p class="" v-pre>{!! $status->rendered !!}</p>
@ -80,7 +80,7 @@
</details> </details>
@else @else
<div class="media py-5"> <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"> <div class="media-body">
<h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5> <h5 class="mt-0 font-weight-bold" v-pre>{{$status->profile->username}}</h5>
<p class="" v-pre>{!! $status->rendered !!}</p> <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="card-body p-0 m-0 bg-light border-bottom">
<div class="d-flex p-0 m-0 align-items-center"> <div class="d-flex p-0 m-0 align-items-center">
@if($status->comments()->first()->media()->count()) @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 @endif
<div class="p-4 w-100"> <div class="p-4 w-100">
<div class=""> <div class="">
<div class="media"> <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"> <div class="media-body">
<span class="font-weight-bold" v-pre>{{$status->comments()->first()->profile->username}}</span> <span class="font-weight-bold" v-pre>{{$status->comments()->first()->profile->username}}</span>
<div class=""> <div class="">