31
.dependabot/config.yml
Normal 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
|
@ -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=
|
|
@ -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
|
||||
|
35
CHANGELOG.md
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,15 +36,17 @@ 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();
|
||||
$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);
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,39 +34,54 @@ 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);
|
||||
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']) && 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') {
|
||||
if( isset($remote['type']) &&
|
||||
$remote['type'] == 'Note') {
|
||||
$item = Helpers::statusFetch($tag);
|
||||
$tokens['posts'] = [[
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'status',
|
||||
|
@ -70,69 +91,12 @@ class SearchController extends Controller
|
|||
'thumb' => $item->thumb(),
|
||||
]];
|
||||
}
|
||||
}
|
||||
}
|
||||
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
|
||||
$hashtags = Hashtag::select('id', 'name', 'slug')
|
||||
->where('slug', 'like', '%'.$htag.'%')
|
||||
->whereHas('posts')
|
||||
->limit(20)
|
||||
->get();
|
||||
if($hashtags->count() > 0) {
|
||||
$tags = $hashtags->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => $item->posts()->count(),
|
||||
'url' => $item->url(),
|
||||
'type' => 'hashtag',
|
||||
'value' => $item->name,
|
||||
'tokens' => '',
|
||||
'name' => null,
|
||||
];
|
||||
});
|
||||
$tokens['hashtags'] = $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();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
$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)
|
||||
->whereProfileId(Auth::user()->profile_id)
|
||||
->where('caption', 'like', '%'.$tag.'%')
|
||||
->latest()
|
||||
->limit(10)
|
||||
|
@ -151,10 +115,121 @@ class SearchController extends Controller
|
|||
'filter' => $item->firstMedia()->filter_class
|
||||
];
|
||||
});
|
||||
$tokens['posts'] = $posts;
|
||||
$this->tokens['posts'] = $posts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($tokens);
|
||||
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.'%')
|
||||
->whereHas('posts')
|
||||
->limit(20)
|
||||
->get();
|
||||
if($hashtags->count() > 0) {
|
||||
$tags = $hashtags->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => $item->posts()->count(),
|
||||
'url' => $item->url(),
|
||||
'type' => 'hashtag',
|
||||
'value' => $item->name,
|
||||
'tokens' => '',
|
||||
'name' => null,
|
||||
];
|
||||
});
|
||||
return $tags;
|
||||
}
|
||||
});
|
||||
$this->tokens['hashtags'] = $tokens;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
// 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($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()
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 [];
|
||||
|
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
'share' => 'reblog',
|
||||
'like' => 'favourite',
|
||||
'comment' => 'mention',
|
||||
'admin.user.modlog.comment' => 'modlog'
|
||||
];
|
||||
return $verbs[$verb];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
||||
];
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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"]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
file_uploads = On
|
||||
memory_limit = 128M
|
||||
upload_max_filesize = 64M
|
||||
post_max_size = 64M
|
||||
max_execution_time = 600
|
1916
contrib/docker/php.production.ini
Normal file
15
contrib/docker/start.apache.sh
Normal 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
|
15
contrib/docker/start.fpm.sh
Normal 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
|
|
@ -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
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
After Width: | Height: | Size: 54 KiB |
BIN
public/_landing/2.jpeg
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
public/_landing/3.jpeg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
public/_landing/4.jpeg
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
public/_landing/5.jpeg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/_landing/6.jpeg
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
public/_landing/7.jpeg
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
public/_landing/8.jpeg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
public/_landing/9.jpeg
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
public/css/landing.css
vendored
BIN
public/js/profile.js
vendored
BIN
public/js/rempos.js
vendored
Normal file
BIN
public/js/rempro.js
vendored
Normal file
BIN
public/js/search.js
vendored
BIN
public/js/status.js
vendored
BIN
public/js/story-compose.js
vendored
BIN
public/js/theme-monokai.js
vendored
BIN
public/js/timeline.js
vendored
BIN
public/js/vendor.js
vendored
|
@ -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;
|
||||
|
|
|
@ -137,15 +137,21 @@
|
|||
|
||||
<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-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>
|
||||
|
@ -161,9 +167,9 @@
|
|||
<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" 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 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="">
|
||||
|
@ -171,12 +177,14 @@
|
|||
<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)">
|
||||
<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" :key="'cr' + s.id + '_' + index">
|
||||
<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>
|
||||
|
@ -198,6 +206,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">
|
||||
<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">@{{user.acct.split('@')[1]}}</span>
|
||||
</p>
|
||||
<p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
|
||||
{{user.display_name}}
|
||||
</a>
|
||||
</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
|
||||
|
@ -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');
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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">@{{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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1028
resources/assets/js/components/RemotePost.vue
Normal file
447
resources/assets/js/components/RemoteProfile.vue
Normal 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>
|
|
@ -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>
|
||||
</div>
|
||||
<div class="col-12 col-md-10">
|
||||
<p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p>
|
||||
<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 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">
|
||||
</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 class="lead mb-0 small font-weight-bold text-dark">
|
||||
<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-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">
|
||||
<div v-else>
|
||||
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 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 class="mb-0 small font-weight-bold text-muted text-uppercase">
|
||||
{{profile.entity.post_count}} Posts
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<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 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>
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!results.hashtags && !results.profiles && !results.statuses">
|
||||
<p class="text-center lead">No results found!</p>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 = '/';
|
||||
document.querySelectorAll('#storyContainer .story')[0].click();
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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) {
|
||||
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
|
@ -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
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'remote-profile',
|
||||
require('./components/RemoteProfile.vue').default
|
||||
);
|
4
resources/assets/sass/landing.scss
vendored
|
@ -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;
|
||||
|
|
11
resources/lang/pl/exception.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'compose' => [
|
||||
'invalid' => [
|
||||
'album' => 'Musi zawierać jedno zdjęcie lub film, lub wiele zdjęć.',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
26
resources/lang/pl/helpcenter.php
Normal 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'
|
||||
|
||||
];
|
|
@ -1,12 +1,13 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'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',
|
||||
|
@ -14,5 +15,5 @@ return [
|
|||
'admin' => 'Administracja',
|
||||
'logout' => 'Wyloguj się',
|
||||
'directMessages' => 'Wiadomości bezpośrednie',
|
||||
|
||||
'composePost' => 'Uwtórz wpis',
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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.',
|
||||
];
|
||||
|
|
|
@ -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',
|
||||
|
||||
];
|
||||
|
|
|
@ -13,9 +13,7 @@
|
|||
<form method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="form-group row">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{__('Password')}}" required>
|
||||
|
||||
@if ($errors->has('password'))
|
||||
|
@ -24,6 +22,12 @@
|
|||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="form-group row mb-0">
|
||||
|
|
|
@ -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>
|
||||
|
|
16
resources/views/profile/remote.blade.php
Normal 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
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
21
resources/views/site/help/labs-deprecation.blade.php
Normal 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
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -23,98 +23,57 @@
|
|||
<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>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<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 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 class="text-center">
|
||||
<p class="small text-uppercase font-weight-bold text-muted">Account Login</p>
|
||||
</div>
|
||||
<div>
|
||||
@if(true === config('pixelfed.open_registration'))
|
||||
<form class="px-1" method="POST" action="{{ route('register') }}" id="register_form">
|
||||
<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="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>
|
||||
<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">
|
||||
|
@ -125,6 +84,7 @@
|
|||
</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>
|
||||
|
||||
|
@ -138,46 +98,206 @@
|
|||
|
||||
<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
|
||||
<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">
|
||||
|
||||
<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">
|
||||
{{ __('Register') }}
|
||||
<button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold text-uppercase">
|
||||
{{ __('Login') }}
|
||||
</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>
|
||||
</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="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="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 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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</main>
|
||||
@include('layouts.partial.footer')
|
||||
|
|
17
resources/views/status/remote.blade.php
Normal 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
|
|
@ -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({
|
||||
|
|
|
@ -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
|
@ -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',
|
||||
|
|