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

Follow Hashtags
This commit is contained in:
daniel 2019-07-08 22:44:43 -06:00 committed by GitHub
commit da01872796
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 886 additions and 969 deletions

View file

@ -0,0 +1,109 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use DB;
use App\{
Hashtag,
Status,
StatusHashtag
};
class FixHashtags extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'fix:hashtags';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fix Hashtags';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info(' ____ _ ______ __ ');
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
$this->info(' ');
$this->info(' ');
$this->info('Pixelfed version: ' . config('pixelfed.version'));
$this->info(' ');
$this->info('Running Fix Hashtags command');
$this->info(' ');
$missingCount = StatusHashtag::doesntHave('profile')->doesntHave('status')->count();
if($missingCount > 0) {
$this->info("Found {$missingCount} orphaned StatusHashtag records to delete ...");
$this->info(' ');
$bar = $this->output->createProgressBar($missingCount);
$bar->start();
foreach(StatusHashtag::doesntHave('profile')->doesntHave('status')->get() as $tag) {
$tag->delete();
$bar->advance();
}
$bar->finish();
$this->info(' ');
} else {
$this->info(' ');
$this->info('Found no orphaned hashtags to delete!');
}
$this->info(' ');
$count = StatusHashtag::whereNull('status_visibility')->count();
if($count > 0) {
$this->info("Found {$count} hashtags to fix ...");
$this->info(' ');
} else {
$this->info('Found no hashtags to fix!');
$this->info(' ');
return;
}
$bar = $this->output->createProgressBar($count);
$bar->start();
StatusHashtag::with('status')
->whereNull('status_visibility')
->chunk(50, function($tags) use($bar) {
foreach($tags as $tag) {
if(!$tag->status || !$tag->status->scope) {
continue;
}
$tag->status_visibility = $tag->status->scope;
$tag->save();
$bar->advance();
}
});
$bar->finish();
$this->info(' ');
$this->info(' ');
}
}

19
app/HashtagFollow.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class HashtagFollow extends Model
{
protected $fillable = [
'user_id',
'profile_id',
'hashtag_id'
];
public function hashtag()
{
return $this->belongsTo(Hashtag::class);
}
}

View file

@ -59,14 +59,11 @@ class BaseApiController extends Controller
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
} else { } else {
$this->validate($request, [ $this->validate($request, [
'page' => 'nullable|integer|min:1', 'page' => 'nullable|integer|min:1|max:10',
'limit' => 'nullable|integer|min:1|max:10' 'limit' => 'nullable|integer|min:1|max:10'
]); ]);
$limit = $request->input('limit') ?? 10; $limit = $request->input('limit') ?? 10;
$page = $request->input('page') ?? 1; $page = $request->input('page') ?? 1;
if($page > 3) {
return response()->json([]);
}
$end = (int) $page * $limit; $end = (int) $page * $limit;
$start = (int) $end - $limit; $start = (int) $end - $limit;
$res = NotificationService::get($pid, $start, $end); $res = NotificationService::get($pid, $start, $end);

View file

@ -6,6 +6,7 @@ use App\{
DiscoverCategory, DiscoverCategory,
Follower, Follower,
Hashtag, Hashtag,
HashtagFollow,
Profile, Profile,
Status, Status,
StatusHashtag, StatusHashtag,
@ -17,6 +18,7 @@ use App\Transformer\Api\StatusStatelessTransformer;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\StatusHashtagService;
class DiscoverController extends Controller class DiscoverController extends Controller
{ {
@ -36,57 +38,11 @@ class DiscoverController extends Controller
public function showTags(Request $request, $hashtag) public function showTags(Request $request, $hashtag)
{ {
abort_if(!Auth::check(), 403); abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
$tag = Hashtag::whereSlug($hashtag) $tag = Hashtag::whereSlug($hashtag)->firstOrFail();
->firstOrFail(); $tagCount = StatusHashtagService::count($tag->id);
return view('discover.tags.show', compact('tag', 'tagCount'));
$page = 1;
$key = 'discover:tag-'.$tag->id.':page-'.$page;
$keyMinutes = 15;
$posts = Cache::remember($key, now()->addMinutes($keyMinutes), function() use ($tag, $request) {
$tags = StatusHashtag::select('status_id')
->whereHashtagId($tag->id)
->orderByDesc('id')
->take(48)
->pluck('status_id');
return Status::select(
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'scope',
'local',
'created_at',
'updated_at'
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album'])
->with('media')
->whereLocal(true)
->whereNull('uri')
->whereIn('id', $tags)
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereNull('url')
->whereNull('uri')
->withCount(['likes', 'comments'])
->whereIsNsfw(false)
->whereVisibility('public')
->orderBy('id', 'desc')
->get();
});
if($posts->count() == 0) {
abort(404);
}
return view('discover.tags.show', compact('tag', 'posts'));
} }
public function showCategory(Request $request, $slug) public function showCategory(Request $request, $slug)
@ -156,7 +112,6 @@ class DiscoverController extends Controller
return $res; return $res;
} }
public function loopWatch(Request $request) public function loopWatch(Request $request)
{ {
abort_if(!Auth::check(), 403); abort_if(!Auth::check(), 403);
@ -171,4 +126,26 @@ class DiscoverController extends Controller
return response()->json(200); return response()->json(200);
} }
public function getHashtags(Request $request)
{
$auth = Auth::check();
abort_if(!config('instance.discover.tags.is_public') && !$auth, 403);
$this->validate($request, [
'hashtag' => 'required|alphanum|min:2|max:124',
'page' => 'nullable|integer|min:1|max:' . ($auth ? 19 : 3)
]);
$page = $request->input('page') ?? '1';
$end = $page > 1 ? $page * 9 : 1;
$tag = $request->input('hashtag');
$hashtag = Hashtag::whereName($tag)->firstOrFail();
$res['tags'] = StatusHashtagService::get($hashtag->id, $page, $end);
if($page == 1) {
$res['follows'] = HashtagFollow::whereUserId(Auth::id())->whereHashtagId($hashtag->id)->exists();
}
return $res;
}
} }

View file

@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\{
Hashtag,
HashtagFollow,
Status
};
class HashtagFollowController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|alpha_num|min:1|max:124|exists:hashtags,name'
]);
$user = Auth::user();
$profile = $user->profile;
$tag = $request->input('name');
$hashtag = Hashtag::whereName($tag)->firstOrFail();
$hashtagFollow = HashtagFollow::firstOrCreate([
'user_id' => $user->id,
'profile_id' => $user->profile_id ?? $user->profile->id,
'hashtag_id' => $hashtag->id
]);
if($hashtagFollow->wasRecentlyCreated) {
$state = 'created';
// todo: send to HashtagFollowService
} else {
$state = 'deleted';
$hashtagFollow->delete();
}
return [
'state' => $state
];
}
public function getTags(Request $request)
{
return HashtagFollow::with('hashtag')->whereUserId(Auth::id())
->inRandomOrder()
->take(3)
->get()
->map(function($follow, $k) {
return $follow->hashtag->name;
});
}
}

View file

@ -211,6 +211,10 @@ class PublicApiController extends Controller
'limit' => 'nullable|integer|max:20' 'limit' => 'nullable|integer|max:20'
]); ]);
if(config('instance.timeline.local.is_public') == false && !Auth::check()) {
abort(403, 'Authentication required.');
}
$page = $request->input('page'); $page = $request->input('page');
$min = $request->input('min_id'); $min = $request->input('min_id');
$max = $request->input('max_id'); $max = $request->input('max_id');
@ -331,6 +335,8 @@ class PublicApiController extends Controller
->orWhere('status', '!=', null) ->orWhere('status', '!=', null)
->pluck('id'); ->pluck('id');
}); });
$private = $private->diff($following)->flatten();
$filters = UserFilter::whereUserId($pid) $filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile') ->whereFilterableType('App\Profile')

View file

@ -143,6 +143,7 @@ class SearchController extends Controller
'tokens' => [$item->caption], 'tokens' => [$item->caption],
'name' => $item->caption, 'name' => $item->caption,
'thumb' => $item->thumb(), 'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class
]; ];
}); });
$tokens['posts'] = $posts; $tokens['posts'] = $posts;

View file

@ -80,8 +80,10 @@ class DeleteAccountPipeline implements ShouldQueue
Bookmark::whereProfileId($user->profile->id)->forceDelete(); Bookmark::whereProfileId($user->profile->id)->forceDelete();
EmailVerification::whereUserId($user->id)->forceDelete(); EmailVerification::whereUserId($user->id)->forceDelete();
$id = $user->profile->id; $id = $user->profile->id;
StatusHashtag::whereProfileId($id)->delete();
FollowRequest::whereFollowingId($id)->orWhere('follower_id', $id)->forceDelete(); FollowRequest::whereFollowingId($id)->orWhere('follower_id', $id)->forceDelete();
Follower::whereProfileId($id)->orWhere('following_id', $id)->forceDelete(); Follower::whereProfileId($id)->orWhere('following_id', $id)->forceDelete();

View file

@ -89,6 +89,9 @@ class StatusEntityLexer implements ShouldQueue
$status = $this->status; $status = $this->status;
foreach ($tags as $tag) { foreach ($tags as $tag) {
if(mb_strlen($tag) > 124) {
continue;
}
DB::transaction(function () use ($status, $tag) { DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag, '-', false); $slug = str_slug($tag, '-', false);
$hashtag = Hashtag::firstOrCreate( $hashtag = Hashtag::firstOrCreate(
@ -98,7 +101,8 @@ class StatusEntityLexer implements ShouldQueue
[ [
'status_id' => $status->id, 'status_id' => $status->id,
'hashtag_id' => $hashtag->id, 'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id 'profile_id' => $status->profile_id,
'status_visibility' => $status->visibility,
] ]
); );
}); });

View file

@ -0,0 +1,64 @@
<?php
namespace App\Observers;
use App\StatusHashtag;
use App\Services\StatusHashtagService;
class StatusHashtagObserver
{
/**
* Handle the notification "created" event.
*
* @param \App\Notification $notification
* @return void
*/
public function created(StatusHashtag $hashtag)
{
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
}
/**
* Handle the notification "updated" event.
*
* @param \App\Notification $notification
* @return void
*/
public function updated(StatusHashtag $hashtag)
{
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
}
/**
* Handle the notification "deleted" event.
*
* @param \App\Notification $notification
* @return void
*/
public function deleted(StatusHashtag $hashtag)
{
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
}
/**
* Handle the notification "restored" event.
*
* @param \App\Notification $notification
* @return void
*/
public function restored(StatusHashtag $hashtag)
{
StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id);
}
/**
* Handle the notification "force deleted" event.
*
* @param \App\Notification $notification
* @return void
*/
public function forceDeleted(StatusHashtag $hashtag)
{
StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id);
}
}

View file

@ -5,11 +5,13 @@ namespace App\Providers;
use App\Observers\{ use App\Observers\{
AvatarObserver, AvatarObserver,
NotificationObserver, NotificationObserver,
StatusHashtagObserver,
UserObserver UserObserver
}; };
use App\{ use App\{
Avatar, Avatar,
Notification, Notification,
StatusHashtag,
User User
}; };
use Auth, Horizon, URL; use Auth, Horizon, URL;
@ -31,6 +33,7 @@ class AppServiceProvider extends ServiceProvider
Avatar::observe(AvatarObserver::class); Avatar::observe(AvatarObserver::class);
Notification::observe(NotificationObserver::class); Notification::observe(NotificationObserver::class);
StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class); User::observe(UserObserver::class);
Horizon::auth(function ($request) { Horizon::auth(function ($request) {

View file

@ -0,0 +1,80 @@
<?php
namespace App\Services;
use Cache, Redis;
use App\{Status, StatusHashtag};
use App\Transformer\Api\StatusHashtagTransformer;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
class StatusHashtagService {
const CACHE_KEY = 'pf:services:status-hashtag:collection:';
public static function get($id, $page = 1, $stop = 9)
{
return StatusHashtag::whereHashtagId($id)
->whereStatusVisibility('public')
->whereHas('media')
->skip($stop)
->latest()
->take(9)
->pluck('status_id')
->map(function ($i, $k) use ($id) {
return self::getStatus($i, $id);
})
->all();
}
public static function coldGet($id, $start = 0, $stop = 2000)
{
$stop = $stop > 2000 ? 2000 : $stop;
$ids = StatusHashtag::whereHashtagId($id)
->whereStatusVisibility('public')
->whereHas('media')
->latest()
->skip($start)
->take($stop)
->pluck('status_id');
foreach($ids as $key) {
self::set($id, $key);
}
return $ids;
}
public static function set($key, $val)
{
return Redis::zadd(self::CACHE_KEY . $key, $val, $val);
}
public static function del($key)
{
return Redis::zrem(self::CACHE_KEY . $key, $key);
}
public static function count($id)
{
$count = Redis::zcount(self::CACHE_KEY . $id, '-inf', '+inf');
if(empty($count)) {
$count = StatusHashtag::whereHashtagId($id)->count();
}
return $count;
}
public static function getStatus($statusId, $hashtagId)
{
return Cache::remember('pf:services:status-hashtag:post:'.$statusId.':hashtag:'.$hashtagId, now()->addMonths(3), function() use($statusId, $hashtagId) {
$statusHashtag = StatusHashtag::with('profile', 'status', 'hashtag')
->whereStatusVisibility('public')
->whereStatusId($statusId)
->whereHashtagId($hashtagId)
->first();
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($statusHashtag, new StatusHashtagTransformer());
return $fractal->createData($resource)->toArray();
});
}
}

View file

@ -9,7 +9,8 @@ class StatusHashtag extends Model
public $fillable = [ public $fillable = [
'status_id', 'status_id',
'hashtag_id', 'hashtag_id',
'profile_id' 'profile_id',
'status_visibility'
]; ];
public function status() public function status()
@ -26,4 +27,16 @@ class StatusHashtag extends Model
{ {
return $this->belongsTo(Profile::class); return $this->belongsTo(Profile::class);
} }
public function media()
{
return $this->hasManyThrough(
Media::class,
Status::class,
'id',
'status_id',
'status_id',
'id'
);
}
} }

View file

@ -0,0 +1,38 @@
<?php
namespace App\Transformer\Api;
use App\{Hashtag, Status, StatusHashtag};
use League\Fractal;
class StatusHashtagTransformer extends Fractal\TransformerAbstract
{
public function transform(StatusHashtag $statusHashtag)
{
$hashtag = $statusHashtag->hashtag;
$status = $statusHashtag->status;
$profile = $statusHashtag->profile;
return [
'status' => [
'id' => (int) $status->id,
'type' => $status->type,
'url' => $status->url(),
'thumb' => $status->thumb(),
'filter' => $status->firstMedia()->filter_class,
'sensitive' => (bool) $status->is_nsfw,
'like_count' => $status->likes_count,
'share_count' => $status->reblogs_count,
'user' => [
'username' => $profile->username,
'url' => $profile->url(),
],
'visibility' => $status->visibility ?? $status->scope
],
'hashtag' => [
'name' => $hashtag->name,
'url' => $hashtag->url(),
]
];
}
}

View file

@ -264,7 +264,9 @@ class Extractor extends Regex
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) { if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
continue; continue;
} }
if(mb_strlen($hashtag[0]) > 124) {
continue;
}
$tags[] = [ $tags[] = [
'hashtag' => $hashtag[0], 'hashtag' => $hashtag[0],
'indices' => [$start_position, $end_position], 'indices' => [$start_position, $end_position],

View file

@ -49,8 +49,23 @@ trait User {
return 500; return 500;
} }
public function getMaxUserBansPerDayAttribute()
{
return 100;
}
public function getMaxInstanceBansPerDayAttribute() public function getMaxInstanceBansPerDayAttribute()
{ {
return 100; return 100;
} }
public function getMaxHashtagFollowsPerHourAttribute()
{
return 20;
}
public function getMaxHashtagFollowsPerDayAttribute()
{
return 100;
}
} }

View file

@ -1,15 +1,33 @@
<?php <?php
return [ return [
'email' => env('INSTANCE_CONTACT_EMAIL'),
'announcement' => [
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', true),
'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.<br><span class="font-weight-normal">Something else here</span>')
],
'contact' => [ 'contact' => [
'enabled' => env('INSTANCE_CONTACT_FORM', false), 'enabled' => env('INSTANCE_CONTACT_FORM', false),
'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1), 'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1),
], ],
'announcement' => [ 'discover' => [
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', true), 'loops' => [
'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.<br><span class="font-weight-normal">Something else here</span>') 'enabled' => false
] ],
'tags' => [
'is_public' => env('INSTANCE_PUBLIC_HASHTAGS', false)
],
],
'email' => env('INSTANCE_CONTACT_EMAIL'),
'timeline' => [
'local' => [
'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false)
]
],
]; ];

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateHashtagFollowsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('hashtag_follows', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned()->index();
$table->bigInteger('profile_id')->unsigned()->index();
$table->bigInteger('hashtag_id')->unsigned()->index();
$table->unique(['user_id', 'profile_id', 'hashtag_id']);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('hashtag_follows');
}
}

View file

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

BIN
public/js/direct.js vendored

Binary file not shown.

BIN
public/js/hashtag.js vendored Normal file

Binary file not shown.

BIN
public/js/loops.js vendored

Binary file not shown.

BIN
public/js/mode-dot.js vendored

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/quill.js vendored

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,187 @@
<template>
<div>
<div v-if="loaded" class="container">
<div class="profile-header row my-5">
<div class="col-12 col-md-3">
<div class="profile-avatar">
<div class="bg-pixelfed mb-3 d-flex align-items-center justify-content-center display-4 font-weight-bold text-white" style="width: 172px; height: 172px; border-radius: 100%">#</div>
</div>
</div>
<div class="col-12 col-md-9 d-flex align-items-center">
<div class="profile-details">
<div class="username-bar pb-2">
<p class="tag-header mb-0">#{{hashtag}}</p>
<p class="lead"><span class="font-weight-bold">{{tags.length ? hashtagCount : '0'}}</span> posts</p>
<p v-if="authenticated && tags.length" class="pt-3">
<button v-if="!following" type="button" class="btn btn-primary font-weight-bold py-1 px-5" @click="followHashtag">
Follow
</button>
<button v-else type="button" class="btn btn-outline-secondary font-weight-bold py-1 px-5" @click="unfollowHashtag">
Unfollow
</button>
</p>
</div>
</div>
</div>
</div>
<div v-if="tags.length" class="tag-timeline">
<p v-if="top.length" class="font-weight-bold text-muted mb-0">Top Posts</p>
<div class="row pb-5">
<div v-for="(tag, index) in top" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
<a class="card info-overlay card-md-border-0" :href="tag.status.url">
<div :class="[tag.status.filter ? 'square ' + tag.status.filter : 'square']">
<div class="square-content" :style="'background-image: url('+tag.status.thumb+')'"></div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span class="pr-4">
<span class="far fa-heart fa-lg pr-1"></span> {{tag.status.like_count}}
</span>
<span>
<span class="fas fa-retweet fa-lg pr-1"></span> {{tag.status.share_count}}
</span>
</h5>
</div>
</div>
</a>
</div>
</div>
<p class="font-weight-bold text-muted mb-0">Most Recent</p>
<div class="row">
<div v-for="(tag, index) in tags" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
<a class="card info-overlay card-md-border-0" :href="tag.status.url">
<div :class="[tag.status.filter ? 'square ' + tag.status.filter : 'square']">
<div class="square-content" :style="'background-image: url('+tag.status.thumb+')'"></div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span class="pr-4">
<span class="far fa-heart fa-lg pr-1"></span> {{tag.status.like_count}}
</span>
<span>
<span class="fas fa-retweet fa-lg pr-1"></span> {{tag.status.share_count}}
</span>
</h5>
</div>
</div>
</a>
</div>
<div v-if="tags.length && loaded" class="card card-body text-center shadow-none bg-transparent border-0">
<infinite-loading @infinite="infiniteLoader">
<div slot="no-results" class="font-weight-bold"></div>
<div slot="no-more" class="font-weight-bold"></div>
</infinite-loading>
</div>
</div>
</div>
<div v-else>
<p class="text-center lead font-weight-bold">No public posts found.</p>
</div>
</div>
<div v-else class="container text-center">
<div class="mt-5 spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</template>
<style type="text/css" scoped>
.tag-header {
font-size: 28px;
font-weight: 300;
}
</style>
<script type="text/javascript">
export default {
props: [
'hashtag',
'hashtagCount'
],
data() {
return {
loaded: false,
page: 1,
authenticated: false,
following: false,
tags: [],
top: [],
}
},
beforeMount() {
this.authenticated = $('body').hasClass('loggedIn');
this.getResults();
},
methods: {
getResults() {
axios.get('/api/v2/discover/tag', {
params: {
hashtag: this.hashtag,
page: this.page
}
}).then(res => {
let data = res.data;
let tags = data.tags.filter(n => {
if(!n || n.length == 0) {
return false;
}
return true;
});
this.tags = tags;
//this.top = tags.slice(6, 9);
this.loaded = true;
this.following = data.follows;
this.page++;
});
},
infiniteLoader($state) {
if(this.page > (this.authenticated ? 19 : 3)) {
$state.complete();
return;
}
axios.get('/api/v2/discover/tag', {
params: {
hashtag: this.hashtag,
page: this.page,
}
}).then(res => {
let data = res.data;
if(data.tags.length) {
let tags = data.tags.filter(n => {
if(!n || n.length == 0) {
return false;
}
return true;
});
this.tags.push(...tags);
if(tags.length > 9) {
$state.complete();
return;
}
this.page++;
$state.loaded();
} else {
$state.complete();
}
});
},
followHashtag() {
axios.post('/api/local/discover/tag/subscribe', {
name: this.hashtag
}).then(res => {
this.following = true;
});
},
unfollowHashtag() {
axios.post('/api/local/discover/tag/subscribe', {
name: this.hashtag
}).then(res => {
this.following = false;
});
},
}
}
</script>

View file

@ -107,8 +107,8 @@
<div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;"> <div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
<div class="card-body status-comments pb-5"> <div class="card-body status-comments pb-5">
<div class="status-comment"> <div class="status-comment">
<p :class="[status.content.length > 420 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;"> <p :class="[status.content.length > 620 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
<span class="font-weight-bold pr-1">{{statusUsername}}</span> <a class="font-weight-bold pr-1 text-dark text-decoration-none" :href="statusProfileUrl">{{statusUsername}}</a>
<span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span> <span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
</p> </p>
@ -124,10 +124,13 @@
<div class="comments"> <div class="comments">
<div v-for="(reply, index) in results" class="pb-3" :key="'tl' + reply.id + '_' + index"> <div v-for="(reply, index) in results" class="pb-3" :key="'tl' + reply.id + '_' + index">
<div v-if="reply.sensitive == true"> <div v-if="reply.sensitive == true">
<div class="card card-body shadow-none border border-left-blue py-3 px-1 text-center small"> <span class="py-3">
<p class="mb-0">This comment may contain sensitive material</p> <a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
<p class="font-weight-bold text-primary cursor-pointer mb-0" @click="reply.sensitive = false;">Show</p> <span class="text-break">
</div> <span class="font-italic text-muted">This comment may contain sensitive material</span>
<span class="text-primary cursor-pointer pl-1" @click="reply.sensitive = false;">Show</span>
</span>
</span>
</div> </div>
<div v-else> <div v-else>
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;"> <p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">

View file

@ -6,8 +6,8 @@
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a> <a class="dropdown-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a>
<a class="dropdown-item font-weight-bold text-decoration-none" href="#">Share</a> <!-- <a class="dropdown-item font-weight-bold text-decoration-none" href="#">Share</a>
<a class="dropdown-item font-weight-bold text-decoration-none" href="#">Embed</a> <a class="dropdown-item font-weight-bold text-decoration-none" href="#">Embed</a> -->
<span v-if="statusOwner(status) == false"> <span v-if="statusOwner(status) == false">
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a> <a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
</span> </span>
@ -54,8 +54,9 @@
<div class="modal-body"> <div class="modal-body">
<div class="list-group"> <div class="list-group">
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a> <a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Go to post</a>
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Share</a> <!-- <a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Share</a>
<a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Embed</a> <a class="list-group-item font-weight-bold text-decoration-none" :href="status.url">Embed</a> -->
<a class="list-group-item font-weight-bold text-decoration-none" href="#" @click="hidePost(status)">Hide</a>
<span v-if="statusOwner(status) == false"> <span v-if="statusOwner(status) == false">
<a class="list-group-item font-weight-bold text-decoration-none" :href="reportUrl(status)">Report</a> <a class="list-group-item font-weight-bold text-decoration-none" :href="reportUrl(status)">Report</a>
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="muteProfile(status)" href="#">Mute Profile</a> <a class="list-group-item font-weight-bold text-decoration-none" v-on:click="muteProfile(status)" href="#">Mute Profile</a>
@ -157,6 +158,11 @@
$('#mt_pid_'+this.status.id).modal('hide'); $('#mt_pid_'+this.status.id).modal('hide');
}, },
hidePost(status) {
status.sensitive = true;
$('#mt_pid_'+status.id).modal('hide');
},
moderatePost(status, action, $event) { moderatePost(status, action, $event) {
let username = status.account.username; let username = status.account.username;
switch(action) { switch(action) {

View file

@ -67,15 +67,13 @@
<div v-if="filters.statuses && results.statuses" class="row mb-4"> <div v-if="filters.statuses && results.statuses" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Statuses</p> <p class="col-12 font-weight-bold text-muted">Statuses</p>
<a v-for="(status, index) in results.statuses" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="status.url"> <div v-for="(status, index) in results.statuses" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
<div class="card"> <a class="card info-overlay card-md-border-0" :href="status.url">
<img class="card-img-top img-fluid" :src="status.thumb"> <div :class="[status.filter ? 'square ' + status.filter : 'square']">
<div class="card-body text-center "> <div class="square-content" :style="'background-image: url('+status.thumb+')'"></div>
<p class="mb-0 small text-truncate font-weight-bold text-muted" v-html="status.value">
</p>
</div> </div>
</div> </a>
</a> </div>
</div> </div>
<div v-if="!results.hashtags && !results.profiles && !results.statuses"> <div v-if="!results.hashtags && !results.profiles && !results.statuses">

View file

@ -39,6 +39,33 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="index == 4 && showHashtagPosts && hashtagPosts.length" class="card mb-sm-4 status-card card-md-rounded-0">
<div class="card-header d-flex align-items-center justify-content-between bg-white border-0 pb-0">
<span></span>
<h6 class="text-muted font-weight-bold mb-0"><a :href="'/discover/tags/'+hashtagPostsName+'?src=tr'">#{{hashtagPostsName}}</a></h6>
<span class="cursor-pointer text-muted" v-on:click="showHashtagPosts = false"><i class="fas fa-times"></i></span>
</div>
<div class="card-body row mx-0">
<div v-for="(tag, index) in hashtagPosts" class="col-4 p-0 p-sm-2 p-md-3 hashtag-post-square">
<a class="card info-overlay card-md-border-0" :href="tag.status.url">
<div :class="[tag.status.filter ? 'square ' + tag.status.filter : 'square']">
<div class="square-content" :style="'background-image: url('+tag.status.thumb+')'"></div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span class="pr-4">
<span class="far fa-heart fa-lg pr-1"></span> {{tag.status.like_count}}
</span>
<span>
<span class="fas fa-retweet fa-lg pr-1"></span> {{tag.status.share_count}}
</span>
</h5>
</div>
</div>
</a>
</div>
</div>
</div>
<div class="card mb-sm-4 status-card card-md-rounded-0"> <div class="card mb-sm-4 status-card card-md-rounded-0">
<div v-if="!modes.distractionFree" class="card-header d-inline-flex align-items-center bg-white"> <div v-if="!modes.distractionFree" class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;"> <img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">
@ -439,7 +466,10 @@
showReadMore: true, showReadMore: true,
replyStatus: {}, replyStatus: {},
replyText: '', replyText: '',
emoji: ['😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'] emoji: ['😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'],
showHashtagPosts: false,
hashtagPosts: [],
hashtagPostsName: '',
} }
}, },
@ -542,6 +572,7 @@
this.max_id = Math.min(...ids); this.max_id = Math.min(...ids);
$('.timeline .pagination').removeClass('d-none'); $('.timeline .pagination').removeClass('d-none');
this.loading = false; this.loading = false;
this.fetchHashtagPosts();
}).catch(err => { }).catch(err => {
}); });
}, },
@ -1104,6 +1135,30 @@
} }
}, 10000); }, 10000);
}); });
},
fetchHashtagPosts() {
axios.get('/api/local/discover/tag/list')
.then(res => {
let tags = res.data;
if(tags.length == 0) {
return;
}
let hashtag = tags[0];
this.hashtagPostsName = hashtag;
axios.get('/api/v2/discover/tag', {
params: {
hashtag: hashtag
}
}).then(res => {
if(res.data.tags.length) {
this.showHashtagPosts = true;
this.hashtagPosts = res.data.tags.splice(0,3);
}
})
})
} }
} }
} }

4
resources/assets/js/hashtag.js vendored Normal file
View file

@ -0,0 +1,4 @@
Vue.component(
'hashtag-component',
require('./components/Hashtag.vue').default
);

View file

@ -1,53 +1,11 @@
@extends('layouts.app') @extends('layouts.app')
@section('content') @section('content')
<hashtag-component hashtag="{{$tag->name}}" hashtag-count="{{$tagCount}}"></hashtag-component>
<div class="container">
<div class="profile-header row my-5">
<div class="col-12 col-md-3">
<div class="profile-avatar">
<img class="rounded-circle card" src="{{$posts->last()->thumb()}}" width="172px" height="172px">
</div>
</div>
<div class="col-12 col-md-9 d-flex align-items-center">
<div class="profile-details">
<div class="username-bar pb-2 d-flex align-items-center">
<span class="h1">{{$tag->name}}</span>
</div>
</div>
</div>
</div>
<div class="tag-timeline">
<div class="row">
@foreach($posts as $status)
<div class="col-4 p-0 p-sm-2 p-md-3">
<a class="card info-overlay card-md-border-0" href="{{$status->url()}}">
<div class="square {{$status->firstMedia()->filter_class}}">
<div class="square-content" style="background-image: url('{{$status->thumb()}}')"></div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span class="pr-4">
<span class="far fa-heart fa-lg pr-1"></span> {{$status->likes_count}}
</span>
<span>
<span class="far fa-comment fa-lg pr-1"></span> {{$status->comments_count}}
</span>
</h5>
</div>
</div>
</a>
</div>
@endforeach
</div>
</div>
</div>
@endsection @endsection
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{ mix('js/hashtag.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script> <script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">$(document).ready(function(){new Vue({el: '#content'});});</script>
$(document).ready(function(){new Vue({el: '#content'});});
</script>
@endpush @endpush

View file

@ -0,0 +1,10 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="error-page py-5 my-5 text-center">
<h3 class="font-weight-bold">Sorry, this page isn't available.</h3>
<p class="lead">The link you followed may be broken, or the page may have been removed. <a href="/">Go back to Pixelfed.</a></p>
</div>
</div>
@endsection

View file

@ -0,0 +1,10 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="error-page py-5 my-5 text-center">
<h3 class="font-weight-bold">Sorry, this page isn't available.</h3>
<p class="lead">The link you followed may be broken, or the page may have been removed. <a href="/">Go back to Pixelfed.</a></p>
</div>
</div>
@endsection

View file

@ -2,13 +2,9 @@
@section('content') @section('content')
<div class="container"> <div class="container">
<div class="error-page py-5 my-5"> <div class="error-page py-5 my-5 text-center">
<div class="card mx-5"> <h3 class="font-weight-bold">Sorry, this page isn't available.</h3>
<div class="card-body p-5 text-center"> <p class="lead">The link you followed may be broken, or the page may have been removed. <a href="/">Go back to Pixelfed.</a></p>
<h1>Page Not Found</h1>
<img src="/img/fred1.gif" class="img-fluid">
</div>
</div>
</div> </div>
</div> </div>
@endsection @endsection

View file

@ -2,14 +2,9 @@
@section('content') @section('content')
<div class="container"> <div class="container">
<div class="error-page py-5 my-5"> <div class="error-page py-5 my-5 text-center">
<div class="card mx-5"> <h3 class="font-weight-bold">Something went wrong</h3>
<div class="card-body p-5 text-center"> <p class="lead">We cannot process your request at this time, please try again later. <a href="/">Go back to Pixelfed.</a></p>
<h1>Whoops! Something went wrong.</h1>
<p class="mb-0 text-muted lead">If you keep seeing this message, please contact an admin.</p>
<img src="/img/fred1.gif" class="img-fluid">
</div>
</div>
</div> </div>
</div> </div>
@endsection @endsection

View file

@ -2,14 +2,9 @@
@section('content') @section('content')
<div class="container"> <div class="container">
<div class="error-page py-5 my-5"> <div class="error-page py-5 my-5 text-center">
<div class="card mx-5"> <h3 class="font-weight-bold">Service Unavailable</h3>
<div class="card-body p-5 text-center"> <p class="lead">Our service is in maintenance mode, please try again later. <a href="/">Go back to Pixelfed.</a></p>
<h1>Service Unavailable</h1>
<p class="mb-0 text-muted lead">Our services are in maintenance mode, please try again later.</p>
<img src="/img/fred1.gif" class="img-fluid">
</div>
</div>
</div> </div>
</div> </div>
@endsection @endsection

View file

@ -16,6 +16,43 @@
<li class="">You can add up to 30 hashtags to your post or comment.</li> <li class="">You can add up to 30 hashtags to your post or comment.</li>
</ul> </ul>
</div> </div>
<div class="py-4">
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse0" role="button" aria-expanded="false" aria-controls="collapse0">
<i class="fas fa-chevron-down mr-2"></i>
How do I use a hashtag on Pixelfed?
</a>
<div class="collapse" id="collapse0">
<div>
<ul>
<li>You can add hashtags to post captions, if the post is public the hashtag will be discoverable.</li>
<li>You can follow hashtags on Pixelfed to stay connected with interests you care about.</li>
</ul>
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse1" role="button" aria-expanded="false" aria-controls="collapse1">
<i class="fas fa-chevron-down mr-2"></i>
How do I follow a hashtag?
</a>
<div class="collapse" id="collapse1">
<div>
<p>You can follow hashtags on Pixelfed to stay connected with interests you care about.</p>
<p class="mb-0">To follow a hashtag:</p>
<ol>
<li>Tap any hashtag (example: #art) you see on Pixelfed.</li>
<li>Tap <span class="font-weight-bold">Follow</span>. Once you follow a hashtag, you'll see its photos and videos appear in feed.</li>
</ol>
<p>To unfollow a hashtag, tap the hashtag and then tap Unfollow to confirm.</p>
<p class="mb-0">
You can follow up to 20 hashtags per hour or 100 per day.
</p>
</div>
</div>
</p>
</div>
<hr>
<div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;"> <div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;">
<div class="card-header text-light font-weight-bold h4 p-4">Hashtag Tips</div> <div class="card-header text-light font-weight-bold h4 p-4">Hashtag Tips</div>
<div class="card-body bg-white p-3"> <div class="card-body bg-white p-3">

View file

@ -1,104 +0,0 @@
@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
@section('content')
<div class="container px-0 mt-md-4">
<div class="card card-md-rounded-0 status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
<div class="row mx-0">
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
<a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
<div class="status-avatar mr-2">
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
</div>
</a>
<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)
<div class="dropdown-divider"></div>
<form method="post" action="/i/mute">
@csrf
<input type="hidden" name="type" value="user">
<input type="hidden" name="item" value="{{$status->profile_id}}">
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Mute this user</button>
</form>
<form method="post" action="/i/block">
@csrf
<input type="hidden" name="type" value="user">
<input type="hidden" name="item" value="{{$status->profile_id}}">
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Block this user</button>
</form>
@endif
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
<div class="dropdown-divider"></div>
{{-- <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="col-12 col-md-8 status-photo px-0">
@if($status->is_nsfw)
<details class="details-animated">
<summary>
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
<p class="font-weight-light">(click to show)</p>
</summary>
@endif
<div id="photoCarousel" class="carousel slide carousel-fade" data-ride="carousel">
<ol class="carousel-indicators">
@for($i = 0; $i < $status->media_count; $i++)
<li data-target="#photoCarousel" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
@endfor
</ol>
<div class="carousel-inner">
@foreach($status->media()->orderBy('order')->get() as $media)
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
<figure class="{{$media->filter_class}}">
<img class="d-block w-100" src="{{$media->url()}}" title="{{$media->caption}}" data-toggle="tooltip" data-placement="bottom">
</figure>
</div>
@endforeach
</div>
<a class="carousel-control-prev" href="#photoCarousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#photoCarousel" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
@if($status->is_nsfw)
</details>
@endif
</div>
@include('status.show.sidebar')
</div>
</div>
</div>
@endsection
@push('meta')
<meta property="og:description" content="{{ $status->caption }}">
<meta property="og:image" content="{{$status->mediaUrl()}}">
<link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
@endpush

View file

@ -1,85 +0,0 @@
@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
@section('content')
<div class="container px-0 mt-md-4">
<div class="card card-md-rounded-0 status-container orientation-{{$status->firstMedia()->orientation ?? 'unknown'}}">
<div class="row mx-0">
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
<a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
<div class="status-avatar mr-2">
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
</div>
</a>
<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)
<div class="dropdown-divider"></div>
<form method="post" action="/i/mute">
@csrf
<input type="hidden" name="type" value="user">
<input type="hidden" name="item" value="{{$status->profile_id}}">
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Mute this user</button>
</form>
<form method="post" action="/i/block">
@csrf
<input type="hidden" name="type" value="user">
<input type="hidden" name="item" value="{{$status->profile_id}}">
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Block this user</button>
</form>
@endif
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
<div class="dropdown-divider"></div>
{{-- <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="col-12 col-md-8 status-photo px-0">
@if($status->is_nsfw && $status->media_count == 1)
<details class="details-animated">
<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()}}" title="{{$status->firstMedia()->caption}}" data-toggle="tooltip" data-tooltip-placement="bottom">
</a>
</details>
@elseif(!$status->is_nsfw && $status->media_count == 1)
<div class="{{$status->firstMedia()->filter_class}}">
<img src="{{$status->mediaUrl()}}" width="100%" title="{{$status->firstMedia()->caption}}" data-toggle="tooltip" data-placement="bottom">
</div>
@endif
</div>
@include('status.show.sidebar')
</div>
</div>
</div>
@endsection
@push('meta')
<meta property="og:description" content="{{ $status->caption }}">
<meta property="og:image" content="{{$status->mediaUrl()}}">
<link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
@endpush

View file

@ -1,117 +0,0 @@
<div class="col-12 col-md-4 px-0 d-flex flex-column border-left border-md-left-0">
<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
<a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
<div class="status-avatar mr-2">
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
</div>
</a>
<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)
<div class="dropdown-divider"></div>
<form method="post" action="/i/mute">
@csrf
<input type="hidden" name="type" value="user">
<input type="hidden" name="item" value="{{$status->profile_id}}">
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Mute this user</button>
</form>
<form method="post" action="/i/block">
@csrf
<input type="hidden" name="type" value="user">
<input type="hidden" name="item" value="{{$status->profile_id}}">
<button type="submit" class="dropdown-item btn btn-link font-weight-bold">Block this user</button>
</form>
@endif
@if(Auth::user()->profile->id === $status->profile->id || Auth::user()->is_admin == true)
<div class="dropdown-divider"></div>
{{-- <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" 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($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>
</div>
</div>
<div class="card-body flex-grow-0 py-1">
<div class="reactions my-1">
@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="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>
<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="item" value="{{$status->id}}">
<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
<span class="float-right">
<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="m-0 {{$status->bookmarked() ? 'fas fa-bookmark text-warning':'far fa-bookmark'}}"></h3>
</button>
</form>
</span>
</div>
<div class="likes font-weight-bold mb-0">
<span class="like-count" data-count="{{$status->likes_count}}">{{$status->likes_count}}</span> likes
</div>
<div class="timestamp">
<a href="{{$status->url()}}" class="small text-muted">
{{$status->created_at->format('F j, Y')}}
</a>
</div>
</div>
</div>
<div class="card-footer bg-white sticky-md-bottom">
<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…" autocomplete="off">
</form>
</div>
</div>

View file

@ -1,50 +0,0 @@
@extends('layouts.app',['title' => $user->username . " posted a photo: " . $status->likes_count . " likes, " . $status->comments_count . " comments" ])
@section('content')
<div class="container px-0 mt-md-4">
<div class="card card-md-rounded-0 status-container orientation-video">
<div class="row mx-0">
<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
<a href="{{$user->url()}}" class="d-flex align-items-center status-username text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$user->username}}">
<div class="status-avatar mr-2">
<img src="{{$user->avatarUrl()}}" width="24px" height="24px" style="border-radius:12px;">
</div>
<div class="username">
<span class="username-link font-weight-bold text-dark">{{$user->username}}</span>
</div>
</a>
</div>
<div class="col-12 col-md-8 status-photo px-0">
@if($status->is_nsfw && $status->media_count == 1)
<details class="details-animated">
<summary>
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<div class="embed-responsive embed-responsive-16by9">
<video class="embed-responsive-item" controls="">
<source src="{{$status->mediaUrl()}}" type="video/mp4">
</video>
</div>
</details>
@elseif(!$status->is_nsfw && $status->media_count == 1)
<div class="embed-responsive embed-responsive-16by9">
<video class="embed-responsive-item" controls="">
<source src="{{$status->mediaUrl()}}" type="video/mp4">
</video>
</div>
@endif
</div>
@include('status.show.sidebar')
</div>
</div>
</div>
@endsection
@push('meta')
<meta property="og:description" content="{{ $status->caption }}">
<meta property="og:image" content="{{$status->mediaUrl()}}">
<link href='{{$status->url()}}' rel='alternate' type='application/activity+json'>
@endpush

View file

@ -91,7 +91,7 @@
<span class="like-count">{{$item->likes_count}}</span> likes <span class="like-count">{{$item->likes_count}}</span> likes
</div> </div>
<div class="caption"> <div class="caption">
<p class="mb-1"> <p class="mb-1 read-more" style="overflow: hidden;">
<span class="username font-weight-bold"> <span class="username font-weight-bold">
<bdi><a class="text-dark" href="{{$item->profile->url()}}" v-pre>{{$item->profile->username}}</a></bdi> <bdi><a class="text-dark" href="{{$item->profile->url()}}" v-pre>{{$item->profile->username}}</a></bdi>
</span> </span>

View file

@ -1,29 +0,0 @@
@if($status->is_nsfw)
@else
<div id="photo-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="carousel">
<ol class="carousel-indicators">
@for($i = 0; $i < $status->media_count; $i++)
<li data-target="#photo-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
@endfor
</ol>
<div class="carousel-inner">
@foreach($status->media()->orderBy('order')->get() as $media)
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
<figure class="{{$media->filter_class}}">
<span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;">{{$loop->iteration}}/{{$loop->count}}</span>
<img class="d-block w-100" src="{{$media->url()}}" alt="{{$status->caption}}">
</figure>
</div>
@endforeach
</div>
<a class="carousel-control-prev" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#photo-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
@endif

View file

@ -1,15 +0,0 @@
@if($status->is_nsfw)
<details class="details-animated">
<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>
@else
<div class="{{$status->firstMedia()->filter_class}}">
<img src="{{$status->mediaUrl()}}" width="100%">
</div>
@endif

View file

@ -1,57 +0,0 @@
@if($status->is_nsfw)
<div id="video-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="false" data-interval="false">
<ol class="carousel-indicators">
@for($i = 0; $i < $status->media_count; $i++)
<li data-target="#video-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
@endfor
</ol>
<div class="carousel-inner">
@foreach($status->media()->orderBy('order')->get() as $media)
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
<span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;z-index: 999;">{{$loop->iteration}}/{{$loop->count}}</span>
<div class="embed-responsive embed-responsive-4by3">
<video class=" embed-responsive-item" controls loop>
<source src="{{$media->url()}}" type="{{$media->mime}}">
</video>
</div>
</div>
@endforeach
</div>
<a class="carousel-control-prev" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
@else
<div id="video-carousel-wrapper-{{$status->id}}" class="carousel slide carousel-fade" data-ride="false" data-interval="false">
<ol class="carousel-indicators">
@for($i = 0; $i < $status->media_count; $i++)
<li data-target="#video-carousel-wrapper-{{$status->id}}" data-slide-to="{{$i}}" class="{{$i == 0 ? 'active' : ''}}"></li>
@endfor
</ol>
<div class="carousel-inner">
@foreach($status->media()->orderBy('order')->get() as $media)
<div class="carousel-item {{$loop->iteration == 1 ? 'active' : ''}}">
<span class="float-right mr-3 badge badge-dark" style="position:fixed;top:8px;right:0;margin-bottom:-20px;z-index: 999;">{{$loop->iteration}}/{{$loop->count}}</span>
<div class="embed-responsive embed-responsive-4by3">
<video class=" embed-responsive-item" controls loop>
<source src="{{$media->url()}}" type="{{$media->mime}}">
</video>
</div>
</div>
@endforeach
</div>
<a class="carousel-control-prev" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#video-carousel-wrapper-{{$status->id}}" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
@endif

View file

@ -1,19 +0,0 @@
@if($status->is_nsfw)
<details class="details-animated">
<summary>
<p class="mb-0 lead font-weight-bold">CW / NSFW / Hidden Media</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<div class="embed-responsive embed-responsive-16by9">
<video class="video" preload="none" controls loop>
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
</video>
</div>
</details>
@else
<div class="embed-responsive embed-responsive-16by9">
<video class="video" preload="none" controls loop>
<source src="{{$status->firstMedia()->url()}}" type="{{$status->firstMedia()->mime}}">
</video>
</div>
@endif

View file

@ -1,82 +0,0 @@
<div class="card card-md-rounded-0 metro-classic-compose">
<div class="card-header bg-white font-weight-bold d-inline-flex justify-content-between">
<div>{{__('Create New Post')}}</div>
</div>
<div class="card-body" id="statusForm">
<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">
<div class="custom-file">
<input type="file" class="custom-file-input" id="fileInput" name="photo[]" accept="{{config('pixelfed.media_types')}}" 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">
<textarea class="form-control" name="caption" placeholder="Add optional 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-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">Visibility</label>
<div class="switch switch-sm">
<select class="form-control" name="visibility">
@if(Auth::user()->profile->is_private)
<option value="public">Public</option>
<option value="unlisted">Unlisted (hidden from public timelines)</option>
<option value="private" selected="">Followers Only</option>
@else
<option value="public" selected="">Public</option>
<option value="unlisted">Unlisted (hidden from public timelines)</option>
<option value="private">Followers Only</option>
@endif
</select>
</div>
<small class="form-text text-muted">
Set the visibility of this post.
</small>
</div>
<div class="form-group">
<label class="font-weight-bold text-muted small">CW/NSFW</label>
<div class="switch switch-sm">
<input type="checkbox" class="switch" id="cw-switch" name="cw">
<label for="cw-switch" class="small font-weight-bold">(Default off)</label>
</div>
<small class="form-text text-muted">
Please mark all NSFW and controversial content, as per our content policy.
</small>
</div>
<div class="form-group d-none form-preview">
<label class="font-weight-bold text-muted small">Photo Preview</label>
<figure class="filterContainer">
<img class="filterPreview img-fluid">
</figure>
<small class="form-text text-muted font-weight-bold">
No filter selected.
</small>
</div>
<div class="form-group d-none form-filters">
<label for="filterSelectDropdown" class="font-weight-bold text-muted small">Select Filter</label>
<select class="form-control" id="filterSelectDropdown">
<option value="none" selected="">No Filter</option>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-outline-primary btn-block font-weight-bold">Create Post</button>
</form>
</div>
</div>

View file

@ -1,68 +0,0 @@
@extends('layouts.app')
@push('scripts')
<script type="text/javascript" src="{{mix('js/timeline.js')}}"></script>
@endpush
@section('content')
<div class="container p-0">
<div class="col-md-10 col-lg-8 mx-auto pt-4 px-0">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@include('timeline.partial.new-form')
<div class="timeline-feed my-5" data-timeline="personal">
@foreach($timeline as $item)
@include('status.template')
@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>
@endsection

View file

@ -1,59 +0,0 @@
@extends('layouts.app')
@push('scripts')
<script type="text/javascript" src="{{mix('js/timeline.js')}}"></script>
@endpush
@section('content')
<div class="container px-0">
<div class="col-md-10 col-lg-8 mx-auto pt-4 px-0">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@include('timeline.partial.new-form')
<div class="timeline-feed my-5" data-timeline="public">
@foreach($timeline as $item)
@include('status.template')
@endforeach
</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>
@endsection

View file

@ -1,132 +0,0 @@
@extends('layouts.app')
@section('content')
<noscript>
<div class="container">
<div class="card border-left-blue mt-5">
<div class="card-body">
<p class="mb-0 font-weight-bold">Javascript is required for an optimized experience, please enable it to use this site.</p>
<p class="mb-0 font-weight-bold text-muted">(We are working on a lite version that does not require javascript)</p>
</div>
</div>
</div>
</noscript>
<div class="container d-none timeline-container">
<div class="row">
<div class="col-md-8 col-lg-8 pt-4 px-0 my-3">
@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-4 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 text-muted text-truncate pb-0">{{Auth::user()->name}}</p>
</div>
</div>
<div class="mb-4">
<ul class="nav nav-pills flex-column timeline-sidenav" style="max-width: 240px;">
<li class="nav-item">
<a class="nav-link font-weight-bold" href="/" data-type="personal">
<i class="far fa-user pr-1"></i> My Timeline
</a>
</li>
<li class="nav-item">
<a class="nav-link font-weight-bold" href="/timeline/public" data-type="local">
<i class="fas fa-bars pr-1"></i> Local Timeline
</a>
</li>
<li class="nav-item" data-toggle="tooltip" data-placement="bottom" title="The network timeline is not available yet.">
<span class="nav-link font-weight-bold">
<i class="fas fa-globe pr-1"></i> Network Timeline
</span>
</li>
</ul>
</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>
</p>
<p class="mb-0 text-uppercase font-weight-bold text-muted small">
<a href="http://pixelfed.org" class="text-muted" rel="noopener" title="version {{config('pixelfed.version')}}" data-toggle="tooltip">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

@ -106,11 +106,14 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440'); Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
Route::get('loops', 'DiscoverController@loopsApi'); Route::get('loops', 'DiscoverController@loopsApi');
Route::post('loops/watch', 'DiscoverController@loopWatch'); Route::post('loops/watch', 'DiscoverController@loopWatch');
Route::get('discover/tag', 'DiscoverController@getHashtags');
}); });
Route::group(['prefix' => 'local'], function () { Route::group(['prefix' => 'local'], function () {
Route::get('i/follow-suggestions', 'ApiController@followSuggestions'); Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440'); Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
Route::get('exp/rec', 'ApiController@userRecommendations'); Route::get('exp/rec', 'ApiController@userRecommendations');
Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');;
Route::get('discover/tag/list', 'HashtagFollowController@getTags');
}); });
}); });

1
webpack.mix.js vendored
View file

@ -30,6 +30,7 @@ mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js') .js('resources/assets/js/lib/ace/theme-monokai.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public') // .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js') // .js('resources/assets/js/direct.js', 'public/js')
.js('resources/assets/js/hashtag.js', 'public/js')
.extract([ .extract([
'lodash', 'lodash',
'popper.js', 'popper.js',