Merge pull request #263 from dansup/frontend-ui-refactor

Frontend ui refactor
This commit is contained in:
daniel 2018-06-13 19:48:19 -06:00 committed by GitHub
commit 60bbb22454
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 337 additions and 71 deletions

View file

@ -3,8 +3,16 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Avatar extends Model class Avatar extends Model
{ {
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
} }

View file

@ -34,8 +34,8 @@ class CommentController extends Controller
$reply = new Status(); $reply = new Status();
$reply->profile_id = $profile->id; $reply->profile_id = $profile->id;
$reply->caption = $comment; $reply->caption = e(strip_tags($comment));
$reply->rendered = e($comment); $reply->rendered = $comment;
$reply->in_reply_to_id = $status->id; $reply->in_reply_to_id = $status->id;
$reply->in_reply_to_profile_id = $status->profile_id; $reply->in_reply_to_profile_id = $status->profile_id;
$reply->save(); $reply->save();

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use Auth, Cache; use Auth, Cache;
use App\Jobs\StatusPipeline\{NewStatusPipeline, StatusDelete}; use App\Jobs\StatusPipeline\{NewStatusPipeline, StatusDelete};
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\{Media, Profile, Status, User}; use App\{Media, Profile, Status, User};
use Vinkla\Hashids\Facades\Hashids; use Vinkla\Hashids\Facades\Hashids;
@ -14,7 +15,7 @@ class StatusController extends Controller
{ {
$user = Profile::whereUsername($username)->firstOrFail(); $user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id) $status = Status::whereProfileId($user->id)
->withCount(['likes', 'comments']) ->withCount(['likes', 'comments', 'media'])
->findOrFail($id); ->findOrFail($id);
if(!$status->media_path && $status->in_reply_to_id) { if(!$status->media_path && $status->in_reply_to_id) {
return redirect($status->url()); return redirect($status->url());
@ -32,36 +33,47 @@ class StatusController extends Controller
$user = Auth::user(); $user = Auth::user();
$this->validate($request, [ $this->validate($request, [
'photo' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'), 'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
'caption' => 'string|max:' . config('pixelfed.max_caption_length'), 'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
'cw' => 'nullable|string' 'cw' => 'nullable|string',
'filter_class' => 'nullable|string',
'filter_name' => 'nullable|string',
]); ]);
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false; $cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
$monthHash = hash('sha1', date('Y') . date('m')); $monthHash = hash('sha1', date('Y') . date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at); $userHash = hash('sha1', $user->id . (string) $user->created_at);
$storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $request->photo->store($storagePath);
$profile = $user->profile; $profile = $user->profile;
$status = new Status; $status = new Status;
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = $request->caption; $status->caption = strip_tags($request->caption);
$status->is_nsfw = $cw; $status->is_nsfw = $cw;
$status->save(); $status->save();
$media = new Media; $photos = $request->file('photo');
$media->status_id = $status->id; $order = 1;
$media->profile_id = $profile->id; foreach ($photos as $k => $v) {
$media->user_id = $user->id; $storagePath = "public/m/{$monthHash}/{$userHash}";
$media->media_path = $path; $path = $v->store($storagePath);
$media->size = $request->file('photo')->getClientSize(); $media = new Media;
$media->mime = $request->file('photo')->getClientMimeType(); $media->status_id = $status->id;
$media->save(); $media->profile_id = $profile->id;
NewStatusPipeline::dispatch($status, $media); $media->user_id = $user->id;
$media->media_path = $path;
$media->size = $v->getClientSize();
$media->mime = $v->getClientMimeType();
$media->filter_class = $request->input('filter_class');
$media->filter_name = $request->input('filter_name');
$media->order = $order;
$media->save();
ImageOptimize::dispatch($media);
$order++;
}
NewStatusPipeline::dispatch($status);
// TODO: Parse Caption
// TODO: Send to subscribers // TODO: Send to subscribers
return redirect($status->url()); return redirect($status->url());

View file

@ -16,17 +16,15 @@ class NewStatusPipeline implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
protected $media;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Status $status, $media = false) public function __construct(Status $status)
{ {
$this->status = $status; $this->status = $status;
$this->media = $media;
} }
/** /**
@ -37,13 +35,10 @@ class NewStatusPipeline implements ShouldQueue
public function handle() public function handle()
{ {
$status = $this->status; $status = $this->status;
$media = $this->media;
StatusEntityLexer::dispatch($status); StatusEntityLexer::dispatch($status);
StatusActivityPubDeliver::dispatch($status); //StatusActivityPubDeliver::dispatch($status);
if($media) {
ImageOptimize::dispatch($media);
}
Cache::forever('post.' . $status->id, $status); Cache::forever('post.' . $status->id, $status);
$redis = Redis::connection(); $redis = Redis::connection();

View file

@ -2,7 +2,7 @@
namespace App\Jobs\StatusPipeline; namespace App\Jobs\StatusPipeline;
use App\{Media, StatusHashtag, Status}; use App\{Media, Notification, StatusHashtag, Status};
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@ -60,6 +60,9 @@ class StatusDelete implements ShouldQueue
} }
$status->likes()->delete(); $status->likes()->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->delete();
StatusHashtag::whereStatusId($status->id)->delete(); StatusHashtag::whereStatusId($status->id)->delete();
$status->delete(); $status->delete();

View file

@ -6,21 +6,28 @@ use Cache;
use App\{ use App\{
Hashtag, Hashtag,
Media, Media,
Mention,
Profile,
Status, Status,
StatusHashtag StatusHashtag
}; };
use App\Util\Lexer\Hashtag as HashtagLexer; use App\Util\Lexer\Hashtag as HashtagLexer;
use App\Util\Lexer\{Autolink, Extractor};
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use App\Jobs\MentionPipeline\MentionPipeline;
class StatusEntityLexer implements ShouldQueue class StatusEntityLexer implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
protected $entities;
protected $autolink;
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -39,22 +46,40 @@ class StatusEntityLexer implements ShouldQueue
public function handle() public function handle()
{ {
$status = $this->status; $status = $this->status;
$this->parseHashtags(); $this->parseEntities();
} }
public function parseHashtags() public function parseEntities()
{
$this->extractEntities();
}
public function extractEntities()
{
$this->entities = Extractor::create()->extract($this->status->caption);
$this->autolinkStatus();
}
public function autolinkStatus()
{
$this->autolink = Autolink::create()->autolink($this->status->caption);
$this->storeEntities();
}
public function storeEntities()
{ {
$status = $this->status; $status = $this->status;
$text = e($status->caption); $this->storeHashtags();
$tags = HashtagLexer::getHashtags($text); $this->storeMentions();
$rendered = $text; $status->rendered = $this->autolink;
if(count($tags) > 0) { $status->entities = json_encode($this->entities);
$rendered = HashtagLexer::replaceHashtagsWithLinks($text);
}
$status->rendered = $rendered;
$status->save(); $status->save();
}
Cache::forever('post.' . $status->id, $status);
public function storeHashtags()
{
$tags = array_unique($this->entities['hashtags']);
$status = $this->status;
foreach($tags as $tag) { foreach($tags as $tag) {
$slug = str_slug($tag); $slug = str_slug($tag);
@ -64,11 +89,32 @@ class StatusEntityLexer implements ShouldQueue
['slug' => $slug] ['slug' => $slug]
); );
$stag = new StatusHashtag; StatusHashtag::firstOrCreate(
$stag->status_id = $status->id; ['status_id' => $status->id],
$stag->hashtag_id = $htag->id; ['hashtag_id' => $htag->id]
$stag->save(); );
} }
} }
public function storeMentions()
{
$mentions = array_unique($this->entities['mentions']);
$status = $this->status;
foreach($mentions as $mention) {
$mentioned = Profile::whereUsername($mention)->first();
if(empty($mentioned) || !isset($mentioned->id)) {
continue;
}
$m = new Mention;
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
MentionPipeline::dispatch($status, $m);
}
}
} }

View file

@ -3,9 +3,19 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Like extends Model class Like extends Model
{ {
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function actor() public function actor()
{ {
return $this->belongsTo(Profile::class, 'profile_id', 'id'); return $this->belongsTo(Profile::class, 'profile_id', 'id');

View file

@ -2,11 +2,21 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Model;
use Storage; use Storage;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Media extends Model class Media extends Model
{ {
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function url() public function url()
{ {
$path = $this->media_path; $path = $this->media_path;

View file

@ -3,9 +3,18 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Mention extends Model class Mention extends Model
{ {
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function profile() public function profile()
{ {

View file

@ -3,28 +3,37 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Notification extends Model class Notification extends Model
{ {
use SoftDeletes;
public function actor() /**
{ * The attributes that should be mutated to dates.
return $this->belongsTo(Profile::class, 'actor_id', 'id'); *
} * @var array
*/
protected $dates = ['deleted_at'];
public function actor()
{
return $this->belongsTo(Profile::class, 'actor_id', 'id');
}
public function profile() public function profile()
{ {
return $this->belongsTo(Profile::class, 'profile_id', 'id'); return $this->belongsTo(Profile::class, 'profile_id', 'id');
} }
public function item() public function item()
{ {
return $this->morphTo(); return $this->morphTo();
} }
public function status() public function status()
{ {
return $this->belongsTo(Status::class, 'item_id', 'id'); return $this->belongsTo(Status::class, 'item_id', 'id');
} }
} }

View file

@ -5,9 +5,18 @@ namespace App;
use Storage; use Storage;
use App\Util\Lexer\PrettyNumber; use App\Util\Lexer\PrettyNumber;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Profile extends Model class Profile extends Model
{ {
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
protected $hidden = [ protected $hidden = [
'private_key', 'private_key',
]; ];

View file

@ -2,12 +2,21 @@
namespace App; namespace App;
use Illuminate\Database\Eloquent\Model;
use Storage; use Storage;
use Vinkla\Hashids\Facades\Hashids; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Status extends Model class Status extends Model
{ {
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
public function profile() public function profile()
{ {
return $this->belongsTo(Profile::class); return $this->belongsTo(Profile::class);
@ -25,7 +34,7 @@ class Status extends Model
public function thumb() public function thumb()
{ {
if($this->media->count() == 0) { if($this->media->count() == 0 || $this->is_nsfw) {
return "data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="; return "data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
} }
return url(Storage::url($this->firstMedia()->thumbnail_path)); return url(Storage::url($this->firstMedia()->thumbnail_path));
@ -43,6 +52,11 @@ class Status extends Model
return url($path); return url($path);
} }
public function editUrl()
{
return $this->url() . '/edit';
}
public function mediaUrl() public function mediaUrl()
{ {
$media = $this->firstMedia(); $media = $this->firstMedia();

View file

@ -3,11 +3,19 @@
namespace App; namespace App;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable class User extends Authenticatable
{ {
use Notifiable; use Notifiable, SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddFiltersToMediaTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('media', function (Blueprint $table) {
$table->string('filter_name')->nullable()->after('orientation');
$table->string('filter_class')->nullable()->after('filter_name');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('media', function (Blueprint $table) {
$table->dropColumn('filter_name');
$table->dropColumn('filter_class');
});
}
}

View file

@ -0,0 +1,58 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSoftDeletesToModels extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('avatars', function ($table) {
$table->softDeletes();
});
Schema::table('likes', function ($table) {
$table->softDeletes();
});
Schema::table('media', function ($table) {
$table->softDeletes();
});
Schema::table('mentions', function ($table) {
$table->softDeletes();
});
Schema::table('notifications', function ($table) {
$table->softDeletes();
});
Schema::table('profiles', function ($table) {
$table->softDeletes();
});
Schema::table('statuses', function ($table) {
$table->softDeletes();
});
Schema::table('users', function ($table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

BIN
public/css/app.css vendored

Binary file not shown.

Binary file not shown.

View file

@ -16,7 +16,47 @@
</div> </div>
</div> </div>
<div class="col-12 col-md-8 status-photo px-0"> <div class="col-12 col-md-8 status-photo px-0">
<img src="{{$status->mediaUrl()}}" width="100%"> @if($status->is_nsfw && $status->media_count == 1)
<details class="details-animated">
<p>
<summary>NSFW / Hidden Image</summary>
<a class="max-hide-overflow {{$status->firstMedia()->filter_class}}" href="{{$status->url()}}">
<img class="card-img-top" src="{{$status->mediaUrl()}}">
</a>
</p>
</details>
@elseif(!$status->is_nsfw && $status->media_count == 1)
<div class="{{$status->firstMedia()->filter_class}}">
<img src="{{$status->mediaUrl()}}" width="100%">
</div>
@elseif($status->is_nsfw && $status->media_count > 1)
@elseif(!$status->is_nsfw && $status->media_count > 1)
<div id="photoCarousel" class="carousel slide carousel-fade" data-ride="carousel">
<ol class="carousel-indicators">
@for($i = 0; $i < $status->media_count; $i++)
<li data-target="#photoCarousel" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
@endfor
</ol>
<div class="carousel-inner">
@foreach($status->media()->orderBy('order')->get() as $media)
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
<figure class="{{$media->filter_class}}">
<img class="d-block w-100" src="{{$media->url()}}" alt="{{$status->caption}}">
</figure>
</div>
@endforeach
</div>
<a class="carousel-control-prev" href="#photoCarousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#photoCarousel" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
@endif
</div> </div>
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0"> <div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white"> <div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
@ -40,7 +80,7 @@
@foreach($status->comments->reverse()->take(10) as $item) @foreach($status->comments->reverse()->take(10) as $item)
<p class="mb-0"> <p class="mb-0">
<span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$item->profile->url()}}">{{$item->profile->username}}</a></bdi></span> <span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$item->profile->url()}}">{{$item->profile->username}}</a></bdi></span>
<span class="comment-text">{!!$item->rendered!!} <a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right">{{$item->created_at->diffForHumans(null, true, true ,true)}}</a></span> <span class="comment-text">{!! $item->rendered ?? e($item->caption) !!} <a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right">{{$item->created_at->diffForHumans(null, true, true ,true)}}</a></span>
</p> </p>
@endforeach @endforeach
</div> </div>

View file

@ -15,6 +15,7 @@
<a class="dropdown-item" href="#">Embed</a> <a class="dropdown-item" href="#">Embed</a>
@if(Auth::check()) @if(Auth::check())
@if(Auth::user()->profile->id === $item->profile->id || Auth::user()->is_admin == true) @if(Auth::user()->profile->id === $item->profile->id || Auth::user()->is_admin == true)
<a class="dropdown-item" href="{{$item->editUrl()}}">Edit</a>
<form method="post" action="/i/delete"> <form method="post" action="/i/delete">
@csrf @csrf
<input type="hidden" name="type" value="post"> <input type="hidden" name="type" value="post">
@ -29,16 +30,16 @@
</div> </div>
</div> </div>
@if($item->is_nsfw) @if($item->is_nsfw)
<details> <details class="details-animated">
<p> <p>
<summary>NSFW / Hidden Image</summary> <summary>NSFW / Hidden Image</summary>
<a class="max-hide-overflow" href="{{$item->url()}}"> <a class="max-hide-overflow {{$item->firstMedia()->filter_class}}" href="{{$item->url()}}">
<img class="card-img-top" src="{{$item->mediaUrl()}}"> <img class="card-img-top" src="{{$item->mediaUrl()}}">
</a> </a>
</p> </p>
</details> </details>
@else @else
<a class="max-hide-overflow" href="{{$item->url()}}"> <a class="max-hide-overflow {{$item->firstMedia()->filter_class}}" href="{{$item->url()}}">
<img class="card-img-top" src="{{$item->mediaUrl()}}"> <img class="card-img-top" src="{{$item->mediaUrl()}}">
</a> </a>
@endif @endif
@ -84,7 +85,7 @@
<a class="text-dark" href="{{$status->profile->url()}}">{{$status->profile->username}}</a> <a class="text-dark" href="{{$status->profile->url()}}">{{$status->profile->username}}</a>
</bdi> </bdi>
</span> </span>
<span class="comment-text">{!!$status->rendered!!}</span> <span class="comment-text">{!! $item->rendered ?? e($item->caption) !!}</span>
<span class="float-right"> <span class="float-right">
<a href="{{$status->url()}}" class="text-dark small font-weight-bold"> <a href="{{$status->url()}}" class="text-dark small font-weight-bold">
{{$status->created_at->diffForHumans(null, true, true, true)}} {{$status->created_at->diffForHumans(null, true, true, true)}}
@ -95,7 +96,7 @@
@foreach($item->comments->reverse()->take(3) as $comment) @foreach($item->comments->reverse()->take(3) as $comment)
<p class="mb-0"> <p class="mb-0">
<span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$comment->profile->url()}}">{{$comment->profile->username}}</a></bdi></span> <span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$comment->profile->url()}}">{{$comment->profile->username}}</a></bdi></span>
<span class="comment-text">{{ str_limit($comment->caption, 125) }}</span> <span class="comment-text">{!! str_limit($item->rendered ?? e($item->caption), 150) !!}</span>
</p> </p>
@endforeach @endforeach
@endif @endif