diff --git a/CHANGELOG.md b/CHANGELOG.md index 586f1eed6..09aab7fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Release Notes -## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev) +## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev) + +### Added +- Pinned Posts ([2f655d000](https://github.com/pixelfed/pixelfed/commit/2f655d000)) + +### Updates +- Update PublicApiController, use pixelfed entities for /api/pixelfed/v1/accounts/id/statuses with bookmarked state ([5ddb6d842](https://github.com/pixelfed/pixelfed/commit/5ddb6d842)) - ([](https://github.com/pixelfed/pixelfed/commit/)) -## [v0.12.5 (2024-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev) +## [v0.12.5 (2025-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev) ### Added - Add app register email verify resends ([dbd1e17](https://github.com/pixelfed/pixelfed/commit/dbd1e17)) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 7000ace07..9ef510296 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -2,592 +2,592 @@ namespace App\Http\Controllers; +use App\EmailVerification; +use App\Follower; +use App\FollowRequest; +use App\Jobs\FollowPipeline\FollowAcceptPipeline; +use App\Jobs\FollowPipeline\FollowPipeline; +use App\Jobs\FollowPipeline\FollowRejectPipeline; +use App\Mail\ConfirmEmail; +use App\Notification; +use App\Profile; +use App\Services\AccountService; +use App\Services\FollowerService; +use App\Services\NotificationService; +use App\Services\RelationshipService; +use App\Services\UserFilterService; +use App\Transformer\Api\Mastodon\v1\AccountTransformer; +use App\User; +use App\UserFilter; use Auth; use Cache; -use Mail; -use Illuminate\Support\Facades\Redis; -use Illuminate\Support\Str; use Carbon\Carbon; -use App\Mail\ConfirmEmail; use Illuminate\Http\Request; -use PragmaRX\Google2FA\Google2FA; -use App\Jobs\FollowPipeline\FollowPipeline; -use App\{ - DirectMessage, - EmailVerification, - Follower, - FollowRequest, - Media, - Notification, - Profile, - User, - UserDevice, - UserFilter, - UserSetting -}; +use Illuminate\Support\Str; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; -use App\Transformer\Api\Mastodon\v1\AccountTransformer; -use App\Services\AccountService; -use App\Services\FollowerService; -use App\Services\NotificationService; -use App\Services\UserFilterService; -use App\Services\RelationshipService; -use App\Jobs\FollowPipeline\FollowAcceptPipeline; -use App\Jobs\FollowPipeline\FollowRejectPipeline; +use Mail; +use PragmaRX\Google2FA\Google2FA; class AccountController extends Controller { - protected $filters = [ - 'user.mute', - 'user.block', - ]; - - const FILTER_LIMIT_MUTE_TEXT = 'You cannot mute more than '; - const FILTER_LIMIT_BLOCK_TEXT = 'You cannot block more than '; - - public function __construct() - { - $this->middleware('auth'); - } - - public function notifications(Request $request) - { - return view('account.activity'); - } - - public function followingActivity(Request $request) - { - $this->validate($request, [ - 'page' => 'nullable|min:1|max:3', - 'a' => 'nullable|alpha_dash', - ]); - - $action = $request->input('a'); - $allowed = ['like', 'follow']; - $timeago = Carbon::now()->subMonths(3); - - $profile = Auth::user()->profile; - $following = $profile->following->pluck('id'); - - $notifications = Notification::whereIn('actor_id', $following) - ->whereIn('action', $allowed) - ->where('actor_id', '<>', $profile->id) - ->where('profile_id', '<>', $profile->id) - ->whereDate('created_at', '>', $timeago) - ->orderBy('notifications.created_at', 'desc') - ->simplePaginate(30); - - return view('account.following', compact('profile', 'notifications')); - } - - public function verifyEmail(Request $request) - { - $recentSent = EmailVerification::whereUserId(Auth::id()) - ->whereDate('created_at', '>', now()->subHours(12))->count(); - - return view('account.verify_email', compact('recentSent')); - } - - public function sendVerifyEmail(Request $request) - { - $recentAttempt = EmailVerification::whereUserId(Auth::id()) - ->whereDate('created_at', '>', now()->subHours(12))->count(); - - if ($recentAttempt > 0) { - return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.'); - } - - EmailVerification::whereUserId(Auth::id())->delete(); - - $user = User::whereNull('email_verified_at')->find(Auth::id()); - $utoken = Str::uuid() . Str::random(mt_rand(5,9)); - $rtoken = Str::random(mt_rand(64, 70)); - - $verify = new EmailVerification(); - $verify->user_id = $user->id; - $verify->email = $user->email; - $verify->user_token = $utoken; - $verify->random_token = $rtoken; - $verify->save(); - - Mail::to($user->email)->send(new ConfirmEmail($verify)); - - return redirect()->back()->with('status', 'Verification email sent!'); - } - - public function confirmVerifyEmail(Request $request, $userToken, $randomToken) - { - $verify = EmailVerification::where('user_token', $userToken) - ->where('created_at', '>', now()->subHours(24)) - ->where('random_token', $randomToken) - ->firstOrFail(); - - if (Auth::id() === $verify->user_id && $verify->user_token === $userToken && $verify->random_token === $randomToken) { - $user = User::find(Auth::id()); - $user->email_verified_at = Carbon::now(); - $user->save(); - - return redirect('/'); - } else { - abort(403); - } - } - - public function direct() - { - return view('account.direct'); - } - - public function directMessage(Request $request, $id) - { - $profile = Profile::where('id', '!=', $request->user()->profile_id) - // ->whereNull('domain') - ->findOrFail($id); - return view('account.directmessage', compact('id')); - } - - public function mute(Request $request) - { - $this->validate($request, [ - 'type' => 'required|string|in:user', - 'item' => 'required|integer|min:1', - ]); - - $pid = $request->user()->profile_id; - $count = UserFilterService::muteCount($pid); - $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes'); - abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); - if($count == 0) { - $filterCount = UserFilter::whereUserId($pid)->count(); - abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); - } - $type = $request->input('type'); - $item = $request->input('item'); - $action = $type . '.mute'; - - if (!in_array($action, $this->filters)) { - return abort(406); - } - $filterable = []; - switch ($type) { - case 'user': - $profile = Profile::findOrFail($item); - if ($profile->id == $pid) { - return abort(403); - } - $class = get_class($profile); - $filterable['id'] = $profile->id; - $filterable['type'] = $class; - break; - } - - $filter = UserFilter::firstOrCreate([ - 'user_id' => $pid, - 'filterable_id' => $filterable['id'], - 'filterable_type' => $filterable['type'], - 'filter_type' => 'mute', - ]); - - UserFilterService::mute($pid, $filterable['id']); - $res = RelationshipService::refresh($pid, $profile->id); - - if($request->wantsJson()) { - return response()->json($res); - } else { - return redirect()->back(); - } - } - - public function unmute(Request $request) - { - $this->validate($request, [ - 'type' => 'required|string|in:user', - 'item' => 'required|integer|min:1', - ]); - - $pid = $request->user()->profile_id; - $type = $request->input('type'); - $item = $request->input('item'); - $action = $type . '.mute'; - - if (!in_array($action, $this->filters)) { - return abort(406); - } - $filterable = []; - switch ($type) { - case 'user': - $profile = Profile::findOrFail($item); - if ($profile->id == $pid) { - return abort(403); - } - $class = get_class($profile); - $filterable['id'] = $profile->id; - $filterable['type'] = $class; - break; - - default: - abort(400); - break; - } - - $filter = UserFilter::whereUserId($pid) - ->whereFilterableId($filterable['id']) - ->whereFilterableType($filterable['type']) - ->whereFilterType('mute') - ->first(); - - if($filter) { - UserFilterService::unmute($pid, $filterable['id']); - $filter->delete(); - } - - $res = RelationshipService::refresh($pid, $profile->id); - - if($request->wantsJson()) { - return response()->json($res); - } else { - return redirect()->back(); - } - } - - public function block(Request $request) - { - $this->validate($request, [ - 'type' => 'required|string|in:user', - 'item' => 'required|integer|min:1', - ]); - $pid = $request->user()->profile_id; - $count = UserFilterService::blockCount($pid); - $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks'); - abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); - if($count == 0) { - $filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count(); - abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); - } - $type = $request->input('type'); - $item = $request->input('item'); - $action = $type.'.block'; - if (!in_array($action, $this->filters)) { - return abort(406); - } - $filterable = []; - switch ($type) { - case 'user': - $profile = Profile::findOrFail($item); - if ($profile->id == $pid || ($profile->user && $profile->user->is_admin == true)) { - return abort(403); - } - $class = get_class($profile); - $filterable['id'] = $profile->id; - $filterable['type'] = $class; - - $followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first(); - if($followed) { - $followed->delete(); - $profile->following_count = Follower::whereProfileId($profile->id)->count(); - $profile->save(); - $selfProfile = $request->user()->profile; - $selfProfile->followers_count = Follower::whereFollowingId($pid)->count(); - $selfProfile->save(); - FollowerService::remove($profile->id, $pid); - AccountService::del($pid); - AccountService::del($profile->id); - } - - $following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first(); - if($following) { - $following->delete(); - $profile->followers_count = Follower::whereFollowingId($profile->id)->count(); - $profile->save(); - $selfProfile = $request->user()->profile; - $selfProfile->following_count = Follower::whereProfileId($pid)->count(); - $selfProfile->save(); - FollowerService::remove($pid, $profile->pid); - AccountService::del($pid); - AccountService::del($profile->id); - } - - Notification::whereProfileId($pid) - ->whereActorId($profile->id) - ->get() - ->map(function($n) use($pid) { - NotificationService::del($pid, $n['id']); - $n->forceDelete(); - }); - break; - } - - $filter = UserFilter::firstOrCreate([ - 'user_id' => $pid, - 'filterable_id' => $filterable['id'], - 'filterable_type' => $filterable['type'], - 'filter_type' => 'block', - ]); - - UserFilterService::block($pid, $filterable['id']); - $res = RelationshipService::refresh($pid, $profile->id); - - if($request->wantsJson()) { - return response()->json($res); - } else { - return redirect()->back(); - } - } - - public function unblock(Request $request) - { - $this->validate($request, [ - 'type' => 'required|string|in:user', - 'item' => 'required|integer|min:1', - ]); - - $pid = $request->user()->profile_id; - $type = $request->input('type'); - $item = $request->input('item'); - $action = $type . '.block'; - if (!in_array($action, $this->filters)) { - return abort(406); - } - $filterable = []; - switch ($type) { - case 'user': - $profile = Profile::findOrFail($item); - if ($profile->id == $pid) { - return abort(403); - } - $class = get_class($profile); - $filterable['id'] = $profile->id; - $filterable['type'] = $class; - break; - - default: - abort(400); - break; - } - - - $filter = UserFilter::whereUserId($pid) - ->whereFilterableId($filterable['id']) - ->whereFilterableType($filterable['type']) - ->whereFilterType('block') - ->first(); - - if($filter) { - $filter->delete(); - UserFilterService::unblock($pid, $filterable['id']); - } - - $res = RelationshipService::refresh($pid, $profile->id); - - if($request->wantsJson()) { - return response()->json($res); - } else { - return redirect()->back(); - } - } - - public function followRequests(Request $request) - { - $pid = Auth::user()->profile->id; - $followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->simplePaginate(10); - return view('account.follow-requests', compact('followers')); - } - - public function followRequestsJson(Request $request) - { - $pid = Auth::user()->profile_id; - $followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->get(); - $res = [ - 'count' => $followers->count(), - 'accounts' => $followers->take(10)->map(function($a) { - $actor = $a->actor; - return [ - 'rid' => (string) $a->id, - 'id' => (string) $actor->id, - 'username' => $actor->username, - 'avatar' => $actor->avatarUrl(), - 'url' => $actor->url(), - 'local' => $actor->domain == null, - 'account' => AccountService::get($actor->id) - ]; - }) - ]; - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function followRequestHandle(Request $request) - { - $this->validate($request, [ - 'action' => 'required|string|max:10', - 'id' => 'required|integer|min:1' - ]); - - $pid = Auth::user()->profile->id; - $action = $request->input('action') === 'accept' ? 'accept' : 'reject'; - $id = $request->input('id'); - $followRequest = FollowRequest::whereFollowingId($pid)->findOrFail($id); - $follower = $followRequest->follower; - - switch ($action) { - case 'accept': - $follow = new Follower(); - $follow->profile_id = $follower->id; - $follow->following_id = $pid; - $follow->save(); - - $profile = Profile::findOrFail($pid); - $profile->followers_count++; - $profile->save(); - AccountService::del($profile->id); - - $profile = Profile::findOrFail($follower->id); - $profile->following_count++; - $profile->save(); - AccountService::del($profile->id); - - if($follower->domain != null && $follower->private_key === null) { - FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow'); - } else { - FollowPipeline::dispatch($follow); - $followRequest->delete(); - } - break; - - case 'reject': - if($follower->domain != null && $follower->private_key === null) { - FollowRejectPipeline::dispatch($followRequest)->onQueue('follow'); - } else { - $followRequest->delete(); - } - break; - } - - Cache::forget('profile:follower_count:'.$pid); - Cache::forget('profile:following_count:'.$pid); - RelationshipService::refresh($pid, $follower->id); - - return response()->json(['msg' => 'success'], 200); - } - - public function sudoMode(Request $request) - { - if($request->session()->has('sudoModeAttempts') && $request->session()->get('sudoModeAttempts') >= 3) { - $request->session()->pull('2fa.session.active'); + protected $filters = [ + 'user.mute', + 'user.block', + ]; + + const FILTER_LIMIT_MUTE_TEXT = 'You cannot mute more than '; + + const FILTER_LIMIT_BLOCK_TEXT = 'You cannot block more than '; + + public function __construct() + { + $this->middleware('auth'); + } + + public function notifications(Request $request) + { + return view('account.activity'); + } + + public function followingActivity(Request $request) + { + $this->validate($request, [ + 'page' => 'nullable|min:1|max:3', + 'a' => 'nullable|alpha_dash', + ]); + + $action = $request->input('a'); + $allowed = ['like', 'follow']; + $timeago = Carbon::now()->subMonths(3); + + $profile = Auth::user()->profile; + $following = $profile->following->pluck('id'); + + $notifications = Notification::whereIn('actor_id', $following) + ->whereIn('action', $allowed) + ->where('actor_id', '<>', $profile->id) + ->where('profile_id', '<>', $profile->id) + ->whereDate('created_at', '>', $timeago) + ->orderBy('notifications.created_at', 'desc') + ->simplePaginate(30); + + return view('account.following', compact('profile', 'notifications')); + } + + public function verifyEmail(Request $request) + { + $recentSent = EmailVerification::whereUserId(Auth::id()) + ->whereDate('created_at', '>', now()->subHours(12))->count(); + + return view('account.verify_email', compact('recentSent')); + } + + public function sendVerifyEmail(Request $request) + { + $recentAttempt = EmailVerification::whereUserId(Auth::id()) + ->whereDate('created_at', '>', now()->subHours(12))->count(); + + if ($recentAttempt > 0) { + return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.'); + } + + EmailVerification::whereUserId(Auth::id())->delete(); + + $user = User::whereNull('email_verified_at')->find(Auth::id()); + $utoken = Str::uuid().Str::random(mt_rand(5, 9)); + $rtoken = Str::random(mt_rand(64, 70)); + + $verify = new EmailVerification; + $verify->user_id = $user->id; + $verify->email = $user->email; + $verify->user_token = $utoken; + $verify->random_token = $rtoken; + $verify->save(); + + Mail::to($user->email)->send(new ConfirmEmail($verify)); + + return redirect()->back()->with('status', 'Verification email sent!'); + } + + public function confirmVerifyEmail(Request $request, $userToken, $randomToken) + { + $verify = EmailVerification::where('user_token', $userToken) + ->where('created_at', '>', now()->subHours(24)) + ->where('random_token', $randomToken) + ->firstOrFail(); + + if (Auth::id() === $verify->user_id && $verify->user_token === $userToken && $verify->random_token === $randomToken) { + $user = User::find(Auth::id()); + $user->email_verified_at = Carbon::now(); + $user->save(); + + return redirect('/'); + } else { + abort(403); + } + } + + public function direct() + { + return view('account.direct'); + } + + public function directMessage(Request $request, $id) + { + $profile = Profile::where('id', '!=', $request->user()->profile_id) + ->findOrFail($id); + + return view('account.directmessage', compact('id')); + } + + public function mute(Request $request) + { + $this->validate($request, [ + 'type' => 'required|string|in:user', + 'item' => 'required|integer|min:1', + ]); + + $pid = $request->user()->profile_id; + $count = UserFilterService::muteCount($pid); + $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes'); + abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT.$maxLimit.' accounts'); + if ($count == 0) { + $filterCount = UserFilter::whereUserId($pid)->count(); + abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT.$maxLimit.' accounts'); + } + $type = $request->input('type'); + $item = $request->input('item'); + $action = $type.'.mute'; + + if (! in_array($action, $this->filters)) { + return abort(406); + } + $filterable = []; + switch ($type) { + case 'user': + $profile = Profile::findOrFail($item); + if ($profile->id == $pid) { + return abort(403); + } + $class = get_class($profile); + $filterable['id'] = $profile->id; + $filterable['type'] = $class; + break; + } + + $filter = UserFilter::firstOrCreate([ + 'user_id' => $pid, + 'filterable_id' => $filterable['id'], + 'filterable_type' => $filterable['type'], + 'filter_type' => 'mute', + ]); + + UserFilterService::mute($pid, $filterable['id']); + $res = RelationshipService::refresh($pid, $profile->id); + + if ($request->wantsJson()) { + return response()->json($res); + } else { + return redirect()->back(); + } + } + + public function unmute(Request $request) + { + $this->validate($request, [ + 'type' => 'required|string|in:user', + 'item' => 'required|integer|min:1', + ]); + + $pid = $request->user()->profile_id; + $type = $request->input('type'); + $item = $request->input('item'); + $action = $type.'.mute'; + + if (! in_array($action, $this->filters)) { + return abort(406); + } + $filterable = []; + switch ($type) { + case 'user': + $profile = Profile::findOrFail($item); + if ($profile->id == $pid) { + return abort(403); + } + $class = get_class($profile); + $filterable['id'] = $profile->id; + $filterable['type'] = $class; + break; + + default: + abort(400); + break; + } + + $filter = UserFilter::whereUserId($pid) + ->whereFilterableId($filterable['id']) + ->whereFilterableType($filterable['type']) + ->whereFilterType('mute') + ->first(); + + if ($filter) { + UserFilterService::unmute($pid, $filterable['id']); + $filter->delete(); + } + + $res = RelationshipService::refresh($pid, $profile->id); + + if ($request->wantsJson()) { + return response()->json($res); + } else { + return redirect()->back(); + } + } + + public function block(Request $request) + { + $this->validate($request, [ + 'type' => 'required|string|in:user', + 'item' => 'required|integer|min:1', + ]); + $pid = $request->user()->profile_id; + $count = UserFilterService::blockCount($pid); + $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks'); + abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT.$maxLimit.' accounts'); + if ($count == 0) { + $filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count(); + abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT.$maxLimit.' accounts'); + } + $type = $request->input('type'); + $item = $request->input('item'); + $action = $type.'.block'; + if (! in_array($action, $this->filters)) { + return abort(406); + } + $filterable = []; + switch ($type) { + case 'user': + $profile = Profile::findOrFail($item); + if ($profile->id == $pid || ($profile->user && $profile->user->is_admin == true)) { + return abort(403); + } + $class = get_class($profile); + $filterable['id'] = $profile->id; + $filterable['type'] = $class; + + $followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first(); + if ($followed) { + $followed->delete(); + $profile->following_count = Follower::whereProfileId($profile->id)->count(); + $profile->save(); + $selfProfile = $request->user()->profile; + $selfProfile->followers_count = Follower::whereFollowingId($pid)->count(); + $selfProfile->save(); + FollowerService::remove($profile->id, $pid); + AccountService::del($pid); + AccountService::del($profile->id); + } + + $following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first(); + if ($following) { + $following->delete(); + $profile->followers_count = Follower::whereFollowingId($profile->id)->count(); + $profile->save(); + $selfProfile = $request->user()->profile; + $selfProfile->following_count = Follower::whereProfileId($pid)->count(); + $selfProfile->save(); + FollowerService::remove($pid, $profile->pid); + AccountService::del($pid); + AccountService::del($profile->id); + } + + Notification::whereProfileId($pid) + ->whereActorId($profile->id) + ->get() + ->map(function ($n) use ($pid) { + NotificationService::del($pid, $n['id']); + $n->forceDelete(); + }); + break; + } + + $filter = UserFilter::firstOrCreate([ + 'user_id' => $pid, + 'filterable_id' => $filterable['id'], + 'filterable_type' => $filterable['type'], + 'filter_type' => 'block', + ]); + + UserFilterService::block($pid, $filterable['id']); + $res = RelationshipService::refresh($pid, $profile->id); + + if ($request->wantsJson()) { + return response()->json($res); + } else { + return redirect()->back(); + } + } + + public function unblock(Request $request) + { + $this->validate($request, [ + 'type' => 'required|string|in:user', + 'item' => 'required|integer|min:1', + ]); + + $pid = $request->user()->profile_id; + $type = $request->input('type'); + $item = $request->input('item'); + $action = $type.'.block'; + if (! in_array($action, $this->filters)) { + return abort(406); + } + $filterable = []; + switch ($type) { + case 'user': + $profile = Profile::findOrFail($item); + if ($profile->id == $pid) { + return abort(403); + } + $class = get_class($profile); + $filterable['id'] = $profile->id; + $filterable['type'] = $class; + break; + + default: + abort(400); + break; + } + + $filter = UserFilter::whereUserId($pid) + ->whereFilterableId($filterable['id']) + ->whereFilterableType($filterable['type']) + ->whereFilterType('block') + ->first(); + + if ($filter) { + $filter->delete(); + UserFilterService::unblock($pid, $filterable['id']); + } + + $res = RelationshipService::refresh($pid, $profile->id); + + if ($request->wantsJson()) { + return response()->json($res); + } else { + return redirect()->back(); + } + } + + public function followRequests(Request $request) + { + $pid = Auth::user()->profile->id; + $followers = FollowRequest::whereFollowingId($pid)->orderBy('id', 'desc')->whereIsRejected(0)->simplePaginate(10); + + return view('account.follow-requests', compact('followers')); + } + + public function followRequestsJson(Request $request) + { + $pid = Auth::user()->profile_id; + $followers = FollowRequest::whereFollowingId($pid)->orderBy('id', 'desc')->whereIsRejected(0)->get(); + $res = [ + 'count' => $followers->count(), + 'accounts' => $followers->take(10)->map(function ($a) { + $actor = $a->actor; + + return [ + 'rid' => (string) $a->id, + 'id' => (string) $actor->id, + 'username' => $actor->username, + 'avatar' => $actor->avatarUrl(), + 'url' => $actor->url(), + 'local' => $actor->domain == null, + 'account' => AccountService::get($actor->id), + ]; + }), + ]; + + return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + public function followRequestHandle(Request $request) + { + $this->validate($request, [ + 'action' => 'required|string|max:10', + 'id' => 'required|integer|min:1', + ]); + + $pid = Auth::user()->profile->id; + $action = $request->input('action') === 'accept' ? 'accept' : 'reject'; + $id = $request->input('id'); + $followRequest = FollowRequest::whereFollowingId($pid)->findOrFail($id); + $follower = $followRequest->follower; + + switch ($action) { + case 'accept': + $follow = new Follower; + $follow->profile_id = $follower->id; + $follow->following_id = $pid; + $follow->save(); + + $profile = Profile::findOrFail($pid); + $profile->followers_count++; + $profile->save(); + AccountService::del($profile->id); + + $profile = Profile::findOrFail($follower->id); + $profile->following_count++; + $profile->save(); + AccountService::del($profile->id); + + if ($follower->domain != null && $follower->private_key === null) { + FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow'); + } else { + FollowPipeline::dispatch($follow); + $followRequest->delete(); + } + break; + + case 'reject': + if ($follower->domain != null && $follower->private_key === null) { + FollowRejectPipeline::dispatch($followRequest)->onQueue('follow'); + } else { + $followRequest->delete(); + } + break; + } + + Cache::forget('profile:follower_count:'.$pid); + Cache::forget('profile:following_count:'.$pid); + RelationshipService::refresh($pid, $follower->id); + + return response()->json(['msg' => 'success'], 200); + } + + public function sudoMode(Request $request) + { + if ($request->session()->has('sudoModeAttempts') && $request->session()->get('sudoModeAttempts') >= 3) { + $request->session()->pull('2fa.session.active'); $request->session()->pull('redirectNext'); $request->session()->pull('sudoModeAttempts'); Auth::logout(); + return redirect(route('login')); } - return view('auth.sudo'); - } - - public function sudoModeVerify(Request $request) - { - $this->validate($request, [ - 'password' => 'required|string|max:500', - 'trustDevice' => 'nullable' - ]); - - $user = Auth::user(); - $password = $request->input('password'); - $trustDevice = $request->input('trustDevice') == 'on'; - $next = $request->session()->get('redirectNext', '/'); - if($request->session()->has('sudoModeAttempts')) { - $count = (int) $request->session()->get('sudoModeAttempts'); - $request->session()->put('sudoModeAttempts', $count + 1); - } else { - $request->session()->put('sudoModeAttempts', 1); - } - if(password_verify($password, $user->password) === true) { - $request->session()->put('sudoMode', time()); - if($trustDevice == true) { - $request->session()->put('sudoTrustDevice', 1); - } - - //Fix wrong scheme when using reverse proxy - if(!str_contains($next, 'https') && config('instance.force_https_urls', true)) { + + return view('auth.sudo'); + } + + public function sudoModeVerify(Request $request) + { + $this->validate($request, [ + 'password' => 'required|string|max:500', + 'trustDevice' => 'nullable', + ]); + + $user = Auth::user(); + $password = $request->input('password'); + $trustDevice = $request->input('trustDevice') == 'on'; + $next = $request->session()->get('redirectNext', '/'); + if ($request->session()->has('sudoModeAttempts')) { + $count = (int) $request->session()->get('sudoModeAttempts'); + $request->session()->put('sudoModeAttempts', $count + 1); + } else { + $request->session()->put('sudoModeAttempts', 1); + } + if (password_verify($password, $user->password) === true) { + $request->session()->put('sudoMode', time()); + if ($trustDevice == true) { + $request->session()->put('sudoTrustDevice', 1); + } + + // Fix wrong scheme when using reverse proxy + if (! str_contains($next, 'https') && config('instance.force_https_urls', true)) { $next = Str::of($next)->replace('http', 'https')->toString(); } - return redirect($next); - } else { - return redirect() - ->back() - ->withErrors(['password' => __('auth.failed')]); - } - } - - public function twoFactorCheckpoint(Request $request) - { - return view('auth.checkpoint'); - } - - public function twoFactorVerify(Request $request) - { - $this->validate($request, [ - 'code' => 'required|string|max:32' - ]); - $user = Auth::user(); - $code = $request->input('code'); - $google2fa = new Google2FA(); - $verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code); - if($verify) { - $request->session()->push('2fa.session.active', true); - return redirect('/'); - } else { - - if($this->twoFactorBackupCheck($request, $code, $user)) { - return redirect('/'); - } - - if($request->session()->has('2fa.attempts')) { - $count = (int) $request->session()->get('2fa.attempts'); - if($count == 3) { - Auth::logout(); - return redirect('/'); - } - $request->session()->put('2fa.attempts', $count + 1); - } else { - $request->session()->put('2fa.attempts', 1); - } - return redirect('/i/auth/checkpoint')->withErrors([ - 'code' => 'Invalid code' - ]); - } - } - - protected function twoFactorBackupCheck($request, $code, User $user) - { - $backupCodes = $user->{'2fa_backup_codes'}; - if($backupCodes) { - $codes = json_decode($backupCodes, true); - foreach ($codes as $c) { - if(hash_equals($c, $code)) { - $codes = array_flatten(array_diff($codes, [$code])); - $user->{'2fa_backup_codes'} = json_encode($codes); - $user->save(); - $request->session()->push('2fa.session.active', true); - return true; - } - } + return redirect($next); + } else { + return redirect() + ->back() + ->withErrors(['password' => __('auth.failed')]); + } + } + + public function twoFactorCheckpoint(Request $request) + { + return view('auth.checkpoint'); + } + + public function twoFactorVerify(Request $request) + { + $this->validate($request, [ + 'code' => 'required|string|max:32', + ]); + $user = Auth::user(); + $code = $request->input('code'); + $google2fa = new Google2FA; + $verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code); + if ($verify) { + $request->session()->push('2fa.session.active', true); + + return redirect('/'); + } else { + + if ($this->twoFactorBackupCheck($request, $code, $user)) { + return redirect('/'); + } + + if ($request->session()->has('2fa.attempts')) { + $count = (int) $request->session()->get('2fa.attempts'); + if ($count == 3) { + Auth::logout(); + + return redirect('/'); + } + $request->session()->put('2fa.attempts', $count + 1); + } else { + $request->session()->put('2fa.attempts', 1); + } + + return redirect('/i/auth/checkpoint')->withErrors([ + 'code' => 'Invalid code', + ]); + } + } + + protected function twoFactorBackupCheck($request, $code, User $user) + { + $backupCodes = $user->{'2fa_backup_codes'}; + if ($backupCodes) { + $codes = json_decode($backupCodes, true); + foreach ($codes as $c) { + if (hash_equals($c, $code)) { + $codes = array_flatten(array_diff($codes, [$code])); + $user->{'2fa_backup_codes'} = json_encode($codes); + $user->save(); + $request->session()->push('2fa.session.active', true); + + return true; + } + } + return false; - } else { - return false; - } - } + } else { + return false; + } + } - public function accountRestored(Request $request) - { - } + public function accountRestored(Request $request) {} - public function accountMutes(Request $request) + public function accountMutes(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40' + 'limit' => 'nullable|integer|min:1|max:40', ]); $user = $request->user(); @@ -600,31 +600,32 @@ class AccountController extends Controller ->pluck('filterable_id'); $accounts = Profile::find($mutes); - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Collection($accounts, new AccountTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Collection($accounts, new AccountTransformer); $res = $fractal->createData($resource)->toArray(); $url = $request->url(); $page = $request->input('page', 1); $next = $page < 40 ? $page + 1 : 40; $prev = $page > 1 ? $page - 1 : 1; $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"'; + return response()->json($res, 200, ['Link' => $links]); } public function accountBlocks(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40', - 'page' => 'nullable|integer|min:1|max:10' + 'limit' => 'nullable|integer|min:1|max:40', + 'page' => 'nullable|integer|min:1|max:10', ]); $user = $request->user(); $limit = $request->input('limit') ?? 40; - $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id') + $blocked = UserFilter::select('filterable_id', 'filterable_type', 'filter_type', 'user_id') ->whereUserId($user->profile_id) ->whereFilterableType('App\Profile') ->whereFilterType('block') @@ -632,15 +633,16 @@ class AccountController extends Controller ->pluck('filterable_id'); $profiles = Profile::findOrFail($blocked); - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer); $res = $fractal->createData($resource)->toArray(); $url = $request->url(); $page = $request->input('page', 1); $next = $page < 40 ? $page + 1 : 40; $prev = $page > 1 ? $page - 1 : 1; $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"'; + return response()->json($res, 200, ['Link' => $links]); } diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index a6fdaaea5..cb51fdf97 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -26,6 +26,7 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize; use App\Jobs\LikePipeline\LikePipeline; use App\Jobs\MediaPipeline\MediaDeletePipeline; use App\Jobs\MediaPipeline\MediaSyncLicensePipeline; +use App\Jobs\NotificationPipeline\NotificationWarmUserCache; use App\Jobs\SharePipeline\SharePipeline; use App\Jobs\SharePipeline\UndoSharePipeline; use App\Jobs\StatusPipeline\NewStatusPipeline; @@ -763,7 +764,8 @@ class ApiV1Controller extends Controller 'reblog_of_id', 'type', 'id', - 'scope' + 'scope', + 'pinned_order' ) ->whereProfileId($profile['id']) ->whereNull('in_reply_to_id') @@ -2387,7 +2389,7 @@ class ApiV1Controller extends Controller if (empty($res)) { if (! Cache::has('pf:services:notifications:hasSynced:'.$pid)) { Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); - NotificationService::warmCache($pid, 400, true); + NotificationWarmUserCache::dispatch($pid); } } @@ -4435,4 +4437,61 @@ class ApiV1Controller extends Controller }) ); } + + /** + * GET /api/v1/statuses/{id}/pin + */ + public function statusPin(Request $request, $id) + { + abort_if(! $request->user(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + $user = $request->user(); + $status = Status::whereScope('public')->find($id); + + if (! $status) { + return $this->json(['error' => 'Record not found'], 404); + } + + if ($status->profile_id != $user->profile_id) { + return $this->json(['error' => "Validation failed: Someone else's post cannot be pinned"], 422); + } + + $res = StatusService::markPin($status->id); + + if (! $res['success']) { + return $this->json([ + 'error' => $res['error'], + ], 422); + } + + $statusRes = StatusService::get($status->id, true, true); + $status['pinned'] = true; + + return $this->json($statusRes); + } + + /** + * GET /api/v1/statuses/{id}/unpin + */ + public function statusUnpin(Request $request, $id) + { + abort_if(! $request->user(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + $status = Status::whereScope('public')->findOrFail($id); + $user = $request->user(); + + if ($status->profile_id != $user->profile_id) { + return $this->json(['error' => 'Record not found'], 404); + } + + $res = StatusService::unmarkPin($status->id); + if (! $res) { + return $this->json($res, 422); + } + + $status = StatusService::get($status->id, true, true); + $status['pinned'] = false; + + return $this->json($status); + } } diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index 7ac73b4d0..4fd66690e 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -2,46 +2,22 @@ namespace App\Http\Controllers\Api; -use Illuminate\Http\Request; -use App\Http\Controllers\{ - Controller, - AvatarController -}; -use Auth, Cache, Storage, URL; -use Carbon\Carbon; -use App\{ - Avatar, - Like, - Media, - Notification, - Profile, - Status, - StatusArchived -}; -use App\Transformer\Api\{ - AccountTransformer, - NotificationTransformer, - MediaTransformer, - MediaDraftTransformer, - StatusTransformer, - StatusStatelessTransformer -}; -use League\Fractal; -use App\Util\Media\Filter; -use League\Fractal\Serializer\ArraySerializer; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Avatar; +use App\Http\Controllers\AvatarController; +use App\Http\Controllers\Controller; use App\Jobs\AvatarPipeline\AvatarOptimize; -use App\Jobs\ImageOptimizePipeline\ImageOptimize; -use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail -}; +use App\Jobs\NotificationPipeline\NotificationWarmUserCache; use App\Services\AccountService; use App\Services\NotificationService; -use App\Services\MediaPathService; -use App\Services\MediaBlocklistService; use App\Services\StatusService; +use App\Status; +use App\StatusArchived; +use App\Transformer\Api\StatusStatelessTransformer; +use Auth; +use Cache; +use Illuminate\Http\Request; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; class BaseApiController extends Controller { @@ -50,47 +26,47 @@ class BaseApiController extends Controller public function __construct() { // $this->middleware('auth'); - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); } public function notifications(Request $request) { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $limit = $request->input('limit', 20); - - $since = $request->input('since_id'); - $min = $request->input('min_id'); - $max = $request->input('max_id'); - - if(!$since && !$min && !$max) { - $min = 1; - } - - $maxId = null; - $minId = null; - - if($max) { - $res = NotificationService::getMax($pid, $max, $limit); - $ids = NotificationService::getRankedMaxId($pid, $max, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } - } else { - $res = NotificationService::getMin($pid, $min ?? $since, $limit); - $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } - } - - if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) { - Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); - NotificationService::warmCache($pid, 100, true); + abort_if(! $request->user(), 403); + + $pid = $request->user()->profile_id; + $limit = $request->input('limit', 20); + + $since = $request->input('since_id'); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + + if (! $since && ! $min && ! $max) { + $min = 1; + } + + $maxId = null; + $minId = null; + + if ($max) { + $res = NotificationService::getMax($pid, $max, $limit); + $ids = NotificationService::getRankedMaxId($pid, $max, $limit); + if (! empty($ids)) { + $maxId = max($ids); + $minId = min($ids); + } + } else { + $res = NotificationService::getMin($pid, $min ?? $since, $limit); + $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit); + if (! empty($ids)) { + $maxId = max($ids); + $minId = min($ids); + } + } + + if (empty($res) && ! Cache::has('pf:services:notifications:hasSynced:'.$pid)) { + Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); + NotificationWarmUserCache::dispatch($pid); } return response()->json($res); @@ -98,17 +74,17 @@ class BaseApiController extends Controller public function avatarUpdate(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'), + 'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'), ]); try { $user = Auth::user(); $profile = $user->profile; $file = $request->file('upload'); - $path = (new AvatarController())->getPath($user, $file); + $path = (new AvatarController)->getPath($user, $file); $dir = $path['root']; $name = $path['name']; $public = $path['storage']; @@ -129,13 +105,13 @@ class BaseApiController extends Controller return response()->json([ 'code' => 200, - 'msg' => 'Avatar successfully updated', + 'msg' => 'Avatar successfully updated', ]); } public function verifyCredentials(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $user = $request->user(); if ($user->status != null) { @@ -143,47 +119,51 @@ class BaseApiController extends Controller abort(403); } $res = AccountService::get($user->profile_id); + return response()->json($res); } public function accountLikes(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $this->validate($request, [ - 'page' => 'sometimes|int|min:1|max:20', - 'limit' => 'sometimes|int|min:1|max:10' + 'page' => 'sometimes|int|min:1|max:20', + 'limit' => 'sometimes|int|min:1|max:10', ]); $user = $request->user(); $limit = $request->input('limit', 10); $res = \DB::table('likes') - ->whereProfileId($user->profile_id) - ->latest() - ->simplePaginate($limit) - ->map(function($id) { - $status = StatusService::get($id->status_id, false); - $status['favourited'] = true; - return $status; - }) - ->filter(function($post) { - return $post && isset($post['account']); - }) - ->values(); + ->whereProfileId($user->profile_id) + ->latest() + ->simplePaginate($limit) + ->map(function ($id) use ($user) { + $status = StatusService::get($id->status_id, false); + $status['favourited'] = true; + $status['reblogged'] = (bool) StatusService::isShared($id->status_id, $user->profile_id); + + return $status; + }) + ->filter(function ($post) { + return $post && isset($post['account']); + }) + ->values(); + return response()->json($res); } public function archive(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $status = Status::whereNull('in_reply_to_id') ->whereNull('reblog_of_id') ->whereProfileId($request->user()->profile_id) ->findOrFail($id); - if($status->scope === 'archived') { + if ($status->scope === 'archived') { return [200]; } @@ -204,14 +184,14 @@ class BaseApiController extends Controller public function unarchive(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $status = Status::whereNull('in_reply_to_id') ->whereNull('reblog_of_id') ->whereProfileId($request->user()->profile_id) ->findOrFail($id); - if($status->scope !== 'archived') { + if ($status->scope !== 'archived') { return [200]; } @@ -231,16 +211,17 @@ class BaseApiController extends Controller public function archivedPosts(Request $request) { - abort_if(!$request->user(), 403); + abort_if(! $request->user(), 403); $statuses = Status::whereProfileId($request->user()->profile_id) ->whereScope('archived') ->orderByDesc('id') ->simplePaginate(10); - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer); + return $fractal->createData($resource)->toArray(); } } diff --git a/app/Http/Controllers/ImportPostController.php b/app/Http/Controllers/ImportPostController.php index 47456b2b3..e491019f8 100644 --- a/app/Http/Controllers/ImportPostController.php +++ b/app/Http/Controllers/ImportPostController.php @@ -103,67 +103,95 @@ class ImportPostController extends Controller $uid = $request->user()->id; $pid = $request->user()->profile_id; + $successCount = 0; + $errors = []; + foreach($request->input('files') as $file) { - $media = $file['media']; - $c = collect($media); - $postHash = hash('sha256', $c->toJson()); - $exts = $c->map(function($m) { - $fn = last(explode('/', $m['uri'])); - return last(explode('.', $fn)); - }); - $postType = 'photo'; - - if($exts->count() > 1) { - if($exts->contains('mp4')) { - if($exts->contains('jpg', 'png')) { - $postType = 'photo:video:album'; - } else { - $postType = 'video:album'; - } - } else { - $postType = 'photo:album'; + try { + $media = $file['media']; + $c = collect($media); + + $firstUri = isset($media[0]['uri']) ? $media[0]['uri'] : ''; + $postHash = hash('sha256', $c->toJson() . $firstUri); + + $exists = ImportPost::where('user_id', $uid) + ->where('post_hash', $postHash) + ->exists(); + + if ($exists) { + $errors[] = "Duplicate post detected. Skipping..."; + continue; } - } else { - if(in_array($exts[0], ['jpg', 'png'])) { - $postType = 'photo'; - } else if(in_array($exts[0], ['mp4'])) { - $postType = 'video'; + + $exts = $c->map(function($m) { + $fn = last(explode('/', $m['uri'])); + return last(explode('.', $fn)); + }); + + $postType = $this->determinePostType($exts); + + $ip = new ImportPost; + $ip->user_id = $uid; + $ip->profile_id = $pid; + $ip->post_hash = $postHash; + $ip->service = 'instagram'; + $ip->post_type = $postType; + $ip->media_count = $c->count(); + + $ip->media = $c->map(function($m) { + return [ + 'uri' => $m['uri'], + 'title' => $this->formatHashtags($m['title'] ?? ''), + 'creation_timestamp' => $m['creation_timestamp'] ?? null + ]; + })->toArray(); + + $ip->caption = $c->count() > 1 ? + $this->formatHashtags($file['title'] ?? '') : + $this->formatHashtags($ip->media[0]['title'] ?? ''); + + $originalFilename = last(explode('/', $ip->media[0]['uri'] ?? '')); + $ip->filename = $this->sanitizeFilename($originalFilename); + + $ip->metadata = $c->map(function($m) { + return [ + 'uri' => $m['uri'], + 'media_metadata' => isset($m['media_metadata']) ? $m['media_metadata'] : null + ]; + })->toArray(); + + $creationTimestamp = $c->count() > 1 ? + ($file['creation_timestamp'] ?? null) : + ($media[0]['creation_timestamp'] ?? null); + + if ($creationTimestamp) { + $ip->creation_date = now()->parse($creationTimestamp); + $ip->creation_year = $ip->creation_date->format('y'); + $ip->creation_month = $ip->creation_date->format('m'); + $ip->creation_day = $ip->creation_date->format('d'); + } else { + $ip->creation_date = now(); + $ip->creation_year = now()->format('y'); + $ip->creation_month = now()->format('m'); + $ip->creation_day = now()->format('d'); } - } - $ip = new ImportPost; - $ip->user_id = $uid; - $ip->profile_id = $pid; - $ip->post_hash = $postHash; - $ip->service = 'instagram'; - $ip->post_type = $postType; - $ip->media_count = $c->count(); - $ip->media = $c->map(function($m) { - return [ - 'uri' => $m['uri'], - 'title' => $this->formatHashtags($m['title']), - 'creation_timestamp' => $m['creation_timestamp'] - ]; - })->toArray(); - $ip->caption = $c->count() > 1 ? $this->formatHashtags($file['title']) : $this->formatHashtags($ip->media[0]['title']); - $ip->filename = last(explode('/', $ip->media[0]['uri'])); - $ip->metadata = $c->map(function($m) { - return [ - 'uri' => $m['uri'], - 'media_metadata' => isset($m['media_metadata']) ? $m['media_metadata'] : null - ]; - })->toArray(); - $ip->creation_date = $c->count() > 1 ? now()->parse($file['creation_timestamp']) : now()->parse($media[0]['creation_timestamp']); - $ip->creation_year = now()->parse($ip->creation_date)->format('y'); - $ip->creation_month = now()->parse($ip->creation_date)->format('m'); - $ip->creation_day = now()->parse($ip->creation_date)->format('d'); - $ip->save(); - - ImportService::getImportedFiles($pid, true); - ImportService::getPostCount($pid, true); + $ip->save(); + $successCount++; + + ImportService::getImportedFiles($pid, true); + ImportService::getPostCount($pid, true); + } catch (\Exception $e) { + $errors[] = $e->getMessage(); + \Log::error('Import error: ' . $e->getMessage()); + continue; + } } + return [ - 'msg' => 'Success' + 'success' => true, + 'msg' => 'Successfully imported ' . $successCount . ' posts', + 'errors' => $errors ]; } @@ -173,7 +201,17 @@ class ImportPostController extends Controller $this->checkPermissions($request); - $mimes = config('import.instagram.allow_video_posts') ? 'mimetypes:image/png,image/jpeg,video/mp4' : 'mimetypes:image/png,image/jpeg'; + $allowedMimeTypes = ['image/png', 'image/jpeg']; + + if (config('import.instagram.allow_image_webp') && str_contains(config_cache('pixelfed.media_types'), 'image/webp')) { + $allowedMimeTypes[] = 'image/webp'; + } + + if (config('import.instagram.allow_video_posts')) { + $allowedMimeTypes[] = 'video/mp4'; + } + + $mimes = 'mimetypes:' . implode(',', $allowedMimeTypes); $this->validate($request, [ 'file' => 'required|array|max:10', @@ -186,7 +224,12 @@ class ImportPostController extends Controller ]); foreach($request->file('file') as $file) { - $fileName = $file->getClientOriginalName(); + $extension = $file->getClientOriginalExtension(); + + $originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $originalName); + $fileName = $safeFilename . '.' . $extension; + $file->storeAs('imports/' . $request->user()->id . '/', $fileName); } @@ -197,6 +240,46 @@ class ImportPostController extends Controller ]; } + + private function determinePostType($exts) + { + if ($exts->count() > 1) { + if ($exts->contains('mp4')) { + if ($exts->contains('jpg', 'png', 'webp')) { + return 'photo:video:album'; + } else { + return 'video:album'; + } + } else { + return 'photo:album'; + } + } else { + if ($exts->isEmpty()) { + return 'photo'; + } + + $ext = $exts[0]; + + if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp'])) { + return 'photo'; + } else if (in_array($ext, ['mp4'])) { + return 'video'; + } else { + return 'photo'; + } + } + } + + private function sanitizeFilename($filename) + { + $parts = explode('.', $filename); + $extension = array_pop($parts); + $originalName = implode('.', $parts); + + $safeFilename = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $originalName); + return $safeFilename . '.' . $extension; + } + protected function checkPermissions($request, $abortOnFail = true) { $user = $request->user(); diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index c83fb2a53..bd9a80e68 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -35,6 +35,11 @@ class PublicApiController extends Controller $this->fractal->setSerializer(new ArraySerializer); } + public function json($res, $code = 200, $headers = []) + { + return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } + protected function getUserData($user) { if (! $user) { @@ -667,10 +672,8 @@ class PublicApiController extends Controller 'only_media' => 'nullable', 'pinned' => 'nullable', 'exclude_replies' => 'nullable', - 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, - 'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, - 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, 'limit' => 'nullable|integer|min:1|max:24', + 'cursor' => 'nullable', ]); $user = $request->user(); @@ -683,83 +686,198 @@ class PublicApiController extends Controller } $limit = $request->limit ?? 9; - $max_id = $request->max_id; - $min_id = $request->min_id; $scope = ['photo', 'photo:album', 'video', 'video:album']; $onlyMedia = $request->input('only_media', true); + $pinned = $request->filled('pinned') && $request->boolean('pinned') == true; + $hasCursor = $request->filled('cursor'); + + $visibility = $this->determineVisibility($profile, $user); + + if (empty($visibility)) { + return response()->json([]); + } + + $result = collect(); + $remainingLimit = $limit; + + if ($pinned && ! $hasCursor) { + $pinnedStatuses = Status::whereProfileId($profile['id']) + ->whereNotNull('pinned_order') + ->orderBy('pinned_order') + ->get(); + + $pinnedResult = $this->processStatuses($pinnedStatuses, $user, $onlyMedia); + $result = $pinnedResult; + + $remainingLimit = max(1, $limit - $pinnedResult->count()); + } + + $paginator = Status::whereProfileId($profile['id']) + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->when($pinned, function ($query) { + return $query->whereNull('pinned_order'); + }) + ->whereIn('type', $scope) + ->whereIn('scope', $visibility) + ->orderByDesc('id') + ->cursorPaginate($remainingLimit) + ->withQueryString(); + + $headers = $this->generatePaginationHeaders($paginator); + $regularStatuses = $this->processStatuses($paginator->items(), $user, $onlyMedia); + $result = $result->concat($regularStatuses); + + return response()->json($result, 200, $headers); + } + + /** + * GET /api/pixelfed/v1/statuses/{id}/pin + */ + public function statusPin(Request $request, $id) + { + abort_if(! $request->user(), 403); + $user = $request->user(); + $status = Status::whereScope('public')->find($id); + + if (! $status) { + return $this->json(['error' => 'Record not found'], 404); + } + + if ($status->profile_id != $user->profile_id) { + return $this->json(['error' => "Validation failed: Someone else's post cannot be pinned"], 422); + } + + $res = StatusService::markPin($status->id); + + if (! $res['success']) { + return $this->json([ + 'error' => $res['error'], + ], 422); + } + + $statusRes = StatusService::get($status->id, true, true); + $status['pinned'] = true; + + return $this->json($statusRes); + } + + /** + * GET /api/pixelfed/v1/statuses/{id}/unpin + */ + public function statusUnpin(Request $request, $id) + { + abort_if(! $request->user(), 403); + $status = Status::whereScope('public')->findOrFail($id); + $user = $request->user(); + + if ($status->profile_id != $user->profile_id) { + return $this->json(['error' => 'Record not found'], 404); + } - if (! $min_id && ! $max_id) { - $min_id = 1; + $res = StatusService::unmarkPin($status->id); + if (! $res) { + return $this->json($res, 422); + } + + $status = StatusService::get($status->id, true, true); + $status['pinned'] = false; + + return $this->json($status); + } + + private function determineVisibility($profile, $user) + { + if (! $user || ! isset($user->profile_id)) { + return []; + } + + if (! $profile || ! isset($profile['id'])) { + return []; + } + + if ($profile['id'] == $user->profile_id) { + return ['public', 'unlisted', 'private']; } if ($profile['locked']) { if (! $user) { - return response()->json([]); + return []; } + $pid = $user->profile_id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { - $following = Follower::whereProfileId($pid)->pluck('following_id'); + $isFollowing = FollowerService::follows($pid, $profile['id']); - return $following->push($pid)->toArray(); - }); - $visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : []; + return $isFollowing ? ['public', 'unlisted', 'private'] : ['public']; } else { if ($user) { $pid = $user->profile_id; - $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { - $following = Follower::whereProfileId($pid)->pluck('following_id'); + $isFollowing = FollowerService::follows($pid, $profile['id']); - return $following->push($pid)->toArray(); - }); - $visibility = in_array($profile['id'], $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; + return $isFollowing ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; } else { - $visibility = ['public', 'unlisted']; + return ['public', 'unlisted']; } } - $dir = $min_id ? '>' : '<'; - $id = $min_id ?? $max_id; - $res = Status::whereProfileId($profile['id']) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereIn('type', $scope) - ->where('id', $dir, $id) - ->whereIn('scope', $visibility) - ->limit($limit) - ->orderByDesc('id') - ->get() - ->map(function ($s) use ($user) { - try { - $status = StatusService::get($s->id, false); - if (! $status) { - return false; - } - } catch (\Exception $e) { - $status = false; + } + + private function processStatuses($statuses, $user, $onlyMedia) + { + return collect($statuses)->map(function ($status) use ($user) { + try { + $mastodonStatus = StatusService::get($status->id, false); + if (! $mastodonStatus) { + return null; } - if ($user && $status) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + + if ($user) { + $mastodonStatus['favourited'] = (bool) LikeService::liked($user->profile_id, $status->id); + $mastodonStatus['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $status->id); + $mastodonStatus['reblogged'] = (bool) StatusService::isShared($status->id, $user->profile_id); } - return $status; - }) - ->filter(function ($s) use ($onlyMedia) { - if (! $s) { + return $mastodonStatus; + } catch (\Exception $e) { + return null; + } + }) + ->filter(function ($status) use ($onlyMedia) { + if (! $status) { return false; } + if ($onlyMedia) { - if ( - ! isset($s['media_attachments']) || - ! is_array($s['media_attachments']) || - empty($s['media_attachments']) - ) { - return false; - } + return isset($status['media_attachments']) && + is_array($status['media_attachments']) && + ! empty($status['media_attachments']); } - return $s; + return true; }) ->values(); + } - return response()->json($res); + /** + * Generate pagination link headers from paginator + */ + private function generatePaginationHeaders($paginator) + { + $link = null; + + if ($paginator->onFirstPage()) { + if ($paginator->hasMorePages()) { + $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } else { + if ($paginator->previousPageUrl()) { + $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; + } + + if ($paginator->hasMorePages()) { + $link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } + + return isset($link) ? ['Link' => $link] : []; } } diff --git a/app/Jobs/NotificationPipeline/NotificationWarmUserCache.php b/app/Jobs/NotificationPipeline/NotificationWarmUserCache.php new file mode 100644 index 000000000..8cf4b5aaa --- /dev/null +++ b/app/Jobs/NotificationPipeline/NotificationWarmUserCache.php @@ -0,0 +1,90 @@ +pid = $pid; + } + + /** + * Get the unique ID for the job. + */ + public function uniqueId(): string + { + return 'notifications:profile_warm_cache:'.$this->pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + NotificationService::warmCache($this->pid, 100, true); + } catch (\Exception $e) { + Log::error('Failed to warm notification cache', [ + 'profile_id' => $this->pid, + 'exception' => get_class($e), + 'message' => $e->getMessage(), + 'attempt' => $this->attempts(), + ]); + throw $e; + } + } +} diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index de2f4d112..014c76aba 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -12,6 +12,8 @@ class StatusService { const CACHE_KEY = 'pf:services:status:v1.1:'; + const MAX_PINNED = 3; + public static function key($id, $publicOnly = true) { $p = $publicOnly ? 'pub:' : 'all:'; @@ -82,7 +84,6 @@ class StatusService $status['shortcode'], $status['taggedPeople'], $status['thread'], - $status['pinned'], $status['account']['header_bg'], $status['account']['is_admin'], $status['account']['last_fetched_at'], @@ -198,4 +199,89 @@ class StatusService { return InstanceService::totalLocalStatuses(); } + + public static function isPinned($id) + { + return Status::whereId($id)->whereNotNull('pinned_order')->exists(); + } + + public static function totalPins($pid) + { + return Status::whereProfileId($pid)->whereNotNull('pinned_order')->count(); + } + + public static function markPin($id) + { + $status = Status::find($id); + + if (! $status) { + return [ + 'success' => false, + 'error' => 'Record not found', + ]; + } + + if ($status->scope != 'public') { + return [ + 'success' => false, + 'error' => 'Validation failed: you can only pin public posts', + ]; + } + + if (self::isPinned($id)) { + return [ + 'success' => false, + 'error' => 'This post is already pinned', + ]; + } + + $totalPins = self::totalPins($status->profile_id); + + if ($totalPins >= self::MAX_PINNED) { + return [ + 'success' => false, + 'error' => 'Validation failed: You have already pinned the max number of posts', + ]; + } + + $status->pinned_order = $totalPins + 1; + $status->save(); + + self::refresh($id); + + return [ + 'success' => true, + 'error' => null, + ]; + } + + public static function unmarkPin($id) + { + $status = Status::find($id); + + if (! $status || is_null($status->pinned_order)) { + return false; + } + + $removedOrder = $status->pinned_order; + $profileId = $status->profile_id; + + $status->pinned_order = null; + $status->save(); + + Status::where('profile_id', $profileId) + ->whereNotNull('pinned_order') + ->where('pinned_order', '>', $removedOrder) + ->orderBy('pinned_order', 'asc') + ->chunk(10, function ($statuses) { + foreach ($statuses as $s) { + $s->pinned_order = $s->pinned_order - 1; + $s->save(); + } + }); + + self::refresh($id); + + return true; + } } diff --git a/app/Transformer/Api/StatusStatelessTransformer.php b/app/Transformer/Api/StatusStatelessTransformer.php index 9f52ab50a..9de16465b 100644 --- a/app/Transformer/Api/StatusStatelessTransformer.php +++ b/app/Transformer/Api/StatusStatelessTransformer.php @@ -69,6 +69,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract 'tags' => StatusHashtagService::statusTags($status->id), 'poll' => $poll, 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null, + 'pinned' => (bool) $status->pinned_order, ]; } } diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php index 4a6dc5d7d..4c8520628 100644 --- a/app/Transformer/Api/StatusTransformer.php +++ b/app/Transformer/Api/StatusTransformer.php @@ -71,6 +71,7 @@ class StatusTransformer extends Fractal\TransformerAbstract 'poll' => $poll, 'bookmarked' => BookmarkService::get($pid, $status->id), 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null, + 'pinned' => (bool) $status->pinned_order, ]; } } diff --git a/database/migrations/2025_03_19_022553_add_pinned_columns_statuses_table.php b/database/migrations/2025_03_19_022553_add_pinned_columns_statuses_table.php new file mode 100644 index 000000000..b7bdc684a --- /dev/null +++ b/database/migrations/2025_03_19_022553_add_pinned_columns_statuses_table.php @@ -0,0 +1,30 @@ +tinyInteger('pinned_order')->nullable()->default(null)->index(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + Schema::table('statuses', function (Blueprint $table) { + $table->dropColumn('pinned_order'); + }); + } +}; diff --git a/docker/README.md b/docker/README.md index 5230f60fd..5598908c6 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,5 +1,5 @@ # Pixelfed + Docker + Docker Compose -Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage. +Please see the [Pixelfed Docs (Next)](https://jippi.github.io/docker-pixelfed/) for current documentation on Docker usage. The docs can be [reviewed in the pixelfed/docs-next](https://github.com/pixelfed/docs-next/pull/1) repository. diff --git a/public/js/account-import.js b/public/js/account-import.js index 7ed0a3999..e0a4ba6f3 100644 --- a/public/js/account-import.js +++ b/public/js/account-import.js @@ -1,2 +1,2 @@ /*! For license information please see account-import.js.LICENSE.txt */ -(self.webpackChunkpixelfed=self.webpackChunkpixelfed||[]).push([[9139],{5316:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>a});var i=n(11887),r={};for(const t in i)"default"!==t&&(r[t]=()=>i[t]);n.d(e,r);const a=i.default},10889:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>s});var i=n(85072),r=n.n(i),a=n(52888),o={insert:"head",singleton:!1};r()(a.default,o);const s=a.default.locals||{}},11887:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>u});var i=n(58285);function r(t){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}function a(t){return function(t){if(Array.isArray(t))return o(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return o(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n=0;--a){var o=this.tryEntries[a],s=o.completion;if("root"===o.tryLoc)return r("end");if(o.tryLoc<=this.prev){var l=i.call(o,"catchLoc"),c=i.call(o,"finallyLoc");if(l&&c){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&i.call(r,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),E(n),b}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var i=n.completion;if("throw"===i.type){var r=i.arg;E(n)}return r}}throw Error("illegal catch attempt")},delegateYield:function(e,n,i){return this.delegate={iterator:O(e),resultName:n,nextLoc:i},"next"===this.method&&(this.arg=t),b}},e}function l(t,e,n,i,r,a,o){try{var s=t[a](o),l=s.value}catch(t){return void n(t)}s.done?e(l):Promise.resolve(l).then(i,r)}function c(t){return function(){var e=this,n=arguments;return new Promise((function(i,r){var a=t.apply(e,n);function o(t){l(a,i,r,o,s,"next",t)}function s(t){l(a,i,r,o,s,"throw",t)}o(void 0)}))}}const u={data:function(){return{page:1,step:1,toggleLimit:300,config:{},showDisabledWarning:!1,showNotAllowedWarning:!1,invalidArchive:!1,loaded:!1,existing:[],zipName:void 0,zipFiles:[],postMeta:[],imageCache:[],includeArchives:!1,selectedMedia:[],selectedPostsCounter:0,detailsModalShow:!1,modalData:{},importedPosts:[],finishedCount:void 0,processingCount:void 0,showUploadLoader:!1,importButtonLoading:!1}},mounted:function(){this.fetchConfig()},methods:{fetchConfig:function(){var t=this;axios.get("/api/local/import/ig/config").then((function(e){t.config=e.data,0==e.data.enabled?(t.showDisabledWarning=!0,t.loaded=!0):0==e.data.allowed?(t.showNotAllowedWarning=!0,t.loaded=!0):t.fetchExisting()}))},fetchExisting:function(){var t=this;axios.post("/api/local/import/ig/existing").then((function(e){t.existing=e.data})).finally((function(){t.fetchProcessing()}))},fetchProcessing:function(){var t=this;axios.post("/api/local/import/ig/processing").then((function(e){t.processingCount=e.data.processing_count,t.finishedCount=e.data.finished_count})).finally((function(){t.loaded=!0}))},selectArchive:function(){var t=this;event.currentTarget.blur(),swal({title:"Upload Archive",icon:"success",text:"The .zip archive is probably named something like username_20230606.zip, and was downloaded from the Instagram.com website.",buttons:{cancel:"Cancel",danger:{text:"Upload zip archive",value:"upload"}}}).then((function(e){t.$refs.zipInput.click()}))},zipInputChanged:function(t){var e=this;this.step=2,this.zipName=t.target.files[0].name,this.showUploadLoader=!0,setTimeout((function(){e.reviewImports()}),1e3),setTimeout((function(){e.showUploadLoader=!1}),3e3)},reviewImports:function(){this.invalidArchive=!1,this.checkZip()},model:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return new i.ZipReader(new i.BlobReader(t)).getEntries(e)},formatDate:function(t){return(!(arguments.length>1&&void 0!==arguments[1])||arguments[1]?new Date(1e3*t):new Date(t)).toLocaleDateString()},getFileNameUrl:function(t){return this.imageCache.filter((function(e){return e.filename===t})).map((function(t){return t.blob}))},showDetailsModal:function(t){this.modalData=t,this.detailsModalShow=!0,setTimeout((function(){pixelfed.readmore()}),500)},fixFacebookEncoding:function(t){return c(s().mark((function e(){var n,i;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.replace(/\\u00([a-f0-9]{2})/g,(function(t){return String.fromCharCode(parseInt(t.slice(2),16))})),i=Array.from(n,(function(t){return t.charCodeAt(0)})),e.abrupt("return",(new TextDecoder).decode(new Uint8Array(i)));case 3:case"end":return e.stop()}}),e)})))()},filterPostMeta:function(t){var e=this;return c(s().mark((function n(){var i,r,a;return s().wrap((function(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,e.fixFacebookEncoding(t);case 2:return i=n.sent,r=JSON.parse(i),Array.isArray(r)||(r=new Array(r)),a=r.filter((function(t){return t.media.map((function(t){return t.uri})).filter((function(t){var n=[".png",".jpg"];return e.config.allow_video_posts&&n.push(".mp4"),e.config.allow_image_webp&&n.push(".webp"),n.some((function(e){return t.endsWith(e)}))})).length})).filter((function(t){var n=t.media.map((function(t){return t.uri}));return!e.existing.includes(n[0])})),e.postMeta=a,n.abrupt("return",a);case 8:case"end":return n.stop()}}),n)})))()},checkZip:function(){var t=this;return c(s().mark((function e(){var n,i,r;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.$refs.zipInput.files[0],e.next=3,t.model(n);case 3:if(!(i=e.sent)||!i.length){e.next=15;break}return e.next=7,i.filter((function(t){return"content/posts_1.json"===t.filename||"your_instagram_activity/content/posts_1.json"===t.filename||"your_instagram_activity/media/posts_1.json"===t.filename}));case 7:if((r=e.sent)&&r.length){e.next=14;break}return t.contactModal("Invalid import archive","The .zip archive you uploaded is corrupted, or is invalid. We cannot process your import at this time.\n\nIf this issue persists, please contact an administrator.","error"),t.invalidArchive=!0,e.abrupt("return");case 14:t.readZip();case 15:case"end":return e.stop()}}),e)})))()},readZip:function(){var t=this;return c(s().mark((function e(){var n,r,a,o;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.$refs.zipInput.files[0],e.next=3,t.model(n);case 3:if(!(r=e.sent)||!r.length){e.next=14;break}return t.zipFiles=r,e.next=8,r.filter((function(t){return"content/posts_1.json"===t.filename||"your_instagram_activity/content/posts_1.json"===t.filename||"your_instagram_activity/media/posts_1.json"===t.filename}))[0].getData(new i.TextWriter);case 8:return a=e.sent,t.filterPostMeta(a),e.next=12,Promise.all(r.filter((function(t){return(t.filename.startsWith("media/posts/")||t.filename.startsWith("media/other/"))&&(t.filename.endsWith(".png")||t.filename.endsWith(".jpg")||t.filename.endsWith(".mp4"))})).map(function(){var t=c(s().mark((function t(e){var n,r,a;return s().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(!e.filename.startsWith("media/posts/")&&!e.filename.startsWith("media/other/")||!(e.filename.endsWith(".png")||e.filename.endsWith(".jpg")||e.filename.endsWith(".mp4"))){t.next=10;break}return n={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",mp4:"video/mp4"}[e.filename.split("/").pop().split(".").pop()],t.next=5,e.getData(new i.BlobWriter(n));case 5:return r=t.sent,a=URL.createObjectURL(r),t.abrupt("return",{filename:e.filename,blob:a,file:r});case 10:return t.abrupt("return");case 11:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()));case 12:o=e.sent,t.imageCache=o.flat(2);case 14:setTimeout((function(){t.page=2}),500);case 15:case"end":return e.stop()}}),e)})))()},toggleLimitReached:function(){this.contactModal("Limit reached","You can only import "+this.toggleLimit+" posts at a time.\nYou can import more posts after you finish importing these posts.","error")},toggleSelectedPost:function(t){var e,n=this;if(1===t.media.length)if(e=t.media[0].uri,-1==this.selectedMedia.indexOf(e)){if(this.selectedPostsCounter>=this.toggleLimit)return void this.toggleLimitReached();this.selectedMedia.push(e),this.selectedPostsCounter++}else{var i=this.selectedMedia.indexOf(e);this.selectedMedia.splice(i,1),this.selectedPostsCounter--}else{if(e=t.media[0].uri,-1==this.selectedMedia.indexOf(e)){if(this.selectedPostsCounter>=this.toggleLimit)return void this.toggleLimitReached();this.selectedPostsCounter++}else this.selectedPostsCounter--;t.media.forEach((function(t){if(e=t.uri,-1==n.selectedMedia.indexOf(e))n.selectedMedia.push(e);else{var i=n.selectedMedia.indexOf(e);n.selectedMedia.splice(i,1)}}))}},sliceIntoChunks:function(t,e){for(var n=[],i=0;i0&&void 0!==arguments[0]?arguments[0]:"Error",text:arguments.length>1?arguments[1]:void 0,icon:arguments.length>2?arguments[2]:void 0,dangerMode:!0,buttons:{ok:arguments.length>3&&void 0!==arguments[3]?arguments[3]:"Close",danger:{text:"Contact Support",value:"contact"}}}).then((function(t){"contact"===t&&(window.location.href="/site/contact")}))},handleSelectAll:function(){for(var t=this.postMeta.slice(0,this.toggleLimit),e=t.length-1;e>=0;e--){var n=t[e];this.toggleSelectedPost(n)}},handleClearAll:function(){this.selectedMedia=[],this.selectedPostsCounter=0}}}},18824:(t,e,n)=>{"use strict";n.r(e);var i=n(10889),r={};for(const t in i)"default"!==t&&(r[t]=()=>i[t]);n.d(e,r)},39074:(t,e,n)=>{"use strict";n.r(e),n.d(e,{render:()=>i,staticRenderFns:()=>r});var i=function(){var t=this,e=t._self._c;return e("div",{staticClass:"h-100 pf-import"},[t.loaded?[e("input",{ref:"zipInput",staticClass:"d-none",attrs:{type:"file",name:"file"},on:{change:t.zipInputChanged}}),t._v(" "),1===t.page?[t._m(0),t._v(" "),e("hr"),t._v(" "),t._m(1),t._v(" "),e("section",{staticClass:"mt-4"},[e("ul",{staticClass:"list-group"},[e("li",{staticClass:"list-group-item d-flex justify-content-between flex-column",staticStyle:{gap:"1rem"}},[e("div",{staticClass:"d-flex justify-content-between align-items-center",staticStyle:{gap:"1rem"}},[e("div",[e("p",{staticClass:"font-weight-bold mb-1"},[t._v("Import from Instagram")]),t._v(" "),t.showDisabledWarning?e("p",{staticClass:"small mb-0"},[t._v("This feature has been disabled by the administrators.")]):t.showNotAllowedWarning?e("p",{staticClass:"small mb-0"},[t._v("You have not been permitted to use this feature, or have reached the maximum limits. For more info, view the "),e("a",{staticClass:"font-weight-bold",attrs:{href:"/site/kb/import"}},[t._v("Import Help Center")]),t._v(" page.")]):e("p",{staticClass:"small mb-0"},[t._v("Upload the JSON export from Instagram in .zip format."),e("br"),t._v("For more information click "),e("a",{attrs:{href:"/site/kb/import"}},[t._v("here")]),t._v(".")])]),t._v(" "),t.showDisabledWarning||t.showNotAllowedWarning?t._e():e("div",[1===t.step||t.invalidArchive?e("button",{staticClass:"font-weight-bold btn btn-primary rounded-pill px-4 btn-lg",attrs:{type:"button",disabled:t.showDisabledWarning},on:{click:function(e){return t.selectArchive()}}},[t._v("\n Import\n ")]):2===t.step?[e("div",{staticClass:"d-flex justify-content-center align-items-center flex-column"},[t.showUploadLoader?e("b-spinner",{attrs:{small:""}}):e("button",{staticClass:"font-weight-bold btn btn-outline-primary btn-sm btn-block",attrs:{type:"button"},on:{click:function(e){return t.reviewImports()}}},[t._v("Review Imports")]),t._v(" "),t.zipName?e("p",{staticClass:"small font-weight-bold mt-2 mb-0"},[t._v(t._s(t.zipName))]):t._e()],1)]:t._e()],2)])])]),t._v(" "),e("ul",{staticClass:"list-group mt-3"},[t.processingCount?e("li",{staticClass:"list-group-item d-flex justify-content-between flex-column",staticStyle:{gap:"1rem"}},[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[t._m(2),t._v(" "),e("div",[e("span",{staticClass:"btn btn-danger rounded-pill py-0 font-weight-bold",attrs:{disabled:""}},[t._v(t._s(t.processingCount))])])])]):t._e(),t._v(" "),t.finishedCount?e("li",{staticClass:"list-group-item d-flex justify-content-between flex-column",staticStyle:{gap:"1rem"}},[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[t._m(3),t._v(" "),e("div",[e("button",{staticClass:"font-weight-bold btn btn-primary btn-sm rounded-pill px-4 btn-block",attrs:{type:"button",disabled:!t.finishedCount},on:{click:function(e){return t.handleReviewPosts()}}},[t._v("\n Review "+t._s(t.finishedCount)+" Posts\n ")])])])]):t._e()])])]:2===t.page?[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[t._m(4),t._v(" "),e("button",{staticClass:"btn btn-primary font-weight-bold rounded-pill px-4",class:{disabled:!t.selectedMedia||!t.selectedMedia.length},attrs:{disabled:!t.selectedMedia||!t.selectedMedia.length||t.importButtonLoading},on:{click:function(e){return t.handleImport()}}},[t.importButtonLoading?e("b-spinner",{attrs:{small:""}}):e("span",[t._v("Import")])],1)]),t._v(" "),e("hr"),t._v(" "),e("section",[e("div",{staticClass:"d-flex justify-content-between align-items-center mb-3"},[t.selectedMedia&&t.selectedMedia.length?e("p",{staticClass:"lead mb-0"},[e("span",{staticClass:"font-weight-bold"},[t._v(t._s(t.selectedPostsCounter))]),t._v(" posts selected for import")]):e("div",[e("p",{staticClass:"lead mb-0"},[t._v("Review posts you'd like to import.")]),t._v(" "),e("p",{staticClass:"small text-muted mb-0"},[t._v("Tap on posts to include them in your import.")])]),t._v(" "),t.selectedMedia.length?e("button",{staticClass:"btn btn-outline-danger font-weight-bold rounded-pill btn-sm my-1",on:{click:function(e){return t.handleClearAll()}}},[t._v("Clear all selected")]):e("button",{staticClass:"btn btn-outline-primary font-weight-bold rounded-pill",on:{click:function(e){return t.handleSelectAll()}}},[t._v("Select first "+t._s(t.toggleLimit)+" posts")])])]),t._v(" "),e("section",{staticClass:"row mb-n5 media-selector",staticStyle:{"max-height":"600px","overflow-y":"auto"}},t._l(t.postMeta,(function(n){return e("div",{staticClass:"col-12 col-md-4"},[e("div",{staticClass:"square cursor-pointer",on:{click:function(e){return t.toggleSelectedPost(n)}}},[n.media[0].uri.endsWith(".mp4")?e("div",{staticClass:"info-overlay-text-label rounded",class:{selected:-1!=t.selectedMedia.indexOf(n.media[0].uri)}},[t._m(5,!0)]):e("div",{staticClass:"square-content",class:{selected:-1!=t.selectedMedia.indexOf(n.media[0].uri)},style:{borderRadius:"5px",backgroundImage:"url("+t.getFileNameUrl(n.media[0].uri)+")"}})]),t._v(" "),e("div",{staticClass:"d-flex mt-1 justify-content-between align-items-center"},[e("p",{staticClass:"small"},[e("i",{staticClass:"far fa-clock"}),t._v(" "+t._s(t.formatDate(n.media[0].creation_timestamp)))]),t._v(" "),e("p",{staticClass:"small font-weight-bold"},[e("a",{attrs:{href:"#"},on:{click:function(e){return e.preventDefault(),t.showDetailsModal(n)}}},[e("i",{staticClass:"far fa-info-circle"}),t._v(" Details")])])])])})),0)]:"reviewImports"===t.page?[t._m(6),t._v(" "),e("hr"),t._v(" "),e("section",{staticClass:"row mb-n5 media-selector",staticStyle:{"max-height":"600px","overflow-y":"auto"}},[t._l(t.importedPosts.data,(function(n){return e("div",{staticClass:"col-12 col-md-4"},[e("div",{staticClass:"square cursor-pointer"},[n.media_attachments[0].url.endsWith(".mp4")?e("div",{staticClass:"info-overlay-text-label rounded"},[t._m(7,!0)]):e("div",{staticClass:"square-content",style:{borderRadius:"5px",backgroundImage:"url("+n.media_attachments[0].url+")"}})]),t._v(" "),e("div",{staticClass:"d-flex mt-1 justify-content-between align-items-center"},[e("p",{staticClass:"small"},[e("i",{staticClass:"far fa-clock"}),t._v(" "+t._s(t.formatDate(n.created_at,!1)))]),t._v(" "),e("p",{staticClass:"small font-weight-bold"},[e("a",{attrs:{href:n.url}},[e("i",{staticClass:"far fa-info-circle"}),t._v(" View")])])])])})),t._v(" "),e("div",{staticClass:"col-12 my-3"},[t.importedPosts.meta&&t.importedPosts.meta.next_cursor?e("button",{staticClass:"btn btn-primary btn-block font-weight-bold",on:{click:function(e){return t.loadMorePosts()}}},[t._v("\n Load more\n ")]):t._e()])],2)]:t._e()]:e("div",{staticClass:"d-flex justify-content-center align-items-center h-100"},[e("b-spinner")],1),t._v(" "),e("b-modal",{attrs:{id:"detailsModal",title:"Post Details","ok-only":!0,"ok-title":"Close",centered:""},model:{value:t.detailsModalShow,callback:function(e){t.detailsModalShow=e},expression:"detailsModalShow"}},[e("div",{},t._l(t.modalData.media,(function(n,i){return e("div",{staticClass:"mb-3"},[e("div",{staticClass:"list-group"},[e("div",{staticClass:"list-group-item d-flex justify-content-between align-items-center"},[e("p",{staticClass:"text-center font-weight-bold mb-0"},[t._v("Media #"+t._s(i+1))]),t._v(" "),n.uri.endsWith(".jpg")||n.uri.endsWith(".png")?[e("img",{staticStyle:{"object-fit":"cover","border-radius":"5px"},attrs:{src:t.getFileNameUrl(n.uri),width:"30",height:"30"}})]:t._e()],2),t._v(" "),n.uri.endsWith(".mp4")?[e("div",{staticClass:"list-group-item"},[e("div",{staticClass:"embed-responsive embed-responsive-4by3"},[e("video",{attrs:{src:t.getFileNameUrl(n.uri),controls:""}})])])]:t._e(),t._v(" "),e("div",{staticClass:"list-group-item"},[e("p",{staticClass:"small text-muted"},[t._v("Caption")]),t._v(" "),e("p",{staticClass:"mb-0 small read-more",staticStyle:{"font-size":"12px","overflow-y":"hidden"}},[t._v(t._s(n.title?n.title:t.modalData.title))])]),t._v(" "),e("div",{staticClass:"list-group-item"},[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[e("p",{staticClass:"small mb-0 text-muted"},[t._v("Timestamp")]),t._v(" "),e("p",{staticClass:"font-weight-bold mb-0"},[t._v(t._s(t.formatDate(n.creation_timestamp)))])])])],2)])})),0)])],2)},r=[function(){var t=this._self._c;return t("div",{staticClass:"title"},[t("h3",{staticClass:"font-weight-bold"},[this._v("Import")])])},function(){var t=this._self._c;return t("section",[t("p",{staticClass:"lead"},[this._v("Account Import allows you to import your data from a supported service.")])])},function(){var t=this,e=t._self._c;return e("div",[e("p",{staticClass:"font-weight-bold mb-1"},[t._v("Processing Imported Posts")]),t._v(" "),e("p",{staticClass:"small mb-0"},[t._v("These are posts that are in the process of being imported.")])])},function(){var t=this,e=t._self._c;return e("div",[e("p",{staticClass:"font-weight-bold mb-1"},[t._v("Imported Posts")]),t._v(" "),e("p",{staticClass:"small mb-0"},[t._v("These are posts that have been successfully imported.")])])},function(){var t=this._self._c;return t("div",{staticClass:"title"},[t("h3",{staticClass:"font-weight-bold"},[this._v("Import from Instagram")])])},function(){var t=this._self._c;return t("h5",{staticClass:"text-white m-auto font-weight-bold"},[t("span",[t("span",{staticClass:"far fa-video fa-2x p-2 d-flex-inline"})])])},function(){var t=this._self._c;return t("div",{staticClass:"d-flex justify-content-between align-items-center"},[t("div",{staticClass:"title"},[t("h3",{staticClass:"font-weight-bold"},[this._v("Posts Imported from Instagram")])])])},function(){var t=this._self._c;return t("h5",{staticClass:"text-white m-auto font-weight-bold"},[t("span",[t("span",{staticClass:"far fa-video fa-2x p-2 d-flex-inline"})])])}]},40895:(t,e,n)=>{"use strict";n.r(e);var i=n(39074),r={};for(const t in i)"default"!==t&&(r[t]=()=>i[t]);n.d(e,r)},52888:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>a});var i=n(76798),r=n.n(i)()((function(t){return t[1]}));r.push([t.id,".pf-import .media-selector .selected[data-v-bf3fb15a]{border:5px solid red}",""]);const a=r},70627:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>o});var i=n(40895),r=n(5316),a={};for(const t in r)"default"!==t&&(a[t]=()=>r[t]);n.d(e,a);n(18824);const o=(0,n(14486).default)(r.default,i.render,i.staticRenderFns,!1,null,"bf3fb15a",null).exports},97697:(t,e,n)=>{Vue.component("account-import",n(70627).default)}},t=>{t.O(0,[3660],(()=>{return e=97697,t(t.s=e);var e}));t.O()}]); \ No newline at end of file +(self.webpackChunkpixelfed=self.webpackChunkpixelfed||[]).push([[9139],{4134:(t,e,n)=>{"use strict";n.r(e);var i=n(63959),r={};for(const t in i)"default"!==t&&(r[t]=()=>i[t]);n.d(e,r)},5316:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>a});var i=n(11887),r={};for(const t in i)"default"!==t&&(r[t]=()=>i[t]);n.d(e,r);const a=i.default},11887:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>u});var i=n(58285);function r(t){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}function a(t){return function(t){if(Array.isArray(t))return o(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return o(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n=0;--a){var o=this.tryEntries[a],s=o.completion;if("root"===o.tryLoc)return r("end");if(o.tryLoc<=this.prev){var l=i.call(o,"catchLoc"),c=i.call(o,"finallyLoc");if(l&&c){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&i.call(r,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),E(n),b}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var i=n.completion;if("throw"===i.type){var r=i.arg;E(n)}return r}}throw Error("illegal catch attempt")},delegateYield:function(e,n,i){return this.delegate={iterator:O(e),resultName:n,nextLoc:i},"next"===this.method&&(this.arg=t),b}},e}function l(t,e,n,i,r,a,o){try{var s=t[a](o),l=s.value}catch(t){return void n(t)}s.done?e(l):Promise.resolve(l).then(i,r)}function c(t){return function(){var e=this,n=arguments;return new Promise((function(i,r){var a=t.apply(e,n);function o(t){l(a,i,r,o,s,"next",t)}function s(t){l(a,i,r,o,s,"throw",t)}o(void 0)}))}}const u={data:function(){return{page:1,step:1,toggleLimit:300,config:{},showDisabledWarning:!1,showNotAllowedWarning:!1,invalidArchive:!1,loaded:!1,existing:[],zipName:void 0,zipFiles:[],postMeta:[],imageCache:[],includeArchives:!1,selectedMedia:[],selectedPostsCounter:0,detailsModalShow:!1,modalData:{},importedPosts:[],finishedCount:void 0,processingCount:void 0,showUploadLoader:!1,importButtonLoading:!1}},mounted:function(){this.fetchConfig()},methods:{fetchConfig:function(){var t=this;axios.get("/api/local/import/ig/config").then((function(e){t.config=e.data,0==e.data.enabled?(t.showDisabledWarning=!0,t.loaded=!0):0==e.data.allowed?(t.showNotAllowedWarning=!0,t.loaded=!0):t.fetchExisting()}))},fetchExisting:function(){var t=this;axios.post("/api/local/import/ig/existing").then((function(e){t.existing=e.data})).finally((function(){t.fetchProcessing()}))},fetchProcessing:function(){var t=this;axios.post("/api/local/import/ig/processing").then((function(e){t.processingCount=e.data.processing_count,t.finishedCount=e.data.finished_count})).finally((function(){t.loaded=!0}))},selectArchive:function(){var t=this;event.currentTarget.blur(),swal({title:"Upload Archive",icon:"success",text:"The .zip archive is probably named something like username_20230606.zip, and was downloaded from the Instagram.com website.",buttons:{cancel:"Cancel",danger:{text:"Upload zip archive",value:"upload"}}}).then((function(e){t.$refs.zipInput.click()}))},zipInputChanged:function(t){var e=this;this.step=2,this.zipName=t.target.files[0].name,this.showUploadLoader=!0,setTimeout((function(){e.reviewImports()}),1e3),setTimeout((function(){e.showUploadLoader=!1}),3e3)},reviewImports:function(){this.invalidArchive=!1,this.checkZip()},model:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return new i.ZipReader(new i.BlobReader(t)).getEntries(e)},formatDate:function(t){return(!(arguments.length>1&&void 0!==arguments[1])||arguments[1]?new Date(1e3*t):new Date(t)).toLocaleDateString()},getFileNameUrl:function(t){return this.imageCache.filter((function(e){return e.filename===t})).map((function(t){return t.blob}))},showDetailsModal:function(t){this.modalData=t,this.detailsModalShow=!0,setTimeout((function(){pixelfed.readmore()}),500)},fixFacebookEncoding:function(t){return c(s().mark((function e(){var n,i;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.replace(/\\u00([a-f0-9]{2})/g,(function(t){return String.fromCharCode(parseInt(t.slice(2),16))})),i=Array.from(n,(function(t){return t.charCodeAt(0)})),e.abrupt("return",(new TextDecoder).decode(new Uint8Array(i)));case 3:case"end":return e.stop()}}),e)})))()},filterPostMeta:function(t){var e=this;return c(s().mark((function n(){var i,r,a;return s().wrap((function(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,e.fixFacebookEncoding(t);case 2:return i=n.sent,r=JSON.parse(i),Array.isArray(r)||(r=new Array(r)),a=r.filter((function(t){return t.media.map((function(t){return t.uri})).filter((function(t){var n=[".png",".jpg"];return e.config.allow_video_posts&&n.push(".mp4"),e.config.allow_image_webp&&n.push(".webp"),n.some((function(e){return t.endsWith(e)}))})).length})).filter((function(t){var n=t.media.map((function(t){return t.uri}));return!e.existing.includes(n[0])})),e.postMeta=a,n.abrupt("return",a);case 8:case"end":return n.stop()}}),n)})))()},checkZip:function(){var t=this;return c(s().mark((function e(){var n,i,r;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.$refs.zipInput.files[0],e.next=3,t.model(n);case 3:if(!(i=e.sent)||!i.length){e.next=15;break}return e.next=7,i.filter((function(t){return"content/posts_1.json"===t.filename||"your_instagram_activity/content/posts_1.json"===t.filename||"your_instagram_activity/media/posts_1.json"===t.filename}));case 7:if((r=e.sent)&&r.length){e.next=14;break}return t.contactModal("Invalid import archive","The .zip archive you uploaded is corrupted, or is invalid. We cannot process your import at this time.\n\nIf this issue persists, please contact an administrator.","error"),t.invalidArchive=!0,e.abrupt("return");case 14:t.readZip();case 15:case"end":return e.stop()}}),e)})))()},readZip:function(){var t=this;return c(s().mark((function e(){var n,r,a,o;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.$refs.zipInput.files[0],e.next=3,t.model(n);case 3:if(!(r=e.sent)||!r.length){e.next=14;break}return t.zipFiles=r,e.next=8,r.filter((function(t){return"content/posts_1.json"===t.filename||"your_instagram_activity/content/posts_1.json"===t.filename||"your_instagram_activity/media/posts_1.json"===t.filename}))[0].getData(new i.TextWriter);case 8:return a=e.sent,t.filterPostMeta(a),e.next=12,Promise.all(r.filter((function(e){var n=[".png",".jpg",".jpeg",".mp4"];return t.config.allow_image_webp&&n.push(".webp"),(e.filename.startsWith("media/posts/")||e.filename.startsWith("media/other/"))&&n.some((function(t){return e.filename.endsWith(t)}))})).map(function(){var e=c(s().mark((function e(n){var r,a,o,l;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(r=[".png",".jpg",".jpeg",".mp4"],t.config.allow_image_webp&&r.push(".webp"),!n.filename.startsWith("media/posts/")&&!n.filename.startsWith("media/other/")||!r.some((function(t){return n.filename.endsWith(t)}))){e.next=12;break}return a={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",mp4:"video/mp4",webp:"image/webp"}[n.filename.split("/").pop().split(".").pop()],e.next=7,n.getData(new i.BlobWriter(a));case 7:return o=e.sent,l=URL.createObjectURL(o),e.abrupt("return",{filename:n.filename,blob:l,file:o});case 12:return e.abrupt("return");case 13:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}()));case 12:o=e.sent,t.imageCache=o.flat(2);case 14:setTimeout((function(){t.page=2}),500);case 15:case"end":return e.stop()}}),e)})))()},toggleLimitReached:function(){this.contactModal("Limit reached","You can only import "+this.toggleLimit+" posts at a time.\nYou can import more posts after you finish importing these posts.","error")},toggleSelectedPost:function(t){var e,n=this;if(1===t.media.length)if(e=t.media[0].uri,-1==this.selectedMedia.indexOf(e)){if(this.selectedPostsCounter>=this.toggleLimit)return void this.toggleLimitReached();this.selectedMedia.push(e),this.selectedPostsCounter++}else{var i=this.selectedMedia.indexOf(e);this.selectedMedia.splice(i,1),this.selectedPostsCounter--}else{if(e=t.media[0].uri,-1==this.selectedMedia.indexOf(e)){if(this.selectedPostsCounter>=this.toggleLimit)return void this.toggleLimitReached();this.selectedPostsCounter++}else this.selectedPostsCounter--;t.media.forEach((function(t){if(e=t.uri,-1==n.selectedMedia.indexOf(e))n.selectedMedia.push(e);else{var i=n.selectedMedia.indexOf(e);n.selectedMedia.splice(i,1)}}))}},sliceIntoChunks:function(t,e){for(var n=[],i=0;i0&&void 0!==arguments[0]?arguments[0]:"Error",text:arguments.length>1?arguments[1]:void 0,icon:arguments.length>2?arguments[2]:void 0,dangerMode:!0,buttons:{ok:arguments.length>3&&void 0!==arguments[3]?arguments[3]:"Close",danger:{text:"Contact Support",value:"contact"}}}).then((function(t){"contact"===t&&(window.location.href="/site/contact")}))},handleSelectAll:function(){for(var t=this.postMeta.slice(0,this.toggleLimit),e=t.length-1;e>=0;e--){var n=t[e];this.toggleSelectedPost(n)}},handleClearAll:function(){this.selectedMedia=[],this.selectedPostsCounter=0}}}},18687:(t,e,n)=>{"use strict";n.r(e);var i=n(61022),r={};for(const t in i)"default"!==t&&(r[t]=()=>i[t]);n.d(e,r)},61022:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>s});var i=n(85072),r=n.n(i),a=n(75759),o={insert:"head",singleton:!1};r()(a.default,o);const s=a.default.locals||{}},63959:(t,e,n)=>{"use strict";n.r(e),n.d(e,{render:()=>i,staticRenderFns:()=>r});var i=function(){var t=this,e=t._self._c;return e("div",{staticClass:"h-100 pf-import"},[t.loaded?[e("input",{ref:"zipInput",staticClass:"d-none",attrs:{type:"file",name:"file"},on:{change:t.zipInputChanged}}),t._v(" "),1===t.page?[t._m(0),t._v(" "),e("hr"),t._v(" "),t._m(1),t._v(" "),e("section",{staticClass:"mt-4"},[e("ul",{staticClass:"list-group"},[e("li",{staticClass:"list-group-item d-flex justify-content-between flex-column",staticStyle:{gap:"1rem"}},[e("div",{staticClass:"d-flex justify-content-between align-items-center",staticStyle:{gap:"1rem"}},[e("div",[e("p",{staticClass:"font-weight-bold mb-1"},[t._v("Import from Instagram")]),t._v(" "),t.showDisabledWarning?e("p",{staticClass:"small mb-0"},[t._v("This feature has been disabled by the administrators.")]):t.showNotAllowedWarning?e("p",{staticClass:"small mb-0"},[t._v("You have not been permitted to use this feature, or have reached the maximum limits. For more info, view the "),e("a",{staticClass:"font-weight-bold",attrs:{href:"/site/kb/import"}},[t._v("Import Help Center")]),t._v(" page.")]):e("p",{staticClass:"small mb-0"},[t._v("Upload the JSON export from Instagram in .zip format."),e("br"),t._v("For more information click "),e("a",{attrs:{href:"/site/kb/import"}},[t._v("here")]),t._v(".")])]),t._v(" "),t.showDisabledWarning||t.showNotAllowedWarning?t._e():e("div",[1===t.step||t.invalidArchive?e("button",{staticClass:"font-weight-bold btn btn-primary rounded-pill px-4 btn-lg",attrs:{type:"button",disabled:t.showDisabledWarning},on:{click:function(e){return t.selectArchive()}}},[t._v("\n Import\n ")]):2===t.step?[e("div",{staticClass:"d-flex justify-content-center align-items-center flex-column"},[t.showUploadLoader?e("b-spinner",{attrs:{small:""}}):e("button",{staticClass:"font-weight-bold btn btn-outline-primary btn-sm btn-block",attrs:{type:"button"},on:{click:function(e){return t.reviewImports()}}},[t._v("Review Imports")]),t._v(" "),t.zipName?e("p",{staticClass:"small font-weight-bold mt-2 mb-0"},[t._v(t._s(t.zipName))]):t._e()],1)]:t._e()],2)])])]),t._v(" "),e("ul",{staticClass:"list-group mt-3"},[t.processingCount?e("li",{staticClass:"list-group-item d-flex justify-content-between flex-column",staticStyle:{gap:"1rem"}},[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[t._m(2),t._v(" "),e("div",[e("span",{staticClass:"btn btn-danger rounded-pill py-0 font-weight-bold",attrs:{disabled:""}},[t._v(t._s(t.processingCount))])])])]):t._e(),t._v(" "),t.finishedCount?e("li",{staticClass:"list-group-item d-flex justify-content-between flex-column",staticStyle:{gap:"1rem"}},[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[t._m(3),t._v(" "),e("div",[e("button",{staticClass:"font-weight-bold btn btn-primary btn-sm rounded-pill px-4 btn-block",attrs:{type:"button",disabled:!t.finishedCount},on:{click:function(e){return t.handleReviewPosts()}}},[t._v("\n Review "+t._s(t.finishedCount)+" Posts\n ")])])])]):t._e()])])]:2===t.page?[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[t._m(4),t._v(" "),e("button",{staticClass:"btn btn-primary font-weight-bold rounded-pill px-4",class:{disabled:!t.selectedMedia||!t.selectedMedia.length},attrs:{disabled:!t.selectedMedia||!t.selectedMedia.length||t.importButtonLoading},on:{click:function(e){return t.handleImport()}}},[t.importButtonLoading?e("b-spinner",{attrs:{small:""}}):e("span",[t._v("Import")])],1)]),t._v(" "),e("hr"),t._v(" "),e("section",[e("div",{staticClass:"d-flex justify-content-between align-items-center mb-3"},[t.selectedMedia&&t.selectedMedia.length?e("p",{staticClass:"lead mb-0"},[e("span",{staticClass:"font-weight-bold"},[t._v(t._s(t.selectedPostsCounter))]),t._v(" posts selected for import")]):e("div",[e("p",{staticClass:"lead mb-0"},[t._v("Review posts you'd like to import.")]),t._v(" "),e("p",{staticClass:"small text-muted mb-0"},[t._v("Tap on posts to include them in your import.")])]),t._v(" "),t.selectedMedia.length?e("button",{staticClass:"btn btn-outline-danger font-weight-bold rounded-pill btn-sm my-1",on:{click:function(e){return t.handleClearAll()}}},[t._v("Clear all selected")]):e("button",{staticClass:"btn btn-outline-primary font-weight-bold rounded-pill",on:{click:function(e){return t.handleSelectAll()}}},[t._v("Select first "+t._s(t.toggleLimit)+" posts")])])]),t._v(" "),e("section",{staticClass:"row mb-n5 media-selector",staticStyle:{"max-height":"600px","overflow-y":"auto"}},t._l(t.postMeta,(function(n){return e("div",{staticClass:"col-12 col-md-4"},[e("div",{staticClass:"square cursor-pointer",on:{click:function(e){return t.toggleSelectedPost(n)}}},[n.media[0].uri.endsWith(".mp4")?e("div",{staticClass:"info-overlay-text-label rounded",class:{selected:-1!=t.selectedMedia.indexOf(n.media[0].uri)}},[t._m(5,!0)]):e("div",{staticClass:"square-content",class:{selected:-1!=t.selectedMedia.indexOf(n.media[0].uri)},style:{borderRadius:"5px",backgroundImage:"url("+t.getFileNameUrl(n.media[0].uri)+")"}})]),t._v(" "),e("div",{staticClass:"d-flex mt-1 justify-content-between align-items-center"},[e("p",{staticClass:"small"},[e("i",{staticClass:"far fa-clock"}),t._v(" "+t._s(t.formatDate(n.media[0].creation_timestamp)))]),t._v(" "),e("p",{staticClass:"small font-weight-bold"},[e("a",{attrs:{href:"#"},on:{click:function(e){return e.preventDefault(),t.showDetailsModal(n)}}},[e("i",{staticClass:"far fa-info-circle"}),t._v(" Details")])])])])})),0)]:"reviewImports"===t.page?[t._m(6),t._v(" "),e("hr"),t._v(" "),e("section",{staticClass:"row mb-n5 media-selector",staticStyle:{"max-height":"600px","overflow-y":"auto"}},[t._l(t.importedPosts.data,(function(n){return e("div",{staticClass:"col-12 col-md-4"},[e("div",{staticClass:"square cursor-pointer"},[n.media_attachments[0].url.endsWith(".mp4")?e("div",{staticClass:"info-overlay-text-label rounded"},[t._m(7,!0)]):e("div",{staticClass:"square-content",style:{borderRadius:"5px",backgroundImage:"url("+n.media_attachments[0].url+")"}})]),t._v(" "),e("div",{staticClass:"d-flex mt-1 justify-content-between align-items-center"},[e("p",{staticClass:"small"},[e("i",{staticClass:"far fa-clock"}),t._v(" "+t._s(t.formatDate(n.created_at,!1)))]),t._v(" "),e("p",{staticClass:"small font-weight-bold"},[e("a",{attrs:{href:n.url}},[e("i",{staticClass:"far fa-info-circle"}),t._v(" View")])])])])})),t._v(" "),e("div",{staticClass:"col-12 my-3"},[t.importedPosts.meta&&t.importedPosts.meta.next_cursor?e("button",{staticClass:"btn btn-primary btn-block font-weight-bold",on:{click:function(e){return t.loadMorePosts()}}},[t._v("\n Load more\n ")]):t._e()])],2)]:t._e()]:e("div",{staticClass:"d-flex justify-content-center align-items-center h-100"},[e("b-spinner")],1),t._v(" "),e("b-modal",{attrs:{id:"detailsModal",title:"Post Details","ok-only":!0,"ok-title":"Close",centered:""},model:{value:t.detailsModalShow,callback:function(e){t.detailsModalShow=e},expression:"detailsModalShow"}},[e("div",{},t._l(t.modalData.media,(function(n,i){return e("div",{staticClass:"mb-3"},[e("div",{staticClass:"list-group"},[e("div",{staticClass:"list-group-item d-flex justify-content-between align-items-center"},[e("p",{staticClass:"text-center font-weight-bold mb-0"},[t._v("Media #"+t._s(i+1))]),t._v(" "),n.uri.endsWith(".jpg")||n.uri.endsWith(".png")?[e("img",{staticStyle:{"object-fit":"cover","border-radius":"5px"},attrs:{src:t.getFileNameUrl(n.uri),width:"30",height:"30"}})]:t._e()],2),t._v(" "),n.uri.endsWith(".mp4")?[e("div",{staticClass:"list-group-item"},[e("div",{staticClass:"embed-responsive embed-responsive-4by3"},[e("video",{attrs:{src:t.getFileNameUrl(n.uri),controls:""}})])])]:t._e(),t._v(" "),e("div",{staticClass:"list-group-item"},[e("p",{staticClass:"small text-muted"},[t._v("Caption")]),t._v(" "),e("p",{staticClass:"mb-0 small read-more",staticStyle:{"font-size":"12px","overflow-y":"hidden"}},[t._v(t._s(n.title?n.title:t.modalData.title))])]),t._v(" "),e("div",{staticClass:"list-group-item"},[e("div",{staticClass:"d-flex justify-content-between align-items-center"},[e("p",{staticClass:"small mb-0 text-muted"},[t._v("Timestamp")]),t._v(" "),e("p",{staticClass:"font-weight-bold mb-0"},[t._v(t._s(t.formatDate(n.creation_timestamp)))])])])],2)])})),0)])],2)},r=[function(){var t=this._self._c;return t("div",{staticClass:"title"},[t("h3",{staticClass:"font-weight-bold"},[this._v("Import")])])},function(){var t=this._self._c;return t("section",[t("p",{staticClass:"lead"},[this._v("Account Import allows you to import your data from a supported service.")])])},function(){var t=this,e=t._self._c;return e("div",[e("p",{staticClass:"font-weight-bold mb-1"},[t._v("Processing Imported Posts")]),t._v(" "),e("p",{staticClass:"small mb-0"},[t._v("These are posts that are in the process of being imported.")])])},function(){var t=this,e=t._self._c;return e("div",[e("p",{staticClass:"font-weight-bold mb-1"},[t._v("Imported Posts")]),t._v(" "),e("p",{staticClass:"small mb-0"},[t._v("These are posts that have been successfully imported.")])])},function(){var t=this._self._c;return t("div",{staticClass:"title"},[t("h3",{staticClass:"font-weight-bold"},[this._v("Import from Instagram")])])},function(){var t=this._self._c;return t("h5",{staticClass:"text-white m-auto font-weight-bold"},[t("span",[t("span",{staticClass:"far fa-video fa-2x p-2 d-flex-inline"})])])},function(){var t=this._self._c;return t("div",{staticClass:"d-flex justify-content-between align-items-center"},[t("div",{staticClass:"title"},[t("h3",{staticClass:"font-weight-bold"},[this._v("Posts Imported from Instagram")])])])},function(){var t=this._self._c;return t("h5",{staticClass:"text-white m-auto font-weight-bold"},[t("span",[t("span",{staticClass:"far fa-video fa-2x p-2 d-flex-inline"})])])}]},70627:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>o});var i=n(4134),r=n(5316),a={};for(const t in r)"default"!==t&&(a[t]=()=>r[t]);n.d(e,a);n(18687);const o=(0,n(14486).default)(r.default,i.render,i.staticRenderFns,!1,null,"1896b5bd",null).exports},75759:(t,e,n)=>{"use strict";n.r(e),n.d(e,{default:()=>a});var i=n(76798),r=n.n(i)()((function(t){return t[1]}));r.push([t.id,".pf-import .media-selector .selected[data-v-1896b5bd]{border:5px solid red}",""]);const a=r},97697:(t,e,n)=>{Vue.component("account-import",n(70627).default)}},t=>{t.O(0,[3660],(()=>{return e=97697,t(t.s=e);var e}));t.O()}]); \ No newline at end of file diff --git a/public/js/app.js b/public/js/app.js index 6454cb628..8d5d146b7 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,2 +1,2 @@ /*! For license information please see app.js.LICENSE.txt */ -(self.webpackChunkpixelfed=self.webpackChunkpixelfed||[]).push([[5847],{7640:(e,t,r)=>{"use strict";r.r(t)},9901:function(){function e(t){return e="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(t)}!function(){var t="object"===("undefined"==typeof window?"undefined":e(window))?window:"object"===("undefined"==typeof self?"undefined":e(self))?self:this,r=t.BlobBuilder||t.WebKitBlobBuilder||t.MSBlobBuilder||t.MozBlobBuilder;t.URL=t.URL||t.webkitURL||function(e,t){return(t=document.createElement("a")).href=e,t};var o=t.Blob,n=URL.createObjectURL,i=URL.revokeObjectURL,a=t.Symbol&&t.Symbol.toStringTag,s=!1,f=!1,c=!!t.ArrayBuffer,u=r&&r.prototype.append&&r.prototype.getBlob;try{s=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(e){}function p(e){return e.map((function(e){if(e.buffer instanceof ArrayBuffer){var t=e.buffer;if(e.byteLength!==t.byteLength){var r=new Uint8Array(e.byteLength);r.set(new Uint8Array(t,e.byteOffset,e.byteLength)),t=r.buffer}return t}return e}))}function d(e,t){t=t||{};var o=new r;return p(e).forEach((function(e){o.append(e)})),t.type?o.getBlob(t.type):o.getBlob()}function h(e,t){return new o(p(e),t||{})}t.Blob&&(d.prototype=Blob.prototype,h.prototype=Blob.prototype);var b="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(e){for(var r=0,o=e.length,n=t.Uint8Array||Array,i=0,a=Math.max(32,o+(o>>1)+7),s=new n(a>>3<<3);r=55296&&l<=56319){if(r=55296&&l<=56319)continue}if(i+4>s.length){a+=8,a=(a*=1+r/e.length*2)>>3<<3;var c=new Uint8Array(a);c.set(s),s=c}if(4294967168&l){if(4294965248&l)if(4294901760&l){if(4292870144&l)continue;s[i++]=l>>18&7|240,s[i++]=l>>12&63|128,s[i++]=l>>6&63|128}else s[i++]=l>>12&15|224,s[i++]=l>>6&63|128;else s[i++]=l>>6&31|192;s[i++]=63&l|128}else s[i++]=l}return s.slice(0,i)},y="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(e){for(var t=e.length,r=[],o=0;o239?4:l>223?3:l>191?2:1;if(o+c<=t)switch(c){case 1:l<128&&(f=l);break;case 2:128==(192&(n=e[o+1]))&&(s=(31&l)<<6|63&n)>127&&(f=s);break;case 3:n=e[o+1],i=e[o+2],128==(192&n)&&128==(192&i)&&(s=(15&l)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=e[o+1],i=e[o+2],a=e[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&l)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,c=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=c}var u=r.length,p="";for(o=0;o>2,c=(3&n)<<4|a>>4,u=(15&a)<<2|l>>6,p=63&l;s||(p=64,i||(u=64)),r.push(t[f],t[c],t[u],t[p])}return r.join("")}var o=Object.create||function(e){function t(){}return t.prototype=e,new t};if(c)var a=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],s=ArrayBuffer.isView||function(e){return e&&a.indexOf(Object.prototype.toString.call(e))>-1};function f(r,o){o=null==o?{}:o;for(var n=0,i=(r=r||[]).length;n=t.size&&r.close()}))}})}}catch(e){try{new ReadableStream({}),g=function(e){var t=0;e=this;return new ReadableStream({pull:function(r){return e.slice(t,t+524288).arrayBuffer().then((function(o){t+=o.byteLength;var n=new Uint8Array(o);r.enqueue(n),t==e.size&&r.close()}))}})}}catch(e){try{new Response("").body.getReader().read(),g=function(){return new Response(this).body}}catch(e){g=function(){throw new Error("Include https://github.com/MattiasBuelens/web-streams-polyfill")}}}}m.arrayBuffer||(m.arrayBuffer=function(){var e=new FileReader;return e.readAsArrayBuffer(this),v(e)}),m.text||(m.text=function(){var e=new FileReader;return e.readAsText(this),v(e)}),m.stream||(m.stream=g)}(),function(e){"use strict";var t,r=e.Uint8Array,o=e.HTMLCanvasElement,n=o&&o.prototype,i=/\s*;\s*base64\s*(?:;|$)/i,a="toDataURL",s=function(e){for(var o,n,i=e.length,a=new r(i/4*3|0),s=0,l=0,f=[0,0],c=0,u=0;i--;)n=e.charCodeAt(s++),255!==(o=t[n-43])&&undefined!==o&&(f[1]=f[0],f[0]=n,u=u<<6|o,4===++c&&(a[l++]=u>>>16,61!==f[1]&&(a[l++]=u>>>8),61!==f[0]&&(a[l++]=u),c=0));return a};r&&(t=new r([62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,0,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51])),!o||n.toBlob&&n.toBlobHD||(n.toBlob||(n.toBlob=function(e,t){if(t||(t="image/png"),this.mozGetAsFile)e(this.mozGetAsFile("canvas",t));else if(this.msToBlob&&/^\s*image\/png\s*(?:$|;)/i.test(t))e(this.msToBlob());else{var o,n=Array.prototype.slice.call(arguments,1),l=this[a].apply(this,n),f=l.indexOf(","),c=l.substring(f+1),u=i.test(l.substring(0,f));Blob.fake?((o=new Blob).encoding=u?"base64":"URI",o.data=c,o.size=c.length):r&&(o=u?new Blob([s(c)],{type:t}):new Blob([decodeURIComponent(c)],{type:t})),e(o)}}),!n.toBlobHD&&n.toDataURLHD?n.toBlobHD=function(){a="toDataURLHD";var e=this.toBlob();return a="toDataURL",e}:n.toBlobHD=n.toBlob)}("undefined"!=typeof self&&self||"undefined"!=typeof window&&window||this.content||this)},51595:(e,t,r)=>{"use strict";r.r(t)},55994:(e,t,r)=>{"use strict";r.r(t)},71751:(e,t,r)=>{r(74692);var o=r(74692);r(9901),window._=r(2543),window.Popper=r(48851).default,window.pixelfed=window.pixelfed||{},window.$=r(74692),r(52754),window.axios=r(86425),window.axios.defaults.headers.common["X-Requested-With"]="XMLHttpRequest",r(63899),window.blurhash=r(95341);var n=document.head.querySelector('meta[name="csrf-token"]');n?window.axios.defaults.headers.common["X-CSRF-TOKEN"]=n.content:console.error("CSRF token not found."),window.App=window.App||{},window.App.redirect=function(){document.querySelectorAll("a").forEach((function(e,t){var r=e.getAttribute("href");if(r&&r.length>5&&r.startsWith("https://")){var o=new URL(r);o.host!==window.location.host&&"/i/redirect"!==o.pathname&&e.setAttribute("href","/i/redirect?url="+encodeURIComponent(r))}}))},window.App.boot=function(){new Vue({el:"#content"})},window.addEventListener("load",(function(){"serviceWorker"in navigator&&navigator.serviceWorker.register("/sw.js")})),window.App.util={compose:{post:function(){var e=window.location.pathname;["/","/timeline/public"].includes(e)?o("#composeModal").modal("show"):window.location.href="/?a=co"},circle:function(){console.log("Unsupported method.")},collection:function(){console.log("Unsupported method.")},loop:function(){console.log("Unsupported method.")},story:function(){console.log("Unsupported method.")}},time:function(){return new Date},version:1,format:{count:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"en-GB",r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"compact";return e<1?0:new Intl.NumberFormat(t,{notation:r,compactDisplay:"short"}).format(e)},timeAgo:function(e){var t=new Date(e),r=new Date,o=Math.floor((r-t)/1e3),n=Math.floor(o/31557600);return n>=1?n+"y":(n=Math.floor(o/604800))>=1?n+"w":(n=Math.floor(o/86400))>=1?n+"d":(n=Math.floor(o/3600))>=1?n+"h":(n=Math.floor(o/60))>=1?n+"m":Math.floor(o)+"s"},timeAhead:function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],r=Date.parse(e)-Date.parse(new Date),o=Math.floor(r/1e3),n=Math.floor(o/63072e3);return n>=1?n+(t?"y":" years"):(n=Math.floor(o/604800))>=1?n+(t?"w":" weeks"):(n=Math.floor(o/86400))>=1?n+(t?"d":" days"):(n=Math.floor(o/3600))>=1?n+(t?"h":" hours"):(n=Math.floor(o/60))>=1?n+(t?"m":" minutes"):Math.floor(o)+(t?"s":" seconds")},rewriteLinks:function(e){var t=e.innerText;return e.href.startsWith(window.location.origin)?e.href:t=1==t.startsWith("#")?"/discover/tags/"+t.substr(1)+"?src=rph":1==t.startsWith("@")?"/"+e.innerText+"?src=rpp":"/i/redirect?url="+encodeURIComponent(t)}},filters:[["1984","filter-1977"],["Azen","filter-aden"],["Astairo","filter-amaro"],["Grassbee","filter-ashby"],["Bookrun","filter-brannan"],["Borough","filter-brooklyn"],["Farms","filter-charmes"],["Hairsadone","filter-clarendon"],["Cleana ","filter-crema"],["Catpatch","filter-dogpatch"],["Earlyworm","filter-earlybird"],["Plaid","filter-gingham"],["Kyo","filter-ginza"],["Yefe","filter-hefe"],["Goddess","filter-helena"],["Yards","filter-hudson"],["Quill","filter-inkwell"],["Rankine","filter-kelvin"],["Juno","filter-juno"],["Mark","filter-lark"],["Chill","filter-lofi"],["Van","filter-ludwig"],["Apache","filter-maven"],["May","filter-mayfair"],["Ceres","filter-moon"],["Knoxville","filter-nashville"],["Felicity","filter-perpetua"],["Sandblast","filter-poprocket"],["Daisy","filter-reyes"],["Elevate","filter-rise"],["Nevada","filter-sierra"],["Futura","filter-skyline"],["Sleepy","filter-slumber"],["Steward","filter-stinson"],["Savoy","filter-sutro"],["Blaze","filter-toaster"],["Apricot","filter-valencia"],["Gloming","filter-vesper"],["Walter","filter-walden"],["Poplar","filter-willow"],["Xenon","filter-xpro-ii"]],filterCss:{"filter-1977":"sepia(.5) hue-rotate(-30deg) saturate(1.4)","filter-aden":"sepia(.2) brightness(1.15) saturate(1.4)","filter-amaro":"sepia(.35) contrast(1.1) brightness(1.2) saturate(1.3)","filter-ashby":"sepia(.5) contrast(1.2) saturate(1.8)","filter-brannan":"sepia(.4) contrast(1.25) brightness(1.1) saturate(.9) hue-rotate(-2deg)","filter-brooklyn":"sepia(.25) contrast(1.25) brightness(1.25) hue-rotate(5deg)","filter-charmes":"sepia(.25) contrast(1.25) brightness(1.25) saturate(1.35) hue-rotate(-5deg)","filter-clarendon":"sepia(.15) contrast(1.25) brightness(1.25) hue-rotate(5deg)","filter-crema":"sepia(.5) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-2deg)","filter-dogpatch":"sepia(.35) saturate(1.1) contrast(1.5)","filter-earlybird":"sepia(.25) contrast(1.25) brightness(1.15) saturate(.9) hue-rotate(-5deg)","filter-gingham":"contrast(1.1) brightness(1.1)","filter-ginza":"sepia(.25) contrast(1.15) brightness(1.2) saturate(1.35) hue-rotate(-5deg)","filter-hefe":"sepia(.4) contrast(1.5) brightness(1.2) saturate(1.4) hue-rotate(-10deg)","filter-helena":"sepia(.5) contrast(1.05) brightness(1.05) saturate(1.35)","filter-hudson":"sepia(.25) contrast(1.2) brightness(1.2) saturate(1.05) hue-rotate(-15deg)","filter-inkwell":"brightness(1.25) contrast(.85) grayscale(1)","filter-kelvin":"sepia(.15) contrast(1.5) brightness(1.1) hue-rotate(-10deg)","filter-juno":"sepia(.35) contrast(1.15) brightness(1.15) saturate(1.8)","filter-lark":"sepia(.25) contrast(1.2) brightness(1.3) saturate(1.25)","filter-lofi":"saturate(1.1) contrast(1.5)","filter-ludwig":"sepia(.25) contrast(1.05) brightness(1.05) saturate(2)","filter-maven":"sepia(.35) contrast(1.05) brightness(1.05) saturate(1.75)","filter-mayfair":"contrast(1.1) brightness(1.15) saturate(1.1)","filter-moon":"brightness(1.4) contrast(.95) saturate(0) sepia(.35)","filter-nashville":"sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg)","filter-perpetua":"contrast(1.1) brightness(1.25) saturate(1.1)","filter-poprocket":"sepia(.15) brightness(1.2)","filter-reyes":"sepia(.75) contrast(.75) brightness(1.25) saturate(1.4)","filter-rise":"sepia(.25) contrast(1.25) brightness(1.2) saturate(.9)","filter-sierra":"sepia(.25) contrast(1.5) brightness(.9) hue-rotate(-15deg)","filter-skyline":"sepia(.15) contrast(1.25) brightness(1.25) saturate(1.2)","filter-slumber":"sepia(.35) contrast(1.25) saturate(1.25)","filter-stinson":"sepia(.35) contrast(1.25) brightness(1.1) saturate(1.25)","filter-sutro":"sepia(.4) contrast(1.2) brightness(.9) saturate(1.4) hue-rotate(-10deg)","filter-toaster":"sepia(.25) contrast(1.5) brightness(.95) hue-rotate(-15deg)","filter-valencia":"sepia(.25) contrast(1.1) brightness(1.1)","filter-vesper":"sepia(.35) contrast(1.15) brightness(1.2) saturate(1.3)","filter-walden":"sepia(.35) contrast(.8) brightness(1.25) saturate(1.4)","filter-willow":"brightness(1.2) contrast(.85) saturate(.05) sepia(.2)","filter-xpro-ii":"sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg)"},emoji:["😂","💯","❤️","🙌","👏","👌","😍","😯","😢","😅","😁","🙂","😎","😀","🤣","😃","😄","😆","😉","😊","😋","😘","😗","😙","😚","🤗","🤩","🤔","🤨","😐","😑","😶","🙄","😏","😣","😥","😮","🤐","😪","😫","😴","😌","😛","😜","😝","🤤","😒","😓","😔","😕","🙃","🤑","😲","🙁","😖","😞","😟","😤","😭","😦","😧","😨","😩","🤯","😬","😰","😱","😳","🤪","😵","😡","😠","🤬","😷","🤒","🤕","🤢","🤮","🤧","😇","🤠","🤡","🤥","🤫","🤭","🧐","🤓","😈","👿","👹","👺","💀","👻","👽","🤖","💩","😺","😸","😹","😻","😼","😽","🙀","😿","😾","🤲","👐","🤝","👍","👎","👊","✊","🤛","🤜","🤞","✌️","🤟","🤘","👈","👉","👆","👇","☝️","✋","🤚","🖐","🖖","👋","🤙","💪","🖕","✍️","🙏","💍","💄","💋","👄","👅","👂","👃","👣","👁","👀","🧠","🗣","👤","👥"],embed:{post:function(e){var t=e+"/embed?";return t+=!(arguments.length>1&&void 0!==arguments[1])||arguments[1]?"caption=true&":"caption=false&",t+=arguments.length>2&&void 0!==arguments[2]&&arguments[2]?"likes=true&":"likes=false&",' diff --git a/resources/assets/components/groups/GroupSettings.vue b/resources/assets/components/groups/GroupSettings.vue index 099d598f2..303269ced 100644 --- a/resources/assets/components/groups/GroupSettings.vue +++ b/resources/assets/components/groups/GroupSettings.vue @@ -228,7 +228,7 @@ Update · - + Delete

@@ -256,7 +256,7 @@ Update · - + Delete

@@ -983,6 +983,30 @@ return `/groups/${this.groupId}/members?a=il&pid=${pid}`; }, + handleDeleteAvatar() { + if(!window.confirm('Are you sure you want to delete your group avatar image?')) { + return; + } + this.savingChanges = true; + axios.post('/api/v0/groups/' + this.group.id + '/settings/delete-avatar') + .then(res => { + this.savingChanges = false; + this.group = res.data; + }); + }, + + handleDeleteHeader() { + if(!window.confirm('Are you sure you want to delete your group header image?')) { + return; + } + this.savingChanges = true; + axios.post('/api/v0/groups/' + this.group.id + '/settings/delete-header') + .then(res => { + this.savingChanges = false; + this.group = res.data; + }); + }, + undoBlock(type, val) { let action = type == 'moderate' ? `unblock ${val}?` : `allow anyone to join without approval from ${val}?`; swal({ diff --git a/resources/assets/components/partials/post/ContextMenu.vue b/resources/assets/components/partials/post/ContextMenu.vue index ad94da335..ef6e28a35 100644 --- a/resources/assets/components/partials/post/ContextMenu.vue +++ b/resources/assets/components/partials/post/ContextMenu.vue @@ -112,6 +112,21 @@ @click.prevent="unarchivePost(status)"> {{ $t('menu.unarchive') }} + + {{ $t('menu.pin') }} + + + + {{ $t('menu.unpin') }} + { + const data = res.data; + if(data.id && data.pinned) { + this.$emit('pinned'); + swal('Pinned', 'Successfully pinned post to your profile', 'success'); + } else { + swal('Error', 'An error occured when attempting to pin', 'error'); + } + }) + .catch(err => { + this.closeModals(); + if(err.response?.data?.error) { + swal('Error', err.response?.data?.error, 'error'); + } + }); + }, + + unpinPost(status) { + if(window.confirm(this.$t('menu.unpinPostConfirm')) == false) { + return; + } + this.closeModals(); + + axios.post('/api/pixelfed/v1/statuses/' + status.id.toString() + '/unpin') + .then(res => { + const data = res.data; + if(data.id) { + this.$emit('unpinned'); + swal('Unpinned', 'Successfully unpinned post from your profile', 'success'); + } else { + swal('Error', data.error, 'error'); + } + }) + .catch(err => { + this.closeModals(); + if(err.response?.data?.error) { + swal('Error', err.response?.data?.error, 'error'); + } else { + window.location.reload() + } + }); + }, } } diff --git a/resources/assets/components/partials/profile/ProfileFeed.vue b/resources/assets/components/partials/profile/ProfileFeed.vue index c6c69efb0..12945ca47 100644 --- a/resources/assets/components/partials/profile/ProfileFeed.vue +++ b/resources/assets/components/partials/profile/ProfileFeed.vue @@ -1,1166 +1,1287 @@ diff --git a/resources/assets/components/partials/profile/ProfileSidebar.vue b/resources/assets/components/partials/profile/ProfileSidebar.vue index 7f263aec1..877b1f0ec 100644 --- a/resources/assets/components/partials/profile/ProfileSidebar.vue +++ b/resources/assets/components/partials/profile/ProfileSidebar.vue @@ -105,9 +105,9 @@

- Follows you - Muted - Blocked + {{ $t("profile.followYou")}} + {{ $t("profile.muted")}} + {{ $t("profile.blocked") }}

@@ -145,7 +145,7 @@ -->
@@ -421,10 +421,10 @@ }, getJoinedDate() { - let d = new Date(this.profile.created_at); - let month = new Intl.DateTimeFormat("en-US", { month: "long" }).format(d); - let year = d.getFullYear(); - return `${month} ${year}`; + return new Date(this.profile.created_at).toLocaleDateString(this.$i18n.locale, { + year: 'numeric', + month: 'long', + }); }, follow() { diff --git a/resources/assets/components/partials/timeline/Notification.vue b/resources/assets/components/partials/timeline/Notification.vue index 8f6a011a4..078717a6a 100644 --- a/resources/assets/components/partials/timeline/Notification.vue +++ b/resources/assets/components/partials/timeline/Notification.vue @@ -7,13 +7,13 @@

- @{{n.account.acct}} {{ $t('notifications.liked') }} post. + @{{n.account.acct}} {{ $t('notifications.liked') }} {{ $t("notifications.post")}}.

- @{{n.account.acct}} {{ $t('notifications.commented') }} post. + @{{n.account.acct}} {{ $t('notifications.commented') }} {{ $t("notifications.post")}}.

@@ -25,7 +25,7 @@

- @{{n.account.acct}} {{ $t('notifications.reacted') }} story. + @{{n.account.acct}} {{ $t('notifications.reacted') }} {{ $t('notifications.story') }}.

@@ -141,30 +141,36 @@ return text.slice(0, limit) + '...' }, - timeAgo(ts) { - let date = Date.parse(ts); - let seconds = Math.floor((new Date() - date) / 1000); - let interval = Math.floor(seconds / 31536000); - if (interval >= 1) { - return interval + "y"; - } - interval = Math.floor(seconds / 604800); - if (interval >= 1) { - return interval + "w"; - } - interval = Math.floor(seconds / 86400); - if (interval >= 1) { - return interval + "d"; - } - interval = Math.floor(seconds / 3600); - if (interval >= 1) { - return interval + "h"; - } - interval = Math.floor(seconds / 60); - if (interval >= 1) { - return interval + "m"; - } - return Math.floor(seconds) + "s"; + timeAgo(ts) { + let date = new Date(ts); + let now = new Date(); + let seconds = Math.floor((now - date) / 1000); + let interval = Math.floor(seconds / 31536000); + if (interval >= 1) { + return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'year'); + } + interval = Math.floor(seconds / 2592000); + if (interval >= 1) { + return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'month'); + } + interval = Math.floor(seconds / 604800); + if (interval >= 1) { + return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'week'); + } + interval = Math.floor(seconds / 86400); + if (interval >= 1) { + return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'day'); + } + interval = Math.floor(seconds / 3600); + if (interval >= 1) { + return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'hour'); + } + interval = Math.floor(seconds / 60); + if (interval >= 1) { + return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-interval, 'minute'); + } + return new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: 'auto' }).format(-seconds, 'second'); + }, mentionUrl(status) { diff --git a/resources/assets/components/sections/Notifications.vue b/resources/assets/components/sections/Notifications.vue index 1ddf522fc..d20d472d8 100644 --- a/resources/assets/components/sections/Notifications.vue +++ b/resources/assets/components/sections/Notifications.vue @@ -3,7 +3,7 @@
- Notifications + {{ $t("notifications.title")}}
@@ -49,27 +49,28 @@ class="mr-2 rounded-circle shadow-sm" :src="n.account.avatar" width="32" + height="32" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">

- Your recent post has been unlisted. + {{ $t("notifications.youRecent")}} {{ $t("notifications.post")}} {{ $t("notifications.hasUnlisted")}}.

Click here for more info. @@ -77,64 +78,64 @@

- {{truncate(n.account.username)}} updated a modlog. + {{truncate(n.account.username)}} {{ $t("notifications.updatedA")}} modlog.

- Your application to join the {{truncate(n.group.name)}} group was approved! + {{ $t("notifications.yourApplication")}} {{truncate(n.group.name)}} {{ $t("notifications.wasApproved")}}

- Your application to join {{truncate(n.group.name)}} was rejected. + {{ $t("notifications.yourApplication")}} {{truncate(n.group.name)}} {{ $t("notifications.wasRejected")}}

@@ -146,11 +147,11 @@

- We cannot display this notification at this time. + {{ $t("notifications.cannotDisplay")}}

-
{{timeAgo(n.created_at)}}
+
{{timeAgo(n.created_at)}}
diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index cafd785a2..09f86dabd 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -1,3 +1,5 @@ +import VueI18n from 'vue-i18n'; + require('./polyfill'); window._ = require('lodash'); window.Popper = require('popper.js').default; @@ -19,7 +21,7 @@ if (token) { window.App = window.App || {}; window.App.redirect = function() { - document.querySelectorAll('a').forEach(function(i,k) { + document.querySelectorAll('a').forEach(function(i,k) { let a = i.getAttribute('href'); if(a && a.length > 5 && a.startsWith('https://')) { let url = new URL(a); @@ -31,7 +33,23 @@ window.App.redirect = function() { } window.App.boot = function() { - new Vue({ el: '#content'}); + Vue.use(VueI18n); + + let i18nMessages = { + en: require('./i18n/en.json'), + pt: require('./i18n/pt.json'), + }; + let locale = document.querySelector('html').getAttribute('lang'); + + const i18n = new VueI18n({ + locale: locale, // set locale + fallbackLocale: 'en', + messages: i18nMessages +}); + new Vue({ + el: '#content', + i18n, + }); } window.addEventListener("load", () => { @@ -67,8 +85,8 @@ window.App.util = { console.log('Unsupported method.'); }), }, - time: (function() { - return new Date; + time: (function() { + return new Date; }), version: 1, format: { @@ -78,40 +96,36 @@ window.App.util = { } return new Intl.NumberFormat(locale, { notation: notation , compactDisplay: "short" }).format(count); }), - timeAgo: function(ts) { - const date = new Date(ts); - const now = new Date(); - - const seconds = Math.floor((now - date) / 1000); - - const secondsInYear = 60 * 60 * 24 * 365.25; - let interval = Math.floor(seconds / secondsInYear); + timeAgo: (function(ts) { + let date = new Date(ts); + let now = new Date(); + let seconds = Math.floor((now - date) / 1000); + let interval = Math.floor(seconds / 31536000); if (interval >= 1) { - return interval + "y"; + return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'year'); } - - interval = Math.floor(seconds / (60 * 60 * 24 * 7)); + interval = Math.floor(seconds / 2592000); if (interval >= 1) { - return interval + "w"; + return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'month'); } - - interval = Math.floor(seconds / (60 * 60 * 24)); + interval = Math.floor(seconds / 604800); if (interval >= 1) { - return interval + "d"; + return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'week'); } - - interval = Math.floor(seconds / (60 * 60)); + interval = Math.floor(seconds / 86400); if (interval >= 1) { - return interval + "h"; + return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'day'); + } + interval = Math.floor(seconds / 3600); + if (interval >= 1) { + return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'hour'); } - interval = Math.floor(seconds / 60); if (interval >= 1) { - return interval + "m"; + return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-interval, 'minute'); } - - return Math.floor(seconds) + "s"; - }, + return new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'short' }).format(-seconds, 'second'); + }), timeAhead: (function(ts, short = true) { let date = Date.parse(ts); let diff = date - Date.parse(new Date()); @@ -154,9 +168,9 @@ window.App.util = { tag = '/i/redirect?url=' + encodeURIComponent(tag); } - return tag; + return tag; }) - }, + }, filters: [ ['1984','filter-1977'], ['Azen','filter-aden'], diff --git a/resources/assets/js/components/Profile.vue b/resources/assets/js/components/Profile.vue index 991d4ab62..36c2a3dac 100644 --- a/resources/assets/js/components/Profile.vue +++ b/resources/assets/js/components/Profile.vue @@ -16,7 +16,7 @@
-

You are blocking this account

+

{{ $t("profile.blocking")}}

Click here to view profile

@@ -49,7 +49,7 @@

{{formatCount(profile.statuses_count)}}

-

Posts

+

{{ $t("profile.posts")}}

@@ -57,7 +57,7 @@ @@ -65,7 +65,7 @@ @@ -86,7 +86,7 @@

@@ -106,7 +106,7 @@ - Edit Profile + {{ $t("profile.editProfile") }} @@ -117,19 +117,19 @@
{{formatCount(profile.statuses_count)}} - Posts + {{ $t("profile.posts")}}
@@ -141,11 +141,11 @@

{{formatWebsite(profile.website)}}

- Admin + {{ $t("profile.admin") }} - Follows You + {{ $t("profile.followYou") }} - Joined {{joinedAtFormat(profile.created_at)}} + {{$t("profile.joined")}} {{joinedAtFormat(profile.created_at)}}

@@ -156,7 +156,7 @@

- +