mirror of https://github.com/pixelfed/pixelfed
commit
ae38f2d8a6
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\Status\StoreStatusEditRequest;
|
||||
use App\Status;
|
||||
use App\Models\StatusEdit;
|
||||
use Purify;
|
||||
use App\Services\Status\UpdateStatusService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Jobs\StatusPipeline\StatusLocalUpdateActivityPubDeliverPipeline;
|
||||
|
||||
class StatusEditController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
abort_if(!config('exp.pue'), 404, 'Post editing is not enabled on this server.');
|
||||
}
|
||||
|
||||
public function store(StoreStatusEditRequest $request, $id)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$status = Status::findOrFail($id);
|
||||
abort_if(StatusEdit::whereStatusId($status->id)->count() >= 10, 400, 'You cannot edit your post more than 10 times.');
|
||||
$res = UpdateStatusService::call($status, $validated);
|
||||
|
||||
$status = Status::findOrFail($id);
|
||||
StatusLocalUpdateActivityPubDeliverPipeline::dispatch($status)->delay(now()->addMinutes(1));
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function history(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$status = Status::whereNull('reblog_of_id')->findOrFail($id);
|
||||
abort_if(!in_array($status->scope, ['public', 'unlisted']), 403);
|
||||
if(!$status->edits()->count()) {
|
||||
return [];
|
||||
}
|
||||
$cached = StatusService::get($status->id, false);
|
||||
|
||||
$res = $status->edits->map(function($edit) use($cached) {
|
||||
return [
|
||||
'content' => Autolink::create()->autolink($edit->caption),
|
||||
'spoiler_text' => $edit->spoiler_text,
|
||||
'sensitive' => (bool) $edit->is_nsfw,
|
||||
'created_at' => str_replace('+00:00', 'Z', $edit->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
'account' => $cached['account'],
|
||||
'media_attachments' => $cached['media_attachments'],
|
||||
'emojis' => $cached['emojis'],
|
||||
];
|
||||
})->reverse()->values()->toArray();
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Status;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use App\Media;
|
||||
use App\Status;
|
||||
use Closure;
|
||||
|
||||
class StoreStatusEditRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$profile = $this->user()->profile;
|
||||
if($profile->status != null) {
|
||||
return false;
|
||||
}
|
||||
if($profile->unlisted == true && $profile->cw == true) {
|
||||
return false;
|
||||
}
|
||||
$types = [
|
||||
"photo",
|
||||
"photo:album",
|
||||
"photo:video:album",
|
||||
"reply",
|
||||
"text",
|
||||
"video",
|
||||
"video:album"
|
||||
];
|
||||
$scopes = ['public', 'unlisted', 'private'];
|
||||
$status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id'));
|
||||
return $status && $this->user()->profile_id === $status->profile_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'sometimes|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'spoiler_text' => 'nullable|string|max:140',
|
||||
'sensitive' => 'sometimes|boolean',
|
||||
'media_ids' => [
|
||||
'nullable',
|
||||
'required_without:status',
|
||||
'array',
|
||||
'max:' . config('pixelfed.max_album_length'),
|
||||
function (string $attribute, mixed $value, Closure $fail) {
|
||||
Media::whereProfileId($this->user()->profile_id)
|
||||
->where(function($query) {
|
||||
return $query->whereNull('status_id')
|
||||
->orWhere('status_id', '=', $this->route('id'));
|
||||
})
|
||||
->findOrFail($value);
|
||||
},
|
||||
],
|
||||
'location' => 'sometimes|nullable',
|
||||
'location.id' => 'sometimes|integer|min:1|max:128769',
|
||||
'location.country' => 'required_with:location.id',
|
||||
'location.name' => 'required_with:location.id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use App\Status;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\UpdateNote;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
|
||||
class StatusLocalUpdateActivityPubDeliverPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$profile = $status->profile;
|
||||
|
||||
// ignore group posts
|
||||
// if($status->group_id != null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if($status->local == false || $status->url || $status->uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
$audience = $status->profile->getAudienceInbox();
|
||||
|
||||
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
||||
switch($status->type) {
|
||||
case 'poll':
|
||||
// Polls not yet supported
|
||||
return;
|
||||
break;
|
||||
|
||||
default:
|
||||
$activitypubObject = new UpdateNote();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($status, $activitypubObject);
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity, [
|
||||
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => $userAgent,
|
||||
]);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Media;
|
||||
use App\ModLog;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\Models\StatusEdit;
|
||||
use App\Services\StatusService;
|
||||
use Purify;
|
||||
|
||||
class StatusRemoteUpdatePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $activity;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$activity = $this->activity;
|
||||
$status = Status::with('media')->whereObjectUrl($activity['id'])->first();
|
||||
if(!$status) {
|
||||
return;
|
||||
}
|
||||
$this->createPreviousEdit($status);
|
||||
$this->updateMedia($status, $activity);
|
||||
$this->updateImmediateAttributes($status, $activity);
|
||||
$this->createEdit($status, $activity);
|
||||
}
|
||||
|
||||
protected function createPreviousEdit($status)
|
||||
{
|
||||
if(!$status->edits()->count()) {
|
||||
StatusEdit::create([
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'caption' => $status->caption,
|
||||
'spoiler_text' => $status->cw_summary,
|
||||
'is_nsfw' => $status->is_nsfw,
|
||||
'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
|
||||
'created_at' => $status->created_at
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateMedia($status, $activity)
|
||||
{
|
||||
if(!isset($activity['attachment'])) {
|
||||
return;
|
||||
}
|
||||
$ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]);
|
||||
$nm = collect($activity['attachment'])->filter(function($nm) {
|
||||
return isset(
|
||||
$nm['type'],
|
||||
$nm['mediaType'],
|
||||
$nm['url']
|
||||
) &&
|
||||
in_array($nm['type'], ['Document', 'Image', 'Video']) &&
|
||||
in_array($nm['mediaType'], explode(',', config('pixelfed.media_types')));
|
||||
});
|
||||
|
||||
// Skip when no media
|
||||
if(!$ogm->count() && !$nm->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Media::whereProfileId($status->profile_id)
|
||||
->whereStatusId($status->id)
|
||||
->update([
|
||||
'status_id' => null
|
||||
]);
|
||||
|
||||
$nm->each(function($n, $key) use($status) {
|
||||
$m = new Media;
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $status->profile_id;
|
||||
$m->remote_media = true;
|
||||
$m->media_path = $n['url'];
|
||||
$m->caption = isset($n['name']) && !empty($n['name']) ? Purify::clean($n['name']) : null;
|
||||
$m->remote_url = $n['url'];
|
||||
$m->width = isset($n['width']) && !empty($n['width']) ? $n['width'] : null;
|
||||
$m->height = isset($n['height']) && !empty($n['height']) ? $n['height'] : null;
|
||||
$m->skip_optimize = true;
|
||||
$m->order = $key + 1;
|
||||
$m->save();
|
||||
});
|
||||
}
|
||||
|
||||
protected function updateImmediateAttributes($status, $activity)
|
||||
{
|
||||
if(isset($activity['content'])) {
|
||||
$status->caption = strip_tags($activity['content']);
|
||||
$status->rendered = Purify::clean($activity['content']);
|
||||
}
|
||||
|
||||
if(isset($activity['sensitive'])) {
|
||||
if((bool) $activity['sensitive'] == false) {
|
||||
$status->is_nsfw = false;
|
||||
$exists = ModLog::whereObjectType('App\Status::class')
|
||||
->whereObjectId($status->id)
|
||||
->whereAction('admin.status.moderate')
|
||||
->exists();
|
||||
if($exists == true) {
|
||||
$status->is_nsfw = true;
|
||||
}
|
||||
$profile = Profile::find($status->profile_id);
|
||||
if(!$profile || $profile->cw == true) {
|
||||
$status->is_nsfw = true;
|
||||
}
|
||||
} else {
|
||||
$status->is_nsfw = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($activity['summary'])) {
|
||||
$status->cw_summary = Purify::clean($activity['summary']);
|
||||
} else {
|
||||
$status->cw_summary = null;
|
||||
}
|
||||
|
||||
$status->edited_at = now();
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
|
||||
protected function createEdit($status, $activity)
|
||||
{
|
||||
$cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null;
|
||||
$spoiler_text = isset($activity['summary']) ? Purify::clean($attributes['summary']) : null;
|
||||
$sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null;
|
||||
$mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
|
||||
StatusEdit::create([
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'caption' => $cleaned,
|
||||
'spoiler_text' => $spoiler_text,
|
||||
'is_nsfw' => $sensitive,
|
||||
'ordered_media_attachment_ids' => $mids
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class StatusEdit extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'ordered_media_attachment_ids' => 'array',
|
||||
'media_descriptions' => 'array',
|
||||
'poll_options' => 'array'
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Status;
|
||||
|
||||
use App\Media;
|
||||
use App\ModLog;
|
||||
use App\Status;
|
||||
use App\Models\StatusEdit;
|
||||
use Purify;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class UpdateStatusService
|
||||
{
|
||||
public static function call(Status $status, $attributes)
|
||||
{
|
||||
self::createPreviousEdit($status);
|
||||
self::updateMediaAttachements($status, $attributes);
|
||||
self::handleImmediateAttributes($status, $attributes);
|
||||
self::createEdit($status, $attributes);
|
||||
|
||||
return StatusService::get($status->id);
|
||||
}
|
||||
|
||||
public static function updateMediaAttachements(Status $status, $attributes)
|
||||
{
|
||||
$count = $status->media()->count();
|
||||
if($count === 0 || $count === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oids = $status->media()->orderBy('order')->pluck('id')->map(function($m) { return (string) $m; });
|
||||
$nids = collect($attributes['media_ids']);
|
||||
|
||||
if($oids->toArray() === $nids->toArray()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($oids->diff($nids)->values()->toArray() as $mid) {
|
||||
$media = Media::find($mid);
|
||||
if(!$media) {
|
||||
continue;
|
||||
}
|
||||
$media->status_id = null;
|
||||
$media->save();
|
||||
MediaStorageService::delete($media, true);
|
||||
}
|
||||
|
||||
$nids->each(function($nid, $idx) {
|
||||
$media = Media::find($nid);
|
||||
if(!$media) {
|
||||
return;
|
||||
}
|
||||
$media->order = $idx;
|
||||
$media->save();
|
||||
});
|
||||
MediaService::del($status->id);
|
||||
}
|
||||
|
||||
public static function handleImmediateAttributes(Status $status, $attributes)
|
||||
{
|
||||
if(isset($attributes['status'])) {
|
||||
$cleaned = Purify::clean($attributes['status']);
|
||||
$status->caption = $cleaned;
|
||||
$status->rendered = Autolink::create()->autolink($cleaned);
|
||||
} else {
|
||||
$status->caption = null;
|
||||
$status->rendered = null;
|
||||
}
|
||||
if(isset($attributes['sensitive'])) {
|
||||
if($status->is_nsfw != (bool) $attributes['sensitive'] &&
|
||||
(bool) $attributes['sensitive'] == false)
|
||||
{
|
||||
$exists = ModLog::whereObjectType('App\Status::class')
|
||||
->whereObjectId($status->id)
|
||||
->whereAction('admin.status.moderate')
|
||||
->exists();
|
||||
if(!$exists) {
|
||||
$status->is_nsfw = (bool) $attributes['sensitive'];
|
||||
}
|
||||
} else {
|
||||
$status->is_nsfw = (bool) $attributes['sensitive'];
|
||||
}
|
||||
}
|
||||
if(isset($attributes['spoiler_text'])) {
|
||||
$status->cw_summary = Purify::clean($attributes['spoiler_text']);
|
||||
} else {
|
||||
$status->cw_summary = null;
|
||||
}
|
||||
if(isset($attributes['location'])) {
|
||||
if (isset($attributes['location']['id'])) {
|
||||
$status->place_id = $attributes['location']['id'];
|
||||
} else {
|
||||
$status->place_id = null;
|
||||
}
|
||||
}
|
||||
if($status->cw_summary && !$status->is_nsfw) {
|
||||
$status->cw_summary = null;
|
||||
}
|
||||
$status->edited_at = now();
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
|
||||
public static function createPreviousEdit(Status $status)
|
||||
{
|
||||
if(!$status->edits()->count()) {
|
||||
StatusEdit::create([
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'caption' => $status->caption,
|
||||
'spoiler_text' => $status->cw_summary,
|
||||
'is_nsfw' => $status->is_nsfw,
|
||||
'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
|
||||
'created_at' => $status->created_at
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function createEdit(Status $status, $attributes)
|
||||
{
|
||||
$cleaned = isset($attributes['status']) ? Purify::clean($attributes['status']) : null;
|
||||
$spoiler_text = isset($attributes['spoiler_text']) ? Purify::clean($attributes['spoiler_text']) : null;
|
||||
$sensitive = isset($attributes['sensitive']) ? $attributes['sensitive'] : null;
|
||||
$mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
|
||||
StatusEdit::create([
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'caption' => $cleaned,
|
||||
'spoiler_text' => $spoiler_text,
|
||||
'is_nsfw' => $sensitive,
|
||||
'ordered_media_attachment_ids' => $mids
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use App\Models\CustomEmoji;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UpdateNote extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Status $status)
|
||||
{
|
||||
$mentions = $status->mentions->map(function ($mention) {
|
||||
$webfinger = $mention->emailUrl();
|
||||
$name = Str::startsWith($webfinger, '@') ?
|
||||
$webfinger :
|
||||
'@' . $webfinger;
|
||||
return [
|
||||
'type' => 'Mention',
|
||||
'href' => $mention->permalink(),
|
||||
'name' => $name
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
if($status->in_reply_to_id != null) {
|
||||
$parent = $status->parent()->profile;
|
||||
if($parent) {
|
||||
$webfinger = $parent->emailUrl();
|
||||
$name = Str::startsWith($webfinger, '@') ?
|
||||
$webfinger :
|
||||
'@' . $webfinger;
|
||||
$reply = [
|
||||
'type' => 'Mention',
|
||||
'href' => $parent->permalink(),
|
||||
'name' => $name
|
||||
];
|
||||
$mentions = array_merge($reply, $mentions);
|
||||
}
|
||||
}
|
||||
|
||||
$hashtags = $status->hashtags->map(function ($hashtag) {
|
||||
return [
|
||||
'type' => 'Hashtag',
|
||||
'href' => $hashtag->url(),
|
||||
'name' => "#{$hashtag->name}",
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$emojis = CustomEmoji::scan($status->caption, true) ?? [];
|
||||
$emoji = array_merge($emojis, $mentions);
|
||||
$tags = array_merge($emoji, $hashtags);
|
||||
|
||||
$latestEdit = $status->edits()->latest()->first();
|
||||
|
||||
return [
|
||||
'@context' => [
|
||||
'https://w3id.org/security/v1',
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
[
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'sensitive' => 'as:sensitive',
|
||||
'schema' => 'http://schema.org/',
|
||||
'pixelfed' => 'http://pixelfed.org/ns#',
|
||||
'commentsEnabled' => [
|
||||
'@id' => 'pixelfed:commentsEnabled',
|
||||
'@type' => 'schema:Boolean'
|
||||
],
|
||||
'capabilities' => [
|
||||
'@id' => 'pixelfed:capabilities',
|
||||
'@container' => '@set'
|
||||
],
|
||||
'announce' => [
|
||||
'@id' => 'pixelfed:canAnnounce',
|
||||
'@type' => '@id'
|
||||
],
|
||||
'like' => [
|
||||
'@id' => 'pixelfed:canLike',
|
||||
'@type' => '@id'
|
||||
],
|
||||
'reply' => [
|
||||
'@id' => 'pixelfed:canReply',
|
||||
'@type' => '@id'
|
||||
],
|
||||
'toot' => 'http://joinmastodon.org/ns#',
|
||||
'Emoji' => 'toot:Emoji'
|
||||
]
|
||||
],
|
||||
'id' => $status->permalink('#updates/' . $latestEdit->id),
|
||||
'type' => 'Update',
|
||||
'actor' => $status->profile->permalink(),
|
||||
'published' => $latestEdit->created_at->toAtomString(),
|
||||
'to' => $status->scopeToAudience('to'),
|
||||
'cc' => $status->scopeToAudience('cc'),
|
||||
'object' => [
|
||||
'id' => $status->url(),
|
||||
'type' => 'Note',
|
||||
'summary' => $status->is_nsfw ? $status->cw_summary : null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
|
||||
'published' => $status->created_at->toAtomString(),
|
||||
'url' => $status->url(),
|
||||
'attributedTo' => $status->profile->permalink(),
|
||||
'to' => $status->scopeToAudience('to'),
|
||||
'cc' => $status->scopeToAudience('cc'),
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
|
||||
return [
|
||||
'type' => $media->activityVerb(),
|
||||
'mediaType' => $media->mime,
|
||||
'url' => $media->url(),
|
||||
'name' => $media->caption,
|
||||
];
|
||||
})->toArray(),
|
||||
'tag' => $tags,
|
||||
'commentsEnabled' => (bool) !$status->comments_disabled,
|
||||
'updated' => $latestEdit->created_at->toAtomString(),
|
||||
'capabilities' => [
|
||||
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'like' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'location' => $status->place_id ? [
|
||||
'type' => 'Place',
|
||||
'name' => $status->place->name,
|
||||
'longitude' => $status->place->long,
|
||||
'latitude' => $status->place->lat,
|
||||
'country' => $status->place->country
|
||||
] : null,
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
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
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
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
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
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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
(()=>{"use strict";var e,r,t,n={},a={};function o(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.loaded=!0,t.exports}o.m=n,e=[],o.O=(r,t,n,a)=>{if(!t){var d=1/0;for(l=0;l<e.length;l++){for(var[t,n,a]=e[l],c=!0,i=0;i<t.length;i++)(!1&a||d>=a)&&Object.keys(o.O).every((e=>o.O[e](t[i])))?t.splice(i--,1):(c=!1,a<d&&(d=a));if(c){e.splice(l--,1);var s=n();void 0!==s&&(r=s)}}return r}a=a||0;for(var l=e.length;l>0&&e[l-1][2]>a;l--)e[l]=e[l-1];e[l]=[t,n,a]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce(((r,t)=>(o.f[t](e,r),r)),[])),o.u=e=>"js/"+{1084:"profile~followers.bundle",1983:"kb.bundle",2470:"home.chunk",2521:"about.bundle",2530:"discover~myhashtags.chunk",2586:"compose.chunk",2732:"dms~message.chunk",3351:"discover~settings.chunk",3365:"dms.chunk",3623:"discover~findfriends.chunk",4028:"error404.bundle",4509:"static~privacy.bundle",4958:"discover.chunk",4965:"discover~memories.chunk",5865:"post.chunk",6053:"notifications.chunk",6869:"profile.chunk",7004:"help.bundle",7019:"discover~hashtag.bundle",8021:"contact.bundle",8250:"i18n.bundle",8517:"daci.chunk",8600:"changelog.bundle",8625:"profile~following.bundle",8900:"discover~serverfeed.chunk",9144:"static~tos.bundle"}[e]+"."+{1084:"fe353e697fb7660b",1983:"f6ebdaac1fd552ca",2470:"af8ef7b54f61b18d",2521:"44a18841089fdde3",2530:"089b7465b2359979",2586:"c413851da244ae3f",2732:"1cfdf19c4525eafa",3351:"5757ad3940569422",3365:"91ab72a8dcd1a8a8",3623:"1aabfedaab1849ba",4028:"5075813f1b00e10d",4509:"24c230550b6938b2",4958:"5ceb85dcb38dfbef",4965:"70b04c7698c2172b",5865:"62a9d21c9016fd95",6053:"9de71a122956c663",6869:"0f947cc09af5c8c3",7004:"7c1195b63e04d568",7019:"b8319d6999d3e2e3",8021:"d6c1d467c11796b1",8250:"9b9bf1b64e2aa1c1",8517:"3f13ec9fc49e9d2b",8600:"7f58a5ccc6659eb2",8625:"c406db7b14d07d36",8900:"ff59ca12d08bb810",9144:"65caad6c0546d8c9"}[e]+".js",o.miniCssF=e=>({138:"css/spa",703:"css/admin",1242:"css/appdark",6170:"css/app",8737:"css/portfolio",9994:"css/landing"}[e]+".css"),o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="pixelfed:",o.l=(e,n,a,d)=>{if(r[e])r[e].push(n);else{var c,i;if(void 0!==a)for(var s=document.getElementsByTagName("script"),l=0;l<s.length;l++){var u=s[l];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==t+a){c=u;break}}c||(i=!0,(c=document.createElement("script")).charset="utf-8",c.timeout=120,o.nc&&c.setAttribute("nonce",o.nc),c.setAttribute("data-webpack",t+a),c.src=e),r[e]=[n];var f=(t,n)=>{c.onerror=c.onload=null,clearTimeout(b);var a=r[e];if(delete r[e],c.parentNode&&c.parentNode.removeChild(c),a&&a.forEach((e=>e(n))),t)return t(n)},b=setTimeout(f.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=f.bind(null,c.onerror),c.onload=f.bind(null,c.onload),i&&document.head.appendChild(c)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.p="/",(()=>{var e={8929:0,1242:0,6170:0,8737:0,703:0,9994:0,138:0};o.f.j=(r,t)=>{var n=o.o(e,r)?e[r]:void 0;if(0!==n)if(n)t.push(n[2]);else if(/^(1242|138|6170|703|8737|8929|9994)$/.test(r))e[r]=0;else{var a=new Promise(((t,a)=>n=e[r]=[t,a]));t.push(n[2]=a);var d=o.p+o.u(r),c=new Error;o.l(d,(t=>{if(o.o(e,r)&&(0!==(n=e[r])&&(e[r]=void 0),n)){var a=t&&("load"===t.type?"missing":t.type),d=t&&t.target&&t.target.src;c.message="Loading chunk "+r+" failed.\n("+a+": "+d+")",c.name="ChunkLoadError",c.type=a,c.request=d,n[1](c)}}),"chunk-"+r,r)}},o.O.j=r=>0===e[r];var r=(r,t)=>{var n,a,[d,c,i]=t,s=0;if(d.some((r=>0!==e[r]))){for(n in c)o.o(c,n)&&(o.m[n]=c[n]);if(i)var l=i(o)}for(r&&r(t);s<d.length;s++)a=d[s],o.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return o.O(l)},t=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),o.nc=void 0})();
|
||||
(()=>{"use strict";var e,r,a,t={},n={};function o(e){var r=n[e];if(void 0!==r)return r.exports;var a=n[e]={id:e,loaded:!1,exports:{}};return t[e].call(a.exports,a,a.exports,o),a.loaded=!0,a.exports}o.m=t,e=[],o.O=(r,a,t,n)=>{if(!a){var c=1/0;for(l=0;l<e.length;l++){for(var[a,t,n]=e[l],d=!0,i=0;i<a.length;i++)(!1&n||c>=n)&&Object.keys(o.O).every((e=>o.O[e](a[i])))?a.splice(i--,1):(d=!1,n<c&&(c=n));if(d){e.splice(l--,1);var s=t();void 0!==s&&(r=s)}}return r}n=n||0;for(var l=e.length;l>0&&e[l-1][2]>n;l--)e[l]=e[l-1];e[l]=[a,t,n]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var a in r)o.o(r,a)&&!o.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce(((r,a)=>(o.f[a](e,r),r)),[])),o.u=e=>"js/"+{1084:"profile~followers.bundle",1983:"kb.bundle",2470:"home.chunk",2521:"about.bundle",2530:"discover~myhashtags.chunk",2586:"compose.chunk",2732:"dms~message.chunk",3351:"discover~settings.chunk",3365:"dms.chunk",3623:"discover~findfriends.chunk",4028:"error404.bundle",4509:"static~privacy.bundle",4958:"discover.chunk",4965:"discover~memories.chunk",5865:"post.chunk",6053:"notifications.chunk",6869:"profile.chunk",7004:"help.bundle",7019:"discover~hashtag.bundle",8021:"contact.bundle",8250:"i18n.bundle",8517:"daci.chunk",8600:"changelog.bundle",8625:"profile~following.bundle",8900:"discover~serverfeed.chunk",9144:"static~tos.bundle"}[e]+"."+{1084:"eac566ea09458e75",1983:"7c3d070a9bcc0489",2470:"f0ab2b4f7e84894c",2521:"dcf91eae809841f8",2530:"9aa66068d1e96512",2586:"eb564854474fa255",2732:"1d2a7a110371a12b",3351:"58f94c148a395667",3365:"a36285d6eee3b46f",3623:"1c4a19cf5fda27ad",4028:"f84c69eed21a7d82",4509:"60f5c03624e7626e",4958:"93193c6ec9a42fc4",4965:"a95b0149f5e4817f",5865:"fc948c7ae6cf23f0",6053:"fa21418a86f44a18",6869:"d1c4aa06ad944495",7004:"0d8a2725bcc8ed81",7019:"10cb33346c033ea7",8021:"97bd609a4737ae8d",8250:"83d55d158de68d01",8517:"e0ca30e5fa8c81f0",8600:"500c0754dd59045b",8625:"193b1268a32bbd07",8900:"48875685bb3cec75",9144:"d5389c3b8c2569d5"}[e]+".js",o.miniCssF=e=>({138:"css/spa",703:"css/admin",1242:"css/appdark",6170:"css/app",8737:"css/portfolio",9994:"css/landing"}[e]+".css"),o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},a="pixelfed:",o.l=(e,t,n,c)=>{if(r[e])r[e].push(t);else{var d,i;if(void 0!==n)for(var s=document.getElementsByTagName("script"),l=0;l<s.length;l++){var u=s[l];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==a+n){d=u;break}}d||(i=!0,(d=document.createElement("script")).charset="utf-8",d.timeout=120,o.nc&&d.setAttribute("nonce",o.nc),d.setAttribute("data-webpack",a+n),d.src=e),r[e]=[t];var f=(a,t)=>{d.onerror=d.onload=null,clearTimeout(b);var n=r[e];if(delete r[e],d.parentNode&&d.parentNode.removeChild(d),n&&n.forEach((e=>e(t))),a)return a(t)},b=setTimeout(f.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=f.bind(null,d.onerror),d.onload=f.bind(null,d.onload),i&&document.head.appendChild(d)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.p="/",(()=>{var e={8929:0,1242:0,6170:0,8737:0,703:0,9994:0,138:0};o.f.j=(r,a)=>{var t=o.o(e,r)?e[r]:void 0;if(0!==t)if(t)a.push(t[2]);else if(/^(1242|138|6170|703|8737|8929|9994)$/.test(r))e[r]=0;else{var n=new Promise(((a,n)=>t=e[r]=[a,n]));a.push(t[2]=n);var c=o.p+o.u(r),d=new Error;o.l(c,(a=>{if(o.o(e,r)&&(0!==(t=e[r])&&(e[r]=void 0),t)){var n=a&&("load"===a.type?"missing":a.type),c=a&&a.target&&a.target.src;d.message="Loading chunk "+r+" failed.\n("+n+": "+c+")",d.name="ChunkLoadError",d.type=n,d.request=c,t[1](d)}}),"chunk-"+r,r)}},o.O.j=r=>0===e[r];var r=(r,a)=>{var t,n,[c,d,i]=a,s=0;if(c.some((r=>0!==e[r]))){for(t in d)o.o(d,t)&&(o.m[t]=d[t]);if(i)var l=i(o)}for(r&&r(a);s<c.length;s++)n=c[s],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(l)},a=self.webpackChunkpixelfed=self.webpackChunkpixelfed||[];a.forEach(r.bind(null,0)),a.push=r.bind(null,a.push.bind(a))})(),o.nc=void 0})();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
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
@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-modal
|
||||
v-model="isOpen"
|
||||
centered
|
||||
size="md"
|
||||
:scrollable="true"
|
||||
hide-footer
|
||||
header-class="py-2"
|
||||
body-class="p-0"
|
||||
title-class="w-100 text-center pl-4 font-weight-bold"
|
||||
title-tag="p">
|
||||
<template #modal-header="{ close }">
|
||||
<template v-if="historyIndex === undefined">
|
||||
<div class="d-flex flex-grow-1 justify-content-between align-items-center">
|
||||
<span style="width:40px;"></span>
|
||||
<h5 class="font-weight-bold mb-0">Post History</h5>
|
||||
<b-button size="sm" variant="link" @click="close()">
|
||||
<i class="far fa-times text-dark fa-lg"></i>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="d-flex flex-grow-1 justify-content-between align-items-center pt-1">
|
||||
<b-button size="sm" variant="link" @click.prevent="historyIndex = undefined">
|
||||
<i class="fas fa-chevron-left text-primary fa-lg"></i>
|
||||
</b-button>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center" style="gap: 5px;">
|
||||
<div class="d-flex align-items-center" style="gap: 5px;">
|
||||
<img
|
||||
:src="allHistory[0].account.avatar"
|
||||
width="16"
|
||||
height="16"
|
||||
class="rounded-circle"
|
||||
onerror="this.src='/storage/avatars/default.jpg';this.onerror=null;">
|
||||
<span class="font-weight-bold">{{ allHistory[0].account.username }}</span>
|
||||
</div>
|
||||
|
||||
<div>{{ historyIndex == (allHistory.length - 1) ? 'created' : 'edited' }} {{ formatTime(allHistory[historyIndex].created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-button size="sm" variant="link" @click="close()">
|
||||
<i class="fas fa-times text-dark fa-lg"></i>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div v-if="isLoading" class="d-flex align-items-center justify-content-center" style="min-height: 500px;">
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="historyIndex === undefined" class="list-group border-top-0">
|
||||
<div
|
||||
v-for="(history, idx) in allHistory"
|
||||
class="list-group-item d-flex align-items-center justify-content-between" style="gap: 5px;">
|
||||
<div class="d-flex align-items-center" style="gap: 5px;">
|
||||
<div class="d-flex align-items-center" style="gap: 5px;">
|
||||
<img
|
||||
:src="history.account.avatar"
|
||||
width="24"
|
||||
height="24"
|
||||
class="rounded-circle"
|
||||
onerror="this.src='/storage/avatars/default.jpg';this.onerror=null;">
|
||||
<span class="font-weight-bold">{{ history.account.username }}</span>
|
||||
</div>
|
||||
<div>{{ idx == (allHistory.length - 1) ? 'created' : 'edited' }} {{ formatTime(history.created_at) }}</div>
|
||||
</div>
|
||||
|
||||
<a class="stretched-link text-decoration-none" href="#" @click.prevent="historyIndex = idx">
|
||||
<div class="d-flex align-items-center" style="gap:5px;">
|
||||
<i class="far fa-chevron-right text-primary fa-lg"></i>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="d-flex align-items-center flex-column border-top-0 justify-content-center">
|
||||
<!-- <img :src="allHistory[historyIndex].media_attachments[0].url" style="max-height: 400px;object-fit: contain;"> -->
|
||||
<template v-if="postType() === 'text'">
|
||||
</template>
|
||||
<template v-else-if="postType() === 'image'">
|
||||
<div style="width: 100%">
|
||||
<blur-hash-image
|
||||
:width="32"
|
||||
:height="32"
|
||||
:punch="1"
|
||||
class="img-contain border-bottom"
|
||||
:hash="allHistory[historyIndex].media_attachments[0].blurhash"
|
||||
:src="allHistory[historyIndex].media_attachments[0].url"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="postType() === 'album'">
|
||||
<div style="width: 100%">
|
||||
<b-carousel
|
||||
controls
|
||||
indicators
|
||||
background="#000000"
|
||||
style="text-shadow: 1px 1px 2px #333;"
|
||||
>
|
||||
<b-carousel-slide
|
||||
v-for="(media, idx) in allHistory[historyIndex].media_attachments"
|
||||
:key="'pfph:'+media.id+':'+idx"
|
||||
:img-src="media.url"
|
||||
></b-carousel-slide>
|
||||
</b-carousel>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="postType() === 'video'">
|
||||
<div style="width: 100%">
|
||||
<div class="embed-responsive embed-responsive-16by9 border-bottom">
|
||||
<video class="video" controls playsinline preload="metadata" loop>
|
||||
<source :src="allHistory[historyIndex].media_attachments[0].url" :type="allHistory[historyIndex].media_attachments[0].mime">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p class="lead my-4" v-html="allHistory[historyIndex].content"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
isLoading: true,
|
||||
allHistory: [],
|
||||
historyIndex: undefined,
|
||||
user: window._sharedData.user
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
this.isLoading = true;
|
||||
this.historyIndex = undefined;
|
||||
this.allHistory = [];
|
||||
setTimeout(() => {
|
||||
this.fetchHistory();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
fetchHistory() {
|
||||
axios.get(`/api/v1/statuses/${this.status.id}/history`)
|
||||
.then(res => {
|
||||
this.allHistory = res.data;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
},
|
||||
|
||||
formatTime(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
let interval = Math.floor(seconds / 63072000);
|
||||
if (interval < 0) {
|
||||
return "0s";
|
||||
}
|
||||
if (interval >= 1) {
|
||||
return interval + (interval == 1 ? ' year' : ' years') + " ago";
|
||||
}
|
||||
interval = Math.floor(seconds / 604800);
|
||||
if (interval >= 1) {
|
||||
return interval + (interval == 1 ? ' week' : ' weeks') + " ago";
|
||||
}
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval >= 1) {
|
||||
return interval + (interval == 1 ? ' day' : ' days') + " ago";
|
||||
}
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval >= 1) {
|
||||
return interval + (interval == 1 ? ' hour' : ' hours') + " ago";
|
||||
}
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval >= 1) {
|
||||
return interval + (interval == 1 ? ' minute' : ' minutes') + " ago";
|
||||
}
|
||||
return Math.floor(seconds) + " seconds ago";
|
||||
},
|
||||
|
||||
postType() {
|
||||
if(this.historyIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let post = this.allHistory[this.historyIndex];
|
||||
|
||||
if(!post) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
let media = post.media_attachments;
|
||||
|
||||
if(!media || !media.length) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
if(media.length == 1) {
|
||||
return media[0].type;
|
||||
}
|
||||
|
||||
return 'album';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.img-contain {
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div class="card-header border-0" style="border-top-left-radius: 15px;border-top-right-radius: 15px;">
|
||||
<div class="media align-items-center">
|
||||
<a :href="status.account.url" @click.prevent="goToProfile()" style="margin-right: 10px;">
|
||||
<img :src="getStatusAvatar()" style="border-radius:15px;" width="44" height="44" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
||||
</a>
|
||||
|
||||
<div class="media-body">
|
||||
<p class="font-weight-bold username">
|
||||
<a :href="status.account.url" class="text-dark" :id="'apop_'+status.id" @click.prevent="goToProfile">
|
||||
{{ status.account.acct }}
|
||||
</a>
|
||||
<b-popover :target="'apop_'+status.id" triggers="hover" placement="bottom" custom-class="shadow border-0 rounded-px">
|
||||
<profile-hover-card
|
||||
:profile="status.account"
|
||||
v-on:follow="follow"
|
||||
v-on:unfollow="unfollow" />
|
||||
</b-popover>
|
||||
</p>
|
||||
<p class="text-lighter mb-0" style="font-size: 13px;">
|
||||
<span v-if="status.account.is_admin" class="d-none d-md-inline-block">
|
||||
<span class="badge badge-light text-danger user-select-none" title="Admin account">ADMIN</span>
|
||||
<span class="mx-1 text-lighter">·</span>
|
||||
</span>
|
||||
<a class="timestamp text-lighter" :href="status.url" @click.prevent="goToPost()" :title="status.created_at">
|
||||
{{ timeago(status.created_at) }}
|
||||
</a>
|
||||
|
||||
<span v-if="config.ab.pue && status.hasOwnProperty('edited_at') && status.edited_at">
|
||||
<span class="mx-1 text-lighter">·</span>
|
||||
<a class="text-lighter" href="#" @click.prevent="openEditModal">Edited</a>
|
||||
</span>
|
||||
|
||||
<span class="mx-1 text-lighter">·</span>
|
||||
<span class="visibility text-lighter" :title="scopeTitle(status.visibility)"><i :class="scopeIcon(status.visibility)"></i></span>
|
||||
|
||||
<span v-if="status.place && status.place.hasOwnProperty('name')" class="d-none d-md-inline-block">
|
||||
<span class="mx-1 text-lighter">·</span>
|
||||
<span class="location text-lighter"><i class="far fa-map-marker-alt"></i> {{ status.place.name }}, {{ status.place.country }}</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button v-if="!useDropdownMenu" class="btn btn-link text-lighter" @click="openMenu">
|
||||
<i class="far fa-ellipsis-v fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<b-dropdown
|
||||
v-else
|
||||
no-caret
|
||||
right
|
||||
variant="link"
|
||||
toggle-class="text-lighter"
|
||||
html="<i class='far fa-ellipsis-v fa-lg px-3'></i>"
|
||||
>
|
||||
<b-dropdown-item>
|
||||
<p class="mb-0 font-weight-bold">{{ $t('menu.viewPost') }}</p>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item>
|
||||
<p class="mb-0 font-weight-bold">{{ $t('common.copyLink') }}</p>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="status.local">
|
||||
<p class="mb-0 font-weight-bold">{{ $t('menu.embed') }}</p>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="!owner"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="!owner">
|
||||
<p class="mb-0 font-weight-bold">{{ $t('menu.report') }}</p>
|
||||
<p class="small text-muted mb-0">Report content that violate our rules</p>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="!owner && status.hasOwnProperty('relationship')">
|
||||
<p class="mb-0 font-weight-bold">{{ status.relationship.muting ? 'Unmute' : 'Mute' }}</p>
|
||||
<p class="small text-muted mb-0">Hide posts from this account in your feeds</p>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="!owner && status.hasOwnProperty('relationship')">
|
||||
<p class="mb-0 font-weight-bold text-danger">{{ status.relationship.blocking ? 'Unblock' : 'Block' }}</p>
|
||||
<p class="small text-muted mb-0">Restrict all content from this account</p>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="owner || admin"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="owner || admin">
|
||||
<p class="mb-0 font-weight-bold text-danger">
|
||||
{{ $t('common.delete') }}
|
||||
</p>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
|
||||
<edit-history-modal ref="editModal" :status="status" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
import ProfileHoverCard from './../profile/ProfileHoverCard.vue';
|
||||
import EditHistoryModal from './EditHistoryModal.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
profile: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
useDropdownMenu: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
"profile-hover-card": ProfileHoverCard,
|
||||
"edit-history-modal": EditHistoryModal
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: window.App.config,
|
||||
menuLoading: true,
|
||||
owner: false,
|
||||
admin: false,
|
||||
license: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
timeago(ts) {
|
||||
let short = App.util.format.timeAgo(ts);
|
||||
if(
|
||||
short.endsWith('s') ||
|
||||
short.endsWith('m') ||
|
||||
short.endsWith('h')
|
||||
) {
|
||||
return short;
|
||||
}
|
||||
const intl = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
return intl.format(new Date(ts));
|
||||
},
|
||||
|
||||
openMenu() {
|
||||
this.$emit('menu');
|
||||
},
|
||||
|
||||
scopeIcon(scope) {
|
||||
switch(scope) {
|
||||
case 'public':
|
||||
return 'far fa-globe';
|
||||
break;
|
||||
|
||||
case 'unlisted':
|
||||
return 'far fa-lock-open';
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
return 'far fa-lock';
|
||||
break;
|
||||
|
||||
default:
|
||||
return 'far fa-globe';
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
scopeTitle(scope) {
|
||||
switch(scope) {
|
||||
case 'public':
|
||||
return 'Visible to everyone';
|
||||
break;
|
||||
|
||||
case 'unlisted':
|
||||
return 'Hidden from public feeds';
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
return 'Only visible to followers';
|
||||
break;
|
||||
|
||||
default:
|
||||
return '';
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
goToPost() {
|
||||
if(location.pathname.split('/').pop() == this.status.id) {
|
||||
location.href = this.status.local ? this.status.url + '?fs=1' : this.status.url;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
name: 'post',
|
||||
path: `/i/web/post/${this.status.id}`,
|
||||
params: {
|
||||
id: this.status.id,
|
||||
cachedStatus: this.status,
|
||||
cachedProfile: this.profile
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goToProfile() {
|
||||
this.$nextTick(() => {
|
||||
this.$router.push({
|
||||
name: 'profile',
|
||||
path: `/i/web/profile/${this.status.account.id}`,
|
||||
params: {
|
||||
id: this.status.account.id,
|
||||
cachedProfile: this.status.account,
|
||||
cachedUser: this.profile
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
toggleContentWarning() {
|
||||
this.key++;
|
||||
this.sensitive = true;
|
||||
this.status.sensitive = !this.status.sensitive;
|
||||
},
|
||||
|
||||
like() {
|
||||
event.currentTarget.blur();
|
||||
if(this.status.favourited) {
|
||||
this.$emit('unlike');
|
||||
} else {
|
||||
this.$emit('like');
|
||||
}
|
||||
},
|
||||
|
||||
toggleMenu(bvEvent) {
|
||||
setTimeout(() => {
|
||||
this.menuLoading = false;
|
||||
}, 500);
|
||||
},
|
||||
|
||||
closeMenu(bvEvent) {
|
||||
setTimeout(() => {
|
||||
bvEvent.target.parentNode.firstElementChild.blur();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
showLikes() {
|
||||
event.currentTarget.blur();
|
||||
this.$emit('likes-modal');
|
||||
},
|
||||
|
||||
showShares() {
|
||||
event.currentTarget.blur();
|
||||
this.$emit('shares-modal');
|
||||
},
|
||||
|
||||
showComments() {
|
||||
event.currentTarget.blur();
|
||||
this.showCommentDrawer = !this.showCommentDrawer;
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
event.currentTarget.blur();
|
||||
App.util.clipboard(this.status.url);
|
||||
},
|
||||
|
||||
shareToOther() {
|
||||
if (navigator.canShare) {
|
||||
navigator.share({
|
||||
url: this.status.url
|
||||
})
|
||||
.then(() => console.log('Share was successful.'))
|
||||
.catch((error) => console.log('Sharing failed', error));
|
||||
} else {
|
||||
swal('Not supported', 'Your current device does not support native sharing.', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
counterChange(type) {
|
||||
this.$emit('counter-change', type);
|
||||
},
|
||||
|
||||
showCommentLikes(post) {
|
||||
this.$emit('comment-likes-modal', post);
|
||||
},
|
||||
|
||||
shareStatus() {
|
||||
this.$emit('share');
|
||||
},
|
||||
|
||||
unshareStatus() {
|
||||
this.$emit('unshare');
|
||||
},
|
||||
|
||||
handleReport(post) {
|
||||
this.$emit('handle-report', post);
|
||||
},
|
||||
|
||||
follow() {
|
||||
this.$emit('follow');
|
||||
},
|
||||
|
||||
unfollow() {
|
||||
this.$emit('unfollow');
|
||||
},
|
||||
|
||||
handleReblog() {
|
||||
this.isReblogging = true;
|
||||
if(this.status.reblogged) {
|
||||
this.$emit('unshare');
|
||||
} else {
|
||||
this.$emit('share');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.isReblogging = false;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
handleBookmark() {
|
||||
event.currentTarget.blur();
|
||||
this.isBookmarking = true;
|
||||
this.$emit('bookmark');
|
||||
|
||||
setTimeout(() => {
|
||||
this.isBookmarking = false;
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
getStatusAvatar() {
|
||||
if(window._sharedData.user.id == this.status.account.id) {
|
||||
return window._sharedData.user.avatar;
|
||||
}
|
||||
|
||||
return this.status.account.avatar;
|
||||
},
|
||||
|
||||
openModTools() {
|
||||
this.$emit('mod-tools');
|
||||
},
|
||||
|
||||
openEditModal() {
|
||||
this.$refs.editModal.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Loading…
Reference in New Issue