mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 22:41:27 +00:00
Merge pull request #1317 from pixelfed/frontend-ui-refactor
Hello Loops
This commit is contained in:
commit
cbb98b0462
60 changed files with 1117 additions and 295 deletions
52
app/Console/Commands/VideoThumbnail.php
Normal file
52
app/Console/Commands/VideoThumbnail.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Media;
|
||||
use App\Jobs\VideoPipeline\VideoThumbnail as Pipeline;
|
||||
|
||||
class VideoThumbnail extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'video:thumbnail';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate missing video thumbnails';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = 10;
|
||||
$videos = Media::whereMime('video/mp4')
|
||||
->whereNull('thumbnail_path')
|
||||
->take($limit)
|
||||
->get();
|
||||
foreach($videos as $video) {
|
||||
Pipeline::dispatchNow($video);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@ class Follower extends Model
|
|||
|
||||
protected $fillable = ['profile_id', 'following_id', 'local_profile'];
|
||||
|
||||
const MAX_FOLLOWING = 7500;
|
||||
const FOLLOW_PER_HOUR = 20;
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
|
|
|
@ -159,6 +159,11 @@ class AccountController extends Controller
|
|||
return view('account.messages');
|
||||
}
|
||||
|
||||
public function direct()
|
||||
{
|
||||
return view('account.direct');
|
||||
}
|
||||
|
||||
public function showMessage(Request $request, $id)
|
||||
{
|
||||
return view('account.message');
|
||||
|
|
|
@ -48,7 +48,8 @@ class BaseApiController extends Controller
|
|||
public function notifications(Request $request)
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
if(config('exp.ns') == false) {
|
||||
$pg = $request->input('pg');
|
||||
if($pg == true) {
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($pid)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
|
@ -272,6 +273,7 @@ class BaseApiController extends Controller
|
|||
'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id]
|
||||
);
|
||||
|
||||
$preview_url = $url;
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
|
@ -280,6 +282,8 @@ class BaseApiController extends Controller
|
|||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media);
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -288,7 +292,7 @@ class BaseApiController extends Controller
|
|||
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $url;
|
||||
$res['preview_url'] = $preview_url;
|
||||
$res['url'] = $url;
|
||||
return response()->json($res);
|
||||
}
|
||||
|
@ -326,5 +330,4 @@ class BaseApiController extends Controller
|
|||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ class ApiController extends BaseApiController
|
|||
'ab' => [
|
||||
'lc' => config('exp.lc'),
|
||||
'rec' => config('exp.rec'),
|
||||
'loops' => config('exp.loops')
|
||||
],
|
||||
];
|
||||
});
|
||||
|
|
|
@ -13,21 +13,31 @@ use App\{
|
|||
};
|
||||
use Auth, DB, Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Transformer\Api\StatusStatelessTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
||||
class DiscoverController extends Controller
|
||||
{
|
||||
protected $fractal;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->fractal = new Fractal\Manager();
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
}
|
||||
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
return view('discover.home');
|
||||
}
|
||||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
|
||||
$tag = Hashtag::whereSlug($hashtag)
|
||||
->firstOrFail();
|
||||
|
||||
|
@ -81,6 +91,8 @@ class DiscoverController extends Controller
|
|||
|
||||
public function showCategory(Request $request, $slug)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
|
||||
$tag = DiscoverCategory::whereActive(true)
|
||||
->whereSlug($slug)
|
||||
->firstOrFail();
|
||||
|
@ -99,6 +111,8 @@ class DiscoverController extends Controller
|
|||
|
||||
public function showPersonal(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$tags = Cache::remember('profile-'.$profile->id.':hashtags', now()->addMinutes(15), function() use ($profile){
|
||||
|
@ -115,4 +129,43 @@ class DiscoverController extends Controller
|
|||
});
|
||||
return view('discover.personal', compact('posts', 'tags'));
|
||||
}
|
||||
|
||||
public function showLoops(Request $request)
|
||||
{
|
||||
if(config('exp.loops') != true) {
|
||||
return redirect('/');
|
||||
}
|
||||
return view('discover.loops.home');
|
||||
}
|
||||
|
||||
public function loopsApi(Request $request)
|
||||
{
|
||||
abort_if(!config('exp.loops'), 403);
|
||||
|
||||
// todo proper pagination, maybe LoopService
|
||||
$loops = Status::whereType('video')
|
||||
->whereScope('public')
|
||||
->latest()
|
||||
->take(18)
|
||||
->get();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($loops, new StatusStatelessTransformer());
|
||||
return $this->fractal->createData($resource)->toArray();
|
||||
}
|
||||
|
||||
|
||||
public function loopWatch(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check(), 403);
|
||||
abort_if(!config('exp.loops'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'integer|min:1'
|
||||
]);
|
||||
$id = $request->input('id');
|
||||
|
||||
// todo log loops
|
||||
|
||||
return response()->json(200);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ class FollowerController extends Controller
|
|||
protected function handleFollowRequest($item)
|
||||
{
|
||||
$user = Auth::user()->profile;
|
||||
|
||||
|
||||
$target = Profile::where('id', '!=', $user->id)->whereNull('status')->findOrFail($item);
|
||||
$private = (bool) $target->is_private;
|
||||
$remote = (bool) $target->domain;
|
||||
|
@ -47,7 +49,7 @@ class FollowerController extends Controller
|
|||
->exists();
|
||||
|
||||
if($blocked == true) {
|
||||
return redirect()->back()->with('error', 'You cannot follow this user.');
|
||||
abort(400, 'You cannot follow this user.');
|
||||
}
|
||||
|
||||
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->count();
|
||||
|
@ -61,6 +63,13 @@ class FollowerController extends Controller
|
|||
|
||||
}
|
||||
} elseif ($isFollowing == 0) {
|
||||
if($user->following()->count() >= Follower::MAX_FOLLOWING) {
|
||||
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
|
||||
}
|
||||
|
||||
if($user->following()->where('followers.created_at', '>', now()->subHour())->count() >= Follower::FOLLOW_PER_HOUR) {
|
||||
abort(400, 'You can only follow ' . Follower::FOLLOW_PER_HOUR . ' users per hour');
|
||||
}
|
||||
$follower = new Follower();
|
||||
$follower->profile_id = $user->id;
|
||||
$follower->following_id = $target->id;
|
||||
|
|
|
@ -106,7 +106,12 @@ class InternalApiController extends Controller
|
|||
});
|
||||
$following = array_merge($following, $filters);
|
||||
|
||||
$posts = Status::select('id', 'caption', 'profile_id')
|
||||
$posts = Status::select(
|
||||
'id',
|
||||
'caption',
|
||||
'profile_id',
|
||||
'type'
|
||||
)
|
||||
->whereNull('uri')
|
||||
->whereHas('media')
|
||||
->whereHas('profile', function($q) {
|
||||
|
@ -123,6 +128,7 @@ class InternalApiController extends Controller
|
|||
$res = [
|
||||
'posts' => $posts->map(function($post) {
|
||||
return [
|
||||
'type' => $post->type,
|
||||
'url' => $post->url(),
|
||||
'thumb' => $post->thumb(),
|
||||
];
|
||||
|
|
|
@ -74,17 +74,6 @@ class DeleteAccountPipeline implements ShouldQueue
|
|||
if($user->profile) {
|
||||
$avatar = $user->profile->avatar;
|
||||
|
||||
if(is_file($avatar->media_path)) {
|
||||
if($avatar->media_path != 'public/avatars/default.png') {
|
||||
unlink($avatar->media_path);
|
||||
}
|
||||
}
|
||||
|
||||
if(is_file($avatar->thumb_path)) {
|
||||
if($avatar->thumb_path != 'public/avatars/default.png') {
|
||||
unlink($avatar->thumb_path);
|
||||
}
|
||||
}
|
||||
$avatar->forceDelete();
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ class VideoThumbnail implements ShouldQueue
|
|||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
if($media->mime != 'video/mp4') {
|
||||
return;
|
||||
}
|
||||
$base = $media->media_path;
|
||||
$path = explode('/', $base);
|
||||
$name = last($path);
|
||||
|
@ -43,14 +46,11 @@ class VideoThumbnail implements ShouldQueue
|
|||
$i = count($path) - 1;
|
||||
$path[$i] = $t;
|
||||
$save = implode('/', $path);
|
||||
$video = FFMpeg::open($base);
|
||||
if($video->getDurationInSeconds() < 1) {
|
||||
$video->getFrameFromSeconds(0);
|
||||
} elseif($video->getDurationInSeconds() < 5) {
|
||||
$video->getFrameFromSeconds(4);
|
||||
}
|
||||
$video->export()
|
||||
->save($save);
|
||||
$video = FFMpeg::open($base)
|
||||
->getFrameFromSeconds(0)
|
||||
->export()
|
||||
->toDisk('local')
|
||||
->save($save);
|
||||
|
||||
$media->thumbnail_path = $save;
|
||||
$media->save();
|
||||
|
|
|
@ -48,7 +48,13 @@ class AvatarObserver
|
|||
public function deleting(Avatar $avatar)
|
||||
{
|
||||
$path = storage_path('app/'.$avatar->media_path);
|
||||
@unlink($path);
|
||||
if(is_file($path) && $avatar->media_path != 'public/avatars/default.png') {
|
||||
@unlink($path);
|
||||
}
|
||||
$path = storage_path('app/'.$avatar->thumb_path);
|
||||
if(is_file($path) && $avatar->thumb_path != 'public/avatars/default.png') {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -73,7 +73,7 @@ class NotificationService {
|
|||
public static function getNotification($id)
|
||||
{
|
||||
return Cache::remember('service:notification:'.$id, now()->addDays(7), function() use($id) {
|
||||
$n = Notification::findOrFail($id);
|
||||
$n = Notification::with('item')->findOrFail($id);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($n, new NotificationTransformer());
|
||||
|
|
|
@ -40,9 +40,16 @@ class Status extends Model
|
|||
'story',
|
||||
'story:reply',
|
||||
'story:reaction',
|
||||
'story:live'
|
||||
'story:live',
|
||||
'loop'
|
||||
];
|
||||
|
||||
const MAX_MENTIONS = 5;
|
||||
|
||||
const MAX_HASHTAGS = 30;
|
||||
|
||||
const MAX_LINKS = 2;
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
|
@ -87,7 +94,7 @@ class Status extends Model
|
|||
return Cache::remember('status:thumb:'.$this->id, now()->addMinutes(15), function() use ($showNsfw) {
|
||||
$type = $this->type ?? $this->setType();
|
||||
$is_nsfw = !$showNsfw ? $this->is_nsfw : false;
|
||||
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album'])) {
|
||||
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album', 'video'])) {
|
||||
return url(Storage::url('public/no-preview.png'));
|
||||
}
|
||||
|
||||
|
@ -99,11 +106,12 @@ class Status extends Model
|
|||
{
|
||||
if($this->uri) {
|
||||
return $this->uri;
|
||||
} else {
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
$path = url(config('app.url')."/p/{$username}/{$id}");
|
||||
return $path;
|
||||
}
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
$path = url(config('app.url')."/p/{$username}/{$id}");
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function permalink($suffix = '/activity')
|
||||
|
@ -207,6 +215,8 @@ class Status extends Model
|
|||
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||
if (!empty($parent)) {
|
||||
return $this->findOrFail($parent);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Notification;
|
||||
use App\{
|
||||
Notification,
|
||||
Status
|
||||
};
|
||||
use League\Fractal;
|
||||
|
||||
class NotificationTransformer extends Fractal\TransformerAbstract
|
||||
|
@ -10,6 +13,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'status',
|
||||
'relationship'
|
||||
];
|
||||
|
||||
public function transform(Notification $notification)
|
||||
|
@ -30,9 +34,14 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
|
||||
public function includeStatus(Notification $notification)
|
||||
{
|
||||
$item = $notification->item;
|
||||
if(is_object($item) && get_class($item) === 'App\Status') {
|
||||
return $this->item($item, new StatusTransformer());
|
||||
$item = $notification;
|
||||
if($item->item_id && $item->item_type == 'App\Status') {
|
||||
$status = Status::with('media')->find($item->item_id);
|
||||
if($status) {
|
||||
return $this->item($status, new StatusTransformer());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -50,4 +59,9 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
];
|
||||
return $verbs[$verb];
|
||||
}
|
||||
|
||||
public function includeRelationship(Notification $notification)
|
||||
{
|
||||
return $this->item($notification->actor, new RelationshipTransformer());
|
||||
}
|
||||
}
|
||||
|
|
84
app/Transformer/Api/StatusStatelessTransformer.php
Normal file
84
app/Transformer/Api/StatusStatelessTransformer.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Cache;
|
||||
|
||||
class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'mentions',
|
||||
'media_attachments',
|
||||
'tags',
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
|
||||
'reblog' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'created_at' => $status->created_at->format('c'),
|
||||
'emojis' => [],
|
||||
'reblogs_count' => $status->shares()->count(),
|
||||
'favourites_count' => $status->likes()->count(),
|
||||
'reblogged' => null,
|
||||
'favourited' => null,
|
||||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary,
|
||||
'visibility' => $status->visibility,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
],
|
||||
'language' => null,
|
||||
'pinned' => null,
|
||||
|
||||
'pf_type' => $status->type ?? $status->setType(),
|
||||
'reply_count' => (int) $status->reply_count,
|
||||
'comments_disabled' => $status->comments_disabled ? true : false,
|
||||
'thread' => false,
|
||||
'replies' => [],
|
||||
'parent' => $status->parent() ? $this->transform($status->parent()) : [],
|
||||
];
|
||||
}
|
||||
|
||||
public function includeAccount(Status $status)
|
||||
{
|
||||
$account = $status->profile;
|
||||
|
||||
return $this->item($account, new AccountTransformer());
|
||||
}
|
||||
|
||||
public function includeMentions(Status $status)
|
||||
{
|
||||
$mentions = $status->mentions;
|
||||
|
||||
return $this->collection($mentions, new MentionTransformer());
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addMinutes(3), function() use($status) {
|
||||
if(in_array($status->type, ['photo', 'video'])) {
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$tags = $status->hashtags;
|
||||
|
||||
return $this->collection($tags, new HashtagTransformer());
|
||||
}
|
||||
}
|
|
@ -46,7 +46,8 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'reply_count' => (int) $status->reply_count,
|
||||
'comments_disabled' => $status->comments_disabled ? true : false,
|
||||
'thread' => false,
|
||||
'replies' => []
|
||||
'replies' => [],
|
||||
'parent' => $status->parent() ? $this->transform($status->parent()) : [],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -67,8 +68,10 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addMinutes(3), function() use($status) {
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
if(in_array($status->type, ['photo', 'video'])) {
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -418,7 +418,7 @@ class Autolink extends Regex
|
|||
if(Str::startsWith($entity['screen_name'], '@')) {
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
|
||||
} else {
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex + 1);
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
|
||||
}
|
||||
} else {
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
|
||||
|
@ -708,16 +708,17 @@ class Autolink extends Regex
|
|||
{
|
||||
$attributes = [];
|
||||
|
||||
$screen_name = $entity['screen_name'];
|
||||
if (!empty($entity['list_slug'])) {
|
||||
// Replace the list and username
|
||||
$linkText = $entity['screen_name'];
|
||||
$linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name;
|
||||
$class = $this->class_list;
|
||||
$url = $this->url_base_list.$linkText;
|
||||
$url = $this->url_base_list.$screen_name;
|
||||
} else {
|
||||
// Replace the username
|
||||
$linkText = $entity['screen_name'];
|
||||
$linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name;
|
||||
$class = $this->class_user;
|
||||
$url = $this->url_base_user.$linkText;
|
||||
$url = $this->url_base_user.$screen_name;;
|
||||
}
|
||||
if (!empty($class)) {
|
||||
$attributes['class'] = $class;
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
namespace App\Util\Lexer;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use App\Status;
|
||||
|
||||
/**
|
||||
* Twitter Extractor Class.
|
||||
|
@ -121,7 +122,7 @@ class Extractor extends Regex
|
|||
$hashtagsOnly[] = $hashtagWithIndex['hashtag'];
|
||||
}
|
||||
|
||||
return $hashtagsOnly;
|
||||
return array_slice($hashtagsOnly, 0, Status::MAX_HASHTAGS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,12 +135,6 @@ class Extractor extends Regex
|
|||
public function extractCashtags($tweet = null)
|
||||
{
|
||||
$cashtagsOnly = [];
|
||||
$cashtagsWithIndices = $this->extractCashtagsWithIndices($tweet);
|
||||
|
||||
foreach ($cashtagsWithIndices as $cashtagWithIndex) {
|
||||
$cashtagsOnly[] = $cashtagWithIndex['cashtag'];
|
||||
}
|
||||
|
||||
return $cashtagsOnly;
|
||||
}
|
||||
|
||||
|
@ -159,7 +154,7 @@ class Extractor extends Regex
|
|||
$urlsOnly[] = $urlWithIndex['url'];
|
||||
}
|
||||
|
||||
return $urlsOnly;
|
||||
return array_slice($urlsOnly, 0, Status::MAX_LINKS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -277,7 +272,7 @@ class Extractor extends Regex
|
|||
}
|
||||
|
||||
if (!$checkUrlOverlap) {
|
||||
return $tags;
|
||||
return array_slice($tags, 0, Status::MAX_HASHTAGS);
|
||||
}
|
||||
|
||||
// check url overlap
|
||||
|
@ -292,7 +287,7 @@ class Extractor extends Regex
|
|||
$validTags[] = $entity;
|
||||
}
|
||||
|
||||
return $validTags;
|
||||
return array_slice($validTags, 0, Status::MAX_HASHTAGS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -390,7 +385,7 @@ class Extractor extends Regex
|
|||
}
|
||||
}
|
||||
|
||||
return $urls;
|
||||
return array_slice($urls, 0, Status::MAX_LINKS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -415,7 +410,7 @@ class Extractor extends Regex
|
|||
$usernamesOnly[] = $mention;
|
||||
}
|
||||
|
||||
return $usernamesOnly;
|
||||
return array_slice($usernamesOnly, 0, Status::MAX_MENTIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -472,7 +467,7 @@ class Extractor extends Regex
|
|||
$results[] = $entity;
|
||||
}
|
||||
|
||||
return $results;
|
||||
return array_slice($results, 0, Status::MAX_MENTIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -38,6 +38,13 @@ return [
|
|||
'options' => [
|
||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||
'encrypted' => true,
|
||||
'host' => env('APP_DOMAIN'),
|
||||
'port' => 6001,
|
||||
'scheme' => 'https',
|
||||
'curl_options' => [
|
||||
CURLOPT_SSL_VERIFYHOST => 0,
|
||||
CURLOPT_SSL_VERIFYPEER => 0,
|
||||
]
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ return [
|
|||
|
||||
'lc' => env('EXP_LC', false),
|
||||
'rec' => env('EXP_REC', false),
|
||||
'ns' => env('EXP_NS', false)
|
||||
'ns' => env('EXP_NS', false),
|
||||
'loops' => env('EXP_LOOPS', false)
|
||||
|
||||
];
|
|
@ -78,13 +78,10 @@ return [
|
|||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
'ap_inbox' => env('ACTIVITYPUB_INBOX', false),
|
||||
'ap_shared' => env('ACTIVITYPUB_SHAREDINBOX', false),
|
||||
'activitypub_enabled' => env('ACTIVITY_PUB', false),
|
||||
'ap_delivery_timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0),
|
||||
'ap_delivery_concurrency' => env('ACTIVITYPUB_DELIVERY_CONCURRENCY', 10),
|
||||
|
||||
'remote_follow_enabled' => env('REMOTE_FOLLOW', false),
|
||||
'activitypub_enabled' => env('ACTIVITY_PUB', false),
|
||||
'remote_follow_enabled' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
123
config/websockets.php
Normal file
123
config/websockets.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* This package comes with multi tenancy out of the box. Here you can
|
||||
* configure the different apps that can use the webSockets server.
|
||||
*
|
||||
* Optionally you can disable client events so clients cannot send
|
||||
* messages to each other via the webSockets.
|
||||
*/
|
||||
'apps' => [
|
||||
[
|
||||
'id' => env('PUSHER_APP_ID'),
|
||||
'name' => env('APP_NAME'),
|
||||
'key' => env('PUSHER_APP_KEY'),
|
||||
'secret' => env('PUSHER_APP_SECRET'),
|
||||
'enable_client_messages' => env('WSS_CM', false),
|
||||
'enable_statistics' => env('WSS_STATS', false),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* This class is responsible for finding the apps. The default provider
|
||||
* will use the apps defined in this config file.
|
||||
*
|
||||
* You can create a custom provider by implementing the
|
||||
* `AppProvider` interface.
|
||||
*/
|
||||
'app_provider' => BeyondCode\LaravelWebSockets\Apps\ConfigAppProvider::class,
|
||||
|
||||
/*
|
||||
* This array contains the hosts of which you want to allow incoming requests.
|
||||
* Leave this empty if you want to accept requests from all hosts.
|
||||
*/
|
||||
'allowed_origins' => [
|
||||
//
|
||||
],
|
||||
|
||||
/*
|
||||
* The maximum request size in kilobytes that is allowed for an incoming WebSocket request.
|
||||
*/
|
||||
'max_request_size_in_kb' => 250,
|
||||
|
||||
/*
|
||||
* This path will be used to register the necessary routes for the package.
|
||||
*/
|
||||
'path' => 'laravel-websockets',
|
||||
|
||||
/*
|
||||
* Dashboard Routes Middleware
|
||||
*
|
||||
* These middleware will be assigned to every dashboard route, giving you
|
||||
* the chance to add your own middleware to this list or change any of
|
||||
* the existing middleware. Or, you can simply stick with this list.
|
||||
*/
|
||||
'middleware' => [
|
||||
'web',
|
||||
Authorize::class,
|
||||
],
|
||||
|
||||
'statistics' => [
|
||||
/*
|
||||
* This model will be used to store the statistics of the WebSocketsServer.
|
||||
* The only requirement is that the model should extend
|
||||
* `WebSocketsStatisticsEntry` provided by this package.
|
||||
*/
|
||||
'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,
|
||||
|
||||
/*
|
||||
* Here you can specify the interval in seconds at which statistics should be logged.
|
||||
*/
|
||||
'interval_in_seconds' => 60,
|
||||
|
||||
/*
|
||||
* When the clean-command is executed, all recorded statistics older than
|
||||
* the number of days specified here will be deleted.
|
||||
*/
|
||||
'delete_statistics_older_than_days' => 60,
|
||||
|
||||
/*
|
||||
* Use an DNS resolver to make the requests to the statistics logger
|
||||
* default is to resolve everything to 127.0.0.1.
|
||||
*/
|
||||
'perform_dns_lookup' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
* Define the optional SSL context for your WebSocket connections.
|
||||
* You can see all available options at: http://php.net/manual/en/context.ssl.php
|
||||
*/
|
||||
'ssl' => [
|
||||
/*
|
||||
* Path to local certificate file on filesystem. It must be a PEM encoded file which
|
||||
* contains your certificate and private key. It can optionally contain the
|
||||
* certificate chain of issuers. The private key also may be contained
|
||||
* in a separate file specified by local_pk.
|
||||
*/
|
||||
'local_cert' => null,
|
||||
|
||||
/*
|
||||
* Path to local private key file on filesystem in case of separate files for
|
||||
* certificate (local_cert) and private key.
|
||||
*/
|
||||
'local_pk' => null,
|
||||
|
||||
/*
|
||||
* Passphrase for your local_cert file.
|
||||
*/
|
||||
'passphrase' => null,
|
||||
],
|
||||
|
||||
/*
|
||||
* Channel Manager
|
||||
* This class handles how channel persistence is handled.
|
||||
* By default, persistence is stored in an array by the running webserver.
|
||||
* The only requirement is that the class should implement
|
||||
* `ChannelManager` interface provided by this package.
|
||||
*/
|
||||
'channel_manager' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class,
|
||||
];
|
BIN
public/js/activity.js
vendored
BIN
public/js/activity.js
vendored
Binary file not shown.
BIN
public/js/components.js
vendored
BIN
public/js/components.js
vendored
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.js
vendored
Binary file not shown.
BIN
public/js/direct.js
vendored
Normal file
BIN
public/js/direct.js
vendored
Normal file
Binary file not shown.
BIN
public/js/discover.js
vendored
BIN
public/js/discover.js
vendored
Binary file not shown.
BIN
public/js/loops.js
vendored
Normal file
BIN
public/js/loops.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/search.js
vendored
BIN
public/js/search.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
Binary file not shown.
14
resources/assets/js/activity.js
vendored
14
resources/assets/js/activity.js
vendored
|
@ -1,10 +1,4 @@
|
|||
$(document).ready(function() {
|
||||
$('.pagination').hide();
|
||||
let elem = document.querySelector('.notification-page .list-group');
|
||||
let infScroll = new InfiniteScroll( elem, {
|
||||
path: '.pagination__next',
|
||||
append: '.notification-page .list-group',
|
||||
status: '.page-load-status',
|
||||
history: true,
|
||||
});
|
||||
});
|
||||
Vue.component(
|
||||
'activity-component',
|
||||
require('./components/Activity.vue').default
|
||||
);
|
238
resources/assets/js/components/Activity.vue
Normal file
238
resources/assets/js/components/Activity.vue
Normal file
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- <div class="bg-white py-4">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div></div>
|
||||
<a href="/account/activity" class="cursor-pointer font-weight-bold text-primary">Notifications</a>
|
||||
<a href="/account/direct" class="cursor-pointer font-weight-bold text-dark">Direct Messages</a>
|
||||
<a href="/account/following" class="cursor-pointer font-weight-bold text-dark">Following</a>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="container">
|
||||
<div class="row my-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div v-if="notifications.length > 0" class="media mb-3 align-items-center px-3 border-bottom pb-3" v-for="(n, index) in notifications">
|
||||
<img class="mr-2 rounded-circle" style="border:1px solid #ccc" :src="n.account.avatar" alt="" width="32px" height="32px">
|
||||
<div class="media-body font-weight-light">
|
||||
<div v-if="n.type == 'favourite'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> liked your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'comment'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'mention'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">mentioned</a> you.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'follow'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> followed you.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'share'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="n.status.reblog.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="align-items-center">
|
||||
<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="n.status && n.status && n.status.media_attachments && n.status.media_attachments.length">
|
||||
<a :href="n.status.url">
|
||||
<img :src="n.status.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
|
||||
<a :href="n.status.parent.url">
|
||||
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
|
||||
<a :href="n.status.parent.url">
|
||||
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div> -->
|
||||
<div v-else-if="n.type == 'follow' && n.relationship.following == false">
|
||||
<a href="#" class="btn btn-primary py-0 font-weight-bold" @click.prevent="followProfile(n);">
|
||||
Follow
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div v-else-if="n.status && n.status.parent && !n.status.parent.media_attachments && n.type == 'like' && n.relationship.following == false">
|
||||
<a href="#" class="btn btn-primary py-0 font-weight-bold">
|
||||
Follow
|
||||
</a>
|
||||
</div> -->
|
||||
<div v-else>
|
||||
<a class="btn btn-outline-primary py-0 font-weight-bold" :href="viewContext(n)">View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifications.length">
|
||||
<infinite-loading @infinite="infiniteNotifications">
|
||||
<div slot="no-results" class="font-weight-bold"></div>
|
||||
<div slot="no-more" class="font-weight-bold"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
<div v-if="notifications.length == 0" class="text-lighter text-center py-3">
|
||||
<p class="mb-0"><i class="fas fa-inbox fa-3x"></i></p>
|
||||
<p class="mb-0 small font-weight-bold">0 Notifications!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
notifications: {},
|
||||
notificationCursor: 2,
|
||||
notificationMaxId: 0,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchNotifications();
|
||||
},
|
||||
|
||||
updated() {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchNotifications() {
|
||||
axios.get('/api/v1/notifications', {
|
||||
params: {
|
||||
pg: true
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
let data = res.data.filter(n => {
|
||||
if(n.type == 'share' && !status) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
let ids = res.data.map(n => n.id);
|
||||
this.notificationMaxId = Math.max(...ids);
|
||||
this.notifications = data;
|
||||
$('.notification-card .loader').addClass('d-none');
|
||||
$('.notification-card .contents').removeClass('d-none');
|
||||
});
|
||||
},
|
||||
|
||||
infiniteNotifications($state) {
|
||||
if(this.notificationCursor > 10) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
axios.get('/api/v1/notifications', {
|
||||
params: {
|
||||
page: this.notificationCursor,
|
||||
pg: true
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data.length > 0) {
|
||||
let data = res.data.filter(n => {
|
||||
if(n.type == 'share' && !status) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.notifications.push(...data);
|
||||
this.notificationCursor++;
|
||||
$state.loaded();
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
truncate(text) {
|
||||
if(text.length <= 15) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0,15) + '...'
|
||||
},
|
||||
|
||||
timeAgo(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
let interval = Math.floor(seconds / 31536000);
|
||||
if (interval >= 1) {
|
||||
return interval + "y";
|
||||
}
|
||||
interval = Math.floor(seconds / 604800);
|
||||
if (interval >= 1) {
|
||||
return interval + "w";
|
||||
}
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval >= 1) {
|
||||
return interval + "d";
|
||||
}
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval >= 1) {
|
||||
return interval + "h";
|
||||
}
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval >= 1) {
|
||||
return interval + "m";
|
||||
}
|
||||
return Math.floor(seconds) + "s";
|
||||
},
|
||||
|
||||
mentionUrl(status) {
|
||||
let username = status.account.username;
|
||||
let id = status.id;
|
||||
return '/p/' + username + '/' + id;
|
||||
},
|
||||
|
||||
followProfile(n) {
|
||||
let self = this;
|
||||
let id = n.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
self.notifications.map(notification => {
|
||||
if(notification.account.id === id) {
|
||||
notification.relationship.following = true;
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
viewContext(n) {
|
||||
switch(n.type) {
|
||||
case 'follow':
|
||||
return n.account.url;
|
||||
break;
|
||||
case 'mention':
|
||||
return n.status.url;
|
||||
break;
|
||||
case 'like':
|
||||
case 'favourite':
|
||||
return n.status.url;
|
||||
break;
|
||||
}
|
||||
return '/';
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,16 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="!composeType">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-primary btn-block font-weight-bold" @click="composeType = 'post'">Compose Post</button>
|
||||
<hr>
|
||||
<!-- <button type="button" class="btn btn-outline-secondary btn-block font-weight-bold" @click="composeType = 'story'">Add Story</button> -->
|
||||
<button type="button" class="btn btn-outline-secondary btn-block font-weight-bold" @click="composeType = 'loop'">Create Loop</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="composeType == 'post'">
|
||||
<input type="file" name="media" class="d-none file-input" multiple="" v-bind:accept="config.uploader.media_types">
|
||||
<div class="timeline">
|
||||
<div class="card status-card card-md-rounded-0">
|
||||
|
@ -198,6 +209,30 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="composeType == 'loop'">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-primary btn-block font-weight-bold" @click="composeType = 'post'">Upload Loop</button>
|
||||
<hr>
|
||||
<button type="button" class="btn btn-outline-secondary btn-block font-weight-bold" @click="composeType = ''">Back</button>
|
||||
<!-- <button type="button" class="btn btn-outline-secondary btn-block font-weight-bold">Import from Coub</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-block font-weight-bold">Import from Vine</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-block font-weight-bold">Import from YouTube</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="composeType == 'story'">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-primary btn-block font-weight-bold" @click="composeType = 'post'">Add to Story</button>
|
||||
<hr>
|
||||
<button type="button" class="btn btn-outline-primary btn-block font-weight-bold" @click="composeType = 'post'">New Story</button>
|
||||
<hr>
|
||||
<button type="button" class="btn btn-outline-secondary btn-block font-weight-bold" @click="composeType = ''">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
|
@ -239,7 +274,8 @@ export default {
|
|||
mediaDrawer: false,
|
||||
composeState: 'publish',
|
||||
uploading: false,
|
||||
uploadProgress: 0
|
||||
uploadProgress: 0,
|
||||
composeType: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -293,6 +329,7 @@ export default {
|
|||
['Willow','filter-willow'],
|
||||
['X-Pro II','filter-xpro-ii']
|
||||
];
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -300,6 +337,9 @@ export default {
|
|||
fetchConfig() {
|
||||
axios.get('/api/v2/config').then(res => {
|
||||
this.config = res.data;
|
||||
if(this.config.uploader.media_types.includes('video/mp4') == false) {
|
||||
this.composeType = 'post'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -485,6 +525,7 @@ export default {
|
|||
},
|
||||
|
||||
closeModal() {
|
||||
this.composeType = '';
|
||||
$('#composeModal').modal('hide');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
|
||||
<section class="d-none d-md-flex mb-md-5 pb-md-3 px-2" style="overflow-x: hidden;" v-if="categories.length > 0">
|
||||
<a class="bg-dark rounded d-inline-flex align-items-end justify-content-center mr-3 box-shadow card-disc" href="/discover/personal">
|
||||
<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;border-bottom: 2px solid #fff;">For You</p>
|
||||
<section class="d-none d-md-flex mb-md-2 pt-2 discover-bar" style="width:auto; overflow: auto hidden;" v-if="categories.length > 0">
|
||||
<a v-if="config.ab.loops == true" class="text-decoration-none bg-transparent border border-success rounded d-inline-flex align-items-center justify-content-center mr-3 card-disc" href="/discover/loops">
|
||||
<p class="text-success lead font-weight-bold mb-0">Loops</p>
|
||||
</a>
|
||||
|
||||
<div v-show="categoryCursor > 5" class="text-dark d-inline-flex align-items-center pr-3" v-on:click="categoryPrev()">
|
||||
<i class="fas fa-chevron-circle-left fa-lg text-muted"></i>
|
||||
</div>
|
||||
<!-- <a class="text-decoration-none rounded d-inline-flex align-items-center justify-content-center mr-3 box-shadow card-disc" href="/discover/personal" style="background: rgb(255, 95, 109);">
|
||||
<p class="text-white lead font-weight-bold mb-0">For You</p>
|
||||
</a> -->
|
||||
|
||||
<a v-for="(category, index) in categories" :key="index+'_cat_'" class="bg-dark rounded d-inline-flex align-items-end justify-content-center mr-3 box-shadow card-disc" :href="category.url" :style="'background: linear-gradient(rgba(0, 0, 0, 0.3),rgba(0, 0, 0, 0.3)),url('+category.thumb+');'">
|
||||
<p class="text-white font-weight-bold" style="text-shadow: 3px 3px 16px #272634;">{{category.name}}</p>
|
||||
</a>
|
||||
|
||||
<div v-show="allCategories.length != categoryCursor" class="text-dark d-flex align-items-center" v-on:click="categoryNext()">
|
||||
<i class="fas fa-chevron-circle-right fa-lg text-muted"></i>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mb-5 section-explore">
|
||||
<div class="profile-timeline">
|
||||
|
@ -41,7 +37,11 @@
|
|||
</template>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
.discover-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.card-disc {
|
||||
flex: 0 0 160px;
|
||||
width:160px;
|
||||
height:100px;
|
||||
background-size: cover !important;
|
||||
|
@ -52,12 +52,11 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
people: {},
|
||||
config: {},
|
||||
posts: {},
|
||||
trending: {},
|
||||
categories: {},
|
||||
allCategories: {},
|
||||
categoryCursor: 5,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -75,26 +74,18 @@ export default {
|
|||
el.addClass('btn-outline-secondary').removeClass('btn-primary');
|
||||
el.text('Unfollow');
|
||||
}).catch(err => {
|
||||
swal(
|
||||
'Whoops! Something went wrong…',
|
||||
'An error occurred, please try again later.',
|
||||
'error'
|
||||
);
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchData() {
|
||||
// axios.get('/api/v2/discover/people')
|
||||
// .then((res) => {
|
||||
// let data = res.data;
|
||||
// this.people = data.people;
|
||||
|
||||
// if(this.people.length > 1) {
|
||||
// $('.section-people .loader').hide();
|
||||
// $('.section-people .row.d-none').removeClass('d-none');
|
||||
// }
|
||||
// });
|
||||
|
||||
axios.get('/api/v2/config')
|
||||
.then((res) => {
|
||||
let data = res.data;
|
||||
this.config = data;
|
||||
});
|
||||
axios.get('/api/v2/discover/posts')
|
||||
.then((res) => {
|
||||
let data = res.data;
|
||||
|
@ -111,31 +102,9 @@ export default {
|
|||
axios.get('/api/v2/discover/categories')
|
||||
.then(res => {
|
||||
this.allCategories = res.data;
|
||||
this.categories = _.slice(res.data, 0, 5);
|
||||
this.categories = res.data;
|
||||
});
|
||||
},
|
||||
|
||||
categoryNext() {
|
||||
if(this.categoryCursor > this.allCategories.length - 1) {
|
||||
return;
|
||||
}
|
||||
this.categoryCursor++;
|
||||
let cursor = this.categoryCursor;
|
||||
let start = cursor - 5;
|
||||
let end = cursor;
|
||||
this.categories = _.slice(this.allCategories, start, end);
|
||||
},
|
||||
|
||||
categoryPrev() {
|
||||
if(this.categoryCursor == 5) {
|
||||
return;
|
||||
}
|
||||
this.categoryCursor--;
|
||||
let cursor = this.categoryCursor;
|
||||
let start = cursor - 5;
|
||||
let end = cursor;
|
||||
this.categories = _.slice(this.allCategories, start, end);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
111
resources/assets/js/components/LoopComponent.vue
Normal file
111
resources/assets/js/components/LoopComponent.vue
Normal file
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<p class="text-center">
|
||||
<!-- <a :class="[tab == 'popular'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('popular')">Popular</a> -->
|
||||
<a :class="[tab == 'new'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('new')">New</a>
|
||||
<!-- <a :class="[tab == 'random'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('random')">Random</a> -->
|
||||
<a :class="[tab == 'about'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('about')">About</a>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="tab != 'about'" class="row loops-container">
|
||||
<div class="col-12 col-md-4 mb-3" v-for="(loop, index) in loops">
|
||||
<div class="card border border-success">
|
||||
<div class="embed-responsive embed-responsive-1by1">
|
||||
<video class="embed-responsive-item" :src="videoSrc(loop)" preload="auto" loop @click="toggleVideo(loop, $event)"></video>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="username font-weight-bolder"><a :href="loop.account.url">{{loop.account.acct}}</a> , <a :href="loop.url">{{timestamp(loop)}}</a></p>
|
||||
<p class="small text-muted" v-html="loop.content"></p>
|
||||
<div class="small text-muted d-flex justify-content-between mb-0">
|
||||
<span>{{loop.favourites_count}} Likes</span>
|
||||
<span>{{loop.reblogs_count}} Shares</span>
|
||||
<span>0 Loops</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="lead text-center mb-0">Loops are an exciting new way to explore short videos on Pixelfed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="text/css">
|
||||
.loops-container .card {
|
||||
box-shadow: none;
|
||||
}
|
||||
.loops-container .card .card-img-top{
|
||||
border-radius: 0;
|
||||
}
|
||||
.loops-container a {
|
||||
color: #343a40;
|
||||
}
|
||||
a.hashtag,
|
||||
.loops-container .card-body a:hover {
|
||||
color: #28a745 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
Object.defineProperty(HTMLMediaElement.prototype, 'playing', {
|
||||
get: function(){
|
||||
return !!(this.currentTime > 0 && !this.paused && !this.ended && this.readyState > 2);
|
||||
}
|
||||
})
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
'version': 1,
|
||||
'loops': [],
|
||||
'tab': 'new'
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
axios.get('/api/v2/loops')
|
||||
.then(res => {
|
||||
this.loops = res.data;
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
videoSrc(loop) {
|
||||
return loop.media_attachments[0].url;
|
||||
},
|
||||
setTab(tab) {
|
||||
this.tab = tab;
|
||||
},
|
||||
toggleVideo(loop, $event) {
|
||||
let el = $event.target;
|
||||
$('video').each(function() {
|
||||
if(el.src != $(this)[0].src) {
|
||||
$(this)[0].pause();
|
||||
}
|
||||
});
|
||||
if(!el.playing) {
|
||||
el.play();
|
||||
this.incrementLoop(loop);
|
||||
} else {
|
||||
el.pause();
|
||||
}
|
||||
},
|
||||
incrementLoop(loop) {
|
||||
axios.post('/api/v2/loops/watch', {
|
||||
id: loop.id
|
||||
}).then(res => {
|
||||
console.log(res.data);
|
||||
});
|
||||
},
|
||||
timestamp(loop) {
|
||||
let ts = new Date(loop.created_at);
|
||||
return ts.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -19,31 +19,31 @@
|
|||
<div class="media-body font-weight-light small">
|
||||
<div v-if="n.type == 'favourite'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> liked your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> liked your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'comment'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'mention'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">mentioned</a> you.
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">mentioned</a> you.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'follow'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> followed you.
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> followed you.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'share'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="n.status.reblog.url">post</a>.
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="n.status.reblog.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</div>
|
||||
<div class="small text-muted" :title="n.created_at">{{timeAgo(n.created_at)}}</div>
|
||||
</div>
|
||||
<div v-if="notifications.length">
|
||||
<infinite-loading @infinite="infiniteNotifications">
|
||||
|
@ -73,13 +73,10 @@
|
|||
},
|
||||
|
||||
mounted() {
|
||||
if(window.outerWidth > 767) {
|
||||
this.fetchNotifications();
|
||||
}
|
||||
this.fetchNotifications();
|
||||
},
|
||||
|
||||
updated() {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -950,6 +950,10 @@ export default {
|
|||
this.profile.followers_count++;
|
||||
}
|
||||
this.relationship.following = !this.relationship.following;
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1064,7 +1068,11 @@ export default {
|
|||
this.following.splice(index, 1);
|
||||
this.profile.following_count--;
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
momentBackground() {
|
||||
|
|
|
@ -148,6 +148,10 @@ export default {
|
|||
item: id
|
||||
}).then(res => {
|
||||
window.location.href = window.location.href;
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1083,7 +1083,11 @@
|
|||
item: id
|
||||
}).then(res => {
|
||||
this.suggestions.splice(index, 1);
|
||||
})
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
followModalAction(id, index, type = 'following') {
|
||||
|
@ -1093,7 +1097,11 @@
|
|||
if(type == 'following') {
|
||||
this.following.splice(index, 1);
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
owner(status) {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
>
|
||||
<b-carousel-slide v-for="(media, index) in status.media_attachments" :key="media.id + '-media'">
|
||||
|
||||
<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls loop :alt="media.description" width="100%" height="100%">
|
||||
<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls loop :alt="media.description" width="100%" height="100%" :poster="media.preview_url">
|
||||
<source :src="media.url" :type="media.mime">
|
||||
</video>
|
||||
|
||||
|
@ -38,7 +38,7 @@
|
|||
>
|
||||
<b-carousel-slide v-for="(media, index) in status.media_attachments" :key="media.id + '-media'">
|
||||
|
||||
<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls loop :alt="media.description" width="100%" height="100%">
|
||||
<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls loop :alt="media.description" width="100%" height="100%" :poster="media.preview_url">
|
||||
<source :src="media.url" :type="media.mime">
|
||||
</video>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
:interval="0"
|
||||
>
|
||||
<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
|
||||
<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%">
|
||||
<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
|
||||
<source :src="vid.url" :type="vid.mime">
|
||||
</video>
|
||||
</b-carousel-slide>
|
||||
|
@ -29,7 +29,7 @@
|
|||
:interval="0"
|
||||
>
|
||||
<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
|
||||
<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%">
|
||||
<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
|
||||
<source :src="vid.url" :type="vid.mime">
|
||||
</video>
|
||||
</b-carousel-slide>
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
<p class="font-weight-light">(click to show)</p>
|
||||
</summary>
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<video class="video" preload="none" controls loop>
|
||||
<video class="video" preload="none" controls loop :poster="status.media_attachments[0].preview_url">
|
||||
<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
|
||||
</video>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div v-else class="embed-responsive embed-responsive-16by9">
|
||||
<video class="video" preload="none" controls loop>
|
||||
<video class="video" preload="auto" controls loop :poster="status.media_attachments[0].preview_url">
|
||||
<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
|
||||
</video>
|
||||
</div>
|
||||
|
@ -21,6 +21,6 @@
|
|||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
props: ['status']
|
||||
props: ['status'],
|
||||
}
|
||||
</script>
|
4
resources/assets/js/direct.js
vendored
Normal file
4
resources/assets/js/direct.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'direct-component',
|
||||
require('./components/Direct.vue').default
|
||||
);
|
4
resources/assets/js/loops.js
vendored
Normal file
4
resources/assets/js/loops.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'loops-component',
|
||||
require('./components/LoopComponent.vue').default
|
||||
);
|
|
@ -1,145 +1,14 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container notification-page" style="min-height: 60vh;">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div class="card mt-3">
|
||||
<div class="card-body p-0">
|
||||
<ul class="nav nav-pills d-flex text-center">
|
||||
<li class="nav-item flex-fill">
|
||||
<a class="nav-link font-weight-bold text-uppercase active" href="{{route('notifications')}}">Notifications</a>
|
||||
</li>
|
||||
<li class="nav-item flex-fill">
|
||||
<a class="nav-link font-weight-bold text-uppercase" href="{{route('follow-requests')}}">Follow Requests</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="dropdown text-right mt-2">
|
||||
<a class="btn btn-link btn-sm dropdown-toggle font-weight-bold text-dark" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Filter
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||
<a href="?a=comment" class="dropdown-item font-weight-bold" title="Commented on your post">
|
||||
Comments only
|
||||
</a>
|
||||
<a href="?a=follow" class="dropdown-item font-weight-bold" title="Followed you">
|
||||
New Followers only
|
||||
</a>
|
||||
<a href="?a=mention" class="dropdown-item font-weight-bold" title="Mentioned you">
|
||||
Mentions only
|
||||
</a>
|
||||
<a href="{{route('notifications')}}" class="dropdown-item font-weight-bold text-dark">
|
||||
View All
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
|
||||
@if($notifications->count() > 0)
|
||||
@foreach($notifications as $notification)
|
||||
<li class="list-group-item notification border-0">
|
||||
@switch($notification->action)
|
||||
|
||||
@case('like')
|
||||
<span class="notification-icon pr-3">
|
||||
<img src="{{optional($notification->actor, function($actor) {
|
||||
return $actor->avatarUrl(); }) }}" width="32px" class="rounded-circle">
|
||||
</span>
|
||||
<span class="notification-text">
|
||||
{!! $notification->rendered !!}
|
||||
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
|
||||
</span>
|
||||
<span class="float-right notification-action">
|
||||
@if($notification->item_id && $notification->item_type == 'App\Status')
|
||||
<a href="{{$notification->status->url()}}"><img src="{{$notification->status->thumb()}}" width="32px" height="32px"></a>
|
||||
@endif
|
||||
</span>
|
||||
@break
|
||||
|
||||
@case('follow')
|
||||
<span class="notification-icon pr-3">
|
||||
<img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
|
||||
</span>
|
||||
<span class="notification-text">
|
||||
{!! $notification->rendered !!}
|
||||
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
|
||||
</span>
|
||||
@if($notification->actor->followedBy(Auth::user()->profile) == false)
|
||||
<span class="float-right notification-action">
|
||||
<form class="follow-form" method="post" action="/i/follow" style="display: inline;" data-id="{{$notification->actor->id}}" data-action="follow">
|
||||
@csrf
|
||||
<input type="hidden" name="item" value="{{$notification->actor->id}}">
|
||||
<button class="btn btn-primary font-weight-bold px-4 py-0" type="submit">Follow</button>
|
||||
</form>
|
||||
</span>
|
||||
@endif
|
||||
@break
|
||||
|
||||
@case('comment')
|
||||
<span class="notification-icon pr-3">
|
||||
<img src="{{$notification->actor->avatarUrl()}}" width="32px" class="rounded-circle">
|
||||
</span>
|
||||
<span class="notification-text">
|
||||
{!! $notification->rendered !!}
|
||||
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
|
||||
</span>
|
||||
<span class="float-right notification-action">
|
||||
@if($notification->item_id && $notification->item_type == 'App\Status')
|
||||
@if($notification->status->parent())
|
||||
<a href="{{$notification->status->parent()->url()}}">
|
||||
<div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div>
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
@break
|
||||
|
||||
@case('mention')
|
||||
<span class="notification-icon pr-3">
|
||||
<img src="{{$notification->status->profile->avatarUrl()}}" width="32px" class="rounded-circle">
|
||||
</span>
|
||||
<span class="notification-text">
|
||||
{!! $notification->rendered !!}
|
||||
<span class="text-muted notification-timestamp pl-1">{{$notification->created_at->diffForHumans(null, true, true, true)}}</span>
|
||||
</span>
|
||||
<span class="float-right notification-action">
|
||||
@if($notification->item_id && $notification->item_type === 'App\Status')
|
||||
@if(is_null($notification->status->in_reply_to_id))
|
||||
<a href="{{$notification->status->url()}}">
|
||||
<div class="notification-image" style="background-image: url('{{$notification->status->thumb()}}')"></div>
|
||||
</a>
|
||||
@else
|
||||
<a href="{{$notification->status->parent()->url()}}">
|
||||
<div class="notification-image" style="background-image: url('{{$notification->status->parent()->thumb()}}')"></div>
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
@break
|
||||
|
||||
@endswitch
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<div class="d-flex justify-content-center my-4">
|
||||
{{$notifications->links()}}
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-info font-weight-bold">No unread notifications found.</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<activity-component></activity-component>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ mix('js/activity.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
|
|
93
resources/views/account/circles/create.blade.php
Normal file
93
resources/views/account/circles/create.blade.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 py-5 d-flex justify-content-between align-items-center">
|
||||
<p class="h1 mb-0"><i class="far fa-circle"></i> Create Circle</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-10 offset-md-1">
|
||||
<div class="card">
|
||||
<div class="card-body px-5">
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="post">
|
||||
@csrf
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" placeholder="Circle Name" name="name" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" placeholder="Optional description visible only to you" rows="3" name="description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Visibility</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" name="scope">
|
||||
<option value="public">Public</option>
|
||||
<option value="unlisted">Unlisted</option>
|
||||
<option value="private">Followers Only</option>
|
||||
<option value="exclusive">Circle Only</option>
|
||||
</select>
|
||||
<p class="help-text font-weight-bold text-muted small">Who can view posts from this circle</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-2 font-weight-bold text-muted">BCC Mode</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="bcc">
|
||||
<label class="form-check-label"></label>
|
||||
</div>
|
||||
<p class="help-text mb-0 small text-muted">Send posts without mentioning other circle recipients.</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Members</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10 offset-sm-2">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" name="active" id="activeSwitch">
|
||||
<label class="custom-control-label font-weight-bold text-muted" for="activeSwitch">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-right mb-0">
|
||||
<button type="submit" class="btn btn-primary btn-sm py-1 font-weight-bold">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@endpush
|
17
resources/views/account/direct.blade.php
Normal file
17
resources/views/account/direct.blade.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div>
|
||||
<direct-component></direct-component>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ mix('js/direct.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
});
|
||||
</script>
|
||||
@endpush
|
39
resources/views/discover/loops/home.blade.php
Normal file
39
resources/views/discover/loops/home.blade.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-success" style="height:1.2px;"></div>
|
||||
<div class="profile-header">
|
||||
<div class="container pt-5">
|
||||
<div class="profile-details text-center">
|
||||
<div class="username-bar text-dark">
|
||||
<p class="display-4 font-weight-bold mb-0"><span class="text-success">Loops</span> <sup class="lead">BETA</sup></p>
|
||||
<p class="lead font-weight-lighter">Short looping videos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loop-page container mt-5">
|
||||
<section>
|
||||
<loops-component></loops-component>
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style type="text/css">
|
||||
@media (min-width: 1200px) {
|
||||
.loop-page.container {
|
||||
max-width: 1035px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/loops.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
new Vue({el: '#content'});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
|
@ -56,19 +56,19 @@
|
|||
<div class="card card-body rounded-0 py-2 d-flex align-items-middle box-shadow" style="border-top:1px solid #F1F5F8">
|
||||
<ul class="nav nav-pills nav-fill">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->is('/')?'text-primary':'text-muted'}}" href="/"><i class="fas fa-home fa-lg"></i></a>
|
||||
<a class="nav-link {{request()->is('/')?'text-dark':'text-muted'}}" href="/"><i class="fas fa-home fa-lg"></i></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->is('timeline/public')?'text-primary':'text-muted'}}" href="/timeline/public"><i class="far fa-map fa-lg"></i></a>
|
||||
<a class="nav-link {{request()->is('timeline/public')?'text-dark':'text-muted'}}" href="/timeline/public"><i class="far fa-map fa-lg"></i></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="nav-link text-black cursor-pointer" data-toggle="modal" data-target="#composeModal"><i class="far fa-plus-square fa-lg"></i></div>
|
||||
<div class="nav-link text-primary cursor-pointer" data-toggle="modal" data-target="#composeModal"><i class="fas fa-camera-retro fa-lg"></i></div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->is('discover')?'text-primary':'text-muted'}}" href="{{route('discover')}}"><i class="far fa-compass fa-lg"></i></a>
|
||||
<a class="nav-link {{request()->is('discover')?'text-dark':'text-muted'}}" href="{{route('discover')}}"><i class="far fa-compass fa-lg"></i></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->is('account/activity')?'text-primary':'text-muted'}} tooltip-notification" href="/account/activity"><i class="far fa-bell fa-lg"></i></a>
|
||||
<a class="nav-link {{request()->is('account/activity')?'text-dark':'text-muted'}} tooltip-notification" href="/account/activity"><i class="far fa-bell fa-lg"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -27,18 +27,18 @@
|
|||
<ul class="navbar-nav ml-auto">
|
||||
<div class="d-none d-md-block">
|
||||
<li class="nav-item px-md-2">
|
||||
<a class="nav-link font-weight-bold {{request()->is('/') ?'text-primary':''}}" href="/" title="Home Timeline" data-toggle="tooltip" data-placement="bottom">
|
||||
<a class="nav-link font-weight-bold {{request()->is('/') ?'text-dark':'text-muted'}}" href="/" title="Home Timeline" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="fas fa-home fa-lg"></i>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<div class="d-none d-md-block">
|
||||
{{-- <div class="d-none d-md-block">
|
||||
<li class="nav-item px-md-2">
|
||||
<a class="nav-link font-weight-bold {{request()->is('timeline/public') ?'text-primary':''}}" href="/timeline/public" title="Public Timeline" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-map fa-lg"></i>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</div> --}}
|
||||
|
||||
<li class="d-block d-md-none">
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
|||
</li> --}}
|
||||
<div class="d-none d-md-block">
|
||||
<li class="nav-item px-md-2">
|
||||
<a class="nav-link font-weight-bold {{request()->is('*discover*') ?'text-primary':''}}" href="{{route('discover')}}" title="Discover" data-toggle="tooltip" data-placement="bottom">
|
||||
<a class="nav-link font-weight-bold {{request()->is('*discover*') ?'text-dark':'text-muted'}}" href="{{route('discover')}}" title="Discover" data-toggle="tooltip" data-placement="bottom">
|
||||
<i class="far fa-compass fa-lg"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -60,7 +60,7 @@
|
|||
<li class="nav-item px-md-2">
|
||||
<div title="Create new post" data-toggle="tooltip" data-placement="bottom">
|
||||
<a href="{{route('compose')}}" class="nav-link" data-toggle="modal" data-target="#composeModal">
|
||||
<i class="far fa-plus-square fa-lg text-dark"></i>
|
||||
<i class="fas fa-camera-retro fa-lg text-primary"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<p class="font-weight-bold h5 pb-3">How to use Discover</p>
|
||||
<ul>
|
||||
<li class="mb-3 ">Click the <i class="far fa-compass fa-sm"></i> icon.</li>
|
||||
<li class="mb-3 ">View the latest posts from accounts you don't already follow.</li>
|
||||
<li class="mb-3 ">View the latest posts.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="py-4">
|
||||
|
@ -36,7 +36,6 @@
|
|||
<ul class="pt-3">
|
||||
<li class="lead mb-4">To make your posts more discoverable, add hashtags to your posts.</li>
|
||||
<li class="lead mb-4">Any public posts that contain a hashtag may be included in discover pages.</li>
|
||||
<li class="lead ">No algorithms or behavioral tracking are used in the Discover feature. It may be less personalized than other platforms.</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
<ul>
|
||||
<li class="mb-3 ">People use the hashtag symbol (#) before a relevant phrase or keyword in their post to categorize those posts and make them more discoverable.</li>
|
||||
<li class="mb-3 ">Any hashtags will be linked to a hashtag page with other posts containing the same hashtag.</li>
|
||||
<li class="">Hashtags can be used anywhere in a post.</li>
|
||||
<li class="mb-3">Hashtags can be used anywhere in a post.</li>
|
||||
<li class="">You can add up to 30 hashtags to your post or comment.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;">
|
||||
|
|
|
@ -163,4 +163,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse11" role="button" aria-expanded="false" aria-controls="collapse11">
|
||||
<i class="fas fa-chevron-down mr-2"></i>
|
||||
How many people can I tag or mention in my comments or posts?
|
||||
</a>
|
||||
<div class="collapse" id="collapse11">
|
||||
<div>
|
||||
You can tag or mention up to 5 profiles per comment or post.
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
@endsection
|
|
@ -6,7 +6,7 @@
|
|||
<h3 class="font-weight-bold">Timelines</h3>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="lead">Timelines are chronological feeds of posts from accounts you follow or from other instances.</p>
|
||||
<p class="lead">Timelines are chronological feeds of posts.</p>
|
||||
<p class="font-weight-bold h5 py-3">Pixelfed has 2 different timelines:</p>
|
||||
|
||||
<ul>
|
||||
|
@ -26,4 +26,15 @@
|
|||
<span class="font-weight-light text-muted">Timeline with posts from local and remote accounts - coming soon!</span>
|
||||
</li> --}}
|
||||
</ul>
|
||||
<div class="py-3"></div>
|
||||
<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">Timeline Tips</div>
|
||||
<div class="card-body bg-white p-3">
|
||||
<ul class="pt-3">
|
||||
<li class="lead mb-4">You can mute or block accounts to prevent them from appearing in timelines.</li>
|
||||
<li class="lead mb-4">You can create <span class="font-weight-bold">Unlisted</span> posts that don't appear in public timelines.</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
|
@ -62,6 +62,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('discover/c/{slug}', 'DiscoverController@showCategory');
|
||||
Route::get('discover/personal', 'DiscoverController@showPersonal');
|
||||
Route::get('discover', 'DiscoverController@home')->name('discover');
|
||||
Route::get('discover/loops', 'DiscoverController@showLoops');
|
||||
|
||||
Route::group(['prefix' => 'api'], function () {
|
||||
Route::get('search', 'SearchController@searchAPI');
|
||||
|
@ -95,6 +96,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::post('moderator/action', 'InternalApiController@modAction');
|
||||
Route::get('discover/categories', 'InternalApiController@discoverCategories');
|
||||
Route::post('status/compose', 'InternalApiController@composePost');
|
||||
Route::get('loops', 'DiscoverController@loopsApi');
|
||||
Route::post('loops/watch', 'DiscoverController@loopWatch');
|
||||
});
|
||||
Route::group(['prefix' => 'local'], function () {
|
||||
Route::get('i/follow-suggestions', 'ApiController@followSuggestions');
|
||||
|
|
|
@ -7,6 +7,7 @@ use Illuminate\Foundation\Testing\WithFaker;
|
|||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use App\Status;
|
||||
|
||||
class StatusLexerTest extends TestCase
|
||||
{
|
||||
|
@ -59,7 +60,7 @@ class StatusLexerTest extends TestCase
|
|||
|
||||
public function testAutolink()
|
||||
{
|
||||
$expected = '@<a class="u-url mention" href="https://pixelfed.dev/pixelfed" rel="external nofollow noopener" target="_blank">pixelfed</a> hi, really like the website! <a href="https://pixelfed.dev/discover/tags/píxelfed?src=hash" title="#píxelfed" class="u-url hashtag" rel="external nofollow noopener">#píxelfed</a>';
|
||||
$expected = '<a class="u-url mention" href="https://pixelfed.dev/pixelfed" rel="external nofollow noopener" target="_blank">@pixelfed</a> hi, really like the website! <a href="https://pixelfed.dev/discover/tags/píxelfed?src=hash" title="#píxelfed" class="u-url hashtag" rel="external nofollow noopener">#píxelfed</a>';
|
||||
$this->assertEquals($this->autolink, $expected);
|
||||
}
|
||||
|
||||
|
@ -106,4 +107,35 @@ class StatusLexerTest extends TestCase
|
|||
$actual = Extractor::create()->extract('#dansup @dansup@mstdn.io @test');
|
||||
$this->assertEquals($actual, $expected);
|
||||
}
|
||||
|
||||
/** @test **/
|
||||
public function mentionLimit()
|
||||
{
|
||||
$text = '@test1 @test @test2 @test3 @test4 @test5 test post';
|
||||
|
||||
$entities = Extractor::create()->extract($text);
|
||||
$count = count($entities['mentions']);
|
||||
$this->assertEquals($count, Status::MAX_MENTIONS);
|
||||
}
|
||||
|
||||
/** @test **/
|
||||
public function hashtagLimit()
|
||||
{
|
||||
$text = '#hashtag0 #hashtag1 #hashtag2 #hashtag3 #hashtag4 #hashtag5 #hashtag6 #hashtag7 #hashtag8 #hashtag9 #hashtag10 #hashtag11 #hashtag12 #hashtag13 #hashtag14 #hashtag15 #hashtag16 #hashtag17 #hashtag18 #hashtag19 #hashtag20 #hashtag21 #hashtag22 #hashtag23 #hashtag24 #hashtag25 #hashtag26 #hashtag27 #hashtag28 #hashtag29 #hashtag30 #hashtag31';
|
||||
|
||||
$entities = Extractor::create()->extract($text);
|
||||
$count = count($entities['hashtags']);
|
||||
$this->assertEquals($count, Status::MAX_HASHTAGS);
|
||||
}
|
||||
|
||||
|
||||
/** @test **/
|
||||
public function linkLimit()
|
||||
{
|
||||
$text = 'https://example.org https://example.net https://example.com';
|
||||
|
||||
$entities = Extractor::create()->extract($text);
|
||||
$count = count($entities['urls']);
|
||||
$this->assertEquals($count, Status::MAX_LINKS);
|
||||
}
|
||||
}
|
||||
|
|
6
webpack.mix.js
vendored
6
webpack.mix.js
vendored
|
@ -37,6 +37,12 @@ mix.js('resources/assets/js/app.js', 'public/js')
|
|||
// Developer Components
|
||||
.js('resources/assets/js/developers.js', 'public/js')
|
||||
|
||||
// // Direct Component
|
||||
// .js('resources/assets/js/direct.js', 'public/js')
|
||||
|
||||
// Loops Component
|
||||
.js('resources/assets/js/loops.js', 'public/js')
|
||||
|
||||
.sass('resources/assets/sass/app.scss', 'public/css', {
|
||||
implementation: require('node-sass')
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue