Merge pull request #613 from pixelfed/frontend-ui-refactor

Frontend ui refactor
This commit is contained in:
daniel 2018-12-02 18:44:15 -07:00 committed by GitHub
commit f7c4b45098
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 389 additions and 47 deletions

View file

@ -8,9 +8,15 @@ use App\Http\Controllers\{
AvatarController
};
use Auth, Cache, URL;
use App\{Avatar,Media,Profile};
use App\{
Avatar,
Notification,
Media,
Profile
};
use App\Transformer\Api\{
AccountTransformer,
NotificationTransformer,
MediaTransformer,
StatusTransformer
};
@ -35,6 +41,15 @@ class BaseApiController extends Controller
$this->fractal->setSerializer(new ArraySerializer());
}
public function notification(Request $request, $id)
{
$notification = Notification::findOrFail($id);
$resource = new Fractal\Resource\Item($notification, new NotificationTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function accounts(Request $request, $id)
{
$profile = Profile::findOrFail($id);
@ -173,6 +188,11 @@ class BaseApiController extends Controller
$photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) {
return;
}
$storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $photo->store($storagePath);
$hash = \hash_file('sha256', $photo);
@ -183,8 +203,8 @@ class BaseApiController extends Controller
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $photo->getClientSize();
$media->mime = $photo->getClientMimeType();
$media->size = $photo->getSize();
$media->mime = $photo->getMimeType();
$media->filter_class = null;
$media->filter_name = null;
$media->save();

View file

@ -53,6 +53,7 @@ class InternalApiController extends Controller
$medias = $request->input('media');
$attachments = [];
$status = new Status;
$mimes = [];
foreach($medias as $k => $media) {
$m = Media::findOrFail($media['id']);
@ -69,6 +70,7 @@ class InternalApiController extends Controller
}
$m->save();
$attachments[] = $m;
array_push($mimes, $m->mime);
}
$status->caption = strip_tags($request->caption);
@ -84,6 +86,7 @@ class InternalApiController extends Controller
$status->visibility = $visibility;
$status->scope = $visibility;
$status->type = StatusController::mimeTypeCheck($mimes);
$status->save();
NewStatusPipeline::dispatch($status);
@ -96,6 +99,7 @@ class InternalApiController extends Controller
$this->validate($request, [
'page' => 'nullable|min:1|max:3',
]);
$profile = Auth::user()->profile;
$timeago = Carbon::now()->subMonths(6);
$notifications = Notification::with('actor')
@ -148,8 +152,7 @@ class InternalApiController extends Controller
->get();
$posts = Status::select('id', 'caption', 'profile_id')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereHas('media')
->whereIsNsfw(false)
->whereVisibility('public')
->whereNotIn('profile_id', $following)
@ -233,8 +236,7 @@ class InternalApiController extends Controller
$following = array_merge($following, $filters);
$posts = Status::select('id', 'caption', 'profile_id')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereHas('media')
->whereIsNsfw(false)
->whereVisibility('public')
->whereNotIn('profile_id', $following)

View file

@ -31,7 +31,7 @@ class PublicApiController extends Controller
public function __construct()
{
$this->middleware('throttle:200, 30');
$this->middleware('throttle:3000, 30');
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
}
@ -47,6 +47,30 @@ class PublicApiController extends Controller
}
}
protected function getLikes($status)
{
if(false == Auth::check()) {
return [];
} else {
$profile = Auth::user()->profile;
$likes = $status->likedBy()->orderBy('created_at','desc')->paginate(10);
$collection = new Fractal\Resource\Collection($likes, new AccountTransformer());
return $this->fractal->createData($collection)->toArray();
}
}
protected function getShares($status)
{
if(false == Auth::check()) {
return [];
} else {
$profile = Auth::user()->profile;
$shares = $status->sharedBy()->orderBy('created_at','desc')->paginate(10);
$collection = new Fractal\Resource\Collection($shares, new AccountTransformer());
return $this->fractal->createData($collection)->toArray();
}
}
public function status(Request $request, $username, int $postid)
{
$profile = Profile::whereUsername($username)->first();
@ -56,6 +80,8 @@ class PublicApiController extends Controller
$res = [
'status' => $this->fractal->createData($item)->toArray(),
'user' => $this->getUserData(),
'likes' => $this->getLikes($status),
'shares' => $this->getShares($status),
'reactions' => [
'liked' => $status->liked(),
'shared' => $status->shared(),
@ -104,6 +130,28 @@ class PublicApiController extends Controller
return response()->json($res, 200, [], JSON_PRETTY_PRINT);
}
public function statusLikes(Request $request, $username, $id)
{
$profile = Profile::whereUsername($username)->first();
$status = Status::whereProfileId($profile->id)->find($id);
$this->scopeCheck($profile, $status);
$likes = $this->getLikes($status);
return response()->json([
'data' => $likes
]);
}
public function statusShares(Request $request, $username, $id)
{
$profile = Profile::whereUsername($username)->first();
$status = Status::whereProfileId($profile->id)->find($id);
$this->scopeCheck($profile, $status);
$shares = $this->getShares($status);
return response()->json([
'data' => $shares
]);
}
protected function scopeCheck(Profile $profile, Status $status)
{
if($profile->is_private == true && Auth::check() == false) {

View file

@ -92,7 +92,16 @@ class StatusController extends Controller
$photos = $request->file('photo');
$order = 1;
$mimes = [];
$medias = 0;
foreach ($photos as $k => $v) {
$allowedMimes = explode(',', config('pixelfed.media_types'));
if(in_array($v->getMimeType(), $allowedMimes) == false) {
continue;
}
$storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $v->store($storagePath);
$hash = \hash_file('sha256', $v);
@ -102,16 +111,25 @@ class StatusController extends Controller
$media->user_id = $user->id;
$media->media_path = $path;
$media->original_sha256 = $hash;
$media->size = $v->getClientSize();
$media->mime = $v->getClientMimeType();
$media->size = $v->getSize();
$media->mime = $v->getMimeType();
$media->filter_class = $request->input('filter_class');
$media->filter_name = $request->input('filter_name');
$media->order = $order;
$media->save();
array_push($mimes, $media->mime);
ImageOptimize::dispatch($media);
$order++;
$medias++;
}
if($medias == 0) {
$status->delete();
return;
}
$status->type = (new self)::mimeTypeCheck($mimes);
$status->save();
NewStatusPipeline::dispatch($status);
// TODO: Send to subscribers
@ -254,4 +272,38 @@ class StatusController extends Controller
$allowed = ['public', 'unlisted', 'private'];
return in_array($visibility, $allowed) ? $visibility : 'public';
}
public static function mimeTypeCheck($mimes)
{
$allowed = explode(',', config('pixelfed.media_types'));
$count = count($mimes);
$photos = 0;
$videos = 0;
foreach($mimes as $mime) {
if(in_array($mime, $allowed) == false) {
continue;
}
if(str_contains($mime, 'image/')) {
$photos++;
}
if(str_contains($mime, 'video/')) {
$videos++;
}
}
if($photos == 1 && $videos == 0) {
return 'photo';
}
if($videos == 1 && $photos == 0) {
return 'video';
}
if($photos > 1 && $videos == 0) {
return 'photo:album';
}
if($videos > 1 && $photos == 0) {
return 'video:album';
}
if($photos >= 1 && $videos >= 1) {
return 'photo:video:album';
}
}
}

View file

@ -3,6 +3,7 @@
namespace App;
use Auth, Cache;
use App\Http\Controllers\StatusController;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Storage;
@ -18,7 +19,21 @@ class Status extends Model
*/
protected $dates = ['deleted_at'];
protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id'];
protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id', 'reblog_of_id'];
const STATUS_TYPES = [
'photo',
'photo:album',
'video',
'video:album',
'photo:video:album',
'share',
'reply',
'story',
'story:reply',
'story:reaction',
'story:live'
];
public function profile()
{
@ -35,9 +50,11 @@ class Status extends Model
return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
}
// todo: deprecate after 0.6.0
public function viewType()
{
return Cache::remember('status:view-type:'.$this->id, 40320, function() {
return Cache::remember('status:view-type:'.$this->id, 10080, function() {
$this->setType();
$media = $this->firstMedia();
$mime = explode('/', $media->mime)[0];
$count = $this->media()->count();
@ -49,6 +66,20 @@ class Status extends Model
});
}
// todo: deprecate after 0.6.0
public function setType()
{
if(in_array($this->type, self::STATUS_TYPES)) {
return;
}
$mimes = $this->media->pluck('mime')->toArray();
$type = StatusController::mimeTypeCheck($mimes);
if($type) {
$this->type = $type;
$this->save();
}
}
public function thumb($showNsfw = false)
{
return Cache::remember('status:thumb:'.$this->id, 40320, function() use ($showNsfw) {
@ -108,6 +139,18 @@ class Status extends Model
return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
}
public function likedBy()
{
return $this->hasManyThrough(
Profile::class,
Like::class,
'status_id',
'id',
'id',
'profile_id'
);
}
public function comments()
{
return $this->hasMany(self::class, 'in_reply_to_id');
@ -138,6 +181,18 @@ class Status extends Model
return self::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
}
public function sharedBy()
{
return $this->hasManyThrough(
Profile::class,
Status::class,
'reblog_of_id',
'id',
'id',
'profile_id'
);
}
public function parent()
{
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;

View file

@ -17,29 +17,31 @@ class StatusTransformer extends Fractal\TransformerAbstract
public function transform(Status $status)
{
return [
'id' => $status->id,
'uri' => $status->url(),
'url' => $status->url(),
'in_reply_to_id' => $status->in_reply_to_id,
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
'id' => $status->id,
'uri' => $status->url(),
'url' => $status->url(),
'in_reply_to_id' => $status->in_reply_to_id,
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
'reblog' => $status->reblog_of_id || $status->in_reply_to_id ? $this->transform($status->parent()) : null,
'content' => "$status->rendered",
'created_at' => $status->created_at->format('c'),
'emojis' => [],
'reblogs_count' => $status->shares()->count(),
'favourites_count' => $status->likes()->count(),
'reblogged' => $status->shared(),
'favourited' => $status->liked(),
'muted' => null,
'sensitive' => (bool) $status->is_nsfw,
'spoiler_text' => $status->cw_summary,
'visibility' => $status->visibility,
'application' => [
'name' => 'web',
'website' => null
],
'language' => null,
'pinned' => null,
// TODO: fixme
'reblog' => null,
'content' => "$status->rendered",
'created_at' => $status->created_at->format('c'),
'emojis' => [],
'reblogs_count' => $status->shares()->count(),
'favourites_count' => $status->likes()->count(),
'reblogged' => $status->shared(),
'favourited' => $status->liked(),
'muted' => null,
'sensitive' => (bool) $status->is_nsfw,
'spoiler_text' => '',
'visibility' => $status->visibility,
'application' => null,
'language' => null,
'pinned' => null,
'pf_type' => $status->type,
];
}

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.3.0',
'version' => '0.4.0',
/*
|--------------------------------------------------------------------------

10
package-lock.json generated
View file

@ -11274,6 +11274,11 @@
"integrity": "sha512-2j/t+wIbyVMP5NvctQoSUvLkYKoWAAk2QlQiilrM2a6/ulzFgdcLUJfTvs4XQ/3eZhHiBmmEojbjmM4AzZj8JA==",
"dev": true
},
"vue-infinite-loading": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/vue-infinite-loading/-/vue-infinite-loading-2.4.3.tgz",
"integrity": "sha512-CKITl7I1cb3X4zIHbVSyrupPTs9XxZGVV/N+P5lSxSrGW+D92gq6zuTy/XnvJOwMRkjJuiotJAQrgv+gOwSx3g=="
},
"vue-loader": {
"version": "13.7.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-13.7.3.tgz",
@ -11336,6 +11341,11 @@
}
}
},
"vue-loading-overlay": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-3.1.0.tgz",
"integrity": "sha512-EJOaqxfkSwt6LRoKYnWWPch6fLRRzHWFxLBnRHjXHIK/fP0MSmbBLh9ZRpxarXJeDBiyykQevDXa7h7809JaAA=="
},
"vue-style-loader": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-3.1.2.tgz",

View file

@ -29,6 +29,8 @@
"readmore-js": "^2.2.1",
"socket.io-client": "^2.1.1",
"sweetalert": "^2.1.0",
"twitter-text": "^2.0.5"
"twitter-text": "^2.0.5",
"vue-infinite-loading": "^2.4.3",
"vue-loading-overlay": "^3.1.0"
}
}

BIN
public/css/app.css vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,6 +1,11 @@
window.Vue = require('vue');
import BootstrapVue from 'bootstrap-vue'
import InfiniteLoading from 'vue-infinite-loading';
import Loading from 'vue-loading-overlay';
Vue.use(BootstrapVue);
Vue.use(InfiniteLoading);
Vue.use(Loading);
pixelfed.readmore = () => {
$('.read-more').each(function(k,v) {

View file

@ -1,8 +1,12 @@
<style>
#l-modal .modal-body,
#s-modal .modal-body {
max-height: 70vh;
overflow-y: scroll;
}
</style>
<template>
<div class="postComponent">
<div class="postComponent d-none">
<div class="container px-0 mt-md-4">
<div class="card card-md-rounded-0 status-container orientation-unknown">
<div class="row mx-0">
@ -91,7 +95,7 @@
<div class="status-comment">
<p class="mb-1 read-more" style="overflow: hidden;">
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
<span class="comment-text" :id="status.id + '-status-readmore'"></span>
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
</p>
<post-comments :user="this.user" :post-id="statusId" :post-username="statusUsername"></post-comments>
</div>
@ -124,8 +128,13 @@
</form>
</span>
</div>
<div class="likes font-weight-bold mb-0">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
<div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
</div>
<div class="timestamp">
<a v-bind:href="statusUrl" class="small text-muted">
@ -150,6 +159,69 @@
</div>
</div>
</div>
<b-modal ref="likesModal"
id="l-modal"
hide-footer
centered
title="Likes"
body-class="list-group-flush p-0">
<div class="list-group">
<div class="list-group-item border-0" v-for="user in likes">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '\'s avatar'" width="30px">
</a>
<div class="media-body">
<p class="mb-0" style="font-size: 14px">
<a :href="user.url" class="font-weight-bold text-dark">
{{user.username}}
</a>
</p>
<p class="text-muted mb-0" style="font-size: 14px">
{{user.display_name}}
</a>
</p>
</div>
</div>
</div>
<infinite-loading @infinite="infiniteLikesHandler" spinner="spiral">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</b-modal>
<b-modal ref="sharesModal"
id="s-modal"
hide-footer
centered
title="Shares"
body-class="list-group-flush p-0">
<div class="list-group">
<div class="list-group-item border-0" v-for="user in shares">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '\'s avatar'" width="30px">
</a>
<div class="media-body">
<p class="mb-0" style="font-size: 14px">
<a :href="user.url" class="font-weight-bold text-dark">
{{user.username}}
</a>
</p>
<p class="text-muted mb-0" style="font-size: 14px">
{{user.display_name}}
</a>
</p>
</div>
</div>
</div>
<infinite-loading @infinite="infiniteSharesHandler" spinner="spiral">
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</b-modal>
</div>
</template>
@ -272,9 +344,14 @@ export default {
status: {},
media: {},
user: {},
reactions: {}
reactions: {},
likes: {},
likesPage: 1,
shares: {},
sharesPage: 1,
}
},
mounted() {
let token = $('meta[name="csrf-token"]').attr('content');
$('input[name="_token"]').each(function(k, v) {
@ -282,11 +359,12 @@ export default {
el.val(token);
});
this.fetchData();
//pixelfed.hydrateLikes();
this.authCheck();
},
updated() {
$('.carousel').carousel();
if(this.reactions) {
if(this.reactions.bookmarked == true) {
$('.far.fa-bookmark').removeClass('far').addClass('fas text-warning');
@ -298,6 +376,11 @@ export default {
$('.far.fa-heart ').removeClass('far text-dark').addClass('fas text-danger');
}
}
if(this.status) {
let title = this.status.account.username + ' posted a photo: ' + this.status.favourites_count + ' likes';
$('head title').text(title);
}
},
methods: {
authCheck() {
@ -314,24 +397,36 @@ export default {
$('.post-actions').removeClass('d-none');
}
},
reportUrl() {
return '/i/report?type=post&id=' + this.status.id;
},
timestampFormat() {
let ts = new Date(this.status.created_at);
return ts.toDateString() + ' ' + ts.toLocaleTimeString();
},
fetchData() {
let url = '/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId;
axios.get(url)
let loader = this.$loading.show({
'opacity': 0,
'background-color': '#f5f8fa'
});
axios.get('/api/v2/profile/'+this.statusUsername+'/status/'+this.statusId)
.then(response => {
let self = this;
self.status = response.data.status;
self.user = response.data.user;
self.media = self.status.media_attachments;
self.reactions = response.data.reactions;
self.likes = response.data.likes;
self.shares = response.data.shares;
self.likesPage = 2;
self.sharesPage = 2;
this.buildPresenter();
this.showMuteBlock();
loader.hide();
$('.postComponent').removeClass('d-none');
}).catch(error => {
if(!error.response) {
$('.postPresenterLoader .lds-ring').attr('style','width:100%').addClass('pt-4 font-weight-bold text-muted').text('An error occured, cannot fetch media. Please try again later.');
@ -351,9 +446,58 @@ export default {
}
});
},
commentFocus() {
$('.comment-form input[name="comment"]').focus();
},
likesModal() {
if(this.status.favourites_count == 0 || $('body').hasClass('loggedIn') == false) {
return;
}
this.$refs.likesModal.show();
},
sharesModal() {
if(this.status.reblogs_count == 0 || $('body').hasClass('loggedIn') == false) {
return;
}
this.$refs.sharesModal.show();
},
infiniteLikesHandler($state) {
let api = '/api/v2/likes/profile/'+this.statusUsername+'/status/'+this.statusId;
axios.get(api, {
params: {
page: this.likesPage,
},
}).then(({ data }) => {
if (data.data.length) {
this.likesPage += 1;
this.likes.push(...data.data);
$state.loaded();
} else {
$state.complete();
}
});
},
infiniteSharesHandler($state) {
axios.get('/api/v2/shares/profile/'+this.statusUsername+'/status/'+this.statusId, {
params: {
page: this.sharesPage,
},
}).then(({ data }) => {
if (data.data.length) {
this.sharesPage += 1;
this.shares.push(...data.data);
$state.loaded();
} else {
$state.complete();
}
});
},
buildPresenter() {
let container = $('.postPresenterContainer');
let status = this.status;
@ -364,8 +508,6 @@ export default {
el.val(status.account.id);
});
$('.status-comment .comment-text').html(status.content);
if(container.children().length != 0) {
return;
}

View file

@ -22,3 +22,5 @@
@import '~bootstrap-vue/dist/bootstrap-vue.css';
@import '~plyr/dist/plyr.css';
@import '~vue-loading-overlay/dist/vue-loading.css';

View file

@ -50,6 +50,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/posts', 'InternalApiController@discoverPosts');
Route::get('profile/{username}/status/{postid}', 'PublicApiController@status');
Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments');
Route::get('likes/profile/{username}/status/{id}', 'PublicApiController@statusLikes');
Route::get('shares/profile/{username}/status/{id}', 'PublicApiController@statusShares');
});
Route::group(['prefix' => 'local'], function () {
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');