Merge pull request #1906 from pixelfed/staging

Add S3 + Stories
pull/1941/head v0.10.7
daniel 6 years ago committed by GitHub
commit 700c7805ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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();
});
}
}
}

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

@ -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();
}

@ -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;

@ -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'));
}
}

@ -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)
{
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)
{
return view('stories.home');
$user = $request->user();
abort_if(!$user, 404);
$username = $user->username;
return redirect("/stories/{$username}");
}
}

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

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

@ -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()

@ -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");
}
}

@ -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());

@ -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;
}
}

@ -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',

@ -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;
}
}

@ -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

@ -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",

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

@ -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'),
];

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

@ -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

File diff suppressed because one or more lines are too long

@ -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

File diff suppressed because one or more lines are too long

@ -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"
}

@ -84,31 +84,35 @@
<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 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">
<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>
<i class="fas fa-bolt 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>
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Post</span>
</p>
<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>
<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%;background-color: #008DF5">
<i class="fas fa-history text-white fa-lg"></i>
<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">Add to Story</span>
<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 a photo or video to your story.</p>
<p class="mb-0 text-muted">Add Photo to Story</p>
</div>
</div>
</div>
@ -117,38 +121,24 @@
<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 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">Create a curated collection of photos.</p>
<p class="mb-0 text-muted">New collection of posts</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">
<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-bolt 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">Try ComposeUI v4</span>
<sup>
<span class="badge badge-primary pb-1">BETA</span>
</sup>
</p>
<p class="mb-0 text-muted">The next generation compose experience.</p>
</div>
</div>
</div>
</div>
<p class="pt-3">
<p class="py-3">
<a class="font-weight-bold" href="/site/help">Help</a>
</p>
</div>

@ -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>

@ -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';
}
});
}
}
}

@ -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>

@ -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">

@ -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

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

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

@ -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

@ -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">

@ -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

@ -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

@ -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')

Loading…
Cancel
Save