1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/pixelfed_ynh.git synced 2024-09-03 20:06:04 +02:00

Merge pull request #1906 from pixelfed/staging

Add S3 + Stories
This commit is contained in:
daniel 2020-01-07 00:58:15 -07:00 committed by GitHub
commit 700c7805ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1066 additions and 195 deletions

View file

@ -0,0 +1,64 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\{
DB,
Storage
};
use App\{
Story,
StoryView
};
class StoryGC extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'story:gc';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear expired Stories';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$stories = Story::where('expires_at', '<', now())->take(50)->get();
if($stories->count() == 0) {
exit;
}
foreach($stories as $story) {
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
DB::transaction(function() use($story) {
StoryView::whereStoryId($story->id)->delete();
$story->delete();
});
}
}
}

View file

@ -30,6 +30,7 @@ class Kernel extends ConsoleKernel
$schedule->command('media:gc')
->hourly();
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('story:gc')->everyFiveMinutes();
}
/**

View file

@ -111,6 +111,10 @@ class FollowerController extends Controller
Cache::forget('api:local:exp:rec:'.$user->id);
Cache::forget('user:account:id:'.$target->user_id);
Cache::forget('user:account:id:'.$user->user_id);
Cache::forget('px:profile:followers-v1.3:'.$user->id);
Cache::forget('px:profile:followers-v1.3:'.$target->id);
Cache::forget('px:profile:following-v1.3:'.$user->id);
Cache::forget('px:profile:following-v1.3:'.$target->id);
return $target->url();
}

View file

@ -244,7 +244,7 @@ class InternalApiController extends Controller
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
'comments_disabled' => 'nullable|boolean'
'comments_disabled' => 'nullable'
]);
if(config('costar.enabled') == true) {
@ -301,7 +301,7 @@ class InternalApiController extends Controller
}
if($request->filled('comments_disabled')) {
$status->comments_disabled = $request->input('comments_disabled');
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
$status->caption = strip_tags($request->caption);
@ -314,10 +314,6 @@ class InternalApiController extends Controller
$media->save();
}
// $resource = new Fractal\Resource\Collection($status->media()->orderBy('order')->get(), new StatusMediaContainerTransformer());
// $mediaContainer = $this->fractal->createData($resource)->toArray();
// $status->media_container = json_encode($mediaContainer);
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
$cw = $profile->cw == true ? true : $cw;
$status->is_nsfw = $cw;

View file

@ -9,6 +9,7 @@ use View;
use App\Follower;
use App\FollowRequest;
use App\Profile;
use App\Story;
use App\User;
use App\UserFilter;
use League\Fractal;
@ -135,6 +136,21 @@ class ProfileController extends Controller
return false;
}
public static function accountCheck(Profile $profile)
{
switch ($profile->status) {
case 'disabled':
case 'suspended':
case 'delete':
return view('profile.disabled');
break;
default:
break;
}
return abort(404);
}
protected function blockedProfileCheck(Profile $profile)
{
$pid = Auth::user()->profile->id;
@ -215,4 +231,18 @@ class ProfileController extends Controller
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function stories(Request $request, $username)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$pid = $profile->id;
$authed = Auth::user()->profile;
abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404);
$exists = Story::whereProfileId($pid)
->where('expires_at', '>', now())
->count();
abort_unless($exists > 0, 404);
return view('profile.story', compact('pid'));
}
}

View file

@ -3,6 +3,15 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Media;
use App\Profile;
use App\Story;
use App\StoryView;
use App\Services\StoryService;
use Cache, Storage;
use App\Services\FollowerService;
class StoryController extends Controller
{
@ -12,8 +21,235 @@ class StoryController extends Controller
$this->middleware('auth');
}
public function home(Request $request)
public function apiV1Add(Request $request)
{
return view('stories.home');
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'file.*' => function() {
return [
'required',
'mimes:image/jpeg,image/png',
'max:' . config('pixelfed.max_photo_size'),
];
},
]);
$user = $request->user();
if(Story::whereProfileId($user->profile_id)->where('expires_at', '>', now())->count() >= Story::MAX_PER_DAY) {
abort(400, 'You have reached your limit for new Stories today.');
}
$story = new Story();
$story->profile_id = $user->profile_id;
$story->save();
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
$rid = Str::random(6).'.'.Str::random(9);
$photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [
'image/jpeg',
'image/png'
]) == false) {
abort(400, 'Invalid media type');
return;
}
$storagePath = "public/_esm.t1/{$monthHash}/{$story->id}/{$rid}";
$path = $photo->store($storagePath);
$story->path = $path;
$story->local = true;
$story->expires_at = now()->addHours(24);
$story->save();
return [
'code' => 200,
'msg' => 'Successfully added',
'media_url' => url(Storage::url($story->path))
];
}
public function apiV1Delete(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
$story->delete();
return [
'code' => 200,
'msg' => 'Successfully deleted'
];
}
public function apiV1Recent(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$profile = $request->user()->profile;
$following = FollowerService::build()->profile($profile)->following();
$stories = Story::with('profile')
->whereIn('profile_id', $following)
->groupBy('profile_id')
->where('expires_at', '>', now())
->orderByDesc('expires_at')
->take(9)
->get()
->map(function($s, $k) {
return [
'id' => (string) $s->id,
'photo' => $s->profile->avatarUrl(),
'name' => $s->profile->username,
'link' => $s->profile->url(),
'lastUpdated' => (int) $s->created_at->format('U'),
'seen' => $s->seen(),
'items' => [],
'pid' => (string) $s->profile->id
];
});
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function apiV1Fetch(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$profile = $request->user()->profile;
if($id == $profile->id) {
$publicOnly = true;
} else {
$following = FollowerService::build()->profile($profile)->following();
$publicOnly = in_array($id, $following);
}
$stories = Story::whereProfileId($id)
->orderBy('expires_at', 'desc')
->where('expires_at', '>', now())
->when(!$publicOnly, function($query, $publicOnly) {
return $query->wherePublic(true);
})
->get()
->map(function($s, $k) {
return [
'id' => (string) $s->id,
'type' => 'photo',
'length' => 3,
'src' => url(Storage::url($s->path)),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $s->created_at->format('U'),
'expires_at' => (int) $s->expires_at->format('U'),
'seen' => $s->seen()
];
})->toArray();
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function apiV1Profile(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile;
$profile = Profile::findOrFail($id);
if($id == $authed->id) {
$publicOnly = true;
} else {
$following = FollowerService::build()->profile($authed)->following();
$publicOnly = in_array($id, $following);
}
$stories = Story::whereProfileId($profile->id)
->orderBy('expires_at')
->where('expires_at', '>', now())
->when(!$publicOnly, function($query, $publicOnly) {
return $query->wherePublic(true);
})
->get()
->map(function($s, $k) {
return [
'id' => $s->id,
'type' => 'photo',
'length' => 3,
'src' => url(Storage::url($s->path)),
'preview' => null,
'link' => null,
'linkText' => null,
'time' => $s->created_at->format('U'),
'expires_at' => (int) $s->expires_at->format('U'),
'seen' => $s->seen()
];
})->toArray();
if(count($stories) == 0) {
return [];
}
$cursor = count($stories) - 1;
$stories = [[
'id' => (string) $stories[$cursor]['id'],
'photo' => $profile->avatarUrl(),
'name' => $profile->username,
'link' => $profile->url(),
'lastUpdated' => (int) now()->format('U'),
'seen' => null,
'items' => $stories,
'pid' => (string) $profile->id
]];
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function apiV1Viewed(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:stories',
]);
StoryView::firstOrCreate([
'story_id' => $request->input('id'),
'profile_id' => $request->user()->profile_id
]);
return ['code' => 200];
}
public function compose(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
return view('stories.compose');
}
public function apiV1Exists(Request $request, $id)
{
abort_if(!config('instance.stories.enabled'), 404);
$res = (bool) Story::whereProfileId($id)
->where('expires_at', '>', now())
->count();
return response()->json($res);
}
public function iRedirect(Request $request)
{
$user = $request->user();
abort_if(!$user, 404);
$username = $user->username;
return redirect("/stories/{$username}");
}
}

View file

@ -303,4 +303,9 @@ class Profile extends Model
->whereFollowingId($this->id)
->exists();
}
public function stories()
{
return $this->hasMany(Story::class);
}
}

View file

@ -36,7 +36,6 @@ class AuthServiceProvider extends ServiceProvider
'read',
'write',
'follow',
'push'
]);
Passport::tokensCan([

View file

@ -131,13 +131,9 @@ class Status extends Model
$media = $this->firstMedia();
$path = $media->media_path;
$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
if(config('pixelfed.cloud_storage') == true) {
$url = Storage::disk(config('filesystems.cloud'))->url($path)."?v={$hash}";
} else {
$url = Storage::url($path)."?v={$hash}";
}
$url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}");
return url($url);
return $url;
}
public function likes()

View file

@ -10,6 +10,8 @@ class Story extends Model
{
use HasSnowflakePrimary;
public const MAX_PER_DAY = 10;
/**
* Indicates if the IDs are auto-incrementing.
*
@ -24,6 +26,8 @@ class Story extends Model
*/
protected $dates = ['published_at', 'expires_at'];
protected $fillable = ['profile_id'];
protected $visible = ['id'];
public function profile()
@ -31,16 +35,6 @@ class Story extends Model
return $this->belongsTo(Profile::class);
}
public function items()
{
return $this->hasMany(StoryItem::class);
}
public function reactions()
{
return $this->hasMany(StoryReaction::class);
}
public function views()
{
return $this->hasMany(StoryView::class);
@ -48,7 +42,13 @@ class Story extends Model
public function seen($pid = false)
{
$id = $pid ?? Auth::user()->profile->id;
return $this->views()->whereProfileId($id)->exists();
return StoryView::whereStoryId($this->id)
->whereProfileId(Auth::user()->profile->id)
->exists();
}
public function permalink()
{
return url("/story/$this->id");
}
}

View file

@ -62,7 +62,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
public function includeMediaAttachments(Status $status)
{
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addDays(14), function() use($status) {
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addMinutes(14), function() use($status) {
if(in_array($status->type, ['photo', 'video', 'video:album', 'photo:album', 'loop', 'photo:video:album'])) {
$media = $status->media()->orderBy('order')->get();
return $this->collection($media, new MediaTransformer());

View file

@ -406,7 +406,6 @@ class Helpers {
$remoteUsername = "@{$username}@{$domain}";
abort_if(!self::validateUrl($res['inbox']), 400);
abort_if(!self::validateUrl($res['outbox']), 400);
abort_if(!self::validateUrl($res['id']), 400);
$profile = Profile::whereRemoteUrl($res['id'])->first();
@ -451,4 +450,20 @@ class Helpers {
$response = curl_exec($ch);
return;
}
public static function apSignedPostRequest($senderProfile, $url, $body)
{
abort_if(!self::validateUrl($url), 400);
$payload = json_encode($body);
$headers = HttpSignature::sign($senderProfile, $url, $body);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
return;
}
}

View file

@ -12,7 +12,6 @@ class RestrictedNames
'download',
'domainadmin',
'domainadministrator',
'email',
'errors',
'events',
'example',
@ -26,7 +25,7 @@ class RestrictedNames
'hostmaster',
'imap',
'info',
'info',
'information',
'is',
'isatap',
'it',
@ -142,6 +141,8 @@ class RestrictedNames
'drives',
'driver',
'e',
'email',
'emails',
'error',
'explore',
'export',
@ -206,6 +207,10 @@ class RestrictedNames
'news',
'news',
'newsfeed',
'newsroom',
'newsrooms',
'news-room',
'news-rooms',
'o',
'oauth',
'official',

View file

@ -6,7 +6,7 @@ trait User {
public function isTrustedAccount()
{
return $this->created_at->lt(now()->subDays(20));
return $this->created_at->lt(now()->subDays(60));
}
public function getMaxPostsPerHourAttribute()
@ -98,4 +98,19 @@ trait User {
{
return 5000;
}
public function getMaxStoriesPerHourAttribute()
{
return 20;
}
public function getMaxStoriesPerDayAttribute()
{
return 30;
}
public function getMaxStoryDeletePerDayAttribute()
{
return 35;
}
}

View file

@ -51,7 +51,7 @@ class Config {
'features' => [
'mobile_apis' => config('pixelfed.oauth_enabled'),
'circles' => false,
'stories' => false,
'stories' => config('instance.stories.enabled'),
'video' => Str::contains(config('pixelfed.media_types'), 'video/mp4'),
'import' => [
'instagram' => config('pixelfed.import.instagram.enabled'),

148
composer.lock generated
View file

@ -60,16 +60,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.125.0",
"version": "3.128.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "d9ffe7cf9cc93d3c49f4f6d2db6cf0c469686f9c"
"reference": "a81485e12b2545aff17134bbf29442037f3fcadb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d9ffe7cf9cc93d3c49f4f6d2db6cf0c469686f9c",
"reference": "d9ffe7cf9cc93d3c49f4f6d2db6cf0c469686f9c",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a81485e12b2545aff17134bbf29442037f3fcadb",
"reference": "a81485e12b2545aff17134bbf29442037f3fcadb",
"shasum": ""
},
"require": {
@ -94,7 +94,8 @@
"nette/neon": "^2.3",
"phpunit/phpunit": "^4.8.35|^5.4.3",
"psr/cache": "^1.0",
"psr/simple-cache": "^1.0"
"psr/simple-cache": "^1.0",
"sebastian/comparator": "^1.2.3"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
@ -139,7 +140,7 @@
"s3",
"sdk"
],
"time": "2019-12-02T23:15:42+00:00"
"time": "2019-12-10T19:12:09+00:00"
},
{
"name": "barryvdh/laravel-cors",
@ -448,25 +449,25 @@
},
{
"name": "dnoegel/php-xdg-base-dir",
"version": "0.1",
"version": "v0.1.1",
"source": {
"type": "git",
"url": "https://github.com/dnoegel/php-xdg-base-dir.git",
"reference": "265b8593498b997dc2d31e75b89f053b5cc9621a"
"reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/265b8593498b997dc2d31e75b89f053b5cc9621a",
"reference": "265b8593498b997dc2d31e75b89f053b5cc9621a",
"url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
"reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
},
"require-dev": {
"phpunit/phpunit": "@stable"
"phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35"
},
"type": "project",
"type": "library",
"autoload": {
"psr-4": {
"XdgBaseDir\\": "src/"
@ -477,7 +478,7 @@
"MIT"
],
"description": "implementation of xdg base directory specification for php",
"time": "2014-10-24T07:27:01+00:00"
"time": "2019-12-04T15:06:13+00:00"
},
{
"name": "doctrine/cache",
@ -1246,16 +1247,16 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "6.4.1",
"version": "6.5.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "0895c932405407fd3a7368b6910c09a24d26db11"
"reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/0895c932405407fd3a7368b6910c09a24d26db11",
"reference": "0895c932405407fd3a7368b6910c09a24d26db11",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5",
"reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5",
"shasum": ""
},
"require": {
@ -1270,12 +1271,13 @@
"psr/log": "^1.1"
},
"suggest": {
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.3-dev"
"dev-master": "6.5-dev"
}
},
"autoload": {
@ -1308,7 +1310,7 @@
"rest",
"web service"
],
"time": "2019-10-23T15:58:00+00:00"
"time": "2019-12-07T18:20:45+00:00"
},
{
"name": "guzzlehttp/promises",
@ -1592,16 +1594,16 @@
},
{
"name": "jaybizzle/crawler-detect",
"version": "v1.2.89",
"version": "v1.2.90",
"source": {
"type": "git",
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
"reference": "374d699ce4944107015eee0798eab072e3c47df9"
"reference": "35f963386e6a189697fe4b14dc91fb42b17fda4b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/374d699ce4944107015eee0798eab072e3c47df9",
"reference": "374d699ce4944107015eee0798eab072e3c47df9",
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/35f963386e6a189697fe4b14dc91fb42b17fda4b",
"reference": "35f963386e6a189697fe4b14dc91fb42b17fda4b",
"shasum": ""
},
"require": {
@ -1637,7 +1639,7 @@
"crawlerdetect",
"php crawler detect"
],
"time": "2019-11-16T13:47:52+00:00"
"time": "2019-12-08T20:03:27+00:00"
},
{
"name": "jenssegers/agent",
@ -1710,16 +1712,16 @@
},
{
"name": "laravel/framework",
"version": "v6.6.0",
"version": "v6.7.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "b48528ba5422ac909dbabf0b1cc34534928e7bce"
"reference": "ba4204f3a8b9672b6116398c165bd9c0c6eac077"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/b48528ba5422ac909dbabf0b1cc34534928e7bce",
"reference": "b48528ba5422ac909dbabf0b1cc34534928e7bce",
"url": "https://api.github.com/repos/laravel/framework/zipball/ba4204f3a8b9672b6116398c165bd9c0c6eac077",
"reference": "ba4204f3a8b9672b6116398c165bd9c0c6eac077",
"shasum": ""
},
"require": {
@ -1815,7 +1817,7 @@
"league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
"moontoast/math": "Required to use ordered UUIDs (^1.1).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0)",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).",
@ -1852,7 +1854,7 @@
"framework",
"laravel"
],
"time": "2019-11-26T15:33:08+00:00"
"time": "2019-12-10T16:01:57+00:00"
},
{
"name": "laravel/helpers",
@ -1909,16 +1911,16 @@
},
{
"name": "laravel/horizon",
"version": "v3.4.3",
"version": "v3.4.4",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
"reference": "37226dd66318014fac20351b4cc7ca209dd4ccb6"
"reference": "7c36d24b200b60a059ab20f5b53f5bb6f4d2da40"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/horizon/zipball/37226dd66318014fac20351b4cc7ca209dd4ccb6",
"reference": "37226dd66318014fac20351b4cc7ca209dd4ccb6",
"url": "https://api.github.com/repos/laravel/horizon/zipball/7c36d24b200b60a059ab20f5b53f5bb6f4d2da40",
"reference": "7c36d24b200b60a059ab20f5b53f5bb6f4d2da40",
"shasum": ""
},
"require": {
@ -1926,9 +1928,9 @@
"ext-json": "*",
"ext-pcntl": "*",
"ext-posix": "*",
"illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0",
"illuminate/queue": "~5.7.0|~5.8.0|^6.0|^7.0",
"illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
"illuminate/contracts": "~5.7.0|~5.8.0|^6.0",
"illuminate/queue": "~5.7.0|~5.8.0|^6.0",
"illuminate/support": "~5.7.0|~5.8.0|^6.0",
"php": ">=7.1.0",
"predis/predis": "^1.1",
"ramsey/uuid": "^3.5",
@ -1937,7 +1939,7 @@
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^3.7|^4.0|^5.0",
"orchestra/testbench": "^3.7|^4.0",
"phpunit/phpunit": "^7.0|^8.0"
},
"type": "library",
@ -1974,7 +1976,7 @@
"laravel",
"queue"
],
"time": "2019-11-19T16:23:21+00:00"
"time": "2019-12-10T16:50:59+00:00"
},
{
"name": "laravel/passport",
@ -2217,16 +2219,16 @@
},
{
"name": "league/flysystem",
"version": "1.0.57",
"version": "1.0.61",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a"
"reference": "4fb13c01784a6c9f165a351e996871488ca2d8c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a",
"reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4fb13c01784a6c9f165a351e996871488ca2d8c9",
"reference": "4fb13c01784a6c9f165a351e996871488ca2d8c9",
"shasum": ""
},
"require": {
@ -2297,7 +2299,7 @@
"sftp",
"storage"
],
"time": "2019-10-16T21:01:05+00:00"
"time": "2019-12-08T21:46:50+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
@ -4061,20 +4063,20 @@
},
{
"name": "psy/psysh",
"version": "v0.9.11",
"version": "v0.9.12",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "75d9ac1c16db676de27ab554a4152b594be4748e"
"reference": "90da7f37568aee36b116a030c5f99c915267edd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/75d9ac1c16db676de27ab554a4152b594be4748e",
"reference": "75d9ac1c16db676de27ab554a4152b594be4748e",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/90da7f37568aee36b116a030c5f99c915267edd4",
"reference": "90da7f37568aee36b116a030c5f99c915267edd4",
"shasum": ""
},
"require": {
"dnoegel/php-xdg-base-dir": "0.1",
"dnoegel/php-xdg-base-dir": "0.1.*",
"ext-json": "*",
"ext-tokenizer": "*",
"jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
@ -4131,7 +4133,7 @@
"interactive",
"shell"
],
"time": "2019-11-27T22:44:29+00:00"
"time": "2019-12-06T14:19:43+00:00"
},
{
"name": "ralouphie/getallheaders",
@ -6485,19 +6487,19 @@
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "55cd3f5e892eee6f5aca414d465cc224b062bea6"
"reference": "35638e4f5e714a12dec5ca062e68c625c1309c1c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/55cd3f5e892eee6f5aca414d465cc224b062bea6",
"reference": "55cd3f5e892eee6f5aca414d465cc224b062bea6",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/35638e4f5e714a12dec5ca062e68c625c1309c1c",
"reference": "35638e4f5e714a12dec5ca062e68c625c1309c1c",
"shasum": ""
},
"require": {
"illuminate/routing": "^5.5|^6",
"illuminate/session": "^5.5|^6",
"illuminate/support": "^5.5|^6",
"maximebf/debugbar": "~1.15.0",
"maximebf/debugbar": "^1.15",
"php": ">=7.0",
"symfony/debug": "^3|^4|^5",
"symfony/finder": "^3|^4|^5"
@ -6545,7 +6547,7 @@
"profiler",
"webprofiler"
],
"time": "2019-11-24T09:49:45+00:00"
"time": "2019-12-07T09:33:13+00:00"
},
{
"name": "composer/ca-bundle",
@ -7460,20 +7462,20 @@
},
{
"name": "maximebf/debugbar",
"version": "v1.15.1",
"version": "v1.16.0",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
"reference": "6c4277f6117e4864966c9cb58fb835cee8c74a1e"
"reference": "6ca3502de5e5889dc21311d2461f8cc3b6a094b1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/6c4277f6117e4864966c9cb58fb835cee8c74a1e",
"reference": "6c4277f6117e4864966c9cb58fb835cee8c74a1e",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/6ca3502de5e5889dc21311d2461f8cc3b6a094b1",
"reference": "6ca3502de5e5889dc21311d2461f8cc3b6a094b1",
"shasum": ""
},
"require": {
"php": ">=5.6",
"php": "^7.1",
"psr/log": "^1.0",
"symfony/var-dumper": "^2.6|^3|^4"
},
@ -7488,7 +7490,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.15-dev"
"dev-master": "1.16-dev"
}
},
"autoload": {
@ -7517,7 +7519,7 @@
"debug",
"debugbar"
],
"time": "2019-09-24T14:55:42+00:00"
"time": "2019-10-18T14:34:16+00:00"
},
{
"name": "mockery/mockery",
@ -8617,16 +8619,16 @@
},
{
"name": "phpunit/phpunit",
"version": "8.4.3",
"version": "8.5.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "67f9e35bffc0dd52d55d565ddbe4230454fd6a4e"
"reference": "3ee1c1fd6fc264480c25b6fb8285edefe1702dab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/67f9e35bffc0dd52d55d565ddbe4230454fd6a4e",
"reference": "67f9e35bffc0dd52d55d565ddbe4230454fd6a4e",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3ee1c1fd6fc264480c25b6fb8285edefe1702dab",
"reference": "3ee1c1fd6fc264480c25b6fb8285edefe1702dab",
"shasum": ""
},
"require": {
@ -8670,7 +8672,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.4-dev"
"dev-master": "8.5-dev"
}
},
"autoload": {
@ -8696,7 +8698,7 @@
"testing",
"xunit"
],
"time": "2019-11-06T09:42:23+00:00"
"time": "2019-12-06T05:41:38+00:00"
},
{
"name": "scrivo/highlight.php",
@ -9602,16 +9604,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.5.2",
"version": "3.5.3",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7"
"reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/65b12cdeaaa6cd276d4c3033a95b9b88b12701e7",
"reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb",
"reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb",
"shasum": ""
},
"require": {
@ -9649,7 +9651,7 @@
"phpcs",
"standards"
],
"time": "2019-10-28T04:36:32+00:00"
"time": "2019-12-04T04:46:47+00:00"
},
{
"name": "symfony/http-client",

View file

@ -47,4 +47,8 @@ return [
'custom' => env('USERNAME_REMOTE_CUSTOM_TEXT', null)
]
],
'stories' => [
'enabled' => env('STORIES_ENABLED', false),
]
];

20
config/passport.php Normal file
View file

@ -0,0 +1,20 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Encryption Keys
|--------------------------------------------------------------------------
|
| Passport uses encryption keys while generating secure access tokens for
| your application. By default, the keys are stored as local files but
| can be set via environment variables when that is more convenient.
|
*/
'private_key' => env('PASSPORT_PRIVATE_KEY'),
'public_key' => env('PASSPORT_PUBLIC_KEY'),
];

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your Pixelfed instance.
|
*/
'version' => '0.10.6',
'version' => '0.10.7',
/*
|--------------------------------------------------------------------------

View file

@ -0,0 +1,63 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateStoriesTable extends Migration
{
public function __construct()
{
DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
}
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('stories');
Schema::dropIfExists('story_items');
Schema::dropIfExists('story_reactions');
Schema::dropIfExists('story_views');
Schema::create('stories', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('profile_id')->unsigned()->index();
$table->string('type')->nullable();
$table->unsignedInteger('size')->nullable();
$table->string('mime')->nullable();
$table->smallInteger('duration')->unsigned();
$table->string('path')->nullable();
$table->string('cdn_url')->nullable();
$table->boolean('public')->default(false)->index();
$table->boolean('local')->default(false)->index();
$table->unsignedInteger('view_count')->nullable();
$table->unsignedInteger('comment_count')->nullable();
$table->json('story')->nullable();
$table->unique(['profile_id', 'path']);
$table->timestamp('expires_at')->index();
$table->timestamps();
});
Schema::create('story_views', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('story_id')->unsigned()->index();
$table->bigInteger('profile_id')->unsigned()->index();
$table->unique(['profile_id', 'story_id']);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('stories');
Schema::dropIfExists('story_views');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/story-compose.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{
"/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250",
"/js/vendor.js": "/js/vendor.js?id=d2e140b2d43a9b4d085d",
"/js/vendor.js": "/js/vendor.js?id=4d431dcde216fbb6e91a",
"/js/ace.js": "/js/ace.js?id=a575b37c2085b5003666",
"/js/activity.js": "/js/activity.js?id=028cbbe598f925bb3414",
"/js/app.js": "/js/app.js?id=360dc653e947aa970981",
@ -10,19 +10,20 @@
"/css/quill.css": "/css/quill.css?id=e3741782d15a3031f785",
"/js/collectioncompose.js": "/js/collectioncompose.js?id=3fd79944492361ec7347",
"/js/collections.js": "/js/collections.js?id=38be4150f3d2ebb15f50",
"/js/components.js": "/js/components.js?id=d8581521aef135284631",
"/js/compose.js": "/js/compose.js?id=f06b87dba21d21c96906",
"/js/components.js": "/js/components.js?id=7e6627a20df0db879370",
"/js/compose.js": "/js/compose.js?id=35514f7497c88f275de6",
"/js/compose-classic.js": "/js/compose-classic.js?id=283f19c895f4118a2a8b",
"/js/developers.js": "/js/developers.js?id=f75deca5ccf47d43eb07",
"/js/discover.js": "/js/discover.js?id=ea7279e1612a1989941d",
"/js/hashtag.js": "/js/hashtag.js?id=e6b41cab117cb03c7d2a",
"/js/loops.js": "/js/loops.js?id=ac610897b12207c829b9",
"/js/mode-dot.js": "/js/mode-dot.js?id=1225a9aac7a93d5d232f",
"/js/profile.js": "/js/profile.js?id=0b84a74043019413e09e",
"/js/profile.js": "/js/profile.js?id=c2221e6dd749d3aab260",
"/js/profile-directory.js": "/js/profile-directory.js?id=7160b00d9beda164f1bc",
"/js/quill.js": "/js/quill.js?id=9b15ab0ae830e7293390",
"/js/search.js": "/js/search.js?id=22e8bccee621e57963d9",
"/js/status.js": "/js/status.js?id=e79505d19162a11cb404",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=68116b72a65e5437af52",
"/js/timeline.js": "/js/timeline.js?id=fbfe5ae8d4edf779c820"
"/js/story-compose.js": "/js/story-compose.js?id=7b00ed457af2459b916e",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=39b089458f249e8717ad",
"/js/timeline.js": "/js/timeline.js?id=db3cecdcc13e2d990143"
}

View file

@ -84,52 +84,6 @@
<div class="card-body p-0 border-top">
<div v-if="page == 1" class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: 400px;">
<div class="text-center">
<a class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/compose">
<div class="card-body">
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;background-color: #008DF5">
<i class="far fa-image text-white fa-lg"></i>
</div>
<div class="media-body text-left">
<h5 class="mt-0 font-weight-bold text-primary">New Post</h5>
<p class="mb-0 text-muted">Share up to {{config.uploader.album_limit}} photos or videos.</p>
</div>
</div>
</div>
</a>
<a class="d-none card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" :click="showAddToStoryCard">
<div class="card-body">
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;background-color: #008DF5">
<i class="fas fa-history text-white fa-lg"></i>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">Add to Story</span>
</p>
<p class="mb-0 text-muted">Add a photo or video to your story.</p>
</div>
</div>
</div>
</a>
<a class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/collections/create">
<div class="card-body">
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;background-color: #008DF5">
<i class="fas fa-images text-white fa-lg"></i>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Collection</span>
</p>
<p class="mb-0 text-muted">Create a curated collection of photos.</p>
</div>
</div>
</div>
</a>
<div v-if="media.length == 0" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark">
<div @click.prevent="addMedia" class="card-body">
<div class="media">
@ -138,17 +92,53 @@
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">Try ComposeUI v4</span>
<sup>
<span class="badge badge-primary pb-1">BETA</span>
</sup>
<span class="h5 mt-0 font-weight-bold text-primary">New Post</span>
</p>
<p class="mb-0 text-muted">The next generation compose experience.</p>
<p class="mb-0 text-muted">Share up to {{config.uploader.album_limit}} photos or videos</p>
</div>
</div>
</div>
</div>
<p class="pt-3">
<a v-if="config.features.stories == true" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/stories/new">
<div class="card-body">
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;border: 2px solid #008DF5">
<i class="fas fa-history text-primary fa-lg"></i>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Story</span>
<sup class="float-right mt-2">
<span class="btn btn-outline-lighter p-1 btn-sm font-weight-bold py-0" style="font-size:10px;line-height: 0.6">BETA</span>
</sup>
</p>
<p class="mb-0 text-muted">Add Photo to Story</p>
</div>
</div>
</div>
</a>
<a class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/collections/create">
<div class="card-body">
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;border: 2px solid #008DF5">
<i class="fas fa-images text-primary fa-lg"></i>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Collection</span>
<sup class="float-right mt-2">
<span class="btn btn-outline-lighter p-1 btn-sm font-weight-bold py-0" style="font-size:10px;line-height: 0.6">BETA</span>
</sup>
</p>
<p class="mb-0 text-muted">New collection of posts</p>
</div>
</div>
</div>
</a>
<p class="py-3">
<a class="font-weight-bold" href="/site/help">Help</a>
</p>
</div>

View file

@ -35,7 +35,12 @@
<div class="d-block d-md-none mt-n3 mb-3">
<div class="row">
<div class="col-4">
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border mr-2" :src="profile.avatar" width="77px" height="77px">
<div v-if="hasStory" class="has-story cursor-pointer shadow-sm" @click="storyRedirect()">
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle" :src="profile.avatar" width="77px" height="77px">
</div>
<div v-else>
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle border" :src="profile.avatar" width="77px" height="77px">
</div>
</div>
<div class="col-8">
<div class="d-block d-md-none mt-3 py-2">
@ -72,7 +77,12 @@
<!-- DESKTOP PROFILE PICTURE -->
<div class="d-none d-md-block pb-5">
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px">
<div v-if="hasStory" class="has-story-lg cursor-pointer shadow-sm" @click="storyRedirect()">
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow cursor-pointer" :src="profile.avatar" width="150px" height="150px">
</div>
<div v-else>
<img :alt="profileUsername + '\'s profile picture'" class="rounded-circle box-shadow" :src="profile.avatar" width="150px" height="150px">
</div>
<p v-if="sponsorList.patreon || sponsorList.liberapay || sponsorList.opencollective" class="text-center mt-3">
<button type="button" @click="showSponsorModal" class="btn btn-outline-secondary font-weight-bold py-0">
<i class="fas fa-heart text-danger"></i>
@ -523,6 +533,34 @@
.nav-topbar .nav-link .small {
font-weight: 600;
}
.has-story {
width: 84px;
height: 84px;
border-radius: 50%;
padding: 4px;
background: radial-gradient(ellipse at 70% 70%, #ee583f 8%, #d92d77 42%, #bd3381 58%);
}
.has-story img {
width: 76px;
height: 76px;
border-radius: 50%;
padding: 6px;
background: #fff;
}
.has-story-lg {
width: 159px;
height: 159px;
border-radius: 50%;
padding: 4px;
background: radial-gradient(ellipse at 70% 70%, #ee583f 8%, #d92d77 42%, #bd3381 58%);
}
.has-story-lg img {
width: 150px;
height: 150px;
border-radius: 50%;
padding: 6px;
background:#fff;
}
</style>
<script type="text/javascript">
import VueMasonry from 'vue-masonry-css'
@ -565,7 +603,8 @@
collectionsPage: 2,
isMobile: false,
ctxEmbedPayload: null,
copiedEmbed: false
copiedEmbed: false,
hasStory: null
}
},
beforeMount() {
@ -620,6 +659,10 @@
this.profile = res.data;
}).then(res => {
this.fetchPosts();
axios.get('/api/stories/v1/exists/' + this.profileId)
.then(res => {
this.hasStory = res.data == true;
})
});
},
@ -1133,6 +1176,10 @@
this.$refs.embedModal.hide();
this.$refs.visitorContextMenu.hide();
},
storyRedirect() {
window.location.href = '/stories/' + this.profileUsername;
}
}
}
</script>

View file

@ -1,42 +1,268 @@
<template>
<div>
<div class="container">
<p class="display-4 text-center py-5">Share Your Story</p>
<div class="container mt-2 mt-md-5">
<input type="file" id="pf-dz" name="media" class="w-100 h-100 d-none file-input" draggable="true" v-bind:accept="config.mimes">
<div class="row">
<div class="col-12 col-md-6 offset-md-3">
<div class="d-flex justify-content-center align-item-center">
<div class="bg-dark" style="width:400px;height:600px">
<p class="text-center text-light font-weight-bold">Add Photo</p>
<!-- LANDING -->
<div v-if="page == 'landing'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 90vh;">
<div class="text-center flex-fill mt-5 pt-5">
<img src="/img/pixelfed-icon-grey.svg" width="60px" height="60px">
<p class="font-weight-bold lead text-lighter mt-1">Stories</p>
</div>
<div class="flex-fill">
<div class="card w-100 shadow-none">
<div class="list-group">
<a class="list-group-item text-center lead text-decoration-none text-dark" href="#" @click.prevent="upload()">Add Photo</a>
<a v-if="stories.length" class="list-group-item text-center lead text-decoration-none text-dark" href="#" @click.prevent="edit()">Edit Story</a>
</div>
</div>
</div>
<div class="text-center flex-fill">
<p class="text-lighter small text-uppercase">
<a href="/" class="text-muted font-weight-bold">Home</a>
<span class="px-2 text-lighter">|</span>
<a href="/i/my/story" class="text-muted font-weight-bold">View My Story</a>
<span class="px-2 text-lighter">|</span>
<a href="/site/help" class="text-muted font-weight-bold">Help</a>
</p>
</div>
</div>
<!-- CROP -->
<div v-if="page == 'crop'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 95vh;">
<div class="text-center pt-5 mb-3 d-flex justify-content-between align-items-center">
<div>
<button class="btn btn-outline-lighter btn-sm py-0 px-md-3"><i class="pr-2 fas fa-chevron-left fa-sm"></i> Delete</button>
</div>
<div class="d-flex align-items-center">
<img class="d-inline-block mr-2" src="/img/pixelfed-icon-grey.svg" width="30px" height="30px">
<span class="font-weight-bold lead text-lighter">Stories</span>
</div>
<div>
<button class="btn btn-outline-success btn-sm py-0 px-md-3">Crop <i class="pl-2 fas fa-chevron-right fa-sm"></i></button>
</div>
</div>
<div class="flex-fill">
<div class="card w-100 mt-3">
<div class="card-body p-0">
<vue-cropper
ref="cropper"
:relativeZoom="cropper.zoom"
:aspectRatio="cropper.aspectRatio"
:viewMode="cropper.viewMode"
:zoomable="cropper.zoomable"
:rotatable="true"
:src="mediaUrl"
>
</vue-cropper>
</div>
</div>
</div>
<div class="text-center flex-fill">
<p class="text-lighter small text-uppercase pt-2">
<!-- <a href="#" class="text-muted font-weight-bold">Home</a>
<span class="px-2 text-lighter">|</span>
<a href="#" class="text-muted font-weight-bold">View My Story</a>
<span class="px-2 text-lighter">|</span> -->
<a href="/site/help" class="text-muted font-weight-bold mb-0">Help</a>
</p>
</div>
</div>
<!-- ERROR -->
<div v-if="page == 'error'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center align-items-center" style="height: 90vh;">
<p class="h3 mb-0">Oops!</p>
<p class="text-muted lead">An error occurred, please try again later.</p>
<p class="text-muted mb-0">
<a class="btn btn-outline-secondary py-0 px-5 font-weight-bold" href="/">Go back</a>
</p>
</div>
<div v-if="page == 'edit'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 90vh;">
<div class="text-center flex-fill mt-5 pt-5">
<img src="/img/pixelfed-icon-grey.svg" width="60px" height="60px">
<p class="font-weight-bold lead text-lighter mt-1">Stories</p>
</div>
<div class="flex-fill py-5">
<div class="card w-100 shadow-none" style="max-height: 500px; overflow-y: auto">
<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="media-body">
<p class="mb-0">Expires</p>
<p class="mb-0 text-muted small"><span>{{expiresTimestamp(story.expires_at)}}</span></p>
</div>
<div class="float-right">
<button @click="deleteStory(story, index)" class="btn btn-danger btn-sm font-weight-bold text-uppercase">Delete</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex-fill text-center">
<a class="btn btn-outline-secondary py-0 px-5 font-weight-bold" href="/i/stories/new">Go back</a>
</div>
</div>
</div>
</div>
<b-modal
id="lightbox"
ref="lightboxModal"
hide-header
hide-footer
centered
size="lg"
body-class="p-0"
>
<div v-if="lightboxMedia" class="w-100 h-100">
<img :src="lightboxMedia.url" style="max-height: 100%; max-width: 100%">
</div>
</b-modal>
</div>
</template>
<style type="text/css" scoped>
.navtab .nav-link {
color: #657786;
}
.navtab .nav-link.active {
color: #08d;
border-bottom: 4px solid #08d;
}
</style>
<script type="text/javascript">
import VueTimeago from 'vue-timeago';
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
export default {
components: {
VueCropper,
VueTimeago
},
props: ['profile-id'],
data() {
return {
currentTab: 'upload',
config: window.App.config,
mimes: [
'image/jpeg',
'image/png'
],
page: 'landing',
pages: [
'landing',
'crop',
'edit',
'confirm',
'error'
],
uploading: false,
uploadProgress: 100,
cropper: {
aspectRatio: 9/16,
viewMode: 1,
zoomable: true,
zoom: null
},
mediaUrl: null,
stories: [],
lightboxMedia: false,
};
},
mounted() {
this.welcomeMessage();
this.mediaWatcher();
axios.get('/api/stories/v1/fetch/' + this.profileId)
.then(res => this.stories = res.data);
},
methods: {
welcomeMessage() {
upload() {
let fi = $('.file-input[name="media"]');
fi.trigger('click');
},
mediaWatcher() {
let self = this;
$(document).on('change', '#pf-dz', function(e) {
self.triggerUpload();
});
},
triggerUpload() {
let self = this;
self.uploading = true;
let io = document.querySelector('#pf-dz');
Array.prototype.forEach.call(io.files, function(io, i) {
if(self.media && self.media.length + i >= self.config.uploader.album_limit) {
swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error');
self.uploading = false;
self.page = 2;
return;
}
let type = io.type;
let validated = $.inArray(type, self.mimes);
if(validated == -1) {
swal('Invalid File Type', 'The file you are trying to add is not a valid mime type. Please upload a '+self.mimes+' only.', 'error');
self.uploading = false;
self.page = 'error';
return;
}
let form = new FormData();
form.append('file', io);
let xhrConfig = {
onUploadProgress: function(e) {
let progress = Math.round( (e.loaded * 100) / e.total );
self.uploadProgress = progress;
}
};
axios.post('/api/stories/v1/add', form, xhrConfig)
.then(function(e) {
self.uploadProgress = 100;
self.uploading = false;
window.location.href = '/i/my/story';
self.mediaUrl = e.data.media_url;
}).catch(function(e) {
self.uploading = false;
io.value = null;
swal('Oops!', e.response.data.message, 'warning');
});
io.value = null;
self.uploadProgress = 0;
});
},
expiresTimestamp(ts) {
ts = new Date(ts * 1000);
return ts.toDateString() + ' ' + ts.toLocaleTimeString();
},
edit() {
this.page = 'edit';
},
showLightbox(story) {
this.lightboxMedia = {
url: story.src
}
this.$refs.lightboxModal.show();
},
deleteStory(story, index) {
if(window.confirm('Are you sure you want to delete this Story?') != true) {
return;
}
axios.delete('/api/stories/v1/delete/' + story.id)
.then(res => {
this.stories.splice(index, 1);
if(this.stories.length == 0) {
window.location.href = '/i/stories/new';
}
});
}
}
}

View file

@ -0,0 +1,102 @@
<template>
<div class="container">
<div v-if="loading" class="row">
<div class="col-12 mt-5 pt-5">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>
<div v-if="stories.length != 0">
<div id="storyContainer" class="d-none m-3"></div>
</div>
</div>
</template>
<script type="text/javascript">
import 'zuck.js/dist/zuck.css';
import 'zuck.js/dist/skins/snapgram.css';
window.Zuck = require('zuck.js');
export default {
props: ['pid'],
data() {
return {
loading: true,
stories: {},
}
},
beforeMount() {
this.fetchStories();
},
methods: {
fetchStories() {
axios.get('/api/stories/v1/profile/' + this.pid)
.then(res => {
let data = res.data;
if(data.length == 0) {
window.location.href = '/';
return;
}
window._storyData = data;
window.stories = new Zuck('storyContainer', {
stories: data,
localStorage: false,
callbacks: {
onOpen (storyId, callback) {
document.body.style.overflow = "hidden";
callback()
},
onEnd (storyId, callback) {
axios.post('/i/stories/viewed', {
id: storyId
});
callback();
},
onClose (storyId, callback) {
document.body.style.overflow = "auto";
callback();
window.location.href = '/';
},
}
});
this.loading = false;
// todo: refactor this mess
document.querySelectorAll('#storyContainer .story')[0].click()
})
.catch(err => {
window.location.href = '/';
return;
});
}
}
}
</script>
<style type="text/css">
#storyContainer .story {
margin-right: 2rem;
width: 100%;
max-width: 64px;
}
.stories.carousel .story > .item-link > .item-preview {
height: 64px;
}
#zuck-modal.with-effects {
width: 100%;
}
.stories.carousel .story > .item-link > .info .name {
font-weight: 600;
font-size: 12px;
}
.stories.carousel .story > .item-link > .info {
}
</style>

View file

@ -2,7 +2,7 @@
<div class="container" style="">
<div v-if="layout === 'feed'" class="row">
<div :class="[modes.distractionFree ? 'col-md-8 col-lg-8 offset-md-2 px-0 my-sm-3 timeline order-2 order-md-1':'col-md-8 col-lg-8 px-0 my-sm-3 timeline order-2 order-md-1']">
<div class="d-none" data-id="StoryTimelineComponent"></div>
<story-component v-if="config.features.stories"></story-component>
<div style="padding-top:10px;">
<div v-if="loading" class="text-center">
<div class="spinner-border" role="status">
@ -255,9 +255,9 @@
<announcements-card v-on:show-tips="showTips = $event"></announcements-card>
</div>
<div v-show="modes.notify == true && !loading" class="mb-4">
<!-- <div v-show="modes.notify == true && !loading" class="mb-4">
<notification-card></notification-card>
</div>
</div> -->
<div v-show="showSuggestions == true && suggestions.length && config.ab && config.ab.rec == true" class="mb-4">
<div class="card">

View file

@ -28,6 +28,11 @@ Vue.component(
require('./components/PostMenu.vue').default
);
Vue.component(
'story-viewer',
require('./components/StoryViewer.vue').default
);
Vue.component(
'profile',
require('./components/Profile.vue').default

4
resources/assets/js/story-compose.js vendored Normal file
View file

@ -0,0 +1,4 @@
Vue.component(
'story-compose',
require('./components/StoryCompose.vue').default
);

View file

@ -41,4 +41,9 @@ Vue.component(
Vue.component(
'announcements-card',
require('./components/AnnouncementsCard.vue').default
);
Vue.component(
'story-component',
require('./components/StoryTimelineComponent.vue').default
);

View file

@ -0,0 +1,11 @@
@extends('layouts.app')
@section('content')
<story-viewer pid="{{$pid}}"></story-viewer>
@endsection
@push('scripts')
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
<script type="text/javascript" src="{{mix('js/profile.js')}}"></script>
<script type="text/javascript">App.boot();</script>
@endpush

View file

@ -179,8 +179,8 @@
</div>
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" id="show_tips">
<label class="form-check-label font-weight-bold">Show Tips</label>
<p class="text-muted small help-text">Show Tips on Timelines (Desktop Only)</p>
<label class="form-check-label font-weight-bold">Show Announcements</label>
<p class="text-muted small help-text">Show Announcements on Timelines (Desktop Only)</p>
</div>
<div class="form-check pb-3">
<input class="form-check-input" type="checkbox" id="force_metro">

View file

@ -0,0 +1,11 @@
@extends('layouts.blank')
@section('content')
<story-compose profile-id="{{auth()->user()->profile_id}}"></story-compose>
@endsection
@push('scripts')
<script type="text/javascript" src="{{ mix('js/story-compose.js') }}"></script>
<script type="text/javascript">window.App.boot()</script>
@endpush

View file

@ -178,6 +178,14 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::group(['prefix' => 'admin'], function () {
Route::post('moderate', 'Api\AdminApiController@moderate');
});
Route::group(['prefix' => 'stories'], function () {
Route::get('v1/recent', 'StoryController@apiV1Recent');
Route::post('v1/add', 'StoryController@apiV1Add')->middleware('throttle:maxStoriesPerDay,1440');
Route::get('v1/fetch/{id}', 'StoryController@apiV1Fetch');
Route::get('v1/profile/{id}', 'StoryController@apiV1Profile');
Route::get('v1/exists/{id}', 'StoryController@apiV1Exists');
Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete')->middleware('throttle:maxStoryDeletePerDay,1440');
});
});
@ -238,6 +246,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('me', 'ProfileController@meRedirect');
Route::get('intent/follow', 'SiteController@followIntent');
Route::post('stories/viewed', 'StoryController@apiV1Viewed');
Route::get('stories/new', 'StoryController@compose');
Route::get('my/story', 'StoryController@iRedirect');
});
Route::group(['prefix' => 'account'], function () {
@ -389,6 +400,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('{username}', 'ProfileController@permalinkRedirect');
});
Route::get('stories/{username}', 'ProfileController@stories');
Route::get('c/{collection}', 'CollectionController@show');
Route::get('p/{username}/{id}/c', 'CommentController@showAll');
Route::get('p/{username}/{id}/embed', 'StatusController@showEmbed');

1
webpack.mix.js vendored
View file

@ -33,6 +33,7 @@ mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/collectioncompose.js', 'public/js')
.js('resources/assets/js/collections.js', 'public/js')
.js('resources/assets/js/profile-directory.js', 'public/js')
.js('resources/assets/js/story-compose.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
// .js('resources/assets/js/admin.js', 'public/js')