Merge branch 'frontend-ui-refactor' into patch-7

This commit is contained in:
Stasiek Michalski 2018-06-04 15:10:40 +02:00 committed by GitHub
commit 99490a9d95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 4349 additions and 94 deletions

View file

@ -16,7 +16,9 @@ class AccountController extends Controller
public function notifications(Request $request)
{
$profile = Auth::user()->profile;
$notifications = $this->fetchNotifications($profile->id);
//$notifications = $this->fetchNotifications($profile->id);
$notifications = Notification::whereProfileId($profile->id)
->orderBy('id','desc')->take(30)->simplePaginate();
return view('account.activity', compact('profile', 'notifications'));
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use Auth;
use App\Like;
use Illuminate\Http\Request;
class ApiController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function hydrateLikes(Request $request)
{
$this->validate($request, [
'min' => 'nullable|integer|min:1',
'max' => 'nullable|integer',
]);
$profile = Auth::user()->profile;
$likes = Like::whereProfileId($profile->id)
->orderBy('id', 'desc')
->take(1000)
->pluck('status_id');
return response()->json($likes);
}
}

View file

@ -3,12 +3,21 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Jobs\CommentPipeline\CommentPipeline;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use Auth, Hashids;
use App\{Comment, Profile, Status};
class CommentController extends Controller
{
public function show(Request $request, $username, int $id, int $cid)
{
$user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id)->whereInReplyToId($id)->findOrFail($cid);
return view('status.reply', compact('user', 'status'));
}
public function store(Request $request)
{
if(Auth::check() === false) { abort(403); }
@ -32,9 +41,10 @@ class CommentController extends Controller
$reply->save();
NewStatusPipeline::dispatch($reply, false);
CommentPipeline::dispatch($status, $reply);
if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Comment saved'];
$response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url()];
} else {
$response = redirect($status->url());
}

View file

@ -21,21 +21,30 @@ class LikeController extends Controller
]);
$profile = Auth::user()->profile;
$status = Status::findOrFail($request->input('item'));
$status = Status::withCount('likes')->findOrFail($request->input('item'));
$count = $status->likes_count;
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
$like->delete();
return redirect()->back();
}
$count--;
} else {
$like = new Like;
$like->profile_id = $profile->id;
$like->status_id = $status->id;
$like->save();
$count++;
}
LikePipeline::dispatch($like);
return redirect($status->url());
if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
return $response;
}
}

View file

@ -13,9 +13,11 @@ class StatusController extends Controller
public function show(Request $request, $username, int $id)
{
$user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id)->findOrFail($id);
$status = Status::whereProfileId($user->id)
->withCount('likes')
->findOrFail($id);
if(!$status->media_path && $status->in_reply_to_id) {
return view('status.reply', compact('user', 'status'));
return redirect($status->url());
}
return view('status.show', compact('user', 'status'));
}

View file

@ -17,14 +17,24 @@ class TimelineController extends Controller
{
// TODO: Use redis for timelines
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
$timeline = Status::whereHas('media')->whereNull('in_reply_to_id')->whereIn('profile_id', $following)->orderBy('id','desc')->simplePaginate(10);
$following->push(Auth::user()->profile->id);
$timeline = Status::whereHas('media')
->whereNull('in_reply_to_id')
->whereIn('profile_id', $following)
->orderBy('id','desc')
->withCount(['comments', 'likes'])
->simplePaginate(10);
return view('timeline.personal', compact('timeline'));
}
public function local()
{
// TODO: Use redis for timelines
$timeline = Status::whereHas('media')->whereNull('in_reply_to_id')->orderBy('id','desc')->simplePaginate(10);
$timeline = Status::whereHas('media')
->whereNull('in_reply_to_id')
->orderBy('id','desc')
->withCount(['comments', 'likes'])
->simplePaginate(10);
return view('timeline.public', compact('timeline'));
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Jobs\CommentPipeline;
use Cache, Log, Redis;
use App\{Like, Notification, Status};
use App\Util\Lexer\Hashtag as HashtagLexer;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class CommentPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
protected $comment;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status, Status $comment)
{
$this->status = $status;
$this->comment = $comment;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$comment = $this->comment;
$target = $status->profile;
$actor = $comment->profile;
try {
$notification = new Notification;
$notification->profile_id = $target->id;
$notification->actor_id = $actor->id;
$notification->action = 'comment';
$notification->message = $comment->replyToText();
$notification->rendered = $comment->replyToHtml();
$notification->item_id = $comment->id;
$notification->item_type = "App\Status";
$notification->save();
Cache::forever('notification.' . $notification->id, $notification);
$redis = Redis::connection();
$nkey = config('cache.prefix').':user.' . $target->id . '.notifications';
$redis->lpush($nkey, $notification->id);
} catch (Exception $e) {
Log::error($e);
}
}
}

View file

@ -46,6 +46,8 @@ class FollowPipeline implements ShouldQueue
$notification->action = 'follow';
$notification->message = $follower->toText();
$notification->rendered = $follower->toHtml();
$notification->item_id = $target->id;
$notification->item_type = "App\Profile";
$notification->save();
Cache::forever('notification.' . $notification->id, $notification);

View file

@ -49,6 +49,8 @@ class LikePipeline implements ShouldQueue
$notification->action = 'like';
$notification->message = $like->toText();
$notification->rendered = $like->toHtml();
$notification->item_id = $status->id;
$notification->item_type = "App\Status";
$notification->save();
Cache::forever('notification.' . $notification->id, $notification);

View file

@ -17,4 +17,14 @@ class Notification extends Model
return $this->belongsTo(Profile::class, 'profile_id', 'id');
}
public function item()
{
return $this->morphTo();
}
public function status()
{
return $this->belongsTo(Status::class, 'item_id', 'id');
}
}

View file

@ -32,7 +32,12 @@ class Status extends Model
{
$id = $this->id;
$username = $this->profile->username;
return url(config('app.url') . "/p/{$username}/{$id}");
$path = config('app.url') . "/p/{$username}/{$id}";
if(!is_null($this->in_reply_to_id)) {
$pid = $this->in_reply_to_id;
$path = config('app.url') . "/p/{$username}/{$pid}/c/{$id}";
}
return url($path);
}
public function mediaUrl()
@ -96,4 +101,17 @@ class Status extends Model
return $obj;
}
public function replyToText()
{
$actorName = $this->profile->username;
return "{$actorName} " . __('notification.commented');
}
public function replyToHtml()
{
$actorName = $this->profile->username;
$actorUrl = $this->profile->url();
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
__('notification.commented');
}
}

View file

@ -8,6 +8,7 @@
"php": "^7.1.3",
"99designs/http-signatures-guzzlehttp": "^2.0",
"bitverse/identicon": "^1.1",
"doctrine/dbal": "^2.7",
"fideloper/proxy": "^4.0",
"greggilbert/recaptcha": "dev-master",
"intervention/image": "^2.4",

359
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7be7e27683f56b7ec28eeef962cf2437",
"content-hash": "bb2590c6fbf5fbc46642f6cbccf7969f",
"packages": [
{
"name": "99designs/http-signatures",
@ -244,6 +244,363 @@
"description": "implementation of xdg base directory specification for php",
"time": "2014-10-24T07:27:01+00:00"
},
{
"name": "doctrine/annotations",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
"reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5",
"reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5",
"shasum": ""
},
"require": {
"doctrine/lexer": "1.*",
"php": "^7.1"
},
"require-dev": {
"doctrine/cache": "1.*",
"phpunit/phpunit": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Docblock Annotations Parser",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"annotations",
"docblock",
"parser"
],
"time": "2017-12-06T07:11:42+00:00"
},
{
"name": "doctrine/cache",
"version": "v1.7.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/b3217d58609e9c8e661cd41357a54d926c4a2a1a",
"reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a",
"shasum": ""
},
"require": {
"php": "~7.1"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"alcaeus/mongo-php-adapter": "^1.1",
"mongodb/mongodb": "^1.1",
"phpunit/phpunit": "^5.7",
"predis/predis": "~1.0"
},
"suggest": {
"alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Caching library offering an object-oriented API for many cache backends",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"cache",
"caching"
],
"time": "2017-08-25T07:02:50+00:00"
},
{
"name": "doctrine/collections",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
"reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
"reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
"shasum": ""
},
"require": {
"php": "^7.1"
},
"require-dev": {
"doctrine/coding-standard": "~0.1@dev",
"phpunit/phpunit": "^5.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"psr-0": {
"Doctrine\\Common\\Collections\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Collections Abstraction library",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"array",
"collections",
"iterator"
],
"time": "2017-07-22T10:37:32+00:00"
},
{
"name": "doctrine/common",
"version": "v2.8.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/common.git",
"reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/common/zipball/f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
"reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
"shasum": ""
},
"require": {
"doctrine/annotations": "1.*",
"doctrine/cache": "1.*",
"doctrine/collections": "1.*",
"doctrine/inflector": "1.*",
"doctrine/lexer": "1.*",
"php": "~7.1"
},
"require-dev": {
"phpunit/phpunit": "^5.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "lib/Doctrine/Common"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Common Library for Doctrine projects",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"annotations",
"collections",
"eventmanager",
"persistence",
"spl"
],
"time": "2017-08-31T08:43:38+00:00"
},
{
"name": "doctrine/dbal",
"version": "v2.7.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "11037b4352c008373561dc6fc836834eed80c3b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/11037b4352c008373561dc6fc836834eed80c3b5",
"reference": "11037b4352c008373561dc6fc836834eed80c3b5",
"shasum": ""
},
"require": {
"doctrine/common": "^2.7.1",
"ext-pdo": "*",
"php": "^7.1"
},
"require-dev": {
"doctrine/coding-standard": "^4.0",
"phpunit/phpunit": "^7.0",
"phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5",
"symfony/console": "^2.0.5||^3.0",
"symfony/phpunit-bridge": "^3.4.5|^4.0.5"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"bin": [
"bin/doctrine-dbal"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7.x-dev"
}
},
"autoload": {
"psr-0": {
"Doctrine\\DBAL\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
}
],
"description": "Database Abstraction Layer",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"database",
"dbal",
"persistence",
"queryobject"
],
"time": "2018-04-07T18:44:18+00:00"
},
{
"name": "doctrine/inflector",
"version": "v1.3.0",

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateNotificationsTableAddPolymorphicRelationship extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('notifications', function (Blueprint $table) {
$table->bigInteger('item_id')->unsigned()->nullable()->after('actor_id');
$table->string('item_type')->nullable()->after('item_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -9,6 +9,7 @@ window.Popper = require('popper.js').default;
*/
try {
window.pixelfed = {};
window.$ = window.jQuery = require('jquery');
require('bootstrap');
window.InfiniteScroll = require('infinite-scroll');
@ -16,9 +17,10 @@ try {
window.typeahead = require('./lib/typeahead');
window.Bloodhound = require('./lib/bloodhound');
require('./lib/fontawesome-all');
require('./components/localstorage');
//require('./components/likebutton');
//require('./components/commentform');
require('./components/likebutton');
require('./components/commentform');
require('./components/searchform');
require('./components/bookmarkform');
} catch (e) {}

View file

@ -1,6 +1,11 @@
$(document).ready(function() {
$('.comment-form').submit(function(e, data) {
$('.status-comment-focus').on('click', function(el) {
var el = $(this).parents().eq(2).find('input[name="comment"]');
el.focus();
});
$(document).on('submit', '.comment-form', function(e, data) {
e.preventDefault();
let el = $(this);
@ -8,18 +13,32 @@ $(document).ready(function() {
let commentform = el.find('input[name="comment"]');
let commenttext = commentform.val();
let item = {item: id, comment: commenttext};
try {
axios.post('/i/comment', item);
var comments = el.parent().parent().find('.comments');
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1">' + pixelfed.user.username + '</span><span class="comment-text">'+ commenttext + '</span></p>';
axios.post('/i/comment', item)
.then(function (res) {
var username = res.data.username;
var permalink = res.data.url;
var profile = res.data.profile;
if($('.status-container').length == 1) {
var comments = el.parents().eq(3).find('.comments');
} else {
var comments = el.parents().eq(1).find('.comments');
}
var comment = '<p class="mb-0"><span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="' + profile + '">' + username + '</a></bdi></span><span class="comment-text">'+ commenttext + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
comments.prepend(comment);
commentform.val('');
commentform.blur();
return true;
} catch(e) {
return false;
}
})
.catch(function (res) {
});
});
});

View file

@ -1,29 +1,60 @@
$(document).ready(function() {
if(!ls.get('likes')) {
axios.get('/api/v1/likes')
.then(function (res) {
ls.set('likes', res.data);
console.log(res);
})
.catch(function (res) {
ls.set('likes', []);
})
}
$('.like-form').submit(function(e) {
pixelfed.hydrateLikes = function() {
var likes = ls.get('likes');
$('.like-form').each(function(i, el) {
var el = $(el);
var id = el.data('id');
var heart = el.find('.status-heart');
if(likes.indexOf(id) != -1) {
heart.addClass('fas fa-heart').removeClass('far fa-heart');
}
});
};
pixelfed.hydrateLikes();
$(document).on('submit', '.like-form', function(e) {
e.preventDefault();
var el = $(this);
var id = el.data('id');
var res = axios.post('/i/like', {item: id});
axios.post('/i/like', {item: id})
.then(function (res) {
var likes = ls.get('likes');
var action = false;
var counter = el.parent().parent().find('.like-count');
var count = parseInt(counter.text());
var counter = el.parents().eq(1).find('.like-count');
var count = res.data.count;
var heart = el.find('.status-heart');
if(likes.indexOf(id) > -1) {
likes.splice(id, 1);
count--;
heart.addClass('far fa-heart').removeClass('fas fa-heart');
likes = likes.filter(function(item) {
return item !== id
});
counter.text(count);
action = 'unlike';
} else {
heart.addClass('fas fa-heart').removeClass('far fa-heart');
likes.push(id);
count++;
counter.text(count);
action = 'like';
}
ls.set('likes', likes);
console.log(action + ' - ' + $(this).data('id') + ' like event');
console.log(action + ' - ' + id + ' like event');
});
});
});

3271
resources/assets/js/lib/fontawesome-all.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,12 @@ $(document).ready(function() {
append: '.timeline-feed',
history: false,
});
$("#modal-post").addClass("modal fade");
$("#modal-dialog").addClass("modal-dialog");
$("#modal-text").removeClass("d-none");
infScroll.on( 'append', function( response, path, items ) {
pixelfed.hydrateLikes();
});
});

View file

@ -1,6 +1,7 @@
// Fonts
@import "fonts";
@import "lib/fontawesome";
// Variables
@import "variables";

View file

@ -56,37 +56,14 @@ body, button, input, textarea {
}
.card.status-container .status-photo {
display: block !important;
margin: auto !important;
}
.card.status-container .status-comments {
height: 220px;
overflow-y: scroll;
border-bottom:1px solid rgba(0, 0, 0, 0.1);
}
.card.status-container.orientation-square .status-comments {
height: 360px !important;
}
.card.status-container.orientation-portrait .status-comments {
height: 528px !important;
}
.card.status-container.orientation-landscape .status-photo img {
max-height: 451px !important;
}
.card.status-container.orientation-square .status-photo img {
max-height: 601px !important;
}
.card.status-container.orientation-portrait .status-photo img {
max-height: 772px !important;
}
.no-caret.dropdown-toggle {
text-decoration: none !important;
}
@ -173,3 +150,19 @@ body, button, input, textarea {
background-size: cover;
background-position: 50%;
}
.fas, .far {
font-size: 25px !important;
}
.far.fa-heart {
color: #343a40;
}
.svg-inline--fa.fa-heart.fa-w-16.status-heart.fa-2x {
color: #f70ec4;
}
.fas.fa-heart {
}

View file

@ -0,0 +1,345 @@
/*!
* Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
svg:not(:root).svg-inline--fa {
overflow: visible; }
.svg-inline--fa {
display: inline-block;
font-size: inherit;
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-lg {
vertical-align: -.225em; }
.svg-inline--fa.fa-w-1 {
width: 0.0625em; }
.svg-inline--fa.fa-w-2 {
width: 0.125em; }
.svg-inline--fa.fa-w-3 {
width: 0.1875em; }
.svg-inline--fa.fa-w-4 {
width: 0.25em; }
.svg-inline--fa.fa-w-5 {
width: 0.3125em; }
.svg-inline--fa.fa-w-6 {
width: 0.375em; }
.svg-inline--fa.fa-w-7 {
width: 0.4375em; }
.svg-inline--fa.fa-w-8 {
width: 0.5em; }
.svg-inline--fa.fa-w-9 {
width: 0.5625em; }
.svg-inline--fa.fa-w-10 {
width: 0.625em; }
.svg-inline--fa.fa-w-11 {
width: 0.6875em; }
.svg-inline--fa.fa-w-12 {
width: 0.75em; }
.svg-inline--fa.fa-w-13 {
width: 0.8125em; }
.svg-inline--fa.fa-w-14 {
width: 0.875em; }
.svg-inline--fa.fa-w-15 {
width: 0.9375em; }
.svg-inline--fa.fa-w-16 {
width: 1em; }
.svg-inline--fa.fa-w-17 {
width: 1.0625em; }
.svg-inline--fa.fa-w-18 {
width: 1.125em; }
.svg-inline--fa.fa-w-19 {
width: 1.1875em; }
.svg-inline--fa.fa-w-20 {
width: 1.25em; }
.svg-inline--fa.fa-pull-left {
margin-right: .3em;
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: .3em;
width: auto; }
.svg-inline--fa.fa-border {
height: 1.5em; }
.svg-inline--fa.fa-li {
width: 2em; }
.svg-inline--fa.fa-fw {
width: 1.25em; }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-text, .fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-counter {
background-color: #ff253a;
border-radius: 1em;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #fff;
height: 1.5em;
line-height: 1;
max-width: 5em;
min-width: 1.5em;
overflow: hidden;
padding: .25em;
right: 0;
text-overflow: ellipsis;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: 0;
right: 0;
top: auto;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: bottom right;
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: 0;
left: 0;
right: auto;
top: auto;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: bottom left;
transform-origin: bottom left; }
.fa-layers-top-right {
right: 0;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-top-left {
left: 0;
right: auto;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top left;
transform-origin: top left; }
.fa-lg {
font-size: 1.33333em;
line-height: 0.75em;
vertical-align: -.0667em; }
.fa-xs {
font-size: .75em; }
.fa-sm {
font-size: .875em; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: 2.5em;
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: -2em;
position: absolute;
text-align: center;
width: 2em;
line-height: inherit; }
.fa-border {
border: solid 0.08em #eee;
border-radius: .1em;
padding: .2em .25em .15em; }
.fa-pull-left {
float: left; }
.fa-pull-right {
float: right; }
.fa.fa-pull-left,
.fas.fa-pull-left,
.far.fa-pull-left,
.fal.fa-pull-left,
.fab.fa-pull-left {
margin-right: .3em; }
.fa.fa-pull-right,
.fas.fa-pull-right,
.far.fa-pull-right,
.fal.fa-pull-right,
.fab.fa-pull-right {
margin-left: .3em; }
.fa-spin {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear; }
.fa-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8); }
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
.fa-rotate-90 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.fa-rotate-180 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
.fa-rotate-270 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
.fa-flip-horizontal {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
.fa-flip-vertical {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
.fa-flip-horizontal.fa-flip-vertical {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
:root .fa-rotate-90,
:root .fa-rotate-180,
:root .fa-rotate-270,
:root .fa-flip-horizontal,
:root .fa-flip-vertical {
-webkit-filter: none;
filter: none; }
.fa-stack {
display: inline-block;
height: 2em;
position: relative;
width: 2em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2em; }
.fa-inverse {
color: #fff; }
.sr-only {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px; }
.sr-only-focusable:active, .sr-only-focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
width: auto; }

View file

@ -4,5 +4,6 @@ return [
'likedPhoto' => 'liked your photo.',
'startedFollowingYou' => 'started following you.',
'commented' => 'commented on your post.',
];

View file

@ -18,8 +18,12 @@
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
</span>
<span class="float-right notification-action">
@if($notification->item_id)
<a href="{{$notification->status->url()}}"><img src="{{$notification->status->thumb()}}" width="32px" height="32px"></a>
@endif
</span>
@break
@case('follow')
<span class="notification-icon pr-3">
<img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
@ -38,6 +42,22 @@
</span>
@endif
@break
@case('comment')
<span class="notification-icon pr-3">
<img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
</span>
<span class="notification-text">
{!! $notification->rendered !!}
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
</span>
<span class="float-right notification-action">
@if($notification->item_id)
<a href="{{$notification->status->parent()->url()}}"><img src="{{$notification->status->parent()->thumb()}}" width="32px" height="32px"></a>
@endif
</span>
@break
@endswitch
</li>
@endforeach

View file

@ -1,6 +1,6 @@
<footer class="pt-5 mt-5">
<div class="container mt-5 pt-5">
<p class="mt-5 text-uppercase font-weight-bold small">
<footer>
<div class="container mt-5">
<p class="text-uppercase font-weight-bold small">
<a href="{{route('site.about')}}" class="text-primary pr-2">About Us</a>
<a href="{{route('site.help')}}" class="text-primary pr-2">Support</a>
<a href="" class="text-primary pr-2">API</a>

View file

@ -1,15 +1,12 @@
<nav class="navbar navbar-expand-md navbar-light navbar-laravel">
<nav class="navbar navbar-expand navbar-light navbar-laravel sticky-top">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="{{ url('/timeline') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
<strong class="font-weight-bold">{{ config('app.name', 'Laravel') }}</strong>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<ul class="navbar-nav ml-auto d-none d-md-block">
<form class="form-inline search-form">
<input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
</form>
@ -64,3 +61,8 @@
</div>
</div>
</nav>
<nav class="breadcrumb d-md-none d-flex">
<form class="form-inline search-form mx-auto">
<input class="form-control mr-sm-2 search-form-input" type="search" placeholder="Search" aria-label="Search">
</form>
</nav>

View file

@ -42,7 +42,7 @@
</div>
</div>
<div class="col-12 col-md-8 offset-2">
<div class="col-12 col-md-8 offset-md-2">
@if($followers->count() !== 0)
<ul class="list-group mt-4">
@foreach($followers as $user)

View file

@ -42,7 +42,7 @@
</div>
</div>
<div class="col-12 col-md-8 offset-2">
<div class="col-12 col-md-8 offset-md-2">
@if($following->count() !== 0)
<ul class="list-group mt-4">
@foreach($following as $user)

View file

@ -19,7 +19,7 @@
</div>
</div>
<div class="timestamp mb-0">
<p class="small text-uppercase mb-0"><a href="{{$status->url()}}" class="text-muted">{{$status->created_at->diffForHumans()}}</a></p>
<p class="small text-uppercase mb-0"><a href="{{$status->url()}}" class="text-muted">{{$status->created_at->diffForHumans(null, true, true, true)}}</a></p>
</div>
</div>
<div class="card-body status-comments">
@ -43,16 +43,20 @@
<form class="d-inline-flex like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$status->id}}" data-action="like">
@csrf
<input type="hidden" name="item" value="{{$status->id}}">
<button class="btn btn-link text-dark p-0" type="submit"><h3 class="icon-heart mb-0"></h3></button>
<button class="btn btn-link text-dark p-0" type="submit">
<span class="far fa-heart fa-lg mb-0"></span>
</button>
</form>
<span class="icon-speech pr-3"></span>
<span class="far fa-comment fa-lg pt-1 pr-3"></span>
@if(Auth::check())
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
<form method="post" action="/i/delete" class="d-inline-flex">
@csrf
<input type="hidden" name="type" value="post">
<input type="hidden" name="item" value="{{$status->id}}">
<button type="submit" class="btn btn-link text-dark p-0"><h3 class="icon-trash mb-0"></h3></button>
<button type="submit" class="btn btn-link text-dark p-0">
<span class="far fa-trash-alt fa-lg mb-0"></span>
</button>
</form>
@endif
@endif
@ -60,12 +64,14 @@
<form class="d-inline-flex bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$status->id}}" data-action="bookmark">
@csrf
<input type="hidden" name="item" value="{{$status->id}}">
<button class="btn btn-link text-dark p-0" type="submit"><h3 class="icon-notebook mb-0"></h3></button>
<button class="btn btn-link text-dark p-0" type="submit">
<span class="far fa-bookmark fa-lg mb-0"></span>
</button>
</form>
</span>
</div>
<div class="likes font-weight-bold mb-0">
<span class="like-count">{{$status->likes()->count()}}</span> likes
<span class="like-count" data-count="{{$status->likes_count}}">{{$status->likes_count}}</span> likes
</div>
</div>
<div class="card-footer">

View file

@ -33,22 +33,24 @@
</a>
<div class="card-body">
<div class="reactions h3">
<form class="like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$item->id}}" data-action="like">
<form class="like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$item->id}}" data-action="like" data-count="{{$item->likes_count}}">
@csrf
<input type="hidden" name="item" value="{{$item->id}}">
<button class="btn btn-link text-dark p-0" type="submit"><span class="icon-heart" style="font-size:25px;"></span></button>
<button class="btn btn-link text-dark p-0" type="submit">
<span class="far fa-heart status-heart fa-2x"></span>
</button>
</form>
<span class="icon-speech"></span>
<span class="far fa-comment status-comment-focus"></span>
<span class="float-right">
<form class="bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$item->id}}" data-action="bookmark">
@csrf
<input type="hidden" name="item" value="{{$item->id}}">
<button class="btn btn-link text-dark p-0" type="submit"><span class="icon-notebook" style="font-size:25px;"></span></button>
<button class="btn btn-link text-dark p-0" type="submit"><span class="far fa-bookmark" style="font-size:25px;"></span></button>
</form>
</span>
</div>
<div class="likes font-weight-bold">
<span class="like-count">{{$item->likes()->count()}}</span> likes
<span class="like-count">{{$item->likes_count}}</span> likes
</div>
<div class="caption">
<p class="mb-1">
@ -95,7 +97,7 @@
<form class="comment-form" method="post" action="/i/comment" data-id="{{$item->id}}" data-truncate="true">
@csrf
<input type="hidden" name="item" value="{{$item->id}}">
<input class="form-control" name="comment" placeholder="Add a comment...">
<input class="form-control status-reply-input" name="comment" placeholder="Add a comment...">
</form>
</div>
</div>

View file

@ -47,6 +47,7 @@ Route::domain(config('pixelfed.domain.app'))->group(function() {
Route::get('search/{tag}', 'SearchController@searchAPI')
->where('tag', '[A-Za-z0-9]+');
Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
Route::get('v1/likes', 'ApiController@hydrateLikes');
});
Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');
@ -124,6 +125,7 @@ Route::domain(config('pixelfed.domain.app'))->group(function() {
Route::view('libraries', 'site.libraries')->name('site.libraries');
});
Route::get('p/{username}/{id}/c/{cid}', 'CommentController@show');
Route::get('p/{username}/{id}', 'StatusController@show');
Route::get('{username}/saved', 'ProfileController@savedBookmarks');
Route::get('{username}/followers', 'ProfileController@followers');