1
0
Fork 0

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 906 additions and 2544 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();
} else {
$this->validate($request, [
'page' => 'nullable|integer|min:1',
'page' => 'nullable|integer|min:1|max:10',
'limit' => 'nullable|integer|min:1|max:10'
]);
$limit = $request->input('limit') ?? 10;
$page = $request->input('page') ?? 1;
if($page > 3) {
return response()->json([]);
}
$end = (int) $page * $limit;
$start = (int) $end - $limit;
$res = NotificationService::get($pid, $start, $end);

View File

@ -6,6 +6,7 @@ use App\{
DiscoverCategory,
Follower,
Hashtag,
HashtagFollow,
Profile,
Status,
StatusHashtag,
@ -17,6 +18,7 @@ use App\Transformer\Api\StatusStatelessTransformer;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\StatusHashtagService;
class DiscoverController extends Controller
{
@ -36,57 +38,11 @@ class DiscoverController extends Controller
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)
->firstOrFail();
$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'));
$tag = Hashtag::whereSlug($hashtag)->firstOrFail();
$tagCount = StatusHashtagService::count($tag->id);
return view('discover.tags.show', compact('tag', 'tagCount'));
}
public function showCategory(Request $request, $slug)
@ -156,7 +112,6 @@ class DiscoverController extends Controller
return $res;
}
public function loopWatch(Request $request)
{
abort_if(!Auth::check(), 403);
@ -171,4 +126,26 @@ class DiscoverController extends Controller
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'
]);
if(config('instance.timeline.local.is_public') == false && !Auth::check()) {
abort(403, 'Authentication required.');
}
$page = $request->input('page');
$min = $request->input('min_id');
$max = $request->input('max_id');
@ -331,6 +335,8 @@ class PublicApiController extends Controller
->orWhere('status', '!=', null)
->pluck('id');
});
$private = $private->diff($following)->flatten();
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Profile')

View File

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

View File

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

View File

@ -89,6 +89,9 @@ class StatusEntityLexer implements ShouldQueue
$status = $this->status;
foreach ($tags as $tag) {
if(mb_strlen($tag) > 124) {
continue;
}
DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag, '-', false);
$hashtag = Hashtag::firstOrCreate(
@ -98,7 +101,8 @@ class StatusEntityLexer implements ShouldQueue
[
'status_id' => $status->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\{
AvatarObserver,
NotificationObserver,
StatusHashtagObserver,
UserObserver
};
use App\{
Avatar,
Notification,
StatusHashtag,
User
};
use Auth, Horizon, URL;
@ -31,6 +33,7 @@ class AppServiceProvider extends ServiceProvider
Avatar::observe(AvatarObserver::class);
Notification::observe(NotificationObserver::class);
StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class);
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 = [
'status_id',
'hashtag_id',
'profile_id'
'profile_id',
'status_visibility'
];
public function status()
@ -26,4 +27,16 @@ class StatusHashtag extends Model
{
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])) {
continue;
}
if(mb_strlen($hashtag[0]) > 124) {
continue;
}
$tags[] = [
'hashtag' => $hashtag[0],
'indices' => [$start_position, $end_position],

View File

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

View File

@ -1,15 +1,33 @@
<?php
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' => [
'enabled' => env('INSTANCE_CONTACT_FORM', false),
'max_per_day' => env('INSTANCE_CONTACT_MAX_PER_DAY', 1),
],
'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>')
]
'discover' => [
'loops' => [
'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');
});
}
}

1557
public/js/direct.js vendored

File diff suppressed because it is too large Load Diff

1
public/js/hashtag.js vendored Normal file

File diff suppressed because one or more lines are too long

2
public/js/loops.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/quill.js vendored

File diff suppressed because one or more lines are too long

2
public/js/search.js vendored

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[15],{14:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)}),ace.require(["ace/theme/monokai"],function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)})}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[14,0]]]);
(window.webpackJsonp=window.webpackJsonp||[]).push([[16],{14:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)}),ace.require(["ace/theme/monokai"],function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)})}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[14,0]]]);

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"/js/manifest.js": "/js/manifest.js?id=01c8731923a46c30aaed",
"/js/vendor.js": "/js/vendor.js?id=2b3ccf793d7dbf41c3b8",
"/js/vendor.js": "/js/vendor.js?id=8ea306403cb5cfa88c2b",
"/js/ace.js": "/js/ace.js?id=72ae94b2a68487d22caf",
"/js/activity.js": "/js/activity.js?id=7405cc1a22814a5b2a70",
"/js/app.js": "/js/app.js?id=2f034c84c06dbb3e511d",
@ -12,12 +12,13 @@
"/js/compose.js": "/js/compose.js?id=475dac4628e2d1cb3dc9",
"/js/developers.js": "/js/developers.js?id=e4a227c87d1b09fc2dd9",
"/js/discover.js": "/js/discover.js?id=772fbc6c176aaa9039ec",
"/js/loops.js": "/js/loops.js?id=a2291e15b8b246621b07",
"/js/mode-dot.js": "/js/mode-dot.js?id=3c67c8f827dc52536a4b",
"/js/profile.js": "/js/profile.js?id=c45f4c00d9e3c9aac7c9",
"/js/quill.js": "/js/quill.js?id=d2e82cff48024d45384a",
"/js/search.js": "/js/search.js?id=38a3a6a07e3b37a1e444",
"/js/status.js": "/js/status.js?id=28de331daaf3447a3d3f",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=1f7b44b76af99c0b4e38",
"/js/timeline.js": "/js/timeline.js?id=52af49a558f589d1b569"
"/js/hashtag.js": "/js/hashtag.js?id=32c3a6b1a04949d390ab",
"/js/loops.js": "/js/loops.js?id=cacf532da1e0d50ec014",
"/js/mode-dot.js": "/js/mode-dot.js?id=7d248d684a37f3eb88de",
"/js/profile.js": "/js/profile.js?id=8b6af927d424cb58203e",
"/js/quill.js": "/js/quill.js?id=a8a0855cb59d6a2f7aee",
"/js/search.js": "/js/search.js?id=27506b08cf8c889e3f7f",
"/js/status.js": "/js/status.js?id=665fb54127ca484a1692",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=1bc564c4661eb0fa30b6",
"/js/timeline.js": "/js/timeline.js?id=90a01cbe86e6dff2b5c3"
}

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="card-body status-comments pb-5">
<div class="status-comment">
<p :class="[status.content.length > 420 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
<span class="font-weight-bold pr-1">{{statusUsername}}</span>
<p :class="[status.content.length > 620 ? 'mb-1 read-more' : 'mb-1']" style="overflow: hidden;">
<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>
</p>
@ -124,10 +124,13 @@
<div class="comments">
<div v-for="(reply, index) in results" class="pb-3" :key="'tl' + reply.id + '_' + index">
<div v-if="reply.sensitive == true">
<div class="card card-body shadow-none border border-left-blue py-3 px-1 text-center small">
<p class="mb-0">This comment may contain sensitive material</p>
<p class="font-weight-bold text-primary cursor-pointer mb-0" @click="reply.sensitive = false;">Show</p>
</div>
<span class="py-3">
<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>
<span class="text-break">
<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 v-else>
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">

View File

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

View File

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

View File

@ -39,6 +39,33 @@
</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 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;">
@ -439,7 +466,10 @@
showReadMore: true,
replyStatus: {},
replyText: '',
emoji: ['😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥']
emoji: ['😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'],
showHashtagPosts: false,
hashtagPosts: [],
hashtagPostsName: '',
}
},
@ -542,6 +572,7 @@
this.max_id = Math.min(...ids);
$('.timeline .pagination').removeClass('d-none');
this.loading = false;
this.fetchHashtagPosts();
}).catch(err => {
});
},
@ -1104,6 +1135,30 @@
}
}, 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')
@section('content')
<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>
<hashtag-component hashtag="{{$tag->name}}" hashtag-count="{{$tagCount}}"></hashtag-component>
@endsection
@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">
$(document).ready(function(){new Vue({el: '#content'});});
</script>
<script type="text/javascript">$(document).ready(function(){new Vue({el: '#content'});});</script>
@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')
<div class="container">
<div class="error-page py-5 my-5">
<div class="card mx-5">
<div class="card-body p-5 text-center">
<h1>Page Not Found</h1>
<img src="/img/fred1.gif" class="img-fluid">
</div>
</div>
<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,14 +2,9 @@
@section('content')
<div class="container">
<div class="error-page py-5 my-5">
<div class="card mx-5">
<div class="card-body p-5 text-center">
<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 class="error-page py-5 my-5 text-center">
<h3 class="font-weight-bold">Something went wrong</h3>
<p class="lead">We cannot process your request at this time, please try again later. <a href="/">Go back to Pixelfed.</a></p>
</div>
</div>
@endsection

View File

@ -2,14 +2,9 @@
@section('content')
<div class="container">
<div class="error-page py-5 my-5">
<div class="card mx-5">
<div class="card-body p-5 text-center">
<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 class="error-page py-5 my-5 text-center">
<h3 class="font-weight-bold">Service Unavailable</h3>
<p class="lead">Our service is in maintenance mode, please try again later. <a href="/">Go back to Pixelfed.</a></p>
</div>
</div>
@endsection

View File

@ -16,6 +16,43 @@
<li class="">You can add up to 30 hashtags to your post or comment.</li>
</ul>
</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-header text-light font-weight-bold h4 p-4">Hashtag Tips</div>
<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
</div>
<div class="caption">
<p class="mb-1">
<p class="mb-1 read-more" style="overflow: hidden;">
<span class="username font-weight-bold">
<bdi><a class="text-dark" href="{{$item->profile->url()}}" v-pre>{{$item->profile->username}}</a></bdi>
</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::get('loops', 'DiscoverController@loopsApi');
Route::post('loops/watch', 'DiscoverController@loopWatch');
Route::get('discover/tag', 'DiscoverController@getHashtags');
});
Route::group(['prefix' => 'local'], function () {
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
Route::post('status/compose', 'InternalApiController@compose')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
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/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
.js('resources/assets/js/hashtag.js', 'public/js')
.extract([
'lodash',
'popper.js',