Merge pull request #2083 from pixelfed/staging

Staging
This commit is contained in:
daniel 2020-04-17 19:44:22 -06:00 committed by GitHub
commit 66db7a4d9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 5390 additions and 910 deletions

31
.dependabot/config.yml Normal file
View file

@ -0,0 +1,31 @@
version: 1
update_configs:
- package_manager: "php:composer"
directory: "/"
update_schedule: "daily"
# Supported update schedule: live daily weekly monthly
target_branch: "staging"
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security
- package_manager: "javascript"
directory: "/"
update_schedule: "daily"
# Supported update schedule: live daily weekly monthly
target_branch: "staging"
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security

143
.env.docker Normal file
View file

@ -0,0 +1,143 @@
## Crypto
APP_KEY=
## General Settings
APP_NAME="Pixelfed Prod"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://real.domain
APP_DOMAIN="real.domain"
ADMIN_DOMAIN="real.domain"
SESSION_DOMAIN="real.domain"
OPEN_REGISTRATION=true
ENFORCE_EMAIL_VERIFICATION=false
PF_MAX_USERS=1000
OAUTH_ENABLED=true
APP_TIMEZONE=UTC
APP_LOCALE=en
## Pixelfed Tweaks
LIMIT_ACCOUNT_SIZE=true
MAX_ACCOUNT_SIZE=1000000
MAX_PHOTO_SIZE=15000
MAX_AVATAR_SIZE=2000
MAX_CAPTION_LENGTH=500
MAX_BIO_LENGTH=125
MAX_NAME_LENGTH=30
MAX_ALBUM_LENGTH=4
IMAGE_QUALITY=80
PF_OPTIMIZE_IMAGES=true
PF_OPTIMIZE_VIDEOS=true
ADMIN_ENV_EDITOR=false
ACCOUNT_DELETION=true
ACCOUNT_DELETE_AFTER=false
MAX_LINKS_PER_POST=0
## Instance
#INSTANCE_DESCRIPTION=
INSTANCE_PUBLIC_HASHTAGS=false
#INSTANCE_CONTACT_EMAIL=
INSTANCE_PUBLIC_LOCAL_TIMELINE=false
#BANNED_USERNAMES=
STORIES_ENABLED=false
RESTRICTED_INSTANCE=false
## Mail
MAIL_DRIVER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_FROM_ADDRESS="pixelfed@example.com"
MAIL_FROM_NAME="Pixelfed"
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
## Databases (MySQL)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pixelfed
DB_USERNAME=pixelfed
DB_PASSWORD=pixelfed
## Databases (Postgres)
#DB_CONNECTION=pgsql
#DB_HOST=postgres
#DB_PORT=5432
#DB_DATABASE=pixelfed
#DB_USERNAME=postgres
#DB_PASSWORD=postgres
## Cache (Redis)
REDIS_CLIENT=phpredis
REDIS_SCHEME=tcp
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DATABASE=0
## EXPERIMENTS
EXP_LC=false
EXP_REC=false
EXP_LOOPS=false
## ActivityPub Federation
ACTIVITY_PUB=false
AP_REMOTE_FOLLOW=false
AP_SHAREDINBOX=false
AP_INBOX=false
AP_OUTBOX=false
ATOM_FEEDS=true
NODEINFO=true
WEBFINGER=true
## S3
FILESYSTEM_DRIVER=local
FILESYSTEM_CLOUD=s3
PF_ENABLE_CLOUD=false
#AWS_ACCESS_KEY_ID=
#AWS_SECRET_ACCESS_KEY=
#AWS_DEFAULT_REGION=
#AWS_BUCKET=
#AWS_URL=
#AWS_ENDPOINT=
#AWS_USE_PATH_STYLE_ENDPOINT=false
## Horizon
HORIZON_DARKMODE=false
## COSTAR - Confirm Object Sentiment Transform and Reduce
PF_COSTAR_ENABLED=false
# Media
MEDIA_EXIF_DATABASE=false
## Logging
LOG_CHANNEL=stack
## Image
IMAGE_DRIVER=imagick
## Broadcasting
BROADCAST_DRIVER=log # log driver for local development
## Cache
CACHE_DRIVER=redis
## Purify
RESTRICT_HTML_TYPES=true
## Queue
QUEUE_DRIVER=redis
## Session
SESSION_DRIVER=redis
## Trusted Proxy
TRUST_PROXIES="*"
## Passport
#PASSPORT_PRIVATE_KEY=
#PASSPORT_PUBLIC_KEY=

View file

@ -1,52 +0,0 @@
APP_NAME="Pixelfed Prod"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_DOMAIN="localhost"
ADMIN_DOMAIN="localhost"
SESSION_DOMAIN="localhost"
TRUST_PROXIES="*"
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=pixelfed
DB_USERNAME=pixelfed
DB_PASSWORD=pixelfed
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_DRIVER=redis
REDIS_SCHEME=tcp
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="pixelfed@example.com"
MAIL_FROM_NAME="Pixelfed"
OPEN_REGISTRATION=true
ENFORCE_EMAIL_VERIFICATION=true
PF_MAX_USERS=1000
MAX_PHOTO_SIZE=15000
MAX_CAPTION_LENGTH=150
MAX_ALBUM_LENGTH=4
ACTIVITY_PUB=false
AP_REMOTE_FOLLOW=false
AP_INBOX=false
PF_COSTAR_ENABLED=false

View file

@ -2,6 +2,12 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.8...dev)
### Added
- Added Profile Following Search ([e3280c11](https://github.com/pixelfed/pixelfed/commit/e3280c11))
- Added Trusted Devices to Sudo Mode ([0c82c970](https://github.com/pixelfed/pixelfed/commit/0c82c970))
- Added reply modal to posts and timelines ([974e6bda](https://github.com/pixelfed/pixelfed/commit/974e6bda))
- Added remote posts and profiles ([95bce31e](https://github.com/pixelfed/pixelfed/commit/95bce31e))
- Added Labs deprecation page ([9b215001](https://github.com/pixelfed/pixelfed/commit/9b215001))
- Added new landing page ([84e203a9](https://github.com/pixelfed/pixelfed/commit/84e203a9))
### Fixed
- Stories on postgres instances ([5ffa71da](https://github.com/pixelfed/pixelfed/commit/5ffa71da))
@ -23,8 +29,34 @@
- Updated AdminUserController, add moderation method ([a4cf21ea](https://github.com/pixelfed/pixelfed/commit/a4cf21ea))
- Updated BaseApiController, invalidate session after account deletion ([826978ce](https://github.com/pixelfed/pixelfed/commit/826978ce))
- Updated AdminUserController, add account deletion handler ([9be19ad8](https://github.com/pixelfed/pixelfed/commit/9be19ad8))
- Updated ContactController, fixes #2042 ([c9057e87](https://github.com/pixelfed/pixelfed/commit/c9057e87))
- Updated ContactController, fixes [#2042](https://github.com/pixelfed/pixelfed/issues/2042) ([c9057e87](https://github.com/pixelfed/pixelfed/commit/c9057e87))
- Updated Media model, fix remote media preview ([9947050b](https://github.com/pixelfed/pixelfed/commit/9947050b))
- Updated PostComponent, improve likes modal ([664fd272](https://github.com/pixelfed/pixelfed/commit/664fd272))
- Updated StoryViewer, preload media ([336571d0](https://github.com/pixelfed/pixelfed/commit/336571d0))
- Updated StoryCompose, add expand label for lightbox preview ([fdf59753](https://github.com/pixelfed/pixelfed/commit/fdf59753))
- Updated session config, increase session timeout from 2 days to 60 days ([b8795271](https://github.com/pixelfed/pixelfed/commit/b8795271))
- Updated WebfingerService, cache lookup ([8b9faf31](https://github.com/pixelfed/pixelfed/commit/8b9faf31))
- Updated v1 notifications api, fix optional params ([4e3c952c](https://github.com/pixelfed/pixelfed/commit/4e3c952c))
- Updated ApiV1Controller, fix unfavourite bug [#2088](https://github.com/pixelfed/pixelfed/issues/2088) ([3a828522](https://github.com/pixelfed/pixelfed/commit/3a828522))
- Updated SharePipeline, fix item relation bug ([b5899648](https://github.com/pixelfed/pixelfed/commit/b5899648))
- Updated Profile.vue, add v-once to thumbnails to prevent re-render ([a54685f6](https://github.com/pixelfed/pixelfed/commit/a54685f6))
- Updated SearchResults.vue, improve layout ([7e41b4ae](https://github.com/pixelfed/pixelfed/commit/7e41b4ae))
- Updated PostMenu.vue, fix styling of list-group ([4c3b0b7d](https://github.com/pixelfed/pixelfed/commit/4c3b0b7d))
- Updated PostComponent.vue, update styling ([844566b9](https://github.com/pixelfed/pixelfed/commit/844566b9))
- Updated NotificationCard.vue, fix share notifications ([3cb676b1](https://github.com/pixelfed/pixelfed/commit/3cb676b1))
- Updated PostComponent.vue, remove like count from title, fixes [#2091](https://github.com/pixelfed/pixelfed/issues/2091) ([6026998c](https://github.com/pixelfed/pixelfed/commit/6026998c))
- Updated SearchController, add WebfingerService support ([869b4ff7](https://github.com/pixelfed/pixelfed/commit/869b4ff7))
- Updated Profile model, use change_count for version ([0eae9f8b](https://github.com/pixelfed/pixelfed/commit/0eae9f8b))
- Updated Timeline.vue, add remote post/profile links ([d4147083](https://github.com/pixelfed/pixelfed/commit/d4147083))
- Updated StoryTimelineComponent, added list prop for new timeline layout ([1692a95a](https://github.com/pixelfed/pixelfed/commit/1692a95a))
- Updated blank layout, add sharedData js ([4a293ed9](https://github.com/pixelfed/pixelfed/commit/4a293ed9))
- Updated oauth api, allow multiple redirect_uris. Fixes #[2106](https://github.com/pixelfed/pixelfed/issues/2106) ([0540a28a](https://github.com/pixelfed/pixelfed/commit/0540a28a))
- Updated ActivityPub Outbox, fixes #[2100](https://github.com/pixelfed/pixelfed/issues/2100) ([c84cee5a](https://github.com/pixelfed/pixelfed/commit/c84cee5a))
- Updated ApiV1Controller, fixes #[2112](https://github.com/pixelfed/pixelfed/issues/2112) ([324ccd0a](https://github.com/pixelfed/pixelfed/commit/324ccd0a))
- Updated StatusTransformer, fixes #[2113](https://github.com/pixelfed/pixelfed/issues/2113) ([eefa6e0d](https://github.com/pixelfed/pixelfed/commit/eefa6e0d))
- Updated InternalApiController, limit remote profile ui to remote profiles ([d918a68e](https://github.com/pixelfed/pixelfed/commit/d918a68e))
- Updated NotificationCard, fix pagination bug #[2019](https://github.com/pixelfed/pixelfed/issues/2019) ([32beaad5](https://github.com/pixelfed/pixelfed/commit/32beaad5))
## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
### Added
@ -39,6 +71,7 @@
- Fixed TRUST_PROXIES configuration ([#1941](https://github.com/pixelfed/pixelfed/pull/1941))
- Fixed settings page default language ([4223a11e](https://github.com/pixelfed/pixelfed/commit/4223a11e))
- Fixed DeleteAccountPipeline bug that did not use proper media paths ([578d2f35](https://github.com/pixelfed/pixelfed/commit/578d2f35))
- Fixed mastoapi StatusTransformer, fix in_reply_to_id cast to string instead of int ([6ed00c94](https://github.com/pixelfed/pixelfed/commit/6ed00c94))
### Updated
- Updated presenter components, load fallback image on errors ([273170c5](https://github.com/pixelfed/pixelfed/commit/273170c5))

View file

@ -54,7 +54,7 @@ class StoryGC extends Command
{
$day = now()->day;
if($day !== 3) {
if($day != 3) {
return;
}
@ -81,12 +81,12 @@ class StoryGC extends Command
protected function deleteViews()
{
StoryView::where('created_at', '<', now()->subDays(2))->delete();
StoryView::where('created_at', '<', now()->subMinutes(1441))->delete();
}
protected function deleteStories()
{
$stories = Story::where('expires_at', '<', now())->take(50)->get();
$stories = Story::where('created_at', '<', now()->subMinutes(1441))->take(50)->get();
if($stories->count() == 0) {
exit;

View file

@ -374,10 +374,13 @@ class AccountController extends Controller
public function sudoModeVerify(Request $request)
{
$this->validate($request, [
'password' => 'required|string|max:500'
'password' => 'required|string|max:500',
'trustDevice' => 'nullable'
]);
$user = Auth::user();
$password = $request->input('password');
$trustDevice = $request->input('trustDevice') == 'on';
$next = $request->session()->get('redirectNext', '/');
if($request->session()->has('sudoModeAttempts')) {
$count = (int) $request->session()->get('sudoModeAttempts');
@ -387,6 +390,9 @@ class AccountController extends Controller
}
if(password_verify($password, $user->password) === true) {
$request->session()->put('sudoMode', time());
if($trustDevice == true) {
$request->session()->put('sudoTrustDevice', 1);
}
return redirect($next);
} else {
return redirect()

View file

@ -70,11 +70,13 @@ class ApiV1Controller extends Controller
'website' => 'nullable'
]);
$uris = implode(',', explode('\n', $request->redirect_uris));
$client = Passport::client()->forceFill([
'user_id' => null,
'name' => e($request->client_name),
'secret' => Str::random(40),
'redirect' => $request->redirect_uris,
'redirect' => $uris,
'personal_access_client' => false,
'password_client' => false,
'revoked' => false,
@ -828,7 +830,7 @@ class ApiV1Controller extends Controller
->first();
if($like) {
$like->delete();
$like->forceDelete();
$status->likes_count = $status->likes()->count();
$status->save();
}
@ -1226,7 +1228,9 @@ class ApiV1Controller extends Controller
$min = $request->input('min_id');
$max = $request->input('max_id');
abort_if(!$since && !$min && !$max, 400);
if(!$since && !$min && !$max) {
$min = 1;
}
$dir = $since ? '>' : ($min ? '>=' : '<');
$id = $since ?? $min ?? $max;
@ -1238,6 +1242,9 @@ class ApiV1Controller extends Controller
->limit($limit)
->get();
$minId = $notifications->min('id');
$maxId = $notifications->max('id');
$resource = new Fractal\Resource\Collection(
$notifications,
new NotificationTransformer()
@ -1247,7 +1254,33 @@ class ApiV1Controller extends Controller
->createData($resource)
->toArray();
return response()->json($res);
$baseUrl = config('app.url') . '/api/v1/notifications?';
if($minId == $maxId) {
$minId = null;
}
if($maxId) {
$link = '<'.$baseUrl.'max_id='.$maxId.'>; rel="next"';
}
if($minId) {
$link = '<'.$baseUrl.'min_id='.$minId.'>; rel="prev"';
}
if($maxId && $minId) {
$link = '<'.$baseUrl.'max_id='.$maxId.'>; rel="next",<'.$baseUrl.'min_id='.$minId.'>; rel="prev"';
}
$res = response()->json($res);
if(isset($link)) {
$res->withHeaders([
'Link' => $link,
]);
}
return $res;
}
/**
@ -1655,8 +1688,8 @@ class ApiV1Controller extends Controller
$status = new Status;
$status->caption = strip_tags($request->input('status'));
$status->scope = $request->input('visibility');
$status->visibility = $request->input('visibility');
$status->scope = $request->input('visibility', 'public');
$status->visibility = $request->input('visibility', 'public');
$status->profile_id = $user->profile_id;
$status->is_nsfw = $user->profile->cw == true ? true : $request->input('sensitive', false);
$status->in_reply_to_id = $parent->id;
@ -1690,8 +1723,8 @@ class ApiV1Controller extends Controller
abort(500, 'Invalid media ids');
}
$status->scope = $request->input('visibility');
$status->visibility = $request->input('visibility');
$status->scope = $request->input('visibility', 'public');
$status->visibility = $request->input('visibility', 'public');
$status->type = StatusController::mimeTypeCheck($mimes);
$status->save();
}
@ -1756,7 +1789,9 @@ class ApiV1Controller extends Controller
$share = Status::firstOrCreate([
'profile_id' => $user->profile_id,
'reblog_of_id' => $status->id,
'in_reply_to_profile_id' => $status->profile_id
'in_reply_to_profile_id' => $status->profile_id,
'scope' => 'public',
'visibility' => 'public'
]);
if($share->wasRecentlyCreated == true) {

View file

@ -80,9 +80,19 @@ class FederationController extends Controller
abort_if(!config('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404);
$res = Outbox::get($username);
$profile = Profile::whereNull('domain')
->whereNull('status')
->whereIsPrivate(false)
->whereUsername($username)
->firstOrFail();
return response(json_encode($res))->header('Content-Type', 'application/activity+json');
$key = 'ap:outbox:latest_10:pid:' . $profile->id;
$ttl = now()->addMinutes(15);
$res = Cache::remember($key, $ttl, function() use($profile) {
return Outbox::get($profile);
});
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
}
public function userInbox(Request $request, $username)

View file

@ -343,27 +343,6 @@ class InternalApiController extends Controller
return response()->json($res);
}
public function remoteProfile(Request $request, $id)
{
$profile = Profile::whereNull('status')
->whereNotNull('domain')
->findOrFail($id);
$settings = [
'crawlable' => false,
'following' => [
'count' => true,
'list' => false
],
'followers' => [
'count' => true,
'list' => false
]
];
return view('profile.show', compact('profile', 'settings'));
}
public function accountStatuses(Request $request, $id)
{
$this->validate($request, [
@ -440,6 +419,16 @@ class InternalApiController extends Controller
return response()->json($res);
}
public function remoteProfile(Request $request, $id)
{
$profile = Profile::whereNull('status')
->whereNotNull('domain')
->findOrFail($id);
$user = Auth::user();
return view('profile.remote', compact('profile', 'user'));
}
public function remoteStatus(Request $request, $profileId, $statusId)
{
$user = Profile::whereNull('status')
@ -450,7 +439,7 @@ class InternalApiController extends Controller
->whereNull('reblog_of_id')
->whereVisibility('public')
->findOrFail($statusId);
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
$template = $status->in_reply_to_id ? 'status.reply' : 'status.remote';
return view($template, compact('user', 'status'));
}
}

View file

@ -25,7 +25,7 @@ class LikeController extends Controller
$user = Auth::user();
$profile = $user->profile;
$status = Status::withCount('likes')->findOrFail($request->input('item'));
$status = Status::findOrFail($request->input('item'));
$count = $status->likes_count;
@ -36,14 +36,16 @@ class LikeController extends Controller
$status->likes_count = $count;
$status->save();
} else {
$like = new Like();
$like->profile_id = $profile->id;
$like->status_id = $status->id;
$like->save();
$count++;
$status->likes_count = $count;
$status->save();
LikePipeline::dispatch($like);
$like = Like::firstOrCreate([
'profile_id' => $user->profile_id,
'status_id' => $status->id
]);
if($like->wasRecentlyCreated == true) {
$count++;
$status->likes_count = $count;
$status->save();
LikePipeline::dispatch($like);
}
}
Cache::forget('status:'.$status->id.':likedby:userid:'.$user->id);

View file

@ -20,7 +20,7 @@ use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
RelationshipTransformer,
StatusTransformer,
StatusTransformer
};
use App\Services\{
AccountService,
@ -239,39 +239,6 @@ class PublicApiController extends Controller
$max = $request->input('max_id');
$limit = $request->input('limit') ?? 3;
$private = Cache::remember('profiles:private', now()->addMinutes(1440), function() {
return Profile::whereIsPrivate(true)
->orWhere('unlisted', true)
->orWhere('status', '!=', null)
->pluck('id')
->toArray();
});
// if(Auth::check()) {
// // $pid = Auth::user()->profile->id;
// // $filters = UserFilter::whereUserId($pid)
// // ->whereFilterableType('App\Profile')
// // ->whereIn('filter_type', ['mute', 'block'])
// // ->pluck('filterable_id')->toArray();
// // $filtered = array_merge($private->toArray(), $filters);
// $filtered = UserFilterService::filters(Auth::user()->profile_id);
// } else {
// // $filtered = $private->toArray();
// $filtered = [];
// }
$filtered = Auth::check() ? array_merge($private, UserFilterService::filters(Auth::user()->profile_id)) : [];
// if($max == 0) {
// $res = PublicTimelineService::count();
// if($res == 0) {
// PublicTimelineService::warmCache();
// $res = PublicTimelineService::get(0,4);
// } else {
// $res = PublicTimelineService::get(0,4);
// }
// return response()->json($res);
// }
if($min || $max) {
$dir = $min ? '>' : '<';
$id = $min ?? $max;
@ -295,15 +262,12 @@ class PublicApiController extends Controller
'created_at',
'updated_at'
)->where('id', $dir, $id)
->with('profile', 'hashtags', 'mentions')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->whereLocal(true)
->whereNotIn('profile_id', $filtered)
->whereVisibility('public')
->orderBy('created_at', 'desc')
->limit($limit)
->get();
//->toSql();
} else {
$timeline = Status::select(
'id',
@ -327,11 +291,9 @@ class PublicApiController extends Controller
)->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->with('profile', 'hashtags', 'mentions')
->whereLocal(true)
->whereNotIn('profile_id', $filtered)
->whereVisibility('public')
->orderBy('created_at', 'desc')
->simplePaginate($limit);
//->toSql();
}
$fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
@ -499,11 +461,31 @@ class PublicApiController extends Controller
public function accountFollowing(Request $request, $id)
{
abort_unless(Auth::check(), 403);
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_following) {
return response()->json([]);
$profile = Profile::with('user')
->whereNull('status')
->whereNull('domain')
->findOrFail($id);
// filter by username
$search = $request->input('fbu');
$owner = Auth::id() == $profile->user_id;
$filter = ($owner == true) && ($search != null);
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
if($search) {
abort_if(!$owner, 404);
$following = $profile->following()
->where('profiles.username', 'like', '%'.$search.'%')
->orderByDesc('followers.created_at')
->paginate(10);
} else {
$following = $profile->following()
->orderByDesc('followers.created_at')
->paginate(10);
}
$following = $profile->following()->orderByDesc('followers.created_at')->paginate(10);
$resource = new Fractal\Resource\Collection($following, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
@ -574,8 +556,6 @@ class PublicApiController extends Controller
'updated_at'
)->whereProfileId($profile->id)
->whereIn('type', $scope)
->whereLocal(true)
->whereNull('uri')
->where('id', $dir, $id)
->whereIn('visibility', $visibility)
->latest()

View file

@ -15,9 +15,15 @@ use App\Transformer\Api\{
HashtagTransformer,
StatusTransformer,
};
use App\Services\WebfingerService;
class SearchController extends Controller
{
public $tokens = [];
public $term = '';
public $hash = '';
public $cacheKey = 'api:search:tag:';
public function __construct()
{
$this->middleware('auth');
@ -28,50 +34,98 @@ class SearchController extends Controller
$this->validate($request, [
'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro',
'v' => 'required|integer|in:1'
'v' => 'required|integer|in:1',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
]);
$tag = $request->input('q');
$tag = e(urldecode($tag));
$scope = $request->input('scope') ?? 'all';
$this->term = e(urldecode($request->input('q')));
$this->hash = hash('sha256', $this->term);
switch ($scope) {
case 'all':
$this->getHashtags();
$this->getPosts();
$this->getProfiles();
break;
case 'hashtag':
$this->getHashtags();
break;
case 'profile':
$this->getProfiles();
break;
case 'webfinger':
$this->webfingerSearch();
break;
default:
break;
}
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
}
protected function getPosts()
{
$tag = $this->term;
$hash = hash('sha256', $tag);
$tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) {
$tokens = [];
if(Helpers::validateUrl($tag) != false && config('federation.activitypub.enabled') == true && config('federation.activitypub.remoteFollow') == true) {
abort_if(Helpers::validateLocalUrl($tag), 404);
$remote = Helpers::fetchFromUrl($tag);
if(isset($remote['type']) && in_array($remote['type'], ['Note', 'Person']) == true) {
$type = $remote['type'];
if($type == 'Person') {
$item = Helpers::profileFirstOrNew($tag);
$tokens['profiles'] = [[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain
]
]];
} else if ($type == 'Note') {
$item = Helpers::statusFetch($tag);
$tokens['posts'] = [[
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
]];
}
}
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
config('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) &&
$remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$this->tokens['posts'] = [[
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
]];
}
} else {
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile_id)
->where('caption', 'like', '%'.$tag.'%')
->latest()
->limit(10)
->get();
if($posts->count() > 0) {
$posts = $posts->map(function($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class
];
});
$this->tokens['posts'] = $posts;
}
}
}
protected function getHashtags()
{
$tag = $this->term;
$key = $this->cacheKey . 'hashtags:' . $this->hash;
$ttl = now()->addMinutes(1);
$tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
$hashtags = Hashtag::select('id', 'name', 'slug')
->where('slug', 'like', '%'.$htag.'%')
@ -89,72 +143,93 @@ class SearchController extends Controller
'name' => null,
];
});
$tokens['hashtags'] = $tags;
return $tags;
}
return $tokens;
});
$users = Profile::select('domain', 'username', 'name', 'id')
->whereNull('status')
->whereNull('domain')
->where('id', '!=', Auth::user()->profile->id)
->where('username', 'like', '%'.$tag.'%')
//->orWhere('remote_url', $tag)
->limit(20)
->get();
$this->tokens['hashtags'] = $tokens;
}
if($users->count() > 0) {
$profiles = $users->map(function ($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'avatar' => $item->avatarUrl(),
'id' => $item->id,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain
]
];
});
if(isset($tokens['profiles'])) {
array_push($tokens['profiles'], $profiles);
} else {
$tokens['profiles'] = $profiles;
protected function getProfiles()
{
$tag = $this->term;
$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
$key = $this->cacheKey . 'profiles:' . $this->hash;
$remoteTtl = now()->addMinutes(15);
$ttl = now()->addHours(2);
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
config('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) &&
$remote['type'] == 'Person'
) {
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
$item = Helpers::profileFirstOrNew($tag);
$tokens = [[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain
]
]];
return $tokens;
});
}
}
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile->id)
->where('caption', 'like', '%'.$tag.'%')
->latest()
->limit(10)
// elseif( Str::containsAll($tag, ['@', '.'])
// config('federation.activitypub.enabled') == true &&
// config('federation.activitypub.remoteFollow') == true
// ) {
// if(substr_count($tag, '@') == 2) {
// $domain = last(explode('@', sub_str($u, 1)));
// } else {
// $domain = last(explode('@', $u));
// }
// }
else {
$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
$users = Profile::select('domain', 'username', 'name', 'id')
->whereNull('status')
->where('id', '!=', Auth::user()->profile->id)
->where('username', 'like', '%'.$tag.'%')
->limit(20)
->get();
if($posts->count() > 0) {
$posts = $posts->map(function($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class
];
if($users->count() > 0) {
return $users->map(function ($item, $key) {
return [
'count' => 0,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'avatar' => $item->avatarUrl(),
'id' => (string) $item->id,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count()
]
];
});
}
});
$tokens['posts'] = $posts;
}
return response()->json($tokens);
}
public function results(Request $request)
@ -166,4 +241,31 @@ class SearchController extends Controller
return view('search.results');
}
protected function webfingerSearch()
{
$wfs = WebfingerService::lookup($this->term);
if(empty($wfs)) {
return;
}
$this->tokens['profiles'] = [
[
'count' => 1,
'url' => $wfs['url'],
'type' => 'profile',
'value' => $wfs['username'],
'tokens' => [$wfs['username']],
'name' => $wfs['display_name'],
'entity' => [
'id' => (string) $wfs['id'],
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local']
]
]
];
return;
}
}

View file

@ -22,7 +22,16 @@ class SiteController extends Controller
public function homeGuest()
{
return view('site.index');
$data = Cache::remember('site:landing:data', now()->addHours(3), function() {
return [
'stats' => [
'posts' => App\Util\Lexer\PrettyNumber::convert(App\Status::count()),
'likes' => App\Util\Lexer\PrettyNumber::convert(App\Like::count()),
'hashtags' => App\Util\Lexer\PrettyNumber::convert(App\StatusHashtag::count())
],
];
});
return view('site.index', compact('data'));
}
public function homeTimeline(Request $request)
@ -105,7 +114,7 @@ class SiteController extends Controller
$this->validate($request, [
'url' => 'required|url'
]);
$url = urldecode(request()->input('url'));
$url = request()->input('url');
return view('site.redirect', compact('url'));
}

View file

@ -25,7 +25,7 @@ class DangerZone
if(!Auth::check()) {
return redirect(route('login'));
}
if(!$request->is('i/auth/sudo')) {
if(!$request->is('i/auth/sudo') && $request->session()->get('sudoTrustDevice') != 1) {
if( !$request->session()->has('sudoMode') ) {
$request->session()->put('redirectNext', $request->url());
return redirect('/i/auth/sudo');

View file

@ -80,7 +80,7 @@ class SharePipeline implements ShouldQueue
$notification->action = 'share';
$notification->message = $status->shareToText();
$notification->rendered = $status->shareToHtml();
$notification->item_id = $status->id;
$notification->item_id = $status->reblog_of_id ?? $status->id;
$notification->item_type = "App\Status";
$notification->save();

View file

@ -137,8 +137,7 @@ class Profile extends Model
$url = Cache::remember('avatar:'.$this->id, now()->addYears(1), function () {
$avatar = $this->avatar;
$path = $avatar->media_path;
$version = hash('sha256', $avatar->change_count);
$path = "{$path}?v={$version}";
$path = "{$path}?v={$avatar->change_count}";
return config('app.url') . Storage::url($path);
});

View file

@ -3,6 +3,7 @@
namespace App\Services;
use Cache;
use App\Profile;
use Illuminate\Support\Facades\Redis;
use App\Util\Webfinger\WebfingerUrl;
use Zttp\Zttp;
@ -21,6 +22,12 @@ class WebfingerService
protected function run($query)
{
if($profile = Profile::whereUsername($query)->first()) {
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
return $fractal->createData($resource)->toArray();
}
$url = WebfingerUrl::generateWebfingerUrl($query);
if(!Helpers::validateUrl($url)) {
return [];

View file

@ -33,7 +33,7 @@ class AccountTransformer extends Fractal\TransformerAbstract
'website' => $profile->website,
'local' => (bool) $local,
'is_admin' => (bool) $is_admin,
'created_at' => $profile->created_at->timestamp,
'created_at' => $profile->created_at->toJSON(),
'header_bg' => $profile->header_bg
];
}

View file

@ -52,6 +52,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
'share' => 'reblog',
'like' => 'favourite',
'comment' => 'mention',
'admin.user.modlog.comment' => 'modlog'
];
return $verbs[$verb];
}

View file

@ -19,33 +19,34 @@ class StatusTransformer extends Fractal\TransformerAbstract
{
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->toJSON(),
'emojis' => [],
'replies_count' => 0,
'reblogs_count' => $status->reblogs_count ?? 0,
'favourites_count' => $status->likes_count ?? 0,
'reblogged' => null,
'favourited' => null,
'muted' => null,
'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
'sensitive' => (bool) $status->is_nsfw,
'spoiler_text' => $status->cw_summary ?? '',
'visibility' => $status->visibility ?? $status->scope,
'mentions' => [],
'tags' => [],
'card' => null,
'poll' => null,
'language' => 'en',
'uri' => $status->url(),
'url' => $status->url(),
'replies_count' => 0,
'reblogs_count' => $status->reblogs_count ?? 0,
'favourites_count' => $status->likes_count ?? 0,
'reblogged' => $status->shared(),
'favourited' => $status->liked(),
'muted' => false,
'bookmarked' => false,
'pinned' => false,
'content' => $status->rendered ?? $status->caption ?? '',
'reblog' => null,
'application' => [
'name' => 'web',
'website' => null
],
'language' => null,
'pinned' => null,
'mentions' => [],
'tags' => [],
'emojis' => [],
'card' => null,
'poll' => null,
];
}

View file

@ -3,30 +3,49 @@
namespace App\Util\ActivityPub;
use App\Profile;
use App\Status;
use League\Fractal;
use App\Http\Controllers\ProfileController;
use App\Transformer\ActivityPub\ProfileOutbox;
use App\Transformer\ActivityPub\Verb\CreateNote;
class Outbox {
public static function get($username)
public static function get($profile)
{
abort_if(!config('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404);
$profile = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
if($profile->status != null) {
return ProfileController::accountCheck($profile);
}
if($profile->is_private) {
return response()->json(['error'=>'403', 'msg' => 'private profile'], 403);
return ['error'=>'403', 'msg' => 'private profile'];
}
$timeline = $profile->statuses()->whereVisibility('public')->orderBy('created_at', 'desc')->paginate(10);
$timeline = $profile
->statuses()
->whereVisibility('public')
->orderBy('created_at', 'desc')
->take(10)
->get();
$count = Status::whereProfileId($profile->id)->count();
$fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($profile, new ProfileOutbox());
$resource = new Fractal\Resource\Collection($timeline, new CreateNote());
$res = $fractal->createData($resource)->toArray();
return $res['data'];
$outbox = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'_debug' => 'Outbox only supports latest 10 objects, pagination is not supported',
'id' => $profile->permalink('/outbox'),
'type' => 'OrderedCollection',
'totalItems' => $count,
'orderedItems' => $res['data']
];
return $outbox;
}
}

84
composer.lock generated
View file

@ -147,12 +147,12 @@
"version": "v0.11.4",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-cors.git",
"url": "https://github.com/fruitcake/laravel-cors.git",
"reference": "03492f1a3bc74a05de23f93b94ac7cc5c173eec9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-cors/zipball/03492f1a3bc74a05de23f93b94ac7cc5c173eec9",
"url": "https://api.github.com/repos/fruitcake/laravel-cors/zipball/03492f1a3bc74a05de23f93b94ac7cc5c173eec9",
"reference": "03492f1a3bc74a05de23f93b94ac7cc5c173eec9",
"shasum": ""
},
@ -2706,6 +2706,7 @@
"bcmath",
"math"
],
"abandoned": "brick/math",
"time": "2017-02-16T16:54:46+00:00"
},
{
@ -5135,16 +5136,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v4.4.1",
"version": "v4.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "8bccc59e61b41963d14c3dbdb23181e5c932a1d5"
"reference": "62f92509c9abfd1f73e17b8cf1b72c0bdac6611b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/8bccc59e61b41963d14c3dbdb23181e5c932a1d5",
"reference": "8bccc59e61b41963d14c3dbdb23181e5c932a1d5",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/62f92509c9abfd1f73e17b8cf1b72c0bdac6611b",
"reference": "62f92509c9abfd1f73e17b8cf1b72c0bdac6611b",
"shasum": ""
},
"require": {
@ -5186,7 +5187,7 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
"time": "2019-11-28T13:33:56+00:00"
"time": "2020-03-30T14:07:33+00:00"
},
{
"name": "symfony/http-kernel",
@ -5280,16 +5281,16 @@
},
{
"name": "symfony/mime",
"version": "v5.0.1",
"version": "v5.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "0e6a4ced216e49d457eddcefb61132173a876d79"
"reference": "481b7d6da88922fb1e0d86a943987722b08f3955"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/0e6a4ced216e49d457eddcefb61132173a876d79",
"reference": "0e6a4ced216e49d457eddcefb61132173a876d79",
"url": "https://api.github.com/repos/symfony/mime/zipball/481b7d6da88922fb1e0d86a943987722b08f3955",
"reference": "481b7d6da88922fb1e0d86a943987722b08f3955",
"shasum": ""
},
"require": {
@ -5338,7 +5339,7 @@
"mime",
"mime-type"
],
"time": "2019-11-30T14:12:50+00:00"
"time": "2020-03-27T16:56:45+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -5459,22 +5460,22 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.13.1",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "6f9c239e61e1b0c9229a28ff89a812dc449c3d46"
"reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6f9c239e61e1b0c9229a28ff89a812dc449c3d46",
"reference": "6f9c239e61e1b0c9229a28ff89a812dc449c3d46",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
"reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php72": "^1.9"
"symfony/polyfill-php72": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
@ -5482,7 +5483,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.13-dev"
"dev-master": "1.15-dev"
}
},
"autoload": {
@ -5517,20 +5518,20 @@
"portable",
"shim"
],
"time": "2019-11-27T13:56:44+00:00"
"time": "2020-03-09T19:04:49+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.13.1",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f"
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f",
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
"shasum": ""
},
"require": {
@ -5542,7 +5543,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.13-dev"
"dev-master": "1.15-dev"
}
},
"autoload": {
@ -5576,7 +5577,7 @@
"portable",
"shim"
],
"time": "2019-11-27T14:18:11+00:00"
"time": "2020-03-09T19:04:49+00:00"
},
{
"name": "symfony/polyfill-php56",
@ -5636,16 +5637,16 @@
},
{
"name": "symfony/polyfill-php72",
"version": "v1.13.1",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "66fea50f6cb37a35eea048d75a7d99a45b586038"
"reference": "37b0976c78b94856543260ce09b460a7bc852747"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/66fea50f6cb37a35eea048d75a7d99a45b586038",
"reference": "66fea50f6cb37a35eea048d75a7d99a45b586038",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
"reference": "37b0976c78b94856543260ce09b460a7bc852747",
"shasum": ""
},
"require": {
@ -5654,7 +5655,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.13-dev"
"dev-master": "1.15-dev"
}
},
"autoload": {
@ -5687,7 +5688,7 @@
"portable",
"shim"
],
"time": "2019-11-27T13:56:44+00:00"
"time": "2020-02-27T09:26:54+00:00"
},
{
"name": "symfony/polyfill-php73",
@ -6477,6 +6478,7 @@
"psr",
"psr-7"
],
"abandoned": "laminas/laminas-diactoros",
"time": "2019-11-13T19:16:13+00:00"
}
],
@ -6487,19 +6489,19 @@
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "35638e4f5e714a12dec5ca062e68c625c1309c1c"
"reference": "7fa9ff7945f44f10c76d7bc46f508f4cf593f4c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/35638e4f5e714a12dec5ca062e68c625c1309c1c",
"reference": "35638e4f5e714a12dec5ca062e68c625c1309c1c",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/7fa9ff7945f44f10c76d7bc46f508f4cf593f4c5",
"reference": "7fa9ff7945f44f10c76d7bc46f508f4cf593f4c5",
"shasum": ""
},
"require": {
"illuminate/routing": "^5.5|^6",
"illuminate/session": "^5.5|^6",
"illuminate/support": "^5.5|^6",
"maximebf/debugbar": "^1.15",
"illuminate/routing": "^5.5|^6|^7",
"illuminate/session": "^5.5|^6|^7",
"illuminate/support": "^5.5|^6|^7",
"maximebf/debugbar": "^1.15.1",
"php": ">=7.0",
"symfony/debug": "^3|^4|^5",
"symfony/finder": "^3|^4|^5"
@ -6547,7 +6549,7 @@
"profiler",
"webprofiler"
],
"time": "2019-12-07T09:33:13+00:00"
"time": "2020-03-24T06:32:37+00:00"
},
{
"name": "composer/ca-bundle",
@ -7127,8 +7129,8 @@
"authors": [
{
"name": "Filipe Dobreira",
"role": "Developer",
"homepage": "https://github.com/filp"
"homepage": "https://github.com/filp",
"role": "Developer"
}
],
"description": "php error handling for cool kids",

View file

@ -1,17 +0,0 @@
<?php
return [
// Enable or disable partialcache alltogether
'enabled' => true,
// The name of the blade directive to register
'directive' => 'cache',
// The base key that used for cache items
'key' => 'partialcache',
// The default cache duration in minutes, set null to remember forever
'default_duration' => null,
];

View file

@ -29,7 +29,7 @@ return [
|
*/
'lifetime' => env('SESSION_LIFETIME', 2880),
'lifetime' => env('SESSION_LIFETIME', 86400),
'expire_on_close' => false,

35
contrib/docker-nginx.conf Normal file
View file

@ -0,0 +1,35 @@
upstream fe {
server 127.0.0.1:8080;
}
server {
server_name real.domain;
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/real.domain/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/real.domain/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
proxy_redirect off;
proxy_pass http://fe/;
}
}
server {
if ($host = real.domain) {
return 301 https://$host$request_uri;
}
listen 80;
listen [::]:80;
server_name real.domain;
return 404;
}

View file

@ -1,25 +1,79 @@
FROM php:7.4-apache-buster
ARG COMPOSER_VERSION="1.9.1"
ARG COMPOSER_CHECKSUM="1f210b9037fcf82670d75892dfc44400f13fe9ada7af9e787f93e50e3b764111"
# Use the default production configuration
COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini"
RUN apt-get update \
&& apt-get install -y --no-install-recommends apt-utils \
&& apt-get install -y --no-install-recommends git gosu ffmpeg \
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev libcurl4-openssl-dev \
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-6 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev mariadb-client\
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
&& locale-gen && update-locale \
&& docker-php-source extract \
&& docker-php-ext-configure gd \
# Install Composer
ENV COMPOSER_VERSION 1.9.2
ENV COMPOSER_HOME /var/www/.composer
RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer \
&& curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \
&& php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \
&& php /tmp/composer-setup.php --no-ansi --install-dir=/usr/local/bin --filename=composer --version=${COMPOSER_VERSION} && rm -rf /tmp/composer-setup.php
# Update OS Packages
RUN apt-get update
# Install OS Packages
RUN apt-get install -y --no-install-recommends apt-utils
RUN apt-get install -y --no-install-recommends \
## Standard
locales locales-all \
git \
gosu \
zip \
unzip \
libzip-dev \
libcurl4-openssl-dev \
## Image Optimization
optipng \
pngquant \
jpegoptim \
gifsicle \
## Image Processing
libjpeg62-turbo-dev \
libpng-dev \
# Required for GD
libxpm4 \
libxpm-dev \
libwebp6 \
libwebp-dev \
## Video Processing
ffmpeg
# Update Local data
RUN sed -i '/en_US/s/^#//g' /etc/locale.gen && locale-gen && update-locale
# Install PHP extensions
RUN docker-php-source extract
#PHP Imagemagick extensions
RUN apt-get install -y --no-install-recommends libmagickwand-dev
RUN pecl install imagick
RUN docker-php-ext-enable imagick
# PHP GD extensions
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg \
--with-webp \
--with-xpm \
&& docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite pcntl gd exif bcmath intl zip curl \
&& docker-php-ext-enable pcntl gd exif zip curl \
&& a2enmod rewrite remoteip \
--with-xpm
RUN docker-php-ext-install "-j$(nproc) gd"
#PHP Redis extensions
RUN pecl install redis
RUN docker-php-ext-enable redis
#PHP Database extensions
RUN apt-get install -y --no-install-recommends libpq-dev libsqlite3-dev
RUN docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite
#PHP extensions (dependencies)
RUN docker-php-ext-configure intl
RUN docker-php-ext-install "-j$(nproc) intl bcmath zip pcntl exif curl"
#APACHE Bootstrap
RUN a2enmod rewrite remoteip \
&& {\
echo RemoteIPHeader X-Real-IP ;\
echo RemoteIPTrustedProxy 10.0.0.0/8 ;\
@ -27,45 +81,26 @@ RUN apt-get update \
echo RemoteIPTrustedProxy 192.168.0.0/16 ;\
echo SetEnvIf X-Forwarded-Proto "https" HTTPS=on ;\
} > /etc/apache2/conf-available/remoteip.conf \
&& a2enconf remoteip \
&& curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /usr/bin/composer \
&& echo "${COMPOSER_CHECKSUM} /usr/bin/composer" | sha256sum -c - \
&& chmod 755 /usr/bin/composer \
&& apt-get autoremove --purge -y \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
&& rm -rf /var/cache/apt \
&& docker-php-source delete
&& a2enconf remoteip
#Cleanup
RUN docker-php-source delete
RUN apt-get autoremove --purge -y
RUN apt-get clean
RUN rm -rf /var/cache/apt
RUN rm -rf /var/lib/apt/lists/*
ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
COPY . /var/www/
WORKDIR /var/www/
RUN cp -r storage storage.skel \
&& cp contrib/docker/php.ini /usr/local/etc/php/conf.d/pixelfed.ini \
&& composer global require hirak/prestissimo --no-interaction --no-suggest --prefer-dist \
&& composer install --prefer-dist --no-interaction \
&& composer global remove hirak/prestissimo \
&& rm -rf html && ln -s public html
RUN cp -r storage storage.skel
RUN composer global require hirak/prestissimo --no-interaction --no-suggest --prefer-dist
RUN composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader
RUN composer global remove hirak/prestissimo
RUN rm -rf html && ln -s public html
VOLUME /var/www/storage /var/www/bootstrap
ENV APP_ENV=production \
APP_DEBUG=false \
LOG_CHANNEL=stderr \
DB_CONNECTION=mysql \
DB_PORT=3306 \
DB_HOST=db \
BROADCAST_DRIVER=log \
QUEUE_DRIVER=redis \
HORIZON_PREFIX=horizon-pixelfed \
REDIS_HOST=redis \
SESSION_SECURE_COOKIE=true \
API_BASE="/api/1/" \
API_SEARCH="/api/search" \
OPEN_REGISTRATION=true \
ENFORCE_EMAIL_VERIFICATION=true \
REMOTE_FOLLOW=false \
ACTIVITY_PUB=false
CMD /var/www/contrib/docker/start.sh
CMD ["/var/www/contrib/docker/start.apache.sh"]

View file

@ -1,66 +1,94 @@
FROM php:7.4-fpm-buster
ARG COMPOSER_VERSION="1.9.1"
ARG COMPOSER_CHECKSUM="1f210b9037fcf82670d75892dfc44400f13fe9ada7af9e787f93e50e3b764111"
RUN apt-get update \
&& apt-get install -y --no-install-recommends apt-utils \
&& apt-get install -y --no-install-recommends git gosu ffmpeg \
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev libcurl4-openssl-dev \
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-6 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev mariadb-client\
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
&& locale-gen && update-locale \
&& docker-php-source extract \
&& docker-php-ext-configure gd \
# Use the default production configuration
COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini"
# Install Composer
ENV COMPOSER_VERSION 1.9.2
ENV COMPOSER_HOME /var/www/.composer
RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer \
&& curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \
&& php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \
&& php /tmp/composer-setup.php --no-ansi --install-dir=/usr/local/bin --filename=composer --version=${COMPOSER_VERSION} && rm -rf /tmp/composer-setup.php
# Update OS Packages
RUN apt-get update
# Install OS Packages
RUN apt-get install -y --no-install-recommends apt-utils
RUN apt-get install -y --no-install-recommends \
## Standard
locales locales-all \
git \
gosu \
zip \
unzip \
libzip-dev \
libcurl4-openssl-dev \
## Image Optimization
optipng \
pngquant \
jpegoptim \
gifsicle \
## Image Processing
libjpeg62-turbo-dev \
libpng-dev \
# Required for GD
libxpm4 \
libxpm-dev \
libwebp6 \
libwebp-dev \
## Video Processing
ffmpeg
# Update Local data
RUN sed -i '/en_US/s/^#//g' /etc/locale.gen && locale-gen && update-locale
# Install PHP extensions
RUN docker-php-source extract
#PHP Imagemagick extensions
RUN apt-get install -y --no-install-recommends libmagickwand-dev
RUN pecl install imagick
RUN docker-php-ext-enable imagick
# PHP GD extensions
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg \
--with-webp \
--with-xpm \
&& docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite pcntl gd exif bcmath intl zip curl \
&& docker-php-ext-enable pcntl gd exif zip curl \
&& curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /usr/bin/composer \
&& echo "${COMPOSER_CHECKSUM} /usr/bin/composer" | sha256sum -c - \
&& chmod 755 /usr/bin/composer \
&& apt-get autoremove --purge -y \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
&& rm -rf /var/cache/apt \
&& docker-php-source delete
--with-xpm
RUN docker-php-ext-install -j$(nproc) gd
#PHP Redis extensions
RUN pecl install redis
RUN docker-php-ext-enable redis
#PHP Database extensions
RUN apt-get install -y --no-install-recommends libpq-dev libsqlite3-dev
RUN docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite
#PHP extensions (dependencies)
RUN docker-php-ext-configure intl
RUN docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl
#Cleanup
RUN docker-php-source delete
RUN apt-get autoremove --purge -y
RUN rm -rf /var/cache/apt
RUN rm -rf /var/lib/apt/lists/*
ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
COPY . /var/www/
WORKDIR /var/www/
RUN cp -r storage storage.skel \
&& cp contrib/docker/php.ini /usr/local/etc/php/conf.d/pixelfed.ini \
&& composer global require hirak/prestissimo --no-interaction --no-suggest --prefer-dist \
&& composer install --prefer-dist --no-interaction \
&& composer global remove hirak/prestissimo \
&& rm -rf html && ln -s public html
RUN cp -r storage storage.skel
RUN composer global require hirak/prestissimo --no-interaction --no-suggest --prefer-dist
RUN composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader
RUN composer global remove hirak/prestissimo
RUN rm -rf html && ln -s public html
VOLUME /var/www/storage /var/www/bootstrap
ENV APP_ENV=production \
APP_DEBUG=false \
LOG_CHANNEL=stderr \
DB_CONNECTION=mysql \
DB_PORT=3306 \
DB_HOST=db \
BROADCAST_DRIVER=log \
QUEUE_DRIVER=redis \
HORIZON_PREFIX=horizon-pixelfed \
REDIS_HOST=redis \
SESSION_SECURE_COOKIE=true \
API_BASE="/api/1/" \
API_SEARCH="/api/search" \
OPEN_REGISTRATION=true \
ENFORCE_EMAIL_VERIFICATION=true \
REMOTE_FOLLOW=false \
ACTIVITY_PUB=false
CMD cp -r storage.skel/* storage/ \
&& chown -R www-data:www-data storage/ \
&& php artisan storage:link \
&& php artisan migrate --force \
&& php artisan update \
&& exec php-fpm
CMD ["/var/www/contrib/docker/start.fpm.sh"]

View file

@ -1,5 +0,0 @@
file_uploads = On
memory_limit = 128M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 600

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
#!/bin/bash
# Create the storage tree if needed and fix permissions
cp -r storage.skel/* storage/
chown -R www-data:www-data storage/ bootstrap/
# Refresh the environment
php artisan storage:link
php artisan horizon:assets
php artisan route:cache
php artisan view:cache
php artisan config:cache
# Finally run Apache
exec apache2-foreground

View file

@ -0,0 +1,15 @@
#!/bin/bash
# Create the storage tree if needed and fix permissions
cp -r storage.skel/* storage/
chown -R www-data:www-data storage/ bootstrap/
# Refresh the environment
php artisan storage:link
php artisan horizon:assets
php artisan route:cache
php artisan view:cache
php artisan config:cache
# Finally run FPM
exec php-fpm

View file

@ -1,26 +0,0 @@
#!/bin/bash
# Create the storage tree if needed and fix permissions
cp -r storage.skel/* storage/
chown -R www-data:www-data storage/ bootstrap/
# Refresh the environment
php artisan storage:link
php artisan horizon:assets
php artisan route:cache
php artisan view:cache
php artisan config:cache
# Migrate database if the app was upgraded
# gosu www-data:www-data php artisan migrate --force
# Run other specific migratins if required
# gosu www-data:www-data php artisan update
# Run a worker if it is set as embedded
if [ "$HORIZON_EMBED" = "true" ]; then
gosu www-data:www-data php artisan horizon &
fi
# Finally run Apache
exec apache2-foreground

View file

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddFetchedAtToProfilesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('profiles', function (Blueprint $table) {
$table->timestamp('last_fetched_at')->nullable();
$table->unsignedInteger('status_count')->default(0)->nullable();
$table->unsignedInteger('followers_count')->default(0)->nullable();
$table->unsignedInteger('following_count')->default(0)->nullable();
$table->string('webfinger')->unique()->nullable()->index();
$table->string('avatar_url')->nullable();
$table->dropColumn('keybase_proof');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('profiles', function (Blueprint $table) {
$table->dropColumn('last_fetched_at');
$table->dropColumn('status_count');
$table->dropColumn('followers_count');
$table->dropColumn('following_count');
$table->dropColumn('webfinger');
$table->dropColumn('avatar_url');
$table->text('keybase_proof')->nullable()->after('post_layout');
});
}
}

View file

@ -12,50 +12,49 @@ version: '3'
services:
## App and Worker
app:
# Comment to use dockerhub image
build:
context: .
dockerfile: contrib/docker/Dockerfile.apache
#dockerfile: contrib/docker/Dockerfile.fpm
image: pixelfed
restart: unless-stopped
## If you have a traefik running, uncomment this to expose Pixelfed
# labels:
# - traefik.enable=true
# - traefik.frontend.rule=Host:your.url
# - traefik.port=80
## If you have a standard reverse proxy, uncommit this to expose Pixelfed
# ports:
# - "127.0.0.1:8080:80"
env_file:
- ./.env
- ./.env.docker
volumes:
- "app-storage:/var/www/storage"
- "app-bootstrap:/var/www/bootstrap"
- "./.env:/var/www/.env"
- "./.env.docker:/var/www/.env"
networks:
- external
- internal
ports:
- "8080:80"
depends_on:
- db
- redis
worker: # Comment this whole block if HORIZON_EMBED is true.
# Comment to use dockerhub image
worker:
build:
context: .
dockerfile: contrib/docker/Dockerfile.apache
#dockerfile: contrib/docker/Dockerfile.fpm
image: pixelfed
restart: unless-stopped
env_file:
- ./.env
- ./.env.docker
volumes:
- "app-storage:/var/www/storage"
- "app-bootstrap:/var/www/bootstrap"
networks:
- external # Required for ActivityPub
- external
- internal
command: gosu www-data php artisan horizon
depends_on:
- db
- redis
## DB and Cache
db:
image: mysql:8.0
restart: unless-stopped
@ -78,10 +77,9 @@ services:
networks:
- internal
# Adjust your volume data in order to store data where you wish
volumes:
redis-data:
db-data:
redis-data:
app-storage:
app-bootstrap:

40
package-lock.json generated
View file

@ -1080,9 +1080,9 @@
}
},
"acorn": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
"integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw=="
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA=="
},
"adjust-sourcemap-loader": {
"version": "1.2.0",
@ -1306,7 +1306,7 @@
},
"util": {
"version": "0.10.3",
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"requires": {
"inherits": "2.0.1"
@ -1430,7 +1430,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
@ -1447,7 +1447,7 @@
},
"supports-color": {
"version": "2.0.0",
"resolved": "http://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
}
}
@ -2497,12 +2497,12 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0="
},
"regexpu-core": {
"version": "1.0.0",
"resolved": "http://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
"integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
"requires": {
"regenerate": "^1.2.1",
@ -2512,12 +2512,12 @@
},
"regjsgen": {
"version": "0.2.0",
"resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc="
},
"regjsparser": {
"version": "0.1.5",
"resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
"requires": {
"jsesc": "~0.5.0"
@ -2758,7 +2758,7 @@
"dependencies": {
"globby": {
"version": "6.1.0",
"resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
"requires": {
"array-union": "^1.0.1",
@ -2770,7 +2770,7 @@
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
}
}
@ -3263,7 +3263,7 @@
"dependencies": {
"array-flatten": {
"version": "1.1.1",
"resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"debug": {
@ -3606,7 +3606,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
@ -3618,7 +3618,7 @@
},
"supports-color": {
"version": "2.0.0",
"resolved": "http://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
}
}
@ -4833,7 +4833,7 @@
},
"is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
"requires": {
"kind-of": "^3.0.2"
@ -4887,7 +4887,7 @@
},
"is-data-descriptor": {
"version": "0.1.4",
"resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
"requires": {
"kind-of": "^3.0.2"
@ -7309,7 +7309,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@ -7430,7 +7430,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0="
}
}
@ -7611,7 +7611,7 @@
"dependencies": {
"convert-source-map": {
"version": "0.3.5",
"resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
"integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
"dev": true
}

BIN
public/_landing/1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/_landing/2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
public/_landing/3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
public/_landing/4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
public/_landing/5.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
public/_landing/6.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
public/_landing/7.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/_landing/8.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
public/_landing/9.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
public/css/landing.css vendored

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/rempos.js vendored Normal file

Binary file not shown.

BIN
public/js/rempro.js vendored Normal file

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -40,7 +40,7 @@
</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" :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.url">post</a>.
</p>
</div>
<div v-else-if="n.type == 'modlog'">
@ -88,14 +88,9 @@
methods: {
fetchNotifications() {
axios.get('/api/pixelfed/v1/notifications')
axios.get('/api/pixelfed/v1/notifications?pg=true')
.then(res => {
let data = res.data.filter(n => {
if(n.type == 'share' && !status) {
return false;
}
return true;
});
let data = res.data;
let ids = res.data.map(n => n.id);
this.notificationMaxId = Math.min(...ids);
this.notifications = data;

View file

@ -137,60 +137,70 @@
<div v-if="showComments">
<hr>
<div class="postCommentsLoader text-center">
<div class="postCommentsLoader text-center py-2">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="postCommentsContainer d-none">
<p v-if="status.reply_count > 10"class="mb-1 text-center load-more-link d-none"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
<p v-if="status.reply_count > 10" class="mb-1 text-center load-more-link d-none my-3">
<a href="#" class="text-dark" v-on:click="loadMore" title="Load more comments" data-toggle="tooltip" data-placement="bottom">
<svg class="bi bi-plus-circle" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" style="font-size:2em;"> <path fill-rule="evenodd" d="M8 3.5a.5.5 0 01.5.5v4a.5.5 0 01-.5.5H4a.5.5 0 010-1h3.5V4a.5.5 0 01.5-.5z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M7.5 8a.5.5 0 01.5-.5h4a.5.5 0 010 1H8.5V12a.5.5 0 01-1 0V8z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M8 15A7 7 0 108 1a7 7 0 000 14zm0 1A8 8 0 108 0a8 8 0 000 16z" clip-rule="evenodd"/></svg>
</a>
</p>
<div class="comments">
<div v-for="(reply, index) in results" class="pb-3" :key="'tl' + reply.id + '_' + index">
<div v-if="reply.sensitive == true">
<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;">
<span>
<div v-for="(reply, index) in results" class="pb-4 media" :key="'tl' + reply.id + '_' + index">
<img :src="reply.account.avatar" class="rounded-circle border mr-3" width="42px" height="42px">
<div class="media-body">
<div v-if="reply.sensitive == true">
<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" v-html="reply.content"></span>
<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>
<span class="pl-2" style="min-width:38px">
<span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
<post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
</span>
</p>
<p class="">
<a v-once class="text-muted mr-3 text-decoration-none small" style="width: 20px;" v-text="timeAgo(reply.created_at)" :href="reply.url"></a>
<span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
<span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply, index)">Reply</span>
</p>
<div v-if="reply.reply_count > 0" class="cursor-pointer" style="margin-left:30px;" v-on:click="toggleReplies(reply)">
<span class="show-reply-bar"></span>
<span class="comment-reaction font-weight-bold text-muted">{{reply.thread ? 'Hide' : 'View'}} Replies ({{reply.reply_count}})</span>
</div>
<div v-if="reply.thread == true" class="comment-thread">
<div v-for="(s, sindex) in reply.replies" class="pb-3" :key="'cr' + s.id + '_' + index">
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span>
<a class="text-dark font-weight-bold mr-1" :href="s.account.url" :title="s.account.username">{{s.account.username}}</a>
<span class="text-break" v-html="s.content"></span>
</span>
<span class="pl-2" style="min-width:38px">
<span v-on:click="likeReply(s, $event)"><i v-bind:class="[s.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
<post-menu :status="s" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteCommentReply(s.id, sindex, index) "></post-menu>
</span>
</p>
<p class="">
<a v-once class="text-muted mr-3 text-decoration-none small" style="width: 20px;" v-text="timeAgo(s.created_at)" :href="s.url"></a>
<span v-if="s.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{s.favourites_count == 1 ? '1 like' : s.favourites_count + ' likes'}}</span>
</p>
<div v-else>
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span>
<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 " v-html="reply.content"></span>
</span>
<span class="pl-2">
<!-- <span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span> -->
<post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block px-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
</span>
</p>
<p class="">
<a v-once class="text-muted mr-3 text-decoration-none small" style="width: 20px;" v-text="timeAgo(reply.created_at)" :href="reply.url"></a>
<span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
<span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply, index)">Reply</span>
</p>
<div v-if="reply.reply_count > 0" class="cursor-pointer" v-on:click="toggleReplies(reply)">
<span class="show-reply-bar"></span>
<span class="comment-reaction font-weight-bold text-muted">{{reply.thread ? 'Hide' : 'View'}} Replies ({{reply.reply_count}})</span>
</div>
<div v-if="reply.thread == true" class="comment-thread">
<div v-for="(s, sindex) in reply.replies" class="pb-3 media" :key="'cr' + s.id + '_' + index">
<img :src="s.account.avatar" class="rounded-circle border mr-3" width="25px" height="25px">
<div class="media-body">
<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
<span>
<a class="text-dark font-weight-bold mr-1" :href="s.account.url" :title="s.account.username">{{s.account.username}}</a>
<span class="text-break" v-html="s.content"></span>
</span>
<span class="pl-2" style="min-width:38px">
<span v-on:click="likeReply(s, $event)"><i v-bind:class="[s.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
<post-menu :status="s" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteCommentReply(s.id, sindex, index) "></post-menu>
</span>
</p>
<p class="">
<a v-once class="text-muted mr-3 text-decoration-none small" style="width: 20px;" v-text="timeAgo(s.created_at)" :href="s.url"></a>
<span v-if="s.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{s.favourites_count == 1 ? '1 like' : s.favourites_count + ' likes'}}</span>
</p>
</div>
</div>
</div>
</div>
</div>
@ -225,7 +235,7 @@
</div>
</div>
</div>
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
<div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction" v-for="e in emoji">{{e}}</li>
</ul>
@ -405,9 +415,9 @@
hide-footer
centered
title="Likes"
body-class="list-group-flush p-0">
body-class="list-group-flush py-3 px-0">
<div class="list-group">
<div class="list-group-item border-0" v-for="(user, index) in likes" :key="'modal_likes_'+index">
<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px">
@ -418,9 +428,11 @@
{{user.username}}
</a>
</p>
<p class="text-muted mb-0" style="font-size: 14px">
{{user.display_name}}
</a>
<p v-if="!user.local" class="text-muted mb-0 text-truncate mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span>
</p>
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
{{user.display_name}}
</p>
</div>
</div>
@ -465,9 +477,8 @@
</infinite-loading>
</div>
</b-modal>
<b-modal
<b-modal ref="lightboxModal"
id="lightbox"
ref="lightboxModal"
:hide-header="true"
:hide-footer="true"
centered
@ -478,7 +489,7 @@
<img :src="lightboxMedia.url" :class="lightboxMedia.filter_class + ' img-fluid'" style="min-height: 100%; min-width: 100%">
</div>
</b-modal>
<b-modal ref="embedModal"
<b-modal ref="embedModal"
id="ctx-embed-modal"
hide-header
hide-footer
@ -542,8 +553,7 @@
width: 24px;
}
.comment-thread {
margin: 4px 0 0 40px;
width: calc(100% - 40px);
margin-top: 1rem;
}
.emoji-reactions .nav-item {
font-size: 1.2rem;
@ -555,6 +565,12 @@
height: 0px;
background: transparent;
}
@media (min-width: 1200px) {
.container {
max-width: 1100px;
}
}
</style>
<style type="text/css" scoped>
.momentui .bg-dark {
@ -645,6 +661,7 @@ export default {
updated() {
$('.carousel').carousel();
// $('[data-toggle="tooltip"]').tooltip();
if(this.showReadMore == true) {
window.pixelfed.readmore();
}
@ -694,7 +711,6 @@ export default {
this.fetchComments();
}
this.loaded = true;
$('head title').text(this.status.account.username + ' posted a photo: ' + this.status.favourites_count + ' likes');
}).catch(error => {
swal('Oops!', 'An error occured, please try refreshing the page.', 'error');
});
@ -959,7 +975,6 @@ export default {
this.replyToIndex = index;
this.replyingToId = e.id;
this.reply_to_profile_id = e.account.id;
this.replyText = '@' + e.account.username + ' ';
$('textarea[name="comment"]').focus();
},
@ -1009,6 +1024,7 @@ export default {
$('.load-more-link').addClass('d-none');
return;
}
$('.load-more-link').addClass('d-none');
$('.postCommentsLoader').removeClass('d-none');
let next = this.pagination.links.next;
axios.get(next)
@ -1020,6 +1036,7 @@ export default {
this.results.unshift(res[i]);
}
this.pagination = response.data.meta.pagination;
$('.load-more-link').removeClass('d-none');
});
},

View file

@ -8,14 +8,14 @@
<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> -->
<span v-if="statusOwner(status) == false">
<span v-if="activeSession == true && statusOwner(status) == false">
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
</span>
<span v-if="statusOwner(status) == true">
<span v-if="activeSession == true && statusOwner(status) == true">
<a class="dropdown-item font-weight-bold text-decoration-none" @click.prevent="muteProfile(status)">Mute Profile</a>
<a class="dropdown-item font-weight-bold text-decoration-none" @click.prevent="blockProfile(status)">Block Profile</a>
</span>
<span v-if="profile.is_admin == true">
<span v-if="activeSession == true && profile.is_admin == true">
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold text-danger text-decoration-none" v-on:click="deletePost(status)">Delete</a>
<div class="dropdown-divider"></div>
@ -51,38 +51,36 @@
<div class="modal" tabindex="-1" role="dialog" :id="'mt_pid_'+status.id">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<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>
<div class="modal-body text-center">
<div class="list-group text-dark">
<a class="list-group-item text-dark 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="#" @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>
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="blockProfile(status)" href="#">Block Profile</a>
<a class="list-group-item text-dark text-decoration-none" href="#" @click="hidePost(status)">Hide</a>
<a v-if="activeSession == true && !statusOwner(status)" class="list-group-item text-dark text-decoration-none" :href="reportUrl(status)">Report</a>
<a v-if="activeSession == true && !statusOwner(status)" class="list-group-item text-dark text-decoration-none" v-on:click="muteProfile(status)" href="#">Mute Profile</a>
<a v-if="activeSession == true && !statusOwner(status)" class="list-group-item text-dark text-decoration-none" v-on:click="blockProfile(status)" href="#">Block Profile</a>
<span v-if="activeSession == true && statusOwner(status) == true || profile.is_admin == true">
<a class="list-group-item text-danger text-decoration-none" v-on:click="deletePost">Delete</a>
</span>
<span v-if="statusOwner(status) == true || profile.is_admin == true">
<a class="list-group-item font-weight-bold text-danger text-decoration-none" v-on:click="deletePost">Delete</a>
</span>
<span v-if="profile.is_admin == true">
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="moderatePost(status, 'autocw')" href="#">
<span v-if="activeSession == true && profile.is_admin == true">
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'autocw')" href="#">
<p class="mb-0">Enforce CW</p>
<p class="mb-0 small text-muted">Adds a CW to every post <br> made by this account.</p>
</a>
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="moderatePost(status, 'noautolink')" href="#">
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'noautolink')" href="#">
<p class="mb-0">No Autolinking</p>
<p class="mb-0 small text-muted">Do not transform mentions, <br> hashtags or urls into HTML.</p>
</a>
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="moderatePost(status, 'unlisted')" href="#">
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'unlisted')" href="#">
<p class="mb-0">Unlisted Posts</p>
<p class="mb-0 small text-muted">Removes account from <br> public/network timelines.</p>
</a>
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="moderatePost(status, 'disable')" href="#">
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'disable')" href="#">
<p class="mb-0">Disable Account</p>
<p class="mb-0 small text-muted">Temporarily disable account <br> until next time user log in.</p>
</a>
<a class="list-group-item font-weight-bold text-decoration-none" v-on:click="moderatePost(status, 'suspend')" href="#">
<a class="list-group-item text-dark text-decoration-none" v-on:click="moderatePost(status, 'suspend')" href="#">
<p class="mb-0">Suspend Account</p>
<p class="mb-0 small text-muted">This prevents any new interactions, <br> without deleting existing data.</p>
</a>
@ -110,6 +108,17 @@
export default {
props: ['feed', 'status', 'profile', 'size', 'modal'],
data() {
return {
activeSession: false
};
},
mounted() {
let el = document.querySelector('body');
this.activeSession = el.classList.contains('loggedIn') ? true : false;
},
methods: {
reportUrl(status) {
let type = status.in_reply_to ? 'comment' : 'post';

View file

@ -173,8 +173,8 @@
<div class="container px-0">
<div class="profile-timeline mt-md-4">
<div class="row" v-if="mode == 'grid'">
<div class="col-4 p-1 p-md-3" v-for="(s, index) in timeline">
<a class="card info-overlay card-md-border-0" :href="statusUrl(s)">
<div class="col-4 p-1 p-md-3" v-for="(s, index) in timeline" :key="'tlob:'+index">
<a class="card info-overlay card-md-border-0" :href="statusUrl(s)" v-once>
<div :class="[s.sensitive ? 'square' : 'square ' + s.media_attachments[0].filter_class]">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
@ -355,26 +355,47 @@
</div>
</div>
</div>
<b-modal ref="followingModal"
<b-modal
v-if="profile && following"
ref="followingModal"
id="following-modal"
hide-footer
centered
title="Following"
body-class="list-group-flush py-3 px-0"
dialog-class="follow-modal">
<div class="list-group">
<div v-if="!loading" class="list-group" style="min-height: 60vh;">
<div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
<span class="d-flex px-4 pb-0 align-items-center">
<i class="fas fa-search text-lighter"></i>
<input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
</span>
</div>
<div v-if="owner == true" class="btn-group rounded-0 mt-n3 mb-3 border-top" role="group" aria-label="Following">
<!-- <button type="button" :class="[followingModalTab == 'following' ? ' btn btn-light py-3 rounded-0 font-weight-bold modal-tab-active' : 'btn btn-light py-3 rounded-0 font-weight-bold']" style="font-size: 12px;">FOLLOWING</button> -->
<!-- <button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">MUTED</button>
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;">BLOCKED</button> -->
</div>
<div v-else class="btn-group rounded-0 mt-n3 mb-3" role="group" aria-label="Following">
<!-- <button type="button" class="btn btn-light py-3 rounded-0 border-primary border-left-0 border-right-0 border-top-0 font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'following'">FOLLOWING</button>
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'mutual'">MUTUAL</button>
<button type="button" class="btn btn-light py-3 rounded-0 text-muted font-weight-bold" style="font-size: 12px;" @click="followingModalTab = 'blocked'">BLOCKED</button> -->
</div>
<div class="list-group-item border-0 py-1" v-for="(user, index) in following" :key="'following_'+index">
<div class="media">
<a :href="user.url">
<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + 's avatar'" width="30px" loading="lazy">
</a>
<div class="media-body">
<div class="media-body text-truncate">
<p class="mb-0" style="font-size: 14px">
<a :href="user.url" class="font-weight-bold text-dark">
{{user.username}}
</a>
</p>
<p class="text-muted mb-0" style="font-size: 14px">
<p v-if="!user.local" class="text-muted mb-0 text-truncate mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
<span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span>
</p>
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
{{user.display_name}}
</p>
</div>
@ -383,12 +404,12 @@
</div>
</div>
</div>
<div v-if="following.length == 0" class="list-group-item border-0">
<div class="list-group-item border-0">
<p class="p-3 text-center mb-0 lead"></p>
<div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
<div class="list-group-item border-0 pt-5">
<p class="p-3 text-center mb-0 lead">No Results Found</p>
</div>
</div>
<div v-if="followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
<div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
<p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
</div>
</div>
@ -565,6 +586,14 @@
padding: 6px;
background:#fff;
}
.no-focus {
border-color: none;
outline: 0;
box-shadow: none;
}
.modal-tab-active {
border-bottom: 1px solid #08d;
}
</style>
<script type="text/javascript">
import VueMasonry from 'vue-masonry-css'
@ -608,7 +637,10 @@
isMobile: false,
ctxEmbedPayload: null,
copiedEmbed: false,
hasStory: null
hasStory: null,
followingModalSearch: null,
followingModalSearchCache: null,
followingModalTab: 'following',
}
},
beforeMount() {
@ -1013,6 +1045,7 @@
})
.then(res => {
this.following = res.data;
this.followingModalSearchCache = res.data;
this.followingCursor++;
if(res.data.length < 10) {
this.followingMore = false;
@ -1059,15 +1092,18 @@
}
axios.get('/api/pixelfed/v1/accounts/'+this.profile.id+'/following', {
params: {
page: this.followingCursor
page: this.followingCursor,
fbu: this.followingModalSearch
}
})
.then(res => {
if(res.data.length > 0) {
this.following.push(...res.data);
this.followingCursor++;
this.followingModalSearchCache = this.following;
}
if(res.data.length < 10) {
this.followingModalSearchCache = this.following;
this.followingMore = false;
}
});
@ -1186,6 +1222,28 @@
storyRedirect() {
window.location.href = '/stories/' + this.profileUsername;
},
followingModalSearchHandler() {
let self = this;
let q = this.followingModalSearch;
if(q.length == 0) {
this.following = this.followingModalSearchCache;
this.followingModalSearch = null;
}
if(q.length > 0) {
let url = '/api/pixelfed/v1/accounts/' +
self.profileId + '/following?page=1&fbu=' +
q;
axios.get(url).then(res => {
this.following = res.data;
}).catch(err => {
self.following = self.followingModalSearchCache;
self.followingModalSearch = null;
});
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,447 @@
<template>
<div>
<div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom">
<div class="container">
<p class="text-center font-weight-bold">You are blocking this account</p>
<p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false;">here</a> to view profile</p>
</div>
</div>
<div v-if="loading" style="height: 80vh;" class="d-flex justify-content-center align-items-center">
<img src="/img/pixelfed-icon-grey.svg" class="">
</div>
<div v-if="!loading && !warning" class="container">
<div class="row">
<div class="col-12 col-md-4 pt-5">
<div class="card shadow-none border">
<div class="card-header p-0 m-0">
<img v-if="profile.header_bg" :src="profile.header_bg" style="width: 100%; height: 140px; object-fit: cover;">
<div v-else class="bg-primary" style="width: 100%;height: 140px;"></div>
</div>
<div class="card-body pb-0">
<div class="mt-n5 mb-3">
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.avatar" width="90px" height="90px;">
<span class="float-right mt-n1">
<span>
<button v-if="relationship && relationship.following == false" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="followProfile();">Follow</button>
<button v-if="relationship && relationship.following == true" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="unfollowProfile();">Unfollow</button>
</span>
<span class="ml-3">
<button class="btn btn-outline-light btn-sm mt-n1" @click="showCtxMenu()" style="padding-top:2px;padding-bottom:1px;">
<i class="fas fa-cog cursor-pointer" style="font-size:13px;"></i>
</button>
</span>
</span>
</div>
<p class="pl-2 h4 font-weight-bold mb-1">{{profile.display_name}}</p>
<p class="pl-2 font-weight-bold mb-1 text-muted">{{profile.acct}}</p>
<p class="pl-2 text-muted small pt-3" v-html="profile.note"></p>
<p class="pl-2 text-muted small d-flex justify-content-between">
<span>
<span class="font-weight-bold text-dark">{{profile.statuses_count}}</span>
<span>Posts</span>
</span>
<span>
<span class="font-weight-bold text-dark">{{profile.following_count}}</span>
<span>Following</span>
</span>
<span>
<span class="font-weight-bold text-dark">{{profile.followers_count}}</span>
<span>Followers</span>
</span>
</p>
</div>
</div>
</div>
<div class="col-12 col-md-8 pt-5">
<div class="row">
<div class="col-12 mb-2" v-for="(status, index) in feed" :key="'remprop' + index">
<div class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border cursor-pointer">
<div class="card-header d-inline-flex align-items-center bg-white">
<img v-bind:src="profile.avatar" width="38px" height="38px" style="border-radius: 38px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
<div class="pl-2">
<span class="username font-weight-bold text-dark">{{profile.username}}
</span>
</div>
<div class="text-right" style="flex-grow:1;">
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu(status)">
<span class="fas fa-ellipsis-h text-lighter"></span>
</button>
</div>
</div>
<div class="card-body p-0">
<a :href="status.url">
<img v-once :src="status.thumb" class="w-100 h-100">
</a>
</div>
<div class="card-body">
<div class="caption">
<p class="mb-2 read-more" style="overflow: hidden;">
<span class="username font-weight-bold">
<bdi><span class="text-dark">{{profile.username}}</span></bdi>
</span>
<span class="status-content" v-html="status.caption.html"></span>
</p>
</div>
<div class="timestamp mt-2">
<p class="small text-uppercase mb-0">
<a :href="remotePostUrl(status)" class="text-muted">
<timeago :datetime="status.timestamp" :auto-update="90" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.timestamp)" v-b-tooltip.hover.bottom></timeago>
</a>
</p>
</div>
</div>
</div>
</div>
<!-- <div class="col-12 mt-4">
<p class="text-center mb-0 px-0"><button class="btn btn-outline-primary btn-block font-weight-bold">Load More</button></p>
</div> -->
</div>
</div>
</div>
<b-modal ref="visitorContextMenu"
id="visitor-context-menu"
hide-footer
hide-header
centered
size="sm"
body-class="list-group-flush p-0">
<div class="list-group" v-if="relationship">
<div class="list-group-item cursor-pointer text-center rounded text-dark" @click="copyProfileLink">
Copy Link
</div>
<div v-if="user && !owner && !relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="muteProfile">
Mute
</div>
<div v-if="user && !owner && relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="unmuteProfile">
Unmute
</div>
<div v-if="user && !owner" class="list-group-item cursor-pointer text-center rounded text-dark" @click="reportProfile">
Report User
</div>
<div v-if="user && !owner && !relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="blockProfile">
Block
</div>
<div v-if="user && !owner && relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="unblockProfile">
Unblock
</div>
<div class="list-group-item cursor-pointer text-center rounded text-muted" @click="$refs.visitorContextMenu.hide()">
Close
</div>
</div>
</b-modal>
<b-modal ref="ctxModal"
id="ctx-modal"
hide-header
hide-footer
centered
rounded
size="sm"
body-class="list-group-flush p-0 rounded">
<div class="list-group text-center">
<div v-if="ctxMenuStatus && profile.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div>
<div v-if="ctxMenuStatus && profile.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
<div v-if="ctxMenuStatus && profile.id != profile.id && ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">Go to post</div>
<div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
<div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
<div v-if="ctxMenuStatus && (profile.is_admin || profile.id == profile.id)" class="list-group-item rounded cursor-pointer" @click="deletePost(ctxMenuStatus)">Delete</div>
<div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
</div>
</b-modal>
</div>
</div>
</template>
<script type="text/javascript">
export default {
props: [
'profile-id',
],
data() {
return {
id: [],
user: false,
profile: {},
feed: [],
min_id: null,
max_id: null,
loading: true,
owner: false,
layoutType: true,
relationship: null,
warning: false,
ctxMenuStatus: false,
ctxMenuRelationship: false,
}
},
beforeMount() {
this.fetchRelationships();
this.fetchProfile();
},
methods: {
fetchProfile() {
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
this.user = res.data
});
axios.get('/api/pixelfed/v1/accounts/' + this.profileId)
.then(res => {
this.profile = res.data;
this.fetchPosts();
});
},
fetchPosts() {
let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
axios.get(apiUrl, {
params: {
only_media: true,
min_id: 1,
}
})
.then(res => {
let data = res.data
.filter(status => status.media_attachments.length > 0)
.map(status => {
return {
id: status.id,
caption: {
text: status.content_text,
html: status.content
},
count: {
likes: status.favourites_count,
shares: status.reblogs_count,
comments: status.reply_count
},
thumb: status.media_attachments[0].preview_url,
media: status.media_attachments,
timestamp: status.created_at,
type: status.pf_type,
url: status.url
}
});
let ids = data.map(status => status.id);
this.ids = ids;
this.min_id = Math.max(...ids);
this.max_id = Math.min(...ids);
this.feed = data;
this.loading = false;
//this.loadSponsor();
}).catch(err => {
swal('Oops, something went wrong',
'Please release the page.',
'error');
});
},
fetchRelationships() {
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
return;
}
axios.get('/api/pixelfed/v1/accounts/relationships', {
params: {
'id[]': this.profileId
}
}).then(res => {
if(res.data.length) {
this.relationship = res.data[0];
if(res.data[0].blocking == true) {
this.loading = false;
this.warning = true;
}
}
});
},
postPreviewUrl(post) {
return 'background: url("'+post.thumb+'");background-size:cover';
},
timestampFormat(timestamp) {
let ts = new Date(timestamp);
return ts.toDateString() + ' ' + ts.toLocaleTimeString();
},
remoteProfileUrl(profile) {
return '/i/web/profile/_/' + profile.id;
},
remotePostUrl(status) {
return '/i/web/post/_/' + this.profile.id + '/' + status.id;
},
followProfile() {
axios.post('/i/follow', {
item: this.profileId
}).then(res => {
swal('Followed', 'You are now following ' + this.profile.username +'!', 'success');
this.relationship.following = true;
}).catch(err => {
swal('Oops!', 'Something went wrong, please try again later.', 'error');
});
},
unfollowProfile() {
axios.post('/i/follow', {
item: this.profileId
}).then(res => {
swal('Unfollowed', 'You are no longer following ' + this.profile.username +'.', 'warning');
this.relationship.following = false;
}).catch(err => {
swal('Oops!', 'Something went wrong, please try again later.', 'error');
});
},
showCtxMenu() {
this.$refs.visitorContextMenu.show();
},
copyProfileLink() {
navigator.clipboard.writeText(window.location.href);
this.$refs.visitorContextMenu.hide();
},
muteProfile() {
let id = this.profileId;
axios.post('/i/mute', {
type: 'user',
item: id
}).then(res => {
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
this.$refs.visitorContextMenu.hide();
},
unmuteProfile() {
let id = this.profileId;
axios.post('/i/unmute', {
type: 'user',
item: id
}).then(res => {
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully unmuted ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
this.$refs.visitorContextMenu.hide();
},
blockProfile() {
let id = this.profileId;
axios.post('/i/block', {
type: 'user',
item: id
}).then(res => {
this.warning = true;
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
this.$refs.visitorContextMenu.hide();
},
unblockProfile() {
let id = this.profileId;
axios.post('/i/unblock', {
type: 'user',
item: id
}).then(res => {
this.warning = false;
this.fetchRelationships();
this.$refs.visitorContextMenu.hide();
swal('Success', 'You have successfully unblocked ' + this.profile.acct, 'success');
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
this.$refs.visitorContextMenu.hide();
},
reportProfile() {
window.location.href = '/l/i/report?type=profile&id=' + this.profileId;
this.$refs.visitorContextMenu.hide();
},
ctxMenu(status) {
this.ctxMenuStatus = status;
let self = this;
axios.get('/api/pixelfed/v1/accounts/relationships', {
params: {
'id[]': self.profileId
}
}).then(res => {
self.ctxMenuRelationship = res.data[0];
self.$refs.ctxModal.show();
});
},
closeCtxMenu() {
this.ctxMenuStatus = false;
this.ctxMenuRelationship = false;
this.$refs.ctxModal.hide();
},
ctxMenuCopyLink() {
let status = this.ctxMenuStatus;
navigator.clipboard.writeText(status.url);
this.closeCtxMenu();
return;
},
ctxMenuGoToPost() {
let status = this.ctxMenuStatus;
window.location.href = this.statusUrl(status);
this.closeCtxMenu();
return;
},
statusUrl(status) {
return '/i/web/post/_/' + this.profile.id + '/' + status.id;
},
deletePost(status) {
if(this.user.is_admin == false) {
return;
}
if(window.confirm('Are you sure you want to delete this post?') == false) {
return;
}
axios.post('/i/delete', {
type: 'status',
item: status.id
}).then(res => {
this.feed = this.feed.filter(s => {
return s.id != status.id;
});
this.$refs.ctxModal.hide();
}).catch(err => {
swal('Error', 'Something went wrong. Please try again later.', 'error');
});
},
}
}
</script>
<style type="text/css" scoped>
@media (min-width: 1200px) {
.container {
max-width: 1050px;
}
}
</style>

View file

@ -9,86 +9,192 @@
<p class="lead font-weight-lighter">An error occured, results could not be loaded.<br> Please try again later.</p>
</div>
<div v-if="!loading && !networkError" class="mt-5 row">
<div class="col-12 col-md-2 mb-4">
<div v-if="results.hashtags || results.profiles || results.statuses">
<p class="font-weight-bold">Filters</p>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter1" v-model="filters.hashtags">
<label class="custom-control-label text-muted font-weight-light" for="filter1">Hashtags</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter2" v-model="filters.profiles">
<label class="custom-control-label text-muted font-weight-light" for="filter2">Profiles</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="filter3" v-model="filters.statuses">
<label class="custom-control-label text-muted font-weight-light" for="filter3">Statuses</label>
</div>
<div v-if="!loading && !networkError" class="mt-5">
<div v-if="analysis == 'all'" class="row">
<div class="col-12 mb-5">
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
<hr>
</div>
</div>
<div class="col-12 col-md-10">
<p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p>
<hr>
<div v-if="filters.hashtags && results.hashtags" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Hashtags</p>
<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-3 mb-3" style="text-decoration: none;" :href="hashtag.url">
<div class="card card-body text-center shadow-none border">
<p class="lead mb-0 text-truncate text-dark" data-toggle="tooltip" :title="hashtag.value">
#{{hashtag.value}}
</p>
<p class="lead mb-0 small font-weight-bold text-dark">
{{hashtag.count}} posts
</p>
</div>
</a>
</div>
<div v-if="filters.profiles && results.profiles" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Profiles</p>
<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="profile.url">
<div class="card card-body text-center shadow-none border">
<p class="text-center">
<img :src="profile.entity.thumb" width="32px" height="32px" class="rounded-circle box-shadow">
</p>
<p class="font-weight-bold text-truncate text-dark">
{{profile.value}}
</p>
<p class="mb-0 text-center">
<button v-if="profile.entity.follow_request" type="button" class="btn btn-secondary btn-sm py-1 font-weight-bold" disabled>Follow Requested</button>
<button v-if="!profile.entity.follow_request && profile.entity.following" type="button" class="btn btn-secondary btn-sm py-1 font-weight-bold" @click.prevent="followProfile(profile, index)">Unfollow</button>
<button v-if="!profile.entity.follow_request && !profile.entity.following" type="button" class="btn btn-primary btn-sm py-1 font-weight-bold" @click.prevent="followProfile(profile, index)">Follow</button>
</p>
</div>
</a>
</div>
<div v-if="filters.statuses && results.statuses" class="row mb-4">
<p class="col-12 font-weight-bold text-muted">Statuses</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 class="col-md-3">
<div class="mb-4">
<p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
</div>
<div v-if="results.hashtags.length">
<a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
<div class="pb-3">
<div class="media align-items-center py-2 pr-3">
<span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
<i class="fas fa-hashtag text-muted"></i>
</span>
<div class="media-body text-truncate">
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
#{{hashtag.value}}
</p>
<p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
{{hashtag.count}} posts
</p>
</div>
</div>
</div>
</a>
</div>
<div v-else>
<div class="border py-3 text-center font-weight-bold">No results found</div>
</div>
</div>
<div v-if="!results.hashtags && !results.profiles && !results.statuses">
<p class="text-center lead">No results found!</p>
<div class="col-md-5">
<div class="mb-4">
<p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
</div>
<div v-if="results.profiles.length">
<a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
<div class="pb-3">
<div class="media align-items-center py-2 pr-3">
<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
<div class="media-body">
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
{{profile.value}}
</p>
<p class="mb-0 small font-weight-bold text-muted text-uppercase">
{{profile.entity.post_count}} Posts
</p>
</div>
<div class="ml-3">
<a v-if="profile.entity.following" class="btn btn-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">Following</a>
<a v-else class="btn btn-outline-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">View</a>
</div>
</div>
</div>
</a>
</div>
<div v-else>
<div class="border py-3 text-center font-weight-bold">No results found</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-4">
<p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
</div>
<div v-if="results.statuses.length">
<a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
<img :src="status.thumb" width="90px" height="90px" class="mb-2">
</a>
</div>
<div v-else>
<div class="border py-3 text-center font-weight-bold">No results found</div>
</div>
</div>
</div>
<div v-else-if="analysis == 'hashtag'" class="row">
<div class="col-12 mb-5">
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
<hr>
</div>
<div class="col-md-6 offset-md-3">
<div class="mb-4">
<p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
</div>
<div v-if="results.hashtags.length">
<a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
<div class="pb-3">
<div class="media align-items-center py-2 pr-3">
<span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
<i class="fas fa-hashtag text-muted"></i>
</span>
<div class="media-body">
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
#{{hashtag.value}}
</p>
<p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
{{hashtag.count}} posts
</p>
</div>
</div>
</div>
</a>
</div>
<div v-else>
<div class="border py-3 text-center font-weight-bold">No results found</div>
</div>
</div>
</div>
<div v-else-if="analysis == 'profile'" class="row">
<div class="col-12 mb-5">
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
<hr>
</div>
<div class="col-md-6 offset-md-3">
<div class="mb-4">
<p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
</div>
<div v-if="results.profiles.length">
<div v-for="(profile, index) in results.profiles" class="card mb-4">
<div class="card-header p-0 m-0">
<div style="width: 100%;height: 140px;background: #0070b7"></div>
</div>
<div class="card-body">
<div class="text-center mt-n5 mb-4">
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
</div>
<p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
<p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
<div class="d-flex justify-content-center">
<button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Following</button>
<a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="buildUrl('profile',profile)" style="font-weight: 500">View Profile</a>
</div>
</div>
</div>
</div>
<div v-else>
<div class="border py-3 text-center font-weight-bold">No results found</div>
</div>
</div>
</div>
<div v-else-if="analysis == 'webfinger'" class="row">
<div class="col-12 mb-5">
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
<hr>
<div class="col-md-6 offset-md-3">
<div v-for="(profile, index) in results.profiles" class="card mb-2">
<div class="card-header p-0 m-0">
<div style="width: 100%;height: 140px;background: #0070b7"></div>
</div>
<div class="card-body">
<div class="text-center mt-n5 mb-4">
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
</div>
<p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
<p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
<div class="d-flex justify-content-center">
<!-- <button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Unfollow</button> -->
<!-- <button v-else type="button" class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Follow</button> -->
<a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="'/i/web/profile/_/' + profile.entity.id" style="font-weight: 500">View Profile</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="col-12">
<p class="text-center text-muted lead font-weight-bold">No results found</p>
</div>
</div>
</div>
</template>
<style type="text/css" scoped>
.result-card {
text-decoration: none;
}
.result-card .media:hover {
background: #EDF2F7;
}
@media (min-width: 1200px) {
.container {
max-width: 995px;
}
}
</style>
<script type="text/javascript">
@ -108,36 +214,25 @@ export default {
hashtags: true,
profiles: true,
statuses: true
}
},
analysis: 'profile',
}
},
beforeMount() {
this.fetchSearchResults();
this.bootSearch();
},
mounted() {
$('.search-bar input').val(this.query);
},
updated() {
},
methods: {
bootSearch() {
let lexer = this.searchLexer();
this.analysis = lexer;
this.fetchSearchResults();
},
fetchSearchResults() {
axios.get('/api/search', {
params: {
'q': this.query,
'src': 'metro',
'v': 1
}
}).then(res => {
let results = res.data;
this.results.hashtags = results.hashtags;
this.results.profiles = results.profiles;
this.results.statuses = results.posts;
this.loading = false;
}).catch(err => {
this.loading = false;
// this.networkError = true;
})
this.searchContext(this.analysis);
},
followProfile(profile, index) {
@ -159,6 +254,141 @@ export default {
}
});
},
searchLexer() {
let q = this.query;
if(q.startsWith('#')) {
return 'hashtag';
}
if((q.match(/@/g) || []).length == 2) {
return 'webfinger';
}
if(q.startsWith('@') || q.search('@') != -1) {
return 'profile';
}
if(q.startsWith('https://')) {
return 'remote';
}
return 'all';
},
buildUrl(type = 'hashtag', obj) {
switch(type) {
case 'hashtag':
return obj.url + '?src=search';
break;
case 'profile':
if(obj.entity.local == true) {
return obj.url;
}
return '/i/web/profile/_/' + obj.entity.id;
break;
default:
return obj.url + '?src=search';
break;
}
},
searchContext(type) {
switch(type) {
case 'all':
axios.get('/api/search', {
params: {
'q': this.query,
'src': 'metro',
'v': 1,
'scope': 'all'
}
}).then(res => {
let results = res.data;
this.results.hashtags = results.hashtags ? results.hashtags : [];
this.results.profiles = results.profiles ? results.profiles : [];
this.results.statuses = results.posts ? results.posts : [];
this.loading = false;
}).catch(err => {
this.loading = false;
console.log(err);
this.networkError = true;
});
break;
case 'hashtag':
axios.get('/api/search', {
params: {
'q': this.query.slice(1),
'src': 'metro',
'v': 1,
'scope': 'hashtag'
}
}).then(res => {
let results = res.data;
this.results.hashtags = results.hashtags ? results.hashtags : [];
this.results.profiles = results.profiles ? results.profiles : [];
this.results.statuses = results.posts ? results.posts : [];
this.loading = false;
}).catch(err => {
this.loading = false;
console.log(err);
this.networkError = true;
});
break;
case 'profile':
axios.get('/api/search', {
params: {
'q': this.query,
'src': 'metro',
'v': 1,
'scope': 'profile'
}
}).then(res => {
let results = res.data;
this.results.hashtags = results.hashtags ? results.hashtags : [];
this.results.profiles = results.profiles ? results.profiles : [];
this.results.statuses = results.posts ? results.posts : [];
this.loading = false;
}).catch(err => {
this.loading = false;
console.log(err);
this.networkError = true;
});
break;
case 'webfinger':
axios.get('/api/search', {
params: {
'q': this.query,
'src': 'metro',
'v': 1,
'scope': 'webfinger'
}
}).then(res => {
let results = res.data;
this.results.hashtags = [];
this.results.profiles = results.profiles;
this.results.statuses = [];
this.loading = false;
}).catch(err => {
this.loading = false;
console.log(err);
this.networkError = true;
});
break;
default:
this.loading = false;
this.networkError = true;
break;
}
}
}
}

View file

@ -95,7 +95,10 @@
<div class="list-group">
<div v-for="(story, index) in stories" class="list-group-item text-center text-dark" href="#">
<div class="media align-items-center">
<img :src="story.src" class="img-fluid mr-3 cursor-pointer" width="70px" height="70px" @click="showLightbox(story)">
<div class="mr-3 cursor-pointer" @click="showLightbox(story)">
<img :src="story.src" class="img-fluid" width="70px" height="70px">
<p class="small text-muted text-center mb-0">(expand)</p>
</div>
<div class="media-body">
<p class="mb-0">Expires</p>
<p class="mb-0 text-muted small"><span>{{expiresTimestamp(story.expires_at)}}</span></p>

View file

@ -1,7 +1,7 @@
<template>
<div>
<div v-if="stories.length != 0">
<div id="storyContainer" class="m-3"></div>
<div id="storyContainer" :class="[list == true ? 'mt-1 mr-3 mb-0 ml-1':'m-3']"></div>
</div>
</div>
</template>
@ -18,6 +18,7 @@
let Zuck = require('zuck.js');
export default {
props: ['list'],
data() {
return {
stories: {},
@ -34,6 +35,7 @@
.then(res => {
let data = res.data;
let stories = new Zuck('storyContainer', {
list: this.list == true ? true : false,
stories: data,
localStorage: true,
callbacks: {

View file

@ -27,6 +27,7 @@
return {
loading: true,
stories: {},
preloadIndex: null
}
},
@ -36,14 +37,43 @@
methods: {
fetchStories() {
let self = this;
axios.get('/api/stories/v0/profile/' + this.pid)
.then(res => {
let data = res.data;
if(data.length == 0) {
self.stories = res.data;
if(res.data.length == 0) {
window.location.href = '/';
return;
}
window._storyData = data;
self.preloadImages();
})
.catch(err => {
console.log(err);
// window.location.href = '/';
return;
});
},
preloadImages() {
let self = this;
for (var i = 0; i < this.stories[0].items.length; i++) {
var preload = new Image();
$(preload).on('load', function() {
self.preloadIndex = i;
if(i == self.stories[0].items.length) {
self.loadViewer();
return;
}
});
preload.src = self.stories[0].items[i].src;
}
},
loadViewer() {
let data = this.stories;
if(!window.stories) {
window.stories = new Zuck('storyContainer', {
stories: data,
localStorage: false,
@ -67,15 +97,12 @@
},
}
});
this.loading = false;
this.loading = false;
// todo: refactor this mess
document.querySelectorAll('#storyContainer .story')[0].click()
})
.catch(err => {
window.location.href = '/';
return;
});
document.querySelectorAll('#storyContainer .story')[0].click();
}
return;
}
}
}

View file

@ -180,18 +180,18 @@
</div>
</div>
<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white px-2 py-0">
<!--<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white px-2 py-0">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction(status)" v-for="e in emoji">{{e}}</li>
</ul>
</div>
</div>-->
<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white sticky-md-bottom p-0">
<!--<div v-if="status.id == replyId && !status.comments_disabled" class="card-footer bg-white sticky-md-bottom p-0">
<form class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="status.id" data-truncate="false">
<textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
<input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="commentSubmit(status, $event)" :disabled="replyText.length == 0" />
</form>
</div>
</div>-->
</div>
</div>
<div v-if="!loading && feed.length">
@ -448,6 +448,44 @@
<img :src="lightboxMedia.url" style="max-height: 100%; max-width: 100%">
</div>
</b-modal>
<b-modal ref="replyModal"
id="ctx-reply-modal"
hide-footer
centered
rounded
:title-html="replyStatus.account ? 'Reply to <span class=text-dark>' + replyStatus.account.username + '</span>' : ''"
title-tag="p"
title-class="font-weight-bold text-muted"
size="md"
body-class="p-2 rounded">
<div>
<textarea class="form-control" rows="4" style="border: none; font-size: 18px; resize: none; white-space: nowrap;outline: none;" placeholder="Reply here ..." v-model="replyText">
</textarea>
<div class="border-top border-bottom my-2">
<ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
<li class="nav-item" v-on:click="emojiReaction(status)" v-for="e in emoji">{{e}}</li>
</ul>
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="pl-2 small text-muted font-weight-bold text-monospace">
{{replyText.length}}/600
</span>
</div>
<div class="d-flex align-items-center">
<!-- <select class="custom-select custom-select-sm my-0 mr-2">
<option value="public" selected="">Public</option>
<option value="unlisted">Unlisted</option>
<option value="followers">Followers Only</option>
</select> -->
<button class="btn btn-primary btn-sm py-2 px-4 lead text-uppercase font-weight-bold" v-on:click.prevent="commentSubmit(status, $event)" :disabled="replyText.length == 0">
{{replySending == true ? 'POSTING' : 'POST'}}
</button>
</div>
</div>
</div>
</b-modal>
</div>
</template>
@ -513,6 +551,11 @@
padding: 3px;
background: #fff;
}
#ctx-reply-modal .form-control:focus {
border: none;
outline: 0;
box-shadow: none;
}
</style>
<script type="text/javascript">
@ -558,6 +601,7 @@
copiedEmbed: false,
showTips: true,
userStory: false,
replySending: false,
}
},
@ -736,15 +780,20 @@
},
commentFocus(status, $event) {
if(this.replyId == status.id || status.comments_disabled) {
if(status.comments_disabled) {
return;
}
this.status = status;
this.replies = {};
this.replyStatus = {};
this.replyText = '';
this.replyId = status.id;
this.replyStatus = status;
this.$refs.replyModal.show();
this.fetchStatusComments(status, '');
return;
},
likeStatus(status) {
@ -864,6 +913,7 @@
},
commentSubmit(status, $event) {
this.replySending = true;
let id = status.id;
let comment = this.replyText;
axios.post('/i/comment', {
@ -871,8 +921,10 @@
comment: comment
}).then(res => {
this.replyText = '';
this.replies.push(res.data.entity);
this.replies.unshift(res.data.entity);
this.$refs.replyModal.hide();
});
this.replySending = false;
},
moderatePost(status, action, $event) {
@ -1359,22 +1411,19 @@
},
statusUrl(status) {
return status.url;
if(status.local == true) {
return status.url;
}
// if(status.local == true) {
// return status.url;
// }
// return '/i/web/post/_/' + status.account.id + '/' + status.id;
return '/i/web/post/_/' + status.account.id + '/' + status.id;
},
profileUrl(status) {
return status.account.url;
// if(status.local == true) {
// return status.account.url;
// }
if(status.local == true) {
return status.account.url;
}
// return '/i/web/profile/_/' + status.account.id;
return '/i/web/profile/_/' + status.account.id;
},
statusCardUsernameFormat(status) {

34
resources/assets/js/rempos.js vendored Normal file
View file

@ -0,0 +1,34 @@
Vue.component(
'photo-presenter',
require('./components/presenter/PhotoPresenter.vue').default
);
Vue.component(
'video-presenter',
require('./components/presenter/VideoPresenter.vue').default
);
Vue.component(
'photo-album-presenter',
require('./components/presenter/PhotoAlbumPresenter.vue').default
);
Vue.component(
'video-album-presenter',
require('./components/presenter/VideoAlbumPresenter.vue').default
);
Vue.component(
'mixed-album-presenter',
require('./components/presenter/MixedAlbumPresenter.vue').default
);
Vue.component(
'post-menu',
require('./components/PostMenu.vue').default
);
Vue.component(
'remote-post',
require('./components/RemotePost.vue').default
);

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

@ -0,0 +1,4 @@
Vue.component(
'remote-profile',
require('./components/RemoteProfile.vue').default
);

View file

@ -1,10 +1,10 @@
// Landing Page bundle
@import "fonts";
@import "lib/fontawesome";
@import 'variables';
@import '~bootstrap/scss/bootstrap';
@import 'custom';
@import 'landing/carousel';
@import 'landing/devices';
.container.slim {
width: auto;

View file

@ -0,0 +1,11 @@
<?php
return [
'compose' => [
'invalid' => [
'album' => 'Musi zawierać jedno zdjęcie lub film, lub wiele zdjęć.',
],
],
];

View file

@ -0,0 +1,26 @@
<?php
return [
'helpcenter' => 'Centrum pomocy',
'whatsnew' => 'Co nowego',
'gettingStarted' => 'Rozpocznij',
'sharingMedia' => 'Wstawianie multimediów',
'profile' => 'Profil',
'stories' => 'Relacje',
'hashtags' => 'Hashtagi',
'discover' => 'Odkrywanie',
'directMessages' => 'Wiadomości bezpośrednie',
'timelines' => 'Osie czasu',
'embed' => 'Embed',
'communityGuidelines' => 'Wytyczne dla społeczności',
'whatIsTheFediverse' => 'Czym jest Fediwersum?',
'controllingVisibility' => 'Kontrolowanie widoczności',
'blockingAccounts' => 'Blokowanie kont',
'safetyTips' => 'Wskazówki dot. bezpieczeństwa',
'reportSomething' => 'Zgłaszanie treści',
'dataPolicy' => 'Polityka przechowywania danych'
];

View file

@ -1,18 +1,19 @@
<?php
return [
'search' => 'Szukaj',
'home' => 'Strona główna',
'local' => 'Lokalne',
'discover' => 'Odkrywaj',
'viewMyProfile' => 'Pokaż mój profil',
'myTimeline' => 'Moja oś czasu',
'publicTimeline' => 'Publiczna oś czasu',
'remoteFollow' => 'Zdalne śledzenie',
'settings' => 'Ustawienia',
'admin' => 'Administracja',
'logout' => 'Wyloguj się',
'directMessages' => 'Wiadomości bezpośrednie',
'search' => 'Szukaj',
'home' => 'Strona główna',
'local' => 'Lokalne',
'network' => 'Sieć',
'discover' => 'Odkrywaj',
'viewMyProfile' => 'Pokaż mój profil',
'myProfile' => 'Mój profil',
'myTimeline' => 'Moja oś czasu',
'publicTimeline' => 'Publiczna oś czasu',
'remoteFollow' => 'Zdalne śledzenie',
'settings' => 'Ustawienia',
'admin' => 'Administracja',
'logout' => 'Wyloguj się',
'directMessages' => 'Wiadomości bezpośrednie',
'composePost' => 'Uwtórz wpis',
];

View file

@ -3,6 +3,7 @@
return [
'likedPhoto' => 'polubił(a) Twoje zdjęcie.',
'likedComment' => 'polubił(a) Twój komentarz.',
'startedFollowingYou' => 'zaczął(-ęła) Cię obserwować.',
'commented' => 'skomentował(a) Twój wpis',
'mentionedYou' => 'wspomniał(a) o Tobie.',

View file

@ -9,4 +9,7 @@ return [
'privateProfileWarning' => 'To konto jest prywatne',
'alreadyFollow' => 'Już obserwujesz :username?',
'loginToSeeProfile' => 'aby zobaczyć zdjęcia i filmy tego użytkownika.',
'status.disabled.header' => 'Profil jest niedostępny',
'status.disabled.body' => 'Przepraszamy, ten profil nie jest obecnie dostępny. Spróbuj ponownie za jakiś czas.',
];

View file

@ -12,5 +12,9 @@ return [
'l10nWip' => 'Wciąż pracujemy nad obsługą wielu języków',
'currentLocale' => 'Obecny język',
'selectLocale' => 'Wybierz jeden z dostępnych języków',
'contact' => 'Kontakt',
'contact-us' => 'Skontaktuj się z naim',
'places' => 'Miejsca',
'profiles' => 'Profile',
];

View file

@ -13,16 +13,20 @@
<form method="POST">
@csrf
<div class="form-group row">
<div class="form-group">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{__('Password')}}" required>
<div class="col-md-12">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{__('Password')}}" required>
@if ($errors->has('password'))
<span class="invalid-feedback">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
@if ($errors->has('password'))
<span class="invalid-feedback">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="trusted-device" name="trustDevice">
<label class="custom-control-label text-muted" for="trusted-device">Don't ask me again, trust this device</label>
</div>
</div>

View file

@ -26,11 +26,11 @@
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@stack('styles')
<script type="text/javascript">window.App = {}; window.App.config = {!!App\Util\Site\Config::json()!!}</script>
<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
</head>
<body class="">
<main id="content">
<body class="w-100 h-100">
<main id="content" class="w-100 h-100">
@yield('content')
</main>
<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>

View file

@ -0,0 +1,16 @@
@extends('layouts.app')
@section('content')
<remote-profile profile-id="{{$profile->id}}"></remote-profile>
@endsection
@push('meta')
<meta name="robots" content="noindex, noimageindex, nofollow, nosnippet, noarchive">
@endpush
@push('scripts')
<script type="text/javascript" src="{{mix('js/rempro.js')}}"></script>
<script type="text/javascript">
App.boot();
</script>
@endpush

View file

@ -1,7 +1,7 @@
@extends('layouts.app')
@section('content')
<story-viewer pid="{{$pid}}"></story-viewer>
<story-viewer pid="{{$pid}}" redirect="{{$profile->url()}}"></story-viewer>
@endsection
@push('scripts')

View file

@ -4,12 +4,9 @@
<search-results query="{{request()->query('q')}}" profile-id="{{Auth::user()->profile->id}}"></search-results>
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
<script type="text/javascript" src="{{mix('js/search.js')}}"></script>
<script type="text/javascript">
new Vue({
el: '#content'
})
</script>
<script type="text/javascript">App.boot();</script>
@endpush

View file

@ -9,6 +9,9 @@
<div class="alert alert-primary px-3 h6 text-center">
<strong>Warning:</strong> Some experimental features may contain bugs or missing functionality
</div>
<div class="alert alert-warning px-3 h6">
We are deprecating Labs in a future version. Some features will no longer be supported. For more information, click <a href="{{route('help.labs-deprecation')}}" class="font-weight-bold">here</a>.
</div>
<div class="py-3">
<p class="font-weight-bold text-muted text-center">UI</p>
<hr>

View file

@ -10,8 +10,6 @@
<p>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.muted-users')}}">Muted Users</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-users')}}">Blocked Users</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-keywords')}}">Blocked keywords</a>
<a class="btn btn-outline-secondary py-0 font-weight-bold" href="{{route('settings.privacy.blocked-instances')}}">Blocked instances</a>
</p>
</div>
<form method="post">

View file

@ -23,7 +23,7 @@
</ul>
</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">Discover Tips</div>
<div class="card-header text-light font-weight-bold h4 p-4 bg-primary">Discover Tips</div>
<div class="card-body bg-white p-3">
<ul class="pt-3">
<li class="lead mb-4">To make your posts more discoverable, add hashtags to your posts.</li>

View file

@ -54,7 +54,7 @@
</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-header text-light font-weight-bold h4 p-4 bg-primary">Hashtag Tips</div>
<div class="card-body bg-white p-3">
<ul class="pt-3">
<li class="lead mb-4">You cannot add spaces or punctuation in a hashtag, or it will not work properly.</li>

View file

@ -0,0 +1,21 @@
@extends('site.help.partial.template', ['breadcrumb'=>'Labs Deprecation'])
@section('section')
<div class="title">
<h3 class="font-weight-bold">Labs Deprecation</h3>
</div>
<hr>
<p class="lead">We are deprecating Labs in a future version. No new experiments will be released. We will give 6 months notice before removing Labs.</p>
<hr>
<h4 class="font-weight-bold">When will labs be deprecated?</h4>
<p>TBA</p>
<hr>
<h4 class="font-weight-bold">What features will be deprecated?</h4>
<p>TBA</p>
<hr>
<h4 class="font-weight-bold">Why is Labs being deprecated?</h4>
<p>Labs was started in early 2019 to test experimental designs and features. Now the project is more mature, we are outgrowing some of these experiments.</p>
<hr>
<p class="small">Last Updated: April 10/2020</p>
@endsection

View file

@ -3,12 +3,6 @@
@section('content')
<div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
<div class="col-12">
<div class="alert alert-info">
<p class="lead mb-0">Some sections may contain out of date information</p>
<p class="mb-0">We apologize for any inconvenience, we are working on updating the Help Center.</p>
</div>
</div>
<div class="col-12 px-0">
<div class="card mt-md-5 px-0 mx-md-3">
<div class="card-header font-weight-bold text-muted bg-white py-4">

View file

@ -28,7 +28,7 @@
</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-header text-light font-weight-bold h4 p-4 bg-primary">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>

View file

@ -23,161 +23,281 @@
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
<script type="text/javascript">window.App = {}; window.App.config = {!!App\Util\Site\Config::json()!!}</script>
<style type="text/css">
.feature-circle {
display: flex !important;
-webkit-box-pack: center !important;
justify-content: center !important;
-webkit-box-align: center !important;
align-items: center !important;
margin-right: 1rem !important;
background-color: #08d !important;
color: #fff;
border-radius: 50% !important;
width: 60px;
height:60px;
}
.section-spacer {
height: 13vh;
}
</style>
</head>
<body class="">
<main id="content">
<section class="container">
<div class="row py-5 mb-5">
<div class="section-spacer"></div>
<div class="row pt-md-5 mt-5">
<div class="col-12 col-md-6 d-none d-md-block">
<div class="m-md-4" style="position: absolute; transform: scale(0.66)">
<div class="marvel-device note8" style="position: absolute;z-index:10;">
<div class="inner"></div>
<div class="overflow">
<div class="shadow"></div>
</div>
<div class="speaker"></div>
<div class="sensors"></div>
<div class="more-sensors"></div>
<div class="sleep"></div>
<div class="volume"></div>
<div class="camera"></div>
<div class="screen">
<img src="/img/landing/android_1.jpg" class="img-fluid" loading="lazy">
<div class="m-my-4">
<p class="display-2 font-weight-bold">Photo Sharing</p>
<p class="h1 font-weight-bold">For Everyone.</p>
</div>
</div>
<div class="col-12 col-md-5 offset-md-1">
<div>
<div class="pt-md-3 d-flex justify-content-center align-items-center">
<img src="/img/pixelfed-icon-color.svg" loading="lazy" width="50px" height="50px">
<span class="font-weight-bold h3 ml-2 pt-2">Pixelfed</span>
</div>
<div class="d-block d-md-none">
<p class="font-weight-bold mb-0 text-center">Photo Sharing. For Everyone</p>
</div>
<div class="card my-4 shadow-none border">
<div class="card-body px-lg-5">
<div class="text-center">
<p class="small text-uppercase font-weight-bold text-muted">Account Login</p>
</div>
<div>
<form class="px-1" method="POST" action="{{ route('login') }}" id="login_form">
@csrf
<div class="form-group row">
<div class="col-md-12">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" placeholder="{{__('Email')}}" required autofocus>
@if ($errors->has('email'))
<span class="invalid-feedback">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{__('Password')}}" required>
@if ($errors->has('password'))
<span class="invalid-feedback">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<div class="checkbox">
<label>
<input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}>
<span class="font-weight-bold small ml-1 text-muted">
{{ __('Remember Me') }}
</span>
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-12">
<button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold text-uppercase">
{{ __('Login') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="marvel-device iphone-x" style="position: absolute;z-index: 20;margin: 99px 0 0 151px;">
<div class="notch">
<div class="camera"></div>
<div class="speaker"></div>
<div class="card shadow-none border card-body">
<p class="text-center mb-0 font-weight-bold small">
@if(config('pixelfed.open_registration'))
<a href="/register">Register</a>
<span class="px-1">·</span>
@endif
<a href="/password/reset">Password Reset</a>
</p>
</div>
</div>
</div>
</div>
<div class="section-spacer"></div>
<div class="row py-5 mt-5 mb-5">
<div class="col-12 col-md-6 d-none d-md-block">
<div>
<div class="row mt-4 mb-1">
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/1.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="top-bar"></div>
<div class="sleep"></div>
<div class="bottom-bar"></div>
<div class="volume"></div>
<div class="overflow">
<div class="shadow shadow--tr"></div>
<div class="shadow shadow--tl"></div>
<div class="shadow shadow--br"></div>
<div class="shadow shadow--bl"></div>
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/2.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="inner-shadow"></div>
<div class="screen">
<div id="iosDevice">
<img src="/img/landing/ios_4.jpg" class="img-fluid" loading="lazy">
<img src="/img/landing/ios_3.jpg" class="img-fluid" loading="lazy">
<img src="/img/landing/ios_2.jpg" class="img-fluid" loading="lazy">
<img src="/img/landing/ios_1.jpg" class="img-fluid" loading="lazy">
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/3.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/4.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/5.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/6.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/7.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/8.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
<div class="col-4 mt-2 px-0">
<div class="px-1 shadow-none">
<img src="/_landing/9.jpeg" class="img-fluid" loading="lazy" width="640px" height="640px">
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-5 offset-md-1">
<div>
<div class="card my-4 shadow-none border">
<div class="card-body px-lg-5">
<div class="text-center pt-3">
<img src="/img/pixelfed-icon-color.svg">
</div>
<div class="py-3 text-center">
<h3 class="font-weight-bold">Pixelfed</h3>
<p class="mb-0 lead">Photo sharing for everyone</p>
</div>
<div>
@if(true === config('pixelfed.open_registration'))
<form class="px-1" method="POST" action="{{ route('register') }}" id="register_form">
@csrf
<div class="form-group row">
<div class="col-md-12">
<input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" placeholder="{{ __('Name') }}" required autofocus>
@if ($errors->has('name'))
<span class="invalid-feedback">
<strong>{{ $errors->first('name') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<input id="username" type="text" class="form-control{{ $errors->has('username') ? ' is-invalid' : '' }}" name="username" value="{{ old('username') }}" placeholder="{{ __('Username') }}" required maxlength="15" minlength="2">
@if ($errors->has('username'))
<span class="invalid-feedback">
<strong>{{ $errors->first('username') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" placeholder="{{ __('E-Mail Address') }}" required>
@if ($errors->has('email'))
<span class="invalid-feedback">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{ __('Password') }}" required>
@if ($errors->has('password'))
<span class="invalid-feedback">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required>
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" name="agecheck" type="checkbox" value="true" id="ageCheck" required>
<label class="form-check-label" for="ageCheck">
I am at least 16 years old
</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-md-12">
<button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold">
{{ __('Register') }}
</button>
</div>
</div>
<p class="mb-0 font-weight-bold text-lighter small">By signing up, you agree to our <a href="{{route('site.terms')}}" class="text-muted">Terms of Use</a> and <a href="{{route('site.privacy')}}" class="text-muted">Privacy Policy</a>.</p>
</form>
@else
<div style="min-height: 350px" class="d-flex justify-content-center align-items-center">
<div class="text-center">
<p class="lead">Registrations are closed.</p>
<p class="text-lighter small">You can find a list of other instances on <a href="https://the-federation.info/pixelfed" class="text-muted font-weight-bold">the-federation.info/pixelfed</a> or <a href="https://fediverse.network/pixelfed" class="text-muted font-weight-bold">fediverse.network/pixelfed</a></p>
</div>
</div>
@endif
</div>
</div>
</div>
<div class="card shadow-none border card-body">
<p class="text-center mb-0 font-weight-bold">Have an account? <a href="/login">Log in</a></p>
<div class="section-spacer"></div>
<div class="mt-5">
<p class="text-center h1 font-weight-bold">Simple. Powerful.</p>
</div>
<div class="mt-5">
<div class="d-flex justify-content-between align-items-center">
<span class="text-center">
<span class="font-weight-bold h1">{{$data['stats']['posts']}}</span>
<span class="d-block text-muted text-uppercase">Posts</span>
</span>
<span class="text-center">
<span class="font-weight-bold h1">{{$data['stats']['likes']}}</span>
<span class="d-block text-muted text-uppercase">Likes</span>
</span>
<span class="text-center">
<span class="font-weight-bold h1">{{$data['stats']['hashtags']}}</span>
<span class="d-block text-muted text-uppercase">Hashtags Used</span>
</span>
</div>
</div>
<div class="mt-5">
<p class="lead text-muted text-center">A free and ethical photo sharing platform.</p>
</div>
</div>
</div>
<div class="row py-5 mb-5">
<div class="col-12 col-md-8 offset-md-2">
<div class="section-spacer"></div>
<div class="mt-5">
<p class="text-center display-4 font-weight-bold">Feature Packed.</p>
</div>
<div class="my-2">
<p class="h4 font-weight-light text-muted text-center">The best for the brightest.</p>
</div>
</div>
</div>
<div class="row pb-5 mb-5">
<div class="col-12 col-md-5 offset-md-1">
<div class="mb-5">
<div class="media">
<div class="feature-circle">
<i class="far fa-images fa-lg"></i>
</div>
<div class="media-body">
<p class="h5 font-weight-bold mt-2 mb-0">Albums</p>
Create an album with up to <span class="font-weight-bold">{{config('pixelfed.max_album_length')}}</span> photos
</div>
</div>
</div>
<div class="mb-5">
<div class="media">
<div class="feature-circle">
<i class="far fa-folder fa-lg"></i>
</div>
<div class="media-body">
<p class="h5 font-weight-bold mt-2 mb-0">Collections</p>
Organize your posts
</div>
</div>
</div>
<div class="mb-5">
<div class="media">
<div class="feature-circle">
<i class="fas fa-image fa-lg"></i>
</div>
<div class="media-body">
<p class="h5 font-weight-bold mt-2 mb-0">Filters</p>
Add a filter to your photos
</div>
</div>
</div>
</div>
<div class="col-12 col-md-5 offset-md-1">
<div class="mb-5">
<div class="media">
<div class="feature-circle">
<i class="far fa-comment fa-lg"></i>
</div>
<div class="media-body">
<p class="h5 font-weight-bold mt-2 mb-0">Comments</p>
Comment on a post, or send a reply
</div>
</div>
</div>
<div class="mb-5">
<div class="media">
<div class="feature-circle">
<i class="far fa-list-alt fa-lg"></i>
</div>
<div class="media-body">
<p class="h5 font-weight-bold mt-2 mb-0">Discover</p>
Explore categories, hashtags and topics
</div>
</div>
</div>
@if(config('instance.stories.enabled'))
<div class="mb-5">
<div class="media">
<div class="feature-circle">
<i class="fas fa-history fa-lg"></i>
</div>
<div class="media-body">
<p class="h5 font-weight-bold mt-2 mb-0">Stories</p>
Share posts that disappear after 24h
</div>
</div>
</div>
@endif
</div>
</div>
</section>
</main>
@include('layouts.partial.footer')

View file

@ -0,0 +1,17 @@
@extends('layouts.app')
@section('content')
<div class="mt-md-4"></div>
<remote-post status-template="{{$status->viewType()}}" status-id="{{$status->id}}" status-username="{{$status->profile->username}}" status-url="{{$status->url()}}" status-profile-url="{{$status->profile->url()}}" status-avatar="{{$status->profile->avatarUrl()}}" status-profile-id="{{$status->profile_id}}" profile-layout="metro"></remote-post>
@endsection
@push('meta')
<meta name="robots" content="noindex, noimageindex, nofollow, nosnippet, noarchive">
@endpush
@push('scripts')
<script type="text/javascript" src="{{ mix('js/rempos.js') }}"></script>
<script type="text/javascript">App.boot()</script>
@endpush

View file

@ -1,4 +1,4 @@
@extends('layouts.app',['title' => "A post by " . $user->username])
@extends('layouts.app',['title' => "{$user->username} shared a post"])
@section('content')
<noscript>
@ -25,7 +25,6 @@
@push('scripts')
<script type="text/javascript" src="{{ mix('js/status.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">
$(document).ready(function() {
new Vue({

View file

@ -171,7 +171,7 @@ 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('exp/rec', 'ApiController@userRecommendations');
Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');;
Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');
Route::get('discover/tag/list', 'HashtagFollowController@getTags');
// Route::get('profile/sponsor/{id}', 'ProfileSponsorController@get');
Route::get('bookmarks', 'InternalApiController@bookmarks');
@ -263,6 +263,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('stories/viewed', 'StoryController@apiV1Viewed');
Route::get('stories/new', 'StoryController@compose');
Route::get('my/story', 'StoryController@iRedirect');
Route::get('web/profile/_/{id}', 'InternalApiController@remoteProfile');
Route::get('web/post/_/{profileId}/{statusid}', 'InternalApiController@remoteStatus');
});
Route::group(['prefix' => 'account'], function () {
@ -393,6 +395,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::view('blocking-accounts', 'site.help.blocking-accounts')->name('help.blocking-accounts');
Route::view('report-something', 'site.help.report-something')->name('help.report-something');
Route::view('data-policy', 'site.help.data-policy')->name('help.data-policy');
Route::view('labs-deprecation', 'site.help.labs-deprecation')->name('help.labs-deprecation');
});
Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show');
Route::get('newsroom/archive', 'NewsroomController@archive');

3
webpack.mix.js vendored
View file

@ -38,6 +38,9 @@ mix.js('resources/assets/js/app.js', 'public/js')
// .js('resources/assets/js/direct.js', 'public/js')
// .js('resources/assets/js/admin.js', 'public/js')
// .js('resources/assets/js/micro.js', 'public/js')
.js('resources/assets/js/rempro.js', 'public/js')
.js('resources/assets/js/rempos.js', 'public/js')
//.js('resources/assets/js/timeline_next.js', 'public/js')
.extract([
'lodash',