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

Frontend ui refactor
This commit is contained in:
daniel 2018-08-09 23:53:30 -06:00 committed by GitHub
commit 3f0b791038
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1499 additions and 489 deletions

View file

@ -32,6 +32,8 @@ MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="pixelfed@example.com
MAIL_FROM_NAME="Pixelfed"
SESSION_DOMAIN="${APP_DOMAIN}"
SESSION_SECURE_COOKIE=true

View file

@ -1,6 +1,6 @@
# PixelFed: Federated Image Sharing
PixelFed is a federated social image sharing platform, similar to instagram.
PixelFed is a federated social image sharing platform, similar to Instagram.
Federation is done using the [ActivityPub](https://activitypub.rocks/) protocol,
which is used by [Mastodon](http://joinmastodon.org/), [PeerTube](https://joinpeertube.org/en/),
[Pleroma](https://pleroma.social/), and more. Through ActivityPub PixelFed can share
@ -73,4 +73,4 @@ Matrix](https://matrix.to/#/#freenode_#pixelfed:matrix.org)
## Support
The lead maintainer is on Patreon! You can become a Patron at
https://www.patreon.com/dansup
https://www.patreon.com/dansup

View file

@ -40,6 +40,25 @@ class AccountController extends Controller
return view('account.activity', compact('profile', 'notifications'));
}
public function followingActivity(Request $request)
{
$this->validate($request, [
'page' => 'nullable|min:1|max:3',
'a' => 'nullable|alpha_dash',
]);
$profile = Auth::user()->profile;
$action = $request->input('a');
$timeago = Carbon::now()->subMonths(1);
$following = $profile->following->pluck('id');
$notifications = Notification::whereIn('actor_id', $following)
->where('profile_id', '!=', $profile->id)
->whereDate('created_at', '>', $timeago)
->orderBy('notifications.id','desc')
->simplePaginate(30);
return view('account.following', compact('profile', 'notifications'));
}
public function verifyEmail(Request $request)
{
return view('account.verify_email');

View file

@ -2,16 +2,13 @@
namespace App\Http\Controllers;
use Auth;
use App\Like;
use Auth, Cache;
use App\{Like, Status};
use Illuminate\Http\Request;
use App\Http\Controllers\Api\BaseApiController;
class ApiController extends Controller
class ApiController extends BaseApiController
{
public function __construct()
{
$this->middleware('auth');
}
public function hydrateLikes(Request $request)
{
@ -21,12 +18,18 @@ class ApiController extends Controller
]);
$profile = Auth::user()->profile;
$likes = Like::whereProfileId($profile->id)
$res = Cache::remember('api:like-ids:user:'.$profile->id, 1440, function() use ($profile) {
return Like::whereProfileId($profile->id)
->orderBy('id', 'desc')
->take(1000)
->pluck('status_id');
});
return response()->json($likes);
return response()->json($res);
}
public function loadMoreComments(Request $request)
{
return;
}
}

View file

@ -18,6 +18,14 @@ class CommentController extends Controller
return view('status.reply', compact('user', 'status'));
}
public function showAll(Request $request, $username, int $id)
{
$user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id)->findOrFail($id);
$replies = Status::whereInReplyToId($id)->paginate(40);
return view('status.comments', compact('user', 'status', 'replies'));
}
public function store(Request $request)
{
if(Auth::check() === false) { abort(403); }

View file

@ -15,17 +15,44 @@ class DiscoverController extends Controller
public function home()
{
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
$people = Profile::inRandomOrder()->where('id', '!=', Auth::user()->profile->id)->whereNotIn('id', $following)->take(3)->get();
$posts = Status::whereHas('media')->where('profile_id', '!=', Auth::user()->profile->id)->whereNotIn('profile_id', $following)->orderBy('created_at', 'desc')->take('21')->get();
$pid = Auth::user()->profile->id;
$following = Follower::whereProfileId($pid)
->pluck('following_id');
$people = Profile::inRandomOrder()
->where('id', '!=', $pid)
->whereNotIn('id', $following)
->take(3)
->get();
$posts = Status::whereHas('media')
->where('profile_id', '!=', $pid)
->whereNotIn('profile_id', $following)
->orderBy('created_at', 'desc')
->simplePaginate(21);
return view('discover.home', compact('people', 'posts'));
}
public function showTags(Request $request, $hashtag)
{
$tag = Hashtag::whereSlug($hashtag)->firstOrFail();
$posts = $tag->posts()->has('media')->orderBy('id','desc')->paginate(12);
$count = $tag->posts()->has('media')->orderBy('id','desc')->count();
return view('discover.tags.show', compact('tag', 'posts', 'count'));
$this->validate($request, [
'page' => 'nullable|integer|min:1|max:10'
]);
$tag = Hashtag::with('posts')
->withCount('posts')
->whereSlug($hashtag)
->firstOrFail();
$posts = $tag->posts()
->whereIsNsfw(false)
->whereVisibility('public')
->has('media')
->orderBy('id','desc')
->simplePaginate(12);
return view('discover.tags.show', compact('tag', 'posts'));
}
}

View file

@ -2,12 +2,25 @@
namespace App\Http\Controllers;
use Auth;
use Illuminate\Http\Request;
use App\{Avatar, Profile, Report, Status, User};
class ReportController extends Controller
{
protected $profile;
public function __construct()
{
$this->middleware('auth');
}
public function showForm(Request $request)
{
$this->validate($request, [
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1'
]);
return view('report.form');
}
@ -35,4 +48,92 @@ class ReportController extends Controller
{
return view('report.spam.profile');
}
public function sensitiveCommentForm(Request $request)
{
return view('report.sensitive.comment');
}
public function sensitivePostForm(Request $request)
{
return view('report.sensitive.post');
}
public function sensitiveProfileForm(Request $request)
{
return view('report.sensitive.profile');
}
public function abusiveCommentForm(Request $request)
{
return view('report.abusive.comment');
}
public function abusivePostForm(Request $request)
{
return view('report.abusive.post');
}
public function abusiveProfileForm(Request $request)
{
return view('report.abusive.profile');
}
public function formStore(Request $request)
{
$this->validate($request, [
'report' => 'required|alpha_dash',
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
'msg' => 'nullable|string|max:150'
]);
$profile = Auth::user()->profile;
$reportType = $request->input('report');
$object_id = $request->input('id');
$object_type = $request->input('type');
$msg = $request->input('msg');
$object = null;
$types = ['spam', 'sensitive', 'abusive'];
if(!in_array($reportType, $types)) {
return redirect('/timeline')->with('error', 'Invalid report type');
}
switch ($object_type) {
case 'post':
$object = Status::findOrFail($object_id);
$object_type = 'App\Status';
$exists = Report::whereUserId(Auth::id())
->whereObjectId($object->id)
->whereObjectType('App\Status')
->count();
break;
default:
return redirect('/timeline')->with('error', 'Invalid report type');
break;
}
if($exists !== 0) {
return redirect('/timeline')->with('error', 'You have already reported this!');
}
if($object->profile_id == $profile->id) {
return redirect('/timeline')->with('error', 'You cannot report your own content!');
}
$report = new Report;
$report->profile_id = $profile->id;
$report->user_id = Auth::id();
$report->object_id = $object->id;
$report->object_type = $object_type;
$report->reported_profile_id = $object->profile_id;
$report->type = $request->input('report');
$report->message = $request->input('msg');
$report->save();
return redirect('/timeline')->with('status', 'Report successfully sent!');
}
}

View file

@ -3,19 +3,28 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{AccountLog, Profile, User};
use App\{AccountLog, Media, Profile, User};
use Auth, DB;
use App\Util\Lexer\PrettyNumber;
class SettingsController extends Controller
{
public function __construct()
{
return $this->middleware('auth');
$this->middleware('auth');
}
public function home()
{
return view('settings.home');
$id = Auth::user()->profile->id;
$storage = [];
$used = Media::whereProfileId($id)->sum('size');
$storage['limit'] = config('pixelfed.max_account_size') * 1024;
$storage['used'] = $used;
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
return view('settings.home', compact('storage'));
}
public function homeUpdate(Request $request)

View file

@ -2,9 +2,10 @@
namespace App\Http\Controllers;
use App, Auth;
use App, Auth, Cache;
use Illuminate\Http\Request;
use App\{Follower, Status, User};
use App\{Follower, Profile, Status, User};
use App\Util\Lexer\PrettyNumber;
class SiteController extends Controller
{
@ -20,7 +21,7 @@ class SiteController extends Controller
public function homeGuest()
{
return view('site.index');
return view('welcome');
}
public function homeTimeline()
@ -29,10 +30,12 @@ class SiteController extends Controller
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
$following->push(Auth::user()->profile->id);
$timeline = Status::whereIn('profile_id', $following)
->whereHas('media')
->orderBy('id','desc')
->withCount(['comments', 'likes', 'shares'])
->simplePaginate(10);
return view('timeline.template', compact('timeline'));
->simplePaginate(20);
$type = 'personal';
return view('timeline.template', compact('timeline', 'type'));
}
public function changeLocale(Request $request, $locale)
@ -43,4 +46,20 @@ class SiteController extends Controller
App::setLocale($locale);
return redirect()->back();
}
public function about()
{
$res = Cache::remember('site:page:about', 15, function() {
$statuses = Status::whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->count();
$statusCount = PrettyNumber::convert($statuses);
$userCount = PrettyNumber::convert(User::count());
$remoteCount = PrettyNumber::convert(Profile::whereNotNull('remote_url')->count());
$adminContact = User::whereIsAdmin(true)->first();
return view('site.about')->with(compact('statusCount', 'userCount', 'remoteCount', 'adminContact'))->render();
});
return $res;
}
}

View file

@ -13,93 +13,147 @@ class StatusController extends Controller
{
public function show(Request $request, $username, int $id)
{
$user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id)
->withCount(['likes', 'comments', 'media'])
->findOrFail($id);
if(!$status->media_path && $status->in_reply_to_id) {
return redirect($status->url());
}
return view('status.show', compact('user', 'status'));
$user = Profile::whereUsername($username)->firstOrFail();
$status = Status::whereProfileId($user->id)
->withCount(['likes', 'comments', 'media'])
->findOrFail($id);
if(!$status->media_path && $status->in_reply_to_id) {
return redirect($status->url());
}
$replies = Status::whereInReplyToId($status->id)->simplePaginate(30);
return view('status.show', compact('user', 'status', 'replies'));
}
public function compose()
{
if(Auth::check() == false)
{
abort(403);
}
return view('status.compose');
}
public function store(Request $request)
{
if(Auth::check() == false)
{
abort(403);
}
if(Auth::check() == false)
{
abort(403);
}
$user = Auth::user();
$user = Auth::user();
$this->validate($request, [
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
'cw' => 'nullable|string',
'filter_class' => 'nullable|string',
'filter_name' => 'nullable|string',
]);
$size = Media::whereUserId($user->id)->sum('size') / 1000;
$limit = (int) config('pixelfed.max_account_size');
if($size >= $limit) {
return redirect()->back()->with('error', 'You have exceeded your storage limit. Please click <a href="#">here</a> for more info.');
}
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
}
$this->validate($request, [
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
'cw' => 'nullable|string',
'filter_class' => 'nullable|string',
'filter_name' => 'nullable|string',
]);
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
$monthHash = hash('sha1', date('Y') . date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);
$profile = $user->profile;
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
}
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
$monthHash = hash('sha1', date('Y') . date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);
$profile = $user->profile;
$status = new Status;
$status->profile_id = $profile->id;
$status->caption = strip_tags($request->caption);
$status->is_nsfw = $cw;
$status = new Status;
$status->profile_id = $profile->id;
$status->caption = strip_tags($request->caption);
$status->is_nsfw = $cw;
$status->save();
$status->save();
$photos = $request->file('photo');
$order = 1;
foreach ($photos as $k => $v) {
$storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $v->store($storagePath);
$media = new Media;
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$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++;
}
$photos = $request->file('photo');
$order = 1;
foreach ($photos as $k => $v) {
$storagePath = "public/m/{$monthHash}/{$userHash}";
$path = $v->store($storagePath);
$media = new Media;
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$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);
NewStatusPipeline::dispatch($status);
// TODO: Send to subscribers
return redirect($status->url());
// TODO: Send to subscribers
return redirect($status->url());
}
public function delete(Request $request)
{
if(!Auth::check()) {
abort(403);
}
if(!Auth::check()) {
abort(403);
}
$this->validate($request, [
'type' => 'required|string',
'item' => 'required|integer|min:1'
]);
$this->validate($request, [
'type' => 'required|string',
'item' => 'required|integer|min:1'
]);
$status = Status::findOrFail($request->input('item'));
$status = Status::findOrFail($request->input('item'));
if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
StatusDelete::dispatch($status);
}
if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
StatusDelete::dispatch($status);
}
return redirect(Auth::user()->url());
return redirect(Auth::user()->url());
}
public function storeShare(Request $request)
{
$this->validate($request, [
'item' => 'required|integer',
]);
$profile = Auth::user()->profile;
$status = Status::withCount('shares')->findOrFail($request->input('item'));
$count = $status->shares_count;
$exists = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->count();
if($exists !== 0) {
$shares = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->get();
foreach($shares as $share) {
$share->delete();
$count--;
}
} else {
$share = new Status;
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->save();
$count++;
}
if($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
} else {
$response = redirect($status->url());
}
return $response;
}
}

View file

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

View file

@ -107,15 +107,15 @@ class StatusEntityLexer implements ShouldQueue
if(empty($mentioned) || !isset($mentioned->id)) {
continue;
}
DB::transaction(function () use ($status, $mentioned) {
$m = new Mention;
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
MentionPipeline::dispatch($status, $m);
});
MentionPipeline::dispatch($status, $m);
}
}

View file

@ -2,7 +2,7 @@
namespace App;
use Storage;
use Auth, Storage;
use App\Util\Lexer\PrettyNumber;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -138,4 +138,35 @@ class Profile extends Model
{
return $this->statuses()->whereHas('media')->count();
}
public function recommendFollowers()
{
$follows = $this->following()->pluck('followers.id');
$following = $this->following()
->orderByRaw('rand()')
->take(3)
->pluck('following_id');
$following->push(Auth::id());
$following = Follower::whereNotIn('profile_id', $follows)
->whereNotIn('following_id', $following)
->whereNotIn('following_id', $follows)
->whereIn('profile_id', $following)
->orderByRaw('rand()')
->limit(3)
->pluck('following_id');
$recommended = [];
foreach($following as $follow) {
$recommended[] = Profile::findOrFail($follow);
}
return $recommended;
}
public function keyId()
{
if($this->remote_url) {
return;
}
return $this->permalink('#main-key');
}
}

38
app/Report.php Normal file
View file

@ -0,0 +1,38 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Report extends Model
{
public function url()
{
return url('/i/admin/reports/show/' . $this->id);
}
public function reporter()
{
return $this->belongsTo(Profile::class, 'profile_id');
}
public function reported()
{
$class = $this->object_type;
switch ($class) {
case 'App\Status':
$column = 'id';
break;
default:
$column = 'id';
break;
}
return (new $class())->where($column, $this->object_id)->firstOrFail();
}
public function reportedUser()
{
return $this->belongsTo(Profile::class, 'reported_profile_id', 'id');
}
}

View file

@ -52,6 +52,14 @@ class Status extends Model
return url($path);
}
public function permalink($suffix = '/activity')
{
$id = $this->id;
$username = $this->profile->username;
$path = config('app.url') . "/p/{$username}/{$id}{$suffix}";
return url($path);
}
public function editUrl()
{
return $this->url() . '/edit';
@ -174,4 +182,9 @@ class Status extends Model
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
__('notification.commented');
}
public function recentComments()
{
return $this->comments()->orderBy('created_at','desc')->take(3);
}
}

View file

@ -115,8 +115,9 @@ class Image {
});
$converted = $this->setBaseName($path, $thumbnail, $img->extension);
$newPath = storage_path('app/'.$converted['path']);
$img->save($newPath, 75);
$quality = config('pixelfed.image_quality');
$img->save($newPath, $quality);
if(!$thumbnail) {
$media->orientation = $orientation;

View file

@ -76,7 +76,7 @@ return [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'simple',
'processes' => 10,
'processes' => 20,
'tries' => 3,
],
],

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your PixelFed instance.
|
*/
'version' => '0.1.2',
'version' => '0.1.3',
/*
|--------------------------------------------------------------------------
@ -86,7 +86,7 @@ return [
|
|
*/
'max_account_size' => env('MAX_ACCOUNT_SIZE', 100000),
'max_account_size' => env('MAX_ACCOUNT_SIZE', 1000000),
/*
|--------------------------------------------------------------------------
@ -106,7 +106,7 @@ return [
| Change the caption length limit for new local posts.
|
*/
'max_caption_length' => env('MAX_CAPTION_LENGTH', 150),
'max_caption_length' => env('MAX_CAPTION_LENGTH', 500),
/*
|--------------------------------------------------------------------------
@ -127,5 +127,15 @@ return [
|
*/
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
/*
|--------------------------------------------------------------------------
| Image Quality
|--------------------------------------------------------------------------
|
| Set the image optimization quality, must be a value between 1-100.
|
*/
'image_quality' => (int) env('IMAGE_QUALITY', 80),
];

View file

@ -46,7 +46,7 @@ services:
- "mysql-data:/var/lib/mysql"
redis:
image: redis:alpine
image: redis:4-alpine
volumes:
- "redis-data:/data"
networks:

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/img/fred1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

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

@ -1,13 +1,6 @@
window._ = require('lodash');
window.Popper = require('popper.js').default;
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
import swal from 'sweetalert';
try {
window.pixelfed = {};
window.$ = window.jQuery = require('jquery');
@ -16,6 +9,7 @@ try {
window.filesize = require('filesize');
window.typeahead = require('./lib/typeahead');
window.Bloodhound = require('./lib/bloodhound');
window.Vue = require('vue');
require('./components/localstorage');
require('./components/likebutton');
@ -23,45 +17,21 @@ try {
require('./components/searchform');
require('./components/bookmarkform');
require('./components/statusform');
Vue.component(
'follow-suggestions',
require('./components/FollowSuggestions.vue')
);
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
$('[data-toggle="tooltip"]').tooltip();
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });

View file

@ -0,0 +1,50 @@
<style scoped>
</style>
<template>
<div class="card mb-4">
<div class="card-header bg-white">
<span class="font-weight-bold h5">Who to follow</span>
<span class="small float-right font-weight-bold">
<a href="javascript:void(0);" class="pr-2" v-on:click="fetchData">Refresh</a>
</span>
</div>
<div class="card-body">
<div v-if="results.length == 0">
<p class="mb-0 font-weight-bold">You are not following anyone yet, try the <a href="/discover">discover</a> feature to find users to follow.</p>
</div>
<div v-for="(user, index) in results">
<div class="media " style="width:100%">
<img class="mr-3" :src="user.avatar" width="40px">
<div class="media-body" style="width:70%">
<p class="my-0 font-weight-bold text-truncate" style="text-overflow: hidden">{{user.acct}} <span class="text-muted font-weight-normal">&commat;{{user.username}}</span></p>
<a class="btn btn-outline-primary px-3 py-0" :href="user.url" style="border-radius:20px;">Follow</a>
</div>
</div>
<div v-if="index != results.length - 1"><hr></div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
results: {},
};
},
mounted() {
this.fetchData();
},
methods: {
fetchData() {
axios.get('/api/local/i/follow-suggestions')
.then(response => {
this.results = response.data;
});
}
}
}
</script>

View file

@ -1,6 +1,12 @@
$(document).ready(function() {
$('.status-comment-focus').on('click', function(el) {
$('.status-card > .card-footer').each(function() {
$(this).addClass('d-none');
});
$(document).on('click', '.status-comment-focus', function(el) {
var form = $(this).parents().eq(2).find('.card-footer');
form.removeClass('d-none');
var el = $(this).parents().eq(2).find('input[name="comment"]');
el.focus();
});
@ -31,7 +37,7 @@ $(document).ready(function() {
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">'+ reply + '</span><span class="float-right"><a href="' + permalink + '" class="text-dark small font-weight-bold">1s</a></span></p>';
comments.prepend(comment);
comments.append(comment);
commentform.val('');
commentform.blur();
@ -41,7 +47,5 @@ $(document).ready(function() {
.catch(function (res) {
});
});
});

View file

@ -1,18 +1,17 @@
$(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', []);
})
pixelfed.fetchLikes = () => {
axios.get('/api/v1/likes')
.then(function (res) {
ls.set('likes', res.data);
})
.catch(function (res) {
ls.set('likes', []);
})
}
pixelfed.hydrateLikes = function() {
pixelfed.hydrateLikes = () => {
var likes = ls.get('likes');
$('.like-form').each(function(i, el) {
var el = $(el);
@ -20,11 +19,14 @@ $(document).ready(function() {
var heart = el.find('.status-heart');
if(likes.indexOf(id) != -1) {
heart.removeClass('far fa-heart').addClass('fas fa-heart');
heart.removeClass('text-dark').addClass('text-primary');
} else {
heart.removeClass('text-primary').addClass('text-dark');
}
});
};
pixelfed.fetchLikes();
pixelfed.hydrateLikes();
$(document).on('submit', '.like-form', function(e) {
@ -33,6 +35,8 @@ $(document).ready(function() {
var id = el.data('id');
axios.post('/i/like', {item: id})
.then(function (res) {
pixelfed.fetchLikes();
pixelfed.hydrateLikes();
var likes = ls.get('likes');
var action = false;
var counter = el.parents().eq(1).find('.like-count');
@ -40,14 +44,14 @@ $(document).ready(function() {
var heart = el.find('.status-heart');
if(likes.indexOf(id) > -1) {
heart.removeClass('fas fa-heart').addClass('far fa-heart');
heart.removeClass('text-primary').addClass('text-dark');
likes = likes.filter(function(item) {
return item !== id
});
counter.text(count);
action = 'unlike';
} else {
heart.removeClass('far fa-heart').addClass('fas fa-heart');
heart.removeClass('text-dark').addClass('text-primary');
likes.push(id);
counter.text(count);
action = 'like';

View file

@ -1,9 +1,5 @@
$(document).ready(function() {
$('#statusForm .btn-filter-select').on('click', function(e) {
let el = $(this);
});
pixelfed.create = {};
pixelfed.filters = {};
pixelfed.create.hasGeneratedSelect = false;
@ -78,7 +74,7 @@ $(document).ready(function() {
pixelfed.create.hasGeneratedSelect = true;
}
$('#fileInput').on('change', function() {
$(document).on('change', '#fileInput', function() {
previewImage(this);
$('#statusForm .form-filters.d-none').removeClass('d-none');
$('#statusForm .form-preview.d-none').removeClass('d-none');
@ -88,23 +84,43 @@ $(document).ready(function() {
}
});
$('#filterSelectDropdown').on('change', function() {
$(document).on('change', '#filterSelectDropdown', function() {
let el = $(this);
let filter = el.val();
let oldFilter = pixelfed.create.currentFilterClass;
if(filter == 'none') {
$('.filterContainer').removeClass(oldFilter);
pixelfed.create.currentFilterClass = false;
pixelfed.create.currentFilterName = 'None';
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
return;
$('input[name=filter_class]').val('');
$('input[name=filter_name]').val('');
$('.filterContainer').removeClass(oldFilter);
pixelfed.create.currentFilterClass = false;
pixelfed.create.currentFilterName = 'None';
$('.form-group.form-preview .form-text').text('Current Filter: No filter selected');
return;
} else {
$('.filterContainer').removeClass(oldFilter).addClass(filter);
pixelfed.create.currentFilterClass = filter;
pixelfed.create.currentFilterName = el.find(':selected').text();
$('.form-group.form-preview .form-text').text('Current Filter: ' + pixelfed.create.currentFilterName);
$('input[name=filter_class]').val(pixelfed.create.currentFilterClass);
$('input[name=filter_name]').val(pixelfed.create.currentFilterName);
return;
}
$('.filterContainer').removeClass(oldFilter).addClass(filter);
pixelfed.create.currentFilterClass = filter;
pixelfed.create.currentFilterName = el.find(':selected').text();
$('.form-group.form-preview .form-text').text('Current Filter: ' + pixelfed.create.currentFilterName);
$('input[name=filter_class]').val(pixelfed.create.currentFilterClass);
$('input[name=filter_name]').val(pixelfed.create.currentFilterName);
});
$(document).on('keyup keydown', '#statusForm textarea[name=caption]', function() {
const el = $(this);
const len = el.val().length;
const limit = el.data('limit');
if(len > limit) {
const diff = limit - len;
$('#statusForm .caption-counter').text(diff).addClass('text-danger');
} else {
$('#statusForm .caption-counter').text(len).removeClass('text-danger');
}
});
$(document).on('focus', '#statusForm textarea[name=caption]', function() {
const el = $(this);
el.attr('rows', '3');
});
});

66
resources/assets/js/lib/bloodhound.js vendored Normal file → Executable file
View file

@ -1,18 +1,18 @@
/*!
* typeahead.js 0.11.1
* typeahead.js 1.2.0
* https://github.com/twitter/typeahead.js
* Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
* Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
*/
(function(root, factory) {
if (typeof define === "function" && define.amd) {
define("bloodhound", [ "jquery" ], function(a0) {
define([ "jquery" ], function(a0) {
return root["Bloodhound"] = factory(a0);
});
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"));
} else {
root["Bloodhound"] = factory(jQuery);
root["Bloodhound"] = factory(root["jQuery"]);
}
})(this, function($) {
var _ = function() {
@ -148,18 +148,27 @@
stringify: function(val) {
return _.isString(val) ? val : JSON.stringify(val);
},
guid: function() {
function _p8(s) {
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
}
return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
},
noop: function() {}
};
}();
var VERSION = "0.11.1";
var VERSION = "1.2.0";
var tokenizers = function() {
"use strict";
return {
nonword: nonword,
whitespace: whitespace,
ngram: ngram,
obj: {
nonword: getObjTokenizer(nonword),
whitespace: getObjTokenizer(whitespace)
whitespace: getObjTokenizer(whitespace),
ngram: getObjTokenizer(ngram)
}
};
function whitespace(str) {
@ -170,6 +179,19 @@
str = _.toStr(str);
return str ? str.split(/\W+/) : [];
}
function ngram(str) {
str = _.toStr(str);
var tokens = [], word = "";
_.each(str.split(""), function(char) {
if (char.match(/\s+/)) {
word = "";
} else {
tokens.push(word + char);
word += char;
}
});
return tokens;
}
function getObjTokenizer(tokenizer) {
return function setKey(keys) {
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
@ -341,9 +363,10 @@
}();
var Transport = function() {
"use strict";
var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10);
function Transport(o) {
o = o || {};
this.maxPendingRequests = o.maxPendingRequests || 6;
this.cancelled = false;
this.lastReq = null;
this._send = o.transport;
@ -351,7 +374,7 @@
this._cache = o.cache === false ? new LruCache(0) : sharedCache;
}
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
maxPendingRequests = num;
this.maxPendingRequests = num;
};
Transport.resetCache = function resetCache() {
sharedCache.reset();
@ -369,7 +392,7 @@
}
if (jqXhr = pendingRequests[fingerprint]) {
jqXhr.done(done).fail(fail);
} else if (pendingRequestsCount < maxPendingRequests) {
} else if (pendingRequestsCount < this.maxPendingRequests) {
pendingRequestsCount++;
pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
} else {
@ -423,6 +446,7 @@
this.identify = o.identify || _.stringify;
this.datumTokenizer = o.datumTokenizer;
this.queryTokenizer = o.queryTokenizer;
this.matchAnyQueryToken = o.matchAnyQueryToken;
this.reset();
}
_.mixin(SearchIndex.prototype, {
@ -459,7 +483,7 @@
tokens = normalizeTokens(this.queryTokenizer(query));
_.each(tokens, function(token) {
var node, chars, ch, ids;
if (matches && matches.length === 0) {
if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
return false;
}
node = that.trie;
@ -471,8 +495,10 @@
ids = node[IDS].slice(0);
matches = matches ? getIntersection(matches, ids) : ids;
} else {
matches = [];
return false;
if (!that.matchAnyQueryToken) {
matches = [];
return false;
}
}
});
return matches ? _.map(unique(matches), function(id) {
@ -614,10 +640,12 @@
this.url = o.url;
this.prepare = o.prepare;
this.transform = o.transform;
this.indexResponse = o.indexResponse;
this.transport = new Transport({
cache: o.cache,
limiter: o.limiter,
transport: o.transport
transport: o.transport,
maxPendingRequests: o.maxPendingRequests
});
}
_.mixin(Remote.prototype, {
@ -655,7 +683,9 @@
identify: _.stringify,
datumTokenizer: null,
queryTokenizer: null,
matchAnyQueryToken: false,
sufficient: 5,
indexRemote: false,
sorter: null,
local: [],
prefetch: null,
@ -744,7 +774,7 @@
} else if (o.wildcard) {
prepare = prepareByWildcard;
} else {
prepare = idenityPrepare;
prepare = identityPrepare;
}
return prepare;
function prepareByReplace(query, settings) {
@ -755,7 +785,7 @@
settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
return settings;
}
function idenityPrepare(query, settings) {
function identityPrepare(query, settings) {
return settings;
}
}
@ -806,6 +836,7 @@
this.sorter = o.sorter;
this.identify = o.identify;
this.sufficient = o.sufficient;
this.indexRemote = o.indexRemote;
this.local = o.local;
this.remote = o.remote ? new Remote(o.remote) : null;
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
@ -875,6 +906,8 @@
},
search: function search(query, sync, async) {
var that = this, local;
sync = sync || _.noop;
async = async || _.noop;
local = this.sorter(this.index.search(query));
sync(this.remote ? local.slice() : local);
if (this.remote && local.length < this.sufficient) {
@ -890,7 +923,8 @@
return that.identify(r) === that.identify(l);
}) && nonDuplicates.push(r);
});
async && async(nonDuplicates);
that.indexRemote && that.add(nonDuplicates);
async(nonDuplicates);
}
},
all: function all() {

228
resources/assets/js/lib/typeahead.js vendored Normal file → Executable file
View file

@ -1,18 +1,18 @@
/*!
* typeahead.js 0.11.1
* typeahead.js 1.2.0
* https://github.com/twitter/typeahead.js
* Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
* Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
*/
(function(root, factory) {
if (typeof define === "function" && define.amd) {
define("typeahead.js", [ "jquery" ], function(a0) {
define([ "jquery" ], function(a0) {
return factory(a0);
});
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"));
} else {
factory(jQuery);
factory(root["jQuery"]);
}
})(this, function($) {
var _ = function() {
@ -148,6 +148,13 @@
stringify: function(val) {
return _.isString(val) ? val : JSON.stringify(val);
},
guid: function() {
function _p8(s) {
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
}
return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
},
noop: function() {}
};
}();
@ -189,7 +196,7 @@
function buildHtml(c) {
return {
wrapper: '<span class="' + c.wrapper + '"></span>',
menu: '<div class="' + c.menu + '"></div>'
menu: '<div role="listbox" class="' + c.menu + '"></div>'
};
}
function buildSelectors(classes) {
@ -264,10 +271,8 @@
}
_.mixin(EventBus.prototype, {
_trigger: function(type, args) {
var $e;
$e = $.Event(namespace + type);
(args = args || []).unshift($e);
this.$el.trigger.apply(this.$el, args);
var $e = $.Event(namespace + type);
this.$el.trigger.call(this.$el, $e, args || []);
return $e;
},
before: function(type) {
@ -384,7 +389,36 @@
tagName: "strong",
className: null,
wordsOnly: false,
caseSensitive: false
caseSensitive: false,
diacriticInsensitive: false
};
var accented = {
A: "[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ℀℁℻⒜Ⓐⓐ㍱-㍴㎀-㎄㎈㎉㎩-㎯㏂㏊㏟㏿Aa]",
B: "[BbᴮᵇḂ-ḇℬ⒝Ⓑⓑ㍴㎅-㎇㏃㏈㏔㏝Bb]",
C: "[CcÇçĆ-čᶜ℀ℂ℃℅℆ℭⅭⅽ⒞Ⓒⓒ㍶㎈㎉㎝㎠㎤㏄-㏇Cc]",
D: "[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟Ⓓⓓ㋏㍲㍷-㍹㎗㎭-㎯㏅㏈Dd]",
E: "[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑ℡ℯℰⅇ⒠Ⓔⓔ㉐㋍㋎Ee]",
F: "[FfᶠḞḟ℉℻⒡Ⓕⓕ㎊-㎌㎙ff-fflFf]",
G: "[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢Ⓖⓖ㋌㋍㎇㎍-㎏㎓㎬㏆㏉㏒㏿Gg]",
H: "[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣Ⓗⓗ㋌㍱㎐-㎔㏊㏋㏗Hh]",
I: "[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈⅠ-ⅣⅥ-ⅨⅪⅫⅰ-ⅳⅵ-ⅸⅺⅻ⒤Ⓘⓘ㍺㏌㏕fiffiIi]",
J: "[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]",
K: "[KkĶķǨǩᴷᵏḰ-ḵK⒦Ⓚⓚ㎄㎅㎉㎏㎑㎘㎞㎢㎦㎪㎸㎾㏀㏆㏍-㏏Kk]",
L: "[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓ℡Ⅼⅼ⒧Ⓛⓛ㋏㎈㎉㏐-㏓㏕㏖㏿flfflLl]",
M: "[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨Ⓜⓜ㍷-㍹㎃㎆㎎㎒㎖㎙-㎨㎫㎳㎷㎹㎽㎿㏁㏂㏎㏐㏔-㏖㏘㏙㏞㏟Mm]",
N: "[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩Ⓝⓝ㎁㎋㎚㎱㎵㎻㏌㏑Nn]",
O: "[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒ℅№ℴ⒪Ⓞⓞ㍵㏇㏒㏖Oo]",
P: "[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㉐㍱㍶㎀㎊㎩-㎬㎰㎴㎺㏋㏗-㏚Pp]",
Q: "[Qq⒬Ⓠⓠ㏃]",
R: "[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭Ⓡⓡ㋍㍴㎭-㎯㏚㏛Rr]",
S: "[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮Ⓢⓢ㎧㎨㎮-㎳㏛㏜stSs]",
T: "[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㉐㋏㎔㏏ſtstTt]",
U: "[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]",
V: "[VvᵛᵥṼ-ṿⅣ-Ⅷⅳ-ⅷ⒱Ⓥⓥⱽ㋎㍵㎴-㎹㏜㏞Vv]",
W: "[WwŴŵʷᵂẀ-ẉẘ⒲Ⓦⓦ㎺-㎿㏝Ww]",
X: "[XxˣẊ-ẍₓ℻Ⅸ-Ⅻⅸ-ⅻ⒳Ⓧⓧ㏓Xx]",
Y: "[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴Ⓨⓨ㏉Yy]",
Z: "[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵Ⓩⓩ㎐-㎔Zz]"
};
return function hightlight(o) {
var regex;
@ -393,7 +427,7 @@
return;
}
o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive);
traverse(o.node, hightlightTextNode);
function hightlightTextNode(textNode) {
var match, patternNode, wrapperNode;
@ -419,10 +453,17 @@
}
}
};
function getRegex(patterns, caseSensitive, wordsOnly) {
function accent_replacer(chr) {
return accented[chr.toUpperCase()] || chr;
}
function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) {
var escapedPatterns = [], regexStr;
for (var i = 0, len = patterns.length; i < len; i++) {
escapedPatterns.push(_.escapeRegExChars(patterns[i]));
var escapedWord = _.escapeRegExChars(patterns[i]);
if (diacriticInsensitive) {
escapedWord = escapedWord.replace(/\S/g, accent_replacer);
}
escapedPatterns.push(escapedWord);
}
regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
@ -448,6 +489,14 @@
www.mixin(this);
this.$hint = $(o.hint);
this.$input = $(o.input);
this.$input.attr({
"aria-activedescendant": "",
"aria-owns": this.$input.attr("id") + "_listbox",
role: "combobox",
"aria-readonly": "true",
"aria-autocomplete": "list"
});
$(www.menu).attr("id", this.$input.attr("id") + "_listbox");
this.query = this.$input.val();
this.queryWhenFocused = this.hasFocus() ? this.query : null;
this.$overflowHelper = buildOverflowHelper(this.$input);
@ -455,6 +504,7 @@
if (this.$hint.length === 0) {
this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
}
this.onSync("cursorchange", this._updateDescendent);
}
Input.normalizeQuery = function(str) {
return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
@ -524,6 +574,9 @@
this.trigger("whitespaceChanged", this.query);
}
},
_updateDescendent: function updateDescendent(event, id) {
this.$input.attr("aria-activedescendant", id);
},
bind: function() {
var that = this, onBlur, onFocus, onKeydown, onInput;
onBlur = _.bind(this._onBlur, this);
@ -647,6 +700,7 @@
"use strict";
var keys, nameGenerator;
keys = {
dataset: "tt-selectable-dataset",
val: "tt-selectable-display",
obj: "tt-selectable-object"
};
@ -666,19 +720,20 @@
}
www.mixin(this);
this.highlight = !!o.highlight;
this.name = o.name || nameGenerator();
this.name = _.toStr(o.name || nameGenerator());
this.limit = o.limit || 5;
this.displayFn = getDisplayFn(o.display || o.displayKey);
this.templates = getTemplates(o.templates, this.displayFn);
this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
this._resetLastSuggestion();
this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
this.$el = $(o.node).attr("role", "presentation").addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
}
Dataset.extractData = function extractData(el) {
var $el = $(el);
if ($el.data(keys.obj)) {
return {
dataset: $el.data(keys.dataset) || "",
val: $el.data(keys.val) || "",
obj: $el.data(keys.obj) || null
};
@ -697,7 +752,7 @@
} else {
this._empty();
}
this.trigger("rendered", this.name, suggestions, false);
this.trigger("rendered", suggestions, false, this.name);
},
_append: function append(query, suggestions) {
suggestions = suggestions || [];
@ -708,7 +763,7 @@
} else if (!this.$lastSuggestion.length && this.templates.notFound) {
this._renderNotFound(query);
}
this.trigger("rendered", this.name, suggestions, true);
this.trigger("rendered", suggestions, true, this.name);
},
_renderSuggestions: function renderSuggestions(query, suggestions) {
var $fragment;
@ -749,7 +804,7 @@
_.each(suggestions, function getSuggestionNode(suggestion) {
var $el, context;
context = that._injectQuery(query, suggestion);
$el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
$el = $(that.templates.suggestion(context)).data(keys.dataset, that.name).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
fragment.appendChild($el[0]);
});
this.highlight && highlight({
@ -787,7 +842,7 @@
this.cancel = function cancel() {
canceled = true;
that.cancel = $.noop;
that.async && that.trigger("asyncCanceled", query);
that.async && that.trigger("asyncCanceled", query, that.name);
};
this.source(query, sync, async);
!syncCalled && sync([]);
@ -800,16 +855,17 @@
rendered = suggestions.length;
that._overwrite(query, suggestions);
if (rendered < that.limit && that.async) {
that.trigger("asyncRequested", query);
that.trigger("asyncRequested", query, that.name);
}
}
function async(suggestions) {
suggestions = suggestions || [];
if (!canceled && rendered < that.limit) {
that.cancel = $.noop;
rendered += suggestions.length;
that._append(query, suggestions.slice(0, that.limit - rendered));
that.async && that.trigger("asyncReceived", query);
var idx = Math.abs(rendered - that.limit);
rendered += idx;
that._append(query, suggestions.slice(0, idx));
that.async && that.trigger("asyncReceived", query, that.name);
}
}
},
@ -843,7 +899,7 @@
suggestion: templates.suggestion || suggestionTemplate
};
function suggestionTemplate(context) {
return $("<div>").text(displayFn(context));
return $('<div role="option">').attr("id", _.guid()).text(displayFn(context));
}
}
function isValidName(str) {
@ -884,10 +940,11 @@
this.trigger.apply(this, arguments);
},
_allDatasetsEmpty: function allDatasetsEmpty() {
return _.every(this.datasets, isDatasetEmpty);
function isDatasetEmpty(dataset) {
return dataset.isEmpty();
}
return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) {
var isEmpty = dataset.isEmpty();
this.$node.attr("aria-expanded", !isEmpty);
return isEmpty;
}, this));
},
_getSelectables: function getSelectables() {
return this.$node.find(this.selectors.selectable);
@ -912,6 +969,12 @@
var that = this, onSelectableClick;
onSelectableClick = _.bind(this._onSelectableClick, this);
this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
this.$node.on("mouseover", this.selectors.selectable, function() {
that.setCursor($(this));
});
this.$node.on("mouseleave", function() {
that._removeCursor();
});
_.each(this.datasets, function(dataset) {
dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
});
@ -921,9 +984,11 @@
return this.$node.hasClass(this.classes.open);
},
open: function open() {
this.$node.scrollTop(0);
this.$node.addClass(this.classes.open);
},
close: function close() {
this.$node.attr("aria-expanded", false);
this.$node.removeClass(this.classes.open);
this._removeCursor();
},
@ -988,6 +1053,55 @@
});
return Menu;
}();
var Status = function() {
"use strict";
function Status(options) {
this.$el = $("<span></span>", {
role: "status",
"aria-live": "polite"
}).css({
position: "absolute",
padding: "0",
border: "0",
height: "1px",
width: "1px",
"margin-bottom": "-1px",
"margin-right": "-1px",
overflow: "hidden",
clip: "rect(0 0 0 0)",
"white-space": "nowrap"
});
options.$input.after(this.$el);
_.each(options.menu.datasets, _.bind(function(dataset) {
if (dataset.onSync) {
dataset.onSync("rendered", _.bind(this.update, this));
dataset.onSync("cleared", _.bind(this.cleared, this));
}
}, this));
}
_.mixin(Status.prototype, {
update: function update(event, suggestions) {
var length = suggestions.length;
var words;
if (length === 1) {
words = {
result: "result",
is: "is"
};
} else {
words = {
result: "results",
is: "are"
};
}
this.$el.text(length + " " + words.result + " " + words.is + " available, use up and down arrow keys to navigate.");
},
cleared: function() {
this.$el.text("");
}
});
return Status;
}();
var DefaultMenu = function() {
"use strict";
var s = Menu.prototype;
@ -1052,6 +1166,7 @@
this.input = o.input;
this.menu = o.menu;
this.enabled = true;
this.autoselect = !!o.autoselect;
this.active = false;
this.input.hasFocus() && this.activate();
this.dir = this.input.getLangDir();
@ -1098,8 +1213,12 @@
_onDatasetCleared: function onDatasetCleared() {
this._updateHint();
},
_onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
_onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) {
this._updateHint();
if (this.autoselect) {
var cursorClass = this.selectors.cursor.substr(1);
this.menu.$node.find(this.selectors.suggestion).first().addClass(cursorClass);
}
this.eventBus.trigger("render", suggestions, async, dataset);
},
_onAsyncRequested: function onAsyncRequested(type, dataset, query) {
@ -1122,7 +1241,15 @@
_onEnterKeyed: function onEnterKeyed(type, $e) {
var $selectable;
if ($selectable = this.menu.getActiveSelectable()) {
this.select($selectable) && $e.preventDefault();
if (this.select($selectable)) {
$e.preventDefault();
$e.stopPropagation();
}
} else if (this.autoselect) {
if (this.select(this.menu.getTopSelectable())) {
$e.preventDefault();
$e.stopPropagation();
}
}
},
_onTabKeyed: function onTabKeyed(type, $e) {
@ -1144,12 +1271,12 @@
},
_onLeftKeyed: function onLeftKeyed() {
if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getTopSelectable());
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
}
},
_onRightKeyed: function onRightKeyed() {
if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getTopSelectable());
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
}
},
_onQueryChanged: function onQueryChanged(e, query) {
@ -1249,9 +1376,9 @@
},
select: function select($selectable) {
var data = this.menu.getSelectableData($selectable);
if (data && !this.eventBus.before("select", data.obj)) {
if (data && !this.eventBus.before("select", data.obj, data.dataset)) {
this.input.setQuery(data.val, true);
this.eventBus.trigger("select", data.obj);
this.eventBus.trigger("select", data.obj, data.dataset);
this.close();
return true;
}
@ -1262,21 +1389,24 @@
query = this.input.getQuery();
data = this.menu.getSelectableData($selectable);
isValid = data && query !== data.val;
if (isValid && !this.eventBus.before("autocomplete", data.obj)) {
if (isValid && !this.eventBus.before("autocomplete", data.obj, data.dataset)) {
this.input.setQuery(data.val);
this.eventBus.trigger("autocomplete", data.obj);
this.eventBus.trigger("autocomplete", data.obj, data.dataset);
return true;
}
return false;
},
moveCursor: function moveCursor(delta) {
var query, $candidate, data, payload, cancelMove;
var query, $candidate, data, suggestion, datasetName, cancelMove, id;
query = this.input.getQuery();
$candidate = this.menu.selectableRelativeToCursor(delta);
data = this.menu.getSelectableData($candidate);
payload = data ? data.obj : null;
suggestion = data ? data.obj : null;
datasetName = data ? data.dataset : null;
id = $candidate ? $candidate.attr("id") : null;
this.input.trigger("cursorchange", id);
cancelMove = this._minLengthMet() && this.menu.update(query);
if (!cancelMove && !this.eventBus.before("cursorchange", payload)) {
if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) {
this.menu.setCursor($candidate);
if (data) {
this.input.setInputValue(data.val);
@ -1284,7 +1414,7 @@
this.input.resetInputValue();
this._updateHint();
}
this.eventBus.trigger("cursorchange", payload);
this.eventBus.trigger("cursorchange", suggestion, datasetName);
return true;
}
return false;
@ -1322,7 +1452,7 @@
www = WWW(o.classNames);
return this.each(attach);
function attach() {
var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor;
var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, status, typeahead, MenuConstructor;
_.each(datasets, function(d) {
d.highlight = !!o.highlight;
});
@ -1353,11 +1483,16 @@
node: $menu,
datasets: datasets
}, www);
status = new Status({
$input: $input,
menu: menu
});
typeahead = new Typeahead({
input: input,
menu: menu,
eventBus: eventBus,
minLength: o.minLength
minLength: o.minLength,
autoselect: o.autoselect
}, www);
$input.data(keys.www, www);
$input.data(keys.typeahead, typeahead);
@ -1450,7 +1585,7 @@
return query;
} else {
ttEach(this, function(t) {
t.setVal(newVal);
t.setVal(_.toStr(newVal));
});
return this;
}
@ -1481,8 +1616,10 @@
});
}
function buildHintFromInput($input, www) {
return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({
autocomplete: "off",
return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({
readonly: true,
required: false
}).removeAttr("id name placeholder").removeClass("required").attr({
spellcheck: "false",
tabindex: -1
});
@ -1495,7 +1632,6 @@
style: $input.attr("style")
});
$input.addClass(www.classes.input).attr({
autocomplete: "off",
spellcheck: false
});
try {

View file

@ -1,13 +1,54 @@
$(document).ready(function() {
$('.pagination').hide();
$('.container.timeline-container').removeClass('d-none');
let elem = document.querySelector('.timeline-feed');
pixelfed.fetchLikes();
let infScroll = new InfiniteScroll( elem, {
path: '.pagination__next',
append: '.timeline-feed',
status: '.page-load-status',
history: false,
});
infScroll.on( 'append', function( response, path, items ) {
pixelfed.hydrateLikes();
$('.status-card > .card-footer').each(function() {
var el = $(this);
if(!el.hasClass('d-none') && !el.find('input[name="comment"]').val()) {
$(this).addClass('d-none');
}
});
});
});
$(document).on("DOMContentLoaded", function() {
var active = false;
var lazyLoad = function() {
if (active === false) {
active = true;
var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
lazyImages.forEach(function(lazyImage) {
if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
lazyImage.classList.remove("lazy");
lazyImages = lazyImages.filter(function(image) {
return image !== lazyImage;
});
}
});
active = false;
};
}
document.addEventListener("scroll", lazyLoad);
window.addEventListener("resize", lazyLoad);
window.addEventListener("orientationchange", lazyLoad);
});

View file

@ -173,11 +173,6 @@ body, button, input, textarea {
}
}
.fas.fa-heart {
color: #f70ec4!important;
}
@media (max-width: map-get($grid-breakpoints, "md")) {
.border-md-left-0 {
border-left:0!important
@ -263,3 +258,35 @@ body, button, input, textarea {
animation-duration: 0.5s;
}
.card {
box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
border: none;
}
.box-shadow {
box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
}
.border-left-primary {
border-left: 3px solid $primary;
}
.settings-nav .nav-item.active .nav-link {
font-weight: bold !important;
}
details summary::-webkit-details-marker {
display: none!important;
}
.details-animated > summary {
display: block;
background-color: #ECF0F1;
padding-top: 50px;
padding-bottom: 50px;
text-align: center;
}
.details-animated[open] > summary {
display: none!important;
}

View file

@ -9,5 +9,6 @@ return [
'settings' => 'Settings',
'admin' => 'Admin',
'logout' => 'Logout',
'directMessages' => 'Direct Messages',
];

View file

@ -0,0 +1,10 @@
<?php
return [
'viewMyProfile' => 'Voir mon profil',
'myTimeline' => 'Ma chronologie',
'publicTimeline' => 'Chronologie publique',
'remoteFollow' => 'Suivre à distance',
'settings' => 'Paramètres',
'admin' => 'Admin',
'logout' => ' Se déconnecter',
];

View file

@ -2,4 +2,6 @@
return [
'likedPhoto' => 'a aimé votre photo.',
'startedFollowingYou' => 'a commencé à vous suivre.',
'commented' => 'commenté sur votre post.',
'mentionedYou' => 'vous à mentionné.'
];

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'Ver perfil',
'myTimeline' => 'A miña liña temporal',
'publicTimeline' => 'Liña temporal pública',
'remoteFollow' => 'Seguimento remoto',
'settings' => 'Axustes',
'admin' => 'Admin',
'logout' => 'Saír',
];

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'Veire mon perfil',
'myTimeline' => 'Ma cronologia',
'publicTimeline' => 'Cronologia publica',
'remoteFollow' => 'Seguir a distància',
'settings' => 'Paramètres',
'admin' => 'Admin',
'logout' => 'Desconnexion',
];

View file

@ -4,5 +4,7 @@ return [
'likedPhoto' => 'a aimat vòstra fòto.',
'startedFollowingYou' => 'a començat de vos seguir.',
'commented' => 'a comentat vòstra publicacion.',
'mentionedYou' => 'vos a mencionat.'
];

View file

@ -1,5 +1,8 @@
<?php
return [
'emptyTimeline' => 'Aqueste utilizaire a pas encara de publicacion !',
'emptyTimeline' => 'Aqueste utilizaire a pas encara de publicacion!',
'emptyFollowers' => 'Aqueste utilizaire a pas encara pas seguidors!',
'emptyFollowing' => 'Aqueste utilizaire sèc degun pel moment!',
'savedWarning' => 'Solament vos vesètz çò que salvagardatz',
];

View file

@ -0,0 +1,7 @@
<?php
return [
'emptyPersonalTimeline' => 'Vòstre cronologia es voida.'
];

View file

@ -0,0 +1,13 @@
<?php
return [
'viewMyProfile' => 'Pokaż mój profil',
'myTimeline' => 'Moja oś czasu',
'publicTimeline' => 'Publiczna oś czasu',
'remoteFollow' => 'Zdalne śledzenie',
'settings' => 'Ustawienia',
'admin' => 'Administrator',
'logout' => 'Wyloguj się',
];

View file

@ -4,5 +4,7 @@ return [
'likedPhoto' => 'polubił Twoje zdjęcie.',
'startedFollowingYou' => 'zaczął Cię obserwować.',
'commented' => 'skomentował Twój wpis',
'mentionedYou' => 'wspomniał o Tobie.'
];

View file

@ -0,0 +1,35 @@
<?php
return [
'exception_message' => 'Mensaxe da exepción: :message',
'exception_trace' => 'Traza da excepción: :trace',
'exception_message_title' => 'Mensaxe da excepción',
'exception_trace_title' => 'Traza da excepción',
'backup_failed_subject' => 'Erro no respaldo de :application_name',
'backup_failed_body' => 'Importante: Algo fallou ao respaldar :application_name',
'backup_successful_subject' => 'Respaldo realizado correctamente :application_name',
'backup_successful_subject_title' => 'Novo respaldo correcto!',
'backup_successful_body' => 'Parabéns, un novo respaldo de :application_name foi realizado correctamente no disco con nome :disk_name.',
'cleanup_failed_subject' => 'Limpando os respaldos de :application_name failed.',
'cleanup_failed_body' => 'Algo fallou mentras se limpaban os respaldos de :application_name',
'cleanup_successful_subject' => 'Limpeza correcta nos respaldos de :application_name',
'cleanup_successful_subject_title' => 'Limpeza dos respaldos correcta!',
'cleanup_successful_body' => 'Realizouse correctamente a limpeza dos respaldos de :application_name no disco con nome :disk_name.',
'healthy_backup_found_subject' => 'Os respaldos de :application_name no disco :disk_name están en bo estado',
'healthy_backup_found_subject_title' => 'Os respaldos de :application_name están ben!',
'healthy_backup_found_body' => 'Os respaldos de :application_name están en bo estado. Bo traballo!',
'unhealthy_backup_found_subject' => 'Importante: Os respaldos de :application_name non están en bo estado',
'unhealthy_backup_found_subject_title' => 'Importante: Os respaldos de :application_name non están ben. :problem',
'unhealthy_backup_found_body' => 'Os respaldos para :application_name no disco :disk_name non están ben.',
'unhealthy_backup_found_not_reachable' => 'Non se puido alcanzar o disco de destino. :error',
'unhealthy_backup_found_empty' => 'Non existen copias de respaldo para esta aplicación.',
'unhealthy_backup_found_old' => 'O último respaldo realizouse en :date e considerase demasiado antigo.',
'unhealthy_backup_found_unknown' => 'Lamentámolo, non se puido determinar unha causa concreta.',
'unhealthy_backup_found_full' => 'Os respaldos están a utilizar demasiado espazo. A utilización actual de :disk_usage é maior que o límite establecido de :disk_limit.',
];

View file

@ -3,16 +3,53 @@
@section('content')
<div class="container notification-page" style="min-height: 60vh;">
<div class="col-12 col-md-8 offset-md-2">
<div class="card mt-3">
<div class="card-body p-0">
<ul class="nav nav-tabs d-flex text-center">
{{--
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase" href="{{route('notifications.following')}}">Following</a>
</li>
--}}
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">My Notifications</a>
</li>
</ul>
</div>
</div>
<div class="">
<div class="dropdown text-right mt-2">
<a class="btn btn-link btn-sm dropdown-toggle font-weight-bold text-dark" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Filter
</a>
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a href="?a=comment" class="dropdown-item font-weight-bold" title="Commented on your post">
Comments only
</a>
<a href="?a=follow" class="dropdown-item font-weight-bold" title="Followed you">
New Followers only
</a>
<a href="?a=mention" class="dropdown-item font-weight-bold" title="Mentioned you">
Mentions only
</a>
<a href="{{route('notifications')}}" class="dropdown-item font-weight-bold text-dark">
View All
</a>
</div>
</div>
</div>
<ul class="list-group">
@if($notifications->count() > 0)
@foreach($notifications as $notification)
<li class="list-group-item notification">
<li class="list-group-item notification border-0">
@switch($notification->action)
@case('like')
<span class="notification-icon pr-3">
<img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
<img src="{{optional($notification->actor, function($actor) {
return $actor->avatarUrl(); }) }}" width="32px" class="rounded-circle">
</span>
<span class="notification-text">
{!! $notification->rendered !!}

View file

@ -0,0 +1,96 @@
@extends('layouts.app')
@section('content')
<div class="container notification-page" style="min-height: 60vh;">
<div class="col-12 col-md-8 offset-md-2">
<div class="card mt-3">
<div class="card-body p-0">
<ul class="nav nav-tabs d-flex text-center">
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications.following')}}">Following</a>
</li>
<li class="nav-item flex-fill">
<a class="nav-link font-weight-bold text-uppercase" href="{{route('notifications')}}">My Notifications</a>
</li>
</ul>
</div>
</div>
<div class="">
{{-- <div class="card-header bg-white">
<span class="font-weight-bold lead">Notifications</span>
<span class="small float-right font-weight-bold">
<a href="?a=comment" class="pr-4 text-muted" title="Commented on your post"><i class="fas fa-comment fa-2x"></i></a>
<a href="?a=follow" class="pr-4 text-muted" title="Followed you"><i class="fas fa-user-plus fa-2x"></i></a>
<a href="?a=mention" class="pr-4 text-muted" title="Mentioned you"><i class="fas fa-comment-dots fa-2x"></i></a>
<a href="{{route('notifications')}}" class="font-weight-bold text-dark">View All</a>
</span>
</div> --}}
</div>
<ul class="list-group">
@if($notifications->count() > 0)
@foreach($notifications as $notification)
@php
if(!in_array($notification->action, ['like', 'follow'])) {
continue;
}
@endphp
<li class="list-group-item notification border-0">
@switch($notification->action)
@case('like')
<span class="notification-icon pr-3">
<img src="{{optional($notification->actor, function($actor) {
return $actor->avatarUrl(); }) }}" width="32px" class="rounded-circle">
</span>
<span class="notification-text">
<a class="font-weight-bold text-dark" href="{{$notification->actor->url()}}">{{$notification->actor->username}}</a>
{{__('liked a post by')}}
<a class="font-weight-bold text-dark" href="{{$notification->item->profile->url()}}">{{$notification->item->profile->username}}</a>
<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 && $notification->item_type == 'App\Status')
<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">
</span>
<span class="notification-text">
<a class="font-weight-bold text-dark" href="{{$notification->actor->url()}}">{{$notification->actor->username}}</a>
{{__('started following')}}
<a class="font-weight-bold text-dark" href="{{$notification->item->url()}}">{{$notification->item->username}}</a>
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
</span>
@break
@endswitch
</li>
@endforeach
</ul>
<div class="d-flex justify-content-center my-4">
{{$notifications->links()}}
</div>
@else
<div class="mt-4">
<div class="alert alert-info font-weight-bold">No unread notifications found.</div>
</div>
@endif
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/activity.js')}}"></script>
@endpush

View file

@ -26,16 +26,16 @@
<link rel="self" type="application/atom+xml" href="{{$profile->permalink('.atom')}}"/>
@foreach($items as $item)
<entry>
<title><![CDATA[{{ $item->caption }}]]></title>
<title>{{ $item->caption }}</title>
<link rel="alternate" href="{{ $item->url() }}" />
<id>{{ url($item->id) }}</id>
<author>
<name> <![CDATA[{{ $item->profile->username }}]]></name>
</author>
<summary type="html">
<![CDATA[{!! $item->caption !!}]]>
{{ $item->caption }}
</summary>
<updated>{{ $item->updated_at->toAtomString() }}</updated>
</entry>
@endforeach
</feed>
</feed>

View file

@ -4,8 +4,9 @@
<div class="container">
<div class="error-page py-5 my-5">
<div class="card mx-5">
<div class="card-body p-5">
<div class="card-body p-5 text-center">
<h1 class="text-center">404 Page Not Found</h1>
<img src="/img/fred1.gif" class="img-fluid">
</div>
</div>
</div>

View file

@ -7,15 +7,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="robots" content="noimageindex, noarchive">
<meta name="mobile-web-app-capable" content="yes">
<title>{{ $title or config('app.name', 'Laravel') }}</title>
<meta property="og:site_name" content="{{ config('app.name', 'Laravel') }}">
<meta property="og:title" content="{{ $title or config('app.name', 'Laravel') }}">
<title>{{ $title ?? config('app.name', 'Laravel') }}</title>
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title or config('app.name', 'pixelfed') }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{request()->url()}}">
@stack('meta')
<meta name="medium" content="image">
@ -34,5 +33,14 @@
@include('layouts.partial.footer')
<script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
@stack('scripts')
@if(Auth::check())
<div class="modal" tabindex="-1" role="dialog" id="composeModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
@include('timeline.partial.new-form')
</div>
</div>
</div>
@endif
</body>
</html>

View file

@ -1,8 +1,8 @@
<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') }}" title="Logo">
<a class="navbar-brand d-flex align-items-center" href="{{ route('timeline.personal') }}" title="Logo">
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2">
<span class="font-weight-bold mb-0" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span>
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
@ -16,19 +16,26 @@
<ul class="navbar-nav ml-auto">
@guest
<li><a class="nav-link font-weight-bold text-primary" href="{{ route('login') }}">{{ __('Login') }}</a></li>
<li><a class="nav-link font-weight-bold" href="{{ route('register') }}">{{ __('Register') }}</a></li>
<li><a class="nav-link font-weight-bold text-primary" href="{{ route('login') }}" title="Login">{{ __('Login') }}</a></li>
<li><a class="nav-link font-weight-bold" href="{{ route('register') }}" title="Register">{{ __('Register') }}</a></li>
@else
<li class="nav-item px-2">
<a class="nav-link" href="{{route('discover')}}" title="Discover"><i class="far fa-compass fa-lg"></i></a>
<a class="nav-link" href="{{route('discover')}}" title="Discover" data-toggle="tooltip" data-placement="bottom"><i class="far fa-compass fa-lg"></i></a>
</li>
<li class="nav-item px-2">
<a class="nav-link" href="{{route('notifications')}}" title="Notifications">
<i class="far fa-heart fa-lg"></i>
<a class="nav-link nav-notification" href="{{route('notifications')}}" title="Notifications" data-toggle="tooltip" data-placement="bottom">
<i class="far fa-heart fa-lg text"></i>
</a>
</li>
<li class="nav-item px-2">
<div title="Create new post" data-toggle="tooltip" data-placement="bottom">
<a href="{{route('compose')}}" class="nav-link" data-toggle="modal" data-target="#composeModal">
<i class="far fa-plus-square fa-lg text-primary"></i>
</a>
</div>
</li>
<li class="nav-item dropdown px-2">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre title="User Menu">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="User Menu" data-toggle="tooltip" data-placement="bottom">
<i class="far fa-user fa-lg"></i> <span class="caret"></span>
</a>
@ -47,6 +54,10 @@
<span class="far fa-list-alt pr-1"></span>
{{__('navmenu.publicTimeline')}}
</a>
{{-- <a class="dropdown-item font-weight-bold" href="{{route('messages')}}">
<span class="far fa-envelope pr-1"></span>
{{__('navmenu.directMessages')}}
</a> --}}
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="{{route('remotefollow')}}">
<span class="fas fa-user-plus pr-1"></span>

View file

@ -62,4 +62,5 @@
@push('meta')
<meta property="og:description" content="{{$profile->bio}}">
<meta property="og:image" content="{{$profile->avatarUrl()}}">
<meta name="robots" content="NOINDEX, NOFOLLOW">
@endpush

View file

@ -62,4 +62,5 @@
@push('meta')
<meta property="og:description" content="{{$profile->bio}}">
<meta property="og:image" content="{{$profile->avatarUrl()}}">
<meta name="robots" content="NOINDEX, NOFOLLOW">
@endpush

View file

@ -3,42 +3,11 @@
<li class="nav-item pl-3 {{request()->is('settings/home')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings')}}">Profile</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/avatar')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.avatar')}}">Avatar</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/password')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.password')}}">Password</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/email')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.email')}}">Email</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/notifications')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.notifications')}}">Notifications</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/privacy')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.privacy')}}">Privacy</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/security')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.security')}}">Security</a>
</li>
<li class="nav-item">
<hr>
</li>
<li class="nav-item pl-3 {{request()->is('settings/import*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.import')}}">Import</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/data-export')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.dataexport')}}">Export</a>
</li>
</li>
<li class="nav-item">
<hr>
</li>
<li class="nav-item pl-3 {{request()->is('settings/applications')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.applications')}}">Applications</a>
</li>
<li class="nav-item pl-3 {{request()->is('settings/developers')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.developers')}}">Developers</a>
</li>
</ul>
</div>

View file

@ -66,13 +66,18 @@
<p class="lead">Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.</p>
<p class="lead">When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.</p>
<h4 class="font-weight-bold">Childrens Online Privacy Protection Act Compliance</h4>
<p class="lead">Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Childrens Online Privacy Protection Act) do not use this site.</p>
<h4 class="font-weight-bold">Site usage by children</h4>
<p class="lead">If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site.</p>
<p class="lead">If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.</p>
<p class="lead">Law requirements can be different if this server is in another jurisdiction.</p>
<h4 class="font-weight-bold">Changes to our Privacy Policy</h4>
<p class="lead">If we decide to change our privacy policy, we will post those changes on this page.</p>
<p class="lead">This document is CC-BY-SA. It was last updated May 31, 2018.</p>
<p class="lead">This document is CC-BY-SA. It was last updated Jun 12, 2018.</p>
<p class="lead">Originally adapted from the <a href="https://mastodon.social/terms">Mastodon</a> privacy policy.</p>
</div>

View file

@ -0,0 +1,37 @@
@extends('layouts.app')
@section('content')
<div class="container px-0 mt-md-4">
<div class="col-12 col-md-8 offset-md-2">
<div class="card">
<div class="card-body">
<p class="mb-0">
<img class="img-thumbnail mr-2" src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
<span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$status->profile->url()}}">{{ str_limit($status->profile->username, 15)}}</a></bdi></span>
<span class="comment-text">{!! $status->rendered ?? e($status->caption) !!} <a href="{{$status->url()}}" class="text-dark small font-weight-bold float-right pl-2">{{$status->created_at->diffForHumans(null, true, true ,true)}}</a></span>
</p>
<hr>
<div class="comments">
@foreach($replies as $item)
<p class="mb-2">
<span class="font-weight-bold pr-1">
<img class="img-thumbnail mr-2" src="{{$item->profile->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
<bdi><a class="text-dark" href="{{$item->profile->url()}}">{{ str_limit($item->profile->username, 15)}}</a></bdi>
</span>
<span class="comment-text">
{!! $item->rendered ?? e($item->caption) !!}
<a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right pl-2">
{{$item->created_at->diffForHumans(null, true, true ,true)}}
</a>
</span>
</p>
@endforeach
</div>
</div>
</div>
</div>
</div>
@endsection

View file

@ -18,12 +18,13 @@
<div class="col-12 col-md-8 status-photo px-0">
@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>
<summary>
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<a class="max-hide-overflow {{$status->firstMedia()->filter_class}}" href="{{$status->url()}}">
<img class="card-img-top" src="{{$status->mediaUrl()}}">
</a>
</details>
@elseif(!$status->is_nsfw && $status->media_count == 1)
<div class="{{$status->firstMedia()->filter_class}}">
@ -68,19 +69,43 @@
<a href="{{$user->url()}}" class="username-link font-weight-bold text-dark">{{$user->username}}</a>
</div>
</div>
<div class="float-right">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item font-weight-bold" href="{{$status->reportUrl()}}">Report</a>
{{-- <a class="dropdown-item" href="#">Embed</a> --}}
@if(Auth::check())
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
{{-- <a class="dropdown-item" href="{{$status->editUrl()}}">Edit</a> --}}
<form method="post" action="/i/delete">
@csrf
<input type="hidden" name="type" value="post">
<input type="hidden" name="item" value="{{$status->id}}">
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Delete</button>
</form>
@endif
@endif
</div>
</div>
</div>
</div>
<div class="d-flex flex-md-column flex-column-reverse h-100">
<div class="card-body status-comments">
<div class="status-comment">
<p class="mb-1">
<span class="font-weight-bold pr-1">{{$status->profile->username}}</span>
<span class="comment-text">{!! $status->rendered ?? e($status->caption) !!}</span>
<span class="comment-text" v-pre>{!! $status->rendered ?? e($status->caption) !!}</span>
</p>
<p class="mb-1"><a href="{{$status->url()}}/c" class="text-muted">View all comments</a></p>
<div class="comments">
@foreach($status->comments->reverse()->take(10) as $item)
<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="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>
@foreach($replies as $item)
<p class="mb-1">
<span class="font-weight-bold pr-1"><bdi><a class="text-dark" href="{{$item->profile->url()}}">{{ str_limit($item->profile->username, 15)}}</a></bdi></span>
<span class="comment-text" v-pre>{!! $item->rendered ?? e($item->caption) !!} <a href="{{$item->url()}}" class="text-dark small font-weight-bold float-right pl-2">{{$item->created_at->diffForHumans(null, true, true ,true)}}</a></span>
</p>
@endforeach
</div>
@ -88,32 +113,30 @@
</div>
<div class="card-body flex-grow-0 py-1">
<div class="reactions my-1">
<form class="d-inline-flex like-form pr-3" method="post" action="/i/like" style="display: inline;" data-id="{{$status->id}}" data-action="like">
@if(Auth::check())
<form class="d-inline-flex 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 border-0" type="submit" title="Like!">
<h3 class="far fa-heart m-0"></h3>
<h3 class="m-0 {{$status->liked() ? 'fas fa-heart text-danger':'far fa-heart text-dark'}}"></h3>
</button>
</form>
<h3 class="far fa-comment pr-3 m-0" title="Comment"></h3>
@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">
<form class="d-inline-flex share-form pr-3" method="post" action="/i/share" style="display: inline;" data-id="{{$status->id}}" data-action="share" data-count="{{$status->shares_count}}">
@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 border-0" title="Remove">
<h3 class="far fa-trash-alt m-0"></h3>
<button class="btn btn-link text-dark p-0" type="submit" title="Share">
<h3 class="m-0 {{$status->shared() ? 'fas fa-share-square text-primary':'far fa-share-square '}}"></h3>
</button>
</form>
@endif
@endif
<span class="float-right">
<form class="d-inline-flex bookmark-form" method="post" action="/i/bookmark" style="display: inline;" data-id="{{$status->id}}" data-action="bookmark">
<form class="d-inline-flex " 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 border-0" type="submit" title="Save">
<h3 class="far fa-bookmark m-0"></h3>
<h3 class="m-0 {{$status->bookmarked() ? 'fas fa-bookmark text-warning':'far fa-bookmark'}}"></h3>
</button>
</form>
</span>
@ -132,7 +155,8 @@
<form class="comment-form" method="post" action="/i/comment" data-id="{{$status->id}}" data-truncate="false">
@csrf
<input type="hidden" name="item" value="{{$status->id}}">
<input class="form-control" name="comment" placeholder="Add a comment...">
<input class="form-control" name="comment" placeholder="Add a comment..." autocomplete="off">
</form>
</div>
</div>
@ -144,5 +168,5 @@
@push('meta')
<meta property="og:description" content="{{ $status->caption }}">
<meta property="og:image" content="{{$status->mediaUrl()}}">
<meta property="og:image" content="{{$status->mediaUrl()}}">
@endpush

View file

@ -1,111 +1,102 @@
<div class="card my-4 status-card card-md-rounded-0">
<div class="card-header d-inline-flex align-items-center bg-white">
<img src="{{$item->profile->avatarUrl()}}" width="32px" height="32px" style="border-radius: 32px;">
<a class="username font-weight-bold pl-2 text-dark" href="{{$item->profile->url()}}">
{{$item->profile->username}}
</a>
<div class="text-right" style="flex-grow:1;">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="{{$item->url()}}">Go to post</a>
<a class="dropdown-item" href="{{route('report.form')}}?type=post&id={{$item->id}}">Report Inappropriate</a>
<a class="dropdown-item" href="#">Embed</a>
@if(Auth::check())
@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">
@csrf
<input type="hidden" name="type" value="post">
<input type="hidden" name="item" value="{{$item->id}}">
<button type="submit" class="dropdown-item btn btn-link">Delete</button>
</form>
@endif
@endif
</div>
</div>
</div>
</div>
@if($item->is_nsfw)
<details class="details-animated">
<p>
<summary>NSFW / Hidden Image</summary>
<a class="max-hide-overflow {{$item->firstMedia()->filter_class}}" href="{{$item->url()}}">
<img class="card-img-top" src="{{$item->mediaUrl()}}">
</a>
</p>
</details>
@else
<a class="max-hide-overflow {{$item->firstMedia()->filter_class}}" href="{{$item->url()}}">
<img class="card-img-top" src="{{$item->mediaUrl()}}">
</a>
@endif
<div class="card-body">
<div class="reactions my-1">
<form class="d-inline-flex 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" title=""Like!>
<h3 class="far fa-heart status-heart m-0"></h3>
</button>
</form>
<h3 class="far fa-comment status-comment-focus" title="Comment"></h3>
<span class="float-right">
<form class="d-inline-flex 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 border-0" type="submit" title="Save">
<h3 class="far fa-bookmark m-0"></h3>
</button>
</form>
</span>
</div>
<div class="likes font-weight-bold">
<span class="like-count">{{$item->likes_count}}</span> likes
</div>
<div class="caption">
<p class="mb-1">
<span class="username font-weight-bold">
<bdi><a class="text-dark" href="{{$item->profile->url()}}">{{$item->profile->username}}</a></bdi>
</span>
<span>{!! $item->rendered ?? e($item->caption) !!}</span>
</p>
</div>
@if($item->comments()->count() > 3)
<div class="more-comments">
<a class="text-muted" href="{{$item->url()}}">Load more comments</a>
</div>
@endif
<div class="comments">
@if(isset($showSingleComment) && $showSingleComment === true)
<p class="mb-0">
<span class="font-weight-bold pr-1">
<bdi>
<a class="text-dark" href="{{$status->profile->url()}}">{{$status->profile->username}}</a>
</bdi>
</span>
<span class="comment-text">{!! $item->rendered ?? e($item->caption) !!}</span>
<span class="float-right">
<a href="{{$status->url()}}" class="text-dark small font-weight-bold">
{{$status->created_at->diffForHumans(null, true, true, true)}}
</a>
</span>
</p>
@else
@endif
</div>
<div class="timestamp pt-1">
<p class="small text-uppercase mb-0"><a href="{{$item->url()}}" class="text-muted">{{$item->created_at->diffForHumans()}}</a></p>
</div>
</div>
<div class="card-footer bg-white">
<form class="comment-form" method="post" action="/i/comment" data-id="{{$item->id}}" data-truncate="true">
<div class="card mb-4 status-card card-md-rounded-0" data-id="{{$item->id}}" data-comment-max-id="0">
<div class="card-header d-inline-flex align-items-center bg-white">
<img src="{{$item->profile->avatarUrl()}}" width="32px" height="32px" style="border-radius: 32px;">
<a class="username font-weight-bold pl-2 text-dark" href="{{$item->profile->url()}}">
{{$item->profile->username}}
</a>
<div class="text-right" style="flex-grow:1;">
<div class="dropdown">
<button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item font-weight-bold" href="{{$item->url()}}">Go to post</a>
<a class="dropdown-item font-weight-bold" href="{{route('report.form')}}?type=post&id={{$item->id}}">Report</a>
<a class="dropdown-item font-weight-bold" href="#">Embed</a>
@if(Auth::check())
@if(Auth::user()->profile->id === $item->profile->id || Auth::user()->is_admin == true)
<a class="dropdown-item font-weight-bold" href="{{$item->editUrl()}}">Edit</a>
<form method="post" action="/i/delete">
@csrf
<input type="hidden" name="type" value="post">
<input type="hidden" name="item" value="{{$item->id}}">
<input class="form-control status-reply-input" name="comment" placeholder="Add a comment…">
<button type="submit" class="dropdown-item btn btn-link text-danger font-weight-bold">Delete</button>
</form>
@endif
@endif
</div>
</div>
</div>
</div>
@if($item->is_nsfw)
<details class="details-animated">
<summary>
<p class="mb-0 px-3 lead font-weight-bold">Content Warning: This may contain potentially sensitive content.</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<a class="max-hide-overflow {{$item->firstMedia()->filter_class}}" href="{{$item->url()}}">
<img class="card-img-top lazy" src="data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" data-src="{{$item->mediaUrl()}}" data-srcset="{{$item->mediaUrl()}} 1x">
</a>
</details>
@else
<a class="max-hide-overflow {{$item->firstMedia()->filter_class}}" href="{{$item->url()}}">
@if($loop->index < 2)
<img class="card-img-top" src="{{$item->mediaUrl()}}" data-srcset="{{$item->mediaUrl()}} 1x">
@else
<img class="card-img-top lazy" src="data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" data-src="{{$item->mediaUrl()}}" data-srcset="{{$item->mediaUrl()}} 1x">
@endif
</a>
@endif
<div class="card-body">
<div class="reactions my-1">
<form class="d-inline-flex 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" title="Like!">
<h3 class="far fa-heart status-heart m-0"></h3>
</button>
</form>
<h3 class="far fa-comment pr-3 status-comment-focus" title="Comment"></h3>
<form class="d-inline-flex share-form pr-3" method="post" action="/i/share" style="display: inline;" data-id="{{$item->id}}" data-action="share" data-count="{{$item->shares_count}}">
@csrf
<input type="hidden" name="item" value="{{$item->id}}">
<button class="btn btn-link text-dark p-0" type="submit" title="Share">
<h3 class="far fa-share-square m-0"></h3>
</button>
</form>
<span class="float-right">
<form class="d-inline-flex 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 border-0" type="submit" title="Save">
<h3 class="far fa-bookmark m-0"></h3>
</button>
</form>
</span>
</div>
<div class="likes font-weight-bold">
<span class="like-count">{{$item->likes_count}}</span> likes
</div>
<div class="caption">
<p class="mb-1">
<span class="username font-weight-bold">
<bdi><a class="text-dark" href="{{$item->profile->url()}}">{{$item->profile->username}}</a></bdi>
</span>
<span>{!! $item->rendered ?? e($item->caption) !!}</span>
</p>
</div>
<div class="comments">
</div>
<div class="timestamp pt-1">
<p class="small text-uppercase mb-0"><a href="{{$item->url()}}" class="text-muted">{{$item->created_at->diffForHumans()}}</a></p>
</div>
</div>
<div class="card-footer bg-white">
<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 status-reply-input" name="comment" placeholder="Add a comment…" autocomplete="off">
</form>
</div>
</div>

View file

@ -1,35 +1,35 @@
<div class="card card-md-rounded-0">
<div class="card-header font-weight-bold">New Post</div>
<div class="card-header bg-white font-weight-bold">
<div>{{__('Create New Post')}}</div>
</div>
<div class="card-body" id="statusForm">
@if (session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<form method="post" action="/timeline" enctype="multipart/form-data">
<form method="post" action="{{route('timeline.personal')}}" enctype="multipart/form-data">
@csrf
<input type="hidden" name="filter_name" value="">
<input type="hidden" name="filter_class" value="">
<div class="form-group">
<label class="font-weight-bold text-muted small">Upload Image</label>
<input type="file" class="form-control-file" id="fileInput" name="photo[]" accept="image/*" multiple="">
<div class="custom-file">
<input type="file" class="custom-file-input" id="fileInput" name="photo[]" accept="image/*" multiple="">
<label class="custom-file-label" for="fileInput">Upload Image(s)</label>
</div>
<small class="form-text text-muted">
Max Size: @maxFileSize(). Supported formats: jpeg, png, gif, bmp. Limited to {{config('pixelfed.max_album_length')}} photos per post.
</small>
</div>
<div class="form-group">
<label class="font-weight-bold text-muted small">Caption</label>
<input type="text" class="form-control" name="caption" placeholder="Add a caption here" autocomplete="off">
<small class="form-text text-muted">
Max length: {{config('pixelfed.max_caption_length')}} characters.
</small>
<textarea class="form-control" name="caption" placeholder="Add a caption here" autocomplete="off" data-limit="{{config('pixelfed.max_caption_length')}}" rows="1"></textarea>
<p class="form-text text-muted small text-right">
<span class="caption-counter">0</span>
<span>/</span>
<span>{{config('pixelfed.max_caption_length')}}</span>
</p>
</div>
<div class="form-group">
<button class="btn btn-primary btn-sm px-3 py-1 font-weight-bold" type="button" data-toggle="collapse" data-target="#collapsePreview" aria-expanded="false" aria-controls="collapsePreview">
Options
<button class="btn btn-outline-primary btn-sm px-3 py-1 font-weight-bold" type="button" data-toggle="collapse" data-target="#collapsePreview" aria-expanded="false" aria-controls="collapsePreview">
Options &nbsp; <i class="fas fa-chevron-down"></i>
</button>
<div class="collapse" id="collapsePreview">
<div class="form-group pt-3">
<label class="font-weight-bold text-muted small">CW/NSFW</label>
<div class="switch switch-sm">
@ -41,17 +41,6 @@
</small>
</div>
{{-- <div class="form-group">
<label class="font-weight-bold text-muted small">Visibility</label>
<div class="switch switch-sm">
<input type="checkbox" class="switch" id="visibility-switch" name="visibility">
<label for="visibility-switch" class="small font-weight-bold">Public | Followers-only</label>
</div>
<small class="form-text text-muted">
Toggle this to limit this post to your followers only.
</small>
</div> --}}
<div class="form-group d-none form-preview">
<label class="font-weight-bold text-muted small">Photo Preview</label>
<figure class="filterContainer">
@ -69,7 +58,7 @@
</div>
</div>
</div>
<button type="submit" class="btn btn-outline-primary btn-block">Post</button>
<button type="submit" class="btn btn-outline-primary btn-block font-weight-bold">Create Post</button>
</form>
</div>
</div>

View file

@ -0,0 +1,115 @@
@extends('layouts.app')
@section('content')
<noscript>
<div class="container">
<div class="card border-left-primary mt-5">
<div class="card-body">
<p class="mb-0 font-weight-bold">Javascript is required for an optimized experience, please enable it or use the <a href="#">lite</a> version.</p>
</div>
</div>
</div>
</noscript>
<div class="container p-0 d-none timeline-container">
<div class="row">
<div class="col-md-10 col-lg-8 mx-auto pt-4 px-0 my-3 pr-2">
@if (session('status'))
<div class="alert alert-success">
<span class="font-weight-bold">{!! session('status') !!}</span>
</div>
@endif
@if (session('error'))
<div class="alert alert-danger">
<span class="font-weight-bold">{!! session('error') !!}</span>
</div>
@endif
<div class="timeline-feed" data-timeline="{{$type}}">
@foreach($timeline as $item)
@if(is_null($item->in_reply_to_id))
@include('status.template')
@endif
@endforeach
@if($timeline->count() == 0)
<div class="card card-md-rounded-0">
<div class="card-body py-5">
<div class="d-flex justify-content-center align-items-center">
<p class="lead font-weight-bold mb-0">{{ __('timeline.emptyPersonalTimeline') }}</p>
</div>
</div>
</div>
@endif
</div>
<div class="page-load-status" style="display: none;">
<div class="infinite-scroll-request" style="display: none;">
<div class="fixed-top loading-page"></div>
</div>
<div class="infinite-scroll-last" style="display: none;">
<h3>No more content</h3>
<p class="text-muted">
Maybe you could try
<a href="{{route('discover')}}">discovering</a>
more people you can follow.
</p>
</div>
<div class="infinite-scroll-error" style="display: none;">
<h3>Whoops, an error</h3>
<p class="text-muted">
Try reloading the page
</p>
</div>
</div>
<div class="d-flex justify-content-center">
{{$timeline->links()}}
</div>
</div>
<div class="col-md-2 col-lg-4 pt-4 my-3">
<div class="media d-flex align-items-center mb-4">
<a href="{{Auth::user()->profile->url()}}">
<img class="mr-3 rounded-circle box-shadow" src="{{Auth::user()->profile->avatarUrl()}}" alt="{{Auth::user()->username}}'s avatar" width="64px">
</a>
<div class="media-body">
<p class="mb-0 px-0 font-weight-bold"><a href="{{Auth::user()->profile->url()}}">&commat;{{Auth::user()->username}}</a></p>
<p class="mb-0 small text-muted">{{Auth::user()->name}}</p>
</div>
</div>
<follow-suggestions></follow-suggestions>
<footer>
<div class="container pb-5">
<p class="mb-0 text-uppercase font-weight-bold text-muted small">
<a href="{{route('site.about')}}" class="text-dark pr-2">About Us</a>
<a href="{{route('site.help')}}" class="text-dark pr-2">Support</a>
<a href="{{route('site.opensource')}}" class="text-dark pr-2">Open Source</a>
<a href="{{route('site.language')}}" class="text-dark pr-2">Language</a>
<a href="{{route('site.terms')}}" class="text-dark pr-2">Terms</a>
<a href="{{route('site.privacy')}}" class="text-dark pr-2">Privacy</a>
<a href="{{route('site.platform')}}" class="text-dark pr-2">API</a>
<a href="#" class="text-dark pr-2">Directory</a>
<a href="#" class="text-dark pr-2">Profiles</a>
<a href="#" class="text-dark">Hashtags</a>
</p>
<p class="mb-0 text-uppercase font-weight-bold text-muted small">
<a href="http://pixelfed.org" class="text-muted" rel="noopener">Powered by PixelFed</a>
</p>
</div>
</footer>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/timeline.js')}}"></script>
@endpush

View file

@ -1,6 +1,6 @@
<?php
Route::domain(config('pixelfed.domain.admin'))->group(function() {
Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(function() {
Route::redirect('/', '/dashboard');
Route::redirect('timeline', config('app.url').'/timeline');
Route::get('dashboard', 'AdminController@home')->name('admin.home');
@ -15,7 +15,8 @@ Route::domain(config('pixelfed.domain.admin'))->group(function() {
Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(function() {
Route::view('/', 'welcome');
Route::get('/', 'SiteController@home')->name('timeline.personal');
Route::post('/', 'StatusController@store');
Auth::routes();
@ -35,18 +36,27 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
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::group(['prefix' => 'v1'], function() {
Route::get('likes', 'ApiController@hydrateLikes');
});
Route::group(['prefix' => 'local'], function() {
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
Route::post('i/more-comments', 'ApiController@loadMoreComments');
});
});
Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');
Route::group(['prefix' => 'i'], function() {
Route::redirect('/', '/');
Route::get('compose', 'StatusController@compose')->name('compose');
Route::get('remote-follow', 'FederationController@remoteFollow')->name('remotefollow');
Route::post('remote-follow', 'FederationController@remoteFollowStore');
Route::post('comment', 'CommentController@store');
Route::post('delete', 'StatusController@delete');
Route::post('like', 'LikeController@store');
Route::post('share', 'StatusController@storeShare');
Route::post('follow', 'FollowerController@store');
Route::post('bookmark', 'BookmarkController@store');
Route::get('lang/{locale}', 'SiteController@changeLocale');
@ -62,6 +72,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
Route::get('spam/post', 'ReportController@spamPostForm')->name('report.spam.post');
Route::get('spam/profile', 'ReportController@spamProfileForm')->name('report.spam.profile');
});
});
Route::group(['prefix' => 'account'], function() {
@ -83,14 +94,25 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
Route::get('security', 'SettingsController@security')->name('settings.security');
Route::get('applications', 'SettingsController@applications')->name('settings.applications');
Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport');
Route::get('import', 'SettingsController@dataImport')->name('settings.import');
Route::get('import/instagram', 'SettingsController@dataImportInstagram')->name('settings.import.ig');
Route::get('developers', 'SettingsController@developers')->name('settings.developers');
});
Route::group(['prefix' => 'site'], function() {
Route::redirect('/', '/');
Route::get('about', 'SiteController@about')->name('site.about');
Route::view('help', 'site.help')->name('site.help');
Route::view('developer-api', 'site.developer')->name('site.developers');
Route::view('fediverse', 'site.fediverse')->name('site.fediverse');
Route::view('open-source', 'site.opensource')->name('site.opensource');
Route::view('banned-instances', 'site.bannedinstances')->name('site.bannedinstances');
Route::view('terms', 'site.terms')->name('site.terms');
Route::view('privacy', 'site.privacy')->name('site.privacy');
Route::view('platform', 'site.platform')->name('site.platform');
Route::view('language', 'site.language')->name('site.language');
});
Route::group(['prefix' => 'timeline'], function() {
Route::get('/', 'TimelineController@personal')->name('timeline.personal');
Route::post('/', 'StatusController@store');
Route::redirect('/', '/');
Route::get('public', 'TimelineController@local')->name('timeline.public');
Route::post('public', 'StatusController@store');
});
@ -100,26 +122,12 @@ Route::domain(config('pixelfed.domain.app'))->middleware('validemail')->group(fu
Route::get('{user}.atom', 'ProfileController@showAtomFeed');
Route::get('{username}/outbox', 'FederationController@userOutbox');
Route::get('{user}', function($user) {
return redirect('/@'.$user);
return redirect('/'.$user);
});
});
Route::group(['prefix' => 'site'], function() {
Route::redirect('/', '/');
Route::view('about', 'site.about')->name('site.about');
Route::view('features', 'site.features')->name('site.features');
Route::view('help', 'site.help')->name('site.help');
Route::view('fediverse', 'site.fediverse')->name('site.fediverse');
Route::view('open-source', 'site.opensource')->name('site.opensource');
Route::view('banned-instances', 'site.bannedinstances')->name('site.bannedinstances');
Route::view('terms', 'site.terms')->name('site.terms');
Route::view('privacy', 'site.privacy')->name('site.privacy');
Route::view('platform', 'site.platform')->name('site.platform');
Route::view('libraries', 'site.libraries')->name('site.libraries');
Route::view('language', 'site.language')->name('site.language');
});
Route::get('p/{username}/{id}/c/{cid}', 'CommentController@show');
Route::get('p/{username}/{id}/c', 'CommentController@showAll');
Route::get('p/{username}/{id}', 'StatusController@show');
Route::get('{username}/saved', 'ProfileController@savedBookmarks');
Route::get('{username}/followers', 'ProfileController@followers');